fancy пре 6 година
родитељ
комит
3e384bb948
40 измењених фајлова са 2211 додато и 324 уклоњено
  1. 3 5
      o2android/app/build.gradle
  2. BIN
      o2android/app/libs/o2_auth_sdk-release.aar
  3. 4 13
      o2android/app/src/main/AndroidManifest.xml
  4. 78 10
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSApplicationActivity.kt
  5. 6 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSApplicationContract.kt
  6. 61 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSApplicationPresenter.kt
  7. 4 1
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSCategoryPresenter.kt
  8. 135 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSPublishDocumentActivity.kt
  9. 25 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSPublishDocumentContract.kt
  10. 61 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSPublishDocumentPresenter.kt
  11. 1 1
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/index/CMSIndexActivity.kt
  12. 356 91
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/view/CMSWebViewActivity.kt
  13. 12 2
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/view/CMSWebViewContract.kt
  14. 167 10
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/view/CMSWebViewPresenter.kt
  15. 3 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/o2/main/IndexPresenter.kt
  16. 15 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/model/bo/CMSWorkControl.kt
  17. 12 0
      o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/model/vo/CmsFilter.kt
  18. 1 2
      o2android/app/src/main/res/layout/activity_cms_main.xml
  19. 129 0
      o2android/app/src/main/res/layout/activity_cms_publish_document.xml
  20. 61 18
      o2android/app/src/main/res/layout/activity_cms_web_view_document.xml
  21. 2 1
      o2android/app/src/main/res/layout/activity_local_image_view.xml
  22. 2 2
      o2android/app/src/main/res/layout/item_cms_main_application.xml
  23. 12 0
      o2android/app/src/main/res/menu/menu_cms_create.xml
  24. 5 0
      o2android/app/src/main/res/values/strings.xml
  25. 8 0
      o2android/app/src/main/res/values/styles.xml
  26. 2 2
      o2android/gradle.properties
  27. 12 0
      o2ios/O2Platform.xcodeproj/project.pbxproj
  28. 2 2
      o2ios/O2Platform/Info.plist
  29. 80 3
      o2ios/O2Platform/cms/c/CMSCategoryListViewController.swift
  30. 170 0
      o2ios/O2Platform/cms/c/CMSCreateDocViewController.swift
  31. 464 139
      o2ios/O2Platform/cms/c/CMSItemDetailViewController.swift
  32. 96 0
      o2ios/O2Platform/cms/m/CMSAttachmentInfoResponse.swift
  33. 6 0
      o2ios/O2Platform/cms/m/CMSCategoryItemData.swift
  34. 2 2
      o2ios/O2Platform/cms/m/CMSPublishInfoData.swift
  35. 100 0
      o2ios/O2Platform/cms/m/CMSSingleApplication.swift
  36. 9 1
      o2ios/O2Platform/cms/v/CMSItemTableViewCell.swift
  37. 32 0
      o2ios/O2Platform/common/ext/Date+Extension.swift
  38. 8 1
      o2ios/O2Platform/config/O2URLContext.swift
  39. 61 14
      o2ios/O2Platform/storyboard/information.storyboard
  40. 4 4
      o2ios/O2Platform/storyboard/task.storyboard

+ 3 - 5
o2android/app/build.gradle

@@ -94,7 +94,7 @@ android {
         multiDexEnabled true
         ndk {
             //选择要添加的对应cpu类型的.so库。
-            abiFilters 'armeabi', 'armeabi-v7a' //, 'arm64-v8a' //, 'x86', 'x86_64'
+            abiFilters 'armeabi', 'armeabi-v7a'
             // 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
         }
         multiDexKeepProguard file('multidex_keep_file.pro')
@@ -137,7 +137,7 @@ android {
                                     BAIDU_SPEECH_APPKEY: project.baiduSpeechAppKey,
                                     BAIDU_MAP_APPKEY   : project.baiduMapAppKey,
                                     BUGLY_APPID        : project.buglyAppId]
-            zipAlignEnabled true     //Zipalign优化
+            zipAlignEnabled true
             minifyEnabled true     //混淆
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 
@@ -201,8 +201,6 @@ repositories {
 }
 
 dependencies {
-
-    //    implementation fileTree(include: ['*.jar'], dir: 'libs')
     implementation files('libs/BaiduLBS_Android.jar')
     implementation files('libs/bdasr_V3_20180320_9066860.jar')
     implementation files('libs/com.baidu.tts_2.3.1.20170808_e39ea89.jar')
@@ -309,7 +307,7 @@ dependencies {
     testImplementation 'junit:junit:4.12'
     implementation 'com.google.code.gson:gson:2.8.5'
 
-    //
+    //activity result
     implementation 'com.github.lwugang:ActivityResult:59b23e3682'
 
 }

BIN
o2android/app/libs/o2_auth_sdk-release.aar


+ 4 - 13
o2android/app/src/main/AndroidManifest.xml

@@ -41,7 +41,10 @@
         android:label="@string/app_name"
         android:roundIcon="@mipmap/logo_round"
         android:theme="@style/XBPMTheme.NoActionBar">
-        <activity android:name=".app.o2.webview.LocalImageViewActivity"></activity>
+        <activity android:name=".app.cms.application.CMSPublishDocumentActivity" />
+        <activity android:name=".app.o2.webview.LocalImageViewActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMTheme.fullscreen" />
         <activity android:name=".app.o2.security.DeviceManagerActivity" />
         <activity
             android:name=".app.o2.launch.LaunchActivity"
@@ -144,18 +147,6 @@
             android:name=".app.o2.webview.TaskWebViewActivity"
             android:label="@string/title_activity_work_web_view"
             android:screenOrientation="portrait" />
-        <activity
-            android:name=".app.o2.webview.TaskCompletedWebViewActivity"
-            android:label="@string/title_activity_work_web_view"
-            android:screenOrientation="portrait" />
-        <activity
-            android:name=".app.o2.webview.ReadWebViewActivity"
-            android:label="@string/title_activity_work_web_view"
-            android:screenOrientation="portrait" />
-        <activity
-            android:name=".app.o2.webview.ReadCompletedWebViewActivity"
-            android:label="@string/title_activity_work_web_view"
-            android:screenOrientation="portrait" />
         <activity
             android:name=".app.o2.process.TaskListActivity"
             android:label="@string/title_activity_task_list"

+ 78 - 10
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSApplicationActivity.kt

@@ -3,33 +3,43 @@ package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.application
 
 import android.os.Bundle
 import android.support.design.widget.TabLayout
+import android.support.v4.content.ContextCompat
 import android.support.v4.view.GravityCompat
 import android.text.SpannableString
 import android.text.Spanned
 import android.text.TextUtils
 import android.text.style.ForegroundColorSpan
+import android.view.Menu
+import android.view.MenuItem
 import android.widget.TextView
 import kotlinx.android.synthetic.main.activity_cms_application.*
 import net.muliba.changeskin.FancySkinManager
 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.cms.view.CMSWebViewActivity
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CMSApplicationPagerAdapter
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonAdapter
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.ViewHolder
 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
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSDocumentInfoJson
+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.addOnPageChangeListener
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.go
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.BottomSheetMenu
 
 
 class CMSApplicationActivity : BaseMVPActivity<CMSApplicationContract.View, CMSApplicationContract.Presenter>(), CMSApplicationContract.View {
+
+
     override var mPresenter: CMSApplicationContract.Presenter = CMSApplicationPresenter()
 
     override fun layoutResId(): Int = R.layout.activity_cms_application
 
     companion object {
-        val CMS_APP_OBJECT = "CMS_APP_OBJECT"
+        const val CMS_APP_OBJECT = "CMS_APP_OBJECT"
 
         fun startBundleData(applicationInfo: CMSApplicationInfoJson): Bundle {
             val bundle = Bundle()
@@ -38,16 +48,16 @@ class CMSApplicationActivity : BaseMVPActivity<CMSApplicationContract.View, CMSA
         }
     }
 
-    var currentCategory = ""
-    var application: CMSApplicationInfoJson? = null
-    val pagerAdapter: CMSApplicationPagerAdapter by lazy { CMSApplicationPagerAdapter(supportFragmentManager, application?.wrapOutCategoryList ?: ArrayList<CMSCategoryInfoJson>()) }
-    val menuList = ArrayList<CMSCategoryInfoJson>()
-    val menuAdapter: CommonAdapter<CMSCategoryInfoJson> by lazy {
+    private var currentCategory = ""
+    private var application: CMSApplicationInfoJson? = null
+    private val pagerAdapter: CMSApplicationPagerAdapter by lazy { CMSApplicationPagerAdapter(supportFragmentManager, application?.wrapOutCategoryList ?: ArrayList<CMSCategoryInfoJson>()) }
+    private val menuList = ArrayList<CMSCategoryInfoJson>()
+    private val menuAdapter: CommonAdapter<CMSCategoryInfoJson> by lazy {
         object : CommonAdapter<CMSCategoryInfoJson>(this, menuList, R.layout.item_tab_application_menu) {
             override fun convert(holder: ViewHolder?, t: CMSCategoryInfoJson?) {
                 val textview = holder?.getView<TextView>(R.id.tv_item_tab_application_menu_name)
                 textview?.text = t?.categoryName ?: ""
-                if (!TextUtils.isEmpty(currentCategory) && currentCategory.equals(t?.categoryName)) {
+                if (!TextUtils.isEmpty(currentCategory) && currentCategory == t?.categoryName) {
                     textview?.setTextColor(FancySkinManager.instance().getColor(this@CMSApplicationActivity, R.color.z_color_primary))
                 } else {
                     textview?.setTextColor(FancySkinManager.instance().getColor(this@CMSApplicationActivity, R.color.z_color_text_primary))
@@ -55,6 +65,8 @@ class CMSApplicationActivity : BaseMVPActivity<CMSApplicationContract.View, CMSA
             }
         }
     }
+    private val canPublishCategories = ArrayList<CMSCategoryInfoJson>()
+    private var publishCategoryIndex = -1
 
     override fun afterSetContentView(savedInstanceState: Bundle?) {
         if (intent.extras?.getSerializable(CMS_APP_OBJECT) == null) {
@@ -63,11 +75,12 @@ class CMSApplicationActivity : BaseMVPActivity<CMSApplicationContract.View, CMSA
             return
         }
         application = intent.extras?.getSerializable(CMS_APP_OBJECT) as CMSApplicationInfoJson
-
-
+        if (application == null) {
+            XToast.toastShort(this, "参数不正确!")
+            finish()
+        }
         setupToolBar(application?.appName ?: "", true)
 
-
         lv_drawer_category_list.adapter = menuAdapter
         lv_drawer_category_list.setOnItemClickListener { _, _, position, _ ->
             view_pager_cms_application.currentItem = position
@@ -104,6 +117,45 @@ class CMSApplicationActivity : BaseMVPActivity<CMSApplicationContract.View, CMSA
         })
         tab_cms_application_category.getTabAt(0)?.select()
         menuAdapter.notifyDataSetChanged()
+        mPresenter.loadCanPublishCategory(application!!.id)
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+        menu?.clear()
+        if (canPublishCategories.size>0) {
+            menuInflater?.inflate(R.menu.menu_cms_create, menu)
+        }
+        return super.onPrepareOptionsMenu(menu)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+        if(item?.itemId == R.id.menu_cms_create) {
+            showPublishCategoriesList()
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+
+
+    override fun canPublishCategories(list: List<CMSCategoryInfoJson>) {
+        if (list.isNotEmpty()) {
+            canPublishCategories.clear()
+            canPublishCategories.addAll(list)
+        }
+        //刷新菜单按钮
+        invalidateOptionsMenu()
+    }
+
+    override fun documentDraft(list: List<CMSDocumentInfoJson>) {
+        if (list.isEmpty()) {
+            XLog.info("没有草稿,跳转到发布页面")
+            go<CMSPublishDocumentActivity>(CMSPublishDocumentActivity.start(canPublishCategories[publishCategoryIndex]))
+        }else {
+            XLog.info("有草稿,跳转到详细页面")
+            val document = list[0]
+            go<CMSWebViewActivity>(CMSWebViewActivity.startBundleData(document.id, document.title))
+        }
     }
 
 
@@ -119,4 +171,20 @@ class CMSApplicationActivity : BaseMVPActivity<CMSApplicationContract.View, CMSA
         tab?.text = spannableString
     }
 
+
+    private fun showPublishCategoriesList() {
+        val items = canPublishCategories.map { it.categoryName }
+        BottomSheetMenu(this)
+                .setTitle("选择发布的分类")
+                .setItems(items, ContextCompat.getColor(this, R.color.z_color_primary)){ index ->
+                    XLog.info("选择了$index 分类")
+                    publishCategoryIndex = index
+                    mPresenter.findDocumentDraftWithCategory(canPublishCategories[index].id)
+                }.setCancelButton("取消", ContextCompat.getColor(this, R.color.z_color_text_hint)) {
+                    XLog.debug("取消。。。。。")
+                }
+                .show()
+    }
+
+
 }

+ 6 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSApplicationContract.kt

@@ -2,12 +2,18 @@ package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.application
 
 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.bo.api.cms.CMSCategoryInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSDocumentInfoJson
 
 
 object CMSApplicationContract {
     interface View : BaseView {
+        fun canPublishCategories(list: List<CMSCategoryInfoJson>)
+        fun documentDraft(list: List<CMSDocumentInfoJson>)
     }
 
     interface Presenter : BasePresenter<View> {
+        fun loadCanPublishCategory(appId: String)
+        fun findDocumentDraftWithCategory(categoryId: String)
     }
 }

+ 61 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSApplicationPresenter.kt

@@ -1,8 +1,69 @@
 package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.application
 
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.CmsFilter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import okhttp3.MediaType
+import okhttp3.RequestBody
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
 
 class CMSApplicationPresenter : BasePresenterImpl<CMSApplicationContract.View>(), CMSApplicationContract.Presenter {
 
 
+
+
+    override fun findDocumentDraftWithCategory(categoryId: String) {
+        val put = CmsFilter()
+        val cateList = ArrayList<String>()
+        cateList.add(categoryId)
+        put.categoryIdList = cateList
+        val personList = ArrayList<String>()
+        personList.add(O2SDKManager.instance().distinguishedName)
+        put.creatorList= personList
+        val json = O2SDKManager.instance().gson.toJson(put)
+        val body = RequestBody.create(MediaType.parse("text/json"), json)
+
+        getCMSAssembleControlService(mView?.getContext())?.findDocumentDraftListWithCategory(body)
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.o2Subscribe {
+                    onNext {
+                        if (it.data!=null) {
+                            mView?.documentDraft(it.data)
+                        }else{
+                            mView?.documentDraft(ArrayList())
+                        }
+                    }
+                    onError { e, isNetworkError ->
+                        XLog.error("查询草稿列表错误, netErr: $isNetworkError", e)
+                        mView?.documentDraft(ArrayList())
+                    }
+                }
+    }
+
+
+    override fun loadCanPublishCategory(appId: String) {
+
+        val service = getCMSAssembleControlService(mView?.getContext())
+        service
+                ?.canPublishCategories(appId)
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.o2Subscribe {
+                    onNext {
+                        val app = it.data
+                        if (app!=null && app.wrapOutCategoryList.isNotEmpty()) {
+                            mView?.canPublishCategories(app.wrapOutCategoryList)
+                        }
+                    }
+                    onError { e, isNetworkError ->
+                        XLog.error("查询发布列表出错, netErr: $isNetworkError", e)
+                    }
+                }
+    }
+
+
 }

+ 4 - 1
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSCategoryPresenter.kt

@@ -24,7 +24,10 @@ class CMSCategoryPresenter : BasePresenterImpl<CMSCategoryContract.View>(), CMSC
         val wrapIn = HashMap<String, ArrayList<String>>()
         val category = ArrayList<String>()
         category.add(id)
-        wrapIn.put("categoryIdList", category)
+        wrapIn["categoryIdList"] = category
+        val status = ArrayList<String>()
+        status.add("published")
+        wrapIn["statusList"] = status
         val json = O2SDKManager.instance().gson.toJson(wrapIn)
         val body = RequestBody.create(MediaType.parse("text/json"), json)
         getCMSAssembleControlService(mView?.getContext())?.let { service ->

+ 135 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSPublishDocumentActivity.kt

@@ -0,0 +1,135 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.application
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.RadioButton
+import android.widget.RadioGroup
+import kotlinx.android.synthetic.main.activity_cms_publish_document.*
+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.cms.view.CMSWebViewActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSCategoryInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSDocumentInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.WoIdentityListItem
+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.go
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.goThenKill
+import org.jetbrains.anko.dip
+import java.util.*
+import kotlin.collections.ArrayList
+
+class CMSPublishDocumentActivity : BaseMVPActivity<CMSPublishDocumentContract.View, CMSPublishDocumentContract.Presenter>(),
+        CMSPublishDocumentContract.View {
+
+
+    companion object {
+        const val  CATEGORY_KEY = "START_CREATE_DOCUMENT_CATEGORY_KEY"
+
+        fun start(category: CMSCategoryInfoJson): Bundle {
+            val bundle = Bundle()
+            bundle.putSerializable(CATEGORY_KEY, category)
+            return bundle
+        }
+
+    }
+
+    override var mPresenter: CMSPublishDocumentContract.Presenter = CMSPublishDocumentPresenter()
+
+
+    override fun layoutResId(): Int = R.layout.activity_cms_publish_document
+
+
+    private var category: CMSCategoryInfoJson? = null
+    private val identityList = ArrayList<WoIdentityListItem>()
+    private var identity = ""
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        category = intent.extras?.getSerializable(CATEGORY_KEY) as? CMSCategoryInfoJson
+        if (category == null) {
+            XToast.toastShort(this, "参数不正确!")
+            finish()
+        }
+        setupToolBar("新建文档 - ${category?.categoryName}", true)
+//        tv_cms_publish_header.text = "新建文档 - ${category?.categoryName}"
+        mPresenter.findCurrentPersonIdentity()
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+        menuInflater?.inflate(R.menu.menu_cms_create, menu)
+        return super.onPrepareOptionsMenu(menu)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+        if(item?.itemId == R.id.menu_cms_create) {
+            createDocument()
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+
+    override fun currentPersonIdentities(list: List<WoIdentityListItem>) {
+        radio_group_cms_publish_identity.removeAllViews()
+        identityList.clear()
+        identityList.addAll(list)
+        if (identityList.size>0) {
+            identityList.mapIndexed { index, it ->
+                val radio = layoutInflater.inflate(R.layout.snippet_radio_button, null) as RadioButton
+                radio.text = if (TextUtils.isEmpty(it.unitName)) it.name else it.name+"/"+it.unitName
+                if (index==0) {
+                    radio.isChecked = true
+                    identity = it.distinguishedName
+                }
+                radio.id = 100 + index//这里必须添加id 否则后面获取选中Radio的时候 group.getCheckedRadioButtonId() 拿不到id 会有空指针异常
+                val layoutParams = RadioGroup.LayoutParams(RadioGroup.LayoutParams.MATCH_PARENT, RadioGroup.LayoutParams.WRAP_CONTENT)
+                layoutParams.setMargins(0, dip(10f), 0, 0)
+                radio_group_cms_publish_identity.addView(radio, layoutParams)
+            }
+        }
+        radio_group_cms_publish_identity.setOnCheckedChangeListener { _, checkedId ->
+            val index = checkedId - 100
+            identity = identityList[index].distinguishedName
+        }
+    }
+
+    override fun newDocumentId(id: String) {
+        if (!TextUtils.isEmpty(id)) {
+            val title = edit_cms_publish_title.text.toString()
+            goThenKill<CMSWebViewActivity>(CMSWebViewActivity.startBundleData(id, title))
+        }else {
+            XToast.toastShort(this, "保存失败, 没有返回id!")
+        }
+    }
+
+    override fun newDocumentFail() {
+        XToast.toastShort(this, "保存失败!")
+    }
+
+    private fun createDocument() {
+        val title = edit_cms_publish_title.text.toString()
+        if(TextUtils.isEmpty(title)) {
+            XToast.toastShort(this, "标题不能为空!")
+            return
+        }
+        if(TextUtils.isEmpty(identity)) {
+            XToast.toastShort(this, "身份不能为空!")
+            return
+        }
+        val document = CMSDocumentInfoJson()
+//        document.id = UUID.randomUUID().toString()
+        document.title = title
+        document.appId = category!!.appId
+        document.categoryId = category!!.id
+        document.categoryAlias = category!!.categoryAlias
+        document.categoryName = category!!.categoryName
+        document.creatorIdentity = identity
+        document.docStatus = "draft"
+        document.isNewDocument = true
+        XLog.info(document.toString())
+        mPresenter.newDocument(document)
+
+    }
+
+}

+ 25 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSPublishDocumentContract.kt

@@ -0,0 +1,25 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.application
+
+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.bo.api.cms.CMSDocumentInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.ProcessWOIdentityJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.WoIdentityListItem
+
+/**
+ * Created by fancyLou on 2019-07-03.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+object CMSPublishDocumentContract {
+    interface View : BaseView {
+        fun currentPersonIdentities(list: List<WoIdentityListItem>)
+        fun newDocumentId(id: String)
+        fun newDocumentFail()
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun findCurrentPersonIdentity()
+        fun newDocument(doc: CMSDocumentInfoJson)
+    }
+}

+ 61 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/application/CMSPublishDocumentPresenter.kt

@@ -0,0 +1,61 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.application
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSDocumentInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import okhttp3.MediaType
+import okhttp3.RequestBody
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+/**
+ * Created by fancyLou on 2019-07-03.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+class CMSPublishDocumentPresenter : BasePresenterImpl<CMSPublishDocumentContract.View>(), CMSPublishDocumentContract.Presenter {
+    override fun findCurrentPersonIdentity() {
+        getAssemblePersonalApi(mView?.getContext())
+                ?.getCurrentPersonInfo()
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.o2Subscribe {
+                    onNext {
+                        val person = it.data
+                        if (person!=null) {
+                            person.woIdentityList
+                            mView?.currentPersonIdentities(person.woIdentityList)
+                        }
+                    }
+                    onError { e, isNetworkError ->
+                        XLog.error("查询身份错误, netErr: $isNetworkError", e)
+                    }
+                }
+    }
+
+    override fun newDocument(doc: CMSDocumentInfoJson) {
+        val json = O2SDKManager.instance().gson.toJson(doc)
+        val body = RequestBody.create(MediaType.parse("text/json"), json)
+        getCMSAssembleControlService(mView?.getContext())
+                ?.documentPost(body)
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.o2Subscribe {
+                    onNext {
+                        val id = it.data
+                        if (id!=null) {
+                            mView?.newDocumentId(id.id)
+                        }else {
+                            mView?.newDocumentFail()
+                        }
+                    }
+                    onError { e, isNetworkError ->
+                        XLog.error("保存文档错误, netErr: $isNetworkError", e)
+                        mView?.newDocumentFail()
+                    }
+                }
+    }
+
+}

+ 1 - 1
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/index/CMSIndexActivity.kt

@@ -53,7 +53,7 @@ class CMSIndexActivity : BaseMVPActivity<CMSIndexContract.View, CMSIndexContract
         recycler_cms_main_content.adapter = adapter
         adapter.setOnItemClickListener { _, position ->
             val info = applicationList[position]
-            if (info.wrapOutCategoryList!=null && !info.wrapOutCategoryList.isEmpty()) {
+            if (info.wrapOutCategoryList.isNotEmpty()) {
                 gotoCmsApplication(info)
             }
         }

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

@@ -1,27 +1,49 @@
 package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.view
 
 
+import android.Manifest
+import android.app.Activity
 import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
 import android.net.http.SslError
 import android.os.Bundle
+import android.provider.MediaStore
 import android.text.TextUtils
 import android.view.Gravity
+import android.view.View
+import android.webkit.JavascriptInterface
 import android.webkit.SslErrorHandler
 import android.webkit.WebView
 import android.webkit.WebViewClient
 import kotlinx.android.synthetic.main.activity_cms_web_view_document.*
+import kotlinx.android.synthetic.main.activity_cms_web_view_document.bottom_operate_button_layout
+import kotlinx.android.synthetic.main.activity_cms_web_view_document.fl_bottom_operation_bar
+import kotlinx.android.synthetic.main.activity_work_web_view.*
+import net.muliba.fancyfilepickerlibrary.FilePicker
+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.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
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.CMSWorkControl
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.AttachmentItemVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.O2UploadImageData
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.*
-import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.go
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.permission.PermissionRequester
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.AttachPopupWindow
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.BottomSheetMenu
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.WebChromeClientWithProgressAndValueCallback
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
 import org.jetbrains.anko.doAsync
 import org.jetbrains.anko.uiThread
 import java.io.DataInputStream
@@ -31,7 +53,7 @@ import java.io.FileOutputStream
 import java.util.concurrent.Future
 
 
-class CMSWebViewActivity : BaseMVPActivity<CMSWebViewContract.View, CMSWebViewContract.Presenter>(), CMSWebViewContract.View, AttachPopupWindow.AttachListener {
+class CMSWebViewActivity : BaseMVPActivity<CMSWebViewContract.View, CMSWebViewContract.Presenter>(), CMSWebViewContract.View {
     override var mPresenter: CMSWebViewContract.Presenter = CMSWebViewPresenter()
 
     override fun layoutResId(): Int  = R.layout.activity_cms_web_view_document
@@ -47,15 +69,24 @@ class CMSWebViewActivity : BaseMVPActivity<CMSWebViewContract.View, CMSWebViewCo
             return bundle
         }
     }
+    private val UPLOAD_REQUEST_CODE = 10086
+    private val REPLACE_REQUEST_CODE = 10087
+    private val TAKE_FROM_PICTURES_CODE = 10088
+    private val TAKE_FROM_CAMERA_CODE = 10089
     private var docId = ""
     private var docTitle = ""
     private var url = ""
-    private val attachList = ArrayList<AttachmentItemVO>()
-    private val popupWindow: AttachPopupWindow by lazy { AttachPopupWindow(this, attachList) }
     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 downloadDocument: DownloadDocument by lazy { DownloadDocument(this) }
+    private val cameraImageUri: Uri by lazy { FileUtil.getUriFromFile(this, File(FileExtensionHelper.getCameraCacheFilePath())) }
+    //上传附件
+    private var site = ""
+    //replace 附件
+    private var attachmentId = ""
+    // 图片控制器
+    private var imageUploadData: O2UploadImageData? = null
 
     override fun afterSetContentView(savedInstanceState: Bundle?) {
         docId = intent.extras?.getString(CMS_VIEW_DOCUMENT_ID_KEY) ?: ""
@@ -72,14 +103,8 @@ class CMSWebViewActivity : BaseMVPActivity<CMSWebViewContract.View, CMSWebViewCo
 
         setupToolBar(docTitle, true)
 
-
-        fab_cms_document_attach.setOnClickListener {
-            popupWindow.animationStyle = R.style.dir_popupwindow_anim
-            popupWindow.showAtLocation(activity_cms_web_view_document, Gravity.BOTTOM, 0, 0)
-            ZoneUtil.lightOff(this@CMSWebViewActivity)
-        }
-
         //init webview
+        web_view_cms_document_content.addJavascriptInterface(this, "o2android")
         jsNotification.setupWebView(web_view_cms_document_content)
         jsUtil.setupWebView(web_view_cms_document_content)
         web_view_cms_document_content.addJavascriptInterface(jsNotification, JSInterfaceO2mNotification.JSInterfaceName)
@@ -103,102 +128,342 @@ class CMSWebViewActivity : BaseMVPActivity<CMSWebViewContract.View, CMSWebViewCo
         web_view_cms_document_content.webViewSetCookie(this, url)
         web_view_cms_document_content.loadUrl(url)
 
-        mPresenter.loadAttachList(docId)
     }
 
-    override fun loadAttachList(list: List<AttachmentItemVO>) {
-        if (list.isNotEmpty()) {
-            fab_cms_document_attach.visible()
-            attachList.clear()
-            attachList.addAll(list)
-            popupWindow.listener = this
-            popupWindow.setOnDismissListener {
-                ZoneUtil.lightOn(this@CMSWebViewActivity)
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        if(webChromeClient.onActivityResult(requestCode, resultCode, data)){
+            return
+        }
+        if(resultCode == Activity.RESULT_OK) {
+            when (requestCode) {
+                UPLOAD_REQUEST_CODE ->{
+                    val result = data?.getStringExtra(FilePicker.FANCY_FILE_PICKER_SINGLE_RESULT_KEY)
+                    if (!TextUtils.isEmpty(result)) {
+                        XLog.debug("uri path:$result")
+                        showLoadingDialog()
+                        //上传附件
+                        mPresenter.uploadAttachment(result!!, site, docId)
+                    } else {
+                        XLog.error("FilePicker 没有返回值!")
+                    }
+                }
+                REPLACE_REQUEST_CODE -> {
+                    val result = data?.getStringExtra(FilePicker.FANCY_FILE_PICKER_SINGLE_RESULT_KEY)
+                    if (!TextUtils.isEmpty(result)) {
+                        XLog.debug("uri path:$result")
+                        showLoadingDialog()
+                        //替换附件
+                        mPresenter.replaceAttachment(result!!, site, attachmentId, docId)
+                    } else {
+                        XLog.error("FilePicker 没有返回值!")
+                    }
+                }
+                TAKE_FROM_PICTURES_CODE -> {
+                    //选择照片
+                    data?.let {
+                        val result = it.extras.getString(PicturePicker.FANCY_PICTURE_PICKER_SINGLE_RESULT_KEY, "")
+                        if (!TextUtils.isEmpty(result)) {
+                            XLog.debug("照片 path:$result")
+                            uploadImage2FileStorageStart(result)
+                        }
+                    }
+                }
+                TAKE_FROM_CAMERA_CODE -> {
+                    //拍照
+                    XLog.debug("拍照//// ")
+                    uploadImage2FileStorageStart(FileExtensionHelper.getCameraCacheFilePath())
+                }
+            }
+        }
+    }
+
+    override fun finishLoading() {
+        hideLoadingDialog()
+    }
+
+    override fun uploadAttachmentSuccess(attachmentId: String, site: String) {
+        XLog.debug("uploadAttachmentResponse attachmentId:$attachmentId, site:$site")
+        hideLoadingDialog()
+        web_view_cms_document_content.evaluateJavascript("layout.appForm.uploadedAttachment(\"$site\", \"$attachmentId\")"){
+            value -> XLog.debug("uploadedAttachment, onReceiveValue value=$value")
+        }
+    }
+
+    override fun replaceAttachmentSuccess(attachmentId: String, site: String) {
+        XLog.debug("replaceAttachmentResponse attachmentId:$attachmentId, site:$site")
+        hideLoadingDialog()
+        web_view_cms_document_content.evaluateJavascript("layout.appForm.replacedAttachment(\"$site\", \"$attachmentId\")"){
+            value -> XLog.debug("replacedAttachment, onReceiveValue value=$value")
+        }
+    }
+
+    override fun downloadAttachmentSuccess(file: File) {
+        hideLoadingDialog()
+        if (file.exists()){
+            if (FileExtensionHelper.isImageFromFileExtension(file.extension)) {
+                go<LocalImageViewActivity>(LocalImageViewActivity.startBundle(file.absolutePath))
+            }else {
+                go<FileReaderActivity>(FileReaderActivity.startBundle(file.absolutePath))
+            }
+        }
+    }
+
+    override fun downloadAttachmentFail(message: String) {
+        finishLoading()
+        XToast.toastShort(this, message)
+    }
+
+    override fun upload2FileStorageFail(message: String) {
+        hideLoadingDialog()
+        XToast.toastShort(this, message)
+    }
+
+    override fun upload2FileStorageSuccess(id: String) {
+        hideLoadingDialog()
+        if (imageUploadData != null) {
+            imageUploadData!!.fileId = id
+            val callback = imageUploadData!!.callback
+            val json = O2SDKManager.instance().gson.toJson(imageUploadData)
+            val js = "$callback('$json')"
+            XLog.debug("执行js:$js")
+            web_view_cms_document_content.evaluateJavascript(js){
+                value -> XLog.debug("replacedAttachment, onReceiveValue value=$value")
             }
-            popupWindow.notifyStatusChanged()
         }else {
-            fab_cms_document_attach.gone()
+            XLog.error("图片控件对象不存在。。。。。。。。")
         }
     }
 
-    override fun getAttachStatus(id: String?): AttachPopupWindow.AttachStatus {
-        if (!TextUtils.isEmpty(id)) {
-            if (taskMap.containsKey(id!!)) {
-                return  AttachPopupWindow.AttachStatus.DOWNLOADING
+    //MARK: 操作按钮
+    /**
+     * 发布文档
+     */
+    fun publishDocument(view: View?) {
+        web_view_cms_document_content.evaluateJavascript("layout.appForm.publishDocument()") { value ->
+            XLog.info("发布文档返回:$value")
+        }
+    }
+
+    /**
+     * 删除文档
+     */
+    fun deleteDocument(view: View?) {
+        O2DialogSupport.openConfirmDialog(this, "你确定要删除当前文档?", listener = {
+            web_view_cms_document_content.evaluateJavascript("layout.appForm.deleteDocumentForMobile()") { value ->
+                XLog.info("删除文档返回:$value")
             }
-            val file = File(getAttachFileLocalPath(id))
-            if (file.exists()) {
-                return AttachPopupWindow.AttachStatus.DOWNLOADCOMPLETED
+        })
+    }
+
+
+    //MARK: javascript interface
+
+    /**
+     * 关闭当前窗口
+     */
+    @JavascriptInterface
+    fun closeDocumentWindow(result: String) {
+        finish()
+    }
+
+    /**
+     * 表单加载完成后回调
+     */
+    @JavascriptInterface
+    fun cmsFormLoaded(control: String) {
+        //{
+        //                        "allowRead": true,
+        //                        "allowPublishDocument": isControl && this.document.docStatus === "draft",
+        //                        "allowSave": isControl && this.document.docStatus === "published",
+        //                        "allowPopularDocument": false,
+        //                        "allowEditDocument":  isControl && !this.document.wf_workId,
+        //                        "allowDeleteDocument":  isControl && !this.document.wf_workId,
+        //                        "allowArchiveDocument" : false,
+        //                        "allowRedraftDocument" : false
+        //                    };
+        XLog.debug("表单加载完成回调:$control")
+        if (!TextUtils.isEmpty(control)) {
+            try {
+                val cmsWorkControl = O2SDKManager.instance().gson.fromJson(control, CMSWorkControl::class.java)
+                runOnUiThread {
+                    var i = 0
+                    if (cmsWorkControl.allowDeleteDocument) {
+                        tv_cms_form_delete_btn.visible()
+                        i++
+                    }
+                    if (cmsWorkControl.allowPublishDocument) {
+                        tv_cms_form_publish_btn.visible()
+                        i++
+                    }
+                    if (i>0) {
+                        fl_bottom_operation_bar.visible()
+                        bottom_operate_button_layout.visible()
+                    }
+                }
+            } catch (e: Exception) {
+                XLog.error("json parse error", e)
+            }
+        }
+
+    }
+
+    /**
+     * 上传附件
+     *
+     * @param site
+     */
+    @JavascriptInterface
+    fun uploadAttachment(site: String) {
+        XLog.debug("upload site:$site")
+        if (TextUtils.isEmpty(site)) {
+            XLog.error("没有传入site")
+            return
+        }
+        this.site = site
+        runOnUiThread {
+            openFancyFilePicker(UPLOAD_REQUEST_CODE)
+        }
+    }
+
+    /**
+     * 替换附件
+     *
+     * @param attachmentId
+     * @param site
+     */
+    @JavascriptInterface
+    fun replaceAttachment(attachmentId: String, site: String) {
+        XLog.debug("replace site:$site, attachmentId:$attachmentId")
+        if (TextUtils.isEmpty(attachmentId) || TextUtils.isEmpty(site)) {
+            XLog.error("没有传入attachmentId 或 site")
+            return
+        }
+        this.site = site
+        this.attachmentId = attachmentId
+        runOnUiThread {
+            openFancyFilePicker(REPLACE_REQUEST_CODE)
+        }
+    }
+
+    /**
+     * 下载附件
+     *
+     * @param attachmentId
+     */
+    @JavascriptInterface
+    fun downloadAttachment(attachmentId: String) {
+        XLog.debug("download attachmentId:$attachmentId")
+        if (TextUtils.isEmpty(attachmentId)) {
+            XLog.error("调用失败,附件id没有传入!")
+            return
+        }
+        runOnUiThread {
+            showLoadingDialog()
+        }
+
+        //下载附件
+        mPresenter.downloadAttachment(attachmentId, docId)
+    }
+
+    /**
+     * 打开文档 公文打开 office pdf 等
+     */
+    @JavascriptInterface
+    fun openDocument(url: String) {
+        XLog.debug("打开文档。。。。。文档地址:$url")
+        runOnUiThread {
+            showLoadingDialog()
+        }
+        //打开文档
+        downloadDocument.downloadDocumentAndOpenIt(url) {
+            hideLoadingDialog()
+        }
+    }
+    /**
+     * 图片控件
+     */
+    @JavascriptInterface
+    fun uploadImage2FileStorage(json: String?) {
+        imageUploadData = null
+        XLog.debug("打开图片上传控件, $json")
+        runOnUiThread {
+            if (json != null) {
+                imageUploadData = O2SDKManager.instance().gson.fromJson(json, O2UploadImageData::class.java)
+                showPictureChooseMenu()
+
             }else {
-                return AttachPopupWindow.AttachStatus.ONCLOUD
+                XToast.toastShort(this, "没有传入对象")
             }
-        }else{
-            return AttachPopupWindow.AttachStatus.ONCLOUD
-        }
-    }
-
-
-
-    override fun openCompletedFile(id: String?) {
-        if (!TextUtils.isEmpty(id)) {
-            val file = File(getAttachFileLocalPath(id!!))
-            if (file != null && file.exists()) AndroidUtils.openFileWithDefaultApp(this, file)
-        }
-    }
-
-    override fun startDownLoadFile(id: String?) {
-        if (!TextUtils.isEmpty(id)){
-            taskMap.put(id!!, doAsync {
-                val filePath = getAttachFileLocalPath(id)
-                val file = File(filePath)
-                var downloadSuccess = false
-                try {
-                    if (!file.exists()) {
-                        val call = RetrofitClient.instance().cmsAssembleControlService().downloadAttach(id)
-                        val response = call.execute()
-                        val input = DataInputStream(response.body()?.byteStream())
-                        val output = DataOutputStream(FileOutputStream(file))
-                        val buffer = ByteArray(4096)
-                        var count = 0
-                        do {
-                            count = input.read(buffer)
-                            if (count > 0) {
-                                output.write(buffer, 0, count)
-                            }
-                        } while (count > 0)
-                        output.close()
-                        input.close()
-                    }
-                    downloadSuccess = true
-                }catch (e: Exception){
-                    XLog.error("下载附件异常", e)
+        }
+    }
+
+
+    //MARK: private method
+
+
+
+    private fun openFancyFilePicker(requestCode: Int) {
+        FilePicker().withActivity(this).requestCode(requestCode)
+                .chooseType(FilePicker.CHOOSE_TYPE_SINGLE)
+                .start()
+    }
+
+    private fun showPictureChooseMenu() {
+        BottomSheetMenu(this)
+                .setTitle("上传照片")
+                .setItem("从相册选择", resources.getColor(R.color.z_color_text_primary)) {
+                    takeFromPictures()
                 }
-                uiThread {
-                    if (taskMap.containsKey(id)){
-                        taskMap.remove(id)
-                    }
-                    if (downloadSuccess) {
-                        popupWindow.notifyStatusChanged()
-                    }else{
-                        if (file.exists()){
-                            file.delete()
+                .setItem("拍照", resources.getColor(R.color.z_color_text_primary)) {
+                    takeFromCamera()
+                }
+                .setCancelButton("取消", resources.getColor(R.color.z_color_text_hint)) {
+                    XLog.debug("取消。。。。。")
+                }
+                .show()
+    }
+
+    private fun takeFromPictures() {
+        PicturePicker()
+                .withActivity(this)
+                .chooseType(PicturePicker.CHOOSE_TYPE_SINGLE)
+                .requestCode(TAKE_FROM_PICTURES_CODE)
+                .start()
+    }
+
+    private fun takeFromCamera() {
+        PermissionRequester(this).request(Manifest.permission.CAMERA)
+                .o2Subscribe {
+                    onNext { (granted, shouldShowRequestPermissionRationale, deniedPermissions) ->
+                        XLog.info("granted:$granted , shouldShowRequest:$shouldShowRequestPermissionRationale, denied:$deniedPermissions")
+                        if (!granted) {
+                            O2DialogSupport.openAlertDialog(this@CMSWebViewActivity, "非常抱歉,相机权限没有开启,无法使用相机!")
+                        } else {
+                            openCamera()
                         }
-                        XToast.toastShort(this@CMSWebViewActivity, "下载附件失败!")
                     }
                 }
-            })
-        }
     }
 
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        super.onActivityResult(requestCode, resultCode, data)
-        webChromeClient.onActivityResult(requestCode, resultCode, data)
+    private fun openCamera() {
+        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+        //return-data false 不是直接返回拍照后的照片Bitmap 因为照片太大会传输失败
+        intent.putExtra("return-data", false)
+        //改用Uri 传递
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraImageUri)
+        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
+        intent.putExtra("noFaceDetection", true)
+        startActivityForResult(intent, TAKE_FROM_CAMERA_CODE)
     }
 
-    private val taskMap = HashMap<String, Future<Unit>>()
-    private fun getAttachFileLocalPath(id: String): String {
-        var path  = ""
-        attachList.asSequence().filter { it.id == id }.map { path = FileExtensionHelper.getXBPMCMSAttachFolder()+File.separator+it.fileName }.toList()
-        return path
+    private fun uploadImage2FileStorageStart(filePath: String) {
+        showLoadingDialog()
+        if (imageUploadData != null) {
+            mPresenter.upload2FileStorage(filePath, imageUploadData!!.referencetype, imageUploadData!!.reference)
+        }else {
+            finishLoading()
+            XToast.toastShort(this, "上传文件参数为空!!!")
+        }
     }
 }

+ 12 - 2
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/view/CMSWebViewContract.kt

@@ -3,14 +3,24 @@ package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.view
 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.AttachmentItemVO
+import java.io.File
 
 
 object CMSWebViewContract {
     interface View : BaseView {
-        fun loadAttachList(list:List<AttachmentItemVO>)
+        fun finishLoading()
+        fun uploadAttachmentSuccess(attachmentId:String, site:String)
+        fun replaceAttachmentSuccess(attachmentId:String, site:String)
+        fun downloadAttachmentSuccess(file: File)
+        fun downloadAttachmentFail(message: String)
+        fun upload2FileStorageFail(message: String)
+        fun upload2FileStorageSuccess(id: String)
     }
 
     interface Presenter : BasePresenter<View> {
-        fun loadAttachList(docId: String)
+        fun uploadAttachment(attachmentFilePath: String, site: String, docId: String)
+        fun replaceAttachment(attachmentFilePath: String, site: String, attachmentId: String, docId: String)
+        fun downloadAttachment(attachmentId: String, filePath: String)
+        fun upload2FileStorage(filePath: String, referenceType: String , reference: String , scale: Int = 500)
     }
 }

+ 167 - 10
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/cms/view/CMSWebViewPresenter.kt

@@ -1,28 +1,185 @@
 package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.view
 
+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.ResponseHandler
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.IdData
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.cms.CMSDocumentAttachmentJson
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.AttachmentItemVO
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.FileExtensionHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.FileUtil
 import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import okhttp3.MediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import rx.Observable
+import rx.Subscriber
 import rx.android.schedulers.AndroidSchedulers
+import rx.functions.Action1
 import rx.schedulers.Schedulers
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.File
+import java.io.FileOutputStream
 
 class CMSWebViewPresenter : BasePresenterImpl<CMSWebViewContract.View>(), CMSWebViewContract.Presenter {
 
-    override fun loadAttachList(docId: String) {
-        getCMSAssembleControlService(mView?.getContext())?.let { service->
-            service.getDocumentAttachList(docId)
+
+    override fun uploadAttachment(attachmentFilePath: String, site: String, docId: String) {
+        if (TextUtils.isEmpty(attachmentFilePath) || TextUtils.isEmpty(site) || TextUtils.isEmpty(docId)) {
+            XLog.error("arguments is null  workid:$docId, site:$site, attachmentFilePath:$attachmentFilePath")
+            mView?.finishLoading()
+            return
+        }
+        val file = File(attachmentFilePath)
+        val requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file)
+        val body = MultipartBody.Part.createFormData("file", file.name, requestBody)
+        val siteBody = RequestBody.create(MediaType.parse("text/plain"), site)
+        getCMSAssembleControlService(mView?.getContext())
+                ?.uploadAttachment(body, siteBody, docId)
+                ?.subscribeOn(Schedulers.io())
+                    ?.observeOn(AndroidSchedulers.mainThread())
+                    ?.subscribe(ResponseHandler { id -> mView?.uploadAttachmentSuccess(id.id, site) },
+                            ExceptionHandler(mView?.getContext()) { e ->
+                                XLog.error("$e")
+                                mView?.finishLoading() })
+
+    }
+
+    override fun replaceAttachment(attachmentFilePath: String, site: String, attachmentId: String, docId: String) {
+        if (TextUtils.isEmpty(attachmentFilePath) || TextUtils.isEmpty(site) || TextUtils.isEmpty(attachmentId) || TextUtils.isEmpty(docId)) {
+            XLog.error("arguments is null att:$attachmentId, workid:$docId, site:$site, attachmentFilePath:$attachmentFilePath")
+            mView?.finishLoading()
+            return
+        }
+        val file = File(attachmentFilePath)
+        val requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file)
+        val body = MultipartBody.Part.createFormData("file", file.name, requestBody)
+
+        getCMSAssembleControlService(mView?.getContext())
+                ?.replaceAttachment(body, attachmentId, docId)
+                    ?.subscribeOn(Schedulers.io())
+                    ?.flatMap { response ->
+                        Observable.create(Observable.OnSubscribe<String> { t ->
+                            try {
+                                val idData: IdData? = response.data
+                                if (idData == null || TextUtils.isEmpty(idData.id)) {
+                                    t?.onError(Exception("没有返回附件id"))
+                                } else {
+                                    val parentFolder = FileExtensionHelper.getXBPMCMSAttachFolder()
+                                    val folder = File(parentFolder)
+                                    if (folder.exists()) {
+                                        folder.listFiles().filter { (it != null && it.exists() && it.isFile) }.map(File::delete)
+                                    }
+                                    t?.onNext(idData.id)
+                                }
+                            } catch (e: Exception) {
+                                t?.onError(e)
+                            }
+                            t?.onCompleted()
+                        })
+                    }?.observeOn(AndroidSchedulers.mainThread())
+                    ?.subscribe(Action1<String> { id -> mView?.replaceAttachmentSuccess(id, site) },
+                            ExceptionHandler(mView?.getContext()) { e ->
+                                XLog.error("", e)
+                                mView?.finishLoading() })
+
+    }
+
+    override fun downloadAttachment(attachmentId: String, documentId: String) {
+        if (TextUtils.isEmpty(attachmentId) || TextUtils.isEmpty(documentId)) {
+            XLog.error("arguments is null att:$attachmentId, documentId:$documentId")
+            mView?.finishLoading()
+            return
+        }
+        val cmsService = getCMSAssembleControlService(mView?.getContext())
+        if (cmsService != null) {
+            cmsService.getDocumentAttachment(attachmentId, documentId).subscribeOn(Schedulers.io())
+                    .flatMap { res->
+                        val attachInfo = res.data
+                        if (attachInfo != null) {
+                            val filePath = FileExtensionHelper.getXBPMCMSAttachFolder() + File.separator + attachInfo.name
+                            val file = File(filePath)
+                            try {
+                                if (!file.exists()) {
+                                    val call = cmsService.downloadAttach(attachmentId)
+                                    val response = call.execute()
+                                    val input = DataInputStream(response.body()?.byteStream())
+                                    val output = DataOutputStream(FileOutputStream(file))
+                                    val buffer = ByteArray(4096)
+                                    var count = 0
+                                    do {
+                                        count = input.read(buffer)
+                                        if (count > 0) {
+                                            output.write(buffer, 0, count)
+                                        }
+                                    } while (count > 0)
+                                    output.close()
+                                    input.close()
+                                }
+                            } catch (e: Exception) {
+                                XLog.error("下载附件异常", e)
+                                if (file.exists()) {
+                                    file.delete()
+                                }
+                            }
+                            Observable.create { t ->
+                                val thisfile = File(filePath)
+                                if (file.exists()) {
+                                    t?.onNext(thisfile)
+                                } else {
+                                    t?.onError(Exception("附件下载异常,找不到文件!"))
+                                }
+                                t?.onCompleted()
+                            }
+                        }else {
+                            Observable.create(object : Observable.OnSubscribe<File> {
+                                override fun call(t: Subscriber<in File>?) {
+                                    t?.onError(Exception("没有获取到附件信息,无法下载附件!"))
+                                    t?.onCompleted()
+                                }
+                            })
+                        }
+                    }.observeOn(AndroidSchedulers.mainThread())
+                    .subscribe({ file -> mView?.downloadAttachmentSuccess(file) }, { e ->
+                        mView?.downloadAttachmentFail( "下载附件失败,${e.message}")
+                    })
+        }else {
+            XLog.error("cms模块接入异常!")
+            mView?.downloadAttachmentFail("cms模块接入异常!")
+        }
+
+    }
+
+    override fun upload2FileStorage(filePath: String, referenceType: String, reference: String, scale: Int) {
+        XLog.debug("上传图片,filePath:$filePath, referenceType:$referenceType, reference:$reference, scale:$scale")
+        if (filePath.isEmpty() || reference.isEmpty() || referenceType.isEmpty()) {
+            mView?.upload2FileStorageFail("传入参数不正确!")
+            return
+        }
+        val file = File(filePath)
+        if (!file.exists()) {
+            mView?.upload2FileStorageFail("文件不存在!!!")
+            return
+        }
+        val fileService = getFileAssembleControlService(mView?.getContext())
+        if (fileService!=null) {
+            val mediaType = FileUtil.getMIMEType(file)
+            val requestBody = RequestBody.create(MediaType.parse(mediaType), file)
+            val body = MultipartBody.Part.createFormData("file", file.name, requestBody)
+            fileService.uploadFile2ReferenceZone(body, referenceType, reference, scale)
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(ResponseHandler<List<CMSDocumentAttachmentJson>> { list ->
-                        val attachList = ArrayList<AttachmentItemVO>()
-                        list.map { attachList.add(it.copyToVO()) }
-                        mView?.loadAttachList(attachList)
-                    }, ExceptionHandler(mView?.getContext()) { e ->
-                        XLog.error("", e)
-                    })
+                    .subscribe(ResponseHandler<IdData> {
+                        id -> mView?.upload2FileStorageSuccess(id.id)
+                    },
+                            ExceptionHandler(mView?.getContext()) { e ->
+                                XLog.error("$e")
+                                mView?.upload2FileStorageFail("文件上传异常") })
+        }else {
+            mView?.upload2FileStorageFail("文件模块接入异常!")
         }
     }
 }

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

@@ -36,6 +36,9 @@ class IndexPresenter : BasePresenterImpl<IndexContract.View>(), IndexContract.Pr
 
     override fun loadNewsList(lastId: String) {
         val wrapIn = HashMap<String, List<String>>()
+        val status = ArrayList<String>()
+        status.add("published")
+        wrapIn["statusList"] = status
         val json = O2SDKManager.instance().gson.toJson(wrapIn)
         val body = RequestBody.create(MediaType.parse("text/json"), json)
         getCMSAssembleControlService(mView?.getContext())?.let { service ->

+ 15 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/model/bo/CMSWorkControl.kt

@@ -0,0 +1,15 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo
+
+/**
+ * Created by fancyLou on 2019-07-04.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+class CMSWorkControl(
+        var allowPublishDocument: Boolean = false,
+        var allowSave: Boolean = false,
+        var allowEditDocument: Boolean = false,
+        var allowDeleteDocument: Boolean = false,
+        var allowArchiveDocument: Boolean = false,
+        var allowRedraftDocument: Boolean = false
+)

+ 12 - 0
o2android/app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/model/vo/CmsFilter.kt

@@ -0,0 +1,12 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo
+
+/**
+ * Created by fancyLou on 2019-07-03.
+ * Copyright © 2019 O2. All rights reserved.
+ */
+
+class CmsFilter(
+        var categoryIdList: List<String> = ArrayList(),
+        var creatorList:  List<String> = ArrayList(),
+        var documentType: String = "全部"
+)

+ 1 - 2
o2android/app/src/main/res/layout/activity_cms_main.xml

@@ -14,8 +14,7 @@
     <android.support.v7.widget.RecyclerView
         android:id="@+id/recycler_cms_main_content"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        ></android.support.v7.widget.RecyclerView>
+        android:layout_height="match_parent" />
 
     <TextView
         android:id="@+id/tv_cms_main_empty"

+ 129 - 0
o2android/app/src/main/res/layout/activity_cms_publish_document.xml

@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout 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"
+    android:background="@color/z_color_background"
+    android:orientation="vertical"
+    tools:context=".app.cms.application.CMSPublishDocumentActivity">
+
+    <include layout="@layout/snippet_appbarlayout_toolbar"/>
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:fillViewport="true">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+
+                <!--<LinearLayout-->
+                    <!--android:layout_width="match_parent"-->
+                    <!--android:layout_height="wrap_content"-->
+                    <!--android:background="@android:color/white"-->
+                    <!--android:orientation="vertical">-->
+
+                    <!--<TextView-->
+                        <!--android:id="@+id/tv_cms_publish_header"-->
+                        <!--android:layout_width="wrap_content"-->
+                        <!--android:layout_height="wrap_content"-->
+                        <!--android:layout_gravity="center|center_horizontal"-->
+                        <!--android:layout_marginLeft="@dimen/spacing_normal"-->
+                        <!--android:layout_marginTop="18dp"-->
+                        <!--android:layout_marginRight="@dimen/spacing_normal"-->
+                        <!--android:layout_marginBottom="18dp"-->
+                        <!--android:textColor="@color/z_color_text_primary_dark"-->
+                        <!--android:textSize="17sp"-->
+                        <!--tools:text="新建文档 - test" />-->
+
+                    <!--<View-->
+                        <!--android:layout_width="match_parent"-->
+                        <!--android:layout_height="0.5dp"-->
+                        <!--android:background="@color/z_color_split_line_ddd" />-->
+                <!--</LinearLayout>-->
+
+                <View
+                    android:layout_width="match_parent"
+                    android:layout_height="0.5dp"
+                    android:layout_marginTop="@dimen/spacing_small"
+                    android:background="@color/z_color_split_line_ddd" />
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:background="@android:color/white"
+                    android:orientation="vertical"
+                    android:paddingLeft="@dimen/spacing_normal"
+                    android:paddingRight="@dimen/spacing_normal">
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="@dimen/spacing_small"
+                        android:layout_marginBottom="@dimen/spacing_small"
+                        android:orientation="horizontal">
+
+                        <TextView
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1"
+                            android:layout_gravity="center_vertical"
+                            android:text="@string/cms_start_create_identity"
+                            android:textColor="@color/z_color_text_primary_dark"
+                            android:textSize="15sp" />
+
+                        <RadioGroup
+                            android:id="@+id/radio_group_cms_publish_identity"
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="2" />
+
+                    </LinearLayout>
+
+                    <View
+                        android:layout_width="match_parent"
+                        android:layout_height="0.5dp"
+                        android:background="@color/z_color_split_line_ddd" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="@dimen/spacing_small"
+                        android:layout_marginBottom="@dimen/spacing_small"
+                        android:orientation="horizontal">
+
+                        <TextView
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1"
+                            android:text="@string/cms_start_create_title"
+                            android:textColor="@color/z_color_text_primary_dark"
+                            android:textSize="15sp" />
+
+                        <EditText
+                            android:id="@+id/edit_cms_publish_title"
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="2"
+                            android:background="@null"
+                            android:hint="@string/cms_start_create_title_hint"
+                            android:imeOptions="actionDone"
+                            android:inputType="text"
+                            android:singleLine="true"
+                            android:textColor="@color/z_color_text_primary"
+                            android:textColorHint="@color/z_color_text_hint"
+                            android:textSize="14sp" />
+                    </LinearLayout>
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+    </ScrollView>
+</LinearLayout>

+ 61 - 18
o2android/app/src/main/res/layout/activity_cms_web_view_document.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<android.support.design.widget.CoordinatorLayout
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -7,26 +7,69 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/z_color_background"
+    android:orientation="vertical"
     tools:context="net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.cms.view.CMSWebViewActivity">
 
     <include layout="@layout/snippet_appbarlayout_toolbar"/>
-    <net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.NestedProgressWebView
-        android:id="@+id/web_view_cms_document_content"
+    <android.support.constraint.ConstraintLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="@android:color/white"
-        app:layout_behavior="@string/appbar_scrolling_view_behavior">
-    </net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.NestedProgressWebView>
+        android:layout_height="0dp"
+        android:layout_weight="1">
+        <net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.NestedProgressWebView
+            android:id="@+id/web_view_cms_document_content"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:background="@android:color/white"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@+id/fl_bottom_operation_bar"
+            app:layout_behavior="@string/appbar_scrolling_view_behavior">
+        </net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.NestedProgressWebView>
+        <FrameLayout
+            android:id="@+id/fl_bottom_operation_bar"
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            app:layout_constraintTop_toBottomOf="@+id/web_view_cms_document_content"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            android:visibility="gone">
+            <LinearLayout
+                android:id="@+id/bottom_operate_button_layout"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@android:color/white"
+                android:orientation="horizontal"
+                android:padding="@dimen/spacing_small"
+                android:visibility="gone">
 
+                <TextView
+                    android:id="@+id/tv_cms_form_delete_btn"
+                    android:layout_height="match_parent"
+                    android:layout_width="0dp"
+                    android:layout_weight="1"
+                    android:text="@string/work_form_delete"
+                    android:textColor="@color/z_color_primary"
+                    android:textSize="@dimen/font_large"
+                    android:gravity="center"
+                    android:visibility="gone"
+                    android:onClick="deleteDocument"
+                    />
+                <TextView
+                    android:id="@+id/tv_cms_form_publish_btn"
+                    android:layout_height="match_parent"
+                    android:layout_width="0dp"
+                    android:layout_weight="1"
+                    android:text="@string/cms_work_form_publish"
+                    android:textColor="@color/z_color_primary"
+                    android:textSize="@dimen/font_large"
+                    android:gravity="center"
+                    android:visibility="gone"
+                    android:onClick="publishDocument"
+                    />
 
-    <android.support.design.widget.FloatingActionButton
-        android:id="@+id/fab_cms_document_attach"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom|end"
-        android:layout_margin="@dimen/fab_margin"
-        android:src="@mipmap/icon_attach_white_24dp"
-        app:backgroundTint="@color/z_color_primary"
-        android:visibility="gone"/>
-
-</android.support.design.widget.CoordinatorLayout>
+            </LinearLayout>
+        </FrameLayout>
+    </android.support.constraint.ConstraintLayout>
+</LinearLayout>

+ 2 - 1
o2android/app/src/main/res/layout/activity_local_image_view.xml

@@ -6,11 +6,11 @@
     android:layout_height="match_parent"
     android:orientation="vertical"
     android:background="@color/z_color_text_primary_dark"
+    android:paddingBottom="@dimen/spacing_small"
     tools:context=".app.o2.webview.LocalImageViewActivity">
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="48dp"
-        android:layout_marginTop="@dimen/toolbar_padding_top"
         android:padding="@dimen/spacing_small">
         <ImageView
             android:id="@+id/image_local_view_back_btn"
@@ -25,6 +25,7 @@
             android:layout_height="wrap_content"
             android:layout_centerInParent="true"
             android:layout_toEndOf="@id/image_local_view_back_btn"
+            android:layout_marginStart="@dimen/spacing_small"
             android:textColor="@android:color/white"
             android:textSize="@dimen/font_normal"
             android:ellipsize="marquee"

+ 2 - 2
o2android/app/src/main/res/layout/item_cms_main_application.xml

@@ -5,8 +5,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="?android:attr/selectableItemBackground"
-    android:paddingBottom="@dimen/spacing_small"
-    android:paddingTop="@dimen/spacing_small">
+    android:padding="@dimen/spacing_small">
 
     <ImageView
         android:id="@+id/image_cms_main_application_icon"
@@ -19,6 +18,7 @@
         android:id="@+id/tv_cms_main_application_name"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:gravity="center"
         android:layout_centerHorizontal="true"
         android:layout_marginTop="@dimen/spacing_small"
         android:layout_below="@id/image_cms_main_application_icon"

+ 12 - 0
o2android/app/src/main/res/menu/menu_cms_create.xml

@@ -0,0 +1,12 @@
+<menu 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"
+    tools:context=".app.cms.application.CMSApplicationActivity">
+
+    <item android:id="@+id/menu_cms_create"
+        app:showAsAction="always"
+        android:orderInCategory="90"
+        android:title="@string/cms_create"/>
+
+
+</menu>

+ 5 - 0
o2android/app/src/main/res/values/strings.xml

@@ -622,8 +622,13 @@
 
     <!-- cms -->
     <string name="cms">内容管理</string>
+    <string name="cms_create">新建</string>
     <string name="title_cms_view">查看文章</string>
     <string name="title_cms_application">栏目</string>
+    <string name="cms_start_create_title">文档标题:</string>
+    <string name="cms_start_create_title_hint">请输入文档标题</string>
+    <string name="cms_start_create_identity">身份:</string>
+    <string name="cms_work_form_publish">发    布</string>
 
 
     <!-- open im key -->

+ 8 - 0
o2android/app/src/main/res/values/styles.xml

@@ -14,6 +14,14 @@
         <item name="android:windowBackground">@mipmap/launch_background</item>
     </style>
 
+    <style name="XBPMTheme.fullscreen" parent="Theme.AppCompat.NoActionBar">
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowBackground">@color/z_color_mask</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
+    </style>
+
     <!-- 清除堆栈之后启动Activity, windowDisablePreview 禁用预览  -->
     <style name="XBPMClearActivityTheme" parent="XBPMTheme">
         <item name="windowActionBar">false</item>

+ 2 - 2
o2android/gradle.properties

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

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

@@ -154,6 +154,7 @@
 		B1BC8CDA216B3D5F00AF571F /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B1BC8CD9216B3D5E00AF571F /* libc++.tbd */; };
 		B1C19025211437E200935829 /* OOCalendarLeftMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C19024211437E200935829 /* OOCalendarLeftMenuController.swift */; };
 		B1C1905D2114410D00935829 /* CalendarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C1905C2114410D00935829 /* CalendarTableViewCell.swift */; };
+		B1C303E622D58C1A00C17E5A /* CMSAttachmentInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C303E522D58C1A00C17E5A /* CMSAttachmentInfoResponse.swift */; };
 		B1DA305F2282754500669418 /* QCalendarPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DA305E2282754500669418 /* QCalendarPicker.swift */; };
 		B1EE2CCE2281729400842F48 /* QDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE2CCD2281729400842F48 /* QDatePicker.swift */; };
 		B1EE2CD02281771600842F48 /* O2JsApiUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE2CCF2281771600842F48 /* O2JsApiUtil.swift */; };
@@ -161,6 +162,8 @@
 		B1FAFD442105C23B008A0CDF /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FAFD432105C23B008A0CDF /* Operators.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 */; };
+		B1FEDACE22D314F90002ECF4 /* CMSCreateDocViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FEDACD22D314F90002ECF4 /* CMSCreateDocViewController.swift */; };
 		B1FFF886223A23B200C170FD /* O2FlutterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FFF885223A23B200C170FD /* O2FlutterViewController.swift */; };
 		B1FFF892223A4DA700C170FD /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = B1FFF890223A4DA700C170FD /* GeneratedPluginRegistrant.m */; };
 		E40502C520722208009A8D30 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40502C220722208009A8D30 /* ImagePickerController.swift */; };
@@ -1468,6 +1471,7 @@
 		B1BC8CD9216B3D5E00AF571F /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
 		B1C19024211437E200935829 /* OOCalendarLeftMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOCalendarLeftMenuController.swift; sourceTree = "<group>"; };
 		B1C1905C2114410D00935829 /* CalendarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarTableViewCell.swift; sourceTree = "<group>"; };
+		B1C303E522D58C1A00C17E5A /* CMSAttachmentInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMSAttachmentInfoResponse.swift; sourceTree = "<group>"; };
 		B1DA305E2282754500669418 /* QCalendarPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QCalendarPicker.swift; sourceTree = "<group>"; };
 		B1EE2CCD2281729400842F48 /* QDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QDatePicker.swift; sourceTree = "<group>"; };
 		B1EE2CCF2281771600842F48 /* O2JsApiUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = O2JsApiUtil.swift; sourceTree = "<group>"; };
@@ -1475,6 +1479,8 @@
 		B1FAFD432105C23B008A0CDF /* Operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operators.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>"; };
+		B1FEDACD22D314F90002ECF4 /* CMSCreateDocViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMSCreateDocViewController.swift; sourceTree = "<group>"; };
 		B1FFF885223A23B200C170FD /* O2FlutterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = O2FlutterViewController.swift; sourceTree = "<group>"; };
 		B1FFF890223A4DA700C170FD /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
 		B1FFF891223A4DA700C170FD /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
@@ -3435,6 +3441,7 @@
 				E4B781721DF912E6007B58A9 /* CMSCategoryListViewController.swift */,
 				E4B7819F1DFA9B23007B58A9 /* CMSItemDetailViewController.swift */,
 				E4CB278A1E797B58004A7ACB /* CMSQLViewController.swift */,
+				B1FEDACD22D314F90002ECF4 /* CMSCreateDocViewController.swift */,
 			);
 			path = c;
 			sourceTree = "<group>";
@@ -3452,6 +3459,8 @@
 				E4B781691DF8F2B2007B58A9 /* CMSData.swift */,
 				E4B7816A1DF8F2B2007B58A9 /* CMSWrapOutCategoryList.swift */,
 				B13707DC212D3F19002113F1 /* CMSCategoryData.swift */,
+				B1FEDA9A22D305C90002ECF4 /* CMSSingleApplication.swift */,
+				B1C303E522D58C1A00C17E5A /* CMSAttachmentInfoResponse.swift */,
 			);
 			path = m;
 			sourceTree = "<group>";
@@ -5871,6 +5880,7 @@
 				B130A74F2281559900282AD1 /* DeviceListViewController.swift in Sources */,
 				E4B888631D9D48F1002E1A46 /* ZLNavigationController.swift in Sources */,
 				E4B889021D9D48F1002E1A46 /* TodoedStatusCell.swift in Sources */,
+				B1C303E622D58C1A00C17E5A /* CMSAttachmentInfoResponse.swift in Sources */,
 				E4740AC31DCC4C3B00686780 /* BBSSubjectListViewController.swift in Sources */,
 				E4B888AD1D9D48F1002E1A46 /* UIView+MJExtension.m in Sources */,
 				E4C24BFB20844F4400E426B0 /* JCSearchController.swift in Sources */,
@@ -6166,6 +6176,7 @@
 				E4C24BBC20844F3C00E426B0 /* JCEmoticonInputView.swift in Sources */,
 				E40E24D920B7DA3C009F8BE7 /* OOMeetingInforHeaderView.swift in Sources */,
 				E4B8886E1D9D48F1002E1A46 /* ContactGroupPersonController.swift in Sources */,
+				B1FEDA9B22D305C90002ECF4 /* CMSSingleApplication.swift in Sources */,
 				E4B888F31D9D48F1002E1A46 /* TodoTaskDetailViewController.swift in Sources */,
 				E4C24B6020844F3C00E426B0 /* JCAreaPickerView.swift in Sources */,
 				E43C24361DB8AE72005082A4 /* ZLBaseTableView.swift in Sources */,
@@ -6426,6 +6437,7 @@
 				E4B697092075FA560062F6E8 /* BaseModel.swift in Sources */,
 				E4B888621D9D48F1002E1A46 /* ZLCollectionView.swift in Sources */,
 				E40E24B220B7DA3C009F8BE7 /* OOMeetingCreateViewController.swift in Sources */,
+				B1FEDACE22D314F90002ECF4 /* CMSCreateDocViewController.swift in Sources */,
 				E4C24BAA20844F3C00E426B0 /* JCFileDownloadViewController.swift in Sources */,
 				E4C24B7220844F3C00E426B0 /* JCCEmoticonLarge.swift in Sources */,
 				E4C24BA420844F3C00E426B0 /* JCSingleSettingViewController.swift in Sources */,

+ 2 - 2
o2ios/O2Platform/Info.plist

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

+ 80 - 3
o2ios/O2Platform/cms/c/CMSCategoryListViewController.swift

@@ -13,6 +13,7 @@ import AlamofireObjectMapper
 import SwiftyJSON
 import ObjectMapper
 import CocoaLumberjack
+import O2OA_Auth_SDK
 
 
 class CMSCategoryListViewController: UIViewController {
@@ -59,7 +60,9 @@ class CMSCategoryListViewController: UIViewController {
             tabIndex = (segmentedControl?.selectedIndex)!
         }
     }
-    
+    // 当前用户能够发布category列表
+    var canPublishCategories: [CMSWrapOutCategoryList] = []
+    var selectedCategory: CMSWrapOutCategoryList?
 
     override func viewDidLoad() {
         super.viewDidLoad()
@@ -85,7 +88,7 @@ class CMSCategoryListViewController: UIViewController {
         })
         currentCategoryId = self.itemsKeys.first!
         self.loadFirstData()
-
+        self.loadCanPublishCategories()
 
         // Do any additional setup after loading the view.
     }
@@ -104,9 +107,18 @@ class CMSCategoryListViewController: UIViewController {
         if segue.identifier == "showDetailContentSegue" {
             let destVC = segue.destination as! CMSItemDetailViewController
             destVC.itemData = sender as! CMSCategoryItemData?
+            destVC.fromCreateDocVC = true
+        }else if segue.identifier == "createDocument" {
+            let createVC = segue.destination as! CMSCreateDocViewController
+            createVC.category = self.selectedCategory
         }
     }
     
+    @IBAction func viewBack2DocumentList(_ sender: UIStoryboardSegue) {
+        DDLogDebug("backto List")
+        self.tableview.mj_header.beginRefreshing()
+    }
+    
     private func sizeForAttributedString(_ attributedString: NSAttributedString) -> CGSize {
         let size = attributedString.size()
         return CGRect(origin: CGPoint.zero, size: size).integral.size
@@ -175,7 +187,7 @@ class CMSCategoryListViewController: UIViewController {
     }
     
     func loadNextPageData(){
-        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsCategoryQuery, parameter: self.pageModel.toDictionary() as [String : AnyObject]?)
+        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsCategoryDetailQuery, parameter: self.pageModel.toDictionary() as [String : AnyObject]?)
         var params:[String:Array<String>] = [:]
         params["categoryIdList"] = [currentCategoryId]
         Alamofire.request(url!, method: .put, parameters: params, encoding: JSONEncoding.default, headers: nil).responseJSON { (response) in
@@ -196,6 +208,71 @@ class CMSCategoryListViewController: UIViewController {
         }
     }
     
+    //获取当前用户能新建文档的分类
+    private func loadCanPublishCategories()  {
+        if let appId = self.cmsData?.wrapOutCategoryList?[0].appId {
+            let url = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsCanPublishCategoryQuery, parameter: ["##appId##": appId as AnyObject])
+            Alamofire.request(url!, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil).responseJSON { (response) in
+                switch response.result {
+                case .success(let val):
+                    let app = Mapper<CMSSingleApplication>().map(JSONObject: val)
+                    self.canPublishCategories = app?.data?.wrapOutCategoryList ?? []
+                    self.addPublishBtn()
+                case .failure(let err):
+                    DDLogError(err.localizedDescription)
+                }
+            }
+        }
+    }
+    //如果有权限就显示新建按钮
+    private func addPublishBtn() {
+        if self.canPublishCategories.count > 0 {
+            self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "新建", style: .plain, target: self, action: #selector(tapPublishBtn))
+        }
+    }
+    // 点击新建按钮显示需要发布的分类列表
+    @objc private func tapPublishBtn() {
+        var actions: [UIAlertAction] = []
+        self.canPublishCategories.forEach { (category) in
+            let item = UIAlertAction(title: "\(category.categoryName ?? "")", style: .default, handler: { (action) in
+                self.selectedCategory = category
+                self.checkDraftThenJump(categoryId: category.id)
+            })
+            actions.append(item)
+        }
+        self.showSheetAction(title: "分类", message: "请选择发布的分类", actions: actions)
+    }
+    //检查选择的分类下是否有未完成的草稿, 有草稿就直接跳转到编辑页面,没有就到新建页面
+    private func checkDraftThenJump(categoryId: String?) {
+        let model = CommonPageModel().toDictionary()
+        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsDocumentDraftQuery, parameter: model as [String : AnyObject]?)
+        var params:[String: Any] = [:]
+        params["categoryIdList"] = [categoryId]
+        if let distinguishedName = O2AuthSDK.shared.myInfo()?.distinguishedName {
+            params["creatorList"] = [distinguishedName]
+        }
+        params["documentType"] = "全部"
+        Alamofire.request(url!, method: .put, parameters: params, encoding: JSONEncoding.default, headers: nil).responseJSON { (response) in
+            switch response.result {
+            case .success(let val):
+                DDLogDebug(JSON(val).description)
+                let res = Mapper<CMSCategory>().map(JSONObject: val)
+                if let docList = res?.data, docList.count > 0 {
+                    self.performSegue(withIdentifier: "showDetailContentSegue", sender: docList[0])
+                }else {
+                    self.gotoNewDocController()
+                }
+            case .failure(let err):
+                DDLogError(err.localizedDescription)
+                self.gotoNewDocController()
+            }
+        }
+    }
+    
+    private func gotoNewDocController() {
+        self.performSegue(withIdentifier: "createDocument", sender: nil)
+    }
+    
 }
 
 

+ 170 - 0
o2ios/O2Platform/cms/c/CMSCreateDocViewController.swift

@@ -0,0 +1,170 @@
+//
+//  CMSCreateDocViewController.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/7/8.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+import UIKit
+import Alamofire
+import AlamofireImage
+import AlamofireObjectMapper
+import SwiftyJSON
+import ObjectMapper
+import Eureka
+import CocoaLumberjack
+
+
+class CMSCreateDocViewController: FormViewController {
+
+    var category: CMSWrapOutCategoryList?
+    
+    var  identityList:[IdentityV2] = []
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        TextRow.defaultCellUpdate = {
+            cell,row in
+            cell.textLabel?.font = setting_content_textFont
+            cell.textLabel?.textColor  = setting_content_textColor
+        }
+        ActionSheetRow<String>.defaultCellUpdate = {
+            cell,row in
+            cell.textLabel?.font = setting_content_textFont
+            cell.textLabel?.textColor  = setting_content_textColor
+        }
+        DateTimeRow.defaultCellUpdate = {
+            cell,row in
+            cell.textLabel?.font = setting_content_textFont
+            cell.textLabel?.textColor  = setting_content_textColor
+        }
+        
+        ButtonRow.defaultCellUpdate = {
+            cell,row in
+            cell.textLabel?.font = setting_item_textFont
+            cell.textLabel?.theme_textColor = ThemeColorPicker(keyPath: "Base.base_color")
+            
+        }
+        title = self.category?.categoryName
+        loadDepartAndIdentity()
+    }
+    
+    
+    func loadDepartAndIdentity(){
+        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(PersonContext.personContextKey, query: PersonContext.personInfoQuery, parameter: nil)
+        Alamofire.request(url!).responseJSON(completionHandler: { response in
+            debugPrint(response.result)
+            switch response.result {
+            case .success(let val):
+                let person = Mapper<PersonV2>().map(JSONString: JSON(val)["data"].description)!
+                if let identities = person.woIdentityList {
+                    self.identityList = identities
+                }
+                DispatchQueue.main.async {
+                    self.showInputUI()
+                }
+            case .failure(let err):
+                DDLogError(err.localizedDescription)
+                DispatchQueue.main.async {
+                    self.showError(title: "读取身份列表失败")
+                }
+            }
+            
+        })
+        
+    }
+    
+    func showInputUI(){
+        form +++ Section("创建文档")
+            <<< TextRow("title") {row in
+                row.title = "文档标题"
+                row.placeholder = "请输入文档标题"
+                }.cellSetup({ (cell, row) in
+                    //
+                })
+            
+            <<< ActionSheetRow<IdentityV2>("selectedIdentity") {
+                $0.title = "用户身份"
+                $0.selectorTitle = "请选择身份"
+                $0.options = self.identityList
+                if(self.identityList.count > 0){
+                    $0.value = self.identityList[0]
+                }
+                }.cellSetup({ (cell, row) in
+                    //cell.height = 50
+                })
+            
+            +++ Section()
+            <<< ButtonRow("createButton") { (row:ButtonRow) in
+                row.title = "创建"
+                }.onCellSelection({ (cell, row) in
+                    let titleRow:TextRow = self.form.rowBy(tag:"title")!
+                    let identityRow:ActionSheetRow<IdentityV2> = self.form.rowBy(tag:"selectedIdentity")!
+                    guard let title = titleRow.value else{
+                        self.showError(title: "请输入标题")
+                        return
+                    }
+                    guard let id = identityRow.value  else {
+                        self.showError(title: "请选择身份")
+                        return
+                    }
+                    self.createDocument(title, id.distinguishedName!)
+                })
+    }
+    
+    func createDocument(_ title: String, _ identity: String) {
+        debugPrint("创建文档:\(title), \(identity)")
+        var json:[String: Any] = [:]
+        json["title"] = title
+        json["appId"] = self.category?.appId
+        json["categoryId"] = self.category?.id
+        json["categoryAlias"] = self.category?.categoryAlias
+        json["categoryName"] = self.category?.categoryName
+        json["creatorIdentity"] = identity
+        json["docStatus"] = "draft"
+        json["isNewDocument"] = true
+        let doc = CMSCategoryItemData(JSON: json)
+        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsDocumentPost, parameter: nil)
+        Alamofire.request(url!, method: .post, parameters: doc?.toJSON(), encoding: JSONEncoding.default, headers: nil).responseJSON { (response) in
+            switch response.result {
+            case .success(let val):
+                let type = JSON(val)["type"]
+                if type == "success" {
+                    let docId = JSON(val)["data"]["id"].string!
+                    DispatchQueue.main.async {
+                        self.showSuccess(title: "创建文档成功")
+                        self.performSegue(withIdentifier: "openDocument", sender: docId)
+                    }
+                }else{
+                    DispatchQueue.main.async {
+                        DDLogError(JSON(val).description)
+                        self.showError(title: "创建文档失败")
+                    }
+                }
+            case .failure(let err):
+                DispatchQueue.main.async {
+                    DDLogError(err.localizedDescription)
+                    self.showError(title: "创建文档失败")
+                }
+            }
+            
+        }
+    }
+    
+
+    /*
+    // 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?) {
+        if segue.identifier == "openDocument" {
+            let destVC = segue.destination as! CMSItemDetailViewController
+            destVC.documentId = sender as? String
+            destVC.fromCreateDocVC = true
+        }
+    }
+ 
+
+}

+ 464 - 139
o2ios/O2Platform/cms/c/CMSItemDetailViewController.swift

@@ -13,76 +13,79 @@ import AlamofireObjectMapper
 import SwiftyJSON
 import ObjectMapper
 import QuickLook
+import Photos
 import CocoaLumberjack
 
 class CMSItemDetailViewController: BaseWebViewUIViewController {
     
     
-    private let qlController = QLPreviewController()
-    
-    private var zonePickerView:ZonePickerView!
-    
+    private let qlController = TaskAttachmentPreviewController()
     
     fileprivate var currentFileURLS:[NSURL] = []
-    
-    private var window:UIWindow?
-    
+ 
     var itemData:CMSCategoryItemData? {
         didSet {
             title = itemData?.title
             itemUrl = AppDelegate.o2Collect.genrateURLWithWebContextKey(DesktopContext.DesktopContextKey, query: DesktopContext.cmsItemDetailQuery, parameter: ["##documentId##":itemData?.id as AnyObject])!
-            attachmentListUrl = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query:CMSContext.cmsAttachmentListQuery, parameter: ["##documentId##":itemData?.id as AnyObject])!
         }
     }
     
     var documentId:String?{
         didSet {
             itemUrl = AppDelegate.o2Collect.genrateURLWithWebContextKey(DesktopContext.DesktopContextKey, query: DesktopContext.cmsItemDetailQuery, parameter: ["##documentId##":documentId as AnyObject])!
-            attachmentListUrl = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query:CMSContext.cmsAttachmentListQuery, parameter: ["##documentId##":documentId as AnyObject])!
         }
     }
     
-    var publishItemInfo:CMSPublishInfo! {
-        didSet {
-            if let datas = publishItemInfo.data  {
-                self.window?.isHidden = datas.count > 0 ? false : true
-                for infoData in datas {
-                    let m = ZonePickerModel()
-                    m.id = infoData.id
-                    m.name = infoData.name
-                    m.sourceObj = infoData
-                    attachModels.append(m)
-                }
-            }
-         
-        }
-    }
+    var itemUrl = ""
     
-    var attachModels:[ZonePickerModel] = []
+    var fromCreateDocVC = false
+    //cms操作control
+    var myControl: [String : AnyObject]?
+    //cms底部操作按钮 toolbar
+    var toolbarView: UIToolbar!
+    //webview的容器
+    @IBOutlet weak var webViewContainer: UIView!
+    @IBOutlet weak var progressView: UIProgressView!
     
-    var attachmentListUrl = ""
-    
-    var itemUrl = ""
     
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
-        NotificationCenter.default.addObserver(self, selector: #selector(showAttachViewInController(_:)), name: NSNotification.Name("SHOW_ATTACH_OBJ"), object: nil)
-        NotificationCenter.default.addObserver(self, selector: #selector(quitAttachViewInController(_:)), name: NSNotification.Name("QUIT_ATTACH_OBJ"), object: nil)
-        
+        //监控进度
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
     }
     
     override func viewWillDisappear(_ animated: Bool) {
         super.viewWillDisappear(animated)
-        NotificationCenter.default.removeObserver(self)
-        self.window?.isHidden = true
+        webView.removeObserver(self, forKeyPath: "estimatedProgress")
     }
 
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.isHidden = webView.estimatedProgress == 1
+            progressView.setProgress(Float(webView.estimatedProgress), animated: true)
+        }
+    }
+    
     override func viewDidLoad() {
         super.viewDidLoad()
-        createButton()
-        loadAttachmentList()
-        theWebView()
-        qlInit()
+        //自定义返回按钮
+        self.navigationItem.hidesBackButton = true
+        self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(named: "icon_fanhui"), style: .plain, target: self, action: #selector(goBack))
+        self.navigationItem.leftItemsSupplementBackButton = true
+        // 底部操作按钮toolbar
+        self.toolbarView = UIToolbar(frame: CGRect(x: 0, y: self.view.height - 44, width: self.view.width, height: 44))
+        self.automaticallyAdjustsScrollViewInsets = false
+
+        
+        //先添加js注入
+        addScriptMessageHandler(key: "cmsFormLoaded", handler: self)
+        addScriptMessageHandler(key: "uploadAttachment", handler: self)
+        addScriptMessageHandler(key: "downloadAttachment", handler: self)
+        addScriptMessageHandler(key: "replaceAttachment", handler: self)
+        addScriptMessageHandler(key: "openDocument", handler: self)
+        addScriptMessageHandler(key: "closeDocumentWindow", handler: self)
+        self.theWebView()
+        self.qlInit()
     }
 
     override func didReceiveMemoryWarning() {
@@ -92,35 +95,40 @@ class CMSItemDetailViewController: BaseWebViewUIViewController {
     
     override func theWebView(){
         super.theWebView()
-        self.view = webView
-        //注入回复的回复函数
-        webView.navigationDelegate = self
         
+        self.webViewContainer.addSubview(self.webView)
+        self.webView.translatesAutoresizingMaskIntoConstraints = false
+        let top = NSLayoutConstraint(item: self.webView as Any, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.webViewContainer, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0)
+        let bottom = NSLayoutConstraint(item: self.webView as Any, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.webViewContainer, attribute: NSLayoutConstraint.Attribute.bottom, multiplier: 1, constant: 0)
+        let trailing = NSLayoutConstraint(item: self.webView as Any, attribute: NSLayoutConstraint.Attribute.trailing, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.webViewContainer, attribute: NSLayoutConstraint.Attribute.trailing, multiplier: 1, constant: 0)
+        let leading = NSLayoutConstraint(item: self.webView as Any, attribute: NSLayoutConstraint.Attribute.leading, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.webViewContainer, attribute: NSLayoutConstraint.Attribute.leading, multiplier: 1, constant: 0)
+        self.webViewContainer.addConstraints([top, bottom, trailing, leading])
+       
+        webView.navigationDelegate = self
         webView.uiDelegate = self
-        
         webView.allowsBackForwardNavigationGestures = true
         
         loadItemDetail()
     }
     
-    private func qlInit(){
-//        qlController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: .plain, target: self, action: #selector(qlCloseWindow))
-        self.qlController.delegate = self
-        self.qlController.dataSource = self
+    @objc func goBack() {
+        if self.fromCreateDocVC {//创建文档页面跳过来的 返回的时候就多跳一级
+            self.performSegue(withIdentifier: "back2DocumentListSegue", sender: nil)
+        } else {
+          self.navigationController?.popViewController(animated: false)
+        }
     }
     
-    private func loadAttachmentList(){
-        Alamofire.request(attachmentListUrl, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil).responseJSON { (response) in
-            switch response.result {
-            case .success(let val):
-                self.publishItemInfo = Mapper<CMSPublishInfo>().map(JSONObject: val)!
-            case .failure(let err):
-                DDLogError(err.localizedDescription)
-            }
-        }
+    private func qlInit(){
+        // 文档查看器
+        self.qlController.dataSource = qlController
+        self.qlController.delegate = qlController
     }
     
+    
+    
     private func loadItemDetail()  {
+        DDLogDebug("url:\(itemUrl)")
         webView.load(Alamofire.request(itemUrl).request!)
     }
     
@@ -130,88 +138,80 @@ class CMSItemDetailViewController: BaseWebViewUIViewController {
         })
     }
     
-    @objc private func quitAttachViewInController(_ noti:NSNotification){
-        self.window?.isHidden = false
-    }
-    
-    @objc private func showAttachViewInController(_ noti:NSNotification){
-        if let obj = noti.object {
-            let m = obj as! ZonePickerModel
-            self.downloadFile(m, { (url) in
-                //self.performSegue(withIdentifier: "showInQL", sender: url)
-                self.currentFileURLS.removeAll(keepingCapacity: true)
-                let currentURL = NSURL(fileURLWithPath: url)
-                if QLPreviewController.canPreview(currentURL) {
-                    self.currentFileURLS.append(currentURL)
-                    self.qlController.reloadData()
-                    if #available(iOS 10, *) {
-                        let navVC = ZLNormalNavViewController(rootViewController: self.qlController)
-                        self.qlController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: .plain, target: self, action: #selector(self.qlCloseWindow))
-                        self.presentVC(navVC)
-                    }else{
-                        //if #available(iOS 9, *){
-                        self.zonePickerView.hidePickerView()
-                        let prController = CMSQLViewController()
-                        prController.delegate = self
-                        prController.dataSource = self
-                        self.pushVC(prController)
-                        //}
+    private func setupBottomToolbar() {
+        var items: [UIBarButtonItem] = []
+        if self.myControl != nil {
+            let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
+            if let allowDelete = self.myControl!["allowDeleteDocument"] as? Bool {
+                if allowDelete { //删除文档
+                    DDLogDebug("删除文档。。。。。。。。。。。。。。。。。。。。。。安装按钮")
+                    let deleteBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
+                    deleteBtn.setTitle("删除", for: .normal)
+                    deleteBtn.setTitleColor(base_color, for: .normal)
+                    deleteBtn.addTapGesture { (tap) in
+                        self.itemBtnDocDeleteAction()
                     }
-                    
-                    
+                    let deleteItem = UIBarButtonItem(customView: deleteBtn)
+                    items.append(spaceItem)
+                    items.append(deleteItem)
+                    items.append(spaceItem)
                 }
-            })
+            }
+            if let allowPublishDocument = self.myControl!["allowPublishDocument"] as? Bool {
+                if allowPublishDocument { //发布文档
+                    DDLogDebug("发布文档。。。。。。。。。。。。。。。。。。。。。。安装按钮")
+                    let publishBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
+                    publishBtn.setTitle("发布", for: .normal)
+                    publishBtn.setTitleColor(base_color, for: .normal)
+                    publishBtn.addTapGesture { (tap) in
+                        self.itemBtnDocPublishAction()
+                    }
+                    let publishItem = UIBarButtonItem(customView: publishBtn)
+                    items.append(spaceItem)
+                    items.append(publishItem)
+                    items.append(spaceItem)
+                }
+            }
+            self.layoutBottomBar(items: items)
         }
     }
-    
-    private func downloadFile(_ model:ZonePickerModel,_ completed:@escaping (_ localURLForFile:String) -> Void){
-        let downURL = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsAttachmentDownloadQuery, parameter: ["##id##":model.id as AnyObject])!
-        let destination: DownloadRequest.DownloadFileDestination = { _, _ in
-            let documentsURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
-            let fileURL = documentsURL.appendingPathComponent(model.name)
-            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
-        }
-        self.showMessage(title: "downloading...")
-        Alamofire.download(downURL, to: destination).response { response in
-            print(response)
-            if response.error == nil, let filePath = response.destinationURL?.path {
-                DispatchQueue.main.async {
-                    self.dismissProgressHUD()
-                }
-               completed(filePath)
-            }else{
-                DispatchQueue.main.async {
-                    self.showError(title: "文件下载出错")
+    private func layoutBottomBar(items: [UIBarButtonItem]) {
+        if items.count > 0 {
+            self.toolbarView.items = items
+            self.view.addSubview(self.toolbarView)
+            self.toolbarView.translatesAutoresizingMaskIntoConstraints = false
+            let heightC = NSLayoutConstraint(item: self.toolbarView as Any, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 0.0, constant: 44)
+            self.toolbarView.addConstraint(heightC)
+            let bottom = NSLayoutConstraint(item: self.toolbarView as Any, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.bottom, multiplier: 1, constant: 0)
+            let trailing = NSLayoutConstraint(item: self.toolbarView as Any, attribute: NSLayoutConstraint.Attribute.trailing, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.trailing, multiplier: 1, constant: 0)
+            let leading = NSLayoutConstraint(item: self.toolbarView as Any, attribute: NSLayoutConstraint.Attribute.leading, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.leading, multiplier: 1, constant: 0)
+            self.view.addConstraints([bottom, leading, trailing])
+            self.view.constraints.forEach { (constraint) in
+                if constraint.identifier == "webViewContainerBottom" {
+                    self.view.removeConstraint(constraint)
                 }
             }
+            let webcTop = NSLayoutConstraint(item: self.webViewContainer as Any, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.toolbarView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0)
+            self.view.addConstraint(webcTop)
+            self.view.layoutIfNeeded()
         }
     }
     
-
-    
-    //创建一个显示列表附件的按钮
-   private func createButton(){
-        let width = SCREEN_WIDTH
-        let height = SCREEN_HEIGHT
-        let button  = UIButton(frame: CGRect(x: 0,y: 0,width:30,height: 30))
-        button.setImage(UIImage(named: "icon_attach"), for:.normal)
-        button.addTarget(self, action: #selector(showAttachmentList(_:)), for: .touchUpInside)
-        self.window = UIWindow(frame: CGRect(x: (width - 30) / 2, y: height - 60, width: 30, height: 30))
-        self.window?.windowLevel = UIWindow.Level.alert + 1
-        self.window?.backgroundColor = UIColor.white
-        self.window?.layer.cornerRadius = 15
-        self.window?.layer.masksToBounds = true
-        self.window?.addSubview(button)
-        self.window?.makeKeyAndVisible()
+    //删除文档
+    private func itemBtnDocDeleteAction() {
+        self.showDefaultConfirm(title: "提示", message: "你确定要删除当前文档?") { (action) in
+            let callJS = "layout.appForm.deleteDocumentForMobile()"
+            self.webView.evaluateJavaScript(callJS, completionHandler: { (result, err) in
+                //
+            })
+        }
     }
-    
-    @objc func showAttachmentList(_ sender:UIButton){
-        print("List AttachList")
-        self.window?.isHidden = true
-        self.zonePickerView = ZonePickerView()
-        zonePickerView.models = attachModels
-        zonePickerView.showPickerView()
-        
+    //发布文档 layout.appForm.publishDocument()
+    private func itemBtnDocPublishAction() {
+        let callJS = "layout.appForm.publishDocument()"
+        self.webView.evaluateJavaScript(callJS, completionHandler: { (result, err) in
+            //
+        })
     }
     
 }
@@ -219,12 +219,12 @@ class CMSItemDetailViewController: BaseWebViewUIViewController {
 extension CMSItemDetailViewController:WKNavigationDelegate,WKUIDelegate {
     
     func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
-        DDLogDebug("didFailProvisionalNavigation \(navigation)  error = \(error)")
+        DDLogDebug("didFailProvisionalNavigation \(String(describing: navigation))  error = \(error)")
     }
     
     
     func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
-        DDLogDebug("didStartProvisionalNavigation \(navigation)")
+        DDLogDebug("didStartProvisionalNavigation \(String(describing: navigation))")
     }
     
     func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
@@ -244,22 +244,347 @@ extension CMSItemDetailViewController:WKNavigationDelegate,WKUIDelegate {
     
 }
 
-extension CMSItemDetailViewController:QLPreviewControllerDataSource,QLPreviewControllerDelegate{
-    func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
-        return self.currentFileURLS.count
+//MARK: js脚本
+extension CMSItemDetailViewController: O2WKScriptMessageHandlerImplement {
+    func userController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        let name = message.name
+        switch name {
+        case "cmsFormLoaded":
+            DDLogDebug("cmsFormLoaded start。。。。")
+            if let newControls = (message.body as? NSString) {
+                let str = newControls as String
+                DDLogDebug("cmsFormLoaded , controls :\(str)")
+                let json = JSON.init(parseJSON: str)
+                self.myControl = json.dictionaryObject! as [String: AnyObject]
+                self.setupBottomToolbar()
+            }
+            break
+        case "uploadAttachment":
+            ZonePermissions.requestImagePickerAuthorization(callback: { (zoneStatus) in
+                if zoneStatus == ZoneAuthorizationStatus.zAuthorizationStatusAuthorized {
+                    let site = (message.body as! NSDictionary)["site"]
+                    self.uploadAttachment(site as! String)
+                }else {
+                    self.gotoApplicationSettings(alertMessage: "需要照片允许访问权限,是否跳转到手机设置页面开启相机权限?")
+                }
+            })
+            break
+        case "downloadAttachment":
+            let attachmentId = (message.body as! NSDictionary)["id"]
+            self.downloadAttachment(attachmentId as! String)
+            break
+        case "replaceAttachment":
+            let attachmentId = (message.body as! NSDictionary)["id"] as! String
+            let site = (message.body as! NSDictionary)["site"] as? String
+            self.replaceAttachment(attachmentId, site ?? "")
+            break
+        case "openDocument":
+            let url = (message.body as! NSString)
+            self.downloadDocumentAndPreview(String(url))
+            break
+        case "closeDocumentWindow":
+            self.goBack()
+            break
+        default:
+            DDLogError("未知方法名:\(name)!")
+            break
+        }
     }
     
-    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
-        return self.currentFileURLS[index]
+    
+    
+    //上传附件
+    private func uploadAttachment(_ site:String){
+        //选择附件上传
+        var id = ""
+        if self.documentId != nil {
+            id = self.documentId!
+        }else {
+            id = self.itemData!.id!
+        }
+        let updloadURL = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsAttachmentUpload, parameter: ["##docId##":id as AnyObject])
+        self.uploadAttachment(site, uploadURL: updloadURL!)
+    }
+    private func uploadAttachment(_ site:String,uploadURL url:String){
+        let vc = FileBSImagePickerViewController()
+        bs_presentImagePickerController(vc, animated: true,
+                                        select: { (asset: PHAsset) -> Void in
+                                            // User selected an asset.
+                                            // Do something with it, start upload perhaps?
+        }, deselect: { (asset: PHAsset) -> Void in
+            // User deselected an assets.
+            // Do something, cancel upload?
+        }, cancel: { (assets: [PHAsset]) -> Void in
+            // User cancelled. And this where the assets currently selected.
+        }, finish: { (assets: [PHAsset]) -> Void in
+            for asset in assets {
+                switch asset.mediaType {
+                case .audio:
+                    DDLogDebug("Audio")
+                case .image:
+                    let options = PHImageRequestOptions()
+                    options.isSynchronous = true
+                    options.deliveryMode = .fastFormat
+                    options.resizeMode = .none
+                    PHImageManager.default().requestImageData(for: asset, options: options, resultHandler: { (imageData, result, imageOrientation, dict) in
+                        //DDLogDebug("result = \(result) imageOrientation = \(imageOrientation) \(dict)")
+                        let fileURL = dict?["PHImageFileURLKey"] as! URL
+                        DispatchQueue.main.async {
+                            self.showMessage(title: "上传中...")
+                        }
+                        DispatchQueue.global(qos: .userInitiated).async {
+                            Alamofire.upload(multipartFormData: { (mData) in
+                                //mData.append(fileURL, withName: "file")
+                                mData.append(imageData!, withName: "file", fileName: fileURL.lastPathComponent, mimeType: "application/octet-stream")
+                                let siteData = site.data(using: String.Encoding.utf8, allowLossyConversion: false)
+                                mData.append(siteData!, withName: "site")
+                            }, to: url, encodingCompletion: { (encodingResult) in
+                                switch encodingResult {
+                                case .success(let upload, _, _):
+                                    debugPrint(upload)
+                                    upload.responseJSON {
+                                        respJSON in
+                                        switch respJSON.result {
+                                        case .success(let val):
+                                            let attachId = JSON(val)["data"]["id"].string!
+                                            DispatchQueue.main.async {
+                                                //ProgressHUD.showSuccess("上传成功")
+                                                let callJS = "layout.appForm.uploadedAttachment(\"\(site)\", \"\(attachId)\")"
+                                                self.webView.evaluateJavaScript(callJS, completionHandler: { (result, err) in
+                                                    self.showSuccess(title: "上传成功")
+                                                })
+                                            }
+                                        case .failure(let err):
+                                            DispatchQueue.main.async {
+                                                DDLogError(err.localizedDescription)
+                                                self.showError(title: "上传失败")
+                                            }
+                                            break
+                                        }
+                                        
+                                    }
+                                case .failure(let errType):
+                                    DispatchQueue.main.async {
+                                        DDLogError(errType.localizedDescription)
+                                        self.showError(title: "上传失败")
+                                    }
+                                }
+                                
+                            })
+                        }
+                    })
+                case .video:
+                    DDLogDebug("video")
+                case .unknown:
+                    DDLogDebug("Unknown")
+                    
+                @unknown default:
+                    DDLogDebug("Unknown")
+                }
+            }
+        }, completion: nil)
     }
     
-    func previewControllerWillDismiss(_ controller: QLPreviewController) {
-        guard #available(iOS 10,*) else{
-            self.showAttachmentList(UIButton(type: .custom))
+    //下载预览附件
+    private func downloadAttachment(_ attachmentId:String){
+        //文档id
+        var id: String?
+        if self.documentId != nil {
+            id = self.documentId
+        }else {
+            id = self.itemData?.id
+        }
+        if id == nil {
+            self.showError(title: "下载文件出错")
             return
         }
+        //
+        let attachInfoURL = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsAttachmentGET, parameter: ["##attachId##":attachmentId as AnyObject, "##documentId##": id as AnyObject])
+        //附件下载链接
+        let downURL = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsAttachmentDownloadNewQuery, parameter: ["##attachId##":attachmentId as AnyObject])
+        self.showMessage(title: "下载中...")
+        // 先获取附件对象
+        Alamofire.request(attachInfoURL!).responseJSON { (response) in
+            switch response.result {
+            case .success(let val):
+                let info = Mapper<CMSAttachmentInfoResponse>().map(JSONString: JSON(val).description)
+                if let fileName = info?.data?.name {
+                    //执行下载
+                    let destination: DownloadRequest.DownloadFileDestination = { _, _ in
+                        let documentsURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+                        let fileURL = documentsURL.appendingPathComponent(fileName)
+                        return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
+                    }
+                    //然后下载附件
+                    Alamofire.download(downURL!, to: destination).response(completionHandler: { (response) in
+                        if response.error == nil , let fileurl = response.destinationURL?.path {
+                            //打开文件
+                            self.dismissProgressHUD()
+                            self.previewAttachment(fileurl)
+                        }else{
+                            DispatchQueue.main.async {
+                                self.showError(title: "下载文件出错")
+                            }
+                        }
+                    })
+                }else {
+                    DispatchQueue.main.async {
+                        self.showError(title: "下载文件出错")
+                    }
+                }
+                break
+            case .failure(let err):
+                DDLogError(err.localizedDescription)
+                DispatchQueue.main.async {
+                    self.showError(title: "下载文件出错")
+                }
+                break
+            }
+        }
+        
+         
     }
+    
+    //替换附件
+    private func replaceAttachment(_ attachmentId:String, _ site:String){
+        var id = ""
+        if self.documentId != nil {
+            id = self.documentId!
+        }else {
+            id = self.itemData!.id!
+        }
+        let replaceURL = AppDelegate.o2Collect.generateURLWithAppContextKey(CMSContext.cmsContextKey, query: CMSContext.cmsAttachmentReplace, parameter: ["##attachId##":attachmentId as AnyObject,"##docId##": id as AnyObject])!
+        self.replaceAttachment(site, attachmentId, replaceURL: replaceURL)
+    }
+    
+    
+    
+    /**
+     * 下载公文 并阅览
+     **/
+    private func downloadDocumentAndPreview(_ url: String) {
+        DDLogDebug("文档下载地址:\(url)")
+        self.showMessage(title: "下载中...")
+        // 文件地址
+        let localFileDestination: DownloadRequest.DownloadFileDestination = { _, response in
+            let documentsURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+            let fileURL = documentsURL.appendingPathComponent(response.suggestedFilename!)
+            // 有重名文件就删除重建
+            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
+        }
+        Alamofire.download(url, to: localFileDestination).response(completionHandler: { (response) in
+            if response.error == nil , let fileurl = response.destinationURL?.path {
+                DDLogDebug("文件地址:\(fileurl)")
+                //打开文件
+                self.dismissProgressHUD()
+                self.previewAttachment(fileurl)
+            }else{
+                let msg = response.error?.localizedDescription ?? ""
+                DDLogError("下载文件出错,\(msg)")
+                DispatchQueue.main.async {
+                    self.showError(title: "预览文件出错")
+                }
+            }
+        })
+    }
+    
+    private func previewAttachment(_ url:String){
+        let currentURL = NSURL(fileURLWithPath: url)
+        if QLPreviewController.canPreview(currentURL) {
+            self.qlController.currentFileURLS.removeAll(keepingCapacity: true)
+            self.qlController.currentFileURLS.append(currentURL)
+            self.qlController.reloadData()
+            if #available(iOS 10, *) {
+                let navVC = ZLNormalNavViewController(rootViewController: qlController)
+                qlController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: .plain, target: qlController, action: #selector(qlController.qlCloseWindow))
+                self.presentVC(navVC)
+            }else{
+                self.pushVC(qlController)
+            }
+        }else{
+            self.showError(title: "此文件无法预览,请在PC端查看")
+        }
+        
+    }
+    
+    
+    private func replaceAttachment(_ site:String,_ attachmentId:String,replaceURL url:String){
+        let vc = FileBSImagePickerViewController()
+        bs_presentImagePickerController(vc, animated: true,
+                                        select: { (asset: PHAsset) -> Void in
+                                            // User selected an asset.
+                                            // Do something with it, start upload perhaps?
+        }, deselect: { (asset: PHAsset) -> Void in
+            // User deselected an assets.
+            // Do something, cancel upload?
+        }, cancel: { (assets: [PHAsset]) -> Void in
+            // User cancelled. And this where the assets currently selected.
+        }, finish: { (assets: [PHAsset]) -> Void in
+            for asset in assets {
+                switch asset.mediaType {
+                case .audio:
+                    DDLogDebug("Audio")
+                case .image:
+                    let options = PHImageRequestOptions()
+                    options.isSynchronous = true
+                    options.deliveryMode = .fastFormat
+                    options.resizeMode = .none
+                    PHImageManager.default().requestImageData(for: asset, options: options, resultHandler: { (imageData, result, imageOrientation, dict) in
+                        //DDLogDebug("result = \(result) imageOrientation = \(imageOrientation) \(dict)")
+                        let fileURL = dict?["PHImageFileURLKey"] as! URL
+                        DispatchQueue.main.async {
+                            self.showMessage(title: "上传中...")
+                        }
+                        DispatchQueue.global(qos: .userInitiated).async {
+                            Alamofire.upload(multipartFormData: { (mData) in
+                                //mData.append(fileURL, withName: "file")
+                                mData.append(imageData!, withName: "file", fileName: fileURL.lastPathComponent, mimeType: "application/octet-stream")
+                                let siteData = site.data(using: String.Encoding.utf8, allowLossyConversion: false)
+                                mData.append(siteData!, withName: "site")
+                            }, usingThreshold: SessionManager.multipartFormDataEncodingMemoryThreshold, to: url, method: .put, headers: nil, encodingCompletion: { (encodingResult) in
+                                switch encodingResult {
+                                case .success(let upload, _, _):
+                                    debugPrint(upload)
+                                    upload.responseJSON {
+                                        respJSON in
+                                        switch respJSON.result {
+                                        case .success( _):
+                                            DispatchQueue.main.async {
+                                                let callJS = "layout.appForm.replacedAttachment(\"\(site)\", \"\(attachmentId)\")"
+                                                self.webView.evaluateJavaScript(callJS, completionHandler: { (result, err) in
+                                                    self.showSuccess(title: "替换成功")
+                                                })
+                                            }
+                                        case .failure(let err):
+                                            DispatchQueue.main.async {
+                                                DDLogError(err.localizedDescription)
+                                                self.showError(title: "替换失败")
+                                            }
+                                            break
+                                        }
+                                        
+                                    }
+                                case .failure(let errType):
+                                    DispatchQueue.main.async {
+                                        DDLogError(errType.localizedDescription)
+                                        self.showError(title: "替换失败")
+                                    }
+                                }
+                                
+                            })
+                        }
+                    })
+                case .video:
+                     DDLogDebug("video")
+                case .unknown:
+                    DDLogDebug("Unknown")
+                    
+                @unknown default:
+                    DDLogDebug("Unknown")
+                }
+            }
+        }, completion: nil)
+    }
+    
+    
 }
-
-
-

+ 96 - 0
o2ios/O2Platform/cms/m/CMSAttachmentInfoResponse.swift

@@ -0,0 +1,96 @@
+//
+//  CMSAttachmentInfoResponse.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/7/10.
+//  Copyright © 2019 zoneland. All rights reserved.
+//
+
+
+import Foundation
+import ObjectMapper
+
+
+class CMSAttachmentInfoResponse : NSObject, NSCoding, Mappable{
+    
+    var count : Int?
+    var data : CMSPublishInfoData?
+    var date : String?
+    var message : String?
+    var position : Int?
+    var size : Int?
+    var spent : Int?
+    var type : String?
+    
+    
+    class func newInstance(map: Map) -> Mappable?{
+        return CMSAttachmentInfoResponse()
+    }
+    required init?(map: Map){}
+    private override init(){}
+    
+    func mapping(map: Map)
+    {
+        count <- map["count"]
+        data <- map["data"]
+        date <- map["date"]
+        message <- map["message"]
+        position <- map["position"]
+        size <- map["size"]
+        spent <- map["spent"]
+        type <- map["type"]
+        
+    }
+    
+    /**
+     * NSCoding required initializer.
+     * Fills the data from the passed decoder
+     */
+    @objc required init(coder aDecoder: NSCoder)
+    {
+        count = aDecoder.decodeObject(forKey: "count") as? Int
+        data = aDecoder.decodeObject(forKey: "data") as? CMSPublishInfoData
+        date = aDecoder.decodeObject(forKey: "date") as? String
+        message = aDecoder.decodeObject(forKey: "message") as? String
+        position = aDecoder.decodeObject(forKey: "position") as? Int
+        size = aDecoder.decodeObject(forKey: "size") as? Int
+        spent = aDecoder.decodeObject(forKey: "spent") as? Int
+        type = aDecoder.decodeObject(forKey: "type") as? String
+        
+    }
+    
+    /**
+     * NSCoding required method.
+     * Encodes mode properties into the decoder
+     */
+    @objc func encode(with aCoder: NSCoder)
+    {
+        if count != nil{
+            aCoder.encode(count, forKey: "count")
+        }
+        if data != nil{
+            aCoder.encode(data, forKey: "data")
+        }
+        if date != nil{
+            aCoder.encode(date, forKey: "date")
+        }
+        if message != nil{
+            aCoder.encode(message, forKey: "message")
+        }
+        if position != nil{
+            aCoder.encode(position, forKey: "position")
+        }
+        if size != nil{
+            aCoder.encode(size, forKey: "size")
+        }
+        if spent != nil{
+            aCoder.encode(spent, forKey: "spent")
+        }
+        if type != nil{
+            aCoder.encode(type, forKey: "type")
+        }
+        
+    }
+    
+}
+

+ 6 - 0
o2ios/O2Platform/cms/m/CMSCategoryItemData.swift

@@ -26,6 +26,7 @@ class CMSCategoryItemData : NSObject, NSCoding, Mappable{
 	var sequence : String?
 	var title : String?
 	var updateTime : String?
+    var publishTime : String?
 
 
 	class func newInstance(map: Map) -> Mappable?{
@@ -54,6 +55,7 @@ class CMSCategoryItemData : NSObject, NSCoding, Mappable{
 		sequence <- map["sequence"]
 		title <- map["title"]
 		updateTime <- map["updateTime"]
+        publishTime <- map["publishTime"]
 		
 	}
 
@@ -81,6 +83,7 @@ class CMSCategoryItemData : NSObject, NSCoding, Mappable{
          sequence = aDecoder.decodeObject(forKey: "sequence") as? String
          title = aDecoder.decodeObject(forKey: "title") as? String
          updateTime = aDecoder.decodeObject(forKey: "updateTime") as? String
+         publishTime = aDecoder.decodeObject(forKey: "publishTime") as? String
 
 	}
 
@@ -144,6 +147,9 @@ class CMSCategoryItemData : NSObject, NSCoding, Mappable{
 		if updateTime != nil{
 			aCoder.encode(updateTime, forKey: "updateTime")
 		}
+        if publishTime != nil{
+            aCoder.encode(publishTime, forKey: "publishTime")
+        }
 
 	}
 

+ 2 - 2
o2ios/O2Platform/cms/m/CMSPublishInfoData.swift

@@ -16,13 +16,13 @@ class CMSPublishInfoData : NSObject, NSCoding, Mappable{
 	var documentId : String?
 	var `extension` : String?
 	var fileHost : String?
-	var fileName : String?
+	var fileName : String? //后台存储的文件名称
 	var filePath : String?
 	var fileType : String?
 	var id : String?
 	var lastUpdateTime : String?
 	var length : Int?
-	var name : String?
+	var name : String? //原文件名称
 	var sequence : String?
 	var site : String?
 	var storage : String?

+ 100 - 0
o2ios/O2Platform/cms/m/CMSSingleApplication.swift

@@ -0,0 +1,100 @@
+//
+//  CMSSingleApplication.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2019/7/8.
+//  Copyright © 2019 zoneland. All rights reserved.
+
+import Foundation
+import ObjectMapper
+
+
+class CMSSingleApplication : NSObject, NSCoding, Mappable{
+    
+    var count : Int?
+    var data : CMSData?
+    var date : String?
+    var message : String?
+    var position : Int?
+    var size : Int?
+    var spent : Int?
+    var type : String?
+    var userMessage : String?
+    
+    
+    class func newInstance(map: Map) -> Mappable?{
+        return CMSSingleApplication()
+    }
+    required init?(map: Map){}
+    private override init(){}
+    
+    func mapping(map: Map)
+    {
+        count <- map["count"]
+        data <- map["data"]
+        date <- map["date"]
+        message <- map["message"]
+        position <- map["position"]
+        size <- map["size"]
+        spent <- map["spent"]
+        type <- map["type"]
+        userMessage <- map["userMessage"]
+        
+    }
+    
+    /**
+     * NSCoding required initializer.
+     * Fills the data from the passed decoder
+     */
+    @objc required init(coder aDecoder: NSCoder)
+    {
+        count = aDecoder.decodeObject(forKey: "count") as? Int
+        data = aDecoder.decodeObject(forKey: "data") as? CMSData
+        date = aDecoder.decodeObject(forKey: "date") as? String
+        message = aDecoder.decodeObject(forKey: "message") as? String
+        position = aDecoder.decodeObject(forKey: "position") as? Int
+        size = aDecoder.decodeObject(forKey: "size") as? Int
+        spent = aDecoder.decodeObject(forKey: "spent") as? Int
+        type = aDecoder.decodeObject(forKey: "type") as? String
+        userMessage = aDecoder.decodeObject(forKey: "userMessage") as? String
+        
+    }
+    
+    /**
+     * NSCoding required method.
+     * Encodes mode properties into the decoder
+     */
+    @objc func encode(with aCoder: NSCoder)
+    {
+        if count != nil{
+            aCoder.encode(count, forKey: "count")
+        }
+        if data != nil{
+            aCoder.encode(data, forKey: "data")
+        }
+        if date != nil{
+            aCoder.encode(date, forKey: "date")
+        }
+        if message != nil{
+            aCoder.encode(message, forKey: "message")
+        }
+        if position != nil{
+            aCoder.encode(position, forKey: "position")
+        }
+        if size != nil{
+            aCoder.encode(size, forKey: "size")
+        }
+        if spent != nil{
+            aCoder.encode(spent, forKey: "spent")
+        }
+        if type != nil{
+            aCoder.encode(type, forKey: "type")
+        }
+        if userMessage != nil{
+            aCoder.encode(userMessage, forKey: "userMessage")
+        }
+        
+    }
+    
+}
+

+ 9 - 1
o2ios/O2Platform/cms/v/CMSItemTableViewCell.swift

@@ -27,7 +27,15 @@ class CMSItemTableViewCell: UITableViewCell {
             let url = URL(string: urlstr!)
             self.itemIconImageView.hnk_setImageFromURL(url!)
             self.titleLabel.text = itemData?.title
-            self.itemTimeLabel.text = itemData?.updateTime?.split(" ")[0]
+            if let publishTime = itemData?.publishTime {
+                if let time = Date.date(publishTime, formatter: "yyyy-MM-dd HH:mm:ss") {
+                    self.itemTimeLabel.text = time.friendlyTime()
+                }else {
+                    self.itemTimeLabel.text = publishTime
+                }
+            }else {
+                self.itemTimeLabel.text = "Unknown"
+            }
         }
     }
 

+ 32 - 0
o2ios/O2Platform/common/ext/Date+Extension.swift

@@ -264,5 +264,37 @@ extension Date {
         let dateString = dateformatter.string(from: self)
         return dateString
     }
+    
+    
+    func friendlyTime() -> String {
+        var returnTimeString = ""
+        let now = Date()
+        if now.haveSameYearMonthDayAndHour(self) {
+            let gap = now.minute - self.minute
+            returnTimeString = "\(gap)分钟前"
+        }else if now.haveSameYearMonthAndDay(self) {
+            let gap = now.hour - self.hour
+            returnTimeString = "\(gap)小时前"
+        }else if now.haveSameYearAndMonth(self) {
+            let gap = now.day - self.day
+            if gap == 1 {
+                returnTimeString = "昨天"
+            }else if gap == 2 {
+                returnTimeString = "前天"
+            }else {
+                returnTimeString = "\(gap)天前"
+            }
+        }else if now.haveSameYear(self) {
+            let gap = now.month - self.month
+            if gap < 4 {
+                returnTimeString = "\(gap)个月前"
+            }else {
+                returnTimeString = self.formatterDate(formatter: "yyyy-MM-dd")
+            }
+        }else {
+            returnTimeString = self.formatterDate(formatter: "yyyy-MM-dd")
+        }
+        return returnTimeString
+    }
 }
 

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

@@ -229,10 +229,17 @@ struct icContext {
 struct CMSContext {
     static let cmsContextKey = "x_cms_assemble_control"
     static let cmsCategoryQuery  = "jaxrs/appinfo/list/user/view"
+    static let cmsCanPublishCategoryQuery  = "jaxrs/appinfo/get/user/publish/##appId##" //GET 查询app下当前用户能发布的category,返回的是app对象
     static let cmsCategoryListQuery = "jaxrs/categoryinfo/list/publish/app/##appId##"
-    static let cmsAttachmentDownloadQuery = "servlet/download/##id##/stream"
     static let cmsCategoryDetailQuery = "jaxrs/document/filter/list/##id##/next/##count##"
+    static let cmsDocumentDraftQuery = "jaxrs/document/draft/list/##id##/next/##count##" //PUT 查询草稿 {"categoryIdList":["36783507-3109-4701-a1bd-487e12340af5"],"creatorList":["楼国栋@louguodong@P"],"documentType":"全部"}
+    static let cmsDocumentPost = "jaxrs/document" //保存修改文档 POST
     static let cmsAttachmentListQuery = "jaxrs/fileinfo/list/document/##documentId##"
+    static let cmsAttachmentGET = "jaxrs/fileinfo/##attachId##/document/##documentId##" //附件对象获取
+    static let cmsAttachmentDownloadQuery = "servlet/download/##id##/stream"
+    static let cmsAttachmentDownloadNewQuery = "jaxrs/fileinfo/download/document/##attachId##" //下载附件 GET
+    static let cmsAttachmentUpload = "jaxrs/fileinfo/upload/document/##docId##" //上传附件POST
+    static let cmsAttachmentReplace = "jaxrs/fileinfo/update/document/##docId##/attachment/##attachId##" //替换附件 POST
 }
 
 

+ 61 - 14
o2ios/O2Platform/storyboard/information.storyboard

@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="y4V-fj-0FO">
+<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="y4V-fj-0FO">
     <device id="retina5_5" 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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
@@ -28,19 +27,19 @@
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="标题" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hM4-5Q-bhQ">
-                                            <rect key="frame" x="68" y="23.666666666666671" width="298" height="21"/>
+                                            <rect key="frame" x="68" y="24.333333333333329" width="298" height="21"/>
                                             <fontDescription key="fontDescription" name="PingFangSC-Regular" family="PingFang SC" pointSize="15"/>
                                             <nil key="textColor"/>
                                             <nil key="highlightedColor"/>
                                         </label>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Hog-ko-RY1">
-                                            <rect key="frame" x="374" y="12" width="20" height="43"/>
+                                            <rect key="frame" x="374" y="11.999999999999996" width="20" height="45.666666666666657"/>
                                             <constraints>
                                                 <constraint firstAttribute="width" constant="20" id="swH-KY-Idr"/>
                                             </constraints>
                                         </imageView>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="7dY-cW-DxB">
-                                            <rect key="frame" x="20" y="17" width="40" height="34"/>
+                                            <rect key="frame" x="20" y="16.999999999999996" width="40" height="35.666666666666657"/>
                                             <constraints>
                                                 <constraint firstAttribute="width" constant="40" id="mVO-mr-a7C"/>
                                             </constraints>
@@ -120,18 +119,15 @@
                                                     </constraints>
                                                 </imageView>
                                                 <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="标题" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ACt-c2-0jq">
-                                                    <rect key="frame" x="46" y="11" width="262" height="28"/>
-                                                    <constraints>
-                                                        <constraint firstAttribute="width" constant="262" id="g7d-5D-MIU"/>
-                                                    </constraints>
+                                                    <rect key="frame" x="46" y="11" width="272" height="28"/>
                                                     <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>
-                                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="时间" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NKD-PT-oKe">
-                                                    <rect key="frame" x="312" y="11" width="90" height="28"/>
+                                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2019-07-02" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NKD-PT-oKe">
+                                                    <rect key="frame" x="326" y="11" width="76" height="28"/>
                                                     <constraints>
-                                                        <constraint firstAttribute="width" constant="90" id="exO-JC-rni"/>
+                                                        <constraint firstAttribute="width" constant="76" id="exO-JC-rni"/>
                                                     </constraints>
                                                     <fontDescription key="fontDescription" name="PingFangSC-Light" family="PingFang SC" pointSize="12"/>
                                                     <color key="textColor" red="0.59999999999999998" green="0.59999999999999998" blue="0.59999999999999998" alpha="1" colorSpace="calibratedRGB"/>
@@ -147,6 +143,7 @@
                                                 <constraint firstItem="NKD-PT-oKe" firstAttribute="top" secondItem="wS1-3l-P8w" secondAttribute="topMargin" id="bEJ-hx-1IA"/>
                                                 <constraint firstAttribute="bottomMargin" secondItem="ACt-c2-0jq" secondAttribute="bottom" id="d9z-Pc-CDb"/>
                                                 <constraint firstAttribute="bottomMargin" secondItem="NKD-PT-oKe" secondAttribute="bottom" id="gyK-4K-5po"/>
+                                                <constraint firstItem="NKD-PT-oKe" firstAttribute="leading" secondItem="ACt-c2-0jq" secondAttribute="trailing" constant="8" id="stB-bz-pwo"/>
                                             </constraints>
                                         </tableViewCellContentView>
                                         <connections>
@@ -169,12 +166,34 @@
                     <connections>
                         <outlet property="tableview" destination="kdT-tS-69W" id="Dtu-LN-QWj"/>
                         <segue destination="2ib-gX-E1U" kind="show" identifier="showDetailContentSegue" id="TNK-Su-KhC"/>
+                        <segue destination="5Uu-WA-8de" kind="show" identifier="createDocument" id="6BI-Sx-Xwv"/>
                     </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="nLm-Df-qgf" userLabel="First Responder" sceneMemberID="firstResponder"/>
             </objects>
             <point key="canvasLocation" x="1869.5652173913045" y="131.25"/>
         </scene>
+        <!--Create Doc View Controller-->
+        <scene sceneID="Sr1-t6-CdC">
+            <objects>
+                <viewController id="5Uu-WA-8de" customClass="CMSCreateDocViewController" customModule="O2Platform" customModuleProvider="target" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="PZk-cw-Lo0"/>
+                        <viewControllerLayoutGuide type="bottom" id="GlA-GC-LNc"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="PzZ-FT-7wu">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    </view>
+                    <connections>
+                        <segue destination="2ib-gX-E1U" kind="show" identifier="openDocument" id="uwA-IT-a7g"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dhd-bM-5c8" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="2320" y="951"/>
+        </scene>
         <!--详细内容-->
         <scene sceneID="zPu-Lq-Q4j">
             <objects>
@@ -186,15 +205,40 @@
                     <view key="view" contentMode="scaleToFill" id="Cnt-3V-z9R">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Qxe-kZ-Lvd">
+                                <rect key="frame" x="0.0" y="0.0" width="414" height="2"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="2" id="2b9-L2-vhR"/>
+                                </constraints>
+                            </progressView>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fPd-cZ-6O6">
+                                <rect key="frame" x="0.0" y="64" width="414" height="672"/>
+                                <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            </view>
+                        </subviews>
                         <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+                        <constraints>
+                            <constraint firstItem="JeB-gc-Sjw" firstAttribute="top" secondItem="fPd-cZ-6O6" secondAttribute="bottom" identifier="webViewContainerBottom" id="Bnf-Xg-Gud"/>
+                            <constraint firstItem="Qxe-kZ-Lvd" firstAttribute="leading" secondItem="Cnt-3V-z9R" secondAttribute="leading" id="RmL-cB-4FR"/>
+                            <constraint firstAttribute="trailing" secondItem="fPd-cZ-6O6" secondAttribute="trailing" id="acZ-gL-kTd"/>
+                            <constraint firstItem="Qxe-kZ-Lvd" firstAttribute="top" secondItem="Cnt-3V-z9R" secondAttribute="top" id="gqe-iA-GSq"/>
+                            <constraint firstItem="fPd-cZ-6O6" firstAttribute="top" secondItem="hnc-En-Vnz" secondAttribute="bottom" id="h39-J2-ztE"/>
+                            <constraint firstItem="fPd-cZ-6O6" firstAttribute="leading" secondItem="Cnt-3V-z9R" secondAttribute="leading" id="okd-fn-0G1"/>
+                            <constraint firstAttribute="trailing" secondItem="Qxe-kZ-Lvd" secondAttribute="trailing" id="rgk-w4-X4b"/>
+                        </constraints>
                     </view>
                     <connections>
+                        <outlet property="progressView" destination="Qxe-kZ-Lvd" id="cKb-Sf-iTw"/>
+                        <outlet property="webViewContainer" destination="fPd-cZ-6O6" id="xxD-Eg-vYr"/>
                         <segue destination="ngT-Ox-BPG" kind="presentation" identifier="showInQL" id="v1v-R3-KwY"/>
+                        <segue destination="zQ2-MF-2R5" kind="unwind" identifier="back2DocumentListSegue" unwindAction="viewBack2DocumentList:" id="3ml-5f-efD"/>
                     </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="CWs-X0-yan" userLabel="First Responder" sceneMemberID="firstResponder"/>
+                <exit id="zQ2-MF-2R5" userLabel="Exit" sceneMemberID="exit"/>
             </objects>
-            <point key="canvasLocation" x="2807" y="133"/>
+            <point key="canvasLocation" x="2805.7971014492755" y="132.88043478260872"/>
         </scene>
         <!--Preview View Controller-->
         <scene sceneID="Fqm-g4-6W2">
@@ -258,4 +302,7 @@
             <point key="canvasLocation" x="3537.68115942029" y="131.25"/>
         </scene>
     </scenes>
+    <inferredMetricsTieBreakers>
+        <segue reference="TNK-Su-KhC"/>
+    </inferredMetricsTieBreakers>
 </document>

+ 4 - 4
o2ios/O2Platform/storyboard/task.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" useSafeAreas="YES" colorMatched="YES" initialViewController="Yna-g7-LKp">
+<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="Yna-g7-LKp">
     <device id="retina5_5" 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="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
@@ -670,9 +670,9 @@
     </scenes>
     <resources>
         <image name="icon_daiban" width="38" height="38"/>
-        <image name="todoedStatusIcon" width="96" height="96"/>
+        <image name="todoedStatusIcon" width="32" height="32"/>
     </resources>
     <inferredMetricsTieBreakers>
-        <segue reference="9TQ-Eb-h06"/>
+        <segue reference="QJV-sf-Oi5"/>
     </inferredMetricsTieBreakers>
 </document>