Jelajahi Sumber

Merge branch 'master' of https://github.com/o2oa/o2oa

roo00 6 tahun lalu
induk
melakukan
1c25af3374
100 mengubah file dengan 7726 tambahan dan 513 penghapusan
  1. 109 3
      o2android/README.md
  2. 3 12
      o2android/app/build.gradle
  3. TEMPAT SAMPAH
      o2android/app/libs/o2_auth_sdk-release.aar
  4. 3 1
      o2android/app/src/main/AndroidManifest.xml
  5. 36 15
      o2android/app/src/main/java/jiguang/chat/activity/ChatDetailActivity.java
  6. 17 5
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPActivity.kt
  7. 17 17
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/clouddrive/CloudDriveActivity.kt
  8. 6 4
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/view/CMSWebViewActivity.kt
  9. 17 13
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/meeting/apply/MeetingApplyActivity.kt
  10. 16 13
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/meeting/edit/MeetingEditActivity.kt
  11. 2 2
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/DownloadAPKFragment.kt
  12. 5 4
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/IndexPortalFragment.kt
  13. 1 1
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/NewContactFragment.kt
  14. 80 49
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/NewContactPresenter.kt
  15. 20 4
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/NewsFragment.kt
  16. 2 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/SettingsFragment.kt
  17. 16 3
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/openim/IMPersonConfigActivity.kt
  18. 15 21
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/openim/IMTribeCreateActivity.kt
  19. 27 31
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/openim/IMTribeInfoActivity.kt
  20. 25 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPersonGroupActivityContract.kt
  21. 69 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPersonGroupActivityPresenter.kt
  22. 200 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPersonGroupPicker.kt
  23. 338 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPickerActivity.kt
  24. 18 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPickerActivityContract.kt
  25. 13 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPickerActivityPresenter.kt
  26. 353 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactUnitAndIdentityPicker.kt
  27. 24 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactUnitAndIdentityPickerContract.kt
  28. 147 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactUnitAndIdentityPickerPresenter.kt
  29. 11 11
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/NewOrganizationActivity.kt
  30. 53 19
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/NewOrganizationPresenter.kt
  31. 4 3
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/process/StartProcessStepTwoFragment.kt
  32. 41 4
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/scanlogin/ScanLoginActivity.kt
  33. 243 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/webview/JSInterfaceO2mBiz.kt
  34. 3 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/webview/TaskWebViewActivity.kt
  35. 54 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/core/component/adapter/ContactComplexPickerListAdapter.kt
  36. 44 1
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/model/vo/O2JsPostMessage.kt
  37. 2 1
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/utils/StringUtil.java
  38. 20 0
      o2android/app/src/main/res/layout/activity_contact_picker.xml
  39. 20 0
      o2android/app/src/main/res/layout/fragment_person_group_picker.xml
  40. 59 0
      o2android/app/src/main/res/layout/fragment_unit_identity_picker.xml
  41. 69 0
      o2android/app/src/main/res/layout/item_contact_complex_picker_identity.xml
  42. 73 0
      o2android/app/src/main/res/layout/item_contact_complex_picker_org.xml
  43. 2 2
      o2android/gradle.properties
  44. 64 0
      o2ios/O2Platform.xcodeproj/project.pbxproj
  45. 497 37
      o2ios/O2Platform/Contact-通讯录/Contacts_new.storyboard
  46. 150 0
      o2ios/O2Platform/Contact-通讯录/Controller/ContactGroupPickerViewController.swift
  47. 225 0
      o2ios/O2Platform/Contact-通讯录/Controller/ContactIdentityPickerViewController.swift
  48. 176 0
      o2ios/O2Platform/Contact-通讯录/Controller/ContactPersonPickerViewController.swift
  49. 428 0
      o2ios/O2Platform/Contact-通讯录/Controller/ContactPickerViewController.swift
  50. 207 0
      o2ios/O2Platform/Contact-通讯录/Controller/ContactUnitPickerViewController.swift
  51. 25 18
      o2ios/O2Platform/Contact-通讯录/Model/OOContactModel.swift
  52. 44 0
      o2ios/O2Platform/Contact-通讯录/View/GroupPickerTableViewCell.swift
  53. 45 0
      o2ios/O2Platform/Contact-通讯录/View/PersonPickerTableViewCell.swift
  54. 70 0
      o2ios/O2Platform/Contact-通讯录/View/UnitBreadcrumbViewCell.swift
  55. 101 0
      o2ios/O2Platform/Contact-通讯录/View/UnitPickerTableViewCell.swift
  56. 223 0
      o2ios/O2Platform/Contact-通讯录/ViewModel/ContactPickerViewModel.swift
  57. 2 2
      o2ios/O2Platform/Info.plist
  58. 4 2
      o2ios/O2Platform/VoiceAI-语音处理/Controller/OOVoiceAIController.swift
  59. 2 2
      o2ios/O2Platform/VoiceAI-语音处理/View/OOCircleRippleView.swift
  60. 9 4
      o2ios/O2Platform/bbs/v/BBSForumCell.swift
  61. 3 0
      o2ios/O2Platform/common/AppDelegate.swift
  62. 3 0
      o2ios/O2Platform/common/BaseWebViewUIViewController.swift
  63. 31 7
      o2ios/O2Platform/common/HTTP/ContactAPI/OOContactAPI.swift
  64. 70 0
      o2ios/O2Platform/common/HTTP/ContactAPI/OOContactExpressAPI.swift
  65. 1 0
      o2ios/O2Platform/common/o2JsApi/O2BaseJsMessageHandler.swift
  66. 212 0
      o2ios/O2Platform/common/o2JsApi/O2JsApiBizUtil.swift
  67. 8 0
      o2ios/O2Platform/common/o2JsApi/O2JsApiNotification.swift
  68. 11 0
      o2ios/O2Platform/common/o2JsApi/O2JsApiUtil.swift
  69. 1 0
      o2ios/O2Platform/config/O2URLContext.swift
  70. 1 1
      o2ios/O2Platform/contacts/c/ContactCompanyDeptController.swift
  71. 21 6
      o2ios/O2Platform/contacts/c/ContactDeptPersonController.swift
  72. 93 35
      o2ios/O2Platform/contacts/c/ContactHomeViewController.swift
  73. 37 7
      o2ios/O2Platform/contacts/v/ContactItemCell.swift
  74. 757 0
      o2ios/O2Platform/framework/MagicBytesMimeType/MimeType.swift
  75. 68 0
      o2ios/O2Platform/framework/MagicBytesMimeType/Swime.swift
  76. 83 41
      o2ios/O2Platform/scan/c/NewScanViewController.swift
  77. 3 0
      o2ios/O2Platform/setting/c/SettingViewController.swift
  78. 14 15
      o2ios/O2Platform/storyboard/contacts.storyboard
  79. 26 1
      o2ios/O2Platform/task/c/TodoTaskDetailViewController.swift
  80. 55 0
      o2ios/O2Platform/task/m/O2WebViewModels.swift
  81. 1168 0
      o2web/source/o2_core/init.js
  82. 28 87
      o2web/source/o2_core/o2.js
  83. 11 9
      o2web/source/o2_core/o2/o2.core.js
  84. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/bg.png
  85. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/buttonbg.png
  86. 472 0
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/css.wcss
  87. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/check.png
  88. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/check_gray.png
  89. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/closeOffice.png
  90. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/closeOffice_gray.png
  91. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config.png
  92. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config_gray.png
  93. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config_single.png
  94. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config_single_over.png
  95. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/createFolder.png
  96. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/createFolder_gray.png
  97. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete.png
  98. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete_gray.png
  99. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete_single.png
  100. TEMPAT SAMPAH
      o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete_single_over.png

+ 109 - 3
o2android/README.md

@@ -1,6 +1,112 @@
-#O2办公平台(O2OA)
+#O2OA Android
 
 
-[O2办公平台(O2OA)](https://www.pgyer.com/ZhiHe_android)是一个可以自定义的私有化高效云工作平台,私有安全,创意无限
+O2平台Android端应用。
 
-test
+[![Build Status](https://travis-ci.com/huqi1980/o2oa_client_web.svg?branch=master)](https://travis-ci.org/o2oa/o2oa)
+[![AGPL](https://img.shields.io/badge/license-AGPL-blue.svg)](https://github.com/o2oa/o2oa)
+[![code-size](https://img.shields.io/github/languages/code-size/o2oa/o2oa.svg)](https://github.com/o2oa/o2oa)
+[![last-commit](https://img.shields.io/github/last-commit/o2oa/o2oa.svg)](https://github.com/o2oa/o2oa)
+---
+
+## 简介
+
+O2平台Android客户端,最低支持Android版本4.4 [**Android KitKat**]。
+
+## 导入编译
+
+请使用最新版本的`Android Studio`进行导入编译,编译的Android SDK版本是 26 [**Android O**] 。
+
+#### SDK环境安装
+
+安装Android Studio完成后,打开设置里面的SDK Manager工具。
+
+![](http://img.muliba.net/post/20190106112546.png)
+
+选择Android 8.0 ,安装SDK。然后选择SDK Tools 选项卡,
+
+![](http://img.muliba.net/post/20190106112529.png)
+
+勾选右下角的Show Package Details,然后选择Android SDK Build-Tools 下面的27.0.3版本进行安装。
+
+#### 应用配置信息修改
+
+导入项目后会生成 `local.properties` ,需要在这个文件中添加如下内容:
+
+```properties
+# 打包证书相关信息
+signingConfig.keyAlias=别名
+signingConfig.keyPassword=密码
+signingConfig.storeFilePath=证书所在路径
+signingConfig.storePassword=证书密码
+
+# 下面是一些第三方SDK的key
+# DEBUG版本的key
+JPUSH_APPKEY_DEBUG=极光推送AppKey
+PGY_APP_ID_DEBUG=蒲公英AppId
+BAIDU_APPID_DEBUG=Baidu地图AppId
+BAIDU_SECRET_DEBUG=Baidu地图Secret
+BAIDU_APPKEY_DEBUG=百度地图Appkey
+# RELEASE版本的key
+JPUSH_APPKEY_RELEASE=极光推送AppKey
+PGY_APP_ID_RELEASE=蒲公英AppId
+BAIDU_APPID_RELEASE=Baidu地图AppId
+BAIDU_SECRET_RELEASE=Baidu地图Secret
+BAIDU_APPKEY_RELEASE=百度地图Appkey
+# 腾讯Bugly AppId
+BUGLY_APPID=腾讯Bugly AppId
+
+JM_IM_USER_PASSWORD=极光IM的用户默认密码
+```
+
+
+
+## 替换应用logo图标、应用名称
+
+Logo图标分两块,第一块是App的桌面图标,第二块是App内部看到的一些O2OA的图标。
+
+### App的桌面图标
+
+这个图标需要编译打包的时候打包进去的,在 `./app/src/main/res`目录下的`mipmap`目录下:
+
+|                                                              |                                                    |
+| :----------------------------------------------------------: | :------------------------------------------------: |
+| ![http://img.muliba.net/post/20190105171759.png](http://img.muliba.net/post/20190105171759.png) | ![](http://img.muliba.net/post/20190105171908.png) |
+
+把四个目录中的`logo.png`和`logo_round.png`都替换了。
+
+### App内部的一些O2OA的图标
+
+App内看到的一些O2OA相关的logo图标,可以不编译打包进App,我们服务端可以进行动态配置。用管理员进入我们O2OA的服务端,找到系统设置->移动办公配置->样式配置,就可以修改图标了:
+
+![](http://img.muliba.net/post/20190105172349.png)
+
+
+
+### 应用名称
+
+应用桌面显示的名称也是编译打包前要修改好的,在strings资源文件中修改就行了:
+
+路径:`./app/src/main/res/values/strings.xml`
+
+![](http://img.muliba.net/post/20190105173144.png)
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 官方网站:
+
+官方网站 : [http://www.o2oa.net](http://www.o2oa.net)
+
+oschina项目主页 : [https://www.oschina.net/p/o2oa](https://www.oschina.net/p/o2oa)
+
+下载地址 : [http://www.o2oa.net](http://www.o2oa.net/download.html)

+ 3 - 12
o2android/app/build.gradle

@@ -23,7 +23,6 @@ ext {
 
 
 def loadProperties() {
-    // load properties
     Properties properties = new Properties()
     properties.load(project.rootProject.file('local.properties').newDataInputStream())
 
@@ -42,7 +41,6 @@ def loadProperties() {
 
     project.jpushIMPassword = properties.getProperty("JM_IM_USER_PASSWORD")
 
-    //bugly
     project.buglyAppId = properties.getProperty("BUGLY_APPID")
 
 
@@ -95,7 +93,6 @@ android {
         ndk {
             //选择要添加的对应cpu类型的.so库。
             abiFilters 'armeabi', 'armeabi-v7a'
-            // 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
         }
         multiDexKeepProguard file('multidex_keep_file.pro')
         vectorDrawables.useSupportLibrary = true
@@ -165,7 +162,6 @@ android {
         checkReleaseBuilds false
         abortOnError false
     }
-    //All flavors must now belong to a named flavor dimension. Learn more at https://d.android.com/r/tools/flavorDimensions-missing-error-message.html
     flavorDimensions "type"
     productFlavors {
         O2PLATFORM {
@@ -217,7 +213,6 @@ dependencies {
     implementation(name: 'image_picker-release', ext: 'aar')
     implementation(name: 'path_provider-release', ext: 'aar')
 
-    //kotlin
     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
     implementation "org.jetbrains.anko:anko-common:$anko_version"
 
@@ -248,7 +243,6 @@ dependencies {
     implementation 'com.yanzhenjie:recyclerview-swipe:1.1.4'
     implementation 'com.race604.waveloading:library:1.1.1'
 
-    //http
     implementation 'com.squareup.retrofit2:retrofit:2.4.0'
     implementation 'com.squareup.retrofit2:converter-gson:2.2.0'
     implementation 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
@@ -266,8 +260,6 @@ dependencies {
     // 此处以JMessage 2.5.0 版本为例。
     implementation 'cn.jiguang.sdk:jcore:1.1.9'
 
-    // 此处以JCore 1.1.9 版本为例。
-
     //im
     implementation 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
     implementation 'com.jakewharton:butterknife:8.4.0'
@@ -280,6 +272,9 @@ dependencies {
     //滚动选择器
     implementation 'com.jzxiang.pickerview:TimePickerDialog:1.0.1'
 
+    //activity result
+    implementation 'com.github.lwugang:ActivityResult:59b23e3682'
+
     //google architecture component
     def lifecycle_version = "1.1.1"
 
@@ -294,7 +289,6 @@ dependencies {
     // alternatively - just LiveData
     implementation "android.arch.lifecycle:livedata:$lifecycle_version"
 
-    // alternatively - Lifecycles only (no ViewModel or LiveData).
 
     //     Support library depends on this lightweight import
     implementation "android.arch.lifecycle:runtime:$lifecycle_version"
@@ -303,12 +297,9 @@ dependencies {
     //noinspection GradleDependency
     kapt "com.android.databinding:compiler:$gradle_version"
 
-    //test
     testImplementation 'junit:junit:4.12'
     implementation 'com.google.code.gson:gson:2.8.5'
 
-    //activity result
-    implementation 'com.github.lwugang:ActivityResult:59b23e3682'
 
 }
 

TEMPAT SAMPAH
o2android/app/libs/o2_auth_sdk-release.aar


+ 3 - 1
o2android/app/src/main/AndroidManifest.xml

@@ -41,8 +41,10 @@
         android:label="@string/app_name"
         android:roundIcon="@mipmap/logo_round"
         android:theme="@style/XBPMTheme.NoActionBar">
+        <activity android:name=".app.o2.organization.ContactPickerActivity"></activity>
         <activity android:name=".app.cms.application.CMSPublishDocumentActivity" />
-        <activity android:name=".app.o2.webview.LocalImageViewActivity"
+        <activity
+            android:name=".app.o2.webview.LocalImageViewActivity"
             android:screenOrientation="portrait"
             android:theme="@style/XBPMTheme.fullscreen" />
         <activity android:name=".app.o2.security.DeviceManagerActivity" />

+ 36 - 15
o2android/app/src/main/java/jiguang/chat/activity/ChatDetailActivity.java

@@ -12,9 +12,12 @@ import android.text.TextUtils;
 import android.view.WindowManager;
 import android.view.inputmethod.InputMethodManager;
 
+import com.wugang.activityresult.library.ActivityResult;
+
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R;
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity;
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog;
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity;
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.ContactPickerResult;
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.ContactPickerResultItem;
 
 import java.io.File;
 import java.lang.ref.WeakReference;
@@ -48,7 +51,6 @@ public class ChatDetailActivity extends BaseActivity {
     public final static String START_FOR_WHICH = "which";
     private final static int GROUP_NAME_REQUEST_CODE = 1;
     private final static int MY_NAME_REQUEST_CODE = 2;
-    private static final int ADD_FRIEND_REQUEST_CODE = 3;
 
     public static final int GROUP_DESC = 70;
     public static final int FLAGS_GROUP_DESC = 71;
@@ -180,14 +182,6 @@ public class ChatDetailActivity extends BaseActivity {
                 case JGApplication.REQUEST_CODE_ALL_MEMBER:
                     mChatDetailController.refreshMemberList();
                     break;
-                //单聊添加人进群
-                case ADD_FRIEND_REQUEST_CODE:
-                    ArrayList<String> list = data.getStringArrayListExtra(NewOrganizationActivity.Companion.getMULTI_PERSON_CHOOSE_RESULT());
-                    XLog.info("list:"+list.size());
-                    if (null != list && list.size() != 0) {
-                        mChatDetailController.addMembersToGroup(list);
-                    }
-                    break;
                 case 4://修改群头像
                     String path = data.getStringExtra("groupAvatarPath");
                     if (path != null) {
@@ -224,10 +218,37 @@ public class ChatDetailActivity extends BaseActivity {
      * 从ContactsActivity中选择朋友加入到群组中
      */
     public void showContacts(Long group) {
-        Intent intent = new Intent(this, NewOrganizationActivity.class);
-        Bundle bundle = NewOrganizationActivity.Companion.startBundleDataForIMChoose(new ArrayList<String>(), NewOrganizationActivity.Companion.getIM_CHOOSE_FROM_REQUEST());
-        intent.putExtras(bundle);
-        startActivityForResult(intent, ADD_FRIEND_REQUEST_CODE);
+        ArrayList<String> modes = new ArrayList<>();
+        modes.add("personPicker");
+        Bundle bundle1 = ContactPickerActivity.Companion.startPickerBundle(modes,
+                new ArrayList<>(),
+                "",
+                0,
+                true,
+                new ArrayList<>(),
+                new ArrayList<>(),
+                new ArrayList<>(),
+                new ArrayList<>(),
+                new ArrayList<>());
+        ActivityResult.of(this)
+                .className(ContactPickerActivity.class)
+                .params(bundle1)
+                .greenChannel().forResult((resultCode, data) -> {
+                    if (data != null) {
+                        ContactPickerResult result = data.getParcelableExtra(ContactPickerActivity.CONTACT_PICKED_RESULT);
+                        if (result != null) {
+                            ArrayList<ContactPickerResultItem> users = result.getUsers();
+                            if (users.size() != 0) {
+                                ArrayList<String> list = new ArrayList<>();
+                                for (int i = 0; i < users.size(); i++) {
+                                    list.add(users.get(i).getDistinguishedName());
+                                }
+                                mChatDetailController.addMembersToGroup(list);
+                            }
+                        }
+                    }
+                });
+
     }
 
 

+ 17 - 5
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPActivity.kt

@@ -1,17 +1,15 @@
 package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
 
 import android.content.Context
-import android.graphics.Color
-import android.os.Build
 import android.os.Bundle
 import android.support.v7.app.AppCompatActivity
 import android.support.v7.widget.Toolbar
 import android.widget.TextView
-import com.readystatesoftware.systembartint.SystemBarTintManager
-import net.muliba.changeskin.FancySkinManager
+import com.wugang.activityresult.library.ActivityResult
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.ContactPickerResult
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.ImmersedStatusBarUtils
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.LoadingDialog
 
 
@@ -102,4 +100,18 @@ abstract class BaseMVPActivity<in V: BaseView, T: BasePresenter<V>>: AppCompatAc
     }
 
 
+    fun contactPicker(bundle: Bundle, callback: (ContactPickerResult?)-> Unit) {
+        ActivityResult.of(this)
+                .className(ContactPickerActivity::class.java)
+                .params(bundle)
+                .greenChannel().forResult { _, data ->
+                    val result = data?.getParcelableExtra<ContactPickerResult>(ContactPickerActivity.CONTACT_PICKED_RESULT)
+                    if (result != null) {
+                        callback(result)
+                    }else {
+                        callback(null)
+                    }
+                }
+    }
+
 }

+ 17 - 17
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/clouddrive/CloudDriveActivity.kt

@@ -16,7 +16,7 @@ import net.muliba.changeskin.FancySkinManager
 import net.muliba.fancyfilepickerlibrary.FilePicker
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonFragmentPagerAdapter
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.enums.FileOperateType
@@ -24,7 +24,6 @@ import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.AndroidUtils
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.FileExtensionHelper
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.goWithRequestCode
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
 import org.jetbrains.anko.doAsync
@@ -43,7 +42,6 @@ class CloudDriveActivity : BaseMVPActivity<CloudDriveContract.View, CloudDriveCo
 
     companion object {
         val YUNPAN_UPLOAD_FILE_REQUEST_CODE = 1
-        val YUNPAN_CHOOSE_PERSON_REQUEST_CODE = 2
         val YUNPAN_FROM_SHARE = "YUNPAN_FROM_SHARE"
         val YUNPAN_FROM_SEND = "YUNPAN_FROM_SEND"
 
@@ -142,19 +140,7 @@ class CloudDriveActivity : BaseMVPActivity<CloudDriveContract.View, CloudDriveCo
                     XLog.debug( "uri path:$filePath")
                     (fragmentList[0] as CloudDriveMyFileFragment).menuUploadFile(filePath)
                 }
-                YUNPAN_CHOOSE_PERSON_REQUEST_CODE -> {
-                    //选人
-                    val array = data?.getStringArrayListExtra(NewOrganizationActivity.MULTI_PERSON_CHOOSE_RESULT) ?: ArrayList<String>()
-                    XLog.debug("$array")
-                    if (back.equals(YUNPAN_FROM_SHARE)){
-                        (fragmentList[0] as CloudDriveMyFileFragment).menuSendResult(array, FileOperateType.SHARE)
-                    }else if (back.equals(YUNPAN_FROM_SEND)){
-                        (fragmentList[0] as CloudDriveMyFileFragment).menuSendResult(array, FileOperateType.SEND)
-                    }else {
-                        XLog.error( "error back , back:"+back)
-                    }
 
-                }
             }
         }
         super.onActivityResult(requestCode, resultCode, data)
@@ -225,8 +211,22 @@ class CloudDriveActivity : BaseMVPActivity<CloudDriveContract.View, CloudDriveCo
     fun menuShareOrSend(from: String) {
         back = from
         if (!TextUtils.isEmpty(back)) {
-            goWithRequestCode<NewOrganizationActivity>(NewOrganizationActivity.startBundleData(mode = NewOrganizationActivity.MULTI_PERSON_CHOOSE_MODE),
-                    YUNPAN_CHOOSE_PERSON_REQUEST_CODE)
+            val bundle = ContactPickerActivity.startPickerBundle(
+                    arrayListOf("personPicker"),
+                    multiple = true)
+            contactPicker(bundle) { result ->
+                if (result != null) {
+                    val users = ArrayList<String>()
+                    result.users.forEach {
+                        users.add(it.distinguishedName)
+                    }
+                    when (back) {
+                        YUNPAN_FROM_SHARE -> (fragmentList[0] as CloudDriveMyFileFragment).menuSendResult(users, FileOperateType.SHARE)
+                        YUNPAN_FROM_SEND -> (fragmentList[0] as CloudDriveMyFileFragment).menuSendResult(users, FileOperateType.SEND)
+                        else -> XLog.error("error back , back:$back")
+                    }
+                }
+            }
         }
     }
 

+ 6 - 4
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/view/CMSWebViewActivity.kt

@@ -25,10 +25,7 @@ import net.muliba.fancyfilepickerlibrary.PicturePicker
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.DownloadDocument
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.JSInterfaceO2mNotification
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.JSInterfaceO2mUtil
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.LocalImageViewActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.*
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.tbs.FileReaderActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
@@ -79,6 +76,8 @@ class CMSWebViewActivity : BaseMVPActivity<CMSWebViewContract.View, CMSWebViewCo
     private val webChromeClient: WebChromeClientWithProgressAndValueCallback by lazy { WebChromeClientWithProgressAndValueCallback.with(this) }
     private val jsNotification: JSInterfaceO2mNotification by lazy { JSInterfaceO2mNotification.with(this) }
     private val jsUtil: JSInterfaceO2mUtil by lazy { JSInterfaceO2mUtil.with(this) }
+    private val jsBiz: JSInterfaceO2mBiz by lazy { JSInterfaceO2mBiz.with(this) }
+
     private val downloadDocument: DownloadDocument by lazy { DownloadDocument(this) }
     private val cameraImageUri: Uri by lazy { FileUtil.getUriFromFile(this, File(FileExtensionHelper.getCameraCacheFilePath())) }
     //上传附件
@@ -107,8 +106,11 @@ class CMSWebViewActivity : BaseMVPActivity<CMSWebViewContract.View, CMSWebViewCo
         web_view_cms_document_content.addJavascriptInterface(this, "o2android")
         jsNotification.setupWebView(web_view_cms_document_content)
         jsUtil.setupWebView(web_view_cms_document_content)
+        jsBiz.setupWebView(web_view_cms_document_content)
+
         web_view_cms_document_content.addJavascriptInterface(jsNotification, JSInterfaceO2mNotification.JSInterfaceName)
         web_view_cms_document_content.addJavascriptInterface(jsUtil, JSInterfaceO2mUtil.JSInterfaceName)
+        web_view_cms_document_content.addJavascriptInterface(jsBiz, JSInterfaceO2mBiz.JSInterfaceName)
         web_view_cms_document_content.webChromeClient = webChromeClient
         web_view_cms_document_content.webViewClient = object : WebViewClient() {
             override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {

+ 17 - 13
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/meeting/apply/MeetingApplyActivity.kt

@@ -19,7 +19,7 @@ import net.muliba.fancyfilepickerlibrary.FilePicker
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.meeting.room.MeetingRoomChooseActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
@@ -33,7 +33,7 @@ import java.util.*
 
 
 class MeetingApplyActivity : BaseMVPActivity<MeetingApplyContract.View, MeetingApplyContract.Presenter>(),
-        MeetingApplyContract.View, View.OnClickListener,com.borax12.materialdaterangepicker.time.TimePickerDialog.OnTimeSetListener{
+        MeetingApplyContract.View, View.OnClickListener, TimePickerDialog.OnTimeSetListener{
 
     override var mPresenter: MeetingApplyContract.Presenter = MeetingApplyPresenter()
     override fun layoutResId(): Int = R.layout.activity_meeting_create_form
@@ -47,7 +47,6 @@ class MeetingApplyActivity : BaseMVPActivity<MeetingApplyContract.View, MeetingA
     private var roomId: String = ""
 
     companion object {
-        val MEETING_CHOOSE_INVITE_PERSON = 1000
         val MEETING_CHOOSE_ROOM = 1001
         val MEETING_FILE_CODE = 1003
     }
@@ -81,7 +80,18 @@ class MeetingApplyActivity : BaseMVPActivity<MeetingApplyContract.View, MeetingA
 
         invitePersonAdapter.setOnItemClickListener { _, position ->
             when (position) {
-                invitePersonList.size - 1 -> goWithRequestCode<NewOrganizationActivity>(NewOrganizationActivity.startBundleData(mode = NewOrganizationActivity.MULTI_PERSON_CHOOSE_MODE), MEETING_CHOOSE_INVITE_PERSON)
+                invitePersonList.size - 1 -> {
+                    val bundle = ContactPickerActivity.startPickerBundle(
+                            arrayListOf("personPicker"),
+                            multiple = true)
+                    contactPicker(bundle) { result ->
+                        if (result != null) {
+                            val users = result.users.map { it.distinguishedName }
+                            XLog.debug("choose invite person, list:$users,")
+                            chooseInvitePersonCallback(users)
+                        }
+                    }
+                }
                 else -> {
                     invitePersonList.removeAt(position)
                     invitePersonAdapter.notifyDataSetChanged()
@@ -208,13 +218,7 @@ class MeetingApplyActivity : BaseMVPActivity<MeetingApplyContract.View, MeetingA
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         if (resultCode == Activity.RESULT_OK) {
             when (requestCode) {
-                MEETING_CHOOSE_INVITE_PERSON -> {
-                    val result = data?.getStringArrayListExtra(NewOrganizationActivity.MULTI_PERSON_CHOOSE_RESULT) ?: ArrayList<String>()
-                    XLog.debug("choose invite person, list:$result,")
-                    if (!result.isEmpty()) {
-                        chooseInvitePersonCallback(result)
-                    }
-                }
+
                 MEETING_CHOOSE_ROOM -> {
                     val resultRoomName = data?.getStringExtra(MeetingRoomChooseActivity.RESULT_ROOM_NAME_KEY) ?: ""
                     val resultRoomId = data?.getStringExtra(MeetingRoomChooseActivity.RESULT_ROOM_ID_KEY) ?: ""
@@ -347,10 +351,10 @@ class MeetingApplyActivity : BaseMVPActivity<MeetingApplyContract.View, MeetingA
         }
     }
 
-    private fun chooseInvitePersonCallback(result: ArrayList<String>) {
+    private fun chooseInvitePersonCallback(result: List<String>) {
         val allList = ArrayList<String>()
         invitePersonList.remove(invitePersonAdd)
-        if (!invitePersonList.isEmpty()) {
+        if (invitePersonList.isNotEmpty()) {
             allList.addAll(invitePersonList)
         }
         allList.addAll(result)

+ 16 - 13
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/meeting/edit/MeetingEditActivity.kt

@@ -17,7 +17,7 @@ import kotlinx.android.synthetic.main.content_meeting_edit_form.*
 import net.muliba.fancyfilepickerlibrary.FilePicker
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
@@ -26,7 +26,6 @@ import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.meeting.MeetingInfoJso
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.FileExtensionHelper
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.goWithRequestCode
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleImageView
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
@@ -47,7 +46,6 @@ class MeetingEditActivity : BaseMVPActivity<MeetingEditContract.View, MeetingEdi
     companion object {
         val MEETING_INFO_KEY = "xbpm.meeting.edit.info"
         val MEETING_INFO_ROOM_NAME_KEY = "xbpm.meeting.edit.room.name"
-        val MEETING_CHOOSE_INVITE_PERSON = 1000
         val MEETING_FILE_CODE = 1003
 
         fun startBundleData(info: MeetingInfoJson, roomName:String): Bundle {
@@ -94,7 +92,18 @@ class MeetingEditActivity : BaseMVPActivity<MeetingEditContract.View, MeetingEdi
         invitePersonList.add(invitePersonAdd)
         invitePersonAdapter.setOnItemClickListener { _, position ->
             when(position) {
-                invitePersonList.size-1 -> goWithRequestCode<NewOrganizationActivity>(NewOrganizationActivity.startBundleData(mode = NewOrganizationActivity.MULTI_PERSON_CHOOSE_MODE), MEETING_CHOOSE_INVITE_PERSON)
+                invitePersonList.size-1 -> {
+                    val bundle = ContactPickerActivity.startPickerBundle(
+                            arrayListOf("personPicker"),
+                            multiple = true)
+                    contactPicker(bundle) { result ->
+                        if (result != null) {
+                            val users = result.users.map { it.distinguishedName }
+                            XLog.debug("choose invite person, list:$users,")
+                            chooseInvitePersonCallback(users)
+                        }
+                    }
+                }
                 else -> {
                     invitePersonList.removeAt(position)
                     invitePersonAdapter.notifyDataSetChanged()
@@ -140,13 +149,7 @@ class MeetingEditActivity : BaseMVPActivity<MeetingEditContract.View, MeetingEdi
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         if (resultCode == Activity.RESULT_OK) {
             when(requestCode){
-                MEETING_CHOOSE_INVITE_PERSON -> {
-                    val result = data?.getStringArrayListExtra(NewOrganizationActivity.MULTI_PERSON_CHOOSE_RESULT) ?: ArrayList<String>()
-                    XLog.debug("choose invite person, list:$result,")
-                    if (!result.isEmpty()) {
-                        chooseInvitePersonCallback(result)
-                    }
-                }
+
                 MEETING_FILE_CODE -> {
                     val result = data?.getStringExtra(FilePicker.FANCY_FILE_PICKER_SINGLE_RESULT_KEY)
                     if (!TextUtils.isEmpty(result)) {
@@ -191,10 +194,10 @@ class MeetingEditActivity : BaseMVPActivity<MeetingEditContract.View, MeetingEdi
         hideLoadingDialog()
     }
 
-    private fun chooseInvitePersonCallback(result: java.util.ArrayList<String>) {
+    private fun chooseInvitePersonCallback(result: List<String>) {
         val allList = ArrayList<String>()
         invitePersonList.remove(invitePersonAdd)
-        if (!invitePersonList.isEmpty()){
+        if (invitePersonList.isNotEmpty()){
             allList.addAll(invitePersonList)
         }
         allList.addAll(result)

+ 2 - 2
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/DownloadAPKFragment.kt

@@ -33,8 +33,8 @@ class DownloadAPKFragment : DialogFragment()  {
     override fun onStart() {
         super.onStart()
         val window = dialog.window
-        window.setGravity(Gravity.CENTER)
-        window.setWindowAnimations(R.style.DialogEmptyAnimation)//取消过渡动画 , 使DialogSearch的出现更加平滑
+        window?.setGravity(Gravity.CENTER)
+        window?.setWindowAnimations(R.style.DialogEmptyAnimation)//取消过渡动画 , 使DialogSearch的出现更加平滑
     }
 
     override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? =

+ 5 - 4
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/IndexPortalFragment.kt

@@ -21,10 +21,7 @@ import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.process.ReadCompletedListAct
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.process.ReadListActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.process.TaskCompletedListActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.process.TaskListActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.JSInterfaceO2mNotification
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.JSInterfaceO2mUtil
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.PortalWebViewActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.TaskWebViewActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview.*
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSApplicationInfoJson
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSCategoryInfoJson
@@ -63,6 +60,7 @@ class IndexPortalFragment : BaseMVPViewPagerFragment<IndexPortalContract.View, I
             by lazy { WebChromeClientWithProgressAndValueCallback.with(this) }
     private val jsNotification: JSInterfaceO2mNotification by lazy { JSInterfaceO2mNotification.with(this) }
     private val jsUtil: JSInterfaceO2mUtil by lazy { JSInterfaceO2mUtil.with(this) }
+    private val jsBiz: JSInterfaceO2mBiz by lazy { JSInterfaceO2mBiz.with(this) }
 
     private var portalId: String = ""
     private var portalUrl: String = ""
@@ -77,11 +75,14 @@ class IndexPortalFragment : BaseMVPViewPagerFragment<IndexPortalContract.View, I
             web_view_portal_content.addJavascriptInterface(this, "o2android") //注册js对象
             jsNotification.setupWebView(web_view_portal_content)
             jsUtil.setupWebView(web_view_portal_content)
+            jsBiz.setupWebView(web_view_portal_content)
             web_view_portal_content.addJavascriptInterface(
                     jsNotification,
                     JSInterfaceO2mNotification.JSInterfaceName
+
             )
             web_view_portal_content.addJavascriptInterface(jsUtil, JSInterfaceO2mUtil.JSInterfaceName)
+            web_view_portal_content.addJavascriptInterface(jsBiz, JSInterfaceO2mBiz.JSInterfaceName)
             web_view_portal_content.webViewSetCookie(activity, portalUrl)
             web_view_portal_content.webChromeClient = webChromeClient
             web_view_portal_content.webViewClient = object : WebViewClient() {

+ 1 - 1
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/NewContactFragment.kt

@@ -66,7 +66,7 @@ class NewContactFragment : BaseMVPViewPagerFragment<NewContactContract.View, New
                 val icon = holder?.getView<CircleImageView>(R.id.image_item_contact_fragment_body_collect_icon)
                 val url = APIAddressHelper.instance().getPersonAvatarUrlWithId(collect.personId)
                 if (icon!=null) {
-                    if (collect.gender.equals("男")) {
+                    if (collect.gender == "男") {
                         O2ImageLoaderManager.instance()
                                 .showImage(icon, url, O2ImageLoaderOptions(placeHolder = R.mipmap.icon_avatar_men))
 

+ 80 - 49
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/NewContactPresenter.kt

@@ -6,6 +6,9 @@ import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.realm.RealmDataService
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.ApiResponse
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.IdentityLevelForm
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.unit.UnitJson
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactFragmentVO
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
@@ -16,64 +19,92 @@ import rx.schedulers.Schedulers
 class NewContactPresenter : BasePresenterImpl<NewContactContract.View>(), NewContactContract.Presenter {
 
     override fun loadNewContact() {
-            val service = getOrganizationAssembleControlApi(mView?.getContext())
-            val identityListObservable = service?.identityListWithPerson(O2SDKManager.instance().distinguishedName)?.map { response ->
-                val identityList = ArrayList<NewContactFragmentVO>()
-                val list = response.data
-                if (list != null && !list.isEmpty()) {
-                    val header = NewContactFragmentVO.GroupHeader("我的部门", R.mipmap.icon_contact_my_company)
-                    identityList.add(header)
-                    list.filter { !TextUtils.isEmpty(it.unit) }.map {
-                        identityList.add(it.copyToVO())
-                    }
+        val service = getOrganizationAssembleControlApi(mView?.getContext())
+        val personService = getAssemblePersonalApi(mView?.getContext())
+        val expressService = getAssembleExpressApi(mView?.getContext())
+        val identityListObservable = service?.identityListWithPerson(O2SDKManager.instance().distinguishedName)?.map { response ->
+            val identityList = ArrayList<NewContactFragmentVO>()
+            val list = response.data
+            if (list != null && list.isNotEmpty()) {
+                val header = NewContactFragmentVO.GroupHeader("我的部门", R.mipmap.icon_contact_my_company)
+                identityList.add(header)
+                list.filter { !TextUtils.isEmpty(it.unit) }.map {
+                    identityList.add(it.copyToVO())
                 }
-                identityList
             }
+            identityList
+        }
 
-            val topUnitListObservable = service?.unitListTop()?.map { response ->
-                val topList = ArrayList<NewContactFragmentVO>()
-                val list = response.data
-                if (list != null && !list.isEmpty()) {
-                    val header = NewContactFragmentVO.GroupHeader("组织结构", R.mipmap.icon_contact_my_department)
-                    topList.add(header)
-                    list.map {
-                        topList.add(it.copyToVO())
-                    }
+        val topUnitListObservable = personService?.getCurrentPersonInfo()?.flatMap { response ->
+            val person = response.data
+            if (person != null) {
+                val identityList = person.woIdentityList
+                if (identityList.isNotEmpty()) {
+                    val identity = identityList[0]
+                    val form = IdentityLevelForm(identity = identity.distinguishedName, level = 1)
+                    expressService?.unitByIdentityAndLevel(form)
+                } else {
+                    Observable.just(ApiResponse<UnitJson>())
                 }
-                topList
+            } else {
+                Observable.just(ApiResponse<UnitJson>())
             }
-            val usuallyObservable = RealmDataService().loadUsuallyPersonByOwner(O2SDKManager.instance().distinguishedName).map { usuallyPersons ->
-                val usList = ArrayList<NewContactFragmentVO>()
-                if (!usuallyPersons.isEmpty()) {
-                    usList.add(NewContactFragmentVO.GroupHeader("常用联系人", R.mipmap.icon_contact_my_collect))
-                    usuallyPersons.map {
-                        usList.add(NewContactFragmentVO.MyCollect(it.person ?: "",
-                                it.personDisplay ?: "",
-                                it.gender ?: "",
-                                it.mobile ?: ""))
-                    }
+        }?.map { response ->
+            val topList = ArrayList<NewContactFragmentVO>()
+            val unit = response.data
+            if (unit != null) {
+                val header = NewContactFragmentVO.GroupHeader("组织结构", R.mipmap.icon_contact_my_department)
+                topList.add(header)
+                val u = unit.copyToVO() as NewContactFragmentVO.MyDepartment
+                u.hasChildren = true
+                topList.add(u)
+            }
+            topList
+        }
+//            val topUnitListObservable = service?.unitListTop()?.map { response ->
+//                val topList = ArrayList<NewContactFragmentVO>()
+//                val list = response.data
+//                if (list != null && !list.isEmpty()) {
+//                    val header = NewContactFragmentVO.GroupHeader("组织结构", R.mipmap.icon_contact_my_department)
+//                    topList.add(header)
+//                    list.map {
+//                        topList.add(it.copyToVO())
+//                    }
+//                }
+//                topList
+//            }
+        val usuallyObservable = RealmDataService().loadUsuallyPersonByOwner(O2SDKManager.instance().distinguishedName).map { usuallyPersons ->
+            val usList = ArrayList<NewContactFragmentVO>()
+            if (usuallyPersons.isNotEmpty()) {
+                usList.add(NewContactFragmentVO.GroupHeader("常用联系人", R.mipmap.icon_contact_my_collect))
+                usuallyPersons.map {
+                    usList.add(NewContactFragmentVO.MyCollect(it.person ?: "",
+                            it.personDisplay ?: "",
+                            it.gender ?: "",
+                            it.mobile ?: ""))
                 }
-                usList
             }
+            usList
+        }
 
-            Observable.zip(identityListObservable, topUnitListObservable, usuallyObservable, { t1, t2, t3 ->
-                val list = ArrayList<NewContactFragmentVO>()
-                list.addAll(t1)
-                list.addAll(t2)
-                list.addAll(t3)
-                list
-            }).subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .o2Subscribe {
-                        onNext { list ->
-                            mView?.loadContact(list)
-                        }
-                        onError { e, _ ->
-                            XLog.error("", e)
-                            mView?.loadContactFail()
-                        }
+        Observable.zip(identityListObservable, topUnitListObservable, usuallyObservable) { t1, t2, t3 ->
+            val list = ArrayList<NewContactFragmentVO>()
+            list.addAll(t1)
+            list.addAll(t2)
+            list.addAll(t3)
+            list
+        }.subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext { list ->
+                        mView?.loadContact(list)
                     }
-        }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        mView?.loadContactFail()
+                    }
+                }
+    }
 
 
 }

+ 20 - 4
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/NewsFragment.kt

@@ -25,12 +25,12 @@ import cn.jpush.im.android.api.model.GroupInfo
 import cn.jpush.im.android.api.model.UserInfo
 import cn.jpush.im.android.eventbus.EventBus
 import jiguang.chat.activity.ChatActivity
-import jiguang.chat.utils.DialogCreator
 import kotlinx.android.synthetic.main.fragment_main_news.*
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.openim.IMTribeCreateActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.im.*
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
@@ -122,7 +122,10 @@ class NewsFragment : BaseMVPViewPagerFragment<NewsContract.View, NewsContract.Pr
         mListAdapter = ConversationListAdapter(activity, mDatas, mConvListView)
         conv_list_view.adapter = mListAdapter
         XLog.info("init news fragment ui finish..........................")
-
+        tv_conversation_log_error.setOnClickListener {
+            XLog.info("click reload 。。。。。。。。。。。。。。")
+            lazyLoad()
+        }
         if (O2App.instance._JMIsLogin()) {
             isLogin = true
             failInitIM(false)
@@ -211,7 +214,20 @@ class NewsFragment : BaseMVPViewPagerFragment<NewsContract.View, NewsContract.Pr
                 XLog.info("创建群聊。。。。。。。。。。。。。。。。。。。。。。。。。。。")
                 if (isLogin) {
                     try {
-                        activity.go<NewOrganizationActivity>(NewOrganizationActivity.startBundleDataForIMChoose(arrayListOf(JMessageClient.getMyInfo().userName)))
+                        val bundle = ContactPickerActivity.startPickerBundle(
+                                arrayListOf("personPicker"),
+                                multiple = true,
+                                initUserList = arrayListOf(JMessageClient.getMyInfo().userName)
+                        )
+                        (activity as MainActivity).contactPicker(bundle) { result ->
+                            if (result != null) {
+                                val list = ArrayList<String>()
+                                val users = result.users
+                                users.map { list.add(it.distinguishedName) }
+                                activity.go<IMTribeCreateActivity>(IMTribeCreateActivity.startCreate(list))
+                            }
+                        }
+
                     } catch (e: Exception) {
                     }
                 } else {

+ 2 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/SettingsFragment.kt

@@ -11,6 +11,7 @@ import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.about.AboutActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.login.LoginActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.notice.NoticeSettingActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.security.AccountSecurityActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.skin.SkinManagerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.BitmapUtil
@@ -20,6 +21,7 @@ import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.*
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.permission.PermissionRequester
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.AndroidShareDialog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.BottomSheetMenu
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2AlertIconEnum
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
 

+ 16 - 3
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/openim/IMPersonConfigActivity.kt

@@ -3,11 +3,10 @@ package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.openim
 import android.os.Bundle
 import android.text.TextUtils
 import kotlinx.android.synthetic.main.activity_im_person_config.*
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.person.PersonActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.person.PersonJson
@@ -15,6 +14,7 @@ import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.goThenKill
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderOptions
+import java.util.*
 
 class IMPersonConfigActivity : BaseMVPActivity<IMPersonConfigContract.View, IMPersonConfigContract.Presenter>(), IMPersonConfigContract.View {
     override var mPresenter: IMPersonConfigContract.Presenter = IMPersonConfigActivityPresenter()
@@ -49,7 +49,20 @@ class IMPersonConfigActivity : BaseMVPActivity<IMPersonConfigContract.View, IMPe
         }
         rl_im_person_tribe_create_btn.setOnClickListener {
             val personList = arrayListOf(O2SDKManager.instance().cId, personId)
-            goThenKill<NewOrganizationActivity>(NewOrganizationActivity.startBundleDataForIMChoose(personList))
+            val bundle = ContactPickerActivity.startPickerBundle(
+                    arrayListOf("personPicker"),
+                    multiple = true,
+                    initUserList = personList
+            )
+            contactPicker(bundle) { result ->
+                if (result != null) {
+                    val list = ArrayList<String>()
+                    val users = result.users
+                    users.map { list.add(it.distinguishedName) }
+                    goThenKill<IMTribeCreateActivity>(IMTribeCreateActivity.startCreate(list))
+                }
+            }
+
         }
         showLoadingDialog()
         mPresenter.loadPersonInfo(personId)

+ 15 - 21
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/openim/IMTribeCreateActivity.kt

@@ -1,6 +1,5 @@
 package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.openim
 
-import android.app.Activity
 import android.content.Intent
 import android.os.Bundle
 import android.support.v7.widget.GridLayoutManager
@@ -18,17 +17,15 @@ import jiguang.chat.application.JGApplication
 import jiguang.chat.entity.Event
 import jiguang.chat.entity.EventType
 import kotlinx.android.synthetic.main.activity_im_tribe_create.*
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.goWithRequestCode
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleImageView
 
@@ -49,7 +46,6 @@ class IMTribeCreateActivity : BaseMVPActivity<IMTribeCreateContract.View, IMTrib
     }
 
     private val invitePersonAdd = "添加"
-    private val choosePersonRequestCode = 1024
 
     private var personList = ArrayList<String>()
 
@@ -67,7 +63,20 @@ class IMTribeCreateActivity : BaseMVPActivity<IMTribeCreateContract.View, IMTrib
                     personList.filter { it != invitePersonAdd }.map {
                         nowList.add(it)
                     }
-                    goWithRequestCode<NewOrganizationActivity>(NewOrganizationActivity.startBundleDataForIMChoose(nowList, NewOrganizationActivity.IM_CHOOSE_FROM_REQUEST), choosePersonRequestCode)
+                    val bundle = ContactPickerActivity.startPickerBundle(
+                            arrayListOf("personPicker"),
+                            multiple = true,
+                            initUserList = nowList
+                    )
+                    contactPicker(bundle) { result ->
+                        if (result != null) {
+                            val users = result.users
+                            personList.clear()
+                            personList.add(invitePersonAdd)
+                            users.map { personList.add(it.distinguishedName)  }
+                            personAdapter.notifyDataSetChanged()
+                        }
+                    }
                 }
                 else -> {
                     if (personList[position] != O2SDKManager.instance().cId) {
@@ -156,21 +165,6 @@ class IMTribeCreateActivity : BaseMVPActivity<IMTribeCreateContract.View, IMTrib
     }
 
 
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        if (resultCode == Activity.RESULT_OK) {
-            when (requestCode) {
-                choosePersonRequestCode -> {
-                    val result = data?.extras?.getStringArrayList(NewOrganizationActivity.MULTI_PERSON_CHOOSE_RESULT) ?: ArrayList()
-                    personList.clear()
-                    personList.add(invitePersonAdd)
-                    personList.addAll(result)
-                    personAdapter.notifyDataSetChanged()
-                }
-            }
-        }
-        super.onActivityResult(requestCode, resultCode, data)
-    }
-
     private val personAdapter: CommonRecycleViewAdapter<String> by lazy {
         object : CommonRecycleViewAdapter<String>(this, personList, R.layout.item_person_avatar_name) {
             override fun convert(holder: CommonRecyclerViewHolder?, t: String?) {

+ 27 - 31
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/openim/IMTribeInfoActivity.kt

@@ -1,7 +1,5 @@
 package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.openim
 
-import android.app.Activity
-import android.content.Intent
 import android.os.Bundle
 import android.support.v7.widget.GridLayoutManager
 import android.text.TextUtils
@@ -12,13 +10,12 @@ import android.widget.TextView
 import kotlinx.android.synthetic.main.activity_im_tribe_info.*
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.NewOrganizationActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.goWithRequestCode
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleImageView
@@ -43,7 +40,6 @@ class IMTribeInfoActivity : BaseMVPActivity<IMTribeInfoContract.View, IMTribeInf
     }
 
     private val invitePersonAdd = "添加"
-    private val choosePersonRequestCode = 1024
     private val personList = ArrayList<String>()
     private var tribeId: Long = DEFAULT_ID
     override fun afterSetContentView(savedInstanceState: Bundle?) {
@@ -63,7 +59,31 @@ class IMTribeInfoActivity : BaseMVPActivity<IMTribeInfoContract.View, IMTribeInf
                 personList.filter { it != invitePersonAdd }.map {
                     nowList.add(it)
                 }
-                goWithRequestCode<NewOrganizationActivity>(NewOrganizationActivity.startBundleDataForIMChoose(nowList, NewOrganizationActivity.IM_CHOOSE_FROM_REQUEST), choosePersonRequestCode)
+                val bundle = ContactPickerActivity.startPickerBundle(
+                        arrayListOf("personPicker"),
+                        multiple = true,
+                        initUserList = nowList
+                )
+                contactPicker(bundle) { result ->
+                    if (result != null) {
+                        val users = result.users
+                        val addList = ArrayList<String>()
+                        for (rId in users) {
+                            var isOld = false
+                            personList.map {
+                                if (rId.distinguishedName == it) {
+                                    isOld = true
+                                    return@map
+                                }
+                            }
+                            if (!isOld){
+                                addList.add(rId.distinguishedName)
+                            }
+                        }
+                        //add
+                        addNewMembers(addList)
+                    }
+                }
             }
         }
         loadTribeInfo()
@@ -139,31 +159,7 @@ class IMTribeInfoActivity : BaseMVPActivity<IMTribeInfoContract.View, IMTribeInf
     }
 
 
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        if (resultCode == Activity.RESULT_OK) {
-            when (requestCode) {
-                choosePersonRequestCode -> {
-                    val result = data?.extras?.getStringArrayList(NewOrganizationActivity.MULTI_PERSON_CHOOSE_RESULT) ?: ArrayList()
-                    val addList = ArrayList<String>()
-                    for (rId in result) {
-                        var isOld = false
-                        personList.map {
-                            if (rId == it) {
-                                isOld = true
-                                return@map
-                            }
-                        }
-                        if (!isOld){
-                            addList.add(rId)
-                        }
-                    }
-                    //add
-                    addNewMembers(addList)
-                }
-            }
-        }
-        super.onActivityResult(requestCode, resultCode, data)
-    }
+
 
     private fun addNewMembers(addList: ArrayList<String>) {
         if (addList.isEmpty()) {

+ 25 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPersonGroupActivityContract.kt

@@ -0,0 +1,25 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+
+/**
+ * Created by fancyLou on 2019-08-09.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+
+object ContactPersonGroupActivityContract {
+    interface View : BaseView {
+        fun callbackResult(list: List<NewContactListVO>)
+        fun backError(error:String)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        /**
+         * @param mode 查询模式 group还是person
+         */
+        fun findListByPage(mode: String, lastId:String)
+    }
+}

+ 69 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPersonGroupActivityPresenter.kt

@@ -0,0 +1,69 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+/**
+ * Created by fancyLou on 2019-08-09.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+
+class ContactPersonGroupActivityPresenter: BasePresenterImpl<ContactPersonGroupActivityContract.View>(), ContactPersonGroupActivityContract.Presenter {
+    override fun findListByPage(mode: String, lastId: String) {
+        val service =  getOrganizationAssembleControlApi(mView?.getContext())
+        if (service == null) {
+            mView?.backError("组织模块异常")
+            return
+        }
+        if (mode == ContactPersonGroupPicker.GROUP_PICK_MODE) {
+            service.groupListByPage(lastId, O2.DEFAULT_PAGE_NUMBER).subscribeOn(Schedulers.io())
+                    .flatMap { response ->
+                        val retList = ArrayList<NewContactListVO>()
+                        val list = response.data
+                        if (list != null && list.isNotEmpty()) {
+                            list.map { retList.add(it.copy2NewContactListVO()) }
+                        }
+                        Observable.just(retList)
+                    }
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext {
+                            mView?.callbackResult(it)
+                        }
+                        onError { e, isNetworkError ->
+                            XLog.error("$isNetworkError ", e)
+                            mView?.backError("查询数据异常")
+                        }
+                    }
+
+        }else {
+            service.personListByPage(lastId, O2.DEFAULT_PAGE_NUMBER).subscribeOn(Schedulers.io())
+                    .flatMap { response ->
+                        val retList = ArrayList<NewContactListVO>()
+                        val list = response.data
+                        if (list != null && list.isNotEmpty()) {
+                            list.map { retList.add(it.copy2NewContactListVO()) }
+                        }
+                        Observable.just(retList)
+                    }
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext {
+                            mView?.callbackResult(it)
+                        }
+                        onError { e, isNetworkError ->
+                            XLog.error("$isNetworkError ", e)
+                            mView?.backError("查询数据异常")
+                        }
+                    }
+        }
+    }
+
+}

+ 200 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPersonGroupPicker.kt

@@ -0,0 +1,200 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.text.TextUtils
+import android.widget.CheckBox
+import kotlinx.android.synthetic.main.fragment_person_group_picker.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.SwipeRefreshCommonRecyclerViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderOptions
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleImageView
+import org.jetbrains.anko.dip
+
+/**
+ * Created by fancyLou on 2019-08-21.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+class ContactPersonGroupPicker : BaseMVPFragment<ContactPersonGroupActivityContract.View, ContactPersonGroupActivityContract.Presenter>(), ContactPersonGroupActivityContract.View {
+    override var mPresenter: ContactPersonGroupActivityContract.Presenter = ContactPersonGroupActivityPresenter()
+
+    override fun layoutResId(): Int = R.layout.fragment_person_group_picker
+
+    companion object {
+        const val GROUP_PICK_MODE = "0" //群组选择
+        const val PERSON_PICK_MODE = "1" //人员选择
+
+        const val PICK_MODE_KEY = "PICK_MODE_KEY"//选择模式的key
+        const val multiple_KEY = "multiple_KEY"
+        const val MAX_NUMBER_KEY = "MAX_NUMBER_KEY"
+
+        /**
+         * @param pickMode 选择模式 GROUP_PICK_MODE群组选择、PERSON_PICK_MODE人员选择
+         */
+        fun startPicker(pickMode: String,
+                              multiple:Boolean = false,
+                              maxNumber: Int = 0
+        ): ContactPersonGroupPicker {
+            val picker = ContactPersonGroupPicker()
+            val bundle = Bundle()
+            bundle.putString(PICK_MODE_KEY, pickMode)
+            bundle.putBoolean(multiple_KEY, multiple)
+            bundle.putInt(MAX_NUMBER_KEY, maxNumber)
+            picker.arguments = bundle
+            return picker
+        }
+    }
+
+    private var pickMode = GROUP_PICK_MODE
+    private var multiple = true
+    private var maxNumber = 0
+    private val itemList: ArrayList<NewContactListVO> = ArrayList()
+
+    private val adapter: SwipeRefreshCommonRecyclerViewAdapter<NewContactListVO> by lazy {
+        object : SwipeRefreshCommonRecyclerViewAdapter<NewContactListVO>(activity, itemList, R.layout.item_contact_complex_picker_identity) {
+            override fun convert(holder: CommonRecyclerViewHolder?, data: NewContactListVO?) {
+                if (data!=null) {
+                    if (data is NewContactListVO.Group) {
+                        holder?.setText(R.id.tv_item_contact_complex_picker_identity_name, data.name)
+                                ?.setImageViewResource(R.id.image_item_contact_complex_picker_identity_icon, R.mipmap.icon_avatar_tribe_40)
+                        val checkBox = holder?.getView<CheckBox>(R.id.check_item_contact_complex_picker_identity_select)
+                        checkBox?.isChecked = false
+                        checkBox?.setOnClickListener {
+                            val check = checkBox.isChecked
+                            toggleCheck(data, check)
+                        }
+                        checkBox?.isChecked = (activity as ContactPickerActivity).isSelectedValue(data)
+                    }else if (data is NewContactListVO.Person) {
+                        holder?.setText(R.id.tv_item_contact_complex_picker_identity_name, data.name)
+                        val icon = holder?.getView<CircleImageView>(R.id.image_item_contact_complex_picker_identity_icon)
+                        if (icon != null) {
+                            val url = APIAddressHelper.instance().getPersonAvatarUrlWithId(data.distinguishedName)
+                            O2ImageLoaderManager.instance().showImage(icon, url, O2ImageLoaderOptions(placeHolder = R.mipmap.icon_avatar_men))
+                        }
+                        val checkBox = holder?.getView<CheckBox>(R.id.check_item_contact_complex_picker_identity_select)
+                        checkBox?.isChecked = false
+                        checkBox?.setOnClickListener {
+                            val check = checkBox.isChecked
+                            toggleCheck(data, check)
+                        }
+                        checkBox?.isChecked = (activity as ContactPickerActivity).isSelectedValue(data)
+                    }
+                }
+            }
+        }
+    }
+
+    private var isRefresh = false
+    private var isLoading = false
+    private var lastId = ""
+
+    override fun initUI() {
+        ///初始化传入参数
+        pickMode = arguments?.getString(PICK_MODE_KEY) ?: GROUP_PICK_MODE
+        multiple = arguments?.getBoolean(multiple_KEY) ?: true
+        maxNumber = arguments?.getInt(MAX_NUMBER_KEY) ?: 0
+
+        swipe_refresh_contact_person_group_picker_main.touchSlop = activity.dip(70f)
+        swipe_refresh_contact_person_group_picker_main.setColorSchemeResources(R.color.z_color_refresh_scuba_blue,
+                R.color.z_color_refresh_red, R.color.z_color_refresh_purple, R.color.z_color_refresh_orange)
+        swipe_refresh_contact_person_group_picker_main.recyclerViewPageNumber = O2.DEFAULT_PAGE_NUMBER
+        swipe_refresh_contact_person_group_picker_main.setOnRefreshListener{
+            if (!isLoading && !isRefresh) {
+                getDatas(true)
+                isRefresh = true
+            }
+        }
+        swipe_refresh_contact_person_group_picker_main.setOnLoadMoreListener {
+            if (!isLoading && !isRefresh) {
+                if (TextUtils.isEmpty(lastId)) {
+                    getDatas(true)
+                } else {
+                    getDatas(false)
+                }
+                isLoading = true
+            }
+        }
+
+        rv_contact_person_group_picker_main.adapter = adapter
+        rv_contact_person_group_picker_main.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
+        adapter.setOnItemClickListener { view, position ->
+            val checkBox = view.findViewById<CheckBox>(R.id.check_item_contact_complex_picker_identity_select)
+            val isCheck = checkBox.isChecked
+            checkBox.isChecked = !isCheck
+            val item = itemList[position]
+            if (item is NewContactListVO.Group) {
+                toggleCheck(item, !isCheck)
+            }else if (item is NewContactListVO.Person) {
+                toggleCheck(item, !isCheck)
+            }
+        }
+        //初始化加载数据
+        isRefresh = true
+        getDatas(true)
+    }
+
+    override fun callbackResult(list: List<NewContactListVO>) {
+        if (isRefresh) {
+            itemList.clear()
+        }
+        itemList.addAll(list)
+        if (list.isNotEmpty()) {
+            val item = list[list.size-1]
+            if (item is NewContactListVO.Person) {
+                lastId = item.id
+            }else if (item is NewContactListVO.Group) {
+                lastId = item.id
+            }
+        }
+        adapter.notifyDataSetChanged()
+        finishAnimation()
+    }
+
+    override fun backError(error: String) {
+        XToast.toastShort(activity, "获取任务列表失败")
+        itemList.clear()
+        adapter.notifyDataSetChanged()
+        finishAnimation()
+    }
+
+
+
+    private fun toggleCheck(v: NewContactListVO, check: Boolean) {
+        XLog.debug("click toggleCheckIdentity, $check")
+        if (check) {
+            (activity as ContactPickerActivity).addSelectedValue(v)
+        } else {
+            (activity as ContactPickerActivity).removeSelectedValue(v)
+        }
+        adapter.notifyDataSetChanged()
+    }
+
+    private fun finishAnimation() {
+        if (isRefresh) {
+            swipe_refresh_contact_person_group_picker_main.isRefreshing = false
+            isRefresh = false
+        }
+        if (isLoading) {
+            swipe_refresh_contact_person_group_picker_main.setLoading(false)
+            isLoading = false
+        }
+    }
+
+    //加载数据
+    private fun getDatas(flag: Boolean) {
+        if (flag) {
+            mPresenter.findListByPage(pickMode, O2.FIRST_PAGE_TAG)
+        }else {
+            mPresenter.findListByPage(pickMode, lastId)
+        }
+    }
+}

+ 338 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPickerActivity.kt

@@ -0,0 +1,338 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import android.app.Activity
+import android.os.Bundle
+import android.support.design.widget.TabLayout
+import android.support.v4.app.Fragment
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MenuItem
+import kotlinx.android.synthetic.main.snippet_appbarlayout_tablayout_toolbar.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.ContactPickerResult
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.ContactPickerResultItem
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.replaceFragmentSafely
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+
+class ContactPickerActivity : BaseMVPActivity<ContactPickerActivityContract.View, ContactPickerActivityContract.Presenter>(), ContactPickerActivityContract.View {
+    override var mPresenter: ContactPickerActivityContract.Presenter = ContactPickerActivityPresenter()
+
+
+    override fun layoutResId(): Int = R.layout.activity_contact_picker
+
+    companion object {
+        const val CONTACT_PICKED_RESULT = "CONTACT_PICKED_RESULT"
+        val picker_mode_list = arrayOf("departmentPicker", "identityPicker", "groupPicker", "personPicker")
+        const val PICKER_MODE_KEY = "PICKER_MODE_KEY"
+        const val TOP_LIST_KEY = "TOP_LIST_KEY"
+        const val ORG_TYPE_KEY = "ORG_TYPE_KEY"
+        const val MULIPLE_KEY = "multiple_KEY"
+        const val MAX_NUMBER_KEY = "MAX_NUMBER_KEY"
+        const val DUTY_KEY = "DUTY_KEY"
+        const val PICKED_DEPT_ARRAY_KEY = "PICKED_DEPT_ARRAY_KEY"
+        const val PICKED_ID_ARRAY_KEY = "PICKED_ID_ARRAY_KEY"
+        const val PICKED_GROUP_ARRAY_KEY = "PICKED_GROUP_ARRAY_KEY"
+        const val PICKED_USER_ARRAY_KEY = "PICKED_USER_ARRAY_KEY"
+
+        fun startPickerBundle(
+                pickerModes: ArrayList<String>,
+                topUnitList: ArrayList<String> = arrayListOf(),
+                unitType: String = "",
+                maxNumber: Int = 0,
+                multiple: Boolean = true,
+                dutyList: ArrayList<String> = arrayListOf(),
+                initDeptList: ArrayList<String> = arrayListOf(),
+                initIdList: ArrayList<String> = arrayListOf(),
+                initGroupList: ArrayList<String> = arrayListOf(),
+                initUserList: ArrayList<String> = arrayListOf()
+        ): Bundle {
+            val bundle = Bundle()
+            bundle.putStringArrayList(PICKER_MODE_KEY, pickerModes)
+            bundle.putStringArrayList(TOP_LIST_KEY, topUnitList)
+            bundle.putString(ORG_TYPE_KEY, unitType)
+            bundle.putInt(MAX_NUMBER_KEY, maxNumber)
+            bundle.putBoolean(MULIPLE_KEY, multiple)
+            bundle.putStringArrayList(DUTY_KEY, dutyList)
+            bundle.putStringArrayList(PICKED_DEPT_ARRAY_KEY, initDeptList)
+            bundle.putStringArrayList(PICKED_ID_ARRAY_KEY, initIdList)
+            bundle.putStringArrayList(PICKED_GROUP_ARRAY_KEY, initGroupList)
+            bundle.putStringArrayList(PICKED_USER_ARRAY_KEY, initUserList)
+            return bundle
+        }
+    }
+    private var pickerModes: ArrayList<String> = arrayListOf()
+    private var multiple = true//是否多选
+    private var maxNumber = 0//当multiple为true的时候,最多可选择的数量
+    private var topList: ArrayList<String> = ArrayList()//可选的顶级组织列表
+    private var orgType = ""//可选择的组织类别
+    private var duty: ArrayList<String> = ArrayList()//人员职责
+
+
+    private val mSelectDepartments: ArrayList<ContactPickerResultItem> = arrayListOf()
+    private val mSelectIdentities: ArrayList<ContactPickerResultItem> = arrayListOf()
+    private val mSelectGroups: ArrayList<ContactPickerResultItem> = arrayListOf()
+    private val mSelectUsers: ArrayList<ContactPickerResultItem> = arrayListOf()
+
+    private val fragments = ArrayList<Fragment>()
+    private var currentSelect = 0
+    private var pickerTitle = "选择器"
+
+
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        pickerModes = intent.extras?.getStringArrayList(PICKER_MODE_KEY) ?: arrayListOf()
+        if (pickerModes.isEmpty()) {
+            pickerModes.addAll( picker_mode_list.toList() )
+        }
+        //初始化传入参数
+        //是否多选
+        multiple = intent.extras?.getBoolean(MULIPLE_KEY) ?: true
+        //组织类型
+        orgType = intent.extras?.getString(ORG_TYPE_KEY) ?: ""
+        //最多可选
+        maxNumber = intent.extras?.getInt(MAX_NUMBER_KEY) ?: 0
+        if (!multiple) {
+            maxNumber = 1
+        }
+        //人员职责
+        duty = intent.extras?.getStringArrayList(DUTY_KEY) ?: ArrayList()
+        //顶层组织
+        topList = intent.extras?.getStringArrayList(TOP_LIST_KEY) ?: ArrayList()
+        val initDeptList: ArrayList<String> = intent.extras?.getStringArrayList(PICKED_DEPT_ARRAY_KEY) ?: ArrayList()
+        initDeptList.forEach {
+            val name = if (it.contains("@")) {
+                it.split("@")[0]
+            }else {
+                it
+            }
+            mSelectDepartments.add(ContactPickerResultItem(name, it))
+        }
+        val initIdList: ArrayList<String> = intent.extras?.getStringArrayList(PICKED_ID_ARRAY_KEY) ?: ArrayList()
+        initIdList.forEach {
+            val name = if (it.contains("@")) {
+                it.split("@")[0]
+            }else {
+                it
+            }
+            mSelectIdentities.add(ContactPickerResultItem(name, it))
+        }
+        val initGroupList: ArrayList<String> = intent.extras?.getStringArrayList(PICKED_GROUP_ARRAY_KEY) ?: ArrayList()
+        initGroupList.forEach {
+            val name = if (it.contains("@")) {
+                it.split("@")[0]
+            }else {
+                it
+            }
+            mSelectGroups.add(ContactPickerResultItem(name, it))
+        }
+        val initUserList: ArrayList<String> = intent.extras?.getStringArrayList(PICKED_USER_ARRAY_KEY) ?: ArrayList()
+        initUserList.forEach {
+            val name = if (it.contains("@")) {
+                it.split("@")[0]
+            }else {
+                it
+            }
+            mSelectUsers.add(ContactPickerResultItem(name, it))
+        }
+
+        initView()
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+        menuInflater.inflate(R.menu.menu_organization_check, menu)
+        return super.onCreateOptionsMenu(menu)
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+        val count = mSelectDepartments.size + mSelectIdentities.size + mSelectGroups.size + mSelectUsers.size
+        if (maxNumber > 0) {
+            menu?.findItem(R.id.org_menu_choose)?.title = getString(R.string.menu_choose)+ "($count / $maxNumber)"
+        }else {
+            menu?.findItem(R.id.org_menu_choose)?.title = getString(R.string.menu_choose)+ "($count)"
+        }
+        return super.onPrepareOptionsMenu(menu)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+        when(item?.itemId) {
+            R.id.org_menu_choose -> {
+                val count = mSelectDepartments.size + mSelectIdentities.size + mSelectGroups.size + mSelectUsers.size
+                XLog.debug("选择了$count")
+                if (count < 1) {
+                    XToast.toastShort(this, "请至少选择一条数据!")
+                    return true
+                }
+                val result = ContactPickerResult(mSelectDepartments, mSelectIdentities, mSelectGroups, mSelectUsers)
+                intent.putExtra(CONTACT_PICKED_RESULT, result)
+                setResult(Activity.RESULT_OK, intent)
+                finish()
+            }
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            var isOperate = true
+            if (currentSelect == 0) {
+                isOperate = (fragments[0] as ContactUnitAndIdentityPicker).clickBackBtn()
+            }
+            return if (!isOperate) {
+                finish()
+                true
+            }else {
+                true
+            }
+        }
+        return super.onKeyDown(keyCode, event)
+    }
+
+
+    // 检查值是否已经包含在选中的列表中
+    fun isSelectedValue(value: NewContactListVO) : Boolean {
+        return when (value) {
+            is NewContactListVO.Department -> mSelectDepartments.any {
+                it.distinguishedName == value.distinguishedName
+            }
+            is NewContactListVO.Identity -> mSelectIdentities.any {
+                it.distinguishedName == value.distinguishedName
+            }
+            is NewContactListVO.Group -> mSelectGroups.any {
+                it.distinguishedName == value.distinguishedName
+            }
+            is NewContactListVO.Person -> mSelectUsers.any {
+                it.distinguishedName == value.distinguishedName
+            }
+            else -> false
+        }
+    }
+    // 删除一个选中的值
+    fun removeSelectedValue(value: NewContactListVO) {
+        when(value) {
+            is NewContactListVO.Department -> mSelectDepartments.remove(ContactPickerResultItem(value.name, value.distinguishedName))
+            is NewContactListVO.Identity -> mSelectIdentities.remove(ContactPickerResultItem(value.name, value.distinguishedName))
+            is NewContactListVO.Group -> mSelectGroups.remove(ContactPickerResultItem(value.name, value.distinguishedName))
+            is NewContactListVO.Person -> mSelectUsers.remove(ContactPickerResultItem(value.name, value.distinguishedName))
+        }
+        refreshMenu()
+    }
+    // 添加一个选中的值
+    fun addSelectedValue(value: NewContactListVO) {
+        val count = mSelectDepartments.size + mSelectIdentities.size + mSelectGroups.size + mSelectUsers.size
+        if (maxNumber in 1..count) {
+           XToast.toastShort(this, "不能添加更多了!")
+            return
+        }
+        when(value) {
+            is NewContactListVO.Department -> mSelectDepartments.add(ContactPickerResultItem(value.name, value.distinguishedName))
+            is NewContactListVO.Identity -> mSelectIdentities.add(ContactPickerResultItem(value.name, value.distinguishedName))
+            is NewContactListVO.Group -> mSelectGroups.add(ContactPickerResultItem(value.name, value.distinguishedName))
+            is NewContactListVO.Person -> mSelectUsers.add(ContactPickerResultItem(value.name, value.distinguishedName))
+        }
+        refreshMenu()
+    }
+
+
+    private fun refreshMenu() {
+        invalidateOptionsMenu()
+    }
+
+
+    private fun addFragment(fragment: Fragment){
+        replaceFragmentSafely(fragment, fragment.javaClass.simpleName, R.id.frame_contact_picker_main, allowState = true)
+    }
+
+    private fun createFragment(mode: String, index: Int, isShowTab:Boolean) {
+        when(mode) {
+            "departmentPicker" -> {
+                if (index == 0) {pickerTitle = "组织选择"}
+                val f = ContactUnitAndIdentityPicker.startPicker(
+                        ContactUnitAndIdentityPicker.ORG_PICK_MODE,
+                        topList = topList,
+                        orgType = orgType,
+                        maxNumber = maxNumber,
+                        multiple = multiple,
+                        duty = duty
+                )
+                fragments.add(f)
+                if (isShowTab) {toolbar_snippet_tab_layout.addTab(toolbar_snippet_tab_layout.newTab().setText("组织选择"))}
+            }
+            "identityPicker" -> {
+                if (index == 0) {pickerTitle = "身份选择"}
+                val f = ContactUnitAndIdentityPicker.startPicker(
+                        ContactUnitAndIdentityPicker.IDENTITY_PICK_MODE,
+                        topList = topList,
+                        orgType = orgType,
+                        maxNumber = maxNumber,
+                        multiple = multiple,
+                        duty = duty
+                )
+                fragments.add(f)
+                if (isShowTab) {toolbar_snippet_tab_layout.addTab(toolbar_snippet_tab_layout.newTab().setText("身份选择"))}
+            }
+            "groupPicker" -> {
+                if (index == 0) {pickerTitle = "群组选择"}
+                val f = ContactPersonGroupPicker.startPicker(
+                        ContactPersonGroupPicker.GROUP_PICK_MODE,
+                        multiple,
+                        maxNumber
+                )
+                fragments.add(f)
+                if (isShowTab) {toolbar_snippet_tab_layout.addTab(toolbar_snippet_tab_layout.newTab().setText("群组选择"))}
+            }
+            "personPicker" -> {
+                if (index == 0) {pickerTitle = "人员选择"}
+                val f =  ContactUnitAndIdentityPicker.startPicker(
+                        ContactUnitAndIdentityPicker.PERSON_PICK_MODE,
+                        topList = topList,
+                        maxNumber = maxNumber,
+                        multiple = multiple
+                )
+                fragments.add(f)
+                if (isShowTab) {toolbar_snippet_tab_layout.addTab(toolbar_snippet_tab_layout.newTab().setText("人员选择"))}
+            }
+        }
+    }
+
+    private fun initView() {
+        if (pickerModes.size == 1) {
+            createFragment(pickerModes[0], 0, false)
+            toolbar_snippet_tab_layout.gone()
+        } else {
+            pickerModes.forEachIndexed { index, mode ->
+                createFragment(mode, index, true)
+            }
+            toolbar_snippet_tab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+                override fun onTabReselected(tab: TabLayout.Tab?) {
+                }
+
+                override fun onTabUnselected(tab: TabLayout.Tab?) {
+                }
+
+                override fun onTabSelected(tab: TabLayout.Tab?) {
+                    XLog.debug("selected..................")
+                    val name = tab?.text.toString()
+                    val p = tab?.position ?: 0
+                    currentSelect = p
+                    addFragment(fragments[p])
+                    updateToolbarTitle(name)
+                }
+            })
+            toolbar_snippet_tab_layout.tabMode = TabLayout.MODE_FIXED
+            toolbar_snippet_tab_layout.visible()
+        }
+        setupToolBar(pickerTitle, true, isCloseBackIcon = true)
+        if (fragments.isEmpty()) {
+            XToast.toastShort(this, "传入的选择器类型不正确!")
+            finish()
+        }else {
+            addFragment(fragments[0])
+        }
+    }
+
+
+}

+ 18 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPickerActivityContract.kt

@@ -0,0 +1,18 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+
+/**
+ * Created by fancyLou on 2019-08-20.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+object ContactPickerActivityContract {
+    interface View: BaseView{
+
+    }
+    interface Presenter: BasePresenter<View> {
+
+    }
+}

+ 13 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactPickerActivityPresenter.kt

@@ -0,0 +1,13 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+
+/**
+ * Created by fancyLou on 2019-08-20.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+class ContactPickerActivityPresenter: BasePresenterImpl<ContactPickerActivityContract.View>(), ContactPickerActivityContract.Presenter {
+
+}

+ 353 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactUnitAndIdentityPicker.kt

@@ -0,0 +1,353 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import android.os.Bundle
+import android.os.Handler
+import android.support.v7.widget.LinearLayoutManager
+import android.text.TextUtils
+import android.util.TypedValue
+import android.view.View
+import android.widget.*
+import kotlinx.android.synthetic.main.fragment_unit_identity_picker.*
+import net.muliba.changeskin.FancySkinManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.ContactComplexPickerListAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.ContactBreadcrumbBean
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.imageloader.O2ImageLoaderOptions
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleImageView
+
+/**
+ * Created by fancyLou on 2019-08-20.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+class ContactUnitAndIdentityPicker: BaseMVPFragment<ContactUnitAndIdentityPickerContract.View,
+        ContactUnitAndIdentityPickerContract.Presenter>(), ContactUnitAndIdentityPickerContract.View {
+
+    companion object {
+        const val ORG_PICK_MODE = "0" //组织选择
+        const val IDENTITY_PICK_MODE = "1" //身份选择
+        const val PERSON_PICK_MODE = "2" //人员选择
+
+        const val PICK_MODE_KEY = "PICK_MODE_KEY"//选择模式的key
+        const val TOP_LIST_KEY = "TOP_LIST_KEY"
+        const val ORG_TYPE_KEY = "ORG_TYPE_KEY"
+        const val MULIPLE_KEY = "multiple_KEY"
+        const val MAX_NUMBER_KEY = "MAX_NUMBER_KEY"
+        const val DUTY_KEY = "DUTY_KEY"
+
+        fun startPicker(pickMode: String,
+                              topList: ArrayList<String> = ArrayList(),
+                              orgType: String = "",
+                              multiple:Boolean = false,
+                              maxNumber: Int = 0,
+                              duty: ArrayList<String> = ArrayList()
+        ): ContactUnitAndIdentityPicker {
+            val picker =  ContactUnitAndIdentityPicker()
+            val bundle = Bundle()
+            bundle.putString(PICK_MODE_KEY, pickMode)
+            bundle.putStringArrayList(TOP_LIST_KEY, topList)
+            bundle.putString(ORG_TYPE_KEY, orgType)
+            bundle.putStringArrayList(DUTY_KEY, duty)
+            bundle.putBoolean(MULIPLE_KEY, multiple)
+            bundle.putInt(MAX_NUMBER_KEY, maxNumber)
+            picker.arguments = bundle
+            return picker
+        }
+    }
+
+    override var mPresenter: ContactUnitAndIdentityPickerContract.Presenter = ContactUnitAndIdentityPickerPresenter()
+
+    override fun layoutResId(): Int = R.layout.fragment_unit_identity_picker
+
+    private var pickMode = ORG_PICK_MODE//选择模式 0组织选择、1身份选择
+    private var topList: List<String> = ArrayList()//可选的顶级组织列表
+    private var orgType = ""//可选择的组织类别
+    private var multiple = true//是否多选
+    private var maxNumber = 0//当multiple为true的时候,最多可选择的数量
+    private var duty: List<String> = ArrayList()//人员职责
+
+
+    //面包屑导航
+    private val breadcrumbBeans = ArrayList<ContactBreadcrumbBean>()
+    private var orgLevel = 0//默认进入的时候是第一层组织
+    private var unitParentId = ""
+    private var unitParentName = ""
+    private val itemList: ArrayList<NewContactListVO> = ArrayList()
+    private val adapter: ContactComplexPickerListAdapter by lazy {
+        object: ContactComplexPickerListAdapter(itemList) {
+            override fun bindDepartment(hold: CommonRecyclerViewHolder?, department: NewContactListVO.Department, position: Int) {
+                val checkBox = hold?.getView<CheckBox>(R.id.check_item_contact_complex_picker_org_body)
+                if (pickMode == ORG_PICK_MODE) {
+                    checkBox?.visible()
+                }else {
+                    checkBox?.gone()
+                }
+                checkBox?.isChecked = false
+                checkBox?.setOnClickListener {
+                    val check = checkBox.isChecked
+                    toggleCheckOrg(department, check)
+                }
+                checkBox?.isChecked = (activity as ContactPickerActivity).isSelectedValue(department)
+                val count = if (pickMode == ORG_PICK_MODE) {
+                    department.departmentCount
+                }else {
+                    department.departmentCount + department.identityCount
+                }
+                hold?.setCircleTextView(R.id.image_item_contact_complex_picker_org_body_icon,
+                        department.name.substring(0, 1), FancySkinManager.instance().getColor(activity, R.color.z_color_primary))
+                        ?.setText(R.id.tv_item_contact_complex_picker_org_body_name, department.name)//+"($count)" 不显示数量 不准确
+                val nextLevelBtn = hold?.getView<Button>(R.id.btn_item_contact_complex_picker_org_body_next)
+                if (count>0) {
+                    nextLevelBtn?.visible()
+                    nextLevelBtn?.setOnClickListener {
+                        val newLevel = orgLevel + 1
+                        val bean = ContactBreadcrumbBean(department.distinguishedName, department.name, newLevel)
+                        breadcrumbBeans.add(bean)
+                        refreshRV()
+                    }
+                }else {
+                    nextLevelBtn?.gone()
+                }
+            }
+
+            override fun clickDepartment(view: View, department: NewContactListVO.Department) {
+                XLog.debug("click Department")
+                if (pickMode == ORG_PICK_MODE) {
+                    val checkBox = view.findViewById<CheckBox>(R.id.check_item_contact_complex_picker_org_body)
+                    val isCheck = checkBox.isChecked
+                    checkBox.isChecked = !isCheck
+                    toggleCheckOrg(department, !isCheck)
+                }
+            }
+
+            override fun bindIdentity(hold: CommonRecyclerViewHolder?, identity: NewContactListVO.Identity, position: Int) {
+                val checkBox = hold?.getView<CheckBox>(R.id.check_item_contact_complex_picker_identity_select)
+                if (pickMode != ORG_PICK_MODE) {
+                    checkBox?.visible()
+                }else {
+                    checkBox?.gone()
+                }
+                checkBox?.isChecked = false
+                checkBox?.setOnClickListener {
+                    val check = checkBox.isChecked
+                    toggleCheckIdentity(identity, check)
+                }
+                if (pickMode == PERSON_PICK_MODE) {
+                    checkBox?.isChecked = (activity as ContactPickerActivity).isSelectedValue(NewContactListVO.Person(name = identity.name, distinguishedName = identity.person))
+                }else {
+                    checkBox?.isChecked = (activity as ContactPickerActivity).isSelectedValue(identity)
+                }
+                val icon = hold?.getView<CircleImageView>(R.id.image_item_contact_complex_picker_identity_icon)
+                if (icon != null) {
+                    val url = APIAddressHelper.instance().getPersonAvatarUrlWithId(identity.person)
+                    O2ImageLoaderManager.instance().showImage(icon, url, O2ImageLoaderOptions(placeHolder = R.mipmap.icon_avatar_men))
+                }
+                //是否显示顶部间隔
+                var isShowGap = false
+                if (position >= 1) {
+                    val preItem = items[(position - 1)]
+                    if ((preItem !is NewContactListVO.Identity)) {
+                        isShowGap = true
+                    }
+                } else {
+                    isShowGap = true
+                }
+                val gap = hold?.getView<RelativeLayout>(R.id.rl_item_contact_complex_picker_identity_top_gap)
+                if (isShowGap) {
+                    gap?.visible()
+                }else {
+                    gap?.gone()
+                }
+                hold?.setText(R.id.tv_item_contact_complex_picker_identity_name, identity.name)
+            }
+
+            override fun clickIdentity(view: View, identity: NewContactListVO.Identity) {
+                XLog.debug("click Identity")
+                if (pickMode != ORG_PICK_MODE) {
+                    val checkBox = view.findViewById<CheckBox>(R.id.check_item_contact_complex_picker_identity_select)
+                    val isCheck = checkBox.isChecked
+                    checkBox.isChecked = !isCheck
+                    toggleCheckIdentity(identity, !isCheck)
+                }
+            }
+
+        }
+    }
+
+    override fun initUI() {
+        //选择模式
+        pickMode = arguments?.getString(PICK_MODE_KEY) ?: ORG_PICK_MODE
+        if (pickMode != ORG_PICK_MODE && pickMode != IDENTITY_PICK_MODE && pickMode != PERSON_PICK_MODE) {
+            pickMode = ORG_PICK_MODE
+        }
+        //是否多选
+        multiple = arguments?.getBoolean(MULIPLE_KEY) ?: true
+        //组织类型
+        orgType = arguments?.getString(ORG_TYPE_KEY) ?: ""
+        //最多可选
+        maxNumber = arguments?.getInt(MAX_NUMBER_KEY) ?: 0
+        if (!multiple) {
+            maxNumber = 1
+        }
+        //人员职责
+        duty = arguments?.getStringArrayList(DUTY_KEY) ?: ArrayList()
+        //顶层组织
+        topList = arguments?.getStringArrayList(TOP_LIST_KEY) ?: ArrayList()
+
+        //初始化view
+        swipe_refresh_contact_complex_picker_main.setColorSchemeResources(R.color.z_color_refresh_scuba_blue,
+                R.color.z_color_refresh_red, R.color.z_color_refresh_purple, R.color.z_color_refresh_orange)
+        swipe_refresh_contact_complex_picker_main.setOnRefreshListener {
+            refreshRV()
+        }
+        rv_contact_complex_picker_main.layoutManager =  LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
+        rv_contact_complex_picker_main.adapter = adapter
+
+        //初始化数据
+        orgLevel = 0
+        unitParentId = "-1" //顶层
+        unitParentName = getString(R.string.tab_contact)
+        breadcrumbBeans.clear()
+        breadcrumbBeans.add(ContactBreadcrumbBean(unitParentId, unitParentName, orgLevel))
+
+        refreshRV()
+    }
+
+    override fun callbackResult(list: List<NewContactListVO>) {
+        itemList.clear()
+        itemList.addAll(list)
+        adapter.notifyDataSetChanged()
+        swipe_refresh_contact_complex_picker_main.isRefreshing = false
+    }
+
+    override fun backError(error: String) {
+        if (!TextUtils.isEmpty(error)) {
+            XToast.toastShort(activity, error)
+        }
+        itemList.clear()
+        adapter.notifyDataSetChanged()
+        swipe_refresh_contact_complex_picker_main.isRefreshing = false
+    }
+
+    //点击返回按钮
+    fun clickBackBtn(): Boolean {
+        if (breadcrumbBeans.size > 1) {
+            breadcrumbBeans.removeAt(breadcrumbBeans.size - 1)
+            refreshRV()
+            return true
+        }
+        return false
+    }
+
+    //刷新列表
+    private fun refreshRV() {
+        val bean = breadcrumbBeans[breadcrumbBeans.size - 1]//最后一个
+        loadOrgAndIdentityData(bean.key, bean.level)
+        refreshBreadcrumb()
+    }
+
+    //加载数据
+    private fun loadOrgAndIdentityData(id: String, level: Int) {
+        orgLevel = level
+        swipe_refresh_contact_complex_picker_main.isRefreshing = true
+        mPresenter.loadUnitWithParent(id, pickMode!=ORG_PICK_MODE, topList, orgType, duty)
+    }
+
+
+    //面包屑导航滚动条
+    val mHandler = Handler()
+    private val mScrollToBottom = Runnable {
+        val off = ll_contact_complex_picker_breadcrumb_layout.measuredWidth - hs_contact_complex_picker_breadcrumb_scroll.width
+        if (off > 0) {
+            hs_contact_complex_picker_breadcrumb_scroll.scrollTo(off, 0)
+        }
+    }
+
+    /**
+     * 刷新导航条
+     */
+    private fun refreshBreadcrumb() {
+        ll_contact_complex_picker_breadcrumb_layout.removeAllViews()
+        breadcrumbBeans.mapIndexed { index, contactBreadcrumbBean ->
+            val breadcrumbTitle = TextView(activity)
+            breadcrumbTitle.text = contactBreadcrumbBean.name
+            breadcrumbTitle.tag = contactBreadcrumbBean.key
+            breadcrumbTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
+            breadcrumbTitle.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
+            if (index == breadcrumbBeans.size - 1) {
+                breadcrumbTitle.setTextColor(FancySkinManager.instance().getColor(activity, R.color.z_color_primary))
+                ll_contact_complex_picker_breadcrumb_layout.addView(breadcrumbTitle)
+            } else {
+                breadcrumbTitle.setTextColor(FancySkinManager.instance().getColor(activity, R.color.z_color_text_primary_dark))
+                breadcrumbTitle.setOnClickListener { view -> onClickBreadcrumb((view as TextView)) }
+                ll_contact_complex_picker_breadcrumb_layout.addView(breadcrumbTitle)
+                val arrow = ImageView(activity)
+                arrow.setImageResource(R.mipmap.icon_arrow_22dp)
+                arrow.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
+                ll_contact_complex_picker_breadcrumb_layout.addView(arrow)
+            }
+        }
+        mHandler.post(mScrollToBottom)//启动线程 把滚动条滚动到最后
+    }
+
+
+    /**
+     * 点击导航条上的某一个层级名
+     */
+    private fun onClickBreadcrumb(textView: TextView) {
+        val tag = textView.tag as String
+        var newLevel = 0
+        for ((index, bean) in breadcrumbBeans.withIndex()) {
+            if (tag == bean.key) {
+                newLevel = index
+                loadOrgAndIdentityData(bean.key, newLevel)
+                break
+            }
+        }
+        //处理breadcrumbBeans 把多余的去掉
+        if (breadcrumbBeans.size > newLevel + 1) {
+            (breadcrumbBeans.size - 1 downTo 0)
+                    .filter { it > newLevel }
+                    .forEach { breadcrumbBeans.removeAt(it) }
+        }
+        refreshBreadcrumb()
+    }
+
+    private fun toggleCheckOrg(department: NewContactListVO.Department, check: Boolean) {
+        XLog.debug("click toggleCheckOrg, $check")
+        if (check) {
+            (activity as ContactPickerActivity).addSelectedValue(department)
+        } else {
+            (activity as ContactPickerActivity).removeSelectedValue(department)
+        }
+        adapter.notifyDataSetChanged()
+    }
+
+    private fun toggleCheckIdentity(identity: NewContactListVO.Identity, check: Boolean) {
+        XLog.debug("click toggleCheckIdentity, $check")
+        if (pickMode == PERSON_PICK_MODE) {
+            if (check) {
+                (activity as ContactPickerActivity).addSelectedValue(NewContactListVO.Person(name = identity.name, distinguishedName = identity.person))
+            } else {
+                (activity as ContactPickerActivity).removeSelectedValue(NewContactListVO.Person(name = identity.name, distinguishedName = identity.person))
+            }
+        }else {
+            if (check) {
+                (activity as ContactPickerActivity).addSelectedValue(identity)
+            } else {
+                (activity as ContactPickerActivity).removeSelectedValue(identity)
+            }
+        }
+        adapter.notifyDataSetChanged()
+    }
+
+}

+ 24 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactUnitAndIdentityPickerContract.kt

@@ -0,0 +1,24 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+
+/**
+ * Created by fancyLou on 2019-08-20.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+object ContactUnitAndIdentityPickerContract {
+    interface View: BaseView {
+        fun callbackResult(list: List<NewContactListVO>)
+        fun backError(error:String)
+    }
+    interface Presenter: BasePresenter<View> {
+        /**
+         * @param parent 上级组织id
+         * @param isLoadIdentity 是否加载组织下的身份列表
+         */
+        fun loadUnitWithParent(parent: String, isLoadIdentity: Boolean, topList: List<String>, orgType: String, dutyList: List<String>)
+    }
+}

+ 147 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/ContactUnitAndIdentityPickerPresenter.kt

@@ -0,0 +1,147 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
+
+import android.text.TextUtils
+import net.muliba.accounting.app.ExceptionHandler
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.service.OrganizationAssembleControlAlphaService
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.UnitDutyIdentityForm
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.unit.UnitListForm
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.functions.Action1
+import rx.schedulers.Schedulers
+
+/**
+ * Created by fancyLou on 2019-08-20.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+class ContactUnitAndIdentityPickerPresenter: BasePresenterImpl<ContactUnitAndIdentityPickerContract.View>(), ContactUnitAndIdentityPickerContract.Presenter {
+
+    override fun loadUnitWithParent(parent: String, isLoadIdentity: Boolean, topList: List<String>, orgType: String, dutyList: List<String>) {
+        XLog.debug("loadUnitWithParent parent:$parent , isLoadIdentity:$isLoadIdentity ,topList:$topList orgType:$orgType dutyList:$dutyList")
+        val service = getOrganizationAssembleControlApi(mView?.getContext())
+        val assService = getAssembleExpressApi(mView?.getContext())
+        if(service!=null && assService!=null) {
+            if (parent == "-1") {
+                if (TextUtils.isEmpty(orgType)) {
+                    val observable = if (topList.isNotEmpty()) {
+                        service.unitList(UnitListForm(topList))
+                    }else {
+                        service.unitListTop()
+                    }
+                    observable.flatMap { response ->
+                        val retList = ArrayList<NewContactListVO>()
+                        val list = response.data
+                        if (list != null && list.isNotEmpty()) {
+                            list.map { retList.add(it.copyToOrgVO()) }
+                        }
+                        Observable.just(retList)
+                    }.subscribeOn(Schedulers.io())
+                            .observeOn(AndroidSchedulers.mainThread())
+                            .subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
+                                    ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") })
+                } else {
+                    if (topList.isNotEmpty()) {
+                        service.unitList(UnitListForm(topList)).flatMap { response ->
+                            val retList = ArrayList<NewContactListVO>()
+                            val list = response.data
+                            if (list != null && list.isNotEmpty()) {
+                                list.map {
+                                    if (it.typeList.contains(orgType)) {
+                                        retList.add(it.copyToOrgVO())
+                                    }
+                                }
+                            }
+                            Observable.just(retList)
+                        }.subscribeOn(Schedulers.io())
+                                .observeOn(AndroidSchedulers.mainThread())
+                                .subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
+                                        ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") })
+                    }else {
+                        searchUnitWithType(service, orgType, arrayListOf())
+                    }
+                }
+            } else {
+                if (isLoadIdentity) {
+                    val unitObservable = service.unitSubDirectList(parent).map { response ->
+                        val retList = ArrayList<NewContactListVO>()
+                        val list = response.data
+                        if (list != null && list.isNotEmpty()) {
+                            list.map { retList.add(it.copyToOrgVO()) }
+                        }
+                        retList
+                    }
+                    val identityObservable = if (dutyList.isEmpty()){
+                        service.identityListWithUnit(parent).map { response ->
+                            val retList = ArrayList<NewContactListVO>()
+                            val list = response.data
+                            if (list != null && list.isNotEmpty()) {
+                                list.map { retList.add(it.copyToOrgVO()) }
+                            }
+                            retList
+                        }
+                    } else {
+                        val form = UnitDutyIdentityForm()
+                        form.unit = parent
+                        form.nameList = dutyList
+                        assService.identityListByUnitAndDuty(form).map { response ->
+                            val retList = ArrayList<NewContactListVO>()
+                            val list = response.data
+                            if (list != null && list.isNotEmpty()) {
+                                list.map { retList.add(it.copyToOrgVO()) }
+                            }
+                            retList
+                        }
+                    }
+                    Observable.zip(unitObservable, identityObservable) { t1, t2 ->
+                        val retList = ArrayList<NewContactListVO>()
+                        retList.addAll(t1)
+                        retList.addAll(t2)
+                        retList
+                    }.subscribeOn(Schedulers.io())
+                            .observeOn(AndroidSchedulers.mainThread())
+                            .subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
+                                    ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") })
+                }else {
+                    if (TextUtils.isEmpty(orgType)) {
+                        service.unitSubDirectList(parent).map { response ->
+                            val retList = ArrayList<NewContactListVO>()
+                            val list = response.data
+                            if (list != null && list.isNotEmpty()) {
+                                list.map { retList.add(it.copyToOrgVO()) }
+                            }
+                            retList
+                        }.subscribeOn(Schedulers.io())
+                                .observeOn(AndroidSchedulers.mainThread())
+                                .subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
+                                        ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") })
+                    } else {
+                        searchUnitWithType(service, orgType, arrayListOf(parent))
+                    }
+                }
+            }
+        }else {
+            mView?.backError("组织服务异常!")
+        }
+    }
+
+    private fun searchUnitWithType(service: OrganizationAssembleControlAlphaService, orgType: String, parentList: List<String>) {
+        val form = UnitListForm()
+        form.type = orgType
+        form.unitList = parentList
+        service.unitListByType(form).flatMap { response ->
+            val retList = ArrayList<NewContactListVO>()
+            val list = response.data
+            if (list != null && list.isNotEmpty()) {
+                list.map { retList.add(NewContactListVO.Department(it.id, it.name, it.distinguishedName, it.woSubDirectUnitList.size, 0)) }
+            }
+            Observable.just(retList)
+        }.subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
+                        ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") })
+    }
+}

+ 11 - 11
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/NewOrganizationActivity.kt

@@ -81,16 +81,16 @@ class NewOrganizationActivity : BaseMVPActivity<NewOrganizationContract.View, Ne
             return bundle
         }
 
-        fun startBundleDataForIMChoose(choosePersonList: ArrayList<String>, chooseFromRequest: Int = IM_CHOOSE_NEED_START_ACTIVITY): Bundle {
-            val bundle = Bundle()
-            bundle.putInt(FROM_IM_CHOOSE_KEY, chooseFromRequest)
-            bundle.putString(UNIT_PARENT_KEY, "")
-            bundle.putString(UNIT_PARENT_NAME_KEY, "")
-            bundle.putInt(MODE_KEY, MULTI_PERSON_CHOOSE_MODE)
-            bundle.putInt(STATUS_KEY, DEFAULT_STATUS)
-            bundle.putStringArrayList(ALREADY_CHOOSE_PERSON_LIST_KEY, choosePersonList)
-            return bundle
-        }
+//        fun startBundleDataForIMChoose(choosePersonList: ArrayList<String>, chooseFromRequest: Int = IM_CHOOSE_NEED_START_ACTIVITY): Bundle {
+//            val bundle = Bundle()
+//            bundle.putInt(FROM_IM_CHOOSE_KEY, chooseFromRequest)
+//            bundle.putString(UNIT_PARENT_KEY, "")
+//            bundle.putString(UNIT_PARENT_NAME_KEY, "")
+//            bundle.putInt(MODE_KEY, MULTI_PERSON_CHOOSE_MODE)
+//            bundle.putInt(STATUS_KEY, DEFAULT_STATUS)
+//            bundle.putStringArrayList(ALREADY_CHOOSE_PERSON_LIST_KEY, choosePersonList)
+//            return bundle
+//        }
     }
 
 
@@ -134,7 +134,7 @@ class NewOrganizationActivity : BaseMVPActivity<NewOrganizationContract.View, Ne
 
             override fun bindIdentity(hold: CommonRecyclerViewHolder?, identity: NewContactListVO.Identity, position: Int) {
                 hold?.setText(R.id.tv_item_contact_person_body_name, identity.name)
-                        ?.setText(R.id.tv_item_contact_person_body_mobile, identity.mobile)
+                        ?.setText(R.id.tv_item_contact_person_body_mobile, "")
                 val icon = hold?.getView<CircleImageView>(R.id.image_item_contact_person_body_icon)
                 if (icon != null) {
                     val url = APIAddressHelper.instance().getPersonAvatarUrlWithId(identity.person)

+ 53 - 19
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/organization/NewOrganizationPresenter.kt

@@ -1,8 +1,13 @@
 package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization
 
 import net.muliba.accounting.app.ExceptionHandler
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.ApiResponse
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.IdentityLevelForm
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.person.PersonListLikeForm
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.unit.UnitJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactFragmentVO
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
@@ -13,25 +18,50 @@ import rx.schedulers.Schedulers
 class NewOrganizationPresenter : BasePresenterImpl<NewOrganizationContract.View>(), NewOrganizationContract.Presenter {
 
     override fun loadChildrenWithParent(unitParentId: String) {
-
+        val personService = getAssemblePersonalApi(mView?.getContext())
+        val expressService = getAssembleExpressApi(mView?.getContext())
             getOrganizationAssembleControlApi(mView?.getContext())?.let { service->
             if (unitParentId == NewOrganizationActivity.UNIT_TOP_PARENT_ID) {
-                service.unitListTop().flatMap { response ->
-                    val retList = ArrayList<NewContactListVO>()
-                    val list = response.data
-                    if (list != null && !list.isEmpty()) {
-                        list.map { retList.add(it.copyToOrgVO()) }
+//                service.unitListTop().flatMap { response ->
+//                    val retList = ArrayList<NewContactListVO>()
+//                    val list = response.data
+//                    if (list != null && list.isNotEmpty()) {
+//                        list.map { retList.add(it.copyToOrgVO()) }
+//                    }
+//                    Observable.just(retList)
+//                }
+                personService?.getCurrentPersonInfo()?.flatMap { response ->
+                    val person = response.data
+                    if (person != null) {
+                        val identityList = person.woIdentityList
+                        if (identityList.isNotEmpty()) {
+                            val identity = identityList[0]
+                            val form = IdentityLevelForm(identity = identity.distinguishedName, level = 1)
+                            expressService?.unitByIdentityAndLevel(form)
+                        } else {
+                            Observable.just(ApiResponse())
+                        }
+                    } else {
+                        Observable.just(ApiResponse())
                     }
-                    Observable.just(retList)
-                }.subscribeOn(Schedulers.io())
-                        .observeOn(AndroidSchedulers.mainThread())
-                        .subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
+                }?.flatMap { response ->
+                    val topList = ArrayList<NewContactListVO>()
+                    val unit = response.data
+                    if (unit != null) {
+                        val u = unit.copyToOrgVO() as NewContactListVO.Department
+                        u.departmentCount = 1
+                        topList.add(u)
+                    }
+                    Observable.just(topList)
+                }?.subscribeOn(Schedulers.io())
+                        ?.observeOn(AndroidSchedulers.mainThread())
+                        ?.subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
                                 ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") })
             } else {
                 val unitObservable = service.unitSubDirectList(unitParentId).map { response ->
                     val retList = ArrayList<NewContactListVO>()
                     val list = response.data
-                    if (list != null && !list.isEmpty()) {
+                    if (list != null && list.isNotEmpty()) {
                         list.map { retList.add(it.copyToOrgVO()) }
                     }
                     retList
@@ -39,18 +69,18 @@ class NewOrganizationPresenter : BasePresenterImpl<NewOrganizationContract.View>
                 val identityObservable = service.identityListWithUnit(unitParentId).map { response ->
                     val retList = ArrayList<NewContactListVO>()
                     val list = response.data
-                    if (list != null && !list.isEmpty()) {
+                    if (list != null && list.isNotEmpty()) {
                         list.map { retList.add(it.copyToOrgVO()) }
                     }
                     retList
                 }
 
-                Observable.zip(unitObservable, identityObservable, { t1, t2 ->
+                Observable.zip(unitObservable, identityObservable) { t1, t2 ->
                     val retList = ArrayList<NewContactListVO>()
                     retList.addAll(t1)
                     retList.addAll(t2)
                     retList
-                }).subscribeOn(Schedulers.io())
+                }.subscribeOn(Schedulers.io())
                         .observeOn(AndroidSchedulers.mainThread())
                         .subscribe(Action1<ArrayList<NewContactListVO>> { list -> mView?.callbackResult(list) },
                                 ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") })
@@ -61,24 +91,28 @@ class NewOrganizationPresenter : BasePresenterImpl<NewOrganizationContract.View>
 
     override fun searchPersonWithKey(result: String) {
             val form = PersonListLikeForm( result)
-            val result = ArrayList<NewContactListVO>()
+            val backResult = ArrayList<NewContactListVO>()
             getOrganizationAssembleControlApi(mView?.getContext())?.let { service ->
                 service.personListLike(form)
                         .subscribeOn(Schedulers.io())
                         .map { response ->
                             val retList = ArrayList<NewContactListVO>()
                             val list = response.data
-                            if (list != null && !list.isEmpty()) {
+                            if (list != null && list.isNotEmpty()) {
                                 list.map {
-                                    retList.add(it.copyToOrgVO())
+                                    retList.add(NewContactListVO.Identity(
+                                            name = it.name,
+                                            person = it.id,
+                                            distinguishedName = it.distinguishedName
+                                    ))
                                 }
                             }
                             retList
                         }
                         .observeOn(AndroidSchedulers.mainThread())
-                        .subscribe(Action1<ArrayList<NewContactListVO>> { list -> result.addAll(list) },
+                        .subscribe(Action1<ArrayList<NewContactListVO>> { list -> backResult.addAll(list) },
                                 ExceptionHandler(mView?.getContext()) { e -> mView?.backError(e.message ?: "") },
-                                Action0 { mView?.callbackResult(result) })
+                                Action0 { mView?.callbackResult(backResult) })
             }
         }
 }

+ 4 - 3
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/process/StartProcessStepTwoFragment.kt

@@ -149,10 +149,11 @@ class StartProcessStepTwoFragment : BaseMVPFragment<StartProcessStepTwoContract.
 
 
     private fun startProcess() {
-        val title = edit_start_process_step_two_title.text.toString()
+        var title = edit_start_process_step_two_title.text.toString()
         if (TextUtils.isEmpty(title)) {
-            XToast.toastShort(activity, "请输入文件标题")
-            return
+//            XToast.toastShort(activity, "请输入文件标题")
+//            return
+            title = "无标题"
         }
         showLoadingDialog()
         mPresenter.startProcess(title, identity, processId)

+ 41 - 4
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/scanlogin/ScanLoginActivity.kt

@@ -4,20 +4,25 @@ package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.scanlogin
 import android.os.Bundle
 import android.text.TextUtils
 import kotlinx.android.synthetic.main.activity_scan_login.*
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.AndroidUtils
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.StringUtil
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Request
+import okhttp3.Response
+import java.io.IOException
 
 
 class ScanLoginActivity : BaseMVPActivity<ScanLoginContract.View, ScanLoginContract.Presenter>(), ScanLoginContract.View {
     override var mPresenter: ScanLoginContract.Presenter = ScanLoginPresenter()
 
-    override fun layoutResId(): Int = R.layout.activity_scan_login
+    override fun layoutResId(): Int = net.zoneland.x.bpm.mobile.v1.zoneXBPM.R.layout.activity_scan_login
 
     companion object {
         val SCAN_RESULT_KEY = "scan_result_key"
@@ -57,12 +62,14 @@ class ScanLoginActivity : BaseMVPActivity<ScanLoginContract.View, ScanLoginContr
             if (result.contains("pgyer.com")) {
                 parseMeta()
                 if (!TextUtils.isEmpty(meta)) {
-                    title = getString(R.string.scan_login_confirm_title)
+                    title = getString(net.zoneland.x.bpm.mobile.v1.zoneXBPM.R.string.scan_login_confirm_title)
                     activity_scan_login.visible()
                     tv_scan_login_text_content.gone()
                 }else{
                     gotoDefaultBrowser()
                 }
+            }else if (result.contains("x_meeting_assemble_control") && result.contains("/checkin")){
+                meetingCheckin(result)//会议签到
             }else{
                 gotoDefaultBrowser()
             }
@@ -70,7 +77,37 @@ class ScanLoginActivity : BaseMVPActivity<ScanLoginContract.View, ScanLoginContr
             activity_scan_login.gone()
             tv_scan_login_text_content.text = result
             tv_scan_login_text_content.visible()
-            title = getString(R.string.scan_login_title)
+            title = getString(net.zoneland.x.bpm.mobile.v1.zoneXBPM.R.string.scan_login_title)
+        }
+    }
+
+    private fun  meetingCheckin(url: String) {
+        XLog.debug("会议签到:$url")
+        val request = Request.Builder().get().url(url).build()
+        val client = RetrofitClient.instance().getO2HttpClient()
+        if (client != null) {
+            val call = client.newCall(request)
+            call.enqueue(object : Callback{
+                override fun onFailure(call: Call, e: IOException) {
+                    XLog.error("", e)
+                    runOnUiThread {
+                        XToast.toastShort(this@ScanLoginActivity, "签到失败")
+                        finish()
+                    }
+
+                }
+
+                override fun onResponse(call: Call, response: Response) {
+                    val result = response.body()?.string()
+                    XLog.debug(result)
+                    runOnUiThread {
+                        XToast.toastShort(this@ScanLoginActivity, "签到成功")
+                        finish()
+                    }
+
+                }
+
+            })
         }
     }
 

+ 243 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/webview/JSInterfaceO2mBiz.kt

@@ -0,0 +1,243 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.webview
+
+
+
+
+import android.os.Bundle
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentActivity
+import android.text.TextUtils
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.wugang.activityresult.library.ActivityResult
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import org.json.JSONObject
+import org.json.JSONTokener
+
+
+class JSInterfaceO2mBiz  private constructor(val activity: FragmentActivity?) {
+    companion object {
+        const val JSInterfaceName = "o2mBiz"
+        fun with(activity: FragmentActivity) = JSInterfaceO2mBiz(activity)
+        fun with(fragment: Fragment) = JSInterfaceO2mBiz(fragment.activity)
+    }
+
+    private lateinit var webView: WebView
+    private val gson: Gson by lazy { Gson() }
+
+    fun setupWebView(webView: WebView) {
+        this.webView = webView
+    }
+
+
+    @JavascriptInterface
+    fun postMessage(message: String?) {
+        if (!TextUtils.isEmpty(message)) {
+            XLog.debug(message)
+            val json = JSONTokener(message).nextValue()
+            if (json is JSONObject) {
+                when (json.getString("type")) {
+                    "contact.departmentPicker" -> departmentsPicker(message!!)
+                    "contact.identityPicker" -> identityPicker(message!!)
+                    "contact.groupPicker" -> groupPicker(message!!)
+                    "contact.personPicker" -> personPicker(message!!)
+                    "contact.complexPicker" -> complexPicker(message!!)
+                }
+            } else {
+                XLog.error("message 格式错误!!!")
+            }
+        } else {
+            XLog.error("o2mBiz.postMessage error, 没有传入message内容!")
+        }
+    }
+
+    private fun callbackJs(js: String) {
+        if (::webView.isInitialized && !TextUtils.isEmpty(js)) {
+            activity?.runOnUiThread {
+                XLog.debug("执行js:$js")
+                webView.evaluateJavascript(js) { value ->
+                    XLog.debug("js执行完成, result:$value")
+                }
+            }
+        } else {
+            XLog.error("没有注入webView,无法执行回调函数!!!!")
+        }
+    }
+
+    private fun identityPicker(message: String) {
+        val type = object : TypeToken<O2JsPostMessage<O2BizIdentityPickerMessage>>() {}.type
+        val value: O2JsPostMessage<O2BizIdentityPickerMessage> = gson.fromJson(message, type)
+        val callback = value.callback
+        if (activity != null) {
+            val jsFormData = value.data
+            val bundle  = if (jsFormData != null) {
+                val dutyList = jsFormData.duty ?: ArrayList()
+                val topList = jsFormData.topList ?: ArrayList()
+                val multiple = jsFormData.multiple ?: true
+                val maxNumber = jsFormData.maxNumber ?: 0
+                val pickedIdentities = jsFormData.pickedIdentities ?: ArrayList()
+                ContactPickerActivity.startPickerBundle(
+                        arrayListOf("identityPicker"),
+                        dutyList = dutyList,
+                        multiple = multiple,
+                        topUnitList = topList,
+                        maxNumber = maxNumber,
+                        unitType = "",
+                        initIdList = pickedIdentities
+                )
+            }else {
+                ContactPickerActivity.startPickerBundle(arrayListOf("identityPicker"))
+            }
+            showPicker(bundle, callback)
+        } else {
+            XLog.error("activity不存在 identityPicker 失败!!")
+        }
+    }
+
+    private fun departmentsPicker(message: String) {
+        val type = object : TypeToken<O2JsPostMessage<O2BizUnitPickerMessage>>() {}.type
+        val value: O2JsPostMessage<O2BizUnitPickerMessage> = gson.fromJson(message, type)
+        val callback = value.callback
+        if (activity != null) {
+            val jsFormData = value.data
+            val bundle  = if (jsFormData != null) {
+                val orgType = jsFormData.orgType ?: ""
+                val topList = jsFormData.topList ?: ArrayList()
+                val multiple = jsFormData.multiple ?: true
+                val maxNumber = jsFormData.maxNumber ?: 0
+                val pickedDepartments = jsFormData.pickedDepartments ?: ArrayList()
+                ContactPickerActivity.startPickerBundle(
+                        arrayListOf("departmentPicker"),
+                        dutyList = arrayListOf(),
+                        multiple = multiple,
+                        topUnitList = topList,
+                        maxNumber = maxNumber,
+                        unitType = orgType,
+                        initDeptList = pickedDepartments
+                )
+            }else {
+                ContactPickerActivity.startPickerBundle(arrayListOf("departmentPicker"))
+            }
+            showPicker(bundle, callback)
+        } else {
+            XLog.error("activity不存在 departmentsPicker 失败!!")
+        }
+    }
+
+    private fun personPicker(message: String) {
+        val type = object : TypeToken<O2JsPostMessage<O2BizPersonPickerMessage>>() {}.type
+        val value: O2JsPostMessage<O2BizPersonPickerMessage> = gson.fromJson(message, type)
+        val callback = value.callback
+        if (activity != null) {
+            val jsFormData = value.data
+            val bundle  = if (jsFormData != null) {
+                val multiple = jsFormData.multiple ?: true
+                val maxNumber = jsFormData.maxNumber ?: 0
+                val pickedPersonList = jsFormData.pickedUsers ?: ArrayList()
+                ContactPickerActivity.startPickerBundle(
+                        arrayListOf("personPicker"),
+                        dutyList = arrayListOf(),
+                        multiple = multiple,
+                        topUnitList = arrayListOf(),
+                        maxNumber = maxNumber,
+                        unitType = "",
+                        initUserList = pickedPersonList
+                )
+            }else {
+                ContactPickerActivity.startPickerBundle(arrayListOf("personPicker"))
+            }
+           showPicker(bundle, callback)
+        } else {
+            XLog.error("activity不存在 personPicker 失败!!")
+        }
+    }
+
+    private fun groupPicker(message: String) {
+        val type = object : TypeToken<O2JsPostMessage<O2BizGroupPickerMessage>>() {}.type
+        val value: O2JsPostMessage<O2BizGroupPickerMessage> = gson.fromJson(message, type)
+        val callback = value.callback
+        if (activity != null) {
+            val jsFormData = value.data
+            val bundle  = if (jsFormData != null) {
+                val multiple = jsFormData.multiple ?: true
+                val maxNumber = jsFormData.maxNumber ?: 0
+                val pickedGroups = jsFormData.pickedGroups ?: ArrayList()
+                ContactPickerActivity.startPickerBundle(
+                        arrayListOf("groupPicker"),
+                        dutyList = arrayListOf(),
+                        multiple = multiple,
+                        topUnitList = arrayListOf(),
+                        maxNumber = maxNumber,
+                        unitType = "",
+                        initGroupList = pickedGroups
+                )
+            }else {
+                ContactPickerActivity.startPickerBundle(arrayListOf("groupPicker"))
+            }
+            showPicker( bundle, callback)
+        } else {
+            XLog.error("activity不存在 groupPicker 失败!!")
+        }
+    }
+
+    private fun complexPicker(message: String) {
+        val type = object : TypeToken<O2JsPostMessage<O2BizComplexPickerMessage>>() {}.type
+        val value: O2JsPostMessage<O2BizComplexPickerMessage> = gson.fromJson(message, type)
+        val callback = value.callback
+        if (activity != null) {
+            val jsFormData = value.data
+            val bundle  = if (jsFormData != null) {
+                val pickMode = jsFormData.pickMode ?: ArrayList()
+                val dutyList = jsFormData.duty ?: ArrayList()
+                val topList = jsFormData.topList ?: ArrayList()
+                val multiple = jsFormData.multiple ?: true
+                val maxNumber = jsFormData.maxNumber ?: 0
+                val orgType = jsFormData.orgType ?: ""
+                val pickedGroups = jsFormData.pickedGroups ?: ArrayList()
+                val pickedDepartments = jsFormData.pickedDepartments ?: ArrayList()
+                val pickedIdentities = jsFormData.pickedIdentities ?: ArrayList()
+                val pickedUsers = jsFormData.pickedUsers ?: ArrayList()
+                ContactPickerActivity.startPickerBundle(
+                        pickMode,
+                        dutyList = dutyList,
+                        multiple = multiple,
+                        topUnitList = topList,
+                        maxNumber = maxNumber,
+                        unitType = orgType,
+                        initDeptList = pickedDepartments,
+                        initGroupList = pickedGroups,
+                        initIdList = pickedIdentities,
+                        initUserList = pickedUsers
+                )
+            }else {
+                ContactPickerActivity.startPickerBundle(ArrayList())
+            }
+            showPicker( bundle, callback)
+        } else {
+            XLog.error("activity不存在 complexPicker 失败!!")
+        }
+    }
+
+    private fun showPicker(bundle: Bundle, callback: String?) {
+        activity!!.runOnUiThread {
+            ActivityResult.of(activity)
+                    .className(ContactPickerActivity::class.java)
+                    .params(bundle)
+                    .greenChannel().forResult { _, data ->
+                        val result = data?.getParcelableExtra<ContactPickerResult>(ContactPickerActivity.CONTACT_PICKED_RESULT)
+                        if (result != null) {
+                            if (!TextUtils.isEmpty(callback)) {
+                                val resultJson = gson.toJson(result)
+                                XLog.debug("返回json:$resultJson")
+                                callbackJs("$callback('$resultJson')")
+                            }
+                        }
+                    }
+        }
+    }
+
+}

+ 3 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/webview/TaskWebViewActivity.kt

@@ -94,6 +94,7 @@ class TaskWebViewActivity : BaseMVPActivity<TaskWebViewContract.View, TaskWebVie
     var imageUploadData: O2UploadImageData? = null
     private val jsNotification: JSInterfaceO2mNotification by lazy { JSInterfaceO2mNotification.with(this) }
     private val jsUtil: JSInterfaceO2mUtil by lazy { JSInterfaceO2mUtil.with(this) }
+    private val jsBiz: JSInterfaceO2mBiz by lazy { JSInterfaceO2mBiz.with(this) }
 
 
 
@@ -120,8 +121,10 @@ class TaskWebViewActivity : BaseMVPActivity<TaskWebViewContract.View, TaskWebVie
         web_view.addJavascriptInterface(this, "o2android")
         jsNotification.setupWebView(web_view)
         jsUtil.setupWebView(web_view)
+        jsBiz.setupWebView(web_view)
         web_view.addJavascriptInterface(jsNotification, JSInterfaceO2mNotification.JSInterfaceName)
         web_view.addJavascriptInterface(jsUtil, JSInterfaceO2mUtil.JSInterfaceName)
+        web_view.addJavascriptInterface(jsBiz, JSInterfaceO2mBiz.JSInterfaceName)
         web_view.webChromeClient = webChromeClient
         web_view.webViewClient = object : WebViewClient() {
             override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {

+ 54 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/core/component/adapter/ContactComplexPickerListAdapter.kt

@@ -0,0 +1,54 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter
+
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.NewContactListVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.inflate
+
+/**
+ * Created by fancy on 2017/4/25.
+ */
+
+abstract class ContactComplexPickerListAdapter(var items: ArrayList<NewContactListVO>) : RecyclerView.Adapter<CommonRecyclerViewHolder>() {
+
+    override fun getItemCount(): Int {
+        return items.size
+    }
+
+    override fun onBindViewHolder(holder: CommonRecyclerViewHolder?, position: Int) {
+        when(items[position]) {
+            is NewContactListVO.Department -> {
+                val department = items[position] as NewContactListVO.Department
+                bindDepartment(holder, department, position)
+                holder?.convertView?.setOnClickListener { view -> clickDepartment(view, department) }
+            }
+            else -> {
+                val identity = items[position] as NewContactListVO.Identity
+                bindIdentity(holder, identity, position)
+                holder?.convertView?.setOnClickListener { view -> clickIdentity(view, identity) }
+            }
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): CommonRecyclerViewHolder {
+        return when(viewType) {
+            0 -> CommonRecyclerViewHolder(parent?.inflate(R.layout.item_contact_complex_picker_org))
+            else -> CommonRecyclerViewHolder(parent?.inflate(R.layout.item_contact_complex_picker_identity))
+        }
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        return  when(items[position]){
+            is NewContactListVO.Department -> 0
+            else -> 1
+        }
+    }
+
+
+    abstract fun bindDepartment(hold: CommonRecyclerViewHolder?, department: NewContactListVO.Department, position: Int)
+    abstract fun clickDepartment(view:View, department: NewContactListVO.Department)
+    abstract fun bindIdentity(hold: CommonRecyclerViewHolder?, identity: NewContactListVO.Identity, position: Int)
+    abstract fun clickIdentity(view:View, identity: NewContactListVO.Identity)
+}

+ 44 - 1
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/model/vo/O2JsPostMessage.kt

@@ -18,4 +18,47 @@ class O2UtilDatePickerMessage(var value: String?,
                        var startDate: String?,
                        var endDate: String?)
 
-class O2UtilNavigationMessage(var title: String?)
+class O2UtilNavigationMessage(var title: String?)
+
+
+class O2BizComplexPickerMessage(
+        var pickMode: ArrayList<String>?,
+        var topList: ArrayList<String>?,
+        var multiple: Boolean?,
+        var maxNumber: Int?,
+        var pickedIdentities: ArrayList<String>?,
+        var pickedDepartments: ArrayList<String>?,
+        var pickedGroups: ArrayList<String>?,
+        var pickedUsers: ArrayList<String>?,
+        var duty: ArrayList<String>?,
+        var orgType: String?
+)
+
+class O2BizIdentityPickerMessage(
+        var topList: ArrayList<String>?,
+        var multiple: Boolean?,
+        var maxNumber: Int?,
+        var pickedIdentities: ArrayList<String>?,
+        var duty: ArrayList<String>?
+)
+
+class O2BizUnitPickerMessage(
+        var topList: ArrayList<String>?,
+        var multiple: Boolean?,
+        var maxNumber: Int?,
+        var pickedDepartments: ArrayList<String>?,
+        var orgType: String?
+)
+
+class O2BizGroupPickerMessage(
+        var multiple: Boolean?,
+        var maxNumber: Int?,
+        var pickedGroups: ArrayList<String>?
+)
+
+
+class O2BizPersonPickerMessage(
+        var multiple: Boolean?,
+        var maxNumber: Int?,
+        var pickedUsers: ArrayList<String>?
+)

+ 2 - 1
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/utils/StringUtil.java

@@ -32,7 +32,8 @@ public class StringUtil {
      * @return
      */
     public static boolean isUrl(String str) {
-        String regex = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";
+//        String regex = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";
+        String regex = "((http|ftp|https)://)(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,5})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?";
         return match(regex, str);
     }
 

+ 20 - 0
o2android/app/src/main/res/layout/activity_contact_picker.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".app.o2.organization.ContactPickerActivity">
+    <include layout="@layout/snippet_appbarlayout_tablayout_toolbar" />
+
+    <FrameLayout
+        android:id="@+id/frame_contact_picker_main"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/app_bar_layout_snippet"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+</android.support.constraint.ConstraintLayout>

+ 20 - 0
o2android/app/src/main/res/layout/fragment_person_group_picker.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.RecyclerViewSwipeRefreshLayout
+        android:id="@+id/swipe_refresh_contact_person_group_picker_main"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent">
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/rv_contact_person_group_picker_main"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@android:color/white"/>
+    </net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.RecyclerViewSwipeRefreshLayout>
+</android.support.constraint.ConstraintLayout>

+ 59 - 0
o2android/app/src/main/res/layout/fragment_unit_identity_picker.xml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:background="@color/z_color_background"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <LinearLayout
+        android:id="@+id/ll_contact_complex_picker_breadcrumb_bar"
+        android:layout_width="match_parent"
+        android:layout_height="40dp"
+        android:layout_marginTop="@dimen/spacing_small"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:orientation="vertical">
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/z_color_split_line_ddd" />
+        <HorizontalScrollView
+            android:id="@+id/hs_contact_complex_picker_breadcrumb_scroll"
+            android:layout_width="match_parent"
+            android:layout_height="39dp"
+            android:background="@android:color/white">
+            <LinearLayout
+                android:id="@+id/ll_contact_complex_picker_breadcrumb_layout"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
+                android:paddingStart="15dp"
+                android:paddingEnd="10dp">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/tab_contact"
+                    android:textColor="@color/z_color_text_primary_dark"
+                    android:textSize="15sp" />
+            </LinearLayout>
+        </HorizontalScrollView>
+    </LinearLayout>
+
+    <android.support.v4.widget.SwipeRefreshLayout
+        android:id="@+id/swipe_refresh_contact_complex_picker_main"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toBottomOf="@+id/ll_contact_complex_picker_breadcrumb_bar"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent">
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/rv_contact_complex_picker_main"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@android:color/white"/>
+    </android.support.v4.widget.SwipeRefreshLayout>
+
+</android.support.constraint.ConstraintLayout>

+ 69 - 0
o2android/app/src/main/res/layout/item_contact_complex_picker_identity.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:background="@color/white">
+    <RelativeLayout
+        android:id="@+id/rl_item_contact_complex_picker_identity_top_gap"
+        android:visibility="gone"
+        android:background="@color/z_color_background"
+        android:layout_width="match_parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:layout_height="10dp">
+        <View
+            android:layout_alignParentTop="true"
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/z_color_split_line_ddd" />
+        <View
+            android:layout_alignParentBottom="true"
+            android:layout_width="match_parent"
+            android:layout_height="0.5dp"
+            android:background="@color/z_color_split_line_ddd" />
+    </RelativeLayout>
+
+    <CheckBox
+        android:id="@+id/check_item_contact_complex_picker_identity_select"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/rl_item_contact_complex_picker_identity_top_gap"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_margin="@dimen/spacing_normal" />
+
+    <net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleImageView
+        android:id="@+id/image_item_contact_complex_picker_identity_icon"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        app:layout_constraintStart_toEndOf="@+id/check_item_contact_complex_picker_identity_select"
+        app:layout_constraintTop_toBottomOf="@+id/rl_item_contact_complex_picker_identity_top_gap"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:src="@mipmap/icon_avatar_men" />
+
+    <TextView
+        android:id="@+id/tv_item_contact_complex_picker_identity_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/spacing_small"
+        app:layout_constraintStart_toEndOf="@+id/image_item_contact_complex_picker_identity_icon"
+        app:layout_constraintTop_toBottomOf="@+id/rl_item_contact_complex_picker_identity_top_gap"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:textColor="@color/z_color_text_primary_dark"
+        android:textSize="@dimen/font_normal"
+        tools:text="@string/contact_list_name" />
+
+    <View
+        android:id="@+id/view_item_contact_complex_picker_identity_divider"
+        android:layout_width="0dp"
+        android:layout_height="0.5dp"
+        android:visibility="visible"
+        app:layout_constraintStart_toEndOf="@+id/check_item_contact_complex_picker_identity_select"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:background="@color/z_color_split_line_ddd" />
+
+</android.support.constraint.ConstraintLayout>

+ 73 - 0
o2android/app/src/main/res/layout/item_contact_complex_picker_org.xml

@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:background="@color/white">
+    <CheckBox
+        android:id="@+id/check_item_contact_complex_picker_org_body"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_marginStart="@dimen/spacing_normal"
+        android:layout_marginTop="@dimen/spacing_small"
+        android:layout_marginBottom="@dimen/spacing_small"
+        android:visibility="gone"
+        />
+    <View
+        android:id="@+id/view_item_contact_complex_picker_org_body_divider"
+        android:layout_width="0dp"
+        android:layout_height="0.5dp"
+        android:visibility="visible"
+        app:layout_constraintStart_toEndOf="@+id/check_item_contact_complex_picker_org_body"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:background="@color/z_color_split_line_ddd" />
+
+    <net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleTextView
+        android:id="@+id/image_item_contact_complex_picker_org_body_icon"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        app:layout_constraintStart_toEndOf="@+id/check_item_contact_complex_picker_org_body"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_marginStart="@dimen/spacing_normal"
+        android:layout_marginTop="@dimen/spacing_small"
+        android:layout_marginBottom="@dimen/spacing_small"
+        app:c_width="40dp"
+        app:c_height="40dp"
+        app:c_text="X"
+        app:c_textColor="@color/z_color_white"
+        app:c_inColor="@color/z_color_primary"/>
+
+    <TextView
+        android:id="@+id/tv_item_contact_complex_picker_org_body_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/spacing_small"
+        app:layout_constraintStart_toEndOf="@+id/image_item_contact_complex_picker_org_body_icon"
+        app:layout_constraintEnd_toStartOf="@+id/btn_item_contact_complex_picker_org_body_next"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:textColor="@color/z_color_text_primary_dark"
+        android:textSize="@dimen/font_normal"
+        tools:text="@string/contact_list_depart" />
+
+
+    <Button
+        android:id="@+id/btn_item_contact_complex_picker_org_body_next"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="?android:attr/borderlessButtonStyle"
+        app:layout_constraintStart_toEndOf="@+id/tv_item_contact_complex_picker_org_body_name"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:text="下级"
+        android:textColor="@color/icon_blue"
+        android:drawableEnd="@mipmap/icon_arrow_22dp"
+        />
+</android.support.constraint.ConstraintLayout>

+ 2 - 2
o2android/gradle.properties

@@ -20,8 +20,8 @@ org.gradle.parallel=true
 
 
 # o2
-o2.versionName=4.9.3
-o2.versionCode=93
+o2.versionName=4.9.4
+o2.versionCode=94
 
 
 # sjgj

+ 64 - 0
o2ios/O2Platform.xcodeproj/project.pbxproj

@@ -93,6 +93,14 @@
 		B130E64D223B778000B68354 /* shared_preferences.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B130E647223B774D00B68354 /* shared_preferences.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		B130E64E223B778000B68354 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B130E60D223B774400B68354 /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		B13707DD212D3F19002113F1 /* CMSCategoryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B13707DC212D3F19002113F1 /* CMSCategoryData.swift */; };
+		B142B081230FB56400E7D127 /* MimeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B142B080230FB56400E7D127 /* MimeType.swift */; };
+		B142B083230FB58B00E7D127 /* Swime.swift in Sources */ = {isa = PBXBuildFile; fileRef = B142B082230FB58B00E7D127 /* Swime.swift */; };
+		B14E07532301137F00AE85A0 /* ContactPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E07522301137F00AE85A0 /* ContactPickerViewController.swift */; };
+		B14E07862301418400AE85A0 /* ContactUnitPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E07852301418400AE85A0 /* ContactUnitPickerViewController.swift */; };
+		B14E07882301419500AE85A0 /* ContactIdentityPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E07872301419500AE85A0 /* ContactIdentityPickerViewController.swift */; };
+		B14E078A230141AC00AE85A0 /* ContactGroupPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E0789230141AC00AE85A0 /* ContactGroupPickerViewController.swift */; };
+		B14E078C230141BE00AE85A0 /* ContactPersonPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E078B230141BE00AE85A0 /* ContactPersonPickerViewController.swift */; };
+		B14E07C523025C6A00AE85A0 /* OOContactExpressAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E07C423025C6A00AE85A0 /* OOContactExpressAPI.swift */; };
 		B1534E3F21F712EA00CC8C35 /* O2DemoAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1534E3E21F712EA00CC8C35 /* O2DemoAlertView.swift */; };
 		B158E95E215DD3F500AB2727 /* AIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B158E95D215DD3F500AB2727 /* AIConstants.swift */; };
 		B15F80F3210EB93000B81F35 /* OOCalendarMainMonthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B15F80F2210EB93000B81F35 /* OOCalendarMainMonthViewController.swift */; };
@@ -160,6 +168,12 @@
 		B1EE2CD02281771600842F48 /* O2JsApiUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE2CCF2281771600842F48 /* O2JsApiUtil.swift */; };
 		B1FAE9E12115F95800981A25 /* OOCalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FAE9E02115F95800981A25 /* OOCalendarViewController.swift */; };
 		B1FAFD442105C23B008A0CDF /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FAFD432105C23B008A0CDF /* Operators.swift */; };
+		B1FB9FAE2302891F00A90722 /* ContactPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FB9FAD2302891F00A90722 /* ContactPickerViewModel.swift */; };
+		B1FB9FE123029EC900A90722 /* UnitBreadcrumbViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FB9FE023029EC900A90722 /* UnitBreadcrumbViewCell.swift */; };
+		B1FB9FE32302A93500A90722 /* UnitPickerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FB9FE22302A93500A90722 /* UnitPickerTableViewCell.swift */; };
+		B1FB9FE72304EF7900A90722 /* GroupPickerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FB9FE62304EF7900A90722 /* GroupPickerTableViewCell.swift */; };
+		B1FBA01A230533FA00A90722 /* PersonPickerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FBA019230533FA00A90722 /* PersonPickerTableViewCell.swift */; };
+		B1FBA01D230A3AB500A90722 /* O2JsApiBizUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FBA01C230A3AB500A90722 /* O2JsApiBizUtil.swift */; };
 		B1FD027021FAE00E000E9817 /* O2MainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FD026F21FAE00E000E9817 /* O2MainController.swift */; };
 		B1FDBE8D22952F0C00BB434E /* O2WorkMoreActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FDBE8C22952F0C00BB434E /* O2WorkMoreActionSheet.swift */; };
 		B1FEDA9B22D305C90002ECF4 /* CMSSingleApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FEDA9A22D305C90002ECF4 /* CMSSingleApplication.swift */; };
@@ -1411,6 +1425,14 @@
 		B130E646223B774D00B68354 /* path_provider.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = path_provider.framework; path = plugins/path_provider.framework; sourceTree = "<group>"; };
 		B130E647223B774D00B68354 /* shared_preferences.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared_preferences.framework; path = plugins/shared_preferences.framework; sourceTree = "<group>"; };
 		B13707DC212D3F19002113F1 /* CMSCategoryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMSCategoryData.swift; sourceTree = "<group>"; };
+		B142B080230FB56400E7D127 /* MimeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MimeType.swift; sourceTree = "<group>"; };
+		B142B082230FB58B00E7D127 /* Swime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swime.swift; sourceTree = "<group>"; };
+		B14E07522301137F00AE85A0 /* ContactPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPickerViewController.swift; sourceTree = "<group>"; };
+		B14E07852301418400AE85A0 /* ContactUnitPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUnitPickerViewController.swift; sourceTree = "<group>"; };
+		B14E07872301419500AE85A0 /* ContactIdentityPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactIdentityPickerViewController.swift; sourceTree = "<group>"; };
+		B14E0789230141AC00AE85A0 /* ContactGroupPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactGroupPickerViewController.swift; sourceTree = "<group>"; };
+		B14E078B230141BE00AE85A0 /* ContactPersonPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPersonPickerViewController.swift; sourceTree = "<group>"; };
+		B14E07C423025C6A00AE85A0 /* OOContactExpressAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOContactExpressAPI.swift; sourceTree = "<group>"; };
 		B1534E3E21F712EA00CC8C35 /* O2DemoAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = O2DemoAlertView.swift; sourceTree = "<group>"; };
 		B158E95D215DD3F500AB2727 /* AIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIConstants.swift; sourceTree = "<group>"; };
 		B15F80F2210EB93000B81F35 /* OOCalendarMainMonthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOCalendarMainMonthViewController.swift; sourceTree = "<group>"; };
@@ -1477,6 +1499,12 @@
 		B1EE2CCF2281771600842F48 /* O2JsApiUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = O2JsApiUtil.swift; sourceTree = "<group>"; };
 		B1FAE9E02115F95800981A25 /* OOCalendarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOCalendarViewController.swift; sourceTree = "<group>"; };
 		B1FAFD432105C23B008A0CDF /* Operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = "<group>"; };
+		B1FB9FAD2302891F00A90722 /* ContactPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPickerViewModel.swift; sourceTree = "<group>"; };
+		B1FB9FE023029EC900A90722 /* UnitBreadcrumbViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitBreadcrumbViewCell.swift; sourceTree = "<group>"; };
+		B1FB9FE22302A93500A90722 /* UnitPickerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitPickerTableViewCell.swift; sourceTree = "<group>"; };
+		B1FB9FE62304EF7900A90722 /* GroupPickerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupPickerTableViewCell.swift; sourceTree = "<group>"; };
+		B1FBA019230533FA00A90722 /* PersonPickerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonPickerTableViewCell.swift; sourceTree = "<group>"; };
+		B1FBA01C230A3AB500A90722 /* O2JsApiBizUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = O2JsApiBizUtil.swift; sourceTree = "<group>"; };
 		B1FD026F21FAE00E000E9817 /* O2MainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = O2MainController.swift; sourceTree = "<group>"; };
 		B1FDBE8C22952F0C00BB434E /* O2WorkMoreActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = O2WorkMoreActionSheet.swift; sourceTree = "<group>"; };
 		B1FEDA9A22D305C90002ECF4 /* CMSSingleApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMSSingleApplication.swift; sourceTree = "<group>"; };
@@ -2574,6 +2602,15 @@
 			path = SwiftTheme;
 			sourceTree = "<group>";
 		};
+		B142B04E230FB53E00E7D127 /* MagicBytesMimeType */ = {
+			isa = PBXGroup;
+			children = (
+				B142B080230FB56400E7D127 /* MimeType.swift */,
+				B142B082230FB58B00E7D127 /* Swime.swift */,
+			);
+			path = MagicBytesMimeType;
+			sourceTree = "<group>";
+		};
 		B165CC9D2241DB9F00373B66 /* DatePickerDialogSwift */ = {
 			isa = PBXGroup;
 			children = (
@@ -2706,6 +2743,7 @@
 				B19FF89D226DBD3600FA0B76 /* O2JsApiNotification.swift */,
 				B1015DDA2272B5DB00C1A7E6 /* O2BaseJsMessageHandler.swift */,
 				B1EE2CCF2281771600842F48 /* O2JsApiUtil.swift */,
+				B1FBA01C230A3AB500A90722 /* O2JsApiBizUtil.swift */,
 			);
 			path = o2JsApi;
 			sourceTree = "<group>";
@@ -3324,6 +3362,7 @@
 			isa = PBXGroup;
 			children = (
 				E4B6978320764A2B0062F6E8 /* OOContactAPI.swift */,
+				B14E07C423025C6A00AE85A0 /* OOContactExpressAPI.swift */,
 			);
 			path = ContactAPI;
 			sourceTree = "<group>";
@@ -3748,6 +3787,7 @@
 		E4B887821D9D48F1002E1A46 /* framework */ = {
 			isa = PBXGroup;
 			children = (
+				B142B04E230FB53E00E7D127 /* MagicBytesMimeType */,
 				B108F3C3229E34B700778050 /* scan */,
 				B12FD1892283D5B700E636BA /* SwiftTheme */,
 				B165CD402242093500373B66 /* Presentr */,
@@ -4818,6 +4858,7 @@
 				E4C24C26208D7EDE00E426B0 /* OOLinkManViewModel.swift */,
 				E4C24C27208D7EDE00E426B0 /* OOListUnitViewModel.swift */,
 				E4F45448208F00A7002FBC32 /* OOPersonListViewModel.swift */,
+				B1FB9FAD2302891F00A90722 /* ContactPickerViewModel.swift */,
 			);
 			path = ViewModel;
 			sourceTree = "<group>";
@@ -4825,6 +4866,7 @@
 		E4C24C28208D7EDE00E426B0 /* Controller */ = {
 			isa = PBXGroup;
 			children = (
+				B14E07522301137F00AE85A0 /* ContactPickerViewController.swift */,
 				E4C24C29208D7EDE00E426B0 /* OOLinkeManViewController.swift */,
 				E4C24C2A208D7EDE00E426B0 /* OOContactMainViewController.swift */,
 				E4C24C2B208D7EDE00E426B0 /* OOUISearchController.swift */,
@@ -4832,6 +4874,10 @@
 				E4C24C2D208D7EDE00E426B0 /* OOContactSearchResultController.swift */,
 				E4F45444208EC359002FBC32 /* OOPersonsViewController.swift */,
 				E4F45445208EC359002FBC32 /* OOPersonsViewController.xib */,
+				B14E07852301418400AE85A0 /* ContactUnitPickerViewController.swift */,
+				B14E07872301419500AE85A0 /* ContactIdentityPickerViewController.swift */,
+				B14E0789230141AC00AE85A0 /* ContactGroupPickerViewController.swift */,
+				B14E078B230141BE00AE85A0 /* ContactPersonPickerViewController.swift */,
 			);
 			path = Controller;
 			sourceTree = "<group>";
@@ -4865,6 +4911,10 @@
 				E4D2310E209C40F600837868 /* OOPersonCollectionViewCell.xib */,
 				E4D23111209C42E700837868 /* OOSelectPersonTableViewCell.swift */,
 				E4D23112209C42E700837868 /* OOSelectPersonTableViewCell.xib */,
+				B1FB9FE023029EC900A90722 /* UnitBreadcrumbViewCell.swift */,
+				B1FB9FE22302A93500A90722 /* UnitPickerTableViewCell.swift */,
+				B1FB9FE62304EF7900A90722 /* GroupPickerTableViewCell.swift */,
+				B1FBA019230533FA00A90722 /* PersonPickerTableViewCell.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -5820,6 +5870,7 @@
 				B190923521071777009A7906 /* OOCalendarModels.swift in Sources */,
 				B17BF43022B23758009E36E0 /* SkinViewController.swift in Sources */,
 				E40E24D520B7DA3C009F8BE7 /* OMeetingPersonActionCell.swift in Sources */,
+				B14E07862301418400AE85A0 /* ContactUnitPickerViewController.swift in Sources */,
 				B107453F21A533E70015F1B2 /* O2PersonalViewModel.swift in Sources */,
 				E4B888A11D9D48F1002E1A46 /* MJRefreshAutoNormalFooter.m in Sources */,
 				E4B698242079AA0D0062F6E8 /* OOTableViewController.swift in Sources */,
@@ -5874,9 +5925,11 @@
 				B1FAFD442105C23B008A0CDF /* Operators.swift in Sources */,
 				E4C24B9520844F3C00E426B0 /* JCConversationCell.swift in Sources */,
 				E45DA8F71DADC04E00E0735D /* TaskUIToolbar.swift in Sources */,
+				B14E07532301137F00AE85A0 /* ContactPickerViewController.swift in Sources */,
 				E4C24BAD20844F3C00E426B0 /* JCGroupSettingViewController.swift in Sources */,
 				09E02E921F16319600579887 /* NSHTTPURLResponse+Haneke.swift in Sources */,
 				E4C24BA720844F3C00E426B0 /* FileManagerViewController.swift in Sources */,
+				B14E07C523025C6A00AE85A0 /* OOContactExpressAPI.swift in Sources */,
 				B130A74F2281559900282AD1 /* DeviceListViewController.swift in Sources */,
 				E4B888631D9D48F1002E1A46 /* ZLNavigationController.swift in Sources */,
 				E4B889021D9D48F1002E1A46 /* TodoedStatusCell.swift in Sources */,
@@ -5966,6 +6019,7 @@
 				E4C24B5520844F3C00E426B0 /* SAIToolboxInputView.swift in Sources */,
 				E4C24B5320844F3C00E426B0 /* SAIInputView.swift in Sources */,
 				E40E24CF20B7DA3C009F8BE7 /* OOFormDateItemView.swift in Sources */,
+				B1FBA01D230A3AB500A90722 /* O2JsApiBizUtil.swift in Sources */,
 				E4B69776207630240062F6E8 /* UIView+Extension.swift in Sources */,
 				E4C24BB420844F3C00E426B0 /* JCMessageContentViewType.swift in Sources */,
 				E4C24B9420844F3C00E426B0 /* JCGroupSettingCell.swift in Sources */,
@@ -5994,6 +6048,7 @@
 				B12FD1E62283D5B700E636BA /* ThemeActivityIndicatorViewStylePicker.swift in Sources */,
 				E40E246B20B68AEB009F8BE7 /* OOAttandanceTotalHeaderView.swift in Sources */,
 				09E02E0A1F14B1C600579887 /* ContactPersonInfoV2ViewController.swift in Sources */,
+				B14E07882301419500AE85A0 /* ContactIdentityPickerViewController.swift in Sources */,
 				E4AA17171EE189FE0030D9AB /* LogFileTableViewCell.swift in Sources */,
 				E4B6976E207630240062F6E8 /* UIButton+Extension.swift in Sources */,
 				E4C24BCA20844F3C00E426B0 /* JCJChatInfoCell.swift in Sources */,
@@ -6012,6 +6067,7 @@
 				E4C24B4520844F3C00E426B0 /* Extensions.swift in Sources */,
 				E4B781731DF912E6007B58A9 /* CMSCategoryListViewController.swift in Sources */,
 				B12FD1E02283D5B700E636BA /* ThemeCGColorPicker.swift in Sources */,
+				B1FBA01A230533FA00A90722 /* PersonPickerTableViewCell.swift in Sources */,
 				E4B6981A2079A8BB0062F6E8 /* OOBindNodeViewController.swift in Sources */,
 				E4F4544F20902263002FBC32 /* OOConfigInfoModels.swift in Sources */,
 				E4B2321A20B3E9440082F30A /* OOAttanceCheckinPromptView.swift in Sources */,
@@ -6096,6 +6152,7 @@
 				E4B8886A1D9D48F1002E1A46 /* O2URLContext.swift in Sources */,
 				E45DA8F31DACD7CA00E0735D /* SegmentedControl.swift in Sources */,
 				E4C24B4020844F3C00E426B0 /* JMSGConversation+.swift in Sources */,
+				B1FB9FE123029EC900A90722 /* UnitBreadcrumbViewCell.swift in Sources */,
 				E4B697002075DE5F0062F6E8 /* OOGuidePageController.swift in Sources */,
 				E4B888F81D9D48F1002E1A46 /* CreateProcessBean.swift in Sources */,
 				E40502C920722208009A8D30 /* ImageRow.swift in Sources */,
@@ -6125,6 +6182,7 @@
 				E4C24B7920844F3C00E426B0 /* GroupAvatorCell.swift in Sources */,
 				09E02E8F1F16319600579887 /* Log.swift in Sources */,
 				E4B888701D9D48F1002E1A46 /* ContactPersonInfoController.swift in Sources */,
+				B142B083230FB58B00E7D127 /* Swime.swift in Sources */,
 				E4C24BB920844F3C00E426B0 /* JCEmoticonInputViewLayout.swift in Sources */,
 				E4C24C45208D7EDE00E426B0 /* OOListUnitViewController.swift in Sources */,
 				E4B6970D2075FA560062F6E8 /* OOUISwitch.swift in Sources */,
@@ -6235,6 +6293,7 @@
 				E40E24B320B7DA3C009F8BE7 /* OOMeetingMeetingRoomManageController.swift in Sources */,
 				E42700A81E7A762200DCCC71 /* ZoneHUD.swift in Sources */,
 				E40E24D820B7DA3C009F8BE7 /* OOMeetingPersonFooterView.swift in Sources */,
+				B14E078A230141AC00AE85A0 /* ContactGroupPickerViewController.swift in Sources */,
 				E4B697C520764A2D0062F6E8 /* O2InformationAPI.swift in Sources */,
 				E4CB276F1E78D5B1004A7ACB /* String+Convenience.swift in Sources */,
 				E4C24C0C20844F4400E426B0 /* JCIdentityVerificationViewController.swift in Sources */,
@@ -6309,6 +6368,7 @@
 				B15F80F3210EB93000B81F35 /* OOCalendarMainMonthViewController.swift in Sources */,
 				E4C24BC720844F3C00E426B0 /* JCMainTabBarController.swift in Sources */,
 				B165CD6C2242093500373B66 /* Presentr+Equatable.swift in Sources */,
+				B14E078C230141BE00AE85A0 /* ContactPersonPickerViewController.swift in Sources */,
 				B12FD1D62283D5B700E636BA /* ThemeBarStylePicker.swift in Sources */,
 				E4B697152075FA560062F6E8 /* OOBaseUIButton.swift in Sources */,
 				E4B6974620761B310062F6E8 /* OOBindRegisterController.swift in Sources */,
@@ -6346,7 +6406,9 @@
 				E45DA8F41DACD7CA00E0735D /* Style.swift in Sources */,
 				E4A748D31EC1598600163F58 /* O2TaskAttachmentInfoData.swift in Sources */,
 				E4C24BDD20844F3C00E426B0 /* UserProtocols.swift in Sources */,
+				B1FB9FAE2302891F00A90722 /* ContactPickerViewModel.swift in Sources */,
 				B12FD1EF228425BE00E636BA /* OOMeetingTabBarController.swift in Sources */,
+				B1FB9FE32302A93500A90722 /* UnitPickerTableViewCell.swift in Sources */,
 				B130A7832281565F00282AD1 /* DeviceTableViewCell.swift in Sources */,
 				E4CB278B1E797B58004A7ACB /* CMSQLViewController.swift in Sources */,
 				B1C1905D2114410D00935829 /* CalendarTableViewCell.swift in Sources */,
@@ -6453,6 +6515,7 @@
 				E4C24B3C20844F3C00E426B0 /* Date+JChat.swift in Sources */,
 				E4C24C0120844F4400E426B0 /* JCUserInfoCell.swift in Sources */,
 				B1B2148F216073B400D9CA7E /* ScanHelper.swift in Sources */,
+				B142B081230FB56400E7D127 /* MimeType.swift in Sources */,
 				B104303C21469E3D0011B08E /* OOCircleRippleView.swift in Sources */,
 				E4B888FE1D9D48F1002E1A46 /* MainAppActionCell.swift in Sources */,
 				B165CD652242093500373B66 /* PresentationType.swift in Sources */,
@@ -6465,6 +6528,7 @@
 				E4B697ED20764A2D0062F6E8 /* OOTaskAPI.swift in Sources */,
 				E4C24B7120844F3C00E426B0 /* JCCEmoticon.swift in Sources */,
 				E4CB27731E78D5B1004A7ACB /* AppearanceConfigurator.swift in Sources */,
+				B1FB9FE72304EF7900A90722 /* GroupPickerTableViewCell.swift in Sources */,
 				E40958BD207F46F5000FECC3 /* OOPlusButtonSubclass.swift in Sources */,
 				E4B697E120764A2D0062F6E8 /* OOHotpicAPI.swift in Sources */,
 			);

+ 497 - 37
o2ios/O2Platform/Contact-通讯录/Contacts_new.storyboard

@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="8OP-2z-Uzo">
-    <device id="retina4_7" orientation="portrait">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="8OP-2z-Uzo">
+    <device id="retina6_1" orientation="portrait">
         <adaptation id="fullscreen"/>
     </device>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
-        <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
@@ -15,16 +15,16 @@
             <objects>
                 <tableViewController title="通讯录" id="8OP-2z-Uzo" customClass="OOContactMainViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
                     <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="40" sectionFooterHeight="1" id="3yJ-ar-ZUn">
-                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
                         <color key="separatorColor" red="0.87058823529411766" green="0.87058823529411766" blue="0.87058823529411766" alpha="1" colorSpace="calibratedRGB"/>
                         <prototypes>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="CDLCell" id="MwA-DB-j1b" customClass="OOCDLCell" customModule="O2Platform" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
+                                <rect key="frame" x="0.0" y="55.5" width="414" height="60"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MwA-DB-j1b" id="IQW-LB-vcT">
-                                    <rect key="frame" x="0.0" y="0.0" width="375" height="59.5"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="59.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ZY5-z1-vWD">
@@ -35,20 +35,20 @@
                                             </constraints>
                                         </imageView>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Il0-rw-nNB">
-                                            <rect key="frame" x="63" y="9" width="284" height="21"/>
+                                            <rect key="frame" x="63" y="9" width="323" height="21"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="14"/>
                                             <nil key="textColor"/>
                                             <nil key="highlightedColor"/>
                                         </label>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_arrow" translatesAutoresizingMaskIntoConstraints="NO" id="9vX-vc-Gtk">
-                                            <rect key="frame" x="347" y="18" width="22" height="22"/>
+                                            <rect key="frame" x="386" y="18.5" width="22" height="22"/>
                                             <constraints>
                                                 <constraint firstAttribute="height" constant="22" id="ONd-YF-0OL"/>
                                                 <constraint firstAttribute="width" constant="22" id="vZu-rB-2l0"/>
                                             </constraints>
                                         </imageView>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="stE-QI-fuX">
-                                            <rect key="frame" x="63" y="29" width="284" height="20"/>
+                                            <rect key="frame" x="63" y="29" width="323" height="20"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="14"/>
                                             <color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
                                             <nil key="highlightedColor"/>
@@ -78,10 +78,10 @@
                                 </connections>
                             </tableViewCell>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="LinkManCell" id="AOK-2P-s7R" customClass="OOLinkManCell" customModule="O2Platform" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="115.5" width="375" height="60"/>
+                                <rect key="frame" x="0.0" y="115.5" width="414" height="60"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="AOK-2P-s7R" id="XLE-gP-wUe">
-                                    <rect key="frame" x="0.0" y="0.0" width="375" height="59.5"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="59.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="fy5-7Y-5BU">
@@ -92,26 +92,26 @@
                                             </constraints>
                                         </imageView>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2Ba-6Q-Jpr">
-                                            <rect key="frame" x="63" y="9" width="284" height="21"/>
+                                            <rect key="frame" x="63" y="9" width="323" height="21"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="14"/>
                                             <nil key="textColor"/>
                                             <nil key="highlightedColor"/>
                                         </label>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fsc-wn-ytF">
-                                            <rect key="frame" x="63" y="29" width="284" height="20"/>
+                                            <rect key="frame" x="63" y="29" width="323" height="20"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="14"/>
                                             <color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
                                             <nil key="highlightedColor"/>
                                         </label>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_arrow" translatesAutoresizingMaskIntoConstraints="NO" id="kiT-3L-ylp">
-                                            <rect key="frame" x="347" y="18" width="22" height="22"/>
+                                            <rect key="frame" x="386" y="18.5" width="22" height="22"/>
                                             <constraints>
                                                 <constraint firstAttribute="width" constant="22" id="T8I-CV-K2l"/>
                                                 <constraint firstAttribute="height" constant="22" id="pl4-zL-0Ae"/>
                                             </constraints>
                                         </imageView>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="arrowTitle" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hmD-fB-Ul0">
-                                            <rect key="frame" x="262" y="18.5" width="90" height="21"/>
+                                            <rect key="frame" x="297" y="19" width="90" height="21"/>
                                             <constraints>
                                                 <constraint firstAttribute="width" constant="90" id="Leg-ay-UKD"/>
                                             </constraints>
@@ -167,17 +167,17 @@
             <objects>
                 <tableViewController title="单元列表" id="rQX-tn-jYk" customClass="OOListUnitViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
                     <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="SUq-mr-dk9">
-                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
                         <color key="separatorColor" red="0.87058823529411766" green="0.87058823529411766" blue="0.87058823529411766" alpha="1" colorSpace="calibratedRGB"/>
                         <inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
                         <prototypes>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="CDLCell" id="fsZ-LM-KKE" customClass="OOCDLCell" customModule="O2Platform" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
+                                <rect key="frame" x="0.0" y="55.5" width="414" height="60"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fsZ-LM-KKE" id="YyS-gj-RWI">
-                                    <rect key="frame" x="0.0" y="0.0" width="375" height="59.5"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="59.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="awb-ld-EiN">
@@ -188,19 +188,19 @@
                                             </constraints>
                                         </imageView>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="upc-z2-w90">
-                                            <rect key="frame" x="59" y="9" width="284" height="21"/>
+                                            <rect key="frame" x="59" y="9" width="323" height="21"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="14"/>
                                             <nil key="textColor"/>
                                             <nil key="highlightedColor"/>
                                         </label>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BTJ-1I-KSt">
-                                            <rect key="frame" x="59" y="29" width="284" height="20"/>
+                                            <rect key="frame" x="59" y="29" width="323" height="20"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="14"/>
                                             <color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
                                             <nil key="highlightedColor"/>
                                         </label>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_arrow" translatesAutoresizingMaskIntoConstraints="NO" id="5KE-hT-FWU">
-                                            <rect key="frame" x="345" y="19" width="22" height="22"/>
+                                            <rect key="frame" x="384" y="19" width="22" height="22"/>
                                             <constraints>
                                                 <constraint firstAttribute="height" constant="22" id="g1Y-Jy-atd"/>
                                                 <constraint firstAttribute="width" constant="22" id="kQB-Pg-rhi"/>
@@ -251,31 +251,469 @@
             </objects>
             <point key="canvasLocation" x="1404" y="-58"/>
         </scene>
+        <!--通讯录选择器-->
+        <scene sceneID="Hy8-WO-Uw9">
+            <objects>
+                <viewController storyboardIdentifier="contactPicker" title="通讯录选择器" id="fWb-Zb-wIl" customClass="ContactPickerViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="CLs-Fc-l3w">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7dB-9p-Tn1">
+                                <rect key="frame" x="0.0" y="88" width="414" height="48"/>
+                                <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="48" id="WkI-oY-R6b"/>
+                                </constraints>
+                                <userDefinedRuntimeAttributes>
+                                    <userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
+                                        <real key="value" value="0.0"/>
+                                    </userDefinedRuntimeAttribute>
+                                    <userDefinedRuntimeAttribute type="color" keyPath="borderColor">
+                                        <color key="value" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    </userDefinedRuntimeAttribute>
+                                </userDefinedRuntimeAttributes>
+                            </view>
+                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="7g2-Xw-0UC">
+                                <rect key="frame" x="10" y="88" width="394" height="48"/>
+                                <subviews>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vzN-FA-mmu">
+                                        <rect key="frame" x="0.0" y="0.0" width="128" height="48"/>
+                                        <color key="tintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                        <state key="normal" title="人员选择">
+                                            <color key="titleColor" red="0.98431372549999996" green="0.2784313725" blue="0.2784313725" alpha="1" colorSpace="calibratedRGB"/>
+                                        </state>
+                                    </button>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kfB-Kj-3Iv">
+                                        <rect key="frame" x="133" y="0.0" width="128" height="48"/>
+                                        <state key="normal" title="身份选择">
+                                            <color key="titleColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                        </state>
+                                    </button>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7eY-4g-WBB">
+                                        <rect key="frame" x="266" y="0.0" width="128" height="48"/>
+                                        <state key="normal" title="身份选择">
+                                            <color key="titleColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                        </state>
+                                    </button>
+                                </subviews>
+                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="48" id="Ab3-N3-nZB"/>
+                                </constraints>
+                            </stackView>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="N3U-aC-Z3u">
+                                <rect key="frame" x="0.0" y="136" width="414" height="726"/>
+                                <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            </view>
+                        </subviews>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <constraints>
+                            <constraint firstItem="7g2-Xw-0UC" firstAttribute="leading" secondItem="Nfr-Ie-jh7" secondAttribute="leading" constant="10" id="BfH-Oh-OFa"/>
+                            <constraint firstItem="Nfr-Ie-jh7" firstAttribute="trailing" secondItem="N3U-aC-Z3u" secondAttribute="trailing" id="NSd-7g-WED"/>
+                            <constraint firstItem="7g2-Xw-0UC" firstAttribute="top" secondItem="Nfr-Ie-jh7" secondAttribute="top" id="Usf-jq-WPL"/>
+                            <constraint firstItem="Nfr-Ie-jh7" firstAttribute="bottom" secondItem="N3U-aC-Z3u" secondAttribute="bottom" id="YR3-Ui-mZl"/>
+                            <constraint firstItem="7dB-9p-Tn1" firstAttribute="leading" secondItem="Nfr-Ie-jh7" secondAttribute="leading" id="bKR-vF-3tL"/>
+                            <constraint firstItem="N3U-aC-Z3u" firstAttribute="leading" secondItem="Nfr-Ie-jh7" secondAttribute="leading" id="ffn-vW-8vM"/>
+                            <constraint firstItem="N3U-aC-Z3u" firstAttribute="top" secondItem="7g2-Xw-0UC" secondAttribute="bottom" id="msp-Gp-UFs"/>
+                            <constraint firstItem="Nfr-Ie-jh7" firstAttribute="trailing" secondItem="7g2-Xw-0UC" secondAttribute="trailing" constant="10" id="pOe-7h-6wD"/>
+                            <constraint firstItem="7dB-9p-Tn1" firstAttribute="top" secondItem="Nfr-Ie-jh7" secondAttribute="top" id="u2Q-ZX-zTX"/>
+                            <constraint firstItem="Nfr-Ie-jh7" firstAttribute="trailing" secondItem="7dB-9p-Tn1" secondAttribute="trailing" id="xKL-e4-7Fu"/>
+                        </constraints>
+                        <viewLayoutGuide key="safeArea" id="Nfr-Ie-jh7"/>
+                    </view>
+                    <navigationItem key="navigationItem" id="gVF-jd-rv0"/>
+                    <connections>
+                        <outlet property="pickerContainerView" destination="N3U-aC-Z3u" id="fuD-qr-rJk"/>
+                        <outlet property="topBarStackView" destination="7g2-Xw-0UC" id="Esu-DE-ve2"/>
+                        <outlet property="topBarStackViewHeightConstraint" destination="Ab3-N3-nZB" id="vXJ-QF-tMA"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="GVi-v0-RB7" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-571.01449275362324" y="1439.7321428571429"/>
+        </scene>
+        <!--组织选择-->
+        <scene sceneID="LXL-41-8Kz">
+            <objects>
+                <tableViewController storyboardIdentifier="unitPicker" title="组织选择" id="cRj-mM-HCV" customClass="ContactUnitPickerViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="D4k-CF-Rqz">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="breadcrumbViewCell" id="qw3-ug-cog" customClass="UnitBreadcrumbViewCell" customModule="O2Platform" customModuleProvider="target">
+                                <rect key="frame" x="0.0" y="28" width="414" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="qw3-ug-cog" id="Li7-pS-wIr">
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6pW-Jm-gec">
+                                            <rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
+                                        </scrollView>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstItem="6pW-Jm-gec" firstAttribute="top" secondItem="Li7-pS-wIr" secondAttribute="top" id="IBe-ZN-mor"/>
+                                        <constraint firstAttribute="trailing" secondItem="6pW-Jm-gec" secondAttribute="trailing" constant="20" id="M9e-1e-2Yp"/>
+                                        <constraint firstAttribute="bottom" secondItem="6pW-Jm-gec" secondAttribute="bottom" id="MrY-W7-ZxW"/>
+                                        <constraint firstItem="6pW-Jm-gec" firstAttribute="leading" secondItem="Li7-pS-wIr" secondAttribute="leading" constant="20" id="UAQ-kK-dB2"/>
+                                    </constraints>
+                                </tableViewCellContentView>
+                                <connections>
+                                    <outlet property="breadcrumbScrollView" destination="6pW-Jm-gec" id="Dav-P5-fay"/>
+                                </connections>
+                            </tableViewCell>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="unitPickerViewCell" id="dqZ-6W-fez" customClass="UnitPickerTableViewCell" customModule="O2Platform" customModuleProvider="target">
+                                <rect key="frame" x="0.0" y="72" width="414" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="dqZ-6W-fez" id="uOI-mf-f6w">
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="unselected" translatesAutoresizingMaskIntoConstraints="NO" id="2M8-f3-RaO">
+                                            <rect key="frame" x="10" y="13.5" width="24" height="24"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="24" id="9cd-7n-Ghr"/>
+                                                <constraint firstAttribute="width" constant="24" id="rVd-ib-6UT"/>
+                                            </constraints>
+                                        </imageView>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pic_oval" translatesAutoresizingMaskIntoConstraints="NO" id="3G0-A9-nBq">
+                                            <rect key="frame" x="44" y="5" width="40" height="40"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="40" id="MOF-wx-Oya"/>
+                                                <constraint firstAttribute="width" constant="40" id="OZb-by-5hw"/>
+                                            </constraints>
+                                        </imageView>
+                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="无" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="azX-Gb-s7O">
+                                            <rect key="frame" x="44" y="15" width="40" height="21"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="40" id="lL0-EC-i8B"/>
+                                            </constraints>
+                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                            <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="部门名称" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xcZ-2o-vGn">
+                                            <rect key="frame" x="98" y="14.5" width="232" height="22"/>
+                                            <fontDescription key="fontDescription" type="system" pointSize="18"/>
+                                            <nil key="textColor"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                        <button opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9DY-QL-K8W">
+                                            <rect key="frame" x="340" y="8.5" width="64" height="34"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="64" id="l6r-1X-UMe"/>
+                                            </constraints>
+                                            <state key="normal" title="下级">
+                                                <color key="titleColor" red="0.0" green="0.5450980392" blue="0.90196078430000004" alpha="1" colorSpace="calibratedRGB"/>
+                                            </state>
+                                            <connections>
+                                                <action selector="clickNextBtn:" destination="dqZ-6W-fez" eventType="touchUpInside" id="xbb-5b-7ZD"/>
+                                            </connections>
+                                        </button>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstItem="9DY-QL-K8W" firstAttribute="centerY" secondItem="uOI-mf-f6w" secondAttribute="centerY" id="1g4-6F-uAV"/>
+                                        <constraint firstItem="azX-Gb-s7O" firstAttribute="leading" secondItem="2M8-f3-RaO" secondAttribute="trailing" constant="10" id="3K2-mP-5KQ"/>
+                                        <constraint firstItem="3G0-A9-nBq" firstAttribute="top" secondItem="uOI-mf-f6w" secondAttribute="top" constant="5" id="5Zr-qL-NPE"/>
+                                        <constraint firstItem="2M8-f3-RaO" firstAttribute="centerY" secondItem="uOI-mf-f6w" secondAttribute="centerY" id="6yE-5R-LA3"/>
+                                        <constraint firstItem="azX-Gb-s7O" firstAttribute="centerY" secondItem="uOI-mf-f6w" secondAttribute="centerY" id="Ad3-Kv-okA"/>
+                                        <constraint firstItem="9DY-QL-K8W" firstAttribute="leading" secondItem="xcZ-2o-vGn" secondAttribute="trailing" constant="10" id="Ky0-xV-zan"/>
+                                        <constraint firstAttribute="bottom" secondItem="3G0-A9-nBq" secondAttribute="bottom" constant="5" id="bRD-pz-VU4"/>
+                                        <constraint firstItem="xcZ-2o-vGn" firstAttribute="centerY" secondItem="uOI-mf-f6w" secondAttribute="centerY" id="fXt-z1-Ddp"/>
+                                        <constraint firstItem="2M8-f3-RaO" firstAttribute="leading" secondItem="uOI-mf-f6w" secondAttribute="leading" constant="10" id="fdd-kK-WhQ"/>
+                                        <constraint firstAttribute="trailing" secondItem="9DY-QL-K8W" secondAttribute="trailing" constant="10" id="lpj-3b-FGl"/>
+                                        <constraint firstItem="3G0-A9-nBq" firstAttribute="leading" secondItem="2M8-f3-RaO" secondAttribute="trailing" constant="10" id="tjt-od-2NM"/>
+                                        <constraint firstItem="xcZ-2o-vGn" firstAttribute="leading" secondItem="3G0-A9-nBq" secondAttribute="trailing" constant="14" id="uFX-rB-HVW"/>
+                                    </constraints>
+                                </tableViewCellContentView>
+                                <connections>
+                                    <outlet property="checkImageView" destination="2M8-f3-RaO" id="enH-8o-1aL"/>
+                                    <outlet property="nextLevelBtn" destination="9DY-QL-K8W" id="CWp-qw-l6i"/>
+                                    <outlet property="unitIconBgImageView" destination="3G0-A9-nBq" id="RX8-Dr-wpU"/>
+                                    <outlet property="unitIconLabel" destination="azX-Gb-s7O" id="CqP-bj-3Vh"/>
+                                    <outlet property="unitNameLabel" destination="xcZ-2o-vGn" id="Pli-dk-hgQ"/>
+                                </connections>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="cRj-mM-HCV" id="tSv-Rm-fa1"/>
+                            <outlet property="delegate" destination="cRj-mM-HCV" id="tPH-iL-0G0"/>
+                        </connections>
+                    </tableView>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="1Ef-2y-hay" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="324.63768115942031" y="1438.3928571428571"/>
+        </scene>
+        <!--身份选择-->
+        <scene sceneID="mDF-Oj-wgd">
+            <objects>
+                <tableViewController storyboardIdentifier="identityPicker" title="身份选择" id="1ZD-eI-RzP" customClass="ContactIdentityPickerViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="2pd-iS-yvM">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="breadcrumbViewCell" id="NMQ-LW-nCY" customClass="UnitBreadcrumbViewCell" customModule="O2Platform" customModuleProvider="target">
+                                <rect key="frame" x="0.0" y="28" width="414" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="NMQ-LW-nCY" id="aGf-jV-nsI">
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oKR-Ms-qhL">
+                                            <rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
+                                        </scrollView>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstItem="oKR-Ms-qhL" firstAttribute="top" secondItem="aGf-jV-nsI" secondAttribute="top" id="CU5-a2-kcW"/>
+                                        <constraint firstAttribute="trailing" secondItem="oKR-Ms-qhL" secondAttribute="trailing" constant="20" id="EAk-7u-D6I"/>
+                                        <constraint firstItem="oKR-Ms-qhL" firstAttribute="leading" secondItem="aGf-jV-nsI" secondAttribute="leading" constant="20" id="fwg-IQ-OjG"/>
+                                        <constraint firstAttribute="bottom" secondItem="oKR-Ms-qhL" secondAttribute="bottom" id="kNU-Ow-XzC"/>
+                                    </constraints>
+                                </tableViewCellContentView>
+                                <connections>
+                                    <outlet property="breadcrumbScrollView" destination="oKR-Ms-qhL" id="LAX-hu-yjz"/>
+                                </connections>
+                            </tableViewCell>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="unitPickerViewCell" id="3vG-Xz-LHH" customClass="UnitPickerTableViewCell" customModule="O2Platform" customModuleProvider="target">
+                                <rect key="frame" x="0.0" y="72" width="414" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3vG-Xz-LHH" id="lFJ-On-2II">
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="unselected" translatesAutoresizingMaskIntoConstraints="NO" id="JAj-Ri-DQZ">
+                                            <rect key="frame" x="10" y="13.5" width="24" height="24"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="24" id="hdI-qA-qo7"/>
+                                                <constraint firstAttribute="width" constant="24" id="kAb-bv-l0A"/>
+                                            </constraints>
+                                        </imageView>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pic_oval" translatesAutoresizingMaskIntoConstraints="NO" id="dZV-dr-NYp">
+                                            <rect key="frame" x="44" y="5" width="40" height="40"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="40" id="bdJ-8n-vzd"/>
+                                                <constraint firstAttribute="height" constant="40" id="y81-0P-wqu"/>
+                                            </constraints>
+                                        </imageView>
+                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="无" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wCo-bX-uP0">
+                                            <rect key="frame" x="44" y="15" width="40" height="21"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="40" id="326-Q2-fnP"/>
+                                            </constraints>
+                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                            <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="部门名称" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="be0-Hv-kNx">
+                                            <rect key="frame" x="98" y="14.5" width="232" height="22"/>
+                                            <fontDescription key="fontDescription" type="system" pointSize="18"/>
+                                            <nil key="textColor"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                        <button opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SuN-lj-nZ8">
+                                            <rect key="frame" x="340" y="8.5" width="64" height="34"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="64" id="35v-4M-tCB"/>
+                                            </constraints>
+                                            <state key="normal" title="下级">
+                                                <color key="titleColor" red="0.0" green="0.5450980392" blue="0.90196078430000004" alpha="1" colorSpace="calibratedRGB"/>
+                                            </state>
+                                            <connections>
+                                                <action selector="clickNextBtn:" destination="3vG-Xz-LHH" eventType="touchUpInside" id="WI2-SN-IAZ"/>
+                                            </connections>
+                                        </button>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstItem="wCo-bX-uP0" firstAttribute="centerY" secondItem="lFJ-On-2II" secondAttribute="centerY" id="2UF-0b-sdP"/>
+                                        <constraint firstItem="JAj-Ri-DQZ" firstAttribute="centerY" secondItem="lFJ-On-2II" secondAttribute="centerY" id="2hj-zJ-DKo"/>
+                                        <constraint firstItem="SuN-lj-nZ8" firstAttribute="leading" secondItem="be0-Hv-kNx" secondAttribute="trailing" constant="10" id="7Xn-wl-rJG"/>
+                                        <constraint firstAttribute="trailing" secondItem="SuN-lj-nZ8" secondAttribute="trailing" constant="10" id="9YV-1y-EPP"/>
+                                        <constraint firstItem="dZV-dr-NYp" firstAttribute="top" secondItem="lFJ-On-2II" secondAttribute="top" constant="5" id="Dsw-ez-EBS"/>
+                                        <constraint firstItem="dZV-dr-NYp" firstAttribute="leading" secondItem="JAj-Ri-DQZ" secondAttribute="trailing" constant="10" id="KHp-SN-daa"/>
+                                        <constraint firstItem="SuN-lj-nZ8" firstAttribute="centerY" secondItem="lFJ-On-2II" secondAttribute="centerY" id="Tzp-Nh-q3e"/>
+                                        <constraint firstItem="wCo-bX-uP0" firstAttribute="leading" secondItem="JAj-Ri-DQZ" secondAttribute="trailing" constant="10" id="Wgn-oG-Jp1"/>
+                                        <constraint firstItem="JAj-Ri-DQZ" firstAttribute="leading" secondItem="lFJ-On-2II" secondAttribute="leading" constant="10" id="X2I-UR-q8F"/>
+                                        <constraint firstAttribute="bottom" secondItem="dZV-dr-NYp" secondAttribute="bottom" constant="5" id="Xsc-C1-gaF"/>
+                                        <constraint firstItem="be0-Hv-kNx" firstAttribute="leading" secondItem="dZV-dr-NYp" secondAttribute="trailing" constant="14" id="j5Y-Ga-jpl"/>
+                                        <constraint firstItem="be0-Hv-kNx" firstAttribute="centerY" secondItem="lFJ-On-2II" secondAttribute="centerY" id="vL7-d1-73P"/>
+                                    </constraints>
+                                </tableViewCellContentView>
+                                <connections>
+                                    <outlet property="checkImageView" destination="JAj-Ri-DQZ" id="9He-P6-y3O"/>
+                                    <outlet property="nextLevelBtn" destination="SuN-lj-nZ8" id="vct-HF-wIi"/>
+                                    <outlet property="unitIconBgImageView" destination="dZV-dr-NYp" id="Zqz-rm-uFH"/>
+                                    <outlet property="unitIconLabel" destination="wCo-bX-uP0" id="4OG-qb-AkD"/>
+                                    <outlet property="unitNameLabel" destination="be0-Hv-kNx" id="ya4-Pl-L58"/>
+                                </connections>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="1ZD-eI-RzP" id="HKd-0A-pyJ"/>
+                            <outlet property="delegate" destination="1ZD-eI-RzP" id="kFE-5z-bIt"/>
+                        </connections>
+                    </tableView>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="gAN-SF-UjG" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-626" y="2115"/>
+        </scene>
+        <!--群组选择-->
+        <scene sceneID="EQg-BX-giK">
+            <objects>
+                <tableViewController storyboardIdentifier="groupPicker" title="群组选择" id="fa9-Bv-LBG" customClass="ContactGroupPickerViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="3XX-X5-Sd6" customClass="ZLBaseTableView" customModule="O2Platform" customModuleProvider="target">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="groupPickerViewCell" id="Cj3-Rm-3SI" customClass="GroupPickerTableViewCell" customModule="O2Platform" customModuleProvider="target">
+                                <rect key="frame" x="0.0" y="28" width="414" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Cj3-Rm-3SI" id="oCY-NQ-yAd">
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="unselected" translatesAutoresizingMaskIntoConstraints="NO" id="e7H-Hw-70e">
+                                            <rect key="frame" x="10" y="3" width="24" height="24"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="24" id="5r9-kr-mzS"/>
+                                                <constraint firstAttribute="height" constant="24" id="a2m-ov-dft"/>
+                                            </constraints>
+                                        </imageView>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_group" translatesAutoresizingMaskIntoConstraints="NO" id="sag-cm-TfY">
+                                            <rect key="frame" x="44" y="5" width="40" height="20"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="40" id="eFD-C5-7M4"/>
+                                                <constraint firstAttribute="width" constant="40" id="zE9-6B-bzd"/>
+                                            </constraints>
+                                        </imageView>
+                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="群组" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iU9-wl-ajK">
+                                            <rect key="frame" x="94" y="4.5" width="35" height="21"/>
+                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                            <nil key="textColor"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstItem="iU9-wl-ajK" firstAttribute="centerY" secondItem="oCY-NQ-yAd" secondAttribute="centerY" id="2Q6-Gf-upT"/>
+                                        <constraint firstItem="e7H-Hw-70e" firstAttribute="leading" secondItem="oCY-NQ-yAd" secondAttribute="leading" constant="10" id="3Xn-if-kdF"/>
+                                        <constraint firstItem="iU9-wl-ajK" firstAttribute="leading" secondItem="sag-cm-TfY" secondAttribute="trailing" constant="10" id="FtN-dS-ac0"/>
+                                        <constraint firstItem="sag-cm-TfY" firstAttribute="leading" secondItem="e7H-Hw-70e" secondAttribute="trailing" constant="10" id="Z1h-UT-wyz"/>
+                                        <constraint firstAttribute="bottom" secondItem="sag-cm-TfY" secondAttribute="bottom" constant="5" id="c24-tl-qUE"/>
+                                        <constraint firstItem="e7H-Hw-70e" firstAttribute="centerY" secondItem="oCY-NQ-yAd" secondAttribute="centerY" id="eUD-xw-lpL"/>
+                                        <constraint firstItem="sag-cm-TfY" firstAttribute="top" secondItem="oCY-NQ-yAd" secondAttribute="top" constant="5" id="gYi-Xr-U9s"/>
+                                    </constraints>
+                                </tableViewCellContentView>
+                                <connections>
+                                    <outlet property="checkImageView" destination="e7H-Hw-70e" id="hF9-UH-Y76"/>
+                                    <outlet property="groupIconImageView" destination="sag-cm-TfY" id="Vv6-Lo-eFr"/>
+                                    <outlet property="groupNameLabel" destination="iU9-wl-ajK" id="ias-eg-BG5"/>
+                                </connections>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="fa9-Bv-LBG" id="GO0-4A-qmQ"/>
+                            <outlet property="delegate" destination="fa9-Bv-LBG" id="v9e-wV-y5g"/>
+                        </connections>
+                    </tableView>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="mXp-V9-56n" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="118.84057971014494" y="2114.7321428571427"/>
+        </scene>
+        <!--人员选择-->
+        <scene sceneID="IJ9-98-S36">
+            <objects>
+                <tableViewController storyboardIdentifier="personPicker" title="人员选择" id="FE0-vh-KUQ" customClass="ContactPersonPickerViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="Cl9-s2-dz5">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="personPickerViewCell" id="fH2-SF-2xi" customClass="PersonPickerTableViewCell" customModule="O2Platform" customModuleProvider="target">
+                                <rect key="frame" x="0.0" y="28" width="414" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fH2-SF-2xi" id="TCX-Op-2Su">
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="unselected" translatesAutoresizingMaskIntoConstraints="NO" id="Ses-sX-ZNu">
+                                            <rect key="frame" x="10" y="10" width="24" height="24"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="24" id="Yaa-PY-k7g"/>
+                                                <constraint firstAttribute="width" constant="24" id="sV2-BU-7bn"/>
+                                            </constraints>
+                                        </imageView>
+                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_men" translatesAutoresizingMaskIntoConstraints="NO" id="D06-Cs-TbZ">
+                                            <rect key="frame" x="44" y="16" width="40" height="40"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="40" id="S0H-E2-LWY"/>
+                                                <constraint firstAttribute="width" constant="40" id="rTX-01-1Xh"/>
+                                            </constraints>
+                                        </imageView>
+                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="群组" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Df-qW-3cz">
+                                            <rect key="frame" x="94" y="11" width="35" height="21"/>
+                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                            <nil key="textColor"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstItem="3Df-qW-3cz" firstAttribute="centerY" secondItem="TCX-Op-2Su" secondAttribute="centerY" id="2MZ-JQ-7gk"/>
+                                        <constraint firstItem="3Df-qW-3cz" firstAttribute="leading" secondItem="D06-Cs-TbZ" secondAttribute="trailing" constant="10" id="Ma1-Ee-LSN"/>
+                                        <constraint firstItem="D06-Cs-TbZ" firstAttribute="top" secondItem="TCX-Op-2Su" secondAttribute="topMargin" constant="5" id="Poo-cA-1lc"/>
+                                        <constraint firstAttribute="bottomMargin" secondItem="D06-Cs-TbZ" secondAttribute="bottom" constant="5" id="UX0-ak-qdT"/>
+                                        <constraint firstItem="D06-Cs-TbZ" firstAttribute="leading" secondItem="Ses-sX-ZNu" secondAttribute="trailing" constant="10" id="Y1r-S1-zr1"/>
+                                        <constraint firstItem="Ses-sX-ZNu" firstAttribute="leading" secondItem="TCX-Op-2Su" secondAttribute="leading" constant="10" id="aPE-be-NRh"/>
+                                        <constraint firstItem="Ses-sX-ZNu" firstAttribute="centerY" secondItem="TCX-Op-2Su" secondAttribute="centerY" id="nwd-qP-A05"/>
+                                    </constraints>
+                                </tableViewCellContentView>
+                                <connections>
+                                    <outlet property="checkImageView" destination="Ses-sX-ZNu" id="Bth-2C-jFy"/>
+                                    <outlet property="personIconImageView" destination="D06-Cs-TbZ" id="uD4-Rh-vTb"/>
+                                    <outlet property="personNameLabel" destination="3Df-qW-3cz" id="50x-HJ-NH7"/>
+                                </connections>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="FE0-vh-KUQ" id="G1I-Od-OW6"/>
+                            <outlet property="delegate" destination="FE0-vh-KUQ" id="SzE-Ta-gX6"/>
+                        </connections>
+                    </tableView>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="t2C-jy-I2g" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="839" y="2120"/>
+        </scene>
         <!--Contact Search Result Controller-->
         <scene sceneID="MV1-yP-eOJ">
             <objects>
                 <tableViewController storyboardIdentifier="OOContactSearchController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="HZ8-64-2px" customClass="OOContactSearchResultController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
                     <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Pw4-jR-GuJ">
-                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
                         <prototypes>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="OOContactSearchCell" rowHeight="60" id="JjO-oh-zM8" customClass="OOContactSearchCell" customModule="O2Platform" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
+                                <rect key="frame" x="0.0" y="55.5" width="414" height="60"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JjO-oh-zM8" id="Uew-0X-5bX">
-                                    <rect key="frame" x="0.0" y="0.0" width="375" height="59.5"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="59.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="nwU-h6-xBq">
-                                            <rect key="frame" x="16" y="9" width="40" height="40"/>
+                                            <rect key="frame" x="16" y="9.5" width="40" height="40"/>
                                             <constraints>
                                                 <constraint firstAttribute="height" constant="40" id="1gY-7a-1Vr"/>
                                                 <constraint firstAttribute="width" constant="40" id="6N4-V9-Fh9"/>
                                             </constraints>
                                         </imageView>
                                         <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="4CB-0l-xRZ">
-                                            <rect key="frame" x="64" y="9" width="271" height="41"/>
+                                            <rect key="frame" x="64" y="9" width="306" height="41"/>
                                             <subviews>
                                                 <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9KE-Mc-ZJR">
                                                     <rect key="frame" x="0.0" y="0.0" width="26.5" height="19.5"/>
@@ -292,7 +730,7 @@
                                             </subviews>
                                         </stackView>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_arrow" translatesAutoresizingMaskIntoConstraints="NO" id="VCW-WF-j9z">
-                                            <rect key="frame" x="337" y="19" width="22" height="22"/>
+                                            <rect key="frame" x="372" y="19" width="22" height="22"/>
                                             <constraints>
                                                 <constraint firstAttribute="height" constant="22" id="Hsr-A7-pzd"/>
                                                 <constraint firstAttribute="width" constant="22" id="bCn-06-44f"/>
@@ -337,20 +775,20 @@
             <objects>
                 <tableViewController title="个人资料" id="N4C-oX-ZWA" customClass="OOLinkeManViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
                     <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" rowHeight="50" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="2zB-C3-yzy">
-                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
                         <inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
                         <prototypes>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="linkManInfoCell" id="BTf-AE-ip4" customClass="OOLinkManInfoCell" customModule="O2Platform" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="55.5" width="375" height="50"/>
+                                <rect key="frame" x="0.0" y="55.5" width="414" height="50"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="BTf-AE-ip4" id="ohP-K3-5Go">
-                                    <rect key="frame" x="0.0" y="0.0" width="375" height="49.5"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="414" height="49.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ghy-QN-Vyw">
-                                            <rect key="frame" x="16" y="14" width="100" height="21"/>
+                                            <rect key="frame" x="20" y="14" width="100" height="21"/>
                                             <constraints>
                                                 <constraint firstAttribute="width" constant="100" id="UkH-fC-dbx"/>
                                             </constraints>
@@ -359,14 +797,14 @@
                                             <nil key="highlightedColor"/>
                                         </label>
                                         <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aUG-wM-etm">
-                                            <rect key="frame" x="334" y="12" width="25" height="25"/>
+                                            <rect key="frame" x="369" y="12" width="25" height="25"/>
                                             <constraints>
                                                 <constraint firstAttribute="height" constant="25" id="jwM-Zu-tTZ"/>
                                                 <constraint firstAttribute="width" constant="25" id="uRL-EP-tD6"/>
                                             </constraints>
                                         </button>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iPS-rH-N4A">
-                                            <rect key="frame" x="124" y="14" width="26.5" height="21"/>
+                                            <rect key="frame" x="128" y="14" width="26.5" height="21"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="15"/>
                                             <color key="textColor" red="0.20000000000000001" green="0.20000000000000001" blue="0.20000000000000001" alpha="1" colorSpace="calibratedRGB"/>
                                             <nil key="highlightedColor"/>
@@ -415,7 +853,7 @@
                 <navigationController automaticallyAdjustsScrollViewInsets="NO" id="BLU-T0-pTn" customClass="OOBaseNavigationController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
                     <toolbarItems/>
                     <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="vaL-L1-ZGR">
-                        <rect key="frame" x="0.0" y="20" width="375" height="44"/>
+                        <rect key="frame" x="0.0" y="44" width="414" height="44"/>
                         <autoresizingMask key="autoresizingMask"/>
                     </navigationBar>
                     <nil name="viewControllers"/>
@@ -425,7 +863,7 @@
                 </navigationController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="qcu-B4-14g" userLabel="First Responder" sceneMemberID="firstResponder"/>
             </objects>
-            <point key="canvasLocation" x="556" y="1409"/>
+            <point key="canvasLocation" x="1465.217391304348" y="1408.9285714285713"/>
         </scene>
         <!--Base Navigation Controller-->
         <scene sceneID="Suh-Jf-v0R">
@@ -433,7 +871,7 @@
                 <navigationController automaticallyAdjustsScrollViewInsets="NO" id="nVv-Ei-OAh" customClass="OOBaseNavigationController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
                     <toolbarItems/>
                     <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="vHa-TK-872">
-                        <rect key="frame" x="0.0" y="20" width="375" height="44"/>
+                        <rect key="frame" x="0.0" y="44" width="414" height="44"/>
                         <autoresizingMask key="autoresizingMask"/>
                     </navigationBar>
                     <nil name="viewControllers"/>
@@ -445,9 +883,31 @@
             </objects>
             <point key="canvasLocation" x="508" y="663"/>
         </scene>
+        <!--Navigation Controller-->
+        <scene sceneID="DSi-Vr-q31">
+            <objects>
+                <navigationController automaticallyAdjustsScrollViewInsets="NO" id="g8K-0X-r2C" customClass="ZLNavigationController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
+                    <toolbarItems/>
+                    <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="h7S-jE-WaQ">
+                        <rect key="frame" x="0.0" y="44" width="414" height="44"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <nil name="viewControllers"/>
+                    <connections>
+                        <segue destination="fWb-Zb-wIl" kind="relationship" relationship="rootViewController" id="pLd-oK-a8D"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="VBn-bb-OA8" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-1384.0579710144928" y="1440.4017857142856"/>
+        </scene>
     </scenes>
     <resources>
         <image name="icon_arrow" width="22" height="22"/>
+        <image name="icon_group" width="20" height="20"/>
+        <image name="icon_men" width="40" height="40"/>
+        <image name="pic_oval" width="40" height="40"/>
+        <image name="unselected" width="30" height="30"/>
     </resources>
     <inferredMetricsTieBreakers>
         <segue reference="YIn-We-AW2"/>

+ 150 - 0
o2ios/O2Platform/Contact-通讯录/Controller/ContactGroupPickerViewController.swift

@@ -0,0 +1,150 @@
+//
+//  ContactGroupPickerViewController.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/12.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+import CocoaLumberjack
+
+class ContactGroupPickerViewController: UITableViewController {
+
+    
+    private var groupDataList:[OOGroupModel] = []
+    private let viewModel: ContactPickerViewModel = {
+        return ContactPickerViewModel()
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        DDLogDebug("viewdidload ............group")
+        //分页刷新功能
+        self.tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: {
+            self.loadFirstPageData()
+        })
+        
+        self.tableView.mj_footer = MJRefreshAutoFooter(refreshingBlock: {
+            self.loadNextPageData()
+        })
+        self.loadFirstPageData()
+    }
+    
+    // MARK: - Table view data source
+    
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 1
+    }
+    
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+       
+        return self.groupDataList.count
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 10
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 3
+    }
+    
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: "groupPickerViewCell", for: indexPath) as! GroupPickerTableViewCell
+        let group = self.groupDataList[indexPath.row]
+        cell.loadGroupInfo(info: group, checked: self.isSelected(value: group.distinguishedName!))
+        return cell
+    }
+    
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        let value = self.groupDataList[indexPath.row].distinguishedName!
+        let name = self.groupDataList[indexPath.row].name!
+        if self.isSelected(value: value) {
+            self.removeSelected(value: value)
+        }else {
+            self.addSelected(value: value, name: name)
+        }
+        self.tableView.reloadRows(at: [indexPath], with: .automatic)
+        
+        self.tableView.deselectRow(at: indexPath, animated: false)
+    }
+    
+    
+
+    /*
+    // MARK: - Navigation
+
+    // In a storyboard-based application, you will often want to do a little preparation before navigation
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        // Get the new view controller using segue.destination.
+        // Pass the selected object to the new view controller.
+    }
+    */
+    
+    //MARK: - private method
+    
+    private func loadFirstPageData() {
+         DDLogDebug("loadFirstPageData ............group")
+        MBProgressHUD_JChat.showMessage(message: "loading...", toView: view)
+        viewModel.loadGroupList(lastId: "(0)").then { (list)  in
+            self.groupDataList.removeAll()
+            self.groupDataList = list
+            self.tableView.reloadData()
+            MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+            if self.tableView.mj_header.isRefreshing(){
+                self.tableView.mj_header.endRefreshing()
+            }
+            DDLogDebug("loadFirstPageData ............finish")
+        }.catch { (error) in
+                DDLogError(error.localizedDescription)
+                MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+            if self.tableView.mj_header.isRefreshing(){
+                self.tableView.mj_header.endRefreshing()
+            }
+        }
+    }
+    private func loadNextPageData() {
+        DDLogDebug("loadNextPageData ............group")
+        if let last = self.groupDataList.last?.id {
+            viewModel.loadGroupList(lastId: last).then { (list)  in
+                list.forEach({ (model) in
+                    self.groupDataList.append(model)
+                })
+                self.tableView.reloadData()
+                MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+                if self.tableView.mj_footer.isRefreshing(){
+                    self.tableView.mj_footer.endRefreshing()
+                }
+                }.catch { (error) in
+                    DDLogError(error.localizedDescription)
+                    MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+                    if self.tableView.mj_footer.isRefreshing(){
+                        self.tableView.mj_footer.endRefreshing()
+                    }
+            }
+        }else {
+            self.loadFirstPageData()
+        }
+    }
+    
+    private func isSelected(value: String) -> Bool {
+        if let vc = self.parent as? ContactPickerViewController {
+            return vc.isSelectedValue(type: .group, value: value)
+        }
+        return false
+    }
+    
+    private func removeSelected(value: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            vc.removeSelectedValue(type: .group, value: value)
+        }
+    }
+    
+    private func addSelected(value: String, name: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            vc.addSelectedValue(type: .group, name: name, value: value)
+        }
+    }
+
+}

+ 225 - 0
o2ios/O2Platform/Contact-通讯录/Controller/ContactIdentityPickerViewController.swift

@@ -0,0 +1,225 @@
+//
+//  ContactIdentityPickerViewController.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/12.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+import CocoaLumberjack
+import Promises
+
+class ContactIdentityPickerViewController: UITableViewController {
+
+    // MARK: - 需要传入的参数
+    var topUnitList: [String] = [] //顶级组织
+    var dutyList: [String] = []  //职务列表 查询身份用的
+    var backResultIsUser = false // 这个选择器是身份选择和用户选择共用的 这个参数表示返回的结果是用户还是身份
+    
+    // MARK: - 私有属性
+    private var unitDataList:[OOUnitModel] = []
+    private var identityDataList:[OOIdentityModel] = []
+    private var breadcrumbList: [ContactBreadcrumbBean] = []
+    private var unitParent: String = "-1"
+    private var unitParentName: String = "通讯录"
+    private let viewModel: ContactPickerViewModel = {
+        return ContactPickerViewModel()
+    }()
+    
+    
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.loadData()
+    }
+    
+    // MARK: - Table view data source
+    
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 3
+    }
+    
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        if section == 0 {
+            return 1
+        } else if section == 1 {
+            return self.unitDataList.count
+        }
+        return self.identityDataList.count
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 10
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 3
+    }
+    
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        if indexPath.section == 0 {
+            let cell = tableView.dequeueReusableCell(withIdentifier: "breadcrumbViewCell", for: indexPath) as! UnitBreadcrumbViewCell
+            cell.refreshBreadcrumb(breadcrumbList: self.breadcrumbList)
+            cell.delegate = self
+            return cell
+        }else if indexPath.section == 1{
+            let cell = tableView.dequeueReusableCell(withIdentifier: "unitPickerViewCell", for: indexPath) as! UnitPickerTableViewCell
+            let unit = self.unitDataList[indexPath.row]
+            cell.loadUnitNotCheck(info: unit)
+            cell.delegate = self
+            return cell
+        } else {
+            let cell = tableView.dequeueReusableCell(withIdentifier: "unitPickerViewCell", for: indexPath) as! UnitPickerTableViewCell
+            let identity = self.identityDataList[indexPath.row]
+            var isSelected:Bool
+            if backResultIsUser {
+                isSelected = self.isSelected(value: identity.person!)
+            }else {
+                isSelected = self.isSelected(value: identity.distinguishedName!)
+            }
+            cell.loadIdentity(identity: identity, checked: isSelected)
+            return cell
+        }
+    }
+    
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        if indexPath.section == 2 {
+            let value = self.identityDataList[indexPath.row].distinguishedName!
+            let person = self.identityDataList[indexPath.row].person!
+            let name = self.identityDataList[indexPath.row].name!
+            if backResultIsUser {
+                let isSelected = self.isSelected(value: person)
+                if isSelected {
+                    self.removeSelected(value: person)
+                }else {
+                    self.addSelected(value: person, name: name)
+                }
+            }else {
+                let isSelected = self.isSelected(value: value)
+                if isSelected {
+                    self.removeSelected(value: value)
+                }else {
+                    self.addSelected(value: value, name: name)
+                }
+            }
+           
+            self.tableView.reloadRows(at: [indexPath], with: .automatic)
+        }
+        self.tableView.deselectRow(at: indexPath, animated: false)
+    }
+    
+
+    /*
+    // MARK: - Navigation
+
+    // In a storyboard-based application, you will often want to do a little preparation before navigation
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        // Get the new view controller using segue.destination.
+        // Pass the selected object to the new view controller.
+    }
+    */
+    
+    //MARK: - private method
+    
+    //获取组织数据 必须先操作面包屑导航数据
+    private func loadData() {
+        MBProgressHUD_JChat.showMessage(message: "loading...", toView: view)
+        viewModel.loadUnitList(parent: unitParent, topList: topUnitList)
+            .then { (list) -> Promise<[OOIdentityModel]> in
+                DDLogDebug("loadUnitList 结果: \(list.count)")
+                self.unitDataList = list
+                var bean = ContactBreadcrumbBean()
+                bean.key =  self.unitParent
+                bean.name = self.unitParentName
+                bean.level = self.breadcrumbList.count
+                self.breadcrumbList.append(bean)
+                if self.unitParent != "-1" {
+                    return self.viewModel.loadIdentityList(dutyList: self.dutyList, unit: self.unitParent)
+                }else {
+                    return Promise<[OOIdentityModel]> { fufill,reject in
+                        fufill([])
+                    }
+                }
+            }.then({ (result) in
+                DDLogDebug("loadIdentityList 结果: \(result.count)")
+                self.identityDataList = result
+                self.tableView.reloadData()
+                MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+            }).catch { (error) in
+                DDLogError(error.localizedDescription)
+                MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+        }
+    }
+    
+    private func isSelected(value: String) -> Bool {
+        if let vc = self.parent as? ContactPickerViewController {
+            if backResultIsUser {
+                return vc.isSelectedValue(type: .person, value: value)
+            }else {
+                return vc.isSelectedValue(type: .identity, value: value)
+            }
+        }
+        return false
+    }
+    
+    private func removeSelected(value: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            if backResultIsUser {
+                vc.removeSelectedValue(type: .person, value: value)
+            }else {
+                vc.removeSelectedValue(type: .identity, value: value)
+            }
+            
+        }
+    }
+    
+    private func addSelected(value: String, name: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            if backResultIsUser {
+                vc.addSelectedValue(type: .person, name: name, value: value)
+            }else {
+                vc.addSelectedValue(type: .identity, name: name, value: value)
+            }
+            
+        }
+    }
+
+}
+
+
+
+// MARK: - extension delegate
+
+extension ContactIdentityPickerViewController : UnitPickerNextBtnDelegate {
+    //进入下级组织
+    func next(unitName: String?, unitDistinguishedName: String?) {
+        DDLogDebug("name: \(String(describing: unitName)) dis:\(String(describing: unitDistinguishedName))")
+        if unitName == nil || unitDistinguishedName == nil {
+            DDLogError("参数为空。。。。。")
+        }else {
+            self.unitParentName = unitName!
+            self.unitParent = unitDistinguishedName!
+            self.loadData()
+        }
+    }
+}
+
+extension ContactIdentityPickerViewController: UnitPickerBreadcrumbClickDelegate {
+    //点击面包屑导航上的组织按钮
+    func breadcrumbTap(name: String, distinguished: String) {
+        //清空后面的导航按钮
+        for (index,unit) in self.breadcrumbList.enumerated() {
+            if unit.key == distinguished {
+                let n = self.breadcrumbList.count - index
+                self.breadcrumbList.removeLast(n)
+                break
+            }
+        }
+        self.unitParentName = name
+        self.unitParent = distinguished
+        self.loadData()
+    }
+    
+    
+}

+ 176 - 0
o2ios/O2Platform/Contact-通讯录/Controller/ContactPersonPickerViewController.swift

@@ -0,0 +1,176 @@
+//
+//  ContactPersonPickerViewController.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/12.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+import CocoaLumberjack
+
+class ContactPersonPickerViewController: UITableViewController {
+
+    
+    private var personDataList:[OOPersonModel] = []
+    private let viewModel: ContactPickerViewModel = {
+        return ContactPickerViewModel()
+    }()
+    private let searchController = UISearchController(searchResultsController: nil)
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.searchController.delegate = self
+        self.searchController.searchResultsUpdater = self
+        self.searchController.searchBar.delegate = self
+        self.searchBarInit(searchController.searchBar)
+        self.definesPresentationContext = true
+        self.searchController.dimsBackgroundDuringPresentation = false
+        self.searchController.hidesNavigationBarDuringPresentation = false
+        // Setup the Scope Bar
+        self.tableView.tableHeaderView  = searchController.searchBar
+        self.tableView.tableHeaderView?.sizeToFit()
+        
+    }
+    
+    // MARK: - Table view data source
+    
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 1
+    }
+    
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        
+        return self.personDataList.count
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 10
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 3
+    }
+    
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: "personPickerViewCell", for: indexPath) as! PersonPickerTableViewCell
+        let person = self.personDataList[indexPath.row]
+        cell.loadPersonInfo(info: person, checked: self.isSelected(value: person.distinguishedName!))
+        return cell
+    }
+    
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        let value = self.personDataList[indexPath.row].distinguishedName!
+        let name = self.personDataList[indexPath.row].name!
+        if self.isSelected(value: value) {
+            self.removeSelected(value: value)
+        }else {
+            self.addSelected(value: value, name: name)
+        }
+        self.tableView.reloadRows(at: [indexPath], with: .automatic)
+        
+        self.tableView.deselectRow(at: indexPath, animated: false)
+    }
+    
+
+    /*
+    // MARK: - Navigation
+
+    // In a storyboard-based application, you will often want to do a little preparation before navigation
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        // Get the new view controller using segue.destination.
+        // Pass the selected object to the new view controller.
+    }
+    */
+    
+    //MARK: - private method
+    
+    func loadSearchData(_ searchText:String?,scopeIndex:Int){
+        if (searchText == nil || searchText?.isEmpty == true) {
+            self.personDataList.removeAll()
+            self.tableView.reloadData()
+        }else{
+            MBProgressHUD_JChat.showMessage(message: "loading...", toView: view)
+            viewModel.searchPersonList(searchText: searchText!).then { (list)  in
+                self.personDataList.removeAll()
+                self.personDataList = list
+                self.tableView.reloadData()
+                MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+                }.catch { (error) in
+                    DDLogError(error.localizedDescription)
+                    MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+                     
+            }
+        }
+    }
+    
+    private func isSelected(value: String) -> Bool {
+        if let vc = self.parent as? ContactPickerViewController {
+            return vc.isSelectedValue(type: .person, value: value)
+        }
+        return false
+    }
+    
+    private func removeSelected(value: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            vc.removeSelectedValue(type: .person, value: value)
+        }
+    }
+    
+    private func addSelected(value: String, name: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            vc.addSelectedValue(type: .person, name: name, value: value)
+        }
+    }
+    
+    private func searchBarInit(_ searchBar:UISearchBar){
+        if let searchField = searchBar.value(forKey: "searchField") as? UITextField {
+            searchField.placeholder = "请输入搜索关键字"
+        }
+        UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).title = "取消"
+    }
+
+}
+
+
+
+extension ContactPersonPickerViewController:UISearchControllerDelegate{
+    func willPresentSearchController(_ searchController: UISearchController) {
+        NSLog("willPresentSearchController")
+        
+    }
+    
+    func didPresentSearchController(_ searchController: UISearchController) {
+        NSLog("didPresentSearchController")
+        searchController.searchBar.setShowsCancelButton(false, animated: true)
+    }
+    
+    func willDismissSearchController(_ searchController: UISearchController) {
+        NSLog("willDismissSearchController")
+    }
+    
+    func didDismissSearchController(_ searchController: UISearchController) {
+        NSLog("didDismissSearchController")
+    }
+    
+    func presentSearchController(_ searchController: UISearchController) {
+        NSLog("presentSearchController")
+    }
+    
+    
+}
+
+extension ContactPersonPickerViewController:UISearchBarDelegate{
+    func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
+        self.loadSearchData(searchBar.text, scopeIndex: selectedScope)
+    }
+    
+    
+}
+
+extension ContactPersonPickerViewController:UISearchResultsUpdating{
+    func updateSearchResults(for searchController: UISearchController) {
+        let searchBar = searchController.searchBar
+        self.loadSearchData(searchBar.text, scopeIndex: searchBar.selectedScopeButtonIndex)
+    }
+}

+ 428 - 0
o2ios/O2Platform/Contact-通讯录/Controller/ContactPickerViewController.swift

@@ -0,0 +1,428 @@
+//
+//  ContactPickerViewController.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/12.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+import CocoaLumberjack
+
+enum ContactPickerType {
+    case unit
+    case identity
+    case group
+    case person
+}
+
+
+typealias DidPickedContact = (_ result: O2BizContactPickerResult) -> Void ///< 定义确认回调
+
+class ContactPickerViewController: UIViewController {
+    
+    
+    static func providePickerVC(
+        pickerModes:[ContactPickerType],
+        topUnitList: [String] = [],
+        unitType: String = "",
+        maxNumber: Int = 0,
+        multiple: Bool = true,
+        dutyList:[String] = [],
+        initDeptPickedArray:[String] = [],
+        initIdPickedArray:[String] = [],
+        initGroupPickedArray:[String] = [],
+        initUserPickedArray:[String] = [],
+        pickedDelegate: @escaping DidPickedContact
+    ) -> ContactPickerViewController? {
+        
+        if pickerModes.count < 1 {
+            DDLogError("没有选择器类型")
+            return nil
+        }
+        let storyBoard = UIStoryboard(name: "Contacts_new", bundle: nil)
+        let destVC = storyBoard.instantiateViewController(withIdentifier: "contactPicker") as? ContactPickerViewController
+        destVC?.selectorList = pickerModes
+        if topUnitList.count > 0 {
+            destVC?.topUnitList = topUnitList
+        }
+        if !unitType.isEmpty {
+            destVC?.unitType = unitType
+        }
+        if maxNumber > 0 {
+            destVC?.maxNumber = maxNumber
+        }
+        destVC?.multiple = multiple
+        if dutyList.count > 0 {
+            destVC?.dutyList = dutyList
+        }
+        if initDeptPickedArray.count > 0 {
+            destVC?.initDeptPickedArray = initDeptPickedArray
+        }
+        if initIdPickedArray.count > 0 {
+            destVC?.initIdPickedArray = initIdPickedArray
+        }
+        if initGroupPickedArray.count > 0 {
+            destVC?.initGroupPickedArray = initGroupPickedArray
+        }
+        if initUserPickedArray.count > 0 {
+            destVC?.initUserPickedArray = initUserPickedArray
+        }
+        destVC?.pickedDelegate = pickedDelegate
+        
+        return destVC
+    }
+
+
+    @IBOutlet weak var topBarStackView: UIStackView!
+    @IBOutlet weak var pickerContainerView: UIView!
+    @IBOutlet weak var topBarStackViewHeightConstraint: NSLayoutConstraint!
+    
+    
+    
+    //各个初始化参数
+    var selectorList:[ContactPickerType] = [] //选择器 多值
+    var topUnitList: [String] = [] //顶级组织
+    var unitType: String = "" //组织类型 查询组织用的
+    var maxNumber = 0 //可选择的最大数量
+    var multiple = true //是否多选
+    var dutyList:[String] = [] //身份查询的时候的限制的职务列表
+    var initDeptPickedArray:[String] = [] //初始 已选择的数据
+    var initIdPickedArray:[String] = [] //初始 已选择的数据
+    var initGroupPickedArray:[String] = [] //初始 已选择的数据
+    var initUserPickedArray:[String] = [] //初始 已选择的数据
+    var pickedDelegate: DidPickedContact?
+    
+    //已经选中的值
+    private var selectedDeptSet:[O2BizContactPickerResultItem] = []
+    private var selectedIdSet:[O2BizContactPickerResultItem] = []
+    private var selectedGroupSet:[O2BizContactPickerResultItem] = []
+    private var selectedUserSet:[O2BizContactPickerResultItem] = []
+    //选择按钮文字
+    private var pickBtnTitle = ""
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        if initDeptPickedArray.count > 0 {
+            initDeptPickedArray.forEach { (s) in
+                var name = ""
+                if s.contains("@") {
+                    name  = s.split("@")[0]
+                }else {
+                    name = s
+                }
+                selectedDeptSet.append(O2BizContactPickerResultItem(distinguishedName: s, name: name))
+            }
+        }
+        if initIdPickedArray.count > 0 {
+            initIdPickedArray.forEach { (s) in
+                var name = ""
+                if s.contains("@") {
+                    name  = s.split("@")[0]
+                }else {
+                    name = s
+                }
+                selectedIdSet.append(O2BizContactPickerResultItem(distinguishedName: s, name: name))
+            }
+        }
+        if initGroupPickedArray.count > 0 {
+            initGroupPickedArray.forEach { (s) in
+                var name = ""
+                if s.contains("@") {
+                    name  = s.split("@")[0]
+                }else {
+                    name = s
+                }
+                selectedGroupSet.append(O2BizContactPickerResultItem(distinguishedName: s, name: name))
+            }
+        }
+        if initUserPickedArray.count > 0 {
+            initUserPickedArray.forEach { (s) in
+                var name = ""
+                if s.contains("@") {
+                    name  = s.split("@")[0]
+                }else {
+                    name = s
+                }
+                selectedUserSet.append(O2BizContactPickerResultItem(distinguishedName: s, name: name))
+            }
+        }
+        let c = selectedDeptSet.count + selectedIdSet.count + selectedGroupSet.count + selectedUserSet.count
+        pickBtnTitle = "选择(\(c))"
+        if maxNumber > 0 {
+            pickBtnTitle = "选择(\(c)/\(maxNumber))"
+        }
+        navigationItem.rightBarButtonItem = UIBarButtonItem(title: pickBtnTitle, style: .plain, target: self, action: #selector(selected))
+        if selectorList.count == 1 {
+            self.topBarStackView.isHidden = true
+            self.topBarStackViewHeightConstraint.constant = 0.0
+            showPicker(tag:  selectorList[0].hashValue)
+        }else {
+            self.topBarStackView.isHidden = false
+            self.topBarStackViewHeightConstraint.constant = 48.0
+            topBarStackView.axis = .horizontal
+            topBarStackView.alignment = .fill
+            topBarStackView.spacing = 5
+            topBarStackView.distribution = .fillEqually
+            topBarStackView.removeSubviews()
+            selectorList.forEach { (s) in
+                switch(s) {
+                case .unit:
+                    let unitBtn = generatePickerTypeBtn(title: "组织选择", type: .unit)
+                    topBarStackView.addArrangedSubview(unitBtn)
+                case .identity:
+                    let identityBtn = generatePickerTypeBtn(title: "身份选择", type: .identity)
+                    topBarStackView.addArrangedSubview(identityBtn)
+                case .group:
+                    let groupBtn = generatePickerTypeBtn(title: "群组选择", type: .group)
+                    topBarStackView.addArrangedSubview(groupBtn)
+                case .person:
+                    let personBtn = generatePickerTypeBtn(title: "人员选择", type: .person)
+                    topBarStackView.addArrangedSubview(personBtn)
+                }
+            }
+            if topBarStackView.subviews.count > 0 {
+                if let button = (topBarStackView.subviews[0] as? UIButton){
+                    button.isSelected = true
+                    showPicker(tag:  button.tag)
+                }
+            }
+        }
+    }
+    
+    
+    // MARK: - public method 提供给外部是一哦那个
+    
+    // 检查值是否已经包含在选中的列表中
+    func isSelectedValue(type: ContactPickerType, value: String) -> Bool {
+        switch type {
+            case .unit:
+               var f = false
+               self.selectedDeptSet.forEach { (item) in
+                    if item.distinguishedName == value {
+                        f = true
+                    }
+                }
+                return f
+            case .identity:
+                var f = false
+                self.selectedIdSet.forEach { (item) in
+                    if item.distinguishedName == value {
+                        f = true
+                    }
+                }
+                return f
+            case .group:
+                var f = false
+                self.selectedGroupSet.forEach { (item) in
+                    if item.distinguishedName == value {
+                        f = true
+                    }
+                }
+                return f
+            case .person:
+                var f = false
+                self.selectedUserSet.forEach { (item) in
+                    if item.distinguishedName == value {
+                        f = true
+                    }
+                }
+                return f
+        }
+        
+    }
+    // 删除一个选中的值
+    func removeSelectedValue(type: ContactPickerType, value: String) {
+        switch type {
+            case .unit:
+                self.selectedDeptSet.removeAll { (item) -> Bool in
+                    return item.distinguishedName == value
+                }
+                break
+            case .identity:
+                self.selectedIdSet.removeAll { (item) -> Bool in
+                    return item.distinguishedName == value
+                }
+                break
+            case .group:
+                self.selectedGroupSet.removeAll { (item) -> Bool in
+                    return item.distinguishedName == value
+                }
+                break
+            case .person:
+                self.selectedUserSet.removeAll { (item) -> Bool in
+                    return item.distinguishedName == value
+                }
+                break
+        }
+        self.refreshPickButton()
+    }
+    // 添加一个选中的值
+    func addSelectedValue(type: ContactPickerType, name: String, value: String) {
+        let c = selectedDeptSet.count + selectedIdSet.count + selectedGroupSet.count + selectedUserSet.count
+        if maxNumber > 0 && c >= maxNumber {
+            self.showError(title: "不能添加更多了!")
+            return
+        }
+        switch type {
+        case .unit:
+            self.selectedDeptSet.append(O2BizContactPickerResultItem(distinguishedName: value, name: name))
+            break
+        case .identity:
+            self.selectedIdSet.append(O2BizContactPickerResultItem(distinguishedName: value, name: name))
+            break
+        case .group:
+            self.selectedGroupSet.append(O2BizContactPickerResultItem(distinguishedName: value, name: name))
+            break
+        case .person:
+            self.selectedUserSet.append(O2BizContactPickerResultItem(distinguishedName: value, name: name))
+            break
+        }
+        self.refreshPickButton()
+    }
+    
+    
+    
+    
+    
+    // MARK: - private method 当前类私有方法
+
+    @objc private func selected() {
+        let c = selectedDeptSet.count + selectedIdSet.count + selectedGroupSet.count + selectedUserSet.count
+        DDLogDebug("选中了:\(c) 个数据")
+        if c < 1 {
+            self.showError(title: "请至少选择一条数据!")
+            return
+        }else {
+            let result = O2BizContactPickerResult(departments: selectedDeptSet,
+                                     identities: selectedIdSet,
+                                     groups: selectedGroupSet,
+                                     users: selectedUserSet)
+            self.pickedDelegate?(result)
+            self.popVC()
+        }
+    }
+
+    //刷新选择按钮文字内容
+    private func refreshPickButton() {
+        let c = selectedDeptSet.count + selectedIdSet.count + selectedGroupSet.count + selectedUserSet.count
+        pickBtnTitle = "选择(\(c))"
+        if maxNumber > 0 {
+            pickBtnTitle = "选择(\(c) / \(maxNumber))"
+            
+        }
+        navigationItem.rightBarButtonItem?.title = pickBtnTitle
+    }
+    
+    //生成选择器Tab按钮
+    private func generatePickerTypeBtn(title: String, type: ContactPickerType) -> UIButton {
+        let button = UIButton(type: .system)
+        button.setTitle(title, for: .normal)
+        button.tag = type.hashValue
+        button.setTitleColor(toolbar_text_color, for: .normal)
+        button.theme_setTitleColor(ThemeColorPicker(keyPath: "Base.base_color"), forState: .selected)
+        button.tintColor = UIColor.clear
+        button.addTarget(self, action: #selector(clickBtn(btn:)), for: .touchUpInside)
+        return button
+    }
+    
+    //点击选择器Tab按钮
+    @objc private func clickBtn(btn: UIButton) {
+        topBarStackView.subviews.forEach { (v) in
+            if let b = v as? UIButton {
+                b.isSelected = false
+                if b.tag == btn.tag {
+                    b.isSelected = true
+                    showPicker(tag: b.tag)
+                }
+            }
+        }
+    }
+    
+    //显示对应的选择器内容页面
+    private func showPicker(tag: Int) {
+        self.pickerContainerView.removeSubviews()
+        switch(tag) {
+        case ContactPickerType.unit.hashValue:
+            self.title = "组织选择"
+            if let pickerViewController = self.storyboard?.instantiateViewController(withIdentifier: "unitPicker") as? ContactUnitPickerViewController {
+                pickerViewController.topUnitList = self.topUnitList
+                pickerViewController.unitType = self.unitType
+                if self.children.contains(pickerViewController) {
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }else {
+                    pickerViewController.view.frame = CGRect(x: 0, y: 0, w: self.pickerContainerView.frame.width, h: self.pickerContainerView.frame.height)
+                    self.addChild(pickerViewController)
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }
+            }
+            break
+        case ContactPickerType.identity.hashValue:
+            self.title = "身份选择"
+            if let pickerViewController = self.storyboard?.instantiateViewController(withIdentifier: "identityPicker") as? ContactIdentityPickerViewController {
+                pickerViewController.dutyList = self.dutyList
+                pickerViewController.topUnitList = self.topUnitList
+                pickerViewController.backResultIsUser = false
+                if self.children.contains(pickerViewController) {
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }else {
+                    pickerViewController.view.frame = CGRect(x: 0, y: 0, w: self.pickerContainerView.frame.width, h: self.pickerContainerView.frame.height)
+                    self.addChild(pickerViewController)
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }
+            }
+            break
+        case ContactPickerType.group.hashValue:
+            self.title = "群组选择"
+            if let pickerViewController = self.storyboard?.instantiateViewController(withIdentifier: "groupPicker") as? ContactGroupPickerViewController {
+                if self.children.contains(pickerViewController) {
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }else {
+                    pickerViewController.view.frame = CGRect(x: 0, y: 0, w: self.pickerContainerView.frame.width, h: self.pickerContainerView.frame.height)
+                    self.addChild(pickerViewController)
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }
+            }
+            break
+        case ContactPickerType.person.hashValue:
+            self.title = "人员选择"
+            
+            if let pickerViewController = self.storyboard?.instantiateViewController(withIdentifier: "identityPicker") as? ContactIdentityPickerViewController {
+                pickerViewController.dutyList = self.dutyList
+                pickerViewController.topUnitList = self.topUnitList
+                pickerViewController.backResultIsUser = true
+                if self.children.contains(pickerViewController) {
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }else {
+                    pickerViewController.view.frame = CGRect(x: 0, y: 0, w: self.pickerContainerView.frame.width, h: self.pickerContainerView.frame.height)
+                    self.addChild(pickerViewController)
+                    self.pickerContainerView.addSubview(pickerViewController.view)
+                }
+            }
+//
+//
+//            if let pickerViewController = self.storyboard?.instantiateViewController(withIdentifier: "personPicker") as? ContactPersonPickerViewController {
+//                if self.children.contains(pickerViewController) {
+//                    self.pickerContainerView.addSubview(pickerViewController.view)
+//                }else {
+//                    pickerViewController.view.frame = CGRect(x: 0, y: 0, w: self.pickerContainerView.frame.width, h: self.pickerContainerView.frame.height)
+//                    self.addChild(pickerViewController)
+//                    self.pickerContainerView.addSubview(pickerViewController.view)
+//                }
+//            }
+            break
+        default:
+            DDLogDebug("click unkown")
+        }
+    }
+    /*
+    // MARK: - Navigation
+
+    // In a storyboard-based application, you will often want to do a little preparation before navigation
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        // Get the new view controller using segue.destination.
+        // Pass the selected object to the new view controller.
+    }
+    */
+
+}

+ 207 - 0
o2ios/O2Platform/Contact-通讯录/Controller/ContactUnitPickerViewController.swift

@@ -0,0 +1,207 @@
+//
+//  ContactUnitPickerViewController.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/12.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import CocoaLumberjack
+import UIKit
+
+class ContactUnitPickerViewController: UITableViewController {
+    
+    // MARK: - 需要传入的参数
+    var topUnitList: [String] = [] //顶级组织
+    var unitType: String = "" //组织类型 查询组织用的
+
+    // MARK: - 私有属性
+    private var dataList:[OOUnitModel] = []
+    private var breadcrumbList: [ContactBreadcrumbBean] = []
+    private var unitParent: String = "-1"
+    private var unitParentName: String = "通讯录"
+    private let viewModel: ContactPickerViewModel = {
+        return ContactPickerViewModel()
+    }()
+    
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.loadData()
+    }
+    
+    
+    
+    // MARK: - Table view data source
+    
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 2
+    }
+    
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        if section == 0 {
+            return 1
+        }
+        return self.dataList.count
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 10
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 3
+    }
+    
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        if indexPath.section == 0 {
+            let cell = tableView.dequeueReusableCell(withIdentifier: "breadcrumbViewCell", for: indexPath) as! UnitBreadcrumbViewCell
+            cell.refreshBreadcrumb(breadcrumbList: self.breadcrumbList)
+            cell.delegate = self
+            return cell
+        }else {
+            let cell = tableView.dequeueReusableCell(withIdentifier: "unitPickerViewCell", for: indexPath) as! UnitPickerTableViewCell
+            let unit = dataList[indexPath.row]
+            cell.loadUnitInfo(info: unit, checked: self.isSelected(value: unit.distinguishedName!))
+            cell.delegate = self
+            return cell
+        }
+    }
+    
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        if indexPath.section == 1 {
+            let value = dataList[indexPath.row].distinguishedName!
+            let name = dataList[indexPath.row].name!
+            if self.isSelected(value: value) {
+                self.removeSelected(value: value)
+            }else {
+                self.addSelected(value: value, name: name)
+            }
+            tableView.reloadRows(at: [indexPath], with: .automatic)
+        }
+        self.tableView.deselectRow(at: indexPath, animated: false)
+    }
+//
+//    // MARK: - UITextViewDelegate
+//    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
+//        DDLogDebug("url: "+URL.description)
+//        if let scheme = URL.scheme {
+//            switch scheme {
+//            case "reloadto" :
+//                let id = (URL.description as NSString).substring(from: 9)
+//                for unit in self.breadcrumbList {
+//                    if id == unit.key {
+//                        self.clickBreadcrumb(bean: unit)
+//                        break;
+//                    }
+//                }
+//            default:
+//                break
+//            }
+//        }
+//        return true
+//    }
+    /*
+    // MARK: - Navigation
+
+    // In a storyboard-based application, you will often want to do a little preparation before navigation
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        // Get the new view controller using segue.destination.
+        // Pass the selected object to the new view controller.
+    }
+    */
+
+    
+    // MARK: - private method
+    
+    
+    
+    //获取组织数据 必须先操作面包屑导航数据
+    private func loadData() {
+        MBProgressHUD_JChat.showMessage(message: "loading...", toView: view)
+        viewModel.loadUnitList(parent: unitParent, topList: topUnitList, unitType: unitType)
+            .then { (list)  in
+                DDLogDebug("loadUnitList 结果: \(list.count)")
+                self.dataList = list
+                var bean = ContactBreadcrumbBean()
+                bean.key =  self.unitParent
+                bean.name = self.unitParentName
+                bean.level = self.breadcrumbList.count
+                self.breadcrumbList.append(bean)
+                self.tableView.reloadData()
+                MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+            }.catch { (error) in
+                DDLogError(error.localizedDescription)
+                MBProgressHUD_JChat.hide(forView: self.view, animated: true)
+        }
+    }
+    
+    private func isSelected(value: String) -> Bool {
+        if let vc = self.parent as? ContactPickerViewController {
+            return vc.isSelectedValue(type: .unit, value: value)
+        }
+        return false
+    }
+    
+    private func removeSelected(value: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            vc.removeSelectedValue(type: .unit, value: value)
+        }
+    }
+    
+    private func addSelected(value: String, name: String) {
+        if let vc = self.parent as? ContactPickerViewController {
+            vc.addSelectedValue(type: .unit, name: name, value: value)
+        }
+    }
+    
+    //点击面包屑导航上的组织按钮
+//    private func clickBreadcrumb(bean: ContactBreadcrumbBean) {
+//        //清空后面的导航按钮
+//        for (index,unit) in self.breadcrumbList.enumerated() {
+//            if unit.key == bean.key {
+//                let n = self.breadcrumbList.count - index
+//                self.breadcrumbList.removeLast(n)
+//                break
+//            }
+//        }
+//        self.unitParentName = bean.name
+//        self.unitParent = bean.key
+//        self.loadData()
+//    }
+}
+
+// MARK: - extension delegate
+
+extension ContactUnitPickerViewController : UnitPickerNextBtnDelegate {
+    //进入下级组织
+    func next(unitName: String?, unitDistinguishedName: String?) {
+        DDLogDebug("name: \(String(describing: unitName)) dis:\(String(describing: unitDistinguishedName))")
+        if unitName == nil || unitDistinguishedName == nil {
+            DDLogError("参数为空。。。。。")
+        }else {
+            self.unitParentName = unitName!
+            self.unitParent = unitDistinguishedName!
+            self.loadData()
+        }
+    }
+}
+
+extension ContactUnitPickerViewController: UnitPickerBreadcrumbClickDelegate {
+    //点击面包屑导航上的组织按钮
+    func breadcrumbTap(name: String, distinguished: String) {
+        //清空后面的导航按钮
+        for (index,unit) in self.breadcrumbList.enumerated() {
+            if unit.key == distinguished {
+                let n = self.breadcrumbList.count - index
+                self.breadcrumbList.removeLast(n)
+                break
+            }
+        }
+        self.unitParentName = name
+        self.unitParent = distinguished
+        self.loadData()
+    }
+    
+    
+}

+ 25 - 18
o2ios/O2Platform/Contact-通讯录/Model/OOContactModel.swift

@@ -48,32 +48,39 @@ class OOControl : NSObject, NSCoding, DataModel{
     
 }
 
+// 面包屑导航对象
+struct ContactBreadcrumbBean {
+    var key: String = ""
+    var name: String = ""
+    var level: Int = 0
+}
+
 // MARK: - Unit Model
 
 class OOUnitModel : NSObject, NSCoding, DataModel{
     
-    var control : OOControl?
-    var controllerList : [AnyObject]?
-    var createTime : String?
-    var desc : String?
-    var descriptionField : String?
-    var distinguishedName : String?
-    var id : String?
-    var inheritedControllerList : [AnyObject]?
+    @objc open var control : OOControl?
+    @objc open var controllerList : [AnyObject]?
+    @objc open var createTime : String?
+    @objc open var desc : String?
+    @objc open var descriptionField : String?
+    @objc open var distinguishedName : String?
+    @objc open var id : String?
+    @objc open var inheritedControllerList : [AnyObject]?
     var level : Int?
-    var levelName : String?
-    var name : String?
+    @objc open var levelName : String?
+   @objc open  var name : String?
     var orderNumber : Int?
-    var pinyin : String?
-    var pinyinInitial : String?
-    var shortName : String?
+    @objc open var pinyin : String?
+    @objc open var pinyinInitial : String?
+    @objc open var shortName : String?
     var subDirectIdentityCount : Int?
     var subDirectUnitCount : Int?
-    var superior : String?
-    var typeList : [String]?
-    var unique : String?
-    var updateTime : String?
-    var woSubDirectIdentityList:[OOIdentityModel]?
+    @objc open var superior : String?
+   @objc open  var typeList : [String]?
+    @objc open var unique : String?
+    @objc open var updateTime : String?
+    @objc open var woSubDirectIdentityList:[OOIdentityModel]?
     
     override required init(){}
     

+ 44 - 0
o2ios/O2Platform/Contact-通讯录/View/GroupPickerTableViewCell.swift

@@ -0,0 +1,44 @@
+//
+//  GroupPickerTableViewCell.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/15.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+
+class GroupPickerTableViewCell: UITableViewCell {
+    lazy public var trueImage: UIImage = {
+        return UIImage(named: "selected")!
+    }()
+    
+    lazy public var falseImage: UIImage = {
+        return UIImage(named: "unselected")!
+    }()
+    @IBOutlet weak var checkImageView: UIImageView!
+    @IBOutlet weak var groupIconImageView: UIImageView!
+    @IBOutlet weak var groupNameLabel: UILabel!
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        // Initialization code
+        self.checkImageView.image = falseImage
+        self.groupIconImageView.image = O2ThemeManager.image(for: "Icon.icon_group")
+    }
+
+    override func setSelected(_ selected: Bool, animated: Bool) {
+        super.setSelected(selected, animated: animated)
+    }
+    
+    func loadGroupInfo(info: OOGroupModel, checked: Bool) {
+        self.groupNameLabel.text = info.name
+        if checked {
+            self.checkImageView.image = trueImage
+        }else {
+            self.checkImageView.image = falseImage
+        }
+    }
+
+    
+    
+}

+ 45 - 0
o2ios/O2Platform/Contact-通讯录/View/PersonPickerTableViewCell.swift

@@ -0,0 +1,45 @@
+//
+//  PersonPickerTableViewCell.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/15.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+
+class PersonPickerTableViewCell: UITableViewCell {
+    lazy public var trueImage: UIImage = {
+        return UIImage(named: "selected")!
+    }()
+    
+    lazy public var falseImage: UIImage = {
+        return UIImage(named: "unselected")!
+    }()
+    
+    @IBOutlet weak var personIconImageView: UIImageView!
+    @IBOutlet weak var checkImageView: UIImageView!
+    @IBOutlet weak var personNameLabel: UILabel!
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        // Initialization code
+        self.checkImageView.image = falseImage
+    }
+
+    override func setSelected(_ selected: Bool, animated: Bool) {
+        super.setSelected(selected, animated: animated)
+
+        // Configure the view for the selected state
+    }
+    
+    func loadPersonInfo(info: OOPersonModel, checked: Bool) {
+        self.personNameLabel.text = info.name
+        if checked {
+            self.checkImageView.image = trueImage
+        }else {
+            self.checkImageView.image = falseImage
+        }
+    }
+
+}

+ 70 - 0
o2ios/O2Platform/Contact-通讯录/View/UnitBreadcrumbViewCell.swift

@@ -0,0 +1,70 @@
+//
+//  UnitBreadcrumbViewCell.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/13.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+import CocoaLumberjack
+
+
+
+protocol UnitPickerBreadcrumbClickDelegate {
+    func breadcrumbTap(name: String, distinguished: String)
+}
+
+class UnitBreadcrumbViewCell: UITableViewCell {
+
+    @IBOutlet weak var breadcrumbScrollView: UIScrollView!
+    var delegate: UnitPickerBreadcrumbClickDelegate?
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+    }
+
+    override func setSelected(_ selected: Bool, animated: Bool) {
+        super.setSelected(selected, animated: animated)
+    }
+    
+    func refreshBreadcrumb(breadcrumbList: [ContactBreadcrumbBean]) {
+        if breadcrumbList.count > 0 {
+            self.breadcrumbScrollView.removeSubviews()
+            var oX = CGFloat(4.0)
+            breadcrumbList.forEachEnumerated { (index, bar) in
+                var name: String
+                var textColor:UIColor
+                if breadcrumbList.count == (index+1) {
+                    name = bar.name
+                    textColor = base_color
+                }else {
+                    name = bar.name + " > "
+                    textColor = UIColor(hex:"#333333")
+                }
+                let firstSize = name.getSize(with: 15)
+                let oY = (self.breadcrumbScrollView.bounds.height - firstSize.height) / 2
+                let firstLabel = UILabel(frame: CGRect(x: CGFloat(oX), y: oY, width: firstSize.width, height: firstSize.height))
+                firstLabel.textAlignment = .left
+                let textAttributes = [NSAttributedString.Key.foregroundColor: textColor,NSAttributedString.Key.font:UIFont(name:"PingFangSC-Regular",size:15)!]
+                firstLabel.attributedText = NSMutableAttributedString(string: name, attributes: textAttributes)
+                firstLabel.sizeToFit()
+                oX += firstSize.width
+                self.breadcrumbScrollView.addSubview(firstLabel)
+                firstLabel.addTapGesture(action: { (rec) in
+                    DDLogDebug("点击了 \(index)")
+                    if breadcrumbList.count != (index+1) {
+                        self.delegate?.breadcrumbTap(name: bar.name, distinguished: bar.key)
+                    }
+                })
+            }
+            var size = self.breadcrumbScrollView.contentSize;
+            size.width = oX;
+            self.breadcrumbScrollView.showsHorizontalScrollIndicator = true;
+            self.breadcrumbScrollView.contentSize = size;
+            self.breadcrumbScrollView.bounces = true;
+            
+        }
+    }
+
+}

+ 101 - 0
o2ios/O2Platform/Contact-通讯录/View/UnitPickerTableViewCell.swift

@@ -0,0 +1,101 @@
+//
+//  UnitPickerTableViewCell.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/13.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+
+protocol UnitPickerNextBtnDelegate {
+    func next(unitName: String?, unitDistinguishedName: String?)
+}
+
+class UnitPickerTableViewCell: UITableViewCell {
+    
+    var delegate: UnitPickerNextBtnDelegate?
+
+    lazy public var trueImage: UIImage = {
+        return UIImage(named: "selected")!
+    }()
+    
+    lazy public var falseImage: UIImage = {
+        return UIImage(named: "unselected")!
+    }()
+    
+    @IBAction func clickNextBtn(_ sender: UIButton) {
+        if delegate != nil {
+            delegate?.next(unitName: unitInfo?.name, unitDistinguishedName: unitInfo?.distinguishedName)
+        }
+    }
+    @IBOutlet weak var nextLevelBtn: UIButton!
+    @IBOutlet weak var unitIconLabel: UILabel!
+    @IBOutlet weak var unitNameLabel: UILabel!
+    @IBOutlet weak var checkImageView: UIImageView!
+    @IBOutlet weak var unitIconBgImageView: UIImageView!
+    
+    
+    private var unitInfo: OOUnitModel?
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        self.checkImageView.image = falseImage
+        self.unitIconBgImageView.image = O2ThemeManager.image(for: "Icon.pic_oval")
+    }
+
+    override func setSelected(_ selected: Bool, animated: Bool) {
+        super.setSelected(selected, animated: animated)
+    }
+    
+    func loadUnitInfo(info: OOUnitModel, checked: Bool) {
+        self.unitInfo = info
+        if let name = info.name {
+            unitIconLabel.text = NSString(string: name).substring(to: 1)
+        }
+        self.unitIconLabel.isHidden = false
+        self.checkImageView.isHidden = false
+        self.nextLevelBtn.isHidden = false
+        self.unitIconBgImageView.layer.masksToBounds = true
+        self.unitIconBgImageView.layer.cornerRadius =  self.unitIconBgImageView.width / 2.0
+        self.unitIconBgImageView.image = O2ThemeManager.image(for: "Icon.pic_oval")
+        self.unitNameLabel.text = info.name
+        if checked {
+            self.checkImageView.image = trueImage
+        }else {
+            self.checkImageView.image = falseImage
+        }
+    }
+    
+    func loadUnitNotCheck(info: OOUnitModel) {
+        self.unitInfo = info
+        if let name = info.name {
+            unitIconLabel.text = NSString(string: name).substring(to: 1)
+        }
+        self.unitIconBgImageView.layer.masksToBounds = true
+        self.unitIconBgImageView.layer.cornerRadius =  self.unitIconBgImageView.width / 2.0
+        self.unitIconBgImageView.image = O2ThemeManager.image(for: "Icon.pic_oval")
+        self.unitNameLabel.text = info.name
+        self.checkImageView.isHidden = true
+        self.unitIconLabel.isHidden = false
+        self.nextLevelBtn.isHidden = false
+    }
+    
+    func loadIdentity(identity: OOIdentityModel, checked: Bool) {
+        self.checkImageView.isHidden = false
+        self.unitIconLabel.isHidden = true
+        self.nextLevelBtn.isHidden = true
+        self.unitNameLabel.text = identity.name
+        self.unitIconBgImageView.layer.masksToBounds = true
+        self.unitIconBgImageView.layer.cornerRadius =  self.unitIconBgImageView.width / 2.0
+        let urlstr = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.personIconByNameQueryV2, parameter: ["##name##":identity.person as AnyObject], generateTime: false)
+        let url = URL(string: urlstr!)
+        self.unitIconBgImageView.hnk_setImageFromURL(url!)
+        if checked {
+            self.checkImageView.image = trueImage
+        }else {
+            self.checkImageView.image = falseImage
+        }
+    }
+
+}

+ 223 - 0
o2ios/O2Platform/Contact-通讯录/ViewModel/ContactPickerViewModel.swift

@@ -0,0 +1,223 @@
+//
+//  ContactPickerViewModel.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/13.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import Promises
+
+
+class ContactPickerViewModel: NSObject {
+    override init() {
+        super.init()
+    }
+    
+    private let orgControlAPI = OOMoyaProvider<OOContactAPI>()
+    private let orgExpressAPI = OOMoyaProvider<OOContactExpressAPI>()
+
+}
+
+
+extension ContactPickerViewModel {
+    
+    //
+    // 组织查询
+    // @param parent 上级组织id -1就是顶层
+    // @param topList 顶级组织id 列表
+    // @param unitType 组织类型
+    //
+    func loadUnitList(parent: String, topList: [String] = [], unitType: String = "") -> Promise<[OOUnitModel]>  {
+        if parent == "-1" { //顶层
+            if unitType.isEmpty {
+                if topList.isEmpty {
+                    return self.unitListTop()
+                }else {
+                    return self.unitListByIds(distinguishedNameList: topList)
+                }
+            }else {
+                if topList.isEmpty {
+                    return self.unitListByType(type: unitType, parentUnit: nil)
+                }else {//这里没有查询 toplist过滤 组织类型的接口
+                    return self.unitListByIds(distinguishedNameList: topList)
+                }
+            }
+        }else {
+            if unitType.isEmpty {
+                return self.unitSubList(parent: parent)
+            }else {
+                return self.unitListByType(type: unitType, parentUnit: parent)
+            }
+        }
+    }
+    
+    //
+    // 身份查询
+    // @param unit 上级组织id
+    // @param dutyList 过滤的职责
+    //
+    func loadIdentityList(dutyList:[String], unit:String) -> Promise<[OOIdentityModel]>  {
+        if dutyList.isEmpty {
+            return self.identityListByUnit(unit: unit)
+        }else {
+            return self.identityListByUnitFilterDutyList(unit: unit, dutyList: dutyList)
+        }
+    }
+    
+    //
+    // 分页查询群组 每页20条
+    // @param lastId
+    //
+    func loadGroupList(lastId: String) -> Promise<[OOGroupModel]> {
+        return Promise{ fulfill, reject in
+            self.orgControlAPI.request(.groupListNext(lastId, 20), completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOGroupModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    //
+    // 搜索查询人员
+    //
+    func searchPersonList(searchText: String) -> Promise<[OOPersonModel]>  {
+        return Promise{ fulfill, reject in
+            self.orgControlAPI.request(.personLike(searchText), completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOPersonModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    
+    //获取顶级组织列表
+    private func unitListTop() -> Promise<[OOUnitModel]> {
+        return Promise { fulfill, reject in
+            self.orgControlAPI.request(.listTop, completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOUnitModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    //根据父获取子组织列表
+    private func unitSubList(parent: String) -> Promise<[OOUnitModel]>  {
+        return Promise { fulfill, reject in
+            self.orgControlAPI.request(.listSubDirect(parent), completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOUnitModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    //根据id列表获取组织对象列表
+    private func unitListByIds(distinguishedNameList: [String]) -> Promise<[OOUnitModel]>  {
+        return Promise { fulfill, reject in
+            self.orgControlAPI.request(.unitList(distinguishedNameList), completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOUnitModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    //根据组织类型返回组织列表,parentUnit为空就是返回顶级组织列表
+    private func unitListByType(type: String, parentUnit: String?)  -> Promise<[OOUnitModel]>  {
+        var unitList: [String] = []
+        if parentUnit != nil && parentUnit?.isEmpty != true {
+            unitList = [parentUnit!]
+        }
+        return Promise { fulfill, reject in
+            self.orgControlAPI.request(.unitListByType(type, unitList), completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOUnitModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    //e根据组织查询身份列表
+    private func identityListByUnit(unit: String) -> Promise<[OOIdentityModel]> {
+        return Promise{ fulfill, reject in
+            self.orgControlAPI.request(.identityListByUnit(unit), completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOIdentityModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    private func identityListByUnitFilterDutyList(unit: String, dutyList:[String]) -> Promise<[OOIdentityModel]> {
+        return Promise{ fulfill, reject in
+            self.orgExpressAPI.request(.identityListByUnitAndDuty(dutyList, unit), completion: { (result) in
+                let response = OOResult<BaseModelClass<[OOIdentityModel]>>(result)
+                if response.isResultSuccess() {
+                    if let data = response.model?.data {
+                        fulfill(data)
+                    }else {
+                        reject(OOAppError.jsonMapping(message: "返回数据为空!!", statusCode: 1024, data: nil))
+                    }
+                }else {
+                    reject(response.error!)
+                }
+            })
+        }
+    }
+    
+    
+    
+}

+ 2 - 2
o2ios/O2Platform/Info.plist

@@ -17,7 +17,7 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>4.3.4</string>
+	<string>4.3.5</string>
 	<key>CFBundleURLTypes</key>
 	<array>
 		<dict>
@@ -32,7 +32,7 @@
 		</dict>
 	</array>
 	<key>CFBundleVersion</key>
-	<string>54</string>
+	<string>55</string>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
 	<key>NSAppTransportSecurity</key>

+ 4 - 2
o2ios/O2Platform/VoiceAI-语音处理/Controller/OOVoiceAIController.swift

@@ -29,7 +29,7 @@ class OOVoiceAIController: UIViewController {
     var voice: AVSpeechSynthesisVoice!
     
     // 语音识别
-    let recognizeBus = 1024
+    let recognizeBus = 0
     var recognizer: SFSpeechRecognizer!
     var recAudioEngine: AVAudioEngine!
     var recAudioInputNode: AVAudioInputNode!
@@ -289,8 +289,10 @@ extension OOVoiceAIController: SFSpeechRecognitionTaskDelegate {
         DDLogInfo("finish recognize result...........")
         let best = recognitionResult.bestTranscription.formattedString
         DDLogInfo("最佳:\(best)")
+        let removePunctuation = best.trimmingCharacters(in: CharacterSet.punctuationCharacters)
+        DDLogInfo("最佳去掉标点:\(removePunctuation)")
         if !self.closeVC {
-            self.viewModel.command = best
+            self.viewModel.command = removePunctuation
             self.lastRecognizeTime = -1
         }
     }

+ 2 - 2
o2ios/O2Platform/VoiceAI-语音处理/View/OOCircleRippleView.swift

@@ -25,8 +25,8 @@ class OOCircleRippleView: UIView {
         let centerPoint = CGPoint(x: self.width/2, y: self.height/2)
         let beizerPath: UIBezierPath = UIBezierPath(arcCenter: centerPoint, radius: (self.frame.width * self.initSize), startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: true)
         let circlelayer = CAShapeLayer()
-        circlelayer.theme_fillColor = ThemeCGColorPicker(colors: "Base.base_color")// circleColor.cgColor // 填充颜色
-        circlelayer.theme_strokeColor = ThemeCGColorPicker(colors: "Base.base_color") // 边框颜色
+        circlelayer.fillColor  = base_color.cgColor// circleColor.cgColor // 填充颜色
+        circlelayer.strokeColor = base_color.cgColor // 边框颜色
         circlelayer.path = beizerPath.cgPath
         self.layer.addSublayer(circlelayer)
     }

+ 9 - 4
o2ios/O2Platform/bbs/v/BBSForumCell.swift

@@ -17,10 +17,15 @@ class BBSForumCell: UICollectionViewCell {
     var bbsSectionData:BBSectionListData? {
         didSet {
             self.bbsSectionTitleLabel.text = bbsSectionData?.sectionName
-            //self.bbsSectionIconImageView.image =  UIImage.sd_image(with: Data(base64Encoded: (bbsSectionData?.icon)!, options:NSData.Base64DecodingOptions.ignoreUnknownCharacters))
-            let urlstr = AppDelegate.o2Collect.generateURLWithAppContextKey(BBSContext.bbsContextKey, query: BBSContext.bbsSectionIconQuery, parameter: ["##id##":bbsSectionData?.id as AnyObject], generateTime: false)
-            let url = URL(string: urlstr!)
-            self.bbsSectionIconImageView.hnk_setImageFromURL(url!)
+            if bbsSectionData?.icon != nil {
+                self.bbsSectionIconImageView.image =  UIImage.sd_image(with: Data(base64Encoded: (bbsSectionData?.icon)!, options:NSData.Base64DecodingOptions.ignoreUnknownCharacters))
+            }else {
+                self.bbsSectionIconImageView.image = UIImage(named: "icon_forum_default")
+            }
+            
+//            let urlstr = AppDelegate.o2Collect.generateURLWithAppContextKey(BBSContext.bbsContextKey, query: BBSContext.bbsSectionIconQuery, parameter: ["##id##":bbsSectionData?.id as AnyObject], generateTime: false)
+//            let url = URL(string: urlstr!)
+//            self.bbsSectionIconImageView.hnk_setImageFromURL(url!)
         }
     }
     

+ 3 - 0
o2ios/O2Platform/common/AppDelegate.swift

@@ -19,6 +19,9 @@ import IQKeyboardManagerSwift
 
 
 
+
+
+
 let isProduction = true
 
 @UIApplicationMain

+ 3 - 0
o2ios/O2Platform/common/BaseWebViewUIViewController.swift

@@ -70,6 +70,9 @@ open class BaseWebViewUIViewController: UIViewController {
         //o2m.util
         let o2Util = O2JsApiUtil(viewController: self)
         addScriptMessageHandler(key: "o2mUtil", handler: o2Util)
+        // o2m.biz
+        let biz = O2JsApiBizUtil(viewController: self)
+        addScriptMessageHandler(key: "o2mBiz", handler: biz)
         
         setupWebView()
     }

+ 31 - 7
o2ios/O2Platform/common/HTTP/ContactAPI/OOContactAPI.swift

@@ -12,10 +12,15 @@ import O2OA_Auth_SDK
 
 // MARK: - 所有调用的API
 enum OOContactAPI {
+    
+    //根据组织ID返回组织对象
+    case unitList([String])
     //所有顶层单元
     case listTop
     //所有顶层单元子单元
     case listSubDirect(String)
+    //根据组织类型查询组织,第一个参数是type 第二个参数是上级组织 可以多值
+    case unitListByType(String, [String])
     //单元信息
     case getUnit(String)
     //个人信息(包括部门,群组等)
@@ -31,6 +36,10 @@ enum OOContactAPI {
     
     case personListNext(String,Int)
     
+    case groupListNext(String, Int)
+    //根据组织查询身份列表
+    case identityListByUnit(String)
+    
 }
 
 // MARK: - 通讯录上下文
@@ -59,13 +68,17 @@ extension OOContactAPI:TargetType {
     var path: String {
         switch self {
         case .getPerson(let flag):
-            return "/jaxrs/person/\(flag.urlEscaped)"
+            return "/jaxrs/person/\(flag)"
         case .getUnit(let unitFlag):
-            return "/jaxrs/unit/\(unitFlag.urlEscaped)"
+            return "/jaxrs/unit/\(unitFlag)"
         case .listSubDirect(let unitFlag):
-            return "/jaxrs/unit/list/\(unitFlag.urlEscaped)/sub/direct"
+            return "/jaxrs/unit/list/\(unitFlag)/sub/direct"
         case .listTop:
             return "/jaxrs/unit/list/top"
+        case .unitList(_):
+            return "/jaxrs/unit/list"
+        case .unitListByType(_,_):
+            return "/jaxrs/unit/list/unit/type"
         case .iconByPerson(let pid):
             return "/jaxrs/person/\(pid)/icon"
         case .unitLike(_):
@@ -75,16 +88,23 @@ extension OOContactAPI:TargetType {
         case .personLike(_):
             return "/jaxrs/person/list/like"
         case .personListNext(let flag, let count):
-            return "jaxrs/person/list/\(flag)/next/\(count)"
+            return "/jaxrs/person/list/\(flag)/next/\(count)"
+        case .groupListNext(let flag, let count):
+            return "/jaxrs/group/list/\(flag)/next/\(count)"
+        case .identityListByUnit(let unit):
+            return "/jaxrs/identity/list/unit/\(unit)"
+            
         }
     }
     
     var method: Moya.Method {
         switch self{
-        case .getPerson(_),.getUnit(_),.listTop,.listSubDirect(_),.iconByPerson(_),.personListNext(_, _):
+        case .getPerson(_),.getUnit(_),.listTop,.listSubDirect(_),.iconByPerson(_),.personListNext(_, _),.groupListNext(_, _),.identityListByUnit(_):
             return .get
-        case .unitLike(_),.groupLike(_),.personLike(_):
+        case .unitLike(_),.groupLike(_),.personLike(_),.unitListByType(_, _):
             return .put
+        case .unitList(_):
+            return .post
         }
     }
     
@@ -94,7 +114,7 @@ extension OOContactAPI:TargetType {
     
     var task: Task {
         switch self{
-        case .getPerson(_),.getUnit(_),.listTop,.listSubDirect(_),.personListNext(_,_):
+        case .getPerson(_),.getUnit(_),.listTop,.listSubDirect(_),.personListNext(_,_),.groupListNext(_, _),.identityListByUnit(_):
             return .requestPlain
         case .iconByPerson(_):
             return .requestPlain
@@ -102,6 +122,10 @@ extension OOContactAPI:TargetType {
             return .requestParameters(parameters: ["key":searchText], encoding: JSONEncoding.default)
         case .unitLike(let searchText):
             return .requestParameters(parameters: ["key":searchText], encoding: JSONEncoding.default)
+        case .unitListByType(let type, let parentList):
+            return .requestParameters(parameters: ["type": type, "unitList": parentList], encoding: JSONEncoding.default)
+        case .unitList(let idList):
+            return .requestParameters(parameters: ["unitList": idList], encoding: JSONEncoding.default)
         case .personLike(let searchText):
             return .requestParameters(parameters: ["key":searchText], encoding: JSONEncoding.default)
         }

+ 70 - 0
o2ios/O2Platform/common/HTTP/ContactAPI/OOContactExpressAPI.swift

@@ -0,0 +1,70 @@
+//
+//  OOContactExpressAPI.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/13.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import Moya
+import O2OA_Auth_SDK
+
+//x_organization_assemble_express
+
+enum OOContactExpressAPI {
+    //根据职务列表和组织查询 组织下对应的身份列表
+    case identityListByUnitAndDuty([String], String)
+}
+
+
+extension OOContactExpressAPI: OOAPIContextCapable {
+    var apiContextKey: String {
+        return "x_organization_assemble_express"
+    }
+}
+
+extension OOContactExpressAPI: OOAccessTokenAuthorizable {
+    var shouldAuthorize: Bool {
+        return true
+    }
+}
+
+extension OOContactExpressAPI: TargetType {
+    var baseURL: URL {
+        let model = O2AuthSDK.shared.o2APIServer(context: .x_organization_assemble_express)
+        let baseURLString = "\(model?.httpProtocol ?? "http")://\(model?.host ?? ""):\(model?.port ?? 0)\(model?.context ?? "")"
+        return URL(string: baseURLString)!
+    }
+    
+    var path: String {
+        switch self {
+        case .identityListByUnitAndDuty(_, _):
+            return "/jaxrs/unitduty/list/identity/unit/name/object"
+        }
+    }
+    
+    var method: Moya.Method {
+        switch self {
+        case .identityListByUnitAndDuty(_, _):
+            return .post
+        }
+    }
+    
+    var sampleData: Data {
+        return "".data(using: String.Encoding.utf8)!
+    }
+    
+    var task: Task {
+        switch self {
+        case .identityListByUnitAndDuty(let dutyList, let unit):
+            return .requestParameters(parameters: ["nameList": dutyList, "unit": unit], encoding: JSONEncoding.default)
+        }
+    }
+    
+    var headers: [String : String]? {
+        return nil
+    }
+    
+    
+}
+

+ 1 - 0
o2ios/O2Platform/common/o2JsApi/O2BaseJsMessageHandler.swift

@@ -35,6 +35,7 @@ class O2BaseJsMessageHandler: O2WKScriptMessageHandlerImplement {
             }else {
                 DDLogDebug("console.log: unkown type \(message.body)")
             }
+            break
         case "ReplyAction":
             DDLogDebug("回复 帖子 message.body = \(message.body)")
             let pId : String?

+ 212 - 0
o2ios/O2Platform/common/o2JsApi/O2JsApiBizUtil.swift

@@ -0,0 +1,212 @@
+//
+//  O2JsApiBizUtil.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/19.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+import WebKit
+import CocoaLumberjack
+
+
+class O2JsApiBizUtil: O2WKScriptMessageHandlerImplement {
+    
+    let viewController: BaseWebViewUIViewController
+    
+    init(viewController: BaseWebViewUIViewController) {
+        self.viewController = viewController
+    }
+    
+    
+    func userController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        if message.body is NSString {
+            let json = message.body as! NSString
+            DDLogDebug("message json:\(json)")
+            if let jsonData = String(json).data(using: .utf8) {
+                let dicArr = try! JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as! [String:AnyObject]
+                if let type = dicArr["type"] as? String {
+                    switch type {
+                    case "contact.departmentPicker":
+                        departmentsPicker(json: String(json))
+                        break
+                    case "contact.identityPicker":
+                        identityPicker(json: String(json))
+                        break
+                    case "contact.groupPicker":
+                        groupPicker(json: String(json))
+                        break
+                    case "contact.personPicker":
+                        personPicker(json: String(json))
+                        break
+                    case "contact.complexPicker":
+                        complexPicker(json: String(json))
+                        break
+                    default :
+                        DDLogError("notification类型不正确, type: \(type)")
+                    }
+                }else {
+                    DDLogError("util类型不存在 json解析异常。。。。。")
+                }
+            }else {
+                DDLogError("消息json解析异常。。。")
+            }
+        }else {
+            DDLogError("message 消息 body 类型不正确。。。")
+        }
+    }
+    
+    private func departmentsPicker(json: String) {
+        if let alert = O2WebViewBaseMessage<O2BizUnitPickerMessage>.deserialize(from: json) {
+            let maxNumber = alert.data?.maxNumber ?? 0
+            let pickedDepartments = alert.data?.pickedDepartments ?? []
+            let multiple = alert.data?.multiple ?? true
+            let orgType = alert.data?.orgType ?? ""
+            let topList = alert.data?.topList ?? []
+            let callback = alert.callback ?? ""
+            self.showPicker(callback: callback,
+                            pickMode: ["departmentPicker"],
+                            maxNumber: maxNumber,
+                            multiple: multiple,
+                            orgType: orgType,
+                           topList: topList,
+                           deptPickedList: pickedDepartments)
+        }else {
+            DDLogError("departmentsPicker, 解析json失败")
+        }
+    }
+    private func identityPicker(json: String) {
+        if let alert = O2WebViewBaseMessage<O2BizIdentityPickerMessage>.deserialize(from: json) {
+            let maxNumber = alert.data?.maxNumber ?? 0
+            let pickedIdentities = alert.data?.pickedIdentities ?? []
+            let multiple = alert.data?.multiple ?? true
+            let dutyList = alert.data?.duty ?? []
+            let topList = alert.data?.topList ?? []
+            let callback = alert.callback ?? ""
+            self.showPicker(callback: callback,
+                            pickMode: ["identityPicker"],
+                            maxNumber: maxNumber,
+                            multiple: multiple,
+                            dutyList: dutyList,
+                            topList: topList,
+                            idPickedList: pickedIdentities)
+        }else {
+            DDLogError("identityPicker, 解析json失败")
+        }
+    }
+    private func groupPicker(json: String) {
+        if let alert = O2WebViewBaseMessage<O2BizGroupPickerMessage>.deserialize(from: json) {
+            let maxNumber = alert.data?.maxNumber ?? 0
+            let pickedGroups = alert.data?.pickedGroups ?? []
+            let multiple = alert.data?.multiple ?? true
+            let callback = alert.callback ?? ""
+            self.showPicker(callback: callback,
+                            pickMode: ["groupPicker"],
+                            maxNumber: maxNumber,
+                            multiple: multiple,
+                            groupPickedList: pickedGroups)
+        }else {
+            DDLogError("groupPicker, 解析json失败")
+        }
+    }
+    private func personPicker(json: String) {
+        if let alert = O2WebViewBaseMessage<O2BizPersonPickerMessage>.deserialize(from: json) {
+            let maxNumber = alert.data?.maxNumber ?? 0
+            let pickedUsers = alert.data?.pickedUsers ?? []
+            let multiple = alert.data?.multiple ?? true
+            let callback = alert.callback ?? ""
+            self.showPicker(callback: callback,
+                            pickMode: ["personPicker"],
+                            maxNumber: maxNumber,
+                            multiple: multiple,
+                            userPickedList: pickedUsers)
+        }else {
+            DDLogError("personPicker, 解析json失败")
+        }
+    }
+    private func complexPicker(json: String) {
+        if let alert = O2WebViewBaseMessage<O2BizComplexPickerMessage>.deserialize(from: json) {
+            let pickMode = alert.data?.pickMode ?? []
+            let maxNumber = alert.data?.maxNumber ?? 0
+            let pickedDepartments = alert.data?.pickedDepartments ?? []
+            let pickedIdentities = alert.data?.pickedIdentities ?? []
+            let pickedGroups = alert.data?.pickedGroups ?? []
+            let pickedUsers = alert.data?.pickedUsers ?? []
+            let multiple = alert.data?.multiple ?? true
+            let orgType = alert.data?.orgType ?? ""
+            let dutyList = alert.data?.duty ?? []
+            let topList = alert.data?.topList ?? []
+            let callback = alert.callback ?? ""
+            self.showPicker(callback: callback,
+                             pickMode: pickMode,
+                            maxNumber: maxNumber,
+                            multiple: multiple,
+                            orgType: orgType,
+                            dutyList: dutyList,
+                            topList: topList,
+                            deptPickedList: pickedDepartments,
+                            idPickedList: pickedIdentities,
+                            groupPickedList: pickedGroups,
+                            userPickedList: pickedUsers
+                           )
+        }else {
+            DDLogError("complexPicker, 解析json失败")
+        }
+    }
+    
+    private func showPicker(callback: String, pickMode:[String], maxNumber: Int = 0, multiple:Bool = true, orgType: String = "", dutyList:[String] = [], topList:[String] = [], deptPickedList:[String] = [], idPickedList:[String] = [], groupPickedList:[String] = [], userPickedList:[String] = []) {
+        var modes:[ContactPickerType] = []
+        if pickMode.count > 0 {
+            pickMode.forEach { (str) in
+                switch str {
+                case "departmentPicker":
+                    modes.append(ContactPickerType.unit)
+                    break
+                case "identityPicker":
+                    modes.append(ContactPickerType.identity)
+                    break
+                case "groupPicker":
+                    modes.append(ContactPickerType.group)
+                    break
+                case "personPicker":
+                    modes.append(ContactPickerType.person)
+                    break
+                default:
+                    break
+                }
+            }
+        }else {
+            modes = [ContactPickerType.unit, ContactPickerType.identity, ContactPickerType.group, ContactPickerType.person]
+        }
+        
+        if let v = ContactPickerViewController.providePickerVC(
+            pickerModes:modes,
+            topUnitList: topList,
+            unitType: orgType,
+            maxNumber: maxNumber,
+            multiple: multiple,
+            dutyList: dutyList,
+            initDeptPickedArray: deptPickedList,
+            initIdPickedArray: idPickedList,
+            initGroupPickedArray: groupPickedList,
+            initUserPickedArray: userPickedList,
+            pickedDelegate: { (result: O2BizContactPickerResult) in
+                let json = result.toJSONString() ?? "{}"
+                DDLogDebug("返回选择结果:\(json)")
+                self.evaluateJs(callBackJs: "\(callback)('\(json)')")
+                }
+            ) {
+            self.viewController.navigationController?.pushViewController(v, animated: true)
+        }else {
+            self.viewController.showError(title: "选择器生成错误。。。。")
+        }
+    }
+    
+    private func evaluateJs(callBackJs: String) {
+        DDLogDebug("执行回调js:"+callBackJs)
+        self.viewController.webView.evaluateJavaScript(callBackJs, completionHandler: { (result, err) in
+            DDLogDebug("回调js执行完成!")
+        })
+    }
+}

+ 8 - 0
o2ios/O2Platform/common/o2JsApi/O2JsApiNotification.swift

@@ -33,20 +33,28 @@ class O2JsApiNotification: O2WKScriptMessageHandlerImplement {
                     switch type {
                     case "alert":
                         alert(json: String(json))
+                        break
                     case "confirm":
                         confirm(json: String(json))
+                        break
                     case "prompt":
                         prompt(json: String(json))
+                        break
                     case "vibrate":
                         vibrate(json: String(json))
+                        break
                     case "toast":
                         toast(json: String(json))
+                        break
                     case "actionSheet":
                         actionSheet(json: String(json))
+                        break
                     case "showLoading":
                         showLoading(json: String(json))
+                        break
                     case "hideLoading":
                         hideLoading(json: String(json))
+                        break
                     default:
                         DDLogError("notification类型不正确, type: \(type)")
                     }

+ 11 - 0
o2ios/O2Platform/common/o2JsApi/O2JsApiUtil.swift

@@ -30,26 +30,37 @@ class O2JsApiUtil: O2WKScriptMessageHandlerImplement {
                     switch type {
                     case "date.datePicker":
                         datePicker(json: String(json))
+                        break
                     case "date.timePicker":
                         timePicker(json: String(json))
+                        break
                     case "date.dateTimePicker":
                         dateTimePicker(json: String(json))
+                        break
                     case "calendar.chooseOneDay":
                         calendarPickDay(json: String(json))
+                        break
                     case "calendar.chooseDateTime":
                         calendarPickerDateTime(json: String(json))
+                        break
                     case "calendar.chooseInterval":
                         calendarPickerDateInterval(json: String(json))
+                        break
                     case "device.getPhoneInfo":
                         getPhoneInfo(json: String(json))
+                        break
                     case "device.scan":
                         scan(json: String(json))
+                        break
                     case "navigation.setTitle":
                         navigationSetTitle(json: String(json))
+                        break
                     case "navigation.close":
                         navigationClose(json: String(json))
+                        break
                     case "navigation.goBack":
                         navigationGoBack(json: String(json))
+                        break
                     default:
                         DDLogError("notification类型不正确, type: \(type)")
                     }

+ 1 - 0
o2ios/O2Platform/config/O2URLContext.swift

@@ -77,6 +77,7 @@ struct PersonContext {
 struct ContactContext {
     static let contactsContextKey = "x_organization_assemble_express"
     static let contactsContextKeyV2 = "x_organization_assemble_control"
+    static let topLevelUnitByIdentity = "jaxrs/unit/identity/level/object" // x_organization_assemble_express 上下文下的
     static let personInfoByNameQuery = "jaxrs/person/##name##"
     static let personIconByNameQuery = "servlet/icon/##name##" //获取图像
     static let personIconByNameQueryV2 = "jaxrs/person/##name##/icon"

+ 1 - 1
o2ios/O2Platform/contacts/c/ContactCompanyDeptController.swift

@@ -83,7 +83,7 @@ class ContactCompanyDeptController: UITableViewController {
     }
     
     
-    func loadCompData(_ obj:AnyObject?){
+    @objc func loadCompData(_ obj:AnyObject?){
         self.showMessage(title: "加载中...")
         Alamofire.request(self.myCompanyURL!).responseJSON {
             response in

+ 21 - 6
o2ios/O2Platform/contacts/c/ContactDeptPersonController.swift

@@ -19,7 +19,7 @@ class ContactDeptPersonController: UITableViewController, UITextViewDelegate {
     
     var superOrgUnit : OrgUnit? {
         didSet {
-            subUnitURL = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.subUnitByNameQuery, parameter: ["##name##":(superOrgUnit?.unique)! as AnyObject])
+            subUnitURL = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.subUnitByNameQuery, parameter: ["##name##":(superOrgUnit?.distinguishedName)! as AnyObject])
 
             subIdentityURL = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.subIdentityByNameQuery, parameter: ["##name##":(superOrgUnit?.unique)! as AnyObject])
             if self.headBars.count == 0 {
@@ -27,7 +27,7 @@ class ContactDeptPersonController: UITableViewController, UITextViewDelegate {
             }else{
                 var tag = true
                 for (index,unit) in self.headBars.enumerated() {
-                    if unit.id! == self.superOrgUnit!.id! {
+                    if unit.distinguishedName! == self.superOrgUnit!.distinguishedName! {
                         tag = false
                         let n = self.headBars.count - index
                         if n>1 {
@@ -101,7 +101,7 @@ class ContactDeptPersonController: UITableViewController, UITextViewDelegate {
         
         cell.cellViewModel = cellMod
         if indexPath.section == 0 {
-            cell.headBarView.delegate = self
+            cell.delegate = self
         }
         return cell
     }
@@ -140,9 +140,9 @@ class ContactDeptPersonController: UITableViewController, UITextViewDelegate {
         if let scheme = URL.scheme {
             switch scheme {
             case "reloadto" :
-                let id = (URL.description as NSString).substring(from: 9)
+                let distinguishedName = (URL.description as NSString).substring(from: 9)
                 for unit in self.headBars {
-                    if id == unit.id! {
+                    if distinguishedName == unit.distinguishedName! {
                         self.superOrgUnit = unit
                         self.reloadView()
                         break;
@@ -167,7 +167,7 @@ class ContactDeptPersonController: UITableViewController, UITextViewDelegate {
     }
     
     
-    func loadMyDeptData(_ sender:AnyObject?){
+    @objc func loadMyDeptData(_ sender:AnyObject?){
         let urls = [0:"111",1:subUnitURL,2:subIdentityURL]
         self.showMessage(title:"加载中...")
         var num = 0
@@ -235,3 +235,18 @@ class ContactDeptPersonController: UITableViewController, UITextViewDelegate {
     }
 
 }
+
+
+extension ContactDeptPersonController: ContactItemCellBreadcrumbClickDelegate {
+    func breadcrumbTap(name: String, distinguished: String) {
+        for unit in self.headBars {
+            if distinguished == unit.distinguishedName! {
+                self.superOrgUnit = unit
+                self.reloadView()
+                break;
+            }
+        }
+    }
+    
+    
+}

+ 93 - 35
o2ios/O2Platform/contacts/c/ContactHomeViewController.swift

@@ -31,6 +31,17 @@ class ContactHomeViewController: UITableViewController {
         return url
     }
     
+    //当前用户信息 用来查询身份的
+    var myPersonURL:String? {
+        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(PersonContext.personContextKey, query: PersonContext.personInfoQuery, parameter: nil)
+        return url
+    }
+    //根据身份查询顶级组织
+    var topUnitByIdentityURL: String? {
+        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKey, query: ContactContext.topLevelUnitByIdentity, parameter: nil)
+        return url
+    }
+    
     let searchUrl = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.personSearchByKeyQueryV2, parameter: nil)
     
     var searchController : UISearchController!
@@ -250,25 +261,14 @@ class ContactHomeViewController: UITableViewController {
         }
         self.showMessage(title: "加载中...")
         for (order,url) in urls {
-            Alamofire.request(url!, method: .get, parameters: nil, encoding:URLEncoding.default, headers: ["X-ORDER":String(order)]).validate().responseJSON {
-                response in
-                switch response.result {
-                case .success(let val):
-                    let objects = JSON(val)["data"]
-                    print(objects.description)
-                    self.contacts[order]?.removeAll()
-                    switch order {
-                    case 1:
-                        let tile = HeadTitle(name: "组织架构", icon: O2ThemeManager.string(for: "Icon.icon_bumen")!)
-                        let vmt = CellViewModel(name: tile.name, sourceObject: tile)
-                        self.contacts[order]?.append(vmt)
-                        if let units = Mapper<OrgUnit>().mapArray(JSONString:objects.description) {
-                            for unit in units{
-                                let vm = CellViewModel(name: unit.name,sourceObject: unit)
-                                self.contacts[order]?.append(vm)
-                            }
-                        }
-                    case 0:
+            if order == 0 {
+                Alamofire.request(url!, method: .get, parameters: nil, encoding:URLEncoding.default, headers: ["X-ORDER":String(order)]).validate().responseJSON {
+                    response in
+                    switch response.result {
+                    case .success(let val):
+                        let objects = JSON(val)["data"]
+                        print(objects.description)
+                        self.contacts[order]?.removeAll()
                         let tile = HeadTitle(name: "我的部门", icon: O2ThemeManager.string(for: "Icon.icon_company")!)
                         let vmt = CellViewModel(name: tile.name, sourceObject: tile)
                         self.contacts[order]?.append(vmt)
@@ -280,32 +280,90 @@ class ContactHomeViewController: UITableViewController {
                                         let vm = CellViewModel(name: unit.name,sourceObject: unit as AnyObject)
                                         self.contacts[order]?.append(vm)
                                     }
-                                    
                                 }
                             }
                         }
-                    case 2:
-                        break
-                    default:
-                        break
-                        //DDLogDebug(objects.description)
+                    case .failure(let err):
+                        DDLogError(err.localizedDescription)
                     }
                     
-                case .failure(let err):
-                    DDLogError(err.localizedDescription)
+                    count += 1
+                    if count == urls.count {
+                        self.dismissProgressHUD()
+                        if self.tableView.mj_header.isRefreshing() == true {
+                            self.tableView.mj_header.endRefreshing()
+                        }
+                    }
+                    self.tableView.reloadData()
+                    
                 }
+            } else if order == 1 {
                 
-                count += 1
-                if count == urls.count {
-                    self.dismissProgressHUD()
-                    if self.tableView.mj_header.isRefreshing() == true {
-                        self.tableView.mj_header.endRefreshing()
+                Alamofire.request(myPersonURL!, method: .get, parameters: nil, encoding:URLEncoding.default, headers: ["X-ORDER":String(order)]).validate().responseJSON {
+                    response in
+                    switch response.result {
+                    case .success(let val):
+                        let objects = JSON(val)["data"]
+                        print(objects.description)
+                        var identity = ""
+                        if let person = Mapper<PersonV2>().map(JSONString:objects.description) {
+                            if let identities = person.woIdentityList, identities.count > 0 {
+                                identity = identities[0].distinguishedName ?? ""
+                            }
+                        }
+                        if !identity.isEmpty {
+                            Alamofire.request(self.topUnitByIdentityURL!, method: .post, parameters: ["identity": identity as AnyObject, "level": 1 as AnyObject], encoding: JSONEncoding.default, headers: nil).responseJSON(completionHandler: { (res) in
+                                switch res.result {
+                                case .success(let val):
+                                    let objects = JSON(val)["data"]
+                                    print(objects.description)
+                                    
+                                    if let unit = Mapper<OrgUnit>().map(JSONString:objects.description) {
+                                        unit.subDirectUnitCount = 1 //这个接口查询出来的组织没有下级组织的数量,假设是有下级组织的
+                                        let tile = HeadTitle(name: "组织架构", icon: O2ThemeManager.string(for: "Icon.icon_bumen")!)
+                                        let vmt = CellViewModel(name: tile.name, sourceObject: tile)
+                                        self.contacts[order]?.append(vmt)
+                                        // 顶级组织
+                                        let vm = CellViewModel(name: unit.name,sourceObject: unit)
+                                        self.contacts[order]?.append(vm)
+                                    }
+                                    break
+                                case .failure(let err):
+                                    DDLogError(err.localizedDescription)
+                                }
+                                count += 1
+                                if count == urls.count {
+                                    self.dismissProgressHUD()
+                                    if self.tableView.mj_header.isRefreshing() == true {
+                                        self.tableView.mj_header.endRefreshing()
+                                    }
+                                }
+                                self.tableView.reloadData()
+                            })
+                        }else {
+                            count += 1
+                            if count == urls.count {
+                                self.dismissProgressHUD()
+                                if self.tableView.mj_header.isRefreshing() == true {
+                                    self.tableView.mj_header.endRefreshing()
+                                }
+                            }
+                            self.tableView.reloadData()
+                        }
+                        
+                    case .failure(let err):
+                        DDLogError(err.localizedDescription)
+                        count += 1
+                        if count == urls.count {
+                            self.dismissProgressHUD()
+                            if self.tableView.mj_header.isRefreshing() == true {
+                                self.tableView.mj_header.endRefreshing()
+                            }
+                        }
+                        self.tableView.reloadData()
                     }
                 }
-                self.tableView.reloadData()
-                
             }
-            //debugPrint(request)
         }
     }
     

+ 37 - 7
o2ios/O2Platform/contacts/v/ContactItemCell.swift

@@ -11,11 +11,20 @@ import Alamofire
 import AlamofireImage
 import AlamofireObjectMapper
 
+
+protocol ContactItemCellBreadcrumbClickDelegate {
+    func breadcrumbTap(name: String, distinguished: String)
+}
+
 class ContactItemCell: UITableViewCell {
     @IBOutlet weak var iconImageView: UIImageView!
     @IBOutlet weak var nameLabel: UILabel!
-    @IBOutlet weak var headBarView: UITextView!
+    
     @IBOutlet weak var iconTagLabel: UILabel!
+    // 面包屑导航栏
+    @IBOutlet weak var headBarScrollView: UIScrollView!
+    
+    var delegate: ContactItemCellBreadcrumbClickDelegate?
     
     var cellViewModel:CellViewModel? {
         didSet {
@@ -87,16 +96,37 @@ class ContactItemCell: UITableViewCell {
     private func configTitle(_ t:HeadTitle){
         let title = t
         if title.isBar {
-            headBarView.textContainer.maximumNumberOfLines = 1
-            headBarView.textContainer.lineBreakMode = .byTruncatingTail
-            headBarView.text = ""
+            
+            self.headBarScrollView.removeSubviews()
+            var oX = CGFloat(4.0)
+            
             if let bars = title.barText {
                 bars.forEachEnumerated { (index, bar) in
+                    var name: String
+                    var textColor:UIColor
                     if bars.count == (index+1) {
-                        self.headBarView.appendLinkString(string: bar.name ?? "")
-                    }else{
-                        self.headBarView.appendLinkString(string: (bar.name ?? "") + " > ", withURLString: "reloadto:\(bar.id ?? "")")
+                        name = bar.name ?? ""
+                        textColor = base_color
+                    }else {
+                        name = bar.name ?? ""
+                        name = name + " > "
+                        textColor = UIColor(hex:"#333333")
                     }
+                    let firstSize = name.getSize(with: 15)
+                    let oY = (self.headBarScrollView.bounds.height - firstSize.height) / 2
+                    let firstLabel = UILabel(frame: CGRect(x: CGFloat(oX), y: oY, width: firstSize.width, height: firstSize.height))
+                    firstLabel.textAlignment = .left
+                    let textAttributes = [NSAttributedString.Key.foregroundColor: textColor,NSAttributedString.Key.font:UIFont(name:"PingFangSC-Regular",size:15)!]
+                    firstLabel.attributedText = NSMutableAttributedString(string: name, attributes: textAttributes)
+                    firstLabel.sizeToFit()
+                    oX += firstSize.width
+                    self.headBarScrollView.addSubview(firstLabel)
+                    firstLabel.addTapGesture(action: { (rec) in
+                        if bars.count != (index+1) {
+                            self.delegate?.breadcrumbTap(name: bar.name ?? "", distinguished: bar.distinguishedName ?? "")
+                        }
+                    })
+                    
                 }
             }
         }else{

+ 757 - 0
o2ios/O2Platform/framework/MagicBytesMimeType/MimeType.swift

@@ -0,0 +1,757 @@
+//
+//  MimeType.swift
+//  源码来自 https://github.com/sendyhalim/Swime
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/23.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import Foundation
+
+/// https://en.wikipedia.org/wiki/List_of_file_signatures 这个wiki说了 各种文件的 magic bytes
+/// List of type shorthands
+/// with this enum we can check mime type with addition of swift type checker
+/// ```
+/// let swime = Swime(data: data)
+/// swime.type
+/// ```
+
+public struct MagicBytes {
+    
+    public enum FileType {
+        case amr
+        case ar
+        case avi
+        case bmp
+        case bz2
+        case cab
+        case cr2
+        case crx
+        case deb
+        case dmg
+        case eot
+        case epub
+        case exe
+        case flac
+        case flif
+        case flv
+        case gif
+        case gz
+        case ico
+        case jpg
+        case jxr
+        case lz
+        case m4a
+        case m4v
+        case mid
+        case mkv
+        case mov
+        case mp3
+        case mp4
+        case mpg
+        case msi // doc xls ppt msg
+        case mxf
+        case nes
+        case ogg
+        case opus
+        case otf
+        case pdf
+        case png
+        case ps
+        case psd
+        case rar
+        case rpm
+        case rtf
+        case sevenZ // 7z, Swift does not let us define enum that starts with a digit
+        case sqlite
+        case swf
+        case tar
+        case tif
+        case ttf
+        case wav
+        case webm
+        case webp
+        case wmv
+        case woff
+        case woff2
+        case xpi
+        case xz
+        case z
+        case zip // docx xlsx pptx jar
+    }
+
+}
+
+
+public struct MimeType {
+    /// Mime type string representation. For example "application/pdf"
+    public let mime: String
+    
+    /// Mime type extension. For example "pdf"
+    public let ext: String
+    
+    /// Mime type shorthand representation. For example `.pdf`
+    public let type: MagicBytes.FileType
+    
+    /// Number of bytes required for `MimeType` to be able to check if the
+    /// given bytes match with its mime type magic number specifications.
+    fileprivate let bytesCount: Int
+    
+    /// A function to check if the bytes match the `MimeType` specifications.
+    fileprivate let matches: ([UInt8], Swime) -> Bool
+    
+    ///  Check if the given bytes matches with `MimeType`
+    ///  it will check for the `bytes.count` first before delegating the
+    ///  checker function to `matches` property
+    ///
+    ///  - parameter bytes: Bytes represented with `[UInt8]`
+    ///  - parameter swime: Swime instance
+    ///
+    ///  - returns: Bool
+    public func matches(bytes: [UInt8], swime: Swime) -> Bool {
+        return bytes.count >= bytesCount && matches(bytes, swime)
+    }
+    
+    /// List of all supported `MimeType`s
+    public static let all: [MimeType] = [
+        MimeType(
+            mime: "image/jpeg",
+            ext: "jpg",
+            type: .jpg,
+            bytesCount: 3,
+            matches: { bytes, _ in
+                return bytes[0...2] == [0xFF, 0xD8, 0xFF]
+        }
+        ),
+        MimeType(
+            mime: "image/png",
+            ext: "png",
+            type: .png,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x89, 0x50, 0x4E, 0x47]
+        }
+        ),
+        MimeType(
+            mime: "image/gif",
+            ext: "gif",
+            type: .gif,
+            bytesCount: 3,
+            matches: { bytes, _ in
+                return bytes[0...2] == [0x47, 0x49, 0x46]
+        }
+        ),
+        MimeType(
+            mime: "image/webp",
+            ext: "webp",
+            type: .webp,
+            bytesCount: 12,
+            matches: { bytes, _ in
+                return bytes[8...11] == [0x57, 0x45, 0x42, 0x50]
+        }
+        ),
+        MimeType(
+            mime: "image/flif",
+            ext: "flif",
+            type: .flif,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x46, 0x4C, 0x49, 0x46]
+        }
+        ),
+        MimeType(
+            mime: "image/x-canon-cr2",
+            ext: "cr2",
+            type: .cr2,
+            bytesCount: 10,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x49, 0x49, 0x2A, 0x00] || bytes[0...3] == [0x4D, 0x4D, 0x00, 0x2A]) &&
+                    (bytes[8...9] == [0x43, 0x52])
+        }
+        ),
+        MimeType(
+            mime: "image/tiff",
+            ext: "tif",
+            type: .tif,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x49, 0x49, 0x2A, 0x00]) ||
+                    (bytes[0...3] == [0x4D, 0x4D, 0x00, 0x2A])
+        }
+        ),
+        MimeType(
+            mime: "image/bmp",
+            ext: "bmp",
+            type: .bmp,
+            bytesCount: 2,
+            matches: { bytes, _ in
+                return bytes[0...1] == [0x42, 0x4D]
+        }
+        ),
+        MimeType(
+            mime: "image/vnd.ms-photo",
+            ext: "jxr",
+            type: .jxr,
+            bytesCount: 3,
+            matches: { bytes, _ in
+                return bytes[0...2] == [0x49, 0x49, 0xBC]
+        }
+        ),
+        MimeType(
+            mime: "image/vnd.adobe.photoshop",
+            ext: "psd",
+            type: .psd,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x38, 0x42, 0x50, 0x53]
+        }
+        ),
+        MimeType(
+            mime: "application/epub+zip",
+            ext: "epub",
+            type: .epub,
+            bytesCount: 58,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x50, 0x4B, 0x03, 0x04]) &&
+                    (bytes[30...57] == [
+                        0x6D, 0x69, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C,
+                        0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x65, 0x70, 0x75, 0x62,
+                        0x2B, 0x7A, 0x69, 0x70
+                        ])
+        }
+        ),
+        
+        // Needs to be before `zip` check
+        // assumes signed .xpi from addons.mozilla.org
+        MimeType(
+            mime: "application/x-xpinstall",
+            ext: "xpi",
+            type: .xpi,
+            bytesCount: 50,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x50, 0x4B, 0x03, 0x04]) &&
+                    (bytes[30...49] == [
+                        0x4D, 0x45, 0x54, 0x41, 0x2D, 0x49, 0x4E, 0x46, 0x2F, 0x6D, 0x6F, 0x7A,
+                        0x69, 0x6C, 0x6C, 0x61, 0x2E, 0x72, 0x73, 0x61
+                        ])
+        }
+        ),
+        MimeType(
+            mime: "application/zip",
+            ext: "zip",
+            type: .zip,
+            bytesCount: 50,
+            matches: { bytes, _ in
+                return (bytes[0...1] == [0x50, 0x4B]) &&
+                    (bytes[2] == 0x3 || bytes[2] == 0x5 || bytes[2] == 0x7) &&
+                    (bytes[3] == 0x4 || bytes[3] == 0x6 || bytes[3] == 0x8)
+        }
+        ),
+        MimeType(
+            mime: "application/x-tar",
+            ext: "tar",
+            type: .tar,
+            bytesCount: 262,
+            matches: { bytes, _ in
+                return bytes[257...261] == [0x75, 0x73, 0x74, 0x61, 0x72]
+        }
+        ),
+        MimeType(
+            mime: "application/x-rar-compressed",
+            ext: "rar",
+            type: .rar,
+            bytesCount: 7,
+            matches: { bytes, _ in
+                return (bytes[0...5] == [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07]) &&
+                    (bytes[6] == 0x0 || bytes[6] == 0x1)
+        }
+        ),
+        MimeType(
+            mime: "application/gzip",
+            ext: "gz",
+            type: .gz,
+            bytesCount: 3,
+            matches: { bytes, _ in
+                return bytes[0...2] == [0x1F, 0x8B, 0x08]
+        }
+        ),
+        MimeType(
+            mime: "application/x-bzip2",
+            ext: "bz2",
+            type: .bz2,
+            bytesCount: 3,
+            matches: { bytes, _ in
+                return bytes[0...2] == [0x42, 0x5A, 0x68]
+        }
+        ),
+        MimeType(
+            mime: "application/x-7z-compressed",
+            ext: "7z",
+            type: .sevenZ,
+            bytesCount: 6,
+            matches: { bytes, _ in
+                return bytes[0...5] == [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]
+        }
+        ),
+        MimeType(
+            mime: "application/x-apple-diskimage",
+            ext: "dmg",
+            type: .dmg,
+            bytesCount: 2,
+            matches: { bytes, _ in
+                return bytes[0...1] == [0x78, 0x01]
+        }
+        ),
+        MimeType(
+            mime: "video/mp4",
+            ext: "mp4",
+            type: .mp4,
+            bytesCount: 28,
+            matches: { bytes, _ in
+                return (bytes[0...2] == [0x00, 0x00, 0x00] && (bytes[3] == 0x18 || bytes[3] == 0x20) && bytes[4...7] == [0x66, 0x74, 0x79, 0x70]) ||
+                    (bytes[0...3] == [0x33, 0x67, 0x70, 0x35]) ||
+                    (bytes[0...11] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32] &&
+                        bytes[16...27] == [0x6D, 0x70, 0x34, 0x31, 0x6D, 0x70, 0x34, 0x32, 0x69, 0x73, 0x6F, 0x6D]) ||
+                    (bytes[0...11] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D]) ||
+                    (bytes[0...11] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x00])
+        }
+        ),
+        MimeType(
+            mime: "video/x-m4v",
+            ext: "m4v",
+            type: .m4v,
+            bytesCount: 11,
+            matches: { bytes, _ in
+                return bytes[0...10] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x56]
+        }
+        ),
+        MimeType(
+            mime: "audio/midi",
+            ext: "mid",
+            type: .mid,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x4D, 0x54, 0x68, 0x64]
+        }
+        ),
+        MimeType(
+            mime: "video/x-matroska",
+            ext: "mkv",
+            type: .mkv,
+            bytesCount: 4,
+            matches: { bytes, swime in
+                guard bytes[0...3] == [0x1A, 0x45, 0xDF, 0xA3] else {
+                    return false
+                }
+                
+                let _bytes = Array(swime.readBytes(count: 4100)[4 ..< 4100])
+                var idPos = -1
+                
+                for i in 0 ..< (_bytes.count - 1) {
+                    if _bytes[i] == 0x42 && _bytes[i + 1] == 0x82 {
+                        idPos = i
+                        break;
+                    }
+                }
+                
+                guard idPos > -1 else {
+                    return false
+                }
+                
+                let docTypePos = idPos + 3
+                let findDocType: (String) -> Bool = { type in
+                    for i in 0 ..< type.count {
+                        let index = type.index(type.startIndex, offsetBy: i)
+                        let scalars = String(type[index]).unicodeScalars
+                        
+                        if _bytes[docTypePos + i] != UInt8(scalars[scalars.startIndex].value) {
+                            return false
+                        }
+                    }
+                    
+                    return true
+                }
+                
+                return findDocType("matroska")
+        }
+        ),
+        MimeType(
+            mime: "video/webm",
+            ext: "webm",
+            type: .webm,
+            bytesCount: 4,
+            matches: { bytes, swime in
+                guard bytes[0...3] == [0x1A, 0x45, 0xDF, 0xA3] else {
+                    return false
+                }
+                
+                let _bytes = Array(swime.readBytes(count: 4100)[4 ..< 4100])
+                var idPos = -1
+                
+                for i in 0 ..< (_bytes.count - 1) {
+                    if _bytes[i] == 0x42 && _bytes[i + 1] == 0x82 {
+                        idPos = i
+                        break;
+                    }
+                }
+                
+                guard idPos > -1 else {
+                    return false
+                }
+                
+                let docTypePos = idPos + 3
+                let findDocType: (String) -> Bool = { type in
+                    for i in 0 ..< type.count {
+                        let index = type.index(type.startIndex, offsetBy: i)
+                        let scalars = String(type[index]).unicodeScalars
+                        
+                        if _bytes[docTypePos + i] != UInt8(scalars[scalars.startIndex].value) {
+                            return false
+                        }
+                    }
+                    
+                    return true
+                }
+                
+                return findDocType("webm")
+        }
+        ),
+        MimeType(
+            mime: "video/quicktime",
+            ext: "mov",
+            type: .mov,
+            bytesCount: 8,
+            matches: { bytes, _ in
+                return bytes[0...7] == [0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70]
+        }
+        ),
+        MimeType(
+            mime: "video/x-msvideo",
+            ext: "avi",
+            type: .avi,
+            bytesCount: 11,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x52, 0x49, 0x46, 0x46]) &&
+                    (bytes[8...10] == [0x41, 0x56, 0x49])
+        }
+        ),
+        MimeType(
+            mime: "video/x-ms-wmv",
+            ext: "wmv",
+            type: .wmv,
+            bytesCount: 10,
+            matches: { bytes, _ in
+                return bytes[0...9] == [0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9]
+        }
+        ),
+        MimeType(
+            mime: "video/mpeg",
+            ext: "mpg",
+            type: .mpg,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                guard bytes[0...2] == [0x00, 0x00, 0x01]  else {
+                    return false
+                }
+                
+                let hexCode = String(format: "%2X", bytes[3])
+                
+                return hexCode.first != nil && hexCode.first! == "B"
+        }
+        ),
+        MimeType(
+            mime: "audio/mpeg",
+            ext: "mp3",
+            type: .mp3,
+            bytesCount: 3,
+            matches: { bytes, _ in
+                return (bytes[0...2] == [0x49, 0x44, 0x33]) ||
+                    (bytes[0...1] == [0xFF, 0xFB])
+        }
+        ),
+        MimeType(
+            mime: "audio/m4a",
+            ext: "m4a",
+            type: .m4a,
+            bytesCount: 11,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x4D, 0x34, 0x41, 0x20]) ||
+                    (bytes[4...10] == [0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41])
+        }
+        ),
+        
+        // Needs to be before `ogg` check
+        MimeType(
+            mime: "audio/opus",
+            ext: "opus",
+            type: .opus,
+            bytesCount: 36,
+            matches: { bytes, _ in
+                return bytes[28...35] == [0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]
+        }
+        ),
+        MimeType(
+            mime: "audio/ogg",
+            ext: "ogg",
+            type: .ogg,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x4F, 0x67, 0x67, 0x53]
+        }
+        ),
+        MimeType(
+            mime: "audio/x-flac",
+            ext: "flac",
+            type: .flac,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x66, 0x4C, 0x61, 0x43]
+        }
+        ),
+        MimeType(
+            mime: "audio/x-wav",
+            ext: "wav",
+            type: .wav,
+            bytesCount: 12,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x52, 0x49, 0x46, 0x46]) &&
+                    (bytes[8...11] == [0x57, 0x41, 0x56, 0x45])
+        }
+        ),
+        MimeType(
+            mime: "audio/amr",
+            ext: "amr",
+            type: .amr,
+            bytesCount: 6,
+            matches: { bytes, _ in
+                return bytes[0...5] == [0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A]
+        }
+        ),
+        MimeType(
+            mime: "application/pdf",
+            ext: "pdf",
+            type: .pdf,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x25, 0x50, 0x44, 0x46]
+        }
+        ),
+        MimeType(
+            mime: "application/x-msdownload",
+            ext: "exe",
+            type: .exe,
+            bytesCount: 2,
+            matches: { bytes, _ in
+                return bytes[0...1] == [0x4D, 0x5A]
+        }
+        ),
+        MimeType(
+            mime: "application/x-shockwave-flash",
+            ext: "swf",
+            type: .swf,
+            bytesCount: 3,
+            matches: { bytes, _ in
+                return (bytes[0] == 0x43 || bytes[0] == 0x46) && (bytes[1...2] == [0x57, 0x53])
+        }
+        ),
+        MimeType(
+            mime: "application/rtf",
+            ext: "rtf",
+            type: .rtf,
+            bytesCount: 5,
+            matches: { bytes, _ in
+                return bytes[0...4] == [0x7B, 0x5C, 0x72, 0x74, 0x66]
+        }
+        ),
+        MimeType(
+            mime: "application/font-woff",
+            ext: "woff",
+            type: .woff,
+            bytesCount: 8,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x77, 0x4F, 0x46, 0x46]) &&
+                    ((bytes[4...7] == [0x00, 0x01, 0x00, 0x00]) || (bytes[4...7] == [0x4F, 0x54, 0x54, 0x4F]))
+        }
+        ),
+        MimeType(
+            mime: "application/font-woff",
+            ext: "woff2",
+            type: .woff2,
+            bytesCount: 8,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x77, 0x4F, 0x46,  0x32]) &&
+                    ((bytes[4...7] == [0x00, 0x01, 0x00, 0x00]) || (bytes[4...7] == [0x4F, 0x54, 0x54, 0x4F]))
+        }
+        ),
+        MimeType(
+            mime: "application/octet-stream",
+            ext: "eot",
+            type: .eot,
+            bytesCount: 11,
+            matches: { bytes, _ in
+                return (bytes[34...35] == [0x4C, 0x50]) &&
+                    ((bytes[8...10] == [0x00, 0x00, 0x01]) || (bytes[8...10] == [0x01, 0x00, 0x02]) || (bytes[8...10] == [0x02, 0x00, 0x02]))
+        }
+        ),
+        MimeType(
+            mime: "application/font-sfnt",
+            ext: "ttf",
+            type: .ttf,
+            bytesCount: 5,
+            matches: { bytes, _ in
+                return bytes[0...4] == [0x00, 0x01, 0x00, 0x00, 0x00]
+        }
+        ),
+        MimeType(
+            mime: "application/font-sfnt",
+            ext: "otf",
+            type: .otf,
+            bytesCount: 5,
+            matches: { bytes, _ in
+                return bytes[0...4] == [0x4F, 0x54, 0x54, 0x4F, 0x00]
+        }
+        ),
+        MimeType(
+            mime: "image/x-icon",
+            ext: "ico",
+            type: .ico,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x00, 0x00, 0x01, 0x00]
+        }
+        ),
+        MimeType(
+            mime: "video/x-flv",
+            ext: "flv",
+            type: .flv,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x46, 0x4C, 0x56, 0x01]
+        }
+        ),
+        MimeType(
+            mime: "application/postscript",
+            ext: "ps",
+            type: .ps,
+            bytesCount: 2,
+            matches: { bytes, _ in
+                return bytes[0...1] == [0x25, 0x21]
+        }
+        ),
+        MimeType(
+            mime: "application/x-xz",
+            ext: "xz",
+            type: .xz,
+            bytesCount: 6,
+            matches: { bytes, _ in
+                return bytes[0...5] == [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]
+        }
+        ),
+        MimeType(
+            mime: "application/x-sqlite3",
+            ext: "sqlite",
+            type: .sqlite,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x53, 0x51, 0x4C, 0x69]
+        }
+        ),
+        MimeType(
+            mime: "application/x-nintendo-nes-rom",
+            ext: "nes",
+            type: .nes,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x4E, 0x45, 0x53, 0x1A]
+        }
+        ),
+        MimeType(
+            mime: "application/x-google-chrome-extension",
+            ext: "crx",
+            type: .crx,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x43, 0x72, 0x32, 0x34]
+        }
+        ),
+        MimeType(
+            mime: "application/vnd.ms-cab-compressed",
+            ext: "cab",
+            type: .cab,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return (bytes[0...3] == [0x4D, 0x53, 0x43, 0x46]) || (bytes[0...3] == [0x49, 0x53, 0x63, 0x28])
+        }
+        ),
+        
+        // Needs to be before `ar` check
+        MimeType(
+            mime: "application/x-deb",
+            ext: "deb",
+            type: .deb,
+            bytesCount: 21,
+            matches: { bytes, _ in
+                return bytes[0...20] == [
+                    0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A, 0x64, 0x65, 0x62, 0x69,
+                    0x61, 0x6E, 0x2D, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79
+                ]
+        }
+        ),
+        MimeType(
+            mime: "application/x-unix-archive",
+            ext: "ar",
+            type: .ar,
+            bytesCount: 7,
+            matches: { bytes, _ in
+                return bytes[0...6] == [0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E]
+        }
+        ),
+        MimeType(
+            mime: "application/x-rpm",
+            ext: "rpm",
+            type: .rpm,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0xED, 0xAB, 0xEE, 0xDB]
+        }
+        ),
+        MimeType(
+            mime: "application/x-compress",
+            ext: "Z",
+            type: .z,
+            bytesCount: 2,
+            matches: { bytes, _ in
+                return (bytes[0...1] == [0x1F, 0xA0]) || (bytes[0...1] == [0x1F, 0x9D])
+        }
+        ),
+        MimeType(
+            mime: "application/x-lzip",
+            ext: "lz",
+            type: .lz,
+            bytesCount: 4,
+            matches: { bytes, _ in
+                return bytes[0...3] == [0x4C, 0x5A, 0x49, 0x50]
+        }
+        ),
+        MimeType(
+            mime: "application/x-msi",
+            ext: "msi",
+            type: .msi,
+            bytesCount: 8,
+            matches: { bytes, _ in
+                return bytes[0...7] == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
+        }
+        ),
+        MimeType(
+            mime: "application/mxf",
+            ext: "mxf",
+            type: .mxf,
+            bytesCount: 14,
+            matches: { bytes, _ in
+                return bytes[0...13] == [0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02 ]
+        }
+        )
+    ]
+}
+

+ 68 - 0
o2ios/O2Platform/framework/MagicBytesMimeType/Swime.swift

@@ -0,0 +1,68 @@
+//
+//  Swime.swift
+//  源码来自 https://github.com/sendyhalim/Swime
+//  O2Platform
+//
+//  Created by FancyLou on 2019/8/23.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import Foundation
+
+
+
+public struct Swime {
+    /// File data
+    let data: Data
+    
+    ///  A static method to get the `MimeType` that matches the given file data
+    ///
+    ///  - returns: Optional<MimeType>
+    static public func mimeType(data: Data) -> MimeType? {
+        return mimeType(swime: Swime(data: data))
+    }
+    
+    ///  A static method to get the `MimeType` that matches the given bytes
+    ///
+    ///  - returns: Optional<MimeType>
+    static public func mimeType(bytes: [UInt8]) -> MimeType? {
+        return mimeType(swime: Swime(bytes: bytes))
+    }
+    
+    ///  Get the `MimeType` that matches the given `Swime` instance
+    ///
+    ///  - returns: Optional<MimeType>
+    static public func mimeType(swime: Swime) -> MimeType? {
+        let bytes = swime.readBytes(count: min(swime.data.count, 262))
+        
+        for mime in MimeType.all {
+            if mime.matches(bytes: bytes, swime: swime) {
+                return mime
+            }
+        }
+        
+        return nil
+    }
+    
+    public init(data: Data) {
+        self.data = data
+    }
+    
+    public init(bytes: [UInt8]) {
+        self.init(data: Data(bytes))
+    }
+    
+    ///  Read bytes from file data
+    ///
+    ///  - parameter count: Number of bytes to be read
+    ///
+    ///  - returns: Bytes represented with `[UInt8]`
+    internal func readBytes(count: Int) -> [UInt8] {
+        var bytes = [UInt8](repeating: 0, count: count)
+        
+        data.copyBytes(to: &bytes, count: count)
+        
+        return bytes
+    }
+}
+

+ 83 - 41
o2ios/O2Platform/scan/c/NewScanViewController.swift

@@ -44,54 +44,96 @@ class NewScanViewController: LBXScanViewController {
             self.popVC()
         }else {
             let url = NSURL(string: result.strScanned!)
-            let query = url?.query
-            let querys = query?.split("&")
-            var meta = ""
-            querys?.forEach { (e) in
-                let name = e.split("=")[0]
-                if name == "meta" {
-                    meta = e.split("=")[1]
-                }
+            //会议签到功能
+            var isMeetingCheck = false
+            let allU = url?.absoluteString
+            if allU != nil && allU!.contains("/checkin") && allU!.contains("x_meeting_assemble_control") {
+                isMeetingCheck = true
             }
-            if meta != "" {
-                let account = O2AuthSDK.shared.myInfo()
-                let loginURL = AppDelegate.o2Collect.generateURLWithAppContextKey(LoginContext.loginContextKey, query: LoginContext.scanCodeAuthActionQuery, parameter: ["##meta##":meta as AnyObject])
-                Alamofire.request(loginURL!, method: .post, parameters: nil, encoding: JSONEncoding.default, headers: ["x-token":(account?.token)!]).responseJSON(completionHandler: { (response) in
-                    switch response.result {
-                    case .success(let val):
-                        DispatchQueue.main.async {
-                            DDLogDebug(String(describing:val))
-                            let alertController = UIAlertController(title: "扫描结果", message: "PC端登录成功", preferredStyle: .alert)
-                            let okAction = UIAlertAction(title: "确定", style: .default) {
-                                action in
-                                self.popVC()
+            if(isMeetingCheck) {//会议签到
+                self.meetingCheck(url: allU!)
+            }else {
+                let query = url?.query
+                let querys = query?.split("&")
+                var meta = ""
+                querys?.forEach { (e) in
+                    let name = e.split("=")[0]
+                    if name == "meta" {
+                        meta = e.split("=")[1]
+                    }
+                }
+                if meta != "" {//登录O2OA
+                    let account = O2AuthSDK.shared.myInfo()
+                    let loginURL = AppDelegate.o2Collect.generateURLWithAppContextKey(LoginContext.loginContextKey, query: LoginContext.scanCodeAuthActionQuery, parameter: ["##meta##":meta as AnyObject])
+                    Alamofire.request(loginURL!, method: .post, parameters: nil, encoding: JSONEncoding.default, headers: ["x-token":(account?.token)!]).responseJSON(completionHandler: { (response) in
+                        switch response.result {
+                        case .success(let val):
+                            DispatchQueue.main.async {
+                                DDLogDebug(String(describing:val))
+                                let alertController = UIAlertController(title: "扫描结果", message: "PC端登录成功", preferredStyle: .alert)
+                                let okAction = UIAlertAction(title: "确定", style: .default) {
+                                    action in
+                                    self.popVC()
+                                }
+                                alertController.addAction(okAction)
+                                self.presentVC(alertController)
                             }
-                            alertController.addAction(okAction)
-                            self.presentVC(alertController)
-                        }
-                    case .failure(let err):
-                        DispatchQueue.main.async {
-                            DDLogError(err.localizedDescription)
-                            let alertController = UIAlertController(title: "扫描结果", message: "PC端登录失败", preferredStyle: .alert)
-                            let okAction = UIAlertAction(title: "确定", style: .destructive) {
-                                action in
-                                self.popVC()
+                        case .failure(let err):
+                            DispatchQueue.main.async {
+                                DDLogError(err.localizedDescription)
+                                let alertController = UIAlertController(title: "扫描结果", message: "PC端登录失败", preferredStyle: .alert)
+                                let okAction = UIAlertAction(title: "确定", style: .destructive) {
+                                    action in
+                                    self.popVC()
+                                }
+                                alertController.addAction(okAction)
+                                self.presentVC(alertController)
                             }
-                            alertController.addAction(okAction)
-                            self.presentVC(alertController)
+                            
                         }
-                        
+                    })
+                }else {//其他扫描结果
+                    let alertController = UIAlertController(title: "扫描结果", message: result.strScanned!, preferredStyle: .alert)
+                    let okAction = UIAlertAction(title: "确定", style: .default) {
+                        action in
+                        self.popVC()
                     }
-                })
-            }else {
-                let alertController = UIAlertController(title: "扫描结果", message: result.strScanned!, preferredStyle: .alert)
-                let okAction = UIAlertAction(title: "确定", style: .default) {
-                    action in
-                    self.popVC()
+                    alertController.addAction(okAction)
+                    self.presentVC(alertController)
                 }
-                alertController.addAction(okAction)
-                self.presentVC(alertController)
             }
         }
     }
+    
+    
+    //会议签到
+    func meetingCheck(url: String) {
+        let account = O2AuthSDK.shared.myInfo()
+        Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: ["x-token":(account?.token)!]).responseJSON(completionHandler: {(response) in
+            switch response.result {
+            case .success(let val):
+                DispatchQueue.main.async {
+                    DDLogDebug(String(describing:val))
+                    let alertController = UIAlertController(title: "提示", message: "签到成功", preferredStyle: .alert)
+                    let okAction = UIAlertAction(title: "确定", style: .default) {
+                        action in
+                        self.popVC()
+                    }
+                    alertController.addAction(okAction)
+                    self.presentVC(alertController)
+                }
+            case .failure(let err):
+                DispatchQueue.main.async {
+                    DDLogError(err.localizedDescription)
+                    let alertController = UIAlertController(title: "提示", message: "签到失败", preferredStyle: .alert)
+                    let okAction = UIAlertAction(title: "确定", style: .destructive) {
+                        action in
+                        self.popVC()
+                    }
+                    alertController.addAction(okAction)
+                    self.presentVC(alertController)
+                }
+            }
+        })
+    }
 }

+ 3 - 0
o2ios/O2Platform/setting/c/SettingViewController.swift

@@ -96,6 +96,7 @@ class SettingViewController: UIViewController,UITableViewDelegate,UITableViewDat
         if let segue = cellModel?.segueIdentifier {
             if segue == "showIdeaBackSegue" {
                 PgyManager.shared().showFeedbackView()
+//                self.testShowPicker()
             }else{
                 self.performSegue(withIdentifier: segue, sender: nil)
             }
@@ -123,5 +124,7 @@ class SettingViewController: UIViewController,UITableViewDelegate,UITableViewDat
         let avatarUrl = URL(string: avatarUrlString!)
         self.iconImageView.hnk_setImageFromURL(avatarUrl!)
     }
+    
+    
 
 }

+ 14 - 15
o2ios/O2Platform/storyboard/contacts.storyboard

@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="iDJ-yD-7GA">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="iDJ-yD-7GA">
     <device id="retina4_7" orientation="portrait">
         <adaptation id="fullscreen"/>
     </device>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
@@ -507,20 +507,19 @@
                                     <rect key="frame" x="0.0" y="0.0" width="375" height="49.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
-                                        <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" editable="NO" text="text" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="eY3-cV-9oc">
-                                            <rect key="frame" x="8" y="7" width="360" height="36"/>
-                                            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
-                                            <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
-                                            <color key="tintColor" red="0.20000000000000001" green="0.20000000000000001" blue="0.20000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-                                            <color key="textColor" red="0.60784313725490191" green="0.60784313725490191" blue="0.60784313725490191" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-                                            <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="16"/>
-                                            <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
-                                            <dataDetectorType key="dataDetectorTypes" link="YES"/>
-                                        </textView>
+                                        <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="UbD-xq-ZqX">
+                                            <rect key="frame" x="10" y="0.0" width="355" height="49.5"/>
+                                        </scrollView>
                                     </subviews>
+                                    <constraints>
+                                        <constraint firstItem="UbD-xq-ZqX" firstAttribute="leading" secondItem="KbZ-H9-Myg" secondAttribute="leading" constant="10" id="KOd-Ia-wsV"/>
+                                        <constraint firstAttribute="bottom" secondItem="UbD-xq-ZqX" secondAttribute="bottom" id="Pfb-G7-nzn"/>
+                                        <constraint firstAttribute="trailing" secondItem="UbD-xq-ZqX" secondAttribute="trailing" constant="10" id="ZIW-ig-luQ"/>
+                                        <constraint firstItem="UbD-xq-ZqX" firstAttribute="top" secondItem="KbZ-H9-Myg" secondAttribute="top" id="oib-Pp-jGF"/>
+                                    </constraints>
                                 </tableViewCellContentView>
                                 <connections>
-                                    <outlet property="headBarView" destination="eY3-cV-9oc" id="bEn-AF-awP"/>
+                                    <outlet property="headBarScrollView" destination="UbD-xq-ZqX" id="Ce1-n4-Gsv"/>
                                 </connections>
                             </tableViewCell>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="addressUnitCell" id="Tw8-69-DnB" customClass="ContactItemCell" customModule="O2Platform" customModuleProvider="target">
@@ -604,7 +603,7 @@
                 </tableViewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="f9A-8k-8kY" userLabel="First Responder" sceneMemberID="firstResponder"/>
             </objects>
-            <point key="canvasLocation" x="2516" y="1003"/>
+            <point key="canvasLocation" x="2516" y="1002.5487256371815"/>
         </scene>
         <!--Navigation Controller-->
         <scene sceneID="C0x-SJ-txD">
@@ -631,7 +630,7 @@
         <image name="icon_collection_nor" width="50" height="50"/>
         <image name="icon_collection_pro" width="50" height="50"/>
         <image name="icon_girl_1" width="50" height="50"/>
-        <image name="personDefaultIcon" width="48" height="48"/>
+        <image name="personDefaultIcon" width="24" height="24"/>
         <image name="pic_beijing1" width="375" height="230"/>
     </resources>
     <inferredMetricsTieBreakers>

+ 26 - 1
o2ios/O2Platform/task/c/TodoTaskDetailViewController.swift

@@ -958,9 +958,11 @@ extension TodoTaskDetailViewController: O2WKScriptMessageHandlerImplement {
         Alamofire.download(url, to: localFileDestination).response(completionHandler: { (response) in
             if response.error == nil , let fileurl = response.destinationURL?.path {
                 DDLogDebug("文件地址:\(fileurl)")
+                let newUrl = self.dealDocFileSaveAsDocx(fileUrl: response.destinationURL!)
+                DDLogDebug("处理过的文件地址:\(newUrl.path)")
                 //打开文件
                 self.dismissProgressHUD()
-                self.previewAttachment(fileurl)
+                self.previewAttachment(newUrl.path)
             }else{
                 let msg = response.error?.localizedDescription ?? ""
                 DDLogError("下载文件出错,\(msg)")
@@ -1081,5 +1083,28 @@ extension TodoTaskDetailViewController: O2WKScriptMessageHandlerImplement {
         
     }
 
+    
+    
+    
+    //处理特殊情况 docx的文件有可能是doc 需要判断下文件信息头
+    private func dealDocFileSaveAsDocx(fileUrl: URL) -> URL {
+        if fileUrl.pathExtension == "docx" {
+            if let data = try? Data(contentsOf: fileUrl) {
+                let mimeType = Swime.mimeType(data: data)
+                if mimeType?.type == .msi {
+                    let newURL = fileUrl.appendingPathExtension("doc")
+                    do {
+                        DDLogDebug("copy 了一个 文件。。。。。。")
+                        try FileManager.default.copyItem(at: fileUrl, to: newURL)
+                        return newURL
+                    }catch {
+                        DDLogError(error.localizedDescription)
+                    }
+                }
+            }
+        }
+        
+        return fileUrl
+    }
 }
 

+ 55 - 0
o2ios/O2Platform/task/m/O2WebViewModels.swift

@@ -103,3 +103,58 @@ struct O2UtilPhoneInfo: HandyJSON {
     var netInfo: String?
     var operatorType: String?
 }
+//身份选择传入参数对象
+struct O2BizIdentityPickerMessage: HandyJSON {
+    var topList: [String]?
+    var multiple: Bool?
+    var maxNumber: Int?
+    var pickedIdentities: [String]?
+    var duty: [String]?
+}
+//组织选择传入参数对象
+struct O2BizUnitPickerMessage: HandyJSON {
+    var topList: [String]?
+    var multiple: Bool?
+    var maxNumber: Int?
+    var pickedDepartments: [String]?
+    var orgType: String?
+}
+//群组选择传入参数对象
+struct O2BizGroupPickerMessage: HandyJSON {
+    var multiple: Bool?
+    var maxNumber: Int?
+    var pickedGroups: [String]?
+}
+//人员选择传入参数对象
+struct O2BizPersonPickerMessage: HandyJSON {
+    var multiple: Bool?
+    var maxNumber: Int?
+    var pickedUsers: [String]?
+}
+//复合选择传入参数对象
+struct O2BizComplexPickerMessage: HandyJSON {
+    var topList: [String]?
+    var pickMode: [String]?
+    var multiple: Bool?
+    var maxNumber: Int?
+    var pickedDepartments: [String]?
+    var pickedIdentities: [String]?
+    var pickedGroups: [String]?
+    var pickedUsers: [String]?
+    var duty: [String]?
+    var orgType: String?
+}
+
+struct O2BizComplexPickerResults: HandyJSON {
+    var results: [String]?
+}
+struct O2BizContactPickerResult: HandyJSON {
+    var departments: [O2BizContactPickerResultItem]?
+    var identities: [O2BizContactPickerResultItem]?
+    var groups: [O2BizContactPickerResultItem]?
+    var users: [O2BizContactPickerResultItem]?
+}
+struct O2BizContactPickerResultItem: HandyJSON {
+    var distinguishedName: String?
+    var name: String?
+}

+ 1168 - 0
o2web/source/o2_core/init.js

@@ -0,0 +1,1168 @@
+/** ***** BEGIN LICENSE BLOCK *****
+ * |------------------------------------------------------------------------------|
+ * | O2OA 活力办公 创意无限    o2.js                                                 |
+ * |------------------------------------------------------------------------------|
+ * | Distributed under the AGPL license:                                          |
+ * |------------------------------------------------------------------------------|
+ * | Copyright © 2018, o2oa.net, o2server.io O2 Team                              |
+ * | All rights reserved.                                                         |
+ * |------------------------------------------------------------------------------|
+ *
+ *  This file is part of O2OA.
+ *
+ *  O2OA is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  O2OA is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Foobar.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ***** END LICENSE BLOCK ******/
+
+(function(){
+    var _href = window.location.href;
+    var _debug = (_href.indexOf("debugger")!==-1);
+    var _par = _href.substr(_href.lastIndexOf("?")+1, _href.length);
+    var _lp = "zh-cn";
+    if (_par){
+        var _parList = _par.split("&");
+        for (var i=0; i<_parList.length; i++){
+            var _v = _parList[i];
+            var _kv = _v.split("=");
+            if (_kv[0].toLowerCase()==="lg") _lp = _kv[1];
+        }
+    }
+    this.o2 = {
+        "version": {
+            "v": '2.1.0',
+            "build": "2018.11.22",
+            "info": "O2OA 活力办公 创意无限. Copyright © 2018, o2oa.net O2 Team All rights reserved."
+        },
+        "session": {
+            "isDebugger": _debug,
+            "path": "/o2_core/o2"
+        },
+        "language": _lp,
+        "splitStr": /\s*(?:,|;)\s*/
+    };
+
+    var _attempt = function(){
+        for (var i = 0, l = arguments.length; i < l; i++){
+            try {
+                arguments[i]();
+                return arguments[i];
+            } catch (e){}
+        }
+        return null;
+    };
+    var _typeOf = function(item){
+        if (item == null) return 'null';
+        if (item.$family != null) return item.$family();
+        if (item.constructor == window.Array) return "array";
+
+        if (item.nodeName){
+            if (item.nodeType == 1) return 'element';
+            if (item.nodeType == 3) return (/\S/).test(item.nodeValue) ? 'textnode' : 'whitespace';
+        } else if (typeof item.length == 'number'){
+            if (item.callee) return 'arguments';
+        }
+        return typeof item;
+    };
+    this.o2.typeOf = _typeOf;
+
+    var _addListener = function(dom, type, fn){
+        if (type == 'unload'){
+            var old = fn, self = this;
+            fn = function(){
+                _removeListener(dom, 'unload', fn);
+                old();
+            };
+        }
+        if (dom.addEventListener) dom.addEventListener(type, fn, !!arguments[2]);
+        else dom.attachEvent('on' + type, fn);
+    };
+    var _removeListener = function(dom, type, fn){
+        if (dom.removeEventListener) dom.removeEventListener(type, fn, !!arguments[2]);
+        else dom.detachEvent('on' + type, fn);
+    };
+
+    //http request class
+    var _request = (function(){
+        var XMLHTTP = function(){ return new XMLHttpRequest(); };
+        var MSXML2 = function(){ return new ActiveXObject('MSXML2.XMLHTTP'); };
+        var MSXML = function(){ return new ActiveXObject('Microsoft.XMLHTTP'); };
+        return _attempt(XMLHTTP, MSXML2, MSXML);
+    })();
+
+    var _returnBase = function(number, base) {
+        return (number).toString(base).toUpperCase();
+    };
+    var _getIntegerBits = function(val, start, end){
+        var base16 = _returnBase(val, 16);
+        var quadArray = new Array();
+        var quadString = '';
+        var i = 0;
+        for (i = 0; i < base16.length; i++) {
+            quadArray.push(base16.substring(i, i + 1));
+        }
+        for (i = Math.floor(start / 4); i <= Math.floor(end / 4); i++) {
+            if (!quadArray[i] || quadArray[i] == '')
+                quadString += '0';
+            else
+                quadString += quadArray[i];
+        }
+        return quadString;
+    };
+    var _rand = function(max) {
+        return Math.floor(Math.random() * (max + 1));
+    };
+    this.o2.addListener = _addListener;
+    this.o2.removeListener = _removeListener;
+
+    //uuid
+    var _uuid = function(){
+        var dg = new Date(1582, 10, 15, 0, 0, 0, 0);
+        var dc = new Date();
+        var t = dc.getTime() - dg.getTime();
+        var tl = _getIntegerBits(t, 0, 31);
+        var tm = _getIntegerBits(t, 32, 47);
+        var thv = _getIntegerBits(t, 48, 59) + '1';
+        var csar = _getIntegerBits(_rand(4095), 0, 7);
+        var csl = _getIntegerBits(_rand(4095), 0, 7);
+
+        var n = _getIntegerBits(_rand(8191), 0, 7)
+            + _getIntegerBits(_rand(8191), 8, 15)
+            + _getIntegerBits(_rand(8191), 0, 7)
+            + _getIntegerBits(_rand(8191), 8, 15)
+            + _getIntegerBits(_rand(8191), 0, 15);
+        return tl + tm + thv + csar + csl + n;
+    };
+    this.o2.uuid = _uuid;
+
+
+    var _runCallback = function(callback, key, par){
+        if (typeOf(callback).toLowerCase() === 'function'){
+            if (key.toLowerCase()==="success") callback.apply(callback, par);
+        }else{
+            if (typeOf(callback).toLowerCase()==='object'){
+                var name = ("on-"+key).camelCase();
+                if (callback[name]) callback[name].apply(callback, par);
+            }
+        }
+    };
+    this.o2.runCallback = _runCallback;
+
+
+    //load js, css, html adn all.
+    var _getAllOptions = function(options){
+        var doc = (options && options.doc) || document;
+        if (!doc.unid) doc.unid = _uuid();
+        return {
+            "noCache": !!(options && options.nocache),
+            "reload": !!(options && options.reload),
+            "sequence": !!(options && options.sequence),
+            "doc": doc,
+            "dom": (options && options.dom) || document.body,
+            "bind": (options && options.bind) || null,
+            "position": (options && options.position) || "beforeend" //'beforebegin' 'afterbegin' 'beforeend' 'afterend'
+        }
+    };
+    var _getCssOptions = function(options){
+        var doc = (options && options.doc) || document;
+        if (!doc.unid) doc.unid = _uuid();
+        return {
+            "noCache": !!(options && options.nocache),
+            "reload": !!(options && options.reload),
+            "sequence": !!(options && options.sequence),
+            "doc": doc,
+            "dom": (options && options.dom) || null
+        }
+    };
+    var _getJsOptions = function(options){
+        var doc = (options && options.doc) || document;
+        if (!doc.unid) doc.unid = _uuid();
+        return {
+            "noCache": !!(options && options.nocache),
+            "reload": !!(options && options.reload),
+            "sequence": (!(options && options.sequence == false)),
+            "doc": doc
+        }
+    };
+    var _getHtmlOptions = function(options){
+        var doc = (options && options.doc) || document;
+        if (!doc.unid) doc.unid = _uuid();
+        return {
+            "noCache": !!(options && options.nocache),
+            "reload": !!(options && options.reload),
+            "sequence": !!(options && options.sequence),
+            "doc": doc,
+            "dom": (options && options.dom) || null,
+            "bind": (options && options.bind) || null,
+            "position": (options && options.position) || "beforeend" //'beforebegin' 'afterbegin' 'beforeend' 'afterend'
+        }
+    };
+    var _xhr_get = function(url, success, failure, completed){
+        var xhr = new _request();
+        xhr.open("GET", url, true);
+
+        var _checkCssLoaded= function(_, err){
+            if (!(xhr.readyState == 4)) return;
+            if (err){
+                if (completed) completed(xhr);
+                return;
+            }
+
+            _removeListener(xhr, 'readystatechange', _checkCssLoaded);
+            _removeListener(xhr, 'load', _checkCssLoaded);
+            _removeListener(xhr, 'error', _checkCssErrorLoaded);
+
+            if (err) {failure(xhr); return}
+            var status = xhr.status;
+            status = (status == 1223) ? 204 : status;
+            if ((status >= 200 && status < 300))
+                success(xhr);
+            else if ((status >= 300 && status < 400))
+                failure(xhr);
+            else
+                failure(xhr);
+            if (completed) completed(xhr);
+        };
+        var _checkCssErrorLoaded= function(err){ _checkCssLoaded(err) };
+
+        if ("load" in xhr) _addListener(xhr, "load", _checkCssLoaded);
+        if ("error" in xhr) _addListener(xhr, "load", _checkCssErrorLoaded);
+        _addListener(xhr, "readystatechange", _checkCssLoaded);
+        xhr.send();
+    };
+
+    var _loadSequence = function(ms, cb, op, n, thisLoaded, loadSingle, uuid, fun){
+        loadSingle(ms[n], function(module){
+            if (module) thisLoaded.push(module);
+            n++;
+            if (fun) fun(module);
+            if (n===ms.length){
+                if (cb) cb(thisLoaded);
+            }else{
+                _loadSequence(ms, cb, op, n, thisLoaded, loadSingle, uuid, fun);
+            }
+        }, op, uuid);
+    };
+    var _loadDisarray = function(ms, cb, op, thisLoaded, loadSingle, uuid, fun){
+        var count=0;
+        for (var i=0; i<ms.length; i++){
+            loadSingle(ms[i], function(module){
+                if (module) thisLoaded.push(module);
+                count++;
+                if (fun) fun(module);
+                if (count===ms.length) if (cb) cb(thisLoaded);
+            }, op, uuid);
+        }
+    };
+
+    //load js
+    //use framework url
+    var _frameworks = {
+        "o2.core": ["/o2_core/o2/o2.core.js"],
+        "o2.more": ["/o2_core/o2/o2.more.js"],
+        "ie_adapter": ["/o2_lib/o2/ie_adapter.js"],
+        "jquery": ["/o2_lib/jquery/jquery.min.js"],
+        "mootools": ["/o2_lib/mootools/mootools-1.6.0_all.js"],
+        "ckeditor": ["/o2_lib/htmleditor/ckeditor4114/ckeditor.js"],
+        "ckeditor5": ["/o2_lib/htmleditor/ckeditor5-12-1-0/ckeditor.js"],
+        "raphael": ["/o2_lib/raphael/raphael.js"],
+        "d3": ["/o2_lib/d3/d3.min.js"],
+        "ace": ["/o2_lib/ace/src-noconflict/ace.js","/o2_lib/ace/src-noconflict/ext-language_tools.js"],
+        "JSBeautifier": ["/o2_lib/JSBeautifier/beautify.js"],
+        "JSBeautifier_css": ["/o2_lib/JSBeautifier/beautify-css.js"],
+        "JSBeautifier_html": ["/o2_lib/JSBeautifier/beautify-html.js"],
+        "JSONTemplate": ["/o2_lib/mootools/plugin/Template.js"],
+        "kity": ["/o2_lib/kityminder/kity/kity.min.js"],
+        "kityminder": ["/o2_lib/kityminder/core/dist/kityminder.core.js"]
+    };
+    var _loaded = {};
+    var _loadedCss = {};
+    var _loadedHtml = {};
+    var _loadCssRunning = {};
+    var _loadCssQueue = [];
+
+    var _loadSingle = function(module, callback, op){
+        var url = module;
+        var uuid = _uuid();
+        if (op.noCache) url = (url.indexOf("?")!==-1) ? url+"&v="+uuid : addr_uri+"?v="+uuid;
+        var key = encodeURIComponent(url+op.doc.unid);
+        if (!op.reload) if (_loaded[key]){
+            if (callback)callback(); return;
+        }
+
+        var head = (op.doc.head || op.doc.getElementsByTagName("head")[0] || op.doc.documentElement);
+        var s = op.doc.createElement('script');
+        head.appendChild(s);
+        s.id = uuid;
+        s.src = url;
+
+        var _checkScriptLoaded = function(_, isAbort, err){
+            if (isAbort || !s.readyState || s.readyState === "loaded" || s.readyState === "complete") {
+                var scriptObj = {"module": module, "id": uuid, "script": s, "doc": op.doc};
+                if (!err) _loaded[key] = scriptObj;
+                _removeListener(s, 'readystatechange', _checkScriptLoaded);
+                _removeListener(s, 'load', _checkScriptLoaded);
+                _removeListener(s, 'error', _checkScriptErrorLoaded);
+                if (!isAbort || err){
+                    if (err){
+                        if (s) head.removeChild(s);
+                        if (callback)callback();
+                    }else{
+                        //head.removeChild(s);
+                        if (callback)callback(scriptObj);
+                    }
+                }
+            }
+        };
+        var _checkScriptErrorLoaded = function(e, err){
+            console.log("Error: load javascript module: "+module);
+            _checkScriptLoaded(e, true, "error");
+        };
+
+        if ('onreadystatechange' in s) _addListener(s, 'readystatechange', _checkScriptLoaded);
+        _addListener(s, 'load', _checkScriptLoaded);
+        _addListener(s, 'error', _checkScriptErrorLoaded);
+    };
+
+    var _load = function(urls, options, callback){
+        var ms = (_typeOf(urls)==="array") ? urls : [urls];
+        var op =  (_typeOf(options)==="object") ? _getJsOptions(options) : _getJsOptions(null);
+        var cb = (_typeOf(options)==="function") ? options : callback;
+
+        var modules = [];
+        for (var i=0; i<ms.length; i++){
+            var url = ms[i];
+            var module = _frameworks[url] || url;
+            if (_typeOf(module)==="array"){
+                modules = modules.concat(module)
+            }else{
+                modules.push(module)
+            }
+        }
+        var thisLoaded = [];
+        if (op.sequence){
+            _loadSequence(modules, cb, op, 0, thisLoaded, _loadSingle);
+        }else{
+            _loadDisarray(modules, cb, op, thisLoaded, _loadSingle);
+        }
+    };
+    this.o2.load = _load;
+
+    //load css
+    var _loadSingleCss = function(module, callback, op, uuid){
+        var url = module;
+        var uid = _uuid();
+        if (op.noCache) url = (url.indexOf("?")!==-1) ? url+"&v="+uid : url+"?v="+uid;
+
+        var key = encodeURIComponent(url+op.doc.unid);
+        if (_loadCssRunning[key]){
+            _loadCssQueue.push(function(){
+                _loadSingleCss(module, callback, op, uuid);
+            });
+            return;
+        }
+
+        if (_loadedCss[key]) uuid = _loadedCss[key]["class"];
+        if (op.dom) _parseDom(op.dom, function(node){ if (node.className.indexOf(uuid) == -1) node.className += ((node.className) ? " "+uuid : uuid);}, op.doc);
+
+        var completed = function(){
+            if (_loadCssRunning[key]){
+                _loadCssRunning[key] = false;
+                delete _loadCssRunning[key];
+            }
+            if (_loadCssQueue && _loadCssQueue.length){
+                (_loadCssQueue.shift())();
+            }
+        };
+
+        if (_loadedCss[key])if (!op.reload){
+            if (callback)callback(_loadedCss[key]);
+            completed();
+            return;
+        }
+
+        var success = function(xhr){
+            var cssText = xhr.responseText;
+            try{
+                if (cssText){
+                    if (op.bind) cssText = cssText.bindJson(op.bind);
+                    if (op.dom){
+                        var rex = new RegExp("(.+)(?=\\{)", "g");
+                        var match;
+                        while ((match = rex.exec(cssText)) !== null) {
+                            var prefix = "." + uuid + " ";
+                            var rule = prefix + match[0];
+                            cssText = cssText.substring(0, match.index) + rule + cssText.substring(rex.lastIndex, cssText.length);
+                            rex.lastIndex = rex.lastIndex + prefix.length;
+                        }
+                    }
+                    var style = op.doc.createElement("style");
+                    style.setAttribute("type", "text/css");
+                    var head = (op.doc.head || op.doc.getElementsByTagName("head")[0] || op.doc.documentElement);
+                    head.appendChild(style);
+                    if(style.styleSheet){
+                        var setFunc = function(){
+                            style.styleSheet.cssText = cssText;
+                        };
+                        if(style.styleSheet.disabled){
+                            setTimeout(setFunc, 10);
+                        }else{
+                            setFunc();
+                        }
+                    }else{
+                        var cssTextNode = op.doc.createTextNode(cssText);
+                        style.appendChild(cssTextNode);
+                    }
+                }
+                style.id = uid;
+                var styleObj = {"module": module, "id": uid, "style": style, "doc": op.doc, "class": uuid};
+                _loadedCss[key] = styleObj;
+                if (callback) callback(styleObj);
+            }catch (e){
+                if (callback) callback();
+                return;
+            }
+        };
+        var failure = function(xhr){
+            console.log("Error: load css module: "+module);
+            if (callback) callback();
+        };
+
+        _loadCssRunning[key] = true;
+
+        _xhr_get(url, success, failure, completed);
+    };
+
+    var _parseDomString = function(dom, fn, sourceDoc){
+        var doc = sourceDoc || document;
+        var list = doc.querySelectorAll(dom);
+        if (list.length) for (var i=0; i<list.length; i++) _parseDomElement(list[i], fn);
+    };
+    var _parseDomElement = function(dom, fn){
+        if (fn) fn(dom);
+    };
+    var _parseDom = function(dom, fn, sourceDoc){
+        var domType = _typeOf(dom);
+        if (domType==="string") _parseDomString(dom, fn, sourceDoc);
+        if (domType==="element") _parseDomElement(dom, fn);
+        if (domType==="array") for (var i=0; i<dom.length; i++) _parseDom(dom[i], fn, sourceDoc);
+    };
+    var _loadCss = function(modules, options, callback){
+        var ms = (_typeOf(modules)==="array") ? modules : [modules];
+        var op =  (_typeOf(options)==="object") ? _getCssOptions(options) : _getCssOptions(null);
+        var cb = (_typeOf(options)==="function") ? options : callback;
+
+        var uuid = "css"+_uuid();
+        var thisLoaded = [];
+        if (op.sequence){
+            _loadSequence(ms, cb, op, 0, thisLoaded, _loadSingleCss, uuid);
+        }else{
+            _loadDisarray(ms, cb, op, thisLoaded, _loadSingleCss, uuid);
+        }
+    };
+    var _removeCss = function(modules, doc){
+        var thisDoc = doc || document;
+        var ms = (_typeOf(modules)==="array") ? modules : [modules];
+        for (var i=0; i<ms.length; i++){
+            var module = modules[i];
+
+            var k = encodeURIComponent(module+(thisDoc.unid||""));
+            var removeCss = _loadedCss[k];
+            if (!removeCss) for (key in _loadedCss){
+                if (_loadedCss[key].id==module){
+                    removeCss = _loadedCss[key];
+                    k = key;
+                    break;
+                }
+            }
+            if (removeCss){
+                delete _loadedCss[k];
+                var styleNode = removeCss.doc.getElementById(removeCss.id);
+                if (styleNode) styleNode.parentNode.removeChild(styleNode);
+                removeCss = null;
+            }
+        }
+    };
+    this.o2.loadCss = _loadCss;
+    this.o2.removeCss = _removeCss;
+    Element.prototype.loadCss = function(modules, options, callback){
+        var op =  (_typeOf(options)==="object") ? options : {};
+        var cb = (_typeOf(options)==="function") ? options : callback;
+        op.dom = this;
+        _loadCss(modules, op, cb);
+    };
+
+    //load html
+    _loadSingleHtml = function(module, callback, op){
+        var url = module;
+        var uid = _uuid();
+        if (op.noCache) url = (url.indexOf("?")!==-1) ? url+"&v="+uid : url+"?v="+uid;
+        var key = encodeURIComponent(url+op.doc.unid);
+        if (!op.reload) if (_loadedHtml[key]){ if (callback)callback(_loadedHtml[key]); return; }
+
+        var success = function(xhr){
+            var htmlObj = {"module": module, "id": uid, "data": xhr.responseText, "doc": op.doc};
+            _loadedHtml[key] = htmlObj;
+            if (callback) callback(htmlObj);
+        };
+        var failure = function(){
+            console.log("Error: load html module: "+module);
+            if (callback) callback();
+        };
+        _xhr_get(url, success, failure);
+    };
+
+    var _injectHtml = function(op, data){
+        if (op.bind) data = data.bindJson(op.bind);
+        if (op.dom) _parseDom(op.dom, function(node){ node.insertAdjacentHTML(op.position, data) }, op.doc);
+    };
+    var _loadHtml = function(modules, options, callback){
+        var ms = (_typeOf(modules)==="array") ? modules : [modules];
+        var op =  (_typeOf(options)==="object") ? _getHtmlOptions(options) : _getHtmlOptions(null);
+        var cb = (_typeOf(options)==="function") ? options : callback;
+
+        var thisLoaded = [];
+        if (op.sequence){
+            _loadSequence(ms, cb, op, 0, thisLoaded, _loadSingleHtml, null, function(html){ if (html) _injectHtml(op, html.data ); });
+        }else{
+            _loadDisarray(ms, cb, op, thisLoaded, _loadSingleHtml, null, function(html){ if (html) _injectHtml(op, html.data ); });
+        }
+    };
+    this.o2.loadHtml = _loadHtml;
+    Element.prototype.loadHtml = function(modules, options, callback){
+        var op =  (_typeOf(options)==="object") ? options : {};
+        var cb = (_typeOf(options)==="function") ? options : callback;
+        op.dom = this;
+        _loadHtml(modules, op, cb);
+    };
+
+    //load all
+    _loadAll = function(modules, options, callback){
+        //var ms = (_typeOf(modules)==="array") ? modules : [modules];
+        var op =  (_typeOf(options)==="object") ? _getAllOptions(options) : _getAllOptions(null);
+        var cb = (_typeOf(options)==="function") ? options : callback;
+
+        var ms, htmls, styles, sctipts;
+        var _htmlLoaded=(!modules.html), _cssLoaded=(!modules.css), _jsLoaded=(!modules.js);
+        var _checkloaded = function(){
+            if (_htmlLoaded && _cssLoaded && _jsLoaded) if (cb) cb(htmls, styles, sctipts);
+        };
+        if (modules.html){
+            _loadHtml(modules.html, op, function(h){
+                htmls = h;
+                _htmlLoaded = true;
+                _checkloaded();
+            });
+        }
+        if (modules.css){
+            _loadCss(modules.css, op, function(s){
+                styles = s;
+                _cssLoaded = true;
+                _checkloaded();
+            });
+        }
+        if (modules.js){
+            _load(modules.js, op, function(s){
+                sctipts = s;
+                _jsLoaded = true;
+                _checkloaded();
+            });
+        }
+    };
+    this.o2.loadAll = _loadAll;
+    Element.prototype.loadAll = function(modules, options, callback){
+        var op =  (_typeOf(options)==="object") ? options : {};
+        var cb = (_typeOf(options)==="function") ? options : callback;
+        op.dom = this;
+        _loadAll(modules, op, cb);
+    };
+
+    var _getIfBlockEnd = function(v){
+        var rex = /(\{\{if\s+)|(\{\{\s*end if\s*\}\})/gmi;
+        var rexEnd = /\{\{\s*end if\s*\}\}/gmi;
+        var subs = 1;
+        while ((match = rex.exec(v)) !== null) {
+            var fullMatch = match[0];
+            if (fullMatch.search(rexEnd)!==-1){
+                subs--;
+                if (subs==0) break;
+            }else{
+                subs++
+            }
+        }
+        if (match) return {"codeIndex": match.index, "lastIndex": rex.lastIndex};
+        return {"codeIndex": v.length-1, "lastIndex": v.length-1};
+    }
+    var _getEachBlockEnd = function(v){
+        var rex = /(\{\{each\s+)|(\{\{\s*end each\s*\}\})/gmi;
+        var rexEnd = /\{\{\s*end each\s*\}\}/gmi;
+        var subs = 1;
+        while ((match = rex.exec(v)) !== null) {
+            var fullMatch = match[0];
+            if (fullMatch.search(rexEnd)!==-1){
+                subs--;
+                if (subs==0) break;
+            }else{
+                subs++;
+            }
+        }
+        if (match) return {"codeIndex": match.index, "lastIndex": rex.lastIndex};
+        return {"codeIndex": v.length-1, "lastIndex": v.length-1};
+    }
+
+    var _parseHtml = function(str, json){
+        var v = str;
+        var rex = /(\{\{\s*)[\s\S]*?(\s*\}\})/gmi;
+
+        var match;
+        while ((match = rex.exec(v)) !== null) {
+            var fullMatch = match[0];
+            var offset = 0;
+
+            //if statement begin
+            if (fullMatch.search(/\{\{if\s+/i)!==-1){
+                //找到对应的end if
+                var condition = fullMatch.replace(/^\{\{if\s*/i, "");
+                condition = condition.replace(/\s*\}\}$/i, "");
+                var flag = _jsonText(json, condition, "boolean");
+
+                var tmpStr = v.substring(rex.lastIndex, v.length);
+                var endIfIndex = _getIfBlockEnd(tmpStr);
+                if (flag){ //if 为 true
+                    var parseStr = _parseHtml(tmpStr.substring(0, endIfIndex.codeIndex), json);
+                    var vLeft = v.substring(0, match.index);
+                    var vRight = v.substring(rex.lastIndex+endIfIndex.lastIndex, v.length);
+                    v = vLeft + parseStr + vRight;
+                    offset = parseStr.length - fullMatch.length;
+                }else{
+                    v = v.substring(0, match.index) + v.substring(rex.lastIndex+endIfIndex.lastIndex, v.length);
+                    offset = 0-fullMatch.length;
+                }
+            }else  if (fullMatch.search(/\{\{each\s+/)!==-1) { //each statement
+                var itemString = fullMatch.replace(/^\{\{each\s*/, "");
+                itemString = itemString.replace(/\s*\}\}$/, "");
+                var eachValue = _jsonText(json, itemString, "object");
+
+                var tmpEachStr = v.substring(rex.lastIndex, v.length);
+                var endEachIndex = _getEachBlockEnd(tmpEachStr);
+
+                var parseEachStr = tmpEachStr.substring(0, endEachIndex.codeIndex);
+                var eachResult = "";
+                if (eachValue && _typeOf(eachValue)==="array"){
+                    for (var i=0; i<eachValue.length; i++){
+                        eachValue[i]._ = json;
+                        eachResult += _parseHtml(parseEachStr, eachValue[i]);
+                    }
+                    var eLeft = v.substring(0, match.index);
+                    var eRight = v.substring(rex.lastIndex+endEachIndex.lastIndex, v.length);
+                    v = eLeft + eachResult + eRight;
+                    offset = eachResult.length - fullMatch.length;
+                }else{
+                    v = v.substring(0, match.index) + v.substring(rex.lastIndex+endEachIndex.lastIndex, v.length);
+                    offset = 0-fullMatch.length;
+                }
+
+            }else{ //text statement
+                var text = fullMatch.replace(/^\{\{\s*/, "");
+                text = text.replace(/\}\}\s*$/, "");
+                var value = _jsonText(json, text);
+                offset = value.length-fullMatch.length;
+                v = v.substring(0, match.index) + value + v.substring(rex.lastIndex, v.length);
+            }
+            rex.lastIndex = rex.lastIndex + offset;
+        }
+        return v;
+    };
+    var _jsonText = function(json, text, type){
+        try {
+            var $ = json;
+            var f = eval("(function($){\n return "+text+";\n})");
+            returnValue = f.apply(json, [$]);
+            if (returnValue===undefined) returnValue="";
+            if (type==="boolean") return (!!returnValue);
+            if (type==="object") return returnValue;
+            returnValue = returnValue.toString();
+            return returnValue || "";
+        }catch(e){
+            if (type==="boolean") return false;
+            if (type==="object") return null;
+            return "";
+        }
+    };
+
+    o2.bindJson = function(str, json){
+        return _parseHtml(str, json);
+    };
+    String.prototype.bindJson = function(json){
+        return _parseHtml(this, json);
+    };
+
+    //dom ready
+    var _dom = {
+        ready: false,
+        loaded: false,
+        checks: [],
+        shouldPoll: false,
+        timer: null,
+        testElement: document.createElement('div'),
+        readys: [],
+
+        domready: function(){
+            clearTimeout(_dom.timer);
+            if (_dom.ready) return;
+            _dom.loaded = _dom.ready = true;
+            _removeListener(document, 'DOMContentLoaded', _dom.checkReady);
+            _removeListener(document, 'readystatechange', _dom.check);
+            _dom.onReady();
+        },
+        check: function(){
+            for (var i = _dom.checks.length; i--;) if (_dom.checks[i]() && window.MooTools && o2.core && o2.more){
+                _dom.domready();
+                return true;
+            }
+            return false;
+        },
+        poll: function(){
+            clearTimeout(_dom.timer);
+            if (!_dom.check()) _dom.timer = setTimeout(_dom.poll, 10);
+        },
+
+        /*<ltIE8>*/
+        // doScroll technique by Diego Perini http://javascript.nwbox.com/IEContentLoaded/
+        // testElement.doScroll() throws when the DOM is not ready, only in the top window
+        doScrollWorks: function(){
+            try {
+                _dom.testElement.doScroll();
+                return true;
+            } catch (e){}
+            return false;
+        },
+        /*</ltIE8>*/
+
+        onReady: function(){
+            for (var i=0; i<_dom.readys.length; i++){
+                this.readys[i].apply(window);
+            }
+        },
+        addReady: function(fn){
+            if (_dom.loaded){
+                if (fn) fn.apply(window);
+            }else{
+                if (fn) _dom.readys.push(fn);
+            }
+            return _dom;
+        },
+        checkReady: function(){
+            _dom.checks.push(function(){return true});
+            _dom.check();
+        }
+    };
+    var _loadO2 = function(){
+        this.o2.load("o2.core", _dom.check);
+        this.o2.load("o2.more", _dom.check);
+    };
+
+    _addListener(document, 'DOMContentLoaded', _dom.checkReady);
+
+    /*<ltIE8>*/
+    // If doScroll works already, it can't be used to determine domready
+    //   e.g. in an iframe
+    if (_dom.testElement.doScroll && !_dom.doScrollWorks()){
+        _dom.checks.push(_dom.doScrollWorks);
+        _dom.shouldPoll = true;
+    }
+    /*</ltIE8>*/
+
+    if (document.readyState) _dom.checks.push(function(){
+        var state = document.readyState;
+        return (state == 'loaded' || state == 'complete');
+    });
+
+    if ('onreadystatechange' in document) _addListener(document, 'readystatechange', _dom.check);
+    else _dom.shouldPoll = true;
+
+    if (_dom.shouldPoll) _dom.poll();
+
+    if (!window.MooTools){
+        this.o2.load("mootools", function(){ _loadO2(); _dom.check(); });
+    }else{
+        _loadO2();
+    }
+    this.o2.addReady = function(fn){ _dom.addReady.call(_dom, fn); };
+})();
+
+layout = window.layout || {};
+layout.desktop = layout;
+var locate = window.location;
+layout.protocol = locate.protocol;
+layout.session = layout.session || {};
+layout.debugger = (locate.href.toString().indexOf("debugger")!==-1);
+o2.xApplication = o2.xApplication || {};
+
+o2.xDesktop = o2.xDesktop || {};
+o2.xDesktop.requireApp = function(module, clazz, callback, async){
+    o2.requireApp(module, clazz, callback, async);
+};
+o2.addReady(function(){
+    //兼容方法
+    Element.implement({
+        "makeLnk": function(options){}
+    });
+
+    //异步载入必要模块
+    layout.config = null;
+
+    var modules = [ "MWF.xDesktop.Common", "MWF.xAction.RestActions"];
+    MWF.require(modules, function(){
+        if (layout.config) _getDistribute(function(){ _load(); });
+    });
+    o2.getJSON("/x_desktop/res/config/config.json", function(config){
+        layout.config = config;
+        if (MWF.xDesktop.getServiceAddress) _getDistribute(function(){ _load(); });
+    });
+
+
+    var _getDistribute = function(callback){
+        if (layout.config.app_protocol==="auto"){
+            layout.config.app_protocol = window.location.protocol;
+        }
+        MWF.xDesktop.getServiceAddress(layout.config, function(service, center){
+            layout.serviceAddressList = service;
+            layout.centerServer = center;
+            if (callback) callback();
+        }.bind(this));
+    };
+
+    var _load = function(){
+        //先判断用户是否登录
+        MWF.Actions.get("x_organization_assemble_authentication").getAuthentication(function(json){
+            //用户已经登录
+            layout.user = json.data;
+            layout.session = {};
+            layout.session.user = json.data;
+
+            (function(layout){
+                var _loadResource = function(callback){
+                    var isLoadedA = false;
+                    var isLoadedB = false;
+                    //var isLoadedC = false;
+
+                    var lp = o2.session.path+"/lp/"+o2.language+".js";
+                    var modules = [
+                        "o2.xDesktop.Dialog",
+                        "MWF.xDesktop.UserData",
+                        "MWF.xDesktop.Access",
+                        "MWF.widget.UUID",
+                        "MWF.xDesktop.Menu",
+                        "MWF.xDesktop.shortcut",
+                        "MWF.widget.PinYin",
+                        "MWF.xDesktop.Access",
+                        "MWF.xDesktop.MessageMobile",
+                        "MWF.xScript.Macro"
+                    ];
+                    //MWF.xDesktop.requireApp("Common", "", null, false);
+                    var _check = function(){ if (isLoadedA && isLoadedB) if (callback) callback(); };
+
+                    o2.load(["../o2_lib/mootools/plugin/mBox.min.js",lp], function(){isLoadedA = true; _check();});
+                    o2.require("MWF.widget.Common", function(){
+                        o2.require(modules, function(){
+                            o2.requireApp("Common", "", function(){isLoadedB = true; _check();})
+                        });
+                    });
+                };
+
+                var _loadContent =function(){
+                    _loadResource(function(){
+                        //this.Macro = new MWF.Macro["PageContext"](this);
+                        for (var i=0; i<layout.readys.length; i++){
+                            layout.readys[i].apply(window);
+                        }
+                    });
+                };
+
+                _loadContent();
+            })(layout);
+        }, function(){
+            //用户未经登录
+            //打开登录页面
+            var _loadResource = function(callback){
+                var isLoadedA = false;
+                var isLoadedB = false
+                //var isLoadedC = false;
+
+                var lp = o2.session.path+"/lp/"+o2.language+".js";
+                var modules = [
+                    "o2.xDesktop.Dialog",
+                    "MWF.xDesktop.UserData",
+                    "MWF.xDesktop.Access",
+                    "MWF.widget.UUID",
+                    "MWF.xDesktop.Menu",
+                    "MWF.xDesktop.shortcut",
+                    "MWF.widget.PinYin",
+                    "MWF.xDesktop.Access",
+                    "MWF.xDesktop.MessageMobile"
+                ];
+                //MWF.xDesktop.requireApp("Common", "", null, false);
+                var _check = function(){ if (isLoadedA && isLoadedB) if (callback) callback(); };
+
+                o2.load(["../o2_lib/mootools/plugin/mBox.min.js",lp], function(){isLoadedA = true; _check();});
+                o2.require("MWF.widget.Common", function(){
+                    o2.require(modules, function(){
+                        o2.requireApp("Common", "", function(){isLoadedB = true; _check();})
+                    });
+                });
+            };
+            _loadResource(function(){
+                layout.openLogin();
+            });
+
+        });
+
+        layout.openLogin = function(){
+            MWF.require("MWF.widget.Common", null, false);
+            MWF.require("MWF.xDesktop.Authentication", function(){
+                var authentication = new MWF.xDesktop.Authentication({
+                    "onLogin": _load.bind(layout)
+                });
+                authentication.loadLogin(document.body);
+            });
+        };
+    };
+});
+
+(function(layout){
+    layout.readys = [];
+    layout.addReady = function(){
+        for (var i = 0; i<arguments.length; i++){
+            if (o2.typeOf(arguments[i])==="function") layout.readys.push(arguments[i]);
+        }
+    };
+    var _requireApp = function(appNames, callback, clazzName){
+        var appPath = appNames.split(".");
+        var baseObject = o2.xApplication;
+        appPath.each(function(path, i){
+            if (i<(appPath.length-1)){
+                baseObject[path] = baseObject[path] || {};
+            }else {
+                baseObject[path] = baseObject[path] || {"options": Object.clone(MWF.xApplication.Common.options)};
+            }
+            baseObject = baseObject[path];
+        }.bind(this));
+        if (!baseObject.options) baseObject.options = Object.clone(MWF.xApplication.Common.options);
+
+        var _lpLoaded = false;
+        MWF.xDesktop.requireApp(appNames, "lp."+o2.language, {
+            "failure": function(){
+                MWF.xDesktop.requireApp(appNames, "lp.zh-cn", null, false);
+            }.bind(this)
+        }, false);
+        MWF.xDesktop.requireApp(appNames, clazzName, function(){
+            if (callback) callback(baseObject);
+        });
+    };
+    var _createNewApplication = function(e, appNamespace, appName, options, statusObj){
+        var app = new appNamespace["Main"](this, options);
+        app.desktop = layout;
+        app.inBrowser = true;
+        app.status = statusObj;
+        app.load(true);
+
+        var appId = appName;
+        if (options.appId){
+            appId = options.appId;
+        }else{
+            if (appNamespace.options.multitask) appId = appId+"-"+(new MWF.widget.UUID());
+        }
+        app.appId = appId;
+        layout.app = app;
+        layout.desktop.currentApp = app;
+    };
+    var _openWorkAndroid = function(options){
+        if (window.o2android && window.o2android.openO2Work) {
+            if (options.workId) {
+                window.o2android.openO2Work(options.workId, "", title);
+            } else if (options.workCompletedId) {
+                window.o2android.openO2Work("", options.workCompletedId, title);
+            }
+            return true;
+        }
+        return false;
+    };
+    var _openWorkIOS = function(options){
+        if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openO2Work) {
+            if (options.workId) {
+                window.webkit.messageHandlers.openO2Work.postMessage({
+                    "work": options.workId,
+                    "workCompleted": "",
+                    "title": title
+                });
+            } else if (options.workCompletedId) {
+                window.webkit.messageHandlers.openO2Work.postMessage({
+                    "work": "",
+                    "workCompleted": options.workCompletedId,
+                    "title": title
+                });
+            }
+            return true;
+        }
+        return false;
+    };
+    var _openWorkHTML = function(options){
+        var uri = new URI(window.location.href);
+        var redirectlink = uri.getData("redirectlink");
+        if (!redirectlink) {
+            redirectlink = encodeURIComponent(locate.pathname + locate.search);
+        } else {
+            redirectlink = encodeURIComponent(redirectlink);
+        }
+        if (options.workId) {
+            window.location = "workmobilewithaction.html?workid=" + options.workId + "&redirectlink=" + redirectlink;
+        } else if (options.workCompletedId) {
+            window.location = "workmobilewithaction.html?workcompletedid=" + options.workCompletedId + "&redirectlink=" + redirectlink;
+        }
+    };
+    var _openWork = function(options){
+        if (!_openWorkAndroid(options)) if (!_openWorkIOS(options)) _openWorkHTML(options);
+    };
+    var _openDocument = function(appNames, options, statusObj){
+        var par = "app="+encodeURIComponent(appNames)+"&status="+encodeURIComponent((statusObj)? JSON.encode(statusObj) : "")+"&option="+encodeURIComponent((options)? JSON.encode(options) : "");
+        if (window.o2android && window.o2android.openO2CmsDocument){
+            window.o2android.openO2CmsDocument(options.documentId, title);
+        }else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openO2CmsDocument){
+            window.webkit.messageHandlers.openO2CmsDocument.postMessage({"docId":options.documentId,"docTitle":title});
+        }else{
+            window.location = "appMobile.html?"+par;
+        }
+    };
+    var _openCms = function(appNames, options, statusObj){
+        var par = "app="+encodeURIComponent(appNames)+"&status="+encodeURIComponent((statusObj)? JSON.encode(statusObj) : "")+"&option="+encodeURIComponent((options)? JSON.encode(options) : "");
+        if (window.o2android && window.o2android.openO2CmsApplication){
+            window.o2android.openO2CmsApplication(options.columnId, title);
+        }else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openO2CmsApplication){
+            window.webkit.messageHandlers.openO2CmsApplication.postMessage(options.columnId);
+        }else{
+            window.location = "appMobile.html?app="+par;
+        }
+    };
+    var _openMeeting = function(appNames, options, statusObj){
+        var par = "app="+encodeURIComponent(appNames)+"&status="+encodeURIComponent((statusObj)? JSON.encode(statusObj) : "")+"&option="+encodeURIComponent((options)? JSON.encode(options) : "");
+        if (window.o2android && window.o2android.openO2Meeting){
+            window.o2android.openO2Meeting("");
+        }else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openO2Meeting){
+            window.webkit.messageHandlers.openO2Meeting.postMessage("");
+        }else{
+            window.location = "appMobile.html?app="+par;
+        }
+    };
+
+    var _openCalendar = function(appNames, options, statusObj){
+        var par = "app="+encodeURIComponent(appNames)+"&status="+encodeURIComponent((statusObj)? JSON.encode(statusObj) : "")+"&option="+encodeURIComponent((options)? JSON.encode(options) : "");
+        if (window.o2android && window.o2android.openO2Calendar){
+            window.o2android.openO2Calendar("");
+        }else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openO2Calendar){
+            window.webkit.messageHandlers.openO2Calendar.postMessage("");
+        }else{
+            window.location = "appMobile.html?app="+par;
+        }
+    };
+    var _openTaskCenter = function(appNames, options, statusObj){
+        var par = "app="+encodeURIComponent(appNames)+"&status="+encodeURIComponent((statusObj)? JSON.encode(statusObj) : "")+"&option="+encodeURIComponent((options)? JSON.encode(options) : "");
+        var tab = ((options && options.navi) ? options.navi : "task").toLowerCase();
+        if (tab==="done") tab = "taskCompleted";
+        if (tab==="readed") tab = "readCompleted";
+
+        if (window.o2android && window.o2android.openO2WorkSpace){
+            window.o2android.openO2WorkSpace(tab);
+        }else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openO2WorkSpace){
+            window.webkit.messageHandlers.openO2WorkSpace.postMessage(tab);
+        }else{
+            window.location = "appMobile.html?app="+par;
+        }
+    };
+
+    var _openApplicationMobile = function(appNames, options, statusObj){
+        switch (appNames) {
+            case "process.Work":
+                _openWork(options);
+                break;
+            case "cms.Document":
+                _openDocument(appNames, options, statusObj);
+                break;
+            case "cms.Module":
+                _openCms(appNames, options, statusObj);
+                break;
+            case "Meeting":
+                _openMeeting(appNames, options, statusObj);
+                break;
+            case "Calendar":
+                _openCalendar(appNames, options, statusObj);
+                break;
+            case "process.TaskCenter":
+                _openTaskCenter(appNames, options, statusObj);
+                break;
+            default:
+                window.location = "appMobile.html?app="+appNames+"&option="+(optionsStr || "")+"&status="+(statusStr || "");
+        }
+    };
+
+    layout.openApplication = function(e, appNames, options, statusObj){
+        if (layout.app){
+            if (layout.mobile){
+                _openApplicationMobile(appNames, options, statusObj);
+            }else{
+                var par = "app="+encodeURIComponent(appNames)+"&status="+encodeURIComponent((statusObj)? JSON.encode(statusObj) : "")+"&option="+encodeURIComponent((options)? JSON.encode(options) : "");
+                return window.open("app.html?"+par, "_blank");
+            }
+        }else{
+            var appPath = appNames.split(".");
+            var appName = appPath[appPath.length-1];
+
+            _requireApp(appNames, function(appNamespace){
+                _createNewApplication(e, appNamespace, appName, options, statusObj);
+            }.bind(this));
+        }
+    };
+
+    layout.refreshApp = function(app){
+        var status = app.recordStatus();
+
+        var uri = new URI(window.location.href);
+        var appNames = uri.getData("app");
+        var optionsStr = uri.getData("option");
+        var statusStr = uri.getData("status");
+        if (status) statusStr = JSON.encode(status);
+
+        var port = uri.get("port");
+        window.location = uri.get("scheme") + "://" + uri.get("host") + ((port) ? ":" + port + "/" : "") + uri.get("directory ") + "?app=" + encodeURIComponent(appNames) + "&status=" + encodeURIComponent(statusStr) + "&option=" + encodeURIComponent((options) ? JSON.encode(options) : "");
+    };
+
+    layout.load =function(appNames, options, statusObj){
+        layout.message = new MWF.xDesktop.MessageMobile();
+        layout.message.load();
+
+        layout.apps = [];
+        layout.node = $("layout");
+        var appName=appNames, m_status=statusObj, option=options;
+
+        var topWindow = window.opener;
+        if (topWindow){
+            try{
+                if (!appName) appName = topWindow.layout.desktop.openBrowserApp;
+                if (!m_status) m_status = topWindow.layout.desktop.openBrowserStatus;
+                if (!option)  option = topWindow.layout.desktop.openBrowserOption;
+            }catch(e){}
+        }
+        layout.openApplication(null, appName, option||{}, m_status);
+    }
+
+})(layout);

+ 28 - 87
o2web/source/o2_core/o2.js

@@ -53,8 +53,8 @@
     }
     this.o2 = {
         "version": {
-            "v": '2.0.9',
-            "build": "2018.11.22",
+            "v": '2.1.4',
+            "build": "2019.07.31",
             "info": "O2OA 活力办公 创意无限. Copyright © 2018, o2oa.net O2 Team All rights reserved."
         },
         "session": {
@@ -64,6 +64,7 @@
         "language": _lp,
         "splitStr": /\s*(?:,|;)\s*/
     };
+    this.wrdp = this.o2;
     
     var _attempt = function(){
         for (var i = 0, l = arguments.length; i < l; i++){
@@ -412,11 +413,28 @@
                     if (op.dom){
                         var rex = new RegExp("(.+)(?=\\{)", "g");
                         var match;
+                        var prefix = "." + uuid + " ";
+
                         while ((match = rex.exec(cssText)) !== null) {
-                            var prefix = "." + uuid + " ";
-                            var rule = prefix + match[0];
-                            cssText = cssText.substring(0, match.index) + rule + cssText.substring(rex.lastIndex, cssText.length);
-                            rex.lastIndex = rex.lastIndex + prefix.length;
+                            // var rule = prefix + match[0];
+                            // cssText = cssText.substring(0, match.index) + rule + cssText.substring(rex.lastIndex, cssText.length);
+                            // rex.lastIndex = rex.lastIndex + prefix.length;
+
+                            var rulesStr = match[0];
+                            if (rulesStr.indexOf(",")!=-1){
+                                var rules = rulesStr.split(/\s*,\s*/g);
+                                rules = rules.map(function(r){
+                                    return prefix + r;
+                                });
+                                var rule = rules.join(", ");
+                                cssText = cssText.substring(0, match.index) + rule + cssText.substring(rex.lastIndex, cssText.length);
+                                rex.lastIndex = rex.lastIndex + (prefix.length*rules.length);
+
+                            }else{
+                                var rule = prefix + match[0];
+                                cssText = cssText.substring(0, match.index) + rule + cssText.substring(rex.lastIndex, cssText.length);
+                                rex.lastIndex = rex.lastIndex + prefix.length;
+                            }
                         }
                     }
                     var style = op.doc.createElement("style");
@@ -600,85 +618,6 @@
         _loadAll(modules, op, cb);
     };
 
-    //json template
-    // _parseText = function(html, json){
-    //     var _ht = html;
-    //     var regexp = /(text\{).+?\}/g;
-    //     var r = _ht.match(regexp);
-    //     if(r) if (r.length){
-    //         for (var i=0; i<r.length; i++){
-    //             var text = r[i].substr(0,r[i].lastIndexOf("}"));
-    //             text = text.substr(text.indexOf("{")+1,text.length);
-    //             var value = _jsonText(json ,text);
-    //             _ht = _ht.replace(/(text\{).+?\}/,value);
-    //         }
-    //     }
-    //     return _ht;
-    // };
-    // _parseEach = function(html, json){
-    //     var _ht = html;
-    //     var regexp = /(\{each\([\s\S]+\)\})[\s\S]+?(\{endEach\})/g;
-    //     var r = _ht.match(regexp);
-    //     if(r){
-    //         if (r.length){
-    //             for (var i=0; i<r.length; i++){
-    //                 var eachItemsStr = r[i].substr(0,r[i].indexOf(")"));
-    //                 eachItemsStr = eachItemsStr.substr(eachItemsStr.indexOf("(")+1,eachItemsStr.length);
-    //                 var pars = eachItemsStr.split(/,[\s]*/g);
-    //                 eachItemsPar = pars[0];
-    //                 eachItemsCount = pars[1].toInt();
-    //
-    //                 var eachItems = _jsonText(json ,eachItemsPar);
-    //                 if (eachItems) if (eachItemsCount==0) eachItemsCount = eachItems.length;
-    //
-    //                 var eachContentStr = r[i].substr(0,r[i].lastIndexOf("{endEach}"));
-    //                 eachContentStr = eachContentStr.substr(eachContentStr.indexOf("}")+1,eachContentStr.length);
-    //
-    //                 var eachContent = [];
-    //                 if (eachItems){
-    //                     for (var n=0; n<Math.min(eachItems.length, eachItemsCount); n++){
-    //                         var item = eachItems[n];
-    //                         if (item){
-    //                             var tmpEachContentStr = eachContentStr;
-    //                             var textReg = /(eachText\{).+?\}/g;
-    //                             texts = tmpEachContentStr.match(textReg);
-    //                             if (texts){
-    //                                 if (texts.length){
-    //                                     for (var j=0; j<texts.length; j++){
-    //                                         var text = texts[j].substr(0,texts[j].lastIndexOf("}"));
-    //                                         text = text.substr(text.indexOf("{")+1,text.length);
-    //
-    //                                         var value = _jsonText(item ,text);
-    //                                         tmpEachContentStr = tmpEachContentStr.replace(/(eachText\{).+?\}/,value);
-    //                                     }
-    //                                 }
-    //                             }
-    //                             eachContent.push(tmpEachContentStr);
-    //                         }
-    //                     }
-    //                 }
-    //                 _ht = _ht.replace(/(\{each\([\s\S]+\)\})[\s\S]+?(\{endEach\})/,eachContent.join(""));
-    //             }
-    //         }
-    //     }
-    //     return _ht;
-    // };
-    // _jsonText = function(json, text){
-    //     var $ = json;
-    //     var f = eval("(x = function($){\n return "+text+";\n})");
-    //     returnValue = f.apply(json, [$]);
-    //     if (returnValue===undefined) returnValue="";
-    //     returnValue = returnValue.toString();
-    //     return returnValue || "";
-    // };
-    // var _bindJson = function(str, json){
-    //     return _parseEach(_parseText(str, json), json);
-    // };
-    // o2.bindJson = _bindJson;
-    // String.prototype.bindJson = function(json){
-    //     return _parseEach(_parseText(this, json), json);
-    // };
-
     var _getIfBlockEnd = function(v){
         var rex = /(\{\{if\s+)|(\{\{\s*end if\s*\}\})/gmi;
         var rexEnd = /\{\{\s*end if\s*\}\}/gmi;
@@ -784,6 +723,10 @@
             if (type==="boolean") return (!!returnValue);
             if (type==="object") return returnValue;
             returnValue = returnValue.toString();
+            returnValue = returnValue.replace(/\&/g, "&amp;");
+            returnValue = returnValue.replace(/>/g, "&gt;");
+            returnValue = returnValue.replace(/</g, "&lt;");
+            returnValue = returnValue.replace(/\"/g, "&quot;");
             return returnValue || "";
         }catch(e){
             if (type==="boolean") return false;
@@ -799,8 +742,6 @@
         return _parseHtml(this, json);
     };
 
-
-
     //dom ready
     var _dom = {
         ready: false,

+ 11 - 9
o2web/source/o2_core/o2/o2.core.js

@@ -55,9 +55,9 @@
 
     var _loaded = {};
 
-    var _requireJs = function(url, callback, async, compression){
+    var _requireJs = function(url, callback, async, compression, module){
         var key = encodeURIComponent(url);
-        if (_loaded[key]){o2.runCallback(callback, "success"); return "";}
+        if (_loaded[key]){o2.runCallback(callback, "success", [module]); return "";}
 
         var jsPath = (compression || !this.o2.session.isDebugger) ? url.replace(/\.js/, ".min.js") : url;
         jsPath = (jsPath.indexOf("?")!==-1) ? jsPath+"&v="+this.o2.version.v : jsPath+"?v="+this.o2.version.v;
@@ -66,8 +66,8 @@
             url: jsPath, async: async, method: "get",
             onSuccess: function(){
                 //try{
-                    _loaded[key] = true;
-                o2.runCallback(callback, "success");
+                _loaded[key] = true;
+                o2.runCallback(callback, "success", [module]);
                 //}catch (e){
                 //    o2.runCallback(callback, "failure", [e]);
                 //}
@@ -89,13 +89,14 @@
 
         var loadAsync = (async!==false);
 
-        _requireJs(jsPath, callback, loadAsync, compression);
+        _requireJs(jsPath, callback, loadAsync, compression, module);
     };
     var _requireSequence = function(fun, module, thisLoaded, thisErrorLoaded, callback, async, compression){
         var m = module.shift();
         fun(m, {
-            "onSuccess": function(){
-                thisLoaded.push(module[i]);
+            "onSuccess": function(m){
+                thisLoaded.push(m);
+                o2.runCallback(callback, "every", [m]);
                 if (module.length){
                     _requireSequence(module, thisLoaded, thisErrorLoaded, callback);
                 }else{
@@ -115,8 +116,9 @@
     var _requireDisarray = function(fun, module, thisLoaded, thisErrorLoaded, callback, async, compression){
         for (var i=0; i<module.length; i++){
             fun(module[i], {
-                "onSuccess": function(){
-                    thisLoaded.push(module[i]);
+                "onSuccess": function(m){
+                    thisLoaded.push(m);
+                    o2.runCallback(callback, "every", [m]);
                     if ((thisLoaded.length+thisErrorLoaded.length)===module.length){
                         if (thisErrorLoaded.length){
                             o2.runCallback(callback, "failure", [thisLoaded, thisErrorLoaded]);

TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/bg.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/buttonbg.png


+ 472 - 0
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/css.wcss

@@ -0,0 +1,472 @@
+{
+    "container": {
+        "border": "1px solid #b6b6b6",
+        //	"height": "180px",
+        "min-height": "180px",
+        "background": "#FFF",
+        "margin-top": "10px",
+        "overflow": "hidden"
+    },
+    "container_min": {
+        "border": "0px solid #b6b6b6",
+        "min-height": "30px",
+        "overflow": "hidden",
+        "background": "transparent",
+        "height": "auto"
+    },
+    "minActionAreaNode": {
+        "float": "right",
+        "height": "26px",
+        "margin": "2px",
+        "border": "1px solid #979797",
+        "border-radius": "3px",
+        "background-image": "url("+o2.session.path+"/widget/$AttachmentController/default/buttonbg.png)"
+    },
+    "minContentNode": {
+        "margin-right":"150px",
+        "min-height": "30px",
+        "overflow": "hidden"
+    },
+    "titleNode": {
+        "height": "30px",
+        "background-color": "#DDD",
+        "border-top": "1px solid #FFF",
+        "border-bottom": "1px solid #b6b6b6",
+        "line-height": "30px",
+        "font-weight": "bold",
+        "padding-left": "10px",
+        "text-align": "left"
+    },
+    "topNode": {
+        "height": "40px",
+        "background-image": "url("+o2.session.path+"/widget/$AttachmentController/default/bg.png)",
+        "border-top": "1px solid #FFF",
+        "border-bottom": "1px solid #b6b6b6"
+    },
+    "actionsBoxNode": {
+        "overflow": "hidden",
+        "border-bottom": "1px solid #eaebea",
+        "float": "left",
+        "margin-left": "10px",
+        "margin-top": "5px",
+        "border-radius": "3px",
+        "background-image": "url("+o2.session.path+"/widget/$AttachmentController/default/buttonbg.png)"
+    },
+    "actionsGroupNode": {
+        "height": "26px",
+        "border": "1px solid #979797",
+        "border-radius": "3px",
+        "background-image": "url("+o2.session.path+"/widget/$AttachmentController/default/buttonbg.png)"
+    },
+    "actionNode": {
+        "float": "left",
+        "width": "26px",
+        "height": "26px",
+        "cursor": "default"
+    },
+    "actionIconNode": {
+        "width": "26px",
+        "height": "26px",
+        "background-repeat": "no-repeat",
+        "background-position": "center center"
+    },
+    "separateNode": {
+        "width":"1px",
+        "height": "16px",
+        "margin-top": "5px",
+        "margin-left": "5px",
+        "margin-right": "5px",
+        "border-left": "1px solid #bcbcbc",
+        "background-color": "#f1f1f1",
+        "float": "left"
+    },
+    "contentScrollNode": {
+        "background-color": "#FFF",
+        "overflow": "hidden",
+        "min-height": "130px"
+    },
+    "contentNode": {
+        "background-color": "#FFF",
+        "overflow": "hidden",
+        "margin-right": "6px"
+    },
+    "bottomNode": {
+        "height": "7px",
+        "border-top": "1px solid #b6b6b6",
+        "background-color": "#e8e8e8"
+    },
+    "resizeNode": {
+        "height": "7px",
+        "cursor": "row-resize"
+    },
+    "attachmentNode_icon": {
+        "width": "90px",
+        "height": "100px",
+        "padding": "5px",
+        "float": "left",
+        "margin": "10px 0px 10px 10px",
+        "border-radius": "3px",
+        //"border": "2px solid #FFF",
+        "background": "transparent",
+        "-webkit-user-select": "text",
+        "-moz-user-select": "text"
+    },
+    "attachmentNode_icon_over": {
+        //"border": "2px solid #cdcfd1",
+        "background": "#eaf0f5"
+    },
+    "attachmentNode_icon_selected": {
+        //"border": "2px solid #d7e1e8",
+        "background": "#d7e1e8"
+    },
+    "attachmentIconNode": {
+        "width": "90px",
+        "height": "66px",
+        "text-align": "center"
+    },
+    "attachmentIconImgAreaNode": {
+        "width": "64px",
+        "height": "64px",
+        "margin": "auto"
+    },
+    "attachmentIconImgNode": {
+        "width": "64px",
+        "height": "64px"
+    },
+    "attachmentTextNode": {
+        "height": "34px",
+        "line-height": "17px",
+        "overflow": "hidden",
+        "word-break": "break-all",
+        "text-overflow": "ellipsis",
+        "text-align": "center"
+    },
+    "attachmentNode_sequence": {
+        "width": "auto",
+        "height": "30px",
+        "padding": "0px",
+        "float": "none",
+        "border-radius": "0px",
+        "border": "0px solid #FFF",
+        "background": "transparent",
+        "margin": "0px 10px",
+        "-webkit-user-select": "text",
+        "-moz-user-select": "text"
+    },
+    "attachmentNode_sequence_over": {
+        "border": "0px solid #cdcfd1",
+        "background": "#eaf0f5"
+    },
+    "attachmentNode_sequence_selected": {
+        "border": "0px solid #d7e1e8",
+        "background": "#d7e1e8"
+    },
+    "attachmentSeqNode_sequence": {
+        "width": "30px",
+        "height": "30px",
+        "line-height": "30px",
+        "color": "#666666",
+        "float": "left",
+        "text-align": "center"
+    },
+    "attachmentTextNode_sequence": {
+        "height": "30px",
+        "line-height": "30px",
+        "margin-left": "70px",
+        "overflow": "hidden",
+        "word-break": "break-all",
+        "text-overflow": "ellipsis",
+        "text-align": "left"
+    },
+
+    "attachmentNode_list": {
+        "width": "auto",
+        "height": "30px",
+        "padding": "0px",
+        "float": "none",
+        "border-radius": "0px",
+        "border": "0px solid #FFF",
+        "background": "transparent",
+        "margin": "0px 10px",
+        "-webkit-user-select": "text",
+        "-moz-user-select": "text"
+    },
+    "attachmentNode_list_over": {
+        "border": "0px solid #cdcfd1",
+        "background": "#eaf0f5"
+    },
+    "attachmentNode_list_selected": {
+        "border": "0px solid #d7e1e8",
+        "background": "#d7e1e8"
+    },
+
+
+
+    "attachmentIconNode_list": {
+        "width": "40px",
+        "height": "30px",
+        "float": "left"
+    },
+    "attachmentIconImgAreaNode_list": {
+        "width": "24px",
+        "height": "24px",
+        "margin": "3px 8px"
+    },
+    "attachmentIconImgNode_list": {
+        "width": "24px",
+        "height": "24px"
+    },
+    "attachmentTextNode_list": {
+        "height": "30px",
+        "line-height": "30px",
+        "margin-left": "40px",
+        "overflow": "hidden",
+        "word-break": "break-all",
+        "text-overflow": "ellipsis",
+        "text-align": "left"
+    },
+    "attachmentTextTitleNode_list": {
+        "height": "30px",
+        "float": "left",
+        "width": "30%"
+    },
+    "attachmentTextSizeNode_list": {
+        "height": "30px",
+        "float": "left",
+        "width": "15%"
+    },
+    "attachmentTextUploaderNode_list": {
+        "height": "30px",
+        "float": "left",
+        "width": "15%"
+    },
+    "attachmentTextTimeNode_list": {
+        "height": "30px",
+        "float": "left",
+        "width": "20%"
+    },
+    "attachmentTextActivityNode_list": {
+        "height": "30px",
+        "float": "left",
+        "width": "20%"
+    },
+
+    "attachmentNode_preview": {
+        "width": "180px",
+        "height": "160px",
+        "padding": "5px",
+        "float": "left",
+        "margin": "10px 5px 10px 5px",
+        "border-radius": "3px",
+        //"border": "2px solid #FFF",
+        "background": "transparent",
+        "-webkit-user-select": "text",
+        "-moz-user-select": "text"
+    },
+    "attachmentNode_preview_over": {
+        //"border": "2px solid #cdcfd1",
+        "background": "#eaf0f5"
+    },
+    "attachmentNode_preview_selected": {
+        //"border": "2px solid #d7e1e8",
+        "background": "#d7e1e8"
+    },
+    "attachmentPreviewIconNode": {
+        "width": "180px",
+        "height": "126px",
+        "text-align": "center"
+    },
+    "attachmentPreviewIconImgAreaNode": {
+        "width": "72px",
+        "height": "72px",
+        "margin": "auto"
+    },
+    "attachmentPreviewIconImgNode": {
+        "width": "72px",
+        "height": "72px",
+        "margin-top": "22px"
+    },
+    "attachmentPreviewAudioNode": {
+        "width": "180px",
+        "height": "30px",
+        "position": "relative",
+        "top": "-64px",
+        "opacity": "0.7"
+    },
+    "attachmentPreviewVideoNode": {
+        "width": "180px",
+        "height": "126px"
+    },
+    "attachmentPreviewTextNode": {
+        "height": "34px",
+        "line-height": "17px",
+        "overflow": "hidden",
+        "word-break": "break-all",
+        "text-overflow": "ellipsis",
+        "text-align": "center"
+    },
+
+    "minActionNode": {
+        "float": "left",
+        "width": "26px",
+        "height": "26px",
+        "margin" : "0px 5px",
+        "cursor": "default"
+    },
+    "minActionIconNode": {
+        "width": "26px",
+        "height": "26px",
+        "background-repeat": "no-repeat",
+        "background-position": "center center"
+    },
+    "minAttachmentNode_sequence": {
+        "width": "auto",
+        "height": "30px",
+        "padding": "0px",
+        "float": "none",
+        "border-radius": "0px",
+        "border": "0px solid #FFF",
+        "background": "transparent",
+        "margin": "0px 5px",
+        "-webkit-user-select": "text",
+        "-moz-user-select": "text"
+    },
+    "minAttachmentNode_sequence_over": {
+        "border": "0px solid #cdcfd1",
+        "background-color": "#eaf0f5"
+    },
+    "minAttachmentNode_sequence_selected": {
+        "border": "0px solid #d7e1e8",
+        "background-color": "#d7e1e8"
+    },
+    "minAttachmentNode_list": {
+        "width": "auto",
+        "height": "30px",
+        "padding": "13px 0px 13px 10px",
+        "float": "left",
+        "border-radius": "0px",
+        "border": "0px solid #FFF",
+        "background": "transparent",
+        "-webkit-user-select": "text",
+        "-moz-user-select": "text",
+        "width" : "48%",
+        "border-bottom" : "1px dashed #dcdcdc"
+    },
+    "minAttachmentSepNode_list" : {
+        "width" : "1px",
+        "height" : "30px",
+        "float" : "right",
+        "overflow" : "hidden",
+        "border-right" : "1px dashed #dcdcdc"
+    },
+    "minAttachmentNode_list_over": {
+        //"border": "0px solid #cdcfd1",
+        //"background-color": "#eaf0f5"
+    },
+    "minAttachmentNode_list_selected": {
+        //"border": "0px solid #d7e1e8",
+        "background-color": "#d7e1e8"
+    },
+    "minAttachmentIconNode_list": {
+        "width": "30px",
+        "height": "30px",
+        "float": "left"
+    },
+    "minAttachmentIconImgAreaNode_list": {
+        "width": "24px",
+        "height": "24px",
+        "margin": "3px 3px"
+    },
+    "minAttachmentIconImgNode_list": {
+        "width": "24px",
+        "height": "24px"
+    },
+    "minAttachmentTextNode_list": {
+        "height": "30px",
+        "cursor": "default",
+        "line-height": "30px",
+        "margin-left": "0px",
+        "margin-right": "0px",
+        "overflow": "hidden",
+        "word-break": "break-all",
+        "text-overflow": "ellipsis",
+        "text-align": "left",
+        "color" : "#0b82ff"
+    },
+    "minAttachmentSizeNode_list": {
+        "height": "30px",
+        "line-height": "30px",
+        "margin-left" : "0px",
+        "margin-right": "6px",
+        "display" : "inline",
+        "color" : "#555"
+    },
+    "minAttachmentActionAreaNode" : {
+        "float" : "right",
+        "overflow" : "hidden"
+    },
+    "inputUploadAreaNode": {
+        "width": "450px",
+        "height": "160px",
+        "border-radius": "5px",
+        "box-shadow": "0px 0px 10px #FFF",
+        "position": "absolute",
+        "border": "2px solid #999",
+        "border-top": "4px solid #5290e5",
+        "background-color": "#FFF",
+        "z-index": 100
+    },
+    "inputUploadAreaTitleNode": {
+        "height": "30px",
+        "line-height": "30px",
+        "text-align": "center",
+        "font-weight": "bold",
+        "font-family": "微软雅黑",
+        "font-size": "14px"
+    },
+    "inputUploadAreaInforNode": {
+        "height": "24px",
+        "line-height": "24px",
+        "text-align": "center",
+        "color": "#666",
+        "font-family": "微软雅黑",
+        "font-size": "12px",
+        "text-align": "left",
+        "margin": "10px 20px 0px 20px"
+    },
+    "inputUploadAreaInputAreaNode": {
+        "margin": "0px 20px 20px 20px",
+        "height": "24px"
+    },
+    "inputUploadAreaInputNode": {
+        "width": "407px",
+        "height": "23px",
+        "border": "1px solid #666",
+    },
+    "inputUploadActionNode": {
+        "margin": "0px 20px",
+        "border-top": "1px solid #999",
+    },
+    "inputUploadOkButton": {
+        "height": "24px",
+        "width": "80px",
+        "color": "#FFF",
+        "background-color": "#42699e",
+        "border": "1px solid #1e3d67",
+        "border-radius": "3px",
+        "float": "right",
+        "margin-top": "10px",
+        "margin-left": "10px"
+    },
+    "inputUploadCancelButton": {
+        "height": "24px",
+        "width": "80px",
+        "color": "#666",
+        "background-color": "#DFDFDF",
+        "border": "1px solid #666",
+        "border-radius": "3px",
+        "float": "right",
+        "margin-top": "10px",
+        "margin-left": "10px"
+    }
+}

TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/check.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/check_gray.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/closeOffice.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/closeOffice_gray.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config_gray.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config_single.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/config_single_over.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/createFolder.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/createFolder_gray.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete_gray.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete_single.png


TEMPAT SAMPAH
o2web/source/o2_core/o2/widget/$AttachmentController/cmcc/icon/delete_single_over.png


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini