Преглед изворни кода

init: onlyoffice表单模板填写功能

cfort пре 1 година
родитељ
комит
9cc40fdbee

+ 14 - 0
src/api/platform/bpmn/bpmInst.js

@@ -168,6 +168,20 @@ export function startFlowFromList (params) {
     return request({
         url: BPMN_URL() + '/bpm/instance/startFlowFromList',
         method: 'post',
+        isLoading: true,
+        params: params
+    })
+}
+/**
+ * 启动自定义流程按钮【列表】
+ * 带loading提示
+ * @param {*} params
+ */
+export function startFlowFromListLoading (params) {
+    return request({
+        url: BPMN_URL() + '/bpm/instance/startFlowFromList',
+        method: 'post',
+        isLoading: true,
         params: params
     })
 }

+ 61 - 0
src/api/platform/file/onlyoffice.js

@@ -0,0 +1,61 @@
+import request from '@/utils/request'
+import axios from 'axios'
+import { BPMN_URL } from '@/api/baseUrl'
+
+/**
+ * 文件上传
+ * @param {*} file
+ * @param {*} uploadFileVo
+ */
+export function uploadTemplateFile (file, uploadFileVo) {
+    const data = new FormData() // 创建form对象
+    data.append('file', file)
+    // data.append('uploadFileVo', null)
+    return request({
+        url: BPMN_URL() + '/onlyOffice/upload',
+        method: 'post',
+        isLoading: true,
+        gateway: true,
+        data: data,
+        retainData: file
+    })
+}
+
+/**
+ * 删除文件
+ * @param {*} params
+ */
+export function deleteTemplateFile (params) {
+    return request({
+        url: BPMN_URL() + '/onlyOffice/delete',
+        method: 'post',
+        isLoading: true,
+        data: params
+    })
+}
+
+/**
+ * 创建文件
+ * @param {*} params
+ */
+export function createTemplateFile (params) {
+    return request({
+        url: BPMN_URL() + '/onlyOffice/create',
+        method: 'get',
+        isLoading: true,
+        params: params
+    })
+}
+
+/**
+ * 创建文件
+ * @param {*} params
+ */
+export function editTemplateFile (params) {
+    return request({
+        url: BPMN_URL() + '/onlyOffice/editor',
+        method: 'get',
+        isLoading: true,
+        params: params
+    })
+}

+ 3 - 50
src/business/platform/file/attachment/editFile/fileView.vue

@@ -6,9 +6,7 @@
 </template>
 
 <script>
-import {
-    handleDocType
-} from './editor/editor.js'
+import { handleDocType } from './editor/editor.js'
 
 export default {
     name: 'view-file',
@@ -29,64 +27,19 @@ export default {
         option: {
             handler: function (n, o) {
                 this.setEditor(n)
-                this.doctype = handleDocType(n.fileType)
             },
             deep: true
         }
     },
     mounted () {
-        /* 调用初始化方法 ,渲染wps*/
-        if (this.option.url) {
+        if (this.$utils.isNotEmpty(this.option)) {
             this.setEditor(this.option)
         }
     },
     methods: {
         setEditor (option) {
-            this.doctype = handleDocType(option.fileType)
             const config = {
-                document: {
-                    fileType: option.fileType,
-                    key: option.key,
-                    title: option.title,
-                    permissions: {
-                        comment: true,
-                        download: true,
-                        modifyContentControl: true,
-                        modifyFilter: true,
-                        print: true,
-                        edit: option.isEdit,
-                        fillForms: true,
-                        review: true
-                    },
-                    url: option.url
-                },
-                documentType: this.doctype,
-                editorConfig: {
-                    callbackUrl: option.editUrl,
-                    lang: 'zh',
-                    canHistoryRestore: true,
-                    canUseHistory: true,
-                    customization: {
-                        commentAuthorOnly: false,
-                        comments: true,
-                        compactHeader: false,
-                        compactToolbar: true,
-                        plugins: true,
-                        feedback: {
-                            // 隐藏反馈按钮
-                            visible: false
-                        },
-                        // true 表示强制文件保存请求添加到回调处理程序
-                        forcesave: true,
-                        // 取消强制保存,进行手动保存
-                        atuosave: false
-                    },
-                    user: {
-                        id: option.user.id,
-                        name: option.user.name
-                    },
-                    mode: option.mode
-                },
+                ...option,
                 width: '100%',
                 height: document.body.clientHeight + 'px'
             }

+ 651 - 0
src/business/platform/file/attachment/template-selector.vue

@@ -0,0 +1,651 @@
+<template>
+    <div>
+        <ibps-template-attachment-selector
+            ref="attachmentSelector"
+            :items="items"
+            :value="selectorValue"
+            :media-type="mediaType"
+            :media="media"
+            :placeholder="placeholder"
+            :multiple="multiple"
+            :limit="limit"
+            :disabled="disabled"
+            :readonly="readonly"
+            :download="download"
+            :operation-status="operationStatus"
+            :preview="preview"
+            :upload-type="uploadType"
+            :file-size="fileSize"
+            :accept="acceptType"
+            :file-ext="fileExt"
+            @action-event="handleActionEvent"
+        />
+        <!-- 选择器 -->
+        <ibps-uploader-selector-dialog
+            :visible="selectorVisible"
+            :value="selectorValue"
+            :multiple="selectorMultiple"
+            :file-size="fileSize"
+            :accept="acceptType"
+            :file-ext="fileExt"
+            :limit="limit"
+            :upload-method="uploadMethod"
+            @close="visible => selectorVisible = visible"
+            @action-event="handleSelectorActionEvent"
+        />
+        <div v-if="filePreviewVisible">
+            <file-preview
+                :file="attachment"
+                :visible="filePreviewVisible"
+                :option-file="optionFileView"
+                @close="visible => filePreviewVisible = visible"
+            />
+        </div>
+
+        <div v-if="showFile" class="divShow">
+            <fView :option="templateOption" />
+        </div>
+    </div>
+</template>
+<script>
+import { get, transfer, uploadFile } from '@/api/platform/file/attachment'
+import { uploadTemplateFile, editTemplateFile } from '@/api/platform/file/onlyoffice'
+import { downloadFile } from '@/business/platform/file/utils'
+import { remoteRequest, remoteTransRequest } from '@/utils/remote'
+import IbpsTemplateAttachmentSelector from './template'
+import IbpsUploaderSelectorDialog from '@/business/platform/file/uploader/template'
+import FilePreview from '@/business/platform/file/file-preview'
+import { supportFileTypes } from '@/components/ibps-file-viewer/constants'
+import { TRANSFER_DATA } from '@/constant'
+import fView from './editFile/fileView.vue'
+import { SYSTEM_URL, BASE_API } from '@/api/baseUrl'
+
+export default {
+    components: {
+        IbpsTemplateAttachmentSelector,
+        IbpsUploaderSelectorDialog,
+        FilePreview,
+        fView
+    },
+    inject: {
+        elForm: {
+            default: ''
+        },
+        elFormItem: {
+            default: ''
+        }
+    },
+    props: {
+        value: {
+            type: [String, Number, Array, Object]
+        },
+        mediaType: String,
+        media: String,
+        store: {
+            // 存储类型,json: json字符串,id: 只存储id,array: 存储数组数据,arrayId: 字符串类型。
+            type: String,
+            default: 'json',
+            validator: function (value) {
+                return (['json', 'id', 'array', 'arrayId', 'bind'].indexOf(value) !== -1)
+            }
+        },
+        storeSeparator: {
+            // 存储值分割符,对应[多选]有效,对于设置字符串类型的分隔符
+            type: String,
+            default: ','
+        },
+        placeholder: {
+            // 输入框占位文本
+            type: String,
+            default: '请选择附件'
+        },
+        multiple: {
+            // 是否多选
+            type: Boolean,
+            default: false
+        },
+        limit: {
+            // 最大允许上传个数
+            type: Number
+        },
+        // 类型,
+        accept: String,
+        fileExt: {
+            type: Array,
+            default: () => []
+        },
+        disabled: {
+            // 禁用
+            type: Boolean,
+            default: false
+        },
+        readonly: {
+            // 只读
+            type: Boolean,
+            default: false
+        },
+        operationStatus: {
+            // 编辑
+            type: String,
+            default: 'none'
+        },
+        download: {
+            // 允许下载
+            type: Boolean,
+            default: true
+        },
+        preview: {
+            // 允许预览
+            type: Boolean,
+            default: true
+        },
+        fileSize: {
+            // 上传尺寸
+            type: Number
+        },
+        labelKey: {
+            type: String,
+            default: 'filename'
+        },
+        valueKey: {
+            type: String,
+            default: 'filepath'
+        },
+        uploadType: {
+            // 上传方式 ( default:直接打开上传,attachment:ibps上传附件打开上传 )
+            type: String,
+            default: 'attachment'
+        },
+        showExtName: {
+            type: Boolean,
+            default: true
+        },
+        // 上传方法:normal:普通上传,onlyoffice:onlyoffice文件上传
+        uploadMethod: {
+            type: String,
+            default: 'normal'
+        },
+        fileOption: {
+            type: Object,
+            default: () => {}
+        }
+    },
+    data () {
+        return {
+            selectorVisible: false,
+            selectorValue: this.multiple ? [] : {},
+            reselect: false, // 重新选择
+            selectorMultiple: this.multiple,
+            index: -1,
+            cacheData: {},
+            attachment: {},
+            filePreviewVisible: false,
+            acceptType: '',
+            showFile: false,
+            file: '',
+            optionFileView: {},
+            templateOption: this.fileOption
+        }
+    },
+    computed: {
+        items () {
+            if (this.$utils.isEmpty(this.selectorValue)) return []
+            if (this.multiple) {
+                return this.selectorValue.map(data => {
+                    return data[this.labelKey]
+                })
+            } else {
+                return [this.selectorValue[this.labelKey]]
+            }
+        },
+        uploadStyle () {
+            const { width, height } = this
+            return {
+                width: `${width}px`,
+                height: `${height}px`,
+                lineHeight: `${height}px`,
+                display: 'inline'
+            }
+        }
+    },
+    watch: {
+        value (val) {
+            if (this.$utils.isEmpty(this.value)) {
+                this.selectorValue = []
+            } else if (val) {
+                this.initData()
+            }
+        },
+        mediaType: {
+            handler: function (val, oldVal) {
+                if (val === 'custom') {
+                    var arr = this.media.split(',')
+                    const accept = '.' + arr.join(',').replace(/,/g, ',.')
+                    this.acceptType = accept
+                } else {
+                    this.acceptType = this.accept
+                }
+            },
+            immediate: true
+        }
+    },
+    mounted () {
+        this.initData()
+    },
+    methods: {
+        // 替换对应的文件 id  this.optionFile.data.index  为点击编辑时记录的下表号
+        updateFile (data) {
+            if (this.multiple) {
+                this.selectorValue[this.optionFile.data.index].id = data
+            } else {
+                this.selectorValue.id = data
+            }
+            this.handleInput()
+        },
+        /**
+         * 初始化数据
+         */
+        initData () {
+            // const data = this.getArrayValue(this.value)
+            // this.selectorValue = this.multiple ? [] : {}
+            // if (this.$utils.isEmpty(data)) {
+            //     return
+            // }
+            // data.forEach(v => {
+            //     if (this.cacheData[v]) {
+            //         this.setSelectorValue(v)
+            //     } else {
+            //         this.getDataInfo(v)
+            //     }
+            // })
+
+            const data = this.value ? JSON.parse(this.value) : []
+            this.selectorValue = this.multiple ? [data] : data
+        },
+        setCacheData () {
+            if (this.$utils.isEmpty(this.selectorValue)) return
+            const data = this.multiple ? this.selectorValue : [this.selectorValue]
+            data.forEach(v => {
+                this.cacheData[v[this.valueKey]] = v
+            })
+        },
+        setSelectorValue (v) {
+            if (this.multiple) {
+                this.selectorValue.push(this.cacheData[v])
+                const data = this.getArrayValue(this.value)
+                if (this.selectorValue.length === data.length) {
+                    const dataHadSort = []
+                    data.forEach(el => {
+                        const fidData = this.selectorValue.find(fi => fi.id === el)
+                        if (fidData) {
+                            dataHadSort.push(fidData)
+                        }
+                    })
+                    this.selectorValue = dataHadSort
+                }
+            } else {
+                this.selectorValue = JSON.parse(JSON.stringify(this.cacheData[v]))
+            }
+        },
+        /**
+         * 获得数组数据
+         */
+        getArrayValue (value, bindId) {
+            if (this.$utils.isEmpty(value)) {
+                return []
+            }
+            if (this.store === 'json') {
+                // json
+                return this.parseJsonData(value)
+            } else if (this.store === 'id') {
+                // id
+                // 可能是json数据[之前存储的josn格式]
+                if (this.$utils.isJSON(value)) {
+                    return this.parseJsonData(value)
+                } else {
+                    return this.$utils.isString(value) ? value.split(this.storeSeparator) : []
+                }
+            } else if (this.store === 'bind') {
+                // 绑定id
+                if (this.$utils.isEmpty(bindId)) {
+                    return []
+                }
+                return bindId.split(this.storeSeparator)
+            } else {
+                // array
+                return value.map(d => {
+                    return d[this.valueKey]
+                })
+            }
+        },
+        parseJsonData (value) {
+            try {
+                const data = this.$utils.parseData(value)
+                const result = []
+                if (Array.isArray(data)) {
+                    data.forEach(d => {
+                        const node = d[this.valueKey]
+                        if (node) result.push(node)
+                    })
+                } else {
+                    if (this.$utils.isPlainObject(data)) {
+                        result.push(data[this.valueKey])
+                    } else {
+                        const realData = this.$utils.isString(value) ? value.split(this.storeSeparator) : []
+                        realData.forEach(v => { result.push(v) })
+                    }
+                }
+                return result
+            } catch (error) {
+                console.warn(error)
+                return []
+            }
+        },
+        getStoreValue (value) {
+            const res = []
+            if (this.store === 'json') {
+                // json
+                if (this.$utils.isEmpty(value)) {
+                    return ''
+                }
+                if (this.multiple) {
+                    value.forEach(v => {
+                        const o = {}
+                        o[this.valueKey] = v[this.valueKey]
+                        o[this.labelKey] = v[this.labelKey]
+                        res.push(o)
+                    })
+                    return JSON.stringify(res)
+                } else {
+                    const o = {}
+                    o[this.valueKey] = value[this.valueKey]
+                    o[this.labelKey] = value[this.labelKey]
+                    return JSON.stringify(o)
+                }
+            } else if (this.store === 'id') {
+                if (this.$utils.isEmpty(value)) {
+                    return ''
+                }
+                if (this.multiple) {
+                    value.forEach(v => {
+                        res.push(v[this.valueKey])
+                    })
+                } else {
+                    res.push(value[this.valueKey])
+                }
+                return res.join(this.storeSeparator)
+            } else if (this.store === 'bind') {
+                // 绑定id
+                const res = []
+                const bindIdValue = []
+                value.forEach(v => {
+                    bindIdValue.push(v[this.valueKey])
+                    res.push(v[this.labelKey])
+                })
+                this.bindIdValue = bindIdValue.join(this.storeSeparator)
+
+                return res.join(this.storeSeparator)
+            } else {
+                // 数组 array
+                return value || []
+            }
+        },
+        /**
+         * 通过ID获取数据
+         */
+        getDataInfo (id) {
+            if (TRANSFER_DATA === 'transfer') {
+                this.getTransferData(id)
+            } else {
+                this.getRemoteData(id)
+            }
+        },
+        getTransferData (id) {
+            remoteTransRequest('attachment', id).then(idset => {
+                const ids = Array.from(idset)
+                remoteRequest('attachmentIds', ids, () => {
+                    return this.getRemoteTransFunc(ids)
+                }).then(response => {
+                    const responseData = response.data
+                    const data = responseData[id]
+                    this.setRemoteData(data)
+                }).catch(() => {})
+            })
+        },
+        getRemoteTransFunc (ids) {
+            return new Promise((resolve, reject) => {
+                transfer({ ids: ids }).then(response => {
+                    resolve(response)
+                }).catch((error) => {
+                    reject(error)
+                })
+            })
+        },
+        getRemoteData (id) {
+            remoteRequest('attachment' + this.valueKey, id, () => {
+                return this.getRemoteByIdFunc(id)
+            }).then(response => {
+                const data = response.data
+                this.setRemoteData(data)
+            }).catch(() => {})
+        },
+        getRemoteByIdFunc (id) {
+            return new Promise((resolve, reject) => {
+                get({ attachmentId: id }).then(response => {
+                    resolve(response)
+                }).catch(() => {})
+            })
+        },
+        setRemoteData (data) {
+            if (this.$utils.isNotEmpty(data)) {
+                this.cacheData[data[this.valueKey]] = data
+                this.setSelectorValue(data[this.valueKey])
+            }
+        },
+        getExtName (data) {
+            if (!data) {
+                return ''
+            }
+            if (this.showExtName && data['ext']) {
+                return '.' + data['ext']
+            }
+            return ''
+        },
+        // 事件处理
+        handleActionEvent (action, index, data, type) {
+            this.index = index
+            switch (action) {
+                case 'select': // 选择
+                    this.selectorVisible = true
+                    this.selectorMultiple = this.multiple
+                    this.reselect = false
+                    this.showFile = false
+                    break
+                case 'reselect': // 重新选择
+                    this.selectorVisible = true
+                    this.selectorMultiple = false
+                    this.reselect = true
+                    this.showFile = false
+                    break
+                case 'remove': // 删除
+                    this.handleRemove(index)
+                    this.$refs['attachmentSelector'].init()
+                    break
+                case 'download': // 下载
+                    this.handleDownload(index)
+                    break
+                case 'preview': // 预览
+                    this.handlePreview(index)
+                    break
+                case 'confirm': // 默认上传选择文件
+                    this.handleSelectorActionEvent(action, index)
+                    break
+                case 'edit': // 编辑
+                    this.handleEdit(data, 'edit', '_blank')
+                    break
+            }
+        },
+        // 处理编辑文件
+        async handleEdit (data, mode, openType) {
+            if (this.$utils.isEmpty(this.templateOption) && data.filepath) {
+                const res = await editTemplateFile({ fileName: data.filepath })
+                this.templateOption = res.data
+            }
+            this.templateOption.editorConfig.mode = mode
+            // 新标签页打开
+            if (openType === '_blank') {
+                localStorage.setItem('templateOption', JSON.stringify(this.templateOption))
+                window.open('#/templateView', '_blank')
+            } else {
+                // 表单内打开
+                this.showFile = true
+            }
+        },
+        /**
+         * 处理删除
+         */
+        handleRemove (index) {
+            if (this.multiple) {
+                this.selectorValue.splice(index, 1)
+            } else {
+                this.selectorValue = {} // 当前为清空
+            }
+            this.handleInput()
+        },
+        /**
+         * 处理下载
+         */
+        handleDownload (index) {
+            downloadFile(this.multiple ? this.selectorValue[index] : this.selectorValue)
+        },
+        /**
+         * 处理预览
+         */
+        handlePreview (index) {
+            this.attachment = this.multiple ? this.selectorValue[index] : this.selectorValue
+            if (this.attachment.ext === 'pdf') {
+                this.$nextTick(() => {
+                    // this.$refs.viewer.load(this.url)
+                    const newTab = window.open()
+                    const link = newTab.document.createElement('link')
+                    const url = BASE_API() + SYSTEM_URL() + '/file/download?attachmentId=' + this.attachment.id
+                    link.rel = 'shortcut icon'
+                    link.type = 'image/x-icon'
+                    link.href = 'favicon.ico'
+                    // newTab.document.write('<link rel="icon" type="image/x-icon" href="favicon.ico">')
+                    newTab.document.write(`<title>文件预览页-${this.attachment.fileName}</title>`)
+                    newTab.document.write('<style>body { margin: 0px; }</style>')
+                    newTab.document.head.appendChild(link)
+                    newTab.document.write(`<iframe src="${this.$baseUrl}lib/pdfjs-dist/web/viewer.html?file=${encodeURIComponent(url)}" style="width:100%; height:100%;" frameborder="0";>`)
+                    // this.closeDialog()
+                })
+            } else if (supportFileTypes.includes(this.attachment.ext)) {
+                this.getPreview(index)
+            } else {
+                this.$message.closeAll()
+                this.$message.warning('暂不支持该文件类型预览')
+            }
+        },
+        /**
+         * 处理预览
+         */
+        getPreview (index) {
+            // 1、获取文件数据 及下载流接口
+            // 下载地址
+            this.optionFileView.url = BASE_API() + SYSTEM_URL() + '/file/download?attachmentId=' + this.attachment.id
+            // 回调接口url
+            this.optionFileView.editUrl = BASE_API() + SYSTEM_URL() + '/file/editCallback?fileName=' + this.attachment.fileName + '&fileType=' + this.attachment.ext
+            this.optionFileView.title = this.attachment.fileName // 文件名称
+            this.optionFileView.fileType = this.attachment.ext // 类型
+            this.optionFileView.data = this.attachment // 记录编制的位置,需要替换。
+            this.optionFileView.data.index = index
+            this.filePreviewVisible = true
+        },
+        /**
+         *  确定
+         */
+        handleSelectorActionEvent (buttonKey, data) {
+            if (this.uploadType === 'default' && this.$utils.isNotEmpty(this.limit) && this.multiple && this.limit < data.length) {
+                this.$message({
+                    type: 'warning'
+                })
+                this.$refs['attachmentSelector'].init()
+                return
+            }
+            switch (buttonKey) {
+                case 'confirm': // 确定
+                    this.selectorVisible = false
+                    if (this.reselect) {
+                        if (this.multiple) {
+                            this.selectorValue.splice(this.index, 1, data)
+                        } else {
+                            this.selectorValue = data
+                        }
+                    } else {
+                        this.selectorValue = data
+                    }
+                    this.setCacheData()
+                    this.handleInput()
+                    break
+            }
+        },
+        handleInput () {
+            this.$emit('input', this.getStoreValue(this.selectorValue))
+            // 提供一个返回实体,提供调用
+            this.$emit('callback', this.selectorValue)
+        },
+        /**
+         * 文件上传
+         */
+        httpRequest (options) {
+            const uploadMap = {
+                normal: uploadFile,
+                onlyoffice: uploadTemplateFile
+            }
+            return uploadMap[this.uploadMethod || 'normal'](options.file, {})
+        },
+        handleDelete (file, selectorValue) {
+            this.selectorValue = selectorValue
+        },
+        handleSuccess (response, file, selectorValue) {
+            this.selectorValue = selectorValue.map(item => {
+                item.isActive = false
+                return item
+            })
+        },
+        handleChange (file, selectorValue) {
+            this.selectorValue = selectorValue
+        },
+        // 图片上传数量限制
+        handlePicAmount (files, selectorValue) {
+            if (this.multiple && this.limit) {
+                this.$message.closeAll()
+                this.$message({
+                    message: `图片上传上限${this.limit}张`,
+                    type: 'warning'
+                })
+            }
+        },
+        // 格式、大小限制
+        beforeUpload (file) {
+            const isType = this.accept ? file.type.includes(this.accept) : true
+            if (!isType) {
+                this.$message.closeAll()
+                this.$message.error(`上传图片的格式为${this.accept}`)
+            }
+            const isLimitSize = this.size ? (file.size / 1024 / 1024 < this.size) : true
+            if (!isLimitSize) {
+                this.$message.closeAll()
+                this.$message.error(`上传图片的大小不能超过 ${this.size}M!`)
+            }
+            return isLimitSize && isType
+        }
+    }
+}
+</script>
+<style scoped>
+    .divShow {
+        width: 100%;
+        height: calc(100vh);
+    }
+</style>

+ 365 - 0
src/business/platform/file/attachment/template.vue

@@ -0,0 +1,365 @@
+<template>
+    <div class="ibps-attachment-selector">
+        <template v-if="uploadable">
+            <!--ibps 附件上传方式-->
+            <template v-if="uploadType==='attachment'">
+                <ul
+                    :class="{disabled:disabled}"
+                    class="selector-list"
+                    @click="handleUpload"
+                >
+                    <label>
+                        <div class="plus">+</div>
+                        <div class="selector-empty">{{ placeholder }}</div>
+                    </label>
+                </ul>
+            </template>
+            <!--直接弹出选择文件框-->
+            <template v-else>
+                <el-upload
+                    ref="upload"
+                    :file-list="fileList"
+                    :on-success="handleSuccess"
+                    :on-error="handleError"
+                    :before-upload="beforeUpload"
+                    :http-request="httpRequest"
+                    :multiple="multiple"
+                    :accept="accept"
+                    :show-file-list="false"
+                    action="https://www.bpmhome.cn/post"
+                    name="file"
+                    drag
+                    class="ibps-default-upload"
+                >
+                    <ul
+                        :class="{disabled:disabled}"
+                        class="selector-list"
+                    >
+                        <label>
+                            <div class="plus">+</div>
+                            <div class="selector-empty">{{ placeholder }}</div>
+                        </label>
+                    </ul>
+                </el-upload>
+
+                <!--重选附件-->
+                <el-upload
+                    ref="defaultReselectUpload"
+                    style="display:none;"
+                    action="https://www.bpmhome.cn/post"
+                    :on-success="handleReselectSuccess"
+                    :before-upload="beforeUpload"
+                    :http-request="httpRequest"
+                    :file-list="fileList"
+                    :multiple="false"
+                    :accept="accept"
+                    name="file"
+                    drag
+                />
+            </template>
+        </template>
+
+        <ul
+            v-if="$utils.isNotEmpty(items)"
+            :class="['el-upload-list', 'el-upload-list--' + listType, { 'is-disabled': !editable }]"
+        >
+            <vue-draggable
+                v-model="sortList"
+                v-bind="draggableOptions"
+                class="list-group"
+                @start="isDragging = true"
+                @end="()=>{isDragging= false}"
+                @update="updateSort"
+            >
+                <li
+                    v-for="(file,index) in items"
+                    :key="index"
+                    :class="['el-upload-list__item', focusing ? 'focusing' : '']"
+                    tabindex="0"
+                    @focus="focusing = true"
+                    @blur="focusing = false"
+                    @click.stop="focusing = false"
+                >
+                    <span :file="file">
+                        <a
+                            class="el-upload-list__item-name"
+                            :style="{ marginRight:toolsWidth }"
+                            :title="file"
+                            @click.stop="handlePreview(index)"
+                        >
+                            <i class="el-icon-document" />{{ file | ellipsis }}
+                        </a>
+                        <label class="tools">
+                            <template v-if="operationStatus!='none' && editable">
+                                <el-tooltip effect="dark" content="编辑" placement="bottom-start">
+                                    <el-link type="primary" :underline="false" icon="el-icon-edit" @click.stop="handleEdit(index)">&nbsp;</el-link>
+                                </el-tooltip>
+                            </template>
+                            <el-divider direction="vertical" />
+                            <template v-if="editable">
+                                <!--默认附件上传 -->
+                                <!-- <el-tooltip effect="dark" content="拖拽排序" placement="bottom-start">
+                                    <el-link type="info" :underline="false" icon="ibps-icon-arrows" class="draggable">&nbsp;</el-link>
+                                </el-tooltip>
+                                <el-divider direction="vertical" /> -->
+                                <el-tooltip effect="dark" content="重新选择" placement="bottom-start">
+                                    <el-link type="primary" :underline="false" icon="ibps-icon-undo" @click.stop="handleReselect(index)">&nbsp;</el-link>
+                                </el-tooltip>
+                                <el-divider direction="vertical" />
+                                <el-tooltip effect="dark" content="删除" placement="bottom-start">
+                                    <el-link type="danger" :underline="false" icon="ibps-icon-delete" @click.stop="handleRemove(index)">&nbsp;</el-link>
+                                </el-tooltip>
+                                <el-divider v-if="download" direction="vertical" />
+                            </template>
+                            <template v-if="download">
+                                <el-tooltip effect="dark" content="下载" placement="bottom-start">
+                                    <el-link type="primary" :underline="false" icon="ibps-icon-download" @click.stop="handleDownload(index)">&nbsp;</el-link>
+                                </el-tooltip>
+                            </template>
+                        </label>
+                    </span>
+                </li>
+            </vue-draggable>
+        </ul>
+    </div>
+</template>
+<script>
+
+import { fileTypes, allFileTypes, accept as acceptTypes } from '@/business/platform/file/constants/fileTypes'
+import { uploadTemplateFile } from '@/api/platform/file/onlyoffice'
+import VueDraggable from 'vuedraggable'
+
+export default {
+    name: 'ibps-attachment-selector',
+    components: { VueDraggable },
+    filters: {
+        ellipsis (val) {
+            if (!val) return ''
+            if (val.length > 60) {
+                return val.slice(0, 60) + '...'
+            }
+            return val
+        }
+    },
+    props: {
+        items: {
+            type: Array
+        },
+        operationStatus: String, // 编辑
+        value: {
+            type: [Array, Object]
+        },
+        mediaType: String,
+        media: String,
+        placeholder: {
+            type: String,
+            default: '请选择'
+        },
+        multiple: { // 是否多选
+            type: Boolean,
+            default: false
+        },
+        limit: { // 最大允许上传个数
+            type: Number
+        },
+        disabled: {
+            type: Boolean,
+            default: false
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        },
+        download: {
+            type: Boolean,
+            default: false
+        },
+        preview: {
+            type: Boolean,
+            default: true
+        },
+        uploadType: { // 上传方式 ( default:直接打开上传,attachment:ibps上传附件打开上传 )
+            type: String,
+            default: 'attachment'
+        },
+        accept: String, // 允许上传类型
+        fileSize: Number, // 尺寸
+        fileExt: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data () {
+        return {
+            fileList: [],
+            targetExt: false,
+            fileTypes: fileTypes,
+            allFileTypes: allFileTypes,
+            acceptTypes: acceptTypes,
+            defaultSelectIndex: 0,
+            listType: 'text',
+            focusing: false,
+            defaultDisabled: true,
+            sortList: [],
+            isDragging: false,
+            draggableOptions: {
+                handle: '.draggable',
+                ghostClass: 'sortable-ghost',
+                distance: 1,
+                disabled: false,
+                animation: 200,
+                axis: 'y'
+            }
+        }
+    },
+    computed: {
+        // 是否允许上传
+        uploadable () {
+            if (this.readonly) return false
+            if (!this.multiple && this.items.length >= 1) return false
+            if (this.multiple && (this.limit && this.items.length >= this.limit)) return false
+            return true
+        },
+        editable () {
+            return !(this.readonly || this.disabled)
+        },
+        toolsWidth () {
+            const length = this.editable ? 3 : 1
+            return (30 * length) + 'px'
+        }
+    },
+    methods: {
+        init () {
+            this.fileList = []
+        },
+        handleUpload () {
+            if (!this.editable) return
+            this.$emit('action-event', 'select')
+        },
+        // 在线编辑报表
+        handleEdit (index) {
+            this.$emit('action-event', 'edit', index, this.value[index] || this.value, this.operationStatus)
+        },
+        handleReselect (index) {
+            if (!this.editable) return
+            if (this.uploadType === 'attachment') {
+                this.$emit('action-event', 'reselect', index)
+            } else {
+                this.$refs.defaultReselectUpload.$refs['upload-inner'].handleClick()
+                this.defaultSelectIndex = index
+            }
+        },
+        handleRemove (index) {
+            if (!this.editable) return
+            this.$emit('action-event', 'remove', index)
+        },
+        handleDownload (index) {
+            this.$emit('action-event', 'download', index)
+        },
+        handlePreview (index) {
+            this.$emit('action-event', 'preview', index)
+        },
+        // 默认上传模块
+        httpRequest (options) {
+            return uploadTemplateFile(options.file, {})
+        },
+        handleSuccess (response, file, fileList) {
+            this.fileList = fileList
+            this.emitCallback(fileList, file)
+        },
+        emitCallback (list, file) {
+            let data = this.multiple ? [] : {}
+            if (this.multiple) {
+                data = this.getFileDataList(list)
+            } else {
+                data = file.response.data
+            }
+            this.$emit('action-event', 'confirm', data)
+        },
+        getFileDataList (fileList) {
+            if (this.$utils.isEmpty(fileList)) {
+                return []
+            }
+            return fileList.map((file) => {
+                return file.response.data
+            })
+        },
+        handleError () {
+            // TODO:
+        },
+        beforeUpload (file) {
+            if (this.limit && this.limit === 0) {
+                this.$message({
+                    message: '上传文件个数不能为0,请重新设置',
+                    type: 'warning'
+                })
+                return false
+            }
+            if (this.fileSize && file.size > this.fileSize) {
+                this.$message.closeAll()
+                this.$message({
+                    message: `上传文件的尺寸大于${this.$utils.formatSize(this.fileSize, 2, ['B', 'K', 'M', 'G', 'TB'])}`,
+                    type: 'warning'
+                })
+                return false
+            }
+            if (this.accept && !this.fileExtType(file)) {
+                this.$message.closeAll()
+                this.$message({
+                    message: '不允许的文件类型',
+                    type: 'warning'
+                })
+                return false
+            }
+        },
+        /**
+         * 文件类型的检测
+         */
+        fileExtType (file) {
+            const accept = this.accept
+            const acceptTypes = this.acceptTypes
+            const fileTypes = this.fileTypes
+            const arr = file.name.split('.')
+            const result = arr[arr.length - 1]
+            let type = ''
+            this.targetExt = false
+            for (const i in acceptTypes) {
+                if (acceptTypes[i] === accept) {
+                    type = i
+                }
+            }
+            if (type !== '' && this.accept !== '*') {
+                // 现存的附件类型
+                const targetFileTypes = fileTypes[type]
+                this.targetExt = targetFileTypes.includes(result)
+            } else {
+                if (this.accept === '*') {
+                    // 不限制
+                    this.targetExt = true
+                } else {
+                    // 自定义
+                    const targetFileTypes = this.accept.split(',')
+                    this.targetExt = targetFileTypes.includes('.' + result)
+                }
+            }
+            return this.targetExt
+        },
+        handleReselectSuccess (response, file, fileList) {
+            this.fileList = fileList
+            const targetFile = file.response.data
+            var data = this.value
+            if (this.multiple) {
+                data.splice(this.defaultSelectIndex, 1, targetFile)
+            } else {
+                data = targetFile
+            }
+            this.$emit('action-event', 'confirm', data)
+        },
+        updateSort ({ to, from, item, clone, oldIndex, newlndex }) {
+            // console.log(to, from, item, clone, oldIndex, newlndex)
+            // console.log(this.value)
+            // console.log(this.sortList)
+        }
+    }
+}
+</script>

+ 254 - 0
src/business/platform/file/uploader/template.vue

@@ -0,0 +1,254 @@
+<template>
+    <el-dialog
+        v-if="dialogVisible"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :title="title"
+        :top="marginTop"
+        append-to-body
+        custom-class="ibps-uploader-dialog"
+        @close="closeDialog"
+    >
+        <el-tabs
+            v-model="activeName"
+            class="uploader-tab"
+        >
+            <el-tab-pane label="当前上传附件" name="upload">
+                <upload
+                    ref="upload"
+                    :multiple="multiple"
+                    :file-size="size"
+                    :accept="acceptRule"
+                    :height="height"
+                    :init="dialogVisible"
+                    :limit="limit"
+                    :upload-method="uploadMethod"
+                    @callback="uploadCallback"
+                />
+            </el-tab-pane>
+        </el-tabs>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { fileTypes, allFileTypes, accept as acceptTypes } from '@/business/platform/file/constants/fileTypes'
+import upload from './upload'
+import online from './online'
+
+export default {
+    components: {
+        upload,
+        online
+    },
+    props: {
+        value: {
+            type: [String, Number, Array, Object]
+        },
+        multiple: {
+            type: Boolean,
+            default: false
+        },
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        title: {
+            type: String,
+            default: '文件上传'
+        },
+        marginTop: {
+            type: String,
+            default: '5vh'
+        },
+        height: {
+            type: String,
+            default: '400px'
+        },
+        fileSize: [Number, String],
+        limit: {
+            type: Number
+        },
+        accept: {
+            type: String,
+            default: ''
+        },
+        fileExt: {
+            type: Array,
+            default: () => []
+        },
+        // 上传类型:normal:普通上传,onlyoffice:onlyoffice文件上传
+        uploadMethod: {
+            type: String,
+            default: 'onlyoffice'
+        }
+    },
+    data () {
+        return {
+            dialogVisible: this.visible,
+            activeName: 'upload',
+            buttonKey: 'confirm',
+            format: true,
+            fileTypes: fileTypes,
+            allFileTypes: allFileTypes,
+            acceptTypes: acceptTypes,
+            size: null,
+            targetExt: false,
+            acceptRule: '',
+            uploadFileList: [],
+            onlineFileList: [],
+            fileList: this.multiple ? [] : {},
+            toolbars: [
+                { key: 'confirm', type: 'primary', label: '文件上传' },
+                { key: 'cancel' }
+            ]
+        }
+    },
+    watch: {
+        visible: {
+            handler: function (val, oldVal) {
+                this.dialogVisible = this.visible
+                this.activeName = 'upload'
+                this.uploadFileList = []
+            },
+            immediate: true
+        },
+        accept: {
+            handler: function (val, oldVal) {
+                if (val === '*' && this.fileExt.length !== 0) {
+                    const str = '.' + this.fileExt.join(',').replace(/,/g, ',.')
+                    this.acceptRule = str
+                } else {
+                    this.acceptRule = val
+                }
+            },
+            immediate: true
+        },
+        fileSize: {
+            handler: function (val, oldVal) {
+                if (this.$utils.isNotEmpty(val)) {
+                    this.size = typeof val === 'number' ? val * 1024 * 1024 : parseFloat(val) * 1024 * 1024
+                }
+            },
+            immediate: true
+        }
+    },
+    methods: {
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'confirm':
+                    this.handleConfirm()
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        uploadCallback (data) {
+            this.uploadFileList = data
+            if (this.multiple) {
+                this.fileList = this.$utils.isNotEmpty(this.value) ? [...this.value, ...this.uploadFileList] : this.uploadFileList
+            } else {
+                this.onlineFileList = []
+                this.fileList = this.uploadFileList
+            }
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            this.uploadFileList = []
+            this.onlineFileList = []
+            this.$emit('close', false)
+        },
+        /**
+         * 文件类型的限制
+         */
+        fileExtType () {
+            let targetFileTypes = []
+            this.targetExt = false
+            if (this.accept === '*') {
+                // 不限制
+                targetFileTypes = '*'
+            } else {
+                // 自定义
+                targetFileTypes = this.accept.split(',')
+            }
+            const fileList = this.multiple ? this.fileList : [this.fileList]
+            if (targetFileTypes !== '*') {
+                this.targetExt = fileList.every(i => {
+                    // console.log(targetFileTypes, `.${i.ext}`)
+                    return targetFileTypes.includes(`.${i.ext}`)
+                })
+            } else {
+                this.targetExt = true
+            }
+            // console.log(fileList, this.targetExt)
+            return this.targetExt
+        },
+        handleConfirm () {
+            const arr = []
+            if (!this.multiple) {
+                if (this.$utils.isNotEmpty(this.uploadFileList)) {
+                    arr.push(this.uploadFileList)
+                    this.fileList = this.uploadFileList
+                }
+                if (this.$utils.isNotEmpty(this.onlineFileList)) {
+                    arr.push(this.onlineFileList)
+                    this.fileList = this.onlineFileList
+                }
+            }
+            if (this.$utils.isNotEmpty(arr) && arr.length > 1) {
+                this.$message.closeAll()
+                this.$message({
+                    message: '附件上传设置为单选,选择文件数量只能为1个',
+                    type: 'warning'
+                })
+                return
+            }
+            if (this.$utils.isNotEmpty(this.limit) && this.limit < this.fileList.length) {
+                this.$message.closeAll()
+                this.$message({
+                    message: '超过设置最大上传数量限制' + this.limit,
+                    type: 'warning'
+                })
+                return
+            }
+            if (!this.fileExtType()) {
+                this.$message.closeAll()
+                this.$message({
+                    message: '选择的附件中存在不符合规定类型的文件,请重新选择!',
+                    type: 'warning'
+                })
+                return
+            }
+            if (!this.format) {
+                this.$message.closeAll()
+                this.$message.error('选择文件格式不允许!')
+            } else {
+                if (this.$utils.isEmpty(this.fileList)) {
+                    this.$message.closeAll()
+                    this.$message({
+                        message: '请上传或选择文件,或待文件上传成功后在继续操作!',
+                        type: 'warning'
+                    })
+                    return
+                }
+                this.$emit('action-event', this.buttonKey, this.fileList)
+            }
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.uploader-tab{
+    padding: 0 10px;
+}
+</style>

+ 227 - 224
src/business/platform/file/uploader/upload.vue

@@ -21,16 +21,14 @@
                         icon="el-icon-upload"
                         type="primary"
                         size="mini"
-                        >选择要上传的文件</el-button
-                    >
+                    >选择要上传的文件</el-button>
                     <el-button
                         type="danger"
                         icon="ibps-icon-remove"
                         class="ibps-ml-5"
-                        @click="clearFiles"
                         size="mini"
-                        >清空文件</el-button
-                    >
+                        @click="clearFiles"
+                    >清空文件</el-button>
                 </el-upload>
             </div>
         </div>
@@ -55,242 +53,247 @@
             </el-upload>
 
             <el-dialog :visible.sync="dialogVisible" append-to-body>
-                <img :src="dialogImageUrl" width="100%" alt="" />
+                <img :src="dialogImageUrl" width="100%" alt="">
             </el-dialog>
         </div>
     </div>
 </template>
 <script>
-    import { uploadFile, remove, deleteFile } from '@/api/platform/file/attachment'
-    import { fileTypes, allFileTypes, accept as acceptTypes } from '@/business/platform/file/constants/fileTypes'
-    export default {
-        props: {
-            height: String,
-            init: Boolean,
-            limit: Number, // 个数
-            multiple: Boolean,
-            fileSize: Number, // 尺寸
-            accept: String // 类型
+import { uploadFile, remove, deleteFile } from '@/api/platform/file/attachment'
+import { fileTypes, allFileTypes, accept as acceptTypes } from '@/business/platform/file/constants/fileTypes'
+export default {
+    props: {
+        height: String,
+        init: Boolean,
+        limit: Number, // 个数
+        multiple: Boolean,
+        fileSize: Number, // 尺寸
+        accept: String, // 类型
+        // 上传类型:normal:普通上传,onlyoffice:onlyoffice文件上传
+        uploadMethod: {
+            type: String,
+            default: 'normal'
+        }
+    },
+    data () {
+        return {
+            uploadData: {}, // 可以添加分类、文件等信息
+            fileList: [],
+            dialogVisible: false,
+            targetExt: false,
+            dialogImageUrl: '',
+            fileTypes: fileTypes,
+            allFileTypes: allFileTypes,
+            acceptTypes: acceptTypes
+        }
+    },
+    watch: {
+        init: {
+            handler () {
+                if (this.init) {
+                    this.fileList = []
+                }
+            },
+            immediate: true
+        }
+    },
+    methods: {
+        /**
+         * 文件上传
+         */
+        httpRequest (options) {
+            return uploadFile(options.file, {})
         },
-        data() {
-            return {
-                uploadData: {}, // 可以添加分类、文件等信息
-                fileList: [],
-                dialogVisible: false,
-                targetExt: false,
-                dialogImageUrl: '',
-                fileTypes: fileTypes,
-                allFileTypes: allFileTypes,
-                acceptTypes: acceptTypes
+        // 做文件校验
+        beforeUpload (file) {
+            if (this.$utils.isNotEmpty(this.limit) && this.limit === 0) {
+                this.$message({
+                    message: '上传文件个数不能为0,请重新设置',
+                    type: 'warning'
+                })
+                return false
             }
-        },
-        watch: {
-            init: {
-                handler() {
-                    if (this.init) {
-                        this.fileList = []
-                    }
-                },
-                immediate: true
+            if (this.$utils.isNotEmpty(this.fileSize) && file.size > this.fileSize) {
+                this.$message.closeAll()
+                this.$message({
+                    message: '上传文件的大小大于' + this.$utils.formatSize(this.fileSize),
+                    type: 'warning'
+                })
+                return false
+            }
+            // if (this.$utils.isNotEmpty(this.accept) && this.handleAccpt(file) || this.fileExtType(file)) {
+            if (this.$utils.isNotEmpty(this.accept) && this.accept && !this.fileExtType(file)) {
+                this.$message.closeAll()
+                this.$message({
+                    message: '不允许的文件类型',
+                    type: 'warning'
+                })
+                return false
+            }
+            // console.log(this.fileList, 'beforeUpload')
+            if (!this.multiple && this.$utils.isNotEmpty(this.fileList)) {
+                this.$message.closeAll()
+                this.$message({
+                    message: '附件上传设置为单选,文件上传只能为1个',
+                    type: 'warning'
+                })
+                return false
             }
         },
-        methods: {
-            /**
-             * 文件上传
-             */
-            httpRequest(options) {
-                return uploadFile(options.file, {})
-            },
-            // 做文件校验
-            beforeUpload(file) {
-                if (this.$utils.isNotEmpty(this.limit) && this.limit === 0) {
-                    this.$message({
-                        message: '上传文件个数不能为0,请重新设置',
-                        type: 'warning'
-                    })
-                    return false
-                }
-                if (this.$utils.isNotEmpty(this.fileSize) && file.size > this.fileSize) {
-                    this.$message.closeAll()
-                    this.$message({
-                        message: '上传文件的大小大于' + this.$utils.formatSize(this.fileSize),
-                        type: 'warning'
-                    })
-                    return false
-                }
-                // if (this.$utils.isNotEmpty(this.accept) && this.handleAccpt(file) || this.fileExtType(file)) {
-                if (this.$utils.isNotEmpty(this.accept) && this.accept && !this.fileExtType(file)) {
-                    this.$message.closeAll()
-                    this.$message({
-                        message: '不允许的文件类型',
-                        type: 'warning'
-                    })
-                    return false
-                }
-                // console.log(this.fileList, 'beforeUpload')
-                if (!this.multiple && this.$utils.isNotEmpty(this.fileList)) {
-                    this.$message.closeAll()
-                    this.$message({
-                        message: '附件上传设置为单选,文件上传只能为1个',
-                        type: 'warning'
-                    })
-                    return false
+        /**
+         * 文件类型的检测
+         */
+        fileExtType (file) {
+            const accept = this.accept
+            const acceptTypes = this.acceptTypes
+            const fileTypes = this.fileTypes
+            const arr = file.name.split('.')
+            const result = arr[arr.length - 1]
+            let type = ''
+            this.targetExt = false
+            for (const i in acceptTypes) {
+                if (acceptTypes[i] === accept) {
+                    type = i
                 }
-            },
-            /**
-             * 文件类型的检测
-             */
-            fileExtType(file) {
-                const accept = this.accept
-                const acceptTypes = this.acceptTypes
-                const fileTypes = this.fileTypes
-                const arr = file.name.split('.')
-                const result = arr[arr.length - 1]
-                let type = ''
-                this.targetExt = false
-                for (const i in acceptTypes) {
-                    if (acceptTypes[i] === accept) {
-                        type = i
-                    }
-                }
-                if (type !== '' && this.accept !== '*') {
-                    // 现存的附件类型
-                    const targetFileTypes = fileTypes[type]
-                    this.targetExt = targetFileTypes.includes(result)
+            }
+            if (type !== '' && this.accept !== '*') {
+                // 现存的附件类型
+                const targetFileTypes = fileTypes[type]
+                this.targetExt = targetFileTypes.includes(result)
+            } else {
+                if (this.accept === '*') {
+                    // 不限制
+                    this.targetExt = true
                 } else {
-                    if (this.accept === '*') {
-                        // 不限制
-                        this.targetExt = true
-                    } else {
-                        // 自定义
-                        const targetFileTypes = this.accept.split(',')
-                        this.targetExt = targetFileTypes.includes('.' + result)
-                    }
+                    // 自定义
+                    const targetFileTypes = this.accept.split(',')
+                    this.targetExt = targetFileTypes.includes('.' + result)
                 }
-                return this.targetExt
-            },
-            /**
-             * 文件类型的限制
-             */
-            // handleAccpt (file) {
-            //     const rExt = /\.\w+$/
-            //     let accept = ''
-            //     const arr = []
-            //     const extensions = this.accept.split(',')
-            //     const acceptTypes = this.acceptTypes
-            //     const acceptArr = []
-            //     for (var i in acceptTypes) {
-            //         acceptArr.push(acceptTypes[i])
-            //     }
-            //     const inAcceptTypes = acceptArr.includes(this.accept)
+            }
+            return this.targetExt
+        },
+        /**
+         * 文件类型的限制
+         */
+        // handleAccpt (file) {
+        //     const rExt = /\.\w+$/
+        //     let accept = ''
+        //     const arr = []
+        //     const extensions = this.accept.split(',')
+        //     const acceptTypes = this.acceptTypes
+        //     const acceptArr = []
+        //     for (var i in acceptTypes) {
+        //         acceptArr.push(acceptTypes[i])
+        //     }
+        //     const inAcceptTypes = acceptArr.includes(this.accept)
 
-            //     for (let i = 0, len = extensions.length; i < len; i++) {
-            //         const item = extensions[i]
-            //         if (item) {
-            //             if (item.indexOf('/')) {
-            //                 const v = item.split('/')
-            //                 let k = item
-            //                 if (v.length > 0) {
-            //                     k = v[v.length - 1]
-            //                 }
-            //                 arr.push(k)
-            //             } else {
-            //                 arr.push(item)
-            //             }
-            //         }
-            //     }
-            //     if (arr.length) {
-            //         accept = '\\.' + arr.join(',').replace(/,/g, '$|\\.').replace(/\*/g, '.*') + '$'
-            //     }
-            //     accept = new RegExp(accept, 'i')
-            //     // 如果名字中有后缀,才做后缀白名单处理。
-            //     return !file || !file.size || this.accept && rExt.exec(file.name) && !accept.test(file.name) && inAcceptTypes
-            // },
-            handleSuccess(response, file, fileList) {
-                let ext = this.getExtName(file.name)
-                let url = ''
-                if (this.$utils.isEmpty(ext)) {
-                    ext = 'file'
-                }
-                if (['jpg', 'jpeg', 'bmp', 'png'].includes(ext)) {
-                    url = file.url
-                } else {
-                    url = `${this.$baseUrl}images/file/${ext}.png`
-                }
-                file.url = url
-                this.fileList = fileList
-                this.emitCallback(fileList)
-            },
-            // 获取扩展名
-            getExtName(name) {
-                return name ? name.substring(name.lastIndexOf('.') + 1, name.length) : ''
-            },
-            handleError(err, file, fileList) {
-                this.fileList = fileList
-                if (!(err instanceof Error)) {
-                    const data = this.$utils.parseJSON(err.message)
-                    this.$message.closeAll()
-                    this.$message({
-                        message: this.$utils.isNotEmpty(data.message) ? data.message : data.cause,
-                        type: 'error'
-                    })
-                }
-            },
-            handleRemove(file, fileList) {
-                this.fileList = fileList
-                if (file && file.response) {
-                    this.handleRemoteRemove(file.response.data.id, () => {
-                        this.emitCallback(fileList)
-                    })
-                }
-            },
-            emitCallback(fileList) {
-                this.$emit('callback', this.convertFileDataList(fileList, this.multiple))
-            },
-            convertFileDataList(fileList, multiple) {
-                if (this.$utils.isEmpty(fileList)) {
-                    return multiple ? [] : {}
-                }
-                const rtn = []
-                fileList.forEach((file) => {
-                    if (this.$utils.isNotEmpty(file.response)) {
-                        rtn.push(file.response.data)
-                    }
+        //     for (let i = 0, len = extensions.length; i < len; i++) {
+        //         const item = extensions[i]
+        //         if (item) {
+        //             if (item.indexOf('/')) {
+        //                 const v = item.split('/')
+        //                 let k = item
+        //                 if (v.length > 0) {
+        //                     k = v[v.length - 1]
+        //                 }
+        //                 arr.push(k)
+        //             } else {
+        //                 arr.push(item)
+        //             }
+        //         }
+        //     }
+        //     if (arr.length) {
+        //         accept = '\\.' + arr.join(',').replace(/,/g, '$|\\.').replace(/\*/g, '.*') + '$'
+        //     }
+        //     accept = new RegExp(accept, 'i')
+        //     // 如果名字中有后缀,才做后缀白名单处理。
+        //     return !file || !file.size || this.accept && rExt.exec(file.name) && !accept.test(file.name) && inAcceptTypes
+        // },
+        handleSuccess (response, file, fileList) {
+            let ext = this.getExtName(file.name)
+            let url = ''
+            if (this.$utils.isEmpty(ext)) {
+                ext = 'file'
+            }
+            if (['jpg', 'jpeg', 'bmp', 'png'].includes(ext)) {
+                url = file.url
+            } else {
+                url = `${this.$baseUrl}images/file/${ext}.png`
+            }
+            file.url = url
+            this.fileList = fileList
+            this.emitCallback(fileList)
+        },
+        // 获取扩展名
+        getExtName (name) {
+            return name ? name.substring(name.lastIndexOf('.') + 1, name.length) : ''
+        },
+        handleError (err, file, fileList) {
+            this.fileList = fileList
+            if (!(err instanceof Error)) {
+                const data = this.$utils.parseJSON(err.message)
+                this.$message.closeAll()
+                this.$message({
+                    message: this.$utils.isNotEmpty(data.message) ? data.message : data.cause,
+                    type: 'error'
                 })
-                return multiple ? rtn : rtn[rtn.length - 1]
-            },
-            /**
-             * 清空
-             */
-            clearFiles() {
-                const ids = this.$refs.upload.uploadFiles.map((file) => {
-                    // console.log(file.response.data.id)
-                    return file.response.data.id
-                }).join(',')
-
-                if (this.$utils.isEmpty(ids)) {
-                    this.$message.warning('请先上传文件!')
-                    return
-                }
-                this.handleRemoteRemove(ids, () => {
-                    this.$refs.upload.clearFiles()
-                    this.$emit('callback', this.multiple ? [] : {})
+            }
+        },
+        handleRemove (file, fileList) {
+            this.fileList = fileList
+            if (file && file.response) {
+                this.handleRemoteRemove(file.response.data.id, () => {
+                    this.emitCallback(fileList)
                 })
-            },
-            handleRemoteRemove(ids, callback) {
-                // 删除附件文件
-                deleteFile({ attachmentIds: ids }).then(() => {
-                    const _this = this
-                    // this.fileList = []
-                    callback(_this)
-                }).catch(() => {})
-            },
-            handlePreview(file) {
-                this.dialogVisible = true
-                //  this.dialogImageUrl = file.url
             }
+        },
+        emitCallback (fileList) {
+            this.$emit('callback', this.convertFileDataList(fileList, this.multiple))
+        },
+        convertFileDataList (fileList, multiple) {
+            if (this.$utils.isEmpty(fileList)) {
+                return multiple ? [] : {}
+            }
+            const rtn = []
+            fileList.forEach((file) => {
+                if (this.$utils.isNotEmpty(file.response)) {
+                    rtn.push(file.response.data)
+                }
+            })
+            return multiple ? rtn : rtn[rtn.length - 1]
+        },
+        /**
+         * 清空
+         */
+        clearFiles () {
+            const ids = this.$refs.upload.uploadFiles.map((file) => {
+                // console.log(file.response.data.id)
+                return file.response.data.id
+            }).join(',')
+
+            if (this.$utils.isEmpty(ids)) {
+                this.$message.warning('请先上传文件!')
+                return
+            }
+            this.handleRemoteRemove(ids, () => {
+                this.$refs.upload.clearFiles()
+                this.$emit('callback', this.multiple ? [] : {})
+            })
+        },
+        handleRemoteRemove (ids, callback) {
+            // 删除附件文件
+            deleteFile({ attachmentIds: ids }).then(() => {
+                const _this = this
+                // this.fileList = []
+                callback(_this)
+            }).catch(() => {})
+        },
+        handlePreview (file) {
+            this.dialogVisible = true
+            //  this.dialogImageUrl = file.url
         }
     }
+}
 </script>
 <style lang="scss">
     .ibps-uplpad {
@@ -333,4 +336,4 @@
             width: 100%;
         }
     }
-</style>
+</style>

+ 35 - 41
src/components/ibps-tree/index.vue

@@ -88,15 +88,23 @@
                     @node-contextmenu="handleNodeContextmenu"
                 >
                     <span slot-scope="scope" class="ibps-custom-tree-node" :title="scope.node.label">
+                        <!-- <img
+                            v-if="['FILE_TYPE', 'FLOW_TYPE'].includes(categoryKey) && scope.data.categoryKey"
+                            :src="filePng"
+                            style="vertical-align: middle; height: 20px;"
+                        > -->
+                        <template v-if="['FILE_TYPE', 'FLOW_TYPE'].includes(categoryKey)">
+                            <ibps-icon
+                                v-if="scope.data.templateId"
+                                style="font-size: 12px;"
+                                name="file-text-o"
+                            />
+                            <ibps-icon v-else :name="getFolderIcon(scope)" />
+                        </template>
                         <ibps-icon
-                            v-if="showIcon"
+                            v-else-if="showIcon"
                             :name="getIcon(scope.data)"
                         />
-                        <img
-                            v-if="categoryKey === 'FILE_TYPE' || categoryKey ==='FLOW_TYPE'"
-                            :src="filePng"
-                            style="vertical-align: middle; height: 20px;"
-                        >
                         <span>{{ scope.node.label }}</span>
                     </span>
                 </el-tree>
@@ -218,6 +226,7 @@ export default {
             contextmenuList: [],
             zIndex: 2003,
             treeExpandData: [],
+            activedId: '',
             filePng
         }
     },
@@ -268,13 +277,9 @@ export default {
         },
         expandCollapseIcon () {
             if (this.position === 'west') {
-                return this.isExpand
-                    ? 'angle-double-left'
-                    : 'angle-double-right'
+                return this.isExpand ? 'angle-double-left' : 'angle-double-right'
             } else {
-                return this.isExpand
-                    ? 'angle-double-right'
-                    : 'angle-double-left'
+                return this.isExpand ? 'angle-double-right' : 'angle-double-left'
             }
         }
     },
@@ -317,13 +322,7 @@ export default {
          * 判斷tree是否展开
          */
         judgeTitle () {
-            if (this.title === '业务对象管理' ||
-                this.title === undefined ||
-                (
-                    this.treeData[0] && this.treeData[0].children && this.treeData[0].children.length === 1 &&
-                    this.title === '部门管理'
-                )
-            ) {
+            if (this.title === '业务对象管理' || this.title === undefined || (this.treeData[0] && this.treeData[0].children && this.treeData[0].children.length === 1 && this.title === '部门管理')) {
                 return true
             }
             return this.lazy
@@ -335,9 +334,7 @@ export default {
             this.zIndex = PopupManager.getZIndex()
         },
         getIcon (data) {
-            let icon = data
-                ? data[this.treeOptions['iconKey'] || 'icon']
-                : 'list-alt'
+            let icon = data ? data[this.treeOptions['iconKey'] || 'icon'] : 'list-alt'
             if (icon) {
                 return icon
             }
@@ -347,6 +344,11 @@ export default {
                 icon = this.treeOptions['nodeIcon'] || 'list-alt'
             }
         },
+        getFolderIcon ({ data, node }) {
+            const { expanded, childNodes = [] } = node
+            const { id } = data
+            return (expanded && childNodes.length) || id === this.activedId ? 'folder-open-o' : 'folder-o'
+        },
         handleTreeHeight () {
             this.treeHeight = this.height
             if (this.$refs.header) {
@@ -378,6 +380,7 @@ export default {
             }
         },
         handleNodeClick (data) {
+            this.activedId = data.id
             this.$emit('node-click', data)
         },
         refreshNode (id) {
@@ -392,23 +395,9 @@ export default {
             if (!this.contextmenus || this.contextmenus.length === 0) return
             let target = event.target
             let flag = false
-            if (
-                (target &&
-                    target.className.indexOf('el-tree-node__content') > -1) ||
-                (target &&
-                    target.className.indexOf('ibps-custom-tree-node') > -1)
-            ) {
+            if ((target && target.className.indexOf('el-tree-node__content') > -1) || (target && target.className.indexOf('ibps-custom-tree-node') > -1)) {
                 flag = true
-            } else if (
-                (target &&
-                    target.parentNode.className.indexOf(
-                        'el-tree-node__content'
-                    ) > -1) ||
-                (target &&
-                    target.parentNode.className.indexOf(
-                        'ibps-custom-tree-node'
-                    ) > -1)
-            ) {
+            } else if ((target && target.parentNode.className.indexOf('el-tree-node__content') > -1) || (target && target.parentNode.className.indexOf('ibps-custom-tree-node') > -1)) {
                 target = target.parentNode
                 flag = true
             }
@@ -489,7 +478,7 @@ export default {
 }
 </script>
 
-<style lang="scss" >
+<style lang="scss" scoped>
 $border-color: #e5e6e7;
 .ibps-tree {
     .layout-header {
@@ -562,13 +551,18 @@ $border-color: #e5e6e7;
     }
     .ibps-tree-wrapper {
         background: #ffffff;
-        .el-tree > .el-tree-node {
-            display: block;
+        ::v-deep {
+            .el-tree > .el-tree-node {
+                display: block;
+            }
         }
     }
     .ibps-custom-tree-node {
         font-size: 13px;
         padding-right: 8px;
+        .ibps-icon {
+            margin-right: 2px;
+        }
     }
 }
 </style>

+ 198 - 189
src/layout/header-aside/components/header-setting/index.vue

@@ -1,90 +1,99 @@
 <template>
-  <div class="ibps-header-setting">
-    <el-tooltip
-      :content="$t('navbar.setting')"
-      effect="dark"
-      placement="bottom"
-    >
-      <el-button class="btn-text can-hover" type="text" @click="handleClick">
-        <ibps-icon name="cogs" size="14" />
-      </el-button>
-    </el-tooltip>
-    <el-drawer
-      class="ibps-header-setting-drawer"
-      :visible.sync="settingVisible"
-      :title="$t('navbar.setting')"
-      width="30%"
-    >
-      <el-scrollbar
-        style="height: 100%;width:100%;"
-        wrap-class="ibps-header-setting-wrapper ibps-scrollbar-wrapper"
-      >
-        <div class="panel panel-info">
-          <div class="panel-heading">{{ $t('navbar.theme') }}</div>
-          <div class="panel-body">
-            <el-table :data="themeList" :show-header="false" border>
-              <el-table-column width="120">
-                <template slot-scope="scope">{{ $t('layout.header-aside.header-setting.theme.'+scope.row.name) }}</template>
-              </el-table-column>
-              <el-table-column width="100">
-                <div
-                  slot-scope="scope"
-                  :style="{'backgroundImage': `url(${$baseUrl}${scope.row.preview})`}"
-                  class="theme-preview"
-                />
-              </el-table-column>
-              <el-table-column prop="address" align="center">
-                <template slot-scope="scope">
-                  <el-button v-if="activeThemeName === scope.row.name" type="success" icon="el-icon-check" round>{{ $t('layout.header-aside.header-setting.theme.status.activate') }}</el-button>
-                  <el-button v-else round @click="handleSelectTheme(scope.row.name)">{{ $t('layout.header-aside.header-setting.theme.status.select') }}</el-button>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </div>
-        <div class="panel panel-info">
-          <div class="panel-heading">
-            {{ $t('navbar.color') }}
-          </div>
-          <div class="panel-body">
-            <el-color-picker
-              v-model="color"
-              :predefine="predefine"
-              size="mini"
-              @change="handleColorChange"
-            />
-          </div>
-        </div>
-        <div class="panel panel-info">
-          <div class="panel-heading">{{ $t('navbar.language') }}</div>
-          <div class="panel-body">
-            <el-radio-group v-model="language">
-              <el-radio
-                v-for="item in languageList"
-                :key="item.value"
-                :label="item.value"
-              >{{ item.label }}</el-radio>
-            </el-radio-group>
-          </div>
-        </div>
-        <div class="panel panel-info">
-          <div class="panel-heading">{{ $t('navbar.size') }}</div>
-          <div class="panel-body">
-            <el-radio-group v-model="size">
-              <el-radio
-                v-for="item in sizes"
-                :key="item"
-                :label="item"
-              >
-                {{ $t('layout.header-aside.header-setting.size.'+item) }}
-              </el-radio>
-            </el-radio-group>
+    <div class="ibps-header-setting">
+        <el-tooltip
+            :content="$t('navbar.setting')"
+            effect="dark"
+            placement="bottom"
+        >
+            <el-button class="btn-text can-hover" type="text" @click="handleClick">
+                <ibps-icon name="cogs" size="14" />
+            </el-button>
+        </el-tooltip>
+        <el-drawer
+            class="ibps-header-setting-drawer"
+            :visible.sync="settingVisible"
+            :title="$t('navbar.setting')"
+            width="30%"
+        >
+            <el-scrollbar
+                style="height: 100%;width:100%;"
+                wrap-class="ibps-header-setting-wrapper ibps-scrollbar-wrapper"
+            >
+                <div class="panel panel-info">
+                    <div class="panel-heading">{{ $t('navbar.theme') }}</div>
+                    <div class="panel-body">
+                        <el-table :data="themeList" :show-header="false" border>
+                            <el-table-column width="120">
+                                <template slot-scope="scope">{{ $t('layout.header-aside.header-setting.theme.'+scope.row.name) }}</template>
+                            </el-table-column>
+                            <el-table-column width="100">
+                                <div
+                                    slot-scope="scope"
+                                    :style="{'backgroundImage': `url(${$baseUrl}${scope.row.preview})`}"
+                                    class="theme-preview"
+                                />
+                            </el-table-column>
+                            <el-table-column prop="address" align="center">
+                                <template slot-scope="scope">
+                                    <el-button
+                                        v-if="activeThemeName === scope.row.name"
+                                        type="success"
+                                        icon="el-icon-check"
+                                        round
+                                    >{{ $t('layout.header-aside.header-setting.theme.status.activate') }}</el-button>
+                                    <el-button
+                                        v-else
+                                        round
+                                        @click="handleSelectTheme(scope.row.name)"
+                                    >{{ $t('layout.header-aside.header-setting.theme.status.select') }}</el-button>
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </div>
+                </div>
+                <div class="panel panel-info">
+                    <div class="panel-heading">
+                        {{ $t('navbar.color') }}
+                    </div>
+                    <div class="panel-body">
+                        <el-color-picker
+                            v-model="color"
+                            :predefine="predefine"
+                            size="mini"
+                            @change="handleColorChange"
+                        />
+                    </div>
+                </div>
+                <div class="panel panel-info">
+                    <div class="panel-heading">{{ $t('navbar.language') }}</div>
+                    <div class="panel-body">
+                        <el-radio-group v-model="language">
+                            <el-radio
+                                v-for="item in languageList"
+                                :key="item.value"
+                                :label="item.value"
+                            >{{ item.label }}</el-radio>
+                        </el-radio-group>
+                    </div>
+                </div>
+                <div class="panel panel-info">
+                    <div class="panel-heading">{{ $t('navbar.size') }}</div>
+                    <div class="panel-body">
+                        <el-radio-group v-model="size">
+                            <el-radio
+                                v-for="item in sizes"
+                                :key="item"
+                                :label="item"
+                            >
+                                {{ $t('layout.header-aside.header-setting.size.'+item) }}
+                            </el-radio>
+                        </el-radio-group>
 
-          </div>
-        </div>
-      </el-scrollbar>
-    </el-drawer>
-  </div>
+                    </div>
+                </div>
+            </el-scrollbar>
+        </el-drawer>
+    </div>
 </template>
 
 <script>
@@ -93,54 +102,54 @@ import { ELEMENT_COLOR } from '@/constant'
 
 import setting from '@/setting.js'
 export default {
-  data() {
-    return {
-      settingVisible: false,
-      themeDialogVisible: false,
-      languageList: setting.system.languageList,
-      sizes: [
-        'default',
-        'medium',
-        'small',
-        'mini'
-      ],
-      predefine: setting.color.predefine
-    }
-  },
-  computed: {
-    ...mapState({
-      themeList: state => state.ibps.theme.list,
-      activeThemeName: state => state.ibps.theme.activeName,
-      sizeValue: state => state.ibps.size.value,
-      languageValue: state => state.ibps.language.value,
-      colorValue: state => state.ibps.color.value
-    }),
-    language: {
-      get() {
-        return this.languageValue || 'zh-CN'
-      },
-      set(value) {
-        this.handleLanguageChange(value)
-      }
+    data () {
+        return {
+            settingVisible: false,
+            themeDialogVisible: false,
+            languageList: setting.system.languageList,
+            sizes: [
+                'default',
+                'medium',
+                'small',
+                'mini'
+            ],
+            predefine: setting.color.predefine
+        }
     },
-    size: {
-      get() {
-        return this.sizeValue || this.$ELEMENT.size
-      },
-      set(value) {
-        this.handleSizeChange(value)
-      }
+    computed: {
+        ...mapState({
+            themeList: state => state.ibps.theme.list,
+            activeThemeName: state => state.ibps.theme.activeName,
+            sizeValue: state => state.ibps.size.value,
+            languageValue: state => state.ibps.language.value,
+            colorValue: state => state.ibps.color.value
+        }),
+        language: {
+            get () {
+                return this.languageValue || 'zh-CN'
+            },
+            set (value) {
+                this.handleLanguageChange(value)
+            }
+        },
+        size: {
+            get () {
+                return this.sizeValue || this.$ELEMENT.size
+            },
+            set (value) {
+                this.handleSizeChange(value)
+            }
+        },
+        color: {
+            get () {
+                return this.colorValue || ELEMENT_COLOR
+            },
+            set (value) {
+                this.handleColorChange(value)
+            }
+        }
     },
-    color: {
-      get() {
-        return this.colorValue || ELEMENT_COLOR
-      },
-      set(value) {
-        this.handleColorChange(value)
-      }
-    }
-  },
-  watch: {
+    watch: {
     // // 注意 这里是关键
     // // 因为需要访问 this.$ELEMENT 所以只能在这里使用这种方式
     // sizeValue: {
@@ -163,69 +172,69 @@ export default {
     //   immediate: true
     // },
     // 因为需要访问 this.$i18n 所以只能在这里使用这种方式
-    languageValue: {
-      handler(val, oldVal) {
-        if (val === '') {
-          return this.handleLanguageChange(this.$i18n.locale || setting.system.language)
+        languageValue: {
+            handler (val, oldVal) {
+                if (val === '') {
+                    return this.handleLanguageChange(this.$i18n.locale || setting.system.language)
+                }
+                if (oldVal) {
+                    // 这个情况在已经加载完页面 用户改变了国际化时触发
+                    this.$i18n.locale = val
+                    // 由于已经加载过设置 需要清空缓存设置
+                    this.pageKeepAliveClean()
+                    // 由于已经加载过设置 需要刷新此页面
+                    this.$router.replace('/refresh')
+                } else {
+                    // 这个情况在刷新页面时触发
+                    this.$i18n.locale = val
+                }
+            },
+            immediate: true
+        },
+        //
+        colorValue: {
+            handler (val, oldVal) {
+                if (val === '') {
+                    this.handleColorChange(val)
+                }
+            },
+            immediate: true
         }
-        if (oldVal) {
-          // 这个情况在已经加载完页面 用户改变了国际化时触发
-          this.$i18n.locale = val
-          // 由于已经加载过设置 需要清空缓存设置
-          this.pageKeepAliveClean()
-          // 由于已经加载过设置 需要刷新此页面
-          this.$router.replace('/refresh')
-        } else {
-          // 这个情况在刷新页面时触发
-          this.$i18n.locale = val
-        }
-      },
-      immediate: true
+
     },
-    //
-    colorValue: {
-      handler(val, oldVal) {
-        if (val === '') {
-          this.handleColorChange(val)
-        }
-      },
-      immediate: true
-    }
+    methods: {
+        ...mapMutations({
+            pageKeepAliveClean: 'ibps/page/keepAliveClean'
+        }),
+        ...mapActions({
+            themeSet: 'ibps/theme/set',
+            sizeSet: 'ibps/size/set',
+            languageSet: 'ibps/language/set',
+            colorSet: 'ibps/color/set'
+        }),
 
-  },
-  methods: {
-    ...mapMutations({
-      pageKeepAliveClean: 'ibps/page/keepAliveClean'
-    }),
-    ...mapActions({
-      themeSet: 'ibps/theme/set',
-      sizeSet: 'ibps/size/set',
-      languageSet: 'ibps/language/set',
-      colorSet: 'ibps/color/set'
-    }),
+        handleClick () {
+            this.settingVisible = true
+            // this.$refs.setting.$el.blur()
+        },
+        // -----------------主题---------------
+        handleSelectTheme (value) {
+            this.themeSet(value)
+        },
+        // -----------------尺寸---------------
+        handleSizeChange (value) {
+            this.sizeSet(value)
+        },
+        // -----------------语言---------------
+        handleLanguageChange (value) {
+            this.languageSet(value)
+        },
+        // -----------------颜色---------------
+        handleColorChange (value) {
+            this.colorSet(value)
+        }
 
-    handleClick() {
-      this.settingVisible = true
-      // this.$refs.setting.$el.blur()
-    },
-    // -----------------主题---------------
-    handleSelectTheme(value) {
-      this.themeSet(value)
-    },
-    // -----------------尺寸---------------
-    handleSizeChange(value) {
-      this.sizeSet(value)
-    },
-    // -----------------语言---------------
-    handleLanguageChange(value) {
-      this.languageSet(value)
-    },
-    // -----------------颜色---------------
-    handleColorChange(value) {
-      this.colorSet(value)
     }
-
-  }
 }
 </script>
 <style lang="scss" scoped>

+ 8 - 0
src/router/routes.js

@@ -310,6 +310,14 @@ const frameOut = [
         },
         component: _import('/viewFile')
     },
+    {
+        path: '/templateView',
+        name: 'templateView',
+        meta: {
+            title: '模板文件预览页'
+        },
+        component: _import('/viewFile/template')
+    },
     {
         path: '/register',
         name: 'register',

+ 109 - 103
src/utils/tree.js

@@ -10,116 +10,122 @@
  */
 import { defaultsDeep } from 'lodash'
 const defaultSetting = {
-  isParent: 'isParent',
-  childrenKey: 'children',
-  nameKey: 'name',
-  titleKey: 'title',
-  idKey: 'id',
-  pIdKey: 'parentId',
-  levelKey: 'level',
-  rootPId: null
+    isParent: 'isParent',
+    childrenKey: 'children',
+    nameKey: 'name',
+    titleKey: 'title',
+    idKey: 'id',
+    pIdKey: 'parentId',
+    levelKey: 'level',
+    rootPId: null
 }
 const treeUtil = {
-  /**
- * 转换成树形结构
- * @param {*} data 数据
- * @param {*} setting 设置
- */
-  transformToTreeFormat: function(data, setting = {}) {
-    const sNodes = JSON.parse(JSON.stringify(data))
-    setting = defaultsDeep({}, setting, defaultSetting)
-    const idKey = setting.idKey
-    const pIdKey = setting.pIdKey
-    const childrenKey = setting.childrenKey
+    /**
+     * 转换成树形结构
+     * @param {*} data 数据
+     * @param {*} setting 设置
+     */
+    transformToTreeFormat: function (data, setting = {}) {
+        const sNodes = JSON.parse(JSON.stringify(data))
+        setting = defaultsDeep({}, setting, defaultSetting)
+        const idKey = setting.idKey
+        const pIdKey = setting.pIdKey
+        const childrenKey = setting.childrenKey
 
-    let i, j, l
-    if (!idKey || idKey === '' || !sNodes) { return [] }
-    if (sNodes instanceof Array) {
-      const r = []
-      const tmpMap = []
-      for (i = 0, l = sNodes.length; i < l; i++) {
-        tmpMap[sNodes[i][idKey]] = sNodes[i]
-      }
-      for (j = 0, l = sNodes.length; j < l; j++) {
-        if (tmpMap[sNodes[j][pIdKey]] && sNodes[j][idKey] !== sNodes[j][pIdKey]) {
-          if (!tmpMap[sNodes[j][pIdKey]][childrenKey]) {
-            tmpMap[sNodes[j][pIdKey]][childrenKey] = []
-          }
-          tmpMap[sNodes[j][pIdKey]][childrenKey].push(sNodes[j])
+        let i, j, l
+        if (!idKey || idKey === '' || !sNodes) { return [] }
+        if (sNodes instanceof Array) {
+            const r = []
+            const tmpMap = []
+            for (i = 0, l = sNodes.length; i < l; i++) {
+                tmpMap[sNodes[i][idKey]] = sNodes[i]
+            }
+            for (j = 0, l = sNodes.length; j < l; j++) {
+                if (tmpMap[sNodes[j][pIdKey]] && sNodes[j][idKey] !== sNodes[j][pIdKey]) {
+                    if (!tmpMap[sNodes[j][pIdKey]][childrenKey]) {
+                        tmpMap[sNodes[j][pIdKey]][childrenKey] = []
+                    }
+                    tmpMap[sNodes[j][pIdKey]][childrenKey].push(sNodes[j])
+                    if (sNodes[j].subs && sNodes[j].subs.length) {
+                        if (!tmpMap[sNodes[j][idKey]][childrenKey]) {
+                            tmpMap[sNodes[j][idKey]][childrenKey] = []
+                        }
+                        tmpMap[sNodes[j][idKey]][childrenKey].push(...sNodes[j].subs)
+                        // tmpMap[sNodes[j][idKey]][childrenKey] = sNodes[j].subs
+                    }
+                } else {
+                    r.push(sNodes[j])
+                }
+            }
+            return r
         } else {
-          r.push(sNodes[j])
+            return [sNodes]
         }
-      }
-      return r
-    } else {
-      return [sNodes]
-    }
-  },
-  /**
- * 转换成树结构 并设置了层级
- * @param {*} data 数据
- */
-  transformToTreeAndLevelFormat: function(data, setting = {}) {
-    setting = defaultsDeep({}, setting, defaultSetting)
-    const node = treeUtil.transformToTreeFormat(data, setting)
-    // 设置层级
-    for (let i = 0; i < node.length; i++) {
-      treeUtil.setSonNodeLevel(null, node[i], setting)
-    }
-    return node
-  },
-  /**
-   * 树形转换成数组结构
-   *
-   */
-  transformToArrayFormat: function(data, setting = {}) {
-    if (!data) return []
-    function _do(_node) {
-      r.push(_node)
-      const children = treeUtil.nodeChildren(_node, setting)
-      if (children) {
-        r = r.concat(treeUtil.transformToArrayFormat(children, setting))
-      }
-    }
-    const nodes = JSON.parse(JSON.stringify(data))
-    setting = defaultsDeep({}, setting, defaultSetting)
-    let r = []
-    if (nodes instanceof Array) {
-      for (let i = 0, l = nodes.length; i < l; i++) {
-        const node = nodes[i]
-        _do(node)
-      }
-    } else {
-      _do(nodes)
-    }
-    return r
-  },
-  nodeChildren: function(node, setting, newChildren) {
-    if (!node) {
-      return null
-    }
-    const key = setting.childrenKey
-    if (typeof newChildren !== 'undefined') {
-      node[key] = newChildren
-    }
-    return node[key]
-  },
-  /**
-   * 设置儿子节点等级
-   * @param {*} parentNode
-   * @param {*} node
-   */
-  setSonNodeLevel: function(parentNode, node, setting) {
-    if (!node) return
-    const childrenKey = setting.childrenKey
-    const levelKey = setting.levelKey
+    },
+    /**
+     * 转换成树结构 并设置了层级
+     * @param {*} data 数据
+     */
+    transformToTreeAndLevelFormat: function (data, setting = {}) {
+        setting = defaultsDeep({}, setting, defaultSetting)
+        const node = treeUtil.transformToTreeFormat(data, setting)
+        // 设置层级
+        for (let i = 0; i < node.length; i++) {
+            treeUtil.setSonNodeLevel(null, node[i], setting)
+        }
+        return node
+    },
+    /**
+     * 树形转换成数组结构
+     */
+    transformToArrayFormat: function (data, setting = {}) {
+        if (!data) return []
+        function _do (_node) {
+            r.push(_node)
+            const children = treeUtil.nodeChildren(_node, setting)
+            if (children) {
+                r = r.concat(treeUtil.transformToArrayFormat(children, setting))
+            }
+        }
+        const nodes = JSON.parse(JSON.stringify(data))
+        setting = defaultsDeep({}, setting, defaultSetting)
+        let r = []
+        if (nodes instanceof Array) {
+            for (let i = 0, l = nodes.length; i < l; i++) {
+                const node = nodes[i]
+                _do(node)
+            }
+        } else {
+            _do(nodes)
+        }
+        return r
+    },
+    nodeChildren: function (node, setting, newChildren) {
+        if (!node) {
+            return null
+        }
+        const key = setting.childrenKey
+        if (typeof newChildren !== 'undefined') {
+            node[key] = newChildren
+        }
+        return node[key]
+    },
+    /**
+     * 设置儿子节点等级
+     * @param {*} parentNode
+     * @param {*} node
+     */
+    setSonNodeLevel: function (parentNode, node, setting) {
+        if (!node) return
+        const childrenKey = setting.childrenKey
+        const levelKey = setting.levelKey
 
-    node[levelKey] = parentNode ? parentNode[levelKey] + 1 : 0
-    if (!node[childrenKey]) { return }
-    for (let i = 0, l = node[childrenKey].length; i < l; i++) {
-      if (node[childrenKey][i]) { treeUtil.setSonNodeLevel(node, node[childrenKey][i], setting) }
+        node[levelKey] = parentNode ? parentNode[levelKey] + 1 : 0
+        if (!node[childrenKey]) { return }
+        for (let i = 0, l = node[childrenKey].length; i < l; i++) {
+            if (node[childrenKey][i]) { treeUtil.setSonNodeLevel(node, node[childrenKey][i], setting) }
+        }
     }
-  }
 }
 
 export default treeUtil

+ 506 - 0
src/views/business/onlineForm/config.vue

@@ -0,0 +1,506 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        append-to-body
+        width="60%"
+        class="dialog template-config-dialog"
+        top="6vh"
+        @close="closeDialog"
+    >
+        <div class="container">
+            <el-form
+                ref="form"
+                :model="form"
+                :rules="rules"
+                :label-width="formLabelWidth"
+                label-position="left"
+                class="config-form"
+                @submit.native.prevent
+            >
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="部门:" prop="bian_zhi_bu_men_">
+                            <ibps-user-selector
+                                v-model="form.bian_zhi_bu_men_"
+                                type="position"
+                                readonly-text="text"
+                                :disabled="readonly"
+                                :multiple="false"
+                                size="mini"
+                                :filter="filter"
+                                filterable
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="部门分组:" prop="bu_men_fen_zu_">
+                            <ibps-custom-dialog
+                                v-model="form.bu_men_fen_zu_"
+                                template-key="sbbqdhk"
+                                :multiple="false"
+                                :disabled="readonly"
+                                type="dialog"
+                                class="custom-dialog"
+                                icon="el-icon-search"
+                                size="mini"
+                                placeholder="请选择"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="表单名称:" prop="biao_dan_ming_che">
+                            <el-input
+                                v-model="form.biao_dan_ming_che"
+                                clearable
+                                :maxlength="64"
+                                :disabled="readonly"
+                                show-word-limit
+                                size="mini"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="表单模板:" prop="biao_dan_mo_ban_">
+                            <ibps-attachment
+                                v-model="form.biao_dan_mo_ban_"
+                                :download="false"
+                                :multiple="false"
+                                accept=".docx"
+                                :disabled="readonly"
+                                operation-status="saveAdd"
+                                upload-method="onlyoffice"
+                                :file-option="fileOption"
+                                size="mini"
+                                label-key="filename"
+                                value-key="filepath"
+                                store="json"
+                                placeholder="请选择docx格式文档"
+                                @callback="handleFileCallback"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="所属分类:" prop="gui_dang_lu_jing_">
+                            <ibps-type-select
+                                ref="ibpsTypeSelect"
+                                v-model="form.gui_dang_lu_jing_"
+                                :category-key="categoryKey"
+                                :readonly="readonly"
+                                clearable
+                                class="type"
+                                filter-label="name"
+                                :filterable="true"
+                                placeholder="请选择表单模板所属分类"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="备注:" prop="bei_zhu_">
+                            <el-input
+                                v-model="form.bei_zhu_"
+                                type="textarea"
+                                :maxlength="400"
+                                show-word-limit
+                                :rows="2"
+                                :disabled="readonly"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="审批流程配置:">
+                            <div class="button">
+                                <el-button type="success" size="mini" icon="ibps-icon-plus" plain @click="goSubAdd">添加</el-button>
+                                <el-button type="danger" size="mini" icon="ibps-icon-close" plain @click="goSubRemove">删除</el-button>
+                            </div>
+                            <el-table
+                                ref="adjustTable"
+                                :data="processData"
+                                border
+                                stripe
+                                highlight-current-row
+                                style="width: 100%"
+                                class="config-table"
+                                @selection-change="handleSelectionChange"
+                            >
+                                <el-table-column type="selection" width="40" header-align="center" align="center" />
+                                <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                                <el-table-column prop="jie_dian_ming_cheng_" label="节点名称" width="200">
+                                    <template slot-scope="{row}">
+                                        <el-input v-model="row.jie_dian_ming_cheng_" size="mini" />
+                                    </template>
+                                </el-table-column>
+                                <el-table-column prop="fang_shi_" label="方式" width="120">
+                                    <template slot-scope="{row}">
+                                        <el-select v-model="row.fang_shi_" placeholder="请选择" size="mini">
+                                            <el-tooltip
+                                                v-for="item in typeOption"
+                                                :key="item.value"
+                                                effect="dark"
+                                                :content="item.tips"
+                                                placement="right"
+                                            >
+                                                <el-option :label="item.label" :value="item.value" />
+                                            </el-tooltip>
+                                        </el-select>
+                                    </template>
+                                </el-table-column>
+                                <el-table-column prop="yong_hu_lei_xing_" label="用户类型" width="100">
+                                    <template slot-scope="{row}">
+                                        <el-select v-model="row.yong_hu_lei_xing_" placeholder="请选择" size="mini" @change="onCategoryChange(row)">
+                                            <el-option
+                                                v-for="item in userTypeOption"
+                                                :key="item.value"
+                                                :label="item.label"
+                                                :value="item.value"
+                                            />
+                                        </el-select>
+                                    </template>
+
+                                </el-table-column>
+                                <el-table-column prop="chu_li_ren_" label="处理人员/角色" align="center">
+                                    <template slot-scope="{row}">
+                                        <ibps-user-selector
+                                            v-if="row.yong_hu_lei_xing_==='employee'"
+                                            v-model="row.chu_li_ren_"
+                                            size="mini"
+                                            type="user"
+                                            readonly-text="text"
+                                            :disabled="readonly"
+                                            placeholder="请选择人员"
+                                            :multiple="true"
+                                            :filter="filter"
+                                            filterable
+                                        />
+                                        <ibps-role-selector
+                                            v-if="row.yong_hu_lei_xing_==='role'"
+                                            v-model="row.chu_li_ren_"
+                                            :disabled="readonly"
+                                            placeholder="请选择指定角色"
+                                            :multiple="false"
+                                        />
+                                        <span v-if="row.yong_hu_lei_xing_==='all'">/</span>
+                                    </template>
+                                </el-table-column>
+                                <!-- <el-table-column prop="zhi_xing_fang_shi_" label="执行方式" width="100">
+                                    <template slot-scope="{row}">
+                                        <el-select v-model="row.zhi_xing_fang_shi_" placeholder="请选择" size="mini">
+                                            <el-option
+                                                v-for="item in ['或签','与签']"
+                                                :key="item"
+                                                :label="item"
+                                                :value="item"
+                                            />
+                                        </el-select>
+                                    </template>
+                                </el-table-column> -->
+                            </el-table>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </el-form>
+        </div>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar :actions="toolbars" @action-event="handleActionEvent" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import dayjs from 'dayjs'
+import { editTemplateFile } from '@/api/platform/file/onlyoffice'
+
+export default {
+    components: {
+        IbpsCustomDialog: () => import('@/business/platform/data/templaterender/custom-dialog'),
+        ibpsUserSelector: () => import('@/business/platform/org/selector'),
+        IbpsRoleSelector: () => import('@/business/platform/org/role/selector'),
+        IbpsAttachment: () => import('@/business/platform/file/attachment/template-selector'),
+        IbpsTypeSelect: () => import('@/business/platform/cat/type/select')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        },
+        params: {
+            type: Object,
+            default: () => {}
+        },
+        fileOption: {
+            type: Object,
+            default: () => {}
+        }
+    },
+    data () {
+        const { userId, position, level } = this.$store.getters
+        return {
+            userId,
+            position,
+            level: level.second || level.first,
+            dialogVisible: this.visible,
+            loading: false,
+            title: '添加电子表单模板',
+            formLabelWidth: '110px',
+            categoryKey: 'FLOW_TYPE',
+            processData: [],
+            selectionIndex: [],
+            filter: [{
+                descVal: '2',
+                includeSub: true,
+                old: 'position',
+                partyId: this.$store.getters.userInfo.employee.positions,
+                partyName: '',
+                scriptContent: '',
+                type: 'user',
+                userType: 'position'
+            }],
+            form: {
+                biao_dan_ming_che: '',
+                biao_dan_mo_ban_: '',
+                gui_dang_lu_jing_: '',
+                shen_pi_liu_cheng: '',
+                bian_zhi_bu_men_: ''
+            },
+            typeOption: [
+                { label: '提前预设', value: 'presets', tips: '流程流转时,将直接推送到指定人员或角色的待办' },
+                { label: '临时指定', value: 'temp', tips: '流程流转时,由上一节点的操作人在指定人员或角色范围内自行选择流程下一节点的处理人' }
+            ],
+            userTypeOption: [
+                { label: '人员', value: 'employee' },
+                { label: '角色', value: 'role' },
+                { label: '全部', value: 'all' }
+            ],
+            rules: {
+                biao_dan_ming_che: [{ required: true, message: '表单名称不能为空', trigger: 'blur' }],
+                biao_dan_mo_ban_: [{ required: true, message: '表单模板不能为空', trigger: 'blur' }],
+                gui_dang_lu_jing_: [{ required: true, message: '归档路径不能为空', trigger: 'blur' }],
+                shen_pi_liu_cheng: [{ required: true, message: '审批流程不能为空', trigger: 'blur' }],
+                bian_zhi_bu_men_: [{ required: true, message: '部门不能为空', trigger: 'blur' }]
+            },
+            toolbars: [
+                { key: 'save', label: '保存' },
+                { key: 'cancel', label: '关闭', type: 'danger', icon: 'ibps-icon-close' }
+            ]
+        }
+    },
+    watch: {
+        visible: {
+            handler (val) {
+                this.dialogVisible = val
+                if (val) {
+                    this.init()
+                }
+            },
+            immediate: true
+        }
+    },
+    methods: {
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'cancel':
+                    this.closeDialog(true)
+                    break
+                case 'save':
+                    this.handleSave()
+                    break
+                default:
+                    break
+            }
+        },
+        handleSelectionChange (val) {
+            this.selectionData = val
+        },
+        // 用户类型发生改变
+        onCategoryChange (row) {
+            const item = this.processData.find(i => i === row)
+            item.chu_li_ren_ = ''
+        },
+        // 子表添加
+        goSubAdd () {
+            if (this.processData.length >= 4) {
+                return this.$message.warning('超过流程节点的最大限制!')
+            }
+            this.processData.push({
+                jie_dian_ming_cheng_: '',
+                chu_li_ren_: '',
+                // zhi_xing_fang_shi_: '或签',
+                yong_hu_lei_xing_: 'employee',
+                fang_shi_: 'presets'
+            })
+        },
+        // 子表删除
+        goSubRemove () {
+            if (!this.processData.length) {
+                return this.$message.warning('请选择要删除的配置数据!')
+            }
+            this.$confirm('确定要删除选中配置数据吗?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                this.processData.filter(item => !this.selectionData.includes(item))
+                this.selectionData = []
+            }).catch(() => {})
+        },
+        formatData () {
+            const templateInfo = this.form.biao_dan_mo_ban_ ? JSON.parse(this.form.biao_dan_mo_ban_) : {}
+            const formData = {
+                ...this.form,
+                cun_fang_lu_jing_: templateInfo.filepath,
+                liu_cheng_shu_ju_: JSON.stringify(this.processData.map((item, index) => ({
+                    sn: index + 1,
+                    nodeName: item.jie_dian_ming_cheng_,
+                    conditionType: item.fang_shi_,
+                    executeType: item.yong_hu_lei_xing_,
+                    executor: item.chu_li_ren_
+                })))
+            }
+            return formData
+        },
+        handleSave () {
+            const validateResult = this.validateForm()
+            if (validateResult.isError) {
+                return this.$message.warning(validateResult.msgContent)
+            }
+            this.$refs.form.validate((valid) => {
+                if (!valid) {
+                    return false
+                }
+                this.handleSubmitData()
+            })
+        },
+        validateForm () {
+            const result = {
+                isError: false,
+                msgContent: ''
+            }
+            if (!this.processData.length) {
+                return result
+            }
+            for (let i = 0; i < this.processData.length; i++) {
+                const item = this.processData[i]
+                if (!item.jie_dian_ming_cheng_) {
+                    result.isError = true
+                    result.msgContent = `第${i + 1}行缺少节点名称!`
+                    break
+                }
+                if (item.yong_hu_lei_xing_ !== 'all' && !item.chu_li_ren_) {
+                    result.isError = true
+                    result.msgContent = `第${i + 1}行缺少处理人!`
+                    break
+                }
+            }
+            return result
+        },
+        handleSubmitData () {
+            const type = this.params && this.params.id_ ? 'update' : 'add'
+            const tableName = 't_bdmbpzb'
+            const addParams = {
+                tableName,
+                paramWhere: [this.formatData()]
+            }
+            const updateParams = {
+                tableName,
+                updList: [{
+                    where: {
+                        id_: this.params.id_
+                    },
+                    param: this.formatData()
+                }]
+            }
+            this.$common.request(type, type === 'add' ? addParams : updateParams).then(() => {
+                this.$message.success('操作成功!')
+                this.closeDialog(true)
+            }).catch(error => {
+                this.$message.warning(error.message)
+            })
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            this.dialogVisible = false
+            this.$emit('close')
+        },
+        init () {
+            const isEdit = this.params && this.params.id_
+            this.title = isEdit ? '配置电子表单模板' : '添加电子表单模板'
+            this.form = this.params ? JSON.parse(JSON.stringify(this.params)) : {}
+            this.processData = []
+            if (isEdit) {
+                // JSON 解析
+                const liu_cheng_shu_ju_ = this.form.liu_cheng_shu_ju_ ? JSON.parse(this.form.liu_cheng_shu_ju_) : []
+                liu_cheng_shu_ju_.forEach((item, index) => {
+                    this.$set(this.processData, index, {
+                        jie_dian_ming_cheng_: item.nodeName,
+                        chu_li_ren_: item.executor,
+                        fang_shi_: item.conditionType,
+                        yong_hu_lei_xing_: item.executeType
+                    })
+                })
+            } else {
+                this.form.di_dian_ = this.level
+                this.form.bian_zhi_ren_ = this.userId
+                const positons = this.position.split(',')
+                this.form.bian_zhi_bu_men_ = positons[positons.length - 1]
+                this.form.bian_zhi_shi_jian = dayjs().format('YYYY-MM-DD HH:mm:ss')
+                this.form.liu_cheng_shu_ju_ = []
+            }
+        },
+        handleFileCallback (val) {
+            if (this.$utils.isEmpty(val) || this.$utils.isEmpty(val.filepath)) {
+                return
+            }
+            this.form.cun_fang_lu_jing_ = val.filepath
+            editTemplateFile({ fileName: val.filepath }).then(res => {
+                this.form.wen_jian_xin_xi_ = JSON.stringify(res.data)
+            })
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.template-config-dialog {
+    .container {
+        // width: 100%;
+        padding: 20px;
+        .config-form {
+            ::v-deep {
+                .el-form-item {
+                    margin-bottom: 10px !important;
+                    &__label {
+                        position: relative;
+                        font-size: 12px !important;
+                        color: #606266;
+                        &:before {
+                            position: absolute;
+                            left: -8px;
+                        }
+                    }
+                    &__content{
+                        font-size: 13px !important;
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 502 - 0
src/views/business/onlineForm/index.vue

@@ -0,0 +1,502 @@
+<template>
+    <ibps-layout ref="layout">
+        <div slot="west">
+            <ibps-type-tree
+                :width="width"
+                :height="height"
+                title="表单分类"
+                category-key="FLOW_TYPE"
+                :has-contextmenu="true"
+                @refresh="refresh"
+                @node-click="handleNodeClick"
+                @expand-collapse="handleExpandCollapse"
+            />
+        </div>
+        <ibps-container :margin-left="width+'px'" class="page">
+            <ibps-crud
+                ref="crud"
+                :height="height"
+                :data="listData"
+                :index-row="false"
+                :toolbars="listConfig.toolbars"
+                :row-handle="listConfig.rowHandle"
+                :search-form="listConfig.searchForm"
+                :pk-key="pkKey"
+                :columns="listConfig.columns"
+                :pagination="pagination"
+                :loading="loading"
+                @action-event="handleAction"
+                @sort-change="handleSortChange"
+                @pagination-change="handlePaginationChange"
+            >
+                <template slot="dept">
+                    <ibps-user-selector
+                        v-model="searchParams.dept"
+                        type="position"
+                        readonly-text="text"
+                        :multiple="true"
+                        size="mini"
+                        :filter="filterOption"
+                        :temp-search="true"
+                        filterable
+                    />
+                </template>
+                <template slot="user">
+                    <ibps-user-selector
+                        v-model="searchParams.submitBy"
+                        type="user"
+                        readonly-text="text"
+                        :multiple="true"
+                        size="mini"
+                        :filter="filterOption"
+                        :temp-search="true"
+                        filterable
+                    />
+                </template>
+            </ibps-crud>
+        </ibps-container>
+        <!-- 流程启动 -->
+        <bpmn-formrender
+            :visible="startFormVisible"
+            :def-id="editId"
+            :title="title"
+            :add-data-cont="addDataCont"
+            @close="visible => startFormVisible = visible"
+        />
+        <data-template-formrender-dialog
+            :visible="dialogFormVisible"
+            :form-key="formKey"
+            :default-data="defaultFormData"
+            :pk-value="pkValue"
+            :toolbars="editToolbars"
+            :readonly="readonly"
+            @callback="search"
+            @close="visible => dialogFormVisible = visible"
+        />
+    </ibps-layout>
+</template>
+<script>
+
+import { createTemplateFile, editTemplateFile, deleteTemplateFile } from '@/api/platform/file/onlyoffice'
+import ActionUtils from '@/utils/action'
+import FixHeight from '@/mixins/height'
+// import Handle from './mixin/handle'
+import { keyBy, mapValues } from 'lodash'
+
+// 列表排序字段
+const sortField = {
+    DEPT_: 'bian_zhi_bu_men_',
+    SUBMIT_TIME_: 'bian_zhi_shi_jian',
+    FORM_NAME_: 'biao_dan_ming_che'
+}
+
+const stateOption = [
+    { label: '已暂存', value: '已暂存' },
+    { label: '已完成', value: '已完成' }
+]
+
+export default {
+    components: {
+        IbpsTypeTree: () => import('./tree'),
+        SettingType: () => import('@/business/platform/cat/type/setting-type'),
+        BpmnFormrender: () => import('@/business/platform/bpmn/form/dialog'),
+        IbpsUserSelector: () => import('@/business/platform/org/selector'),
+        IbpsEmployeeSelector: () => import('@/business/platform/org/employee/selector'),
+        DataTemplateFormrenderDialog: () => import('@/business/platform/data/templaterender/form/dialog')
+    },
+    mixins: [
+        // Handle,
+        FixHeight
+    ],
+    data () {
+        const roleList = ['xtgljs', 'pxglxzfzr']
+        const { isSuper, role, userId, userList = [], deptList = [] } = this.$store.getters || {}
+        const { first, second } = this.$store.getters.level || {}
+        const hasRole = isSuper || role.some(r => roleList.includes(r.alias))
+        const userOption = userList.map(item => ({ label: item.userName, value: item.userId }))
+        const deptOption = deptList.map(item => ({ label: item.positionName, value: item.positionId }))
+        const filterOption = [{
+            descVal: '2',
+            includeSub: true,
+            old: 'position',
+            partyId: this.$store.getters.userInfo.employee.positions,
+            partyName: '',
+            scriptContent: '',
+            type: 'user',
+            userType: 'position'
+        }]
+        return {
+            userId,
+            userList,
+            userOption,
+            deptOption,
+            hasRole,
+            filterOption,
+            level: second || first,
+
+            width: 250,
+            height: document.clientHeight,
+            // 表单参数
+            dialogFormVisible: false,
+            formKey: '',
+            defaultFormData: {},
+            pkValue: '',
+            editToolbars: [],
+            readonly: false,
+            // 启动流程表单弹窗
+            startFormVisible: false,
+            title: '',
+            // 编辑dialog需要使用
+            editId: '1267498699286642688',
+            defKey: 'Process_16lkr65',
+            addDataCont: {},
+            typeId: '',
+            // 模板数据
+            templateData: '',
+            // 主键  如果主键不是pk需要传主键
+            pkKey: 'id',
+            loading: false,
+            // 模板文件配置参数
+            fileOption: {},
+            listData: [],
+            searchParams: {
+                dept: '',
+                submitBy: ''
+            },
+            listConfig: { // 工具栏
+                toolbars: [
+                    { key: 'search' },
+                    { key: 'online', label: '在线填写', icon: 'ibps-icon-add', type: 'success', visible: false },
+                    // { key: 'upload', label: '扫描上传', icon: 'ibps-icon-upload', type: 'primary', visible: false },
+                    { key: 'remove', visible: hasRole }
+                    // { key: 'more',
+                    //     label: '更多',
+                    //     mode: 'dropdown',
+                    //     type: 'info',
+                    //     visible: false,
+                    //     menus: [
+                    //         { key: 'setTemplate', label: '模板配置', icon: 'ibps-icon-cogs', type: 'info' },
+                    //         { key: 'revoke', label: '废除模板', icon: 'ibps-icon-trash', type: 'danger' }
+                    //     ]
+                    // }
+                ],
+                searchForm: { // 查询条件
+                    labelWidth: 80,
+                    itemWidth: 150,
+                    forms: [
+                        { prop: 'dept', label: '部门', fieldType: 'slot', slotName: 'dept', itemWidth: 120 },
+                        { prop: 'formName', label: '表单名称' },
+                        { prop: 'state', label: '状态', fieldType: 'select', options: stateOption },
+                        { prop: 'submitBy', label: '填写人', fieldType: 'slot', slotName: 'user' },
+                        {
+                            prop: ['submitTime0', 'submitTime1'],
+                            label: '填写时间',
+                            fieldType: 'daterange',
+                            itemWidth: 240
+                        }
+                    ]
+                },
+                // 表格字段配置
+                columns: [
+                    { prop: 'dept', label: '部门', tags: deptOption, width: 100, sortable: true },
+                    { prop: 'formName', label: '表单名称', width: 200, sortable: true },
+                    { prop: 'state', label: '状态', width: 90 },
+                    { prop: 'submitBy', label: '填写人', tags: userOption, width: 100, sortable: true },
+                    { prop: 'submitTime', label: '填写时间', width: 140, sortable: true },
+                    { prop: 'version', label: '表单版本', width: 90 },
+                    { prop: 'attachment', label: '附件', slotName: 'attachment', minWidth: 110 }
+                ],
+                rowHandle: {
+                    effect: 'display',
+                    actions: [
+                        {
+                            label: '编辑',
+                            key: 'edit',
+                            type: 'primary',
+                            hidden: function (rowData, index) {
+                                return !(rowData.submitBy === userId || hasRole)
+                            }
+                        },
+                        {
+                            label: '查阅',
+                            key: 'preview',
+                            type: 'info',
+                            icon: 'ibps-icon-eye',
+                            // hidden: function (rowData, index) {
+                            //     return !(rowData.submitBy === userId || hasRole)
+                            // }
+                        },
+                        // {
+                        //     label: '删除',
+                        //     key: 'remove',
+                        //     type: 'danger',
+                        //     hidden: function (rowData, index) {
+                        //         return !(rowData.submitBy === userId || hasRole)
+                        //     }
+                        // }
+                    ]
+                }
+            },
+            pagination: {},
+            sorts: {}
+        }
+    },
+    created () {
+        this.loadData()
+    },
+    methods: {
+        /**
+         * 加载数据
+         */
+        loadData () {
+            this.loading = true
+            this.getData(this.getSearchFormData()).then(res => {
+                this.loading = false
+                ActionUtils.handleListData(this, res.data)
+            })
+        },
+        /**
+         * 加载数据
+         */
+        getData ({ parameters, requestPage, sorts }) {
+            const { pageNo = 1, limit = 20 } = requestPage || {}
+            let sortParams = ''
+            if (sorts && sorts.length) {
+                sortParams = sorts.map(i => `${sortField[i.field]} ${i.order}`).join(',')
+            } else {
+                sortParams = 'bian_zhi_shi_jian desc, bian_zhi_bu_men_ asc'
+            }
+            const params = this.getParams(parameters)
+            const sql = `select id_ as id, create_by_ as createBy, bian_zhi_ren_ as submitBy, create_time_ as createTime, bian_zhi_shi_jian as submitTime, bian_zhi_bu_men_ as dept, shi_fou_guo_shen_ as state, biao_dan_ming_che as formName, biao_dan_mo_ban_ as formTemplate, mo_ban_id_ as templateId, gui_dang_lu_jing_ as parentId, fu_jian_ as attachment, cun_fang_lu_jing_ as filePath, shuo_ming_ as detail, pei_zhi_ as config from t_bdmbtxjl where di_dian_ = '${this.level}'${params} order by ${sortParams}`
+            return new Promise((resolve, reject) => {
+                this.$common.request('sql', sql).then(res => {
+                    const { data = [] } = res.variables || {}
+                    if (!data.length) {
+                        resolve({
+                            dataResult: [],
+                            pageResult: {
+                                limit: 20,
+                                page: 1,
+                                totalCount: 0,
+                                totalPages: 0
+                            }
+                        })
+                        return
+                    }
+                    const page = {
+                        limit,
+                        page: pageNo,
+                        totalCount: data.length,
+                        totalPages: Math.ceil(data.length / limit)
+                    }
+                    const result = {
+                        data: {
+                            dataResult: data.slice((pageNo - 1) * limit, pageNo * limit),
+                            pageResult: page
+                        }
+                    }
+                    resolve(result)
+                }).catch(error => {
+                    reject(error)
+                })
+            })
+        },
+        // 组装SQL查询参数
+        getParams (parameters) {
+            const temp = mapValues(keyBy(parameters.filter(i => this.$utils.isNotEmpty(i.value)), 'key'), 'value')
+            let params = ''
+
+            const addCondition = (condition, value, isArray = false) => {
+                if (this.$utils.isNotEmpty(value)) {
+                    if (isArray) {
+                        const conditions = value.map(v => `${condition} = '${v}'`).join(' or ')
+                        params += ` and (${conditions})`
+                    } else {
+                        params += ` and ${condition} like '%${value}%'`
+                    }
+                }
+            }
+            addCondition('biao_dan_ming_che', temp.formName)
+            addCondition('bian_zhi_bu_men_', temp.dept?.split(','), true)
+            addCondition('shi_fou_guo_shen_', temp.state)
+            addCondition('bian_zhi_ren_', temp.submitBy?.split(','), true)
+
+            const addDateCondition = (key, field) => {
+                const dateParam = parameters.find(i => i.key.includes(key))
+                if (dateParam) {
+                    params += ` and (${field} >= '${temp[key + '0']}' and ${field} <= '${temp[key + '1']}' or ${field} is null)`
+                }
+            }
+            addDateCondition('submitTime', 'bian_zhi_shi_jian')
+            if (this.typeId) {
+                params += ` and mo_ban_id_ = '${this.typeId}'`
+            }
+            return params
+        },
+        /**
+         * 获取格式化参数
+         */
+        getSearchFormData () {
+            let params = this.$refs['crud'] ? this.$refs['crud'].getSearcFormData() : {}
+            params = {
+                ...params,
+                ...this.searchParams
+            }
+            if (this.$utils.isNotEmpty(this.typeId)) {
+                params['Q^TYPE_ID_^S'] = this.typeId
+            }
+            return ActionUtils.formatParams(
+                params,
+                this.pagination,
+                this.sorts
+            )
+        },
+        /**
+         * 处理分页事件
+         */
+        handlePaginationChange (page) {
+            ActionUtils.setPagination(this.pagination, page)
+            this.loadData()
+        },
+        /**
+         * 处理排序
+         */
+        handleSortChange (sort) {
+            ActionUtils.setSorts(this.sorts, sort)
+            this.loadData()
+        },
+        search () {
+            this.loadData()
+        },
+        /**
+         * 重置查询条件
+         */
+        reset () {
+            this.$refs['crud'].handleReset()
+        },
+        /**
+         * 处理按钮事件
+         */
+        handleAction (command, position, selection, data) {
+            switch (command) {
+                case 'search':// 查询
+                    ActionUtils.setFirstPagination(this.pagination)
+                    this.search()
+                    break
+                case 'online':// 在线填写
+                    this.handleOnlineForm(data)
+                    break
+                case 'upload':// 记录上传
+                    this.handleUploadForm(data)
+                    break
+                case 'edit':// 编辑
+                    this.handleEdit('edit')
+                    break
+                case 'preview':// 查阅
+                    this.handleEdit('view')
+                    break
+                case 'remove':// 删除
+                    if (this.$utils.isEmpty(selection)) {
+                        return this.$message.warning('请选择要删除的数据!')
+                    }
+                    this.$confirm('数据删除后不可恢复,您确定要删除吗?', '提示', {
+                        type: 'warning',
+                        confirmButtonText: '确定',
+                        cancelButtonText: '取消'
+                    }).then(() => {
+                        this.handleRemove(data)
+                    })
+                    break
+                default:
+                    break
+            }
+        },
+        handleNodeClick (typeId, data) {
+            console.log(data)
+            this.typeId = typeId
+            this.templateData = data
+            const { templateId } = this.templateData || {}
+            this.listConfig.toolbars.forEach(item => {
+                // 在线填写按钮仅在模板下显示
+                if (item.key === 'more' || item.key === 'online') {
+                    item.visible = !!templateId
+                }
+                if (item.key === 'upload') {
+                    item.visible = !templateId && !!typeId
+                }
+            })
+            this.listConfig.toolbars = [...this.listConfig.toolbars]
+            this.loadData()
+        },
+        handleExpandCollapse (isExpand) {
+            this.width = isExpand ? 250 : 30
+        },
+        refresh () {
+            this.typeId = ''
+            this.loadData()
+        },
+        handleOnlineForm () {
+            const { templateId, templateFile, parentId, name, filePath, configData } = this.templateData || {}
+            if (this.$utils.isEmpty(templateId)) {
+                return this.$message.warning('获取文件信息失败!')
+            }
+            createTemplateFile({ templateId }).then(res => {
+                this.fileOption = res.data
+                this.addDataCont = {
+                    templateId,
+                    name: name,
+                    file: templateFile,
+                    type: parentId,
+                    path: filePath,
+                    option: res.data ? JSON.stringify(res.data) : '',
+                    config: configData
+                }
+                this.startFormVisible = true
+            })
+        },
+        handleUploadForm (data) {
+
+        },
+        handleEdit (type) {
+            const { templateId, templateFile, parentId, name, filePath, configData, fileOption } = this.templateData || {}
+            if (this.$utils.isEmpty(fileOption)) {
+                return this.$message.warning('获取文件信息失败!')
+            }
+            this.fileOption = JSON.parse(fileOption)
+            this.fileOption.editorConfig.mode = type
+            this.addDataCont = {
+                templateId,
+                name,
+                file: templateFile,
+                type: parentId,
+                path: filePath,
+                option: fileOption,
+                config: configData
+            }
+            this.startFormVisible = true
+        },
+        handleRemove (data) {
+            const idList = data.map(i => i.id)
+            const filePathList = data.map(i => i.filePath).filter(i => i)
+            // 删除记录
+            this.$common.request('delete', {
+                tableName: 't_bdmbtxjl',
+                paramWhere: { id_: idList.join(',') }
+            }).then(() => {
+                // 删除文件
+                if (!filePathList.length) {
+                    this.$message.success('删除成功!')
+                    this.search()
+                    return
+                }
+                deleteTemplateFile({
+                    filePath: filePathList
+                }).then(() => {
+                    this.$message.success('删除成功!')
+                    this.search()
+                })
+            })
+        }
+    }
+}
+</script>

+ 270 - 0
src/views/business/onlineForm/templateFill.vue

@@ -0,0 +1,270 @@
+<template>
+    <div class="template-fill">
+        <div v-show="showApprover">
+            <el-row :gutter="20" class="page-row">
+                <el-col v-for="(approver, index) in approvers" :key="index" :span="12" class="inline-item">
+                    <template v-if="shouldShowApprover(index)">
+                        <div class="label">审批人{{ index + 1 }}</div>
+                        <el-select
+                            v-model="approverData[`approver${index + 1}`]"
+                            clearable
+                            multiple
+                            required
+                            :disabled="isApproverDisabled(index)"
+                            placeholder="请选择"
+                        >
+                            <el-option
+                                v-for="item in options[`approver${index + 1}`]"
+                                :key="item.userId"
+                                :label="item.userName"
+                                :value="item.userId"
+                            />
+                        </el-select>
+                    </template>
+                </el-col>
+            </el-row>
+        </div>
+        <el-row :gutter="20" class="page-row">
+            <el-col :span="24" class="inline-item">
+                <div class="label">所属分类</div>
+                <ibps-type-select
+                    ref="ibpsTypeSelect"
+                    v-model="fileData.type"
+                    :category-key="categoryKey"
+                    :readonly="true"
+                    clearable
+                    class="type"
+                    filter-label="name"
+                    :filterable="true"
+                    placeholder="请选择表单模板所属分类"
+                />
+            </el-col>
+        </el-row>
+        <el-row :gutter="20" class="page-row">
+            <el-col :span="24" class="inline-item">
+                <div class="label">表单模板</div>
+                <ibps-attachment
+                    ref="formTemplate"
+                    v-model="fileData.file"
+                    :download="false"
+                    :multiple="false"
+                    accept=".docx"
+                    :disabled="true"
+                    operation-status="saveAdd"
+                    upload-method="onlyoffice"
+                    :file-option="fileData.option"
+                    size="mini"
+                    label-key="filename"
+                    value-key="filepath"
+                    store="json"
+                    placeholder="请选择docx格式文档"
+                />
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script>
+import { editTemplateFile } from '@/api/platform/file/onlyoffice'
+export default {
+    components: {
+        IbpsTypeSelect: () => import('@/business/platform/cat/type/select'),
+        IbpsAttachment: () => import('@/business/platform/file/attachment/template-selector')
+    },
+    props: {
+        formData: {
+            type: Object,
+            default: () => {}
+        },
+        params: {
+            type: Object,
+            default: () => {}
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { userList } = this.$store.getters || {}
+        return {
+            userList,
+            categoryKey: 'FLOW_TYPE',
+            nodeIdList: ['Activity_1r6j5ip', 'Activity_0agpylp', 'Activity_0l2ri14', 'Activity_0jrg9vp'],
+            approverData: {
+                approver1: [],
+                approver2: [],
+                approver3: [],
+                approver4: []
+            },
+            options: {
+                approver1: [],
+                approver2: [],
+                approver3: [],
+                approver4: []
+            },
+            fileData: {
+                tempalteId: '',
+                name: '',
+                type: '',
+                path: '',
+                file: '',
+                option: {},
+                config: ''
+            },
+            nodeList: [],
+            nodeId: '',
+            rights: {},
+            isInitialized: false,
+            lastApproval: '',
+            showApprover: false,
+            approvers: [1, 2, 3, 4] // 审批人数组
+        }
+    },
+    watch: {
+        formData: {
+            handler (val) {
+                if (val.peiZhi && (!this.isInitialized || this.lastApproval !== val.peiZhi)) {
+                    setTimeout(() => {
+                        this.initFormData(val)
+                    }, 200)
+                }
+            },
+            deep: true
+        },
+        approverData: {
+            handler (val) {
+                if (!this.isInitialized) return
+                this.changeFormData(val)
+            },
+            deep: true
+        }
+    },
+    mounted () {
+        setTimeout(() => {
+            this.initFormData(this.formData)
+        }, 200)
+    },
+    methods: {
+        async initFormData (formData) {
+            const { addDataCont = {}, nodeId } = this.params || {}
+            let pageData = null
+            if (this.$utils.isNotEmpty(addDataCont.file)) {
+                pageData = addDataCont
+            } else {
+                pageData = {
+                    tempalteId: formData.moBanId,
+                    name: formData.biaoDanMingCheng,
+                    file: formData.biaoDanMoBan,
+                    type: formData.guiDangLuJing,
+                    path: formData.cunFangLuJing,
+                    config: formData.peiZhi,
+                    option: formData.muBanXinXi
+                }
+            }
+            const { shenPiRen1, shenPiRen2, shenPiRen3, shenPiRen4 } = formData || {}
+            const { name, file, type, path, tempalteId, config, option } = pageData || {}
+            console.log('pageData:', pageData)
+            this.approverData = {
+                approver1: shenPiRen1 ? shenPiRen1.split(',') : [],
+                approver2: shenPiRen2 ? shenPiRen2.split(',') : [],
+                approver3: shenPiRen3 ? shenPiRen3.split(',') : [],
+                approver4: shenPiRen4 ? shenPiRen4.split(',') : []
+            }
+            this.nodeList = config ? JSON.parse(config) : []
+            this.nodeId = nodeId
+            // const res = await editTemplateFile({ fileName: path })
+            this.fileData = {
+                tempalteId,
+                name,
+                type,
+                path,
+                file,
+                config,
+                option: option ? JSON.parse(option) : {}
+            }
+            this.showApprover = this.nodeList.length > 0
+            if (!this.isInitialized || this.lastApproval !== config) {
+                this.$refs.formTemplate.handleEdit({ filepath: path }, 'edit')
+            }
+            this.lastApproval = JSON.stringify(this.nodeList)
+            this.isInitialized = true
+            this.setApproverOptions(this.nodeList)
+        },
+        setApproverOptions (list) {
+            list.forEach(item => {
+                const x = `approver${item.sn}`
+                if (item.executeType === 'employee') {
+                    this.options[x] = this.userList.filter(i => item.executor.includes(i.userId))
+                } else if (item.executeType === 'role') {
+                    const roles = item.executor.split(',')
+                    this.options[x] = this.userList.filter(i => roles.some(r => i.roleId.includes(r)))
+                } else {
+                    this.options[x] = this.userList
+                }
+                this.rights[x] = item.conditionType === 'presets'
+                if (item.conditionType === 'presets') {
+                    this.approverData[x] = this.options[x].map(i => i.userId)
+                }
+            })
+            this.changeFormData(this.approverData)
+        },
+        changeFormData (val) {
+            const t = JSON.parse(JSON.stringify(val))
+            this.approvers.forEach((_, index) => {
+                this.$emit('change-data', `shenPiRen${index + 1}`, t[`approver${index + 1}`] ? t[`approver${index + 1}`].join(',') : '')
+            })
+            this.$emit('change-data', 'peiZhi', JSON.stringify(this.nodeList))
+            this.$emit('change-data', 'moBanXinXi', JSON.stringify(this.fileData.option))
+            this.$emit('change-data', 'biaoDanMingCheng', this.fileData.name)
+            this.$emit('change-data', 'biaoDanMoBan', this.fileData.file)
+            this.$emit('change-data', 'guiDangLuJing', this.fileData.type)
+            this.$emit('change-data', 'cunFangLuJing', this.fileData.path)
+            this.$emit('change-data', 'moBanId', this.fileData.tempalteId)
+        },
+        shouldShowApprover (index) {
+            return this.nodeList.length > index && (!this.nodeId || !this.nodeIdList.slice(0, index).includes(this.nodeId))
+        },
+        isApproverDisabled (index) {
+            return (this.readonly || this.rights[`approver${index + 1}`]) || this.nodeId !== this.nodeIdList[index]
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.template-fill {
+    color: #606266;
+    .page-row {
+        margin-bottom: 6px;
+        &:nth-last-of-type(1) {
+            // margin-bottom: 0px;
+        }
+        .inline-item {
+            display: flex;
+            margin-top: 12px;
+            &:nth-child(-n+2) {
+                margin-top: 0px;
+            }
+            > div {
+                flex-grow: 1;
+                flex-shrink: 1;
+            }
+            .label {
+                width: calc(110px - 42px);
+                padding: 0 12px 0 30px;
+                flex-grow: 0;
+                flex-shrink: 0;
+                position: relative;
+                &::before {
+                    content: '*';
+                    color: #f56c6c;
+                    left: 24px;
+                    top: 0;
+                    position: absolute;
+                }
+            }
+        }
+    }
+}
+</style>

+ 328 - 0
src/views/business/onlineForm/tree.vue

@@ -0,0 +1,328 @@
+<template>
+    <div class="jbd-tree">
+        <ibps-tree
+            ref="treeIndex"
+            :title="title"
+            :width="width"
+            :height="height"
+            :data="treeData"
+            :location="location"
+            :options="treeOptions"
+            :contextmenus="hasContextmenu ? treeContextmenus : []"
+            :position="position"
+            :has-permission="hasPermission"
+            :category-key="categoryKey"
+            @action-event="handleTreeAction"
+            @node-click="handleNodeClick"
+            @expand-collapse="handleExpandCollapse"
+        />
+        <!-- 分类编辑 -->
+        <type-edit
+            :id="editId"
+            :parent-data="typeData"
+            :is-private="isPrivate"
+            :category-key="categoryKey"
+            :visible="typeFormVisible"
+            :title="editTitle"
+            @callback="loadTreeData"
+            @close="visible => typeFormVisible = visible"
+        />
+        <type-move
+            :id="editId"
+            :parent-data="typeData"
+            :is-private="isPrivate"
+            :category-key="categoryKey"
+            :data="treeData"
+            :visible="moveFormVisible"
+            :title="editTitle"
+            @callback="loadTreeData"
+            @close="visible => moveFormVisible = visible"
+        />
+        <!-- 分类排序 -->
+        <type-sort
+            :id="editId"
+            :visible="sortFormVisible"
+            title="分类排序"
+            @callback="loadTreeData"
+            @close="visible => sortFormVisible = visible"
+        />
+        <template-config
+            :visible="configFormVisible"
+            :params="configParams"
+            :readonly="false"
+            :file-option="fileOption"
+            @update="loadTreeData"
+            @close="visible => configFormVisible = visible"
+        />
+    </div>
+</template>
+
+<script>
+import { findTreeData, remove } from '@/api/platform/cat/type'
+import { deleteTemplateFile } from '@/api/platform/file/onlyoffice'
+import ActionUtils from '@/utils/action'
+
+import TypeEdit from '@/views/platform/cat/type/edit'
+import TypeSort from '@/views/platform/cat/type/sort'
+import TypeMove from '@/views/platform/cat/type/move'
+import TemplateConfig from '@/views/business/onlineForm/config'
+
+function getMenuRights (type) {
+    return function (menu, data, isRoot) {
+        if (type === 'node') {
+            return !data.templateId && !isRoot
+        } else if (type === 'template') {
+            return !!data.templateId && !isRoot
+        } else if (type === 'exceptTemplate') {
+            return !data.templateId
+        }
+        return false
+    }
+}
+
+export default {
+    components: {
+        TypeEdit,
+        TypeSort,
+        TypeMove,
+        TemplateConfig
+    },
+    props: {
+        title: {
+            type: String
+        },
+        location: {
+            type: String,
+            default: 'initial'
+        },
+        categoryKey: {
+            type: String,
+            required: true
+        },
+        hasContextmenu: {
+            // 是否有右键菜单
+            type: Boolean,
+            default: false
+        },
+        width: {
+            type: [String, Number],
+            default: 200
+        },
+        height: {
+            type: [String, Number],
+            default: 500
+        },
+        position: {
+            type: String,
+            default: 'west'
+        },
+        hasPermission: {
+            // 是否有权限控制
+            type: Boolean,
+            default: true
+        }
+    },
+    data () {
+        return {
+            typeFormVisible: false,
+            sortFormVisible: false,
+            moveFormVisible: false,
+            configFormVisible: false,
+            editId: '', // 编辑dialog需要使用
+            editTitle: '编辑分类',
+            // 树配置
+            treeOptions: { rootPId: '-1', showIcon: true },
+            treeContextmenus: [
+                { icon: 'add', label: '添加分类', value: 'add', rights: getMenuRights('exceptTemplate') },
+                { icon: 'edit', label: '编辑分类', value: 'edit', rights: getMenuRights('node') },
+                { icon: 'delete', label: '删除分类', value: 'remove', rights: getMenuRights('node') },
+                // { type: 'divided' },
+                { icon: 'sort', label: '分类排序', value: 'sort', rights: getMenuRights('exceptTemplate') },
+                { icon: 'arrows-v', label: '移动节点', value: 'moveNode', rights: getMenuRights('node') },
+                { icon: 'plus-square-o', label: '添加模板', value: 'addConfig', rights: getMenuRights('node') },
+                { icon: 'cogs', label: '模板配置', value: 'editConfig', rights: getMenuRights('template') },
+                { icon: 'trash', label: '废除模板', value: 'revoke', rights: getMenuRights('template') }
+            ],
+            treeData: [],
+            isPrivate: false,
+            typeData: {}, // 记录分类信息
+            configParams: {},
+            fileOption: {}
+        }
+    },
+    created () {
+        this.loadTreeData()
+    },
+    methods: {
+        loadTreeData () {
+            const { first = '', second = '' } = this.$store.getters.level || {}
+            const params = { categoryKey: this.categoryKey }
+            if (this.categoryKey === 'FILE_TYPE') {
+                params.diDian = second || first
+            }
+            const sql = `select id_ as id, id_ as templateId, bian_zhi_bu_men_ as submitDept, bu_men_fen_zu_ as deptGroup, bian_zhi_ren_ as submitBy, bian_zhi_shi_jian as submitTime, biao_dan_ming_che as name, biao_dan_mo_ban_ as templateFile, di_dian_ as location, gui_dang_lu_jing_ as parentId, cun_fang_lu_jing_ as filePath, liu_cheng_shu_ju_ as configData, bei_zhu_ as remark, wen_jian_xin_xi_ as fileOption from t_bdmbpzb where di_dian_ = '${second || first}' and (shi_fou_guo_shen_ != '已废除' or shi_fou_guo_shen_ is null)`
+            Promise.all([findTreeData(params), this.$common.request('sql', sql)]).then(([res1, res2]) => {
+                this.treeData = res1.data || []
+                const templateData = res2.variables.data || []
+                if (this.hasPermission) {
+                    this.treeData = this.treeData.filter(i => i.isShow !== '1')
+                }
+                this.treeData.forEach(i => {
+                    i.subs = templateData.filter(t => t.parentId === i.id)
+                })
+                // 按照分类过滤
+                const routeName = this.$route.name
+                const routeMap = { 'jssllb': '体系分类', 'ywtxyxjl': '模板分类', 'wjkzgl-ywyxjlsc': '模板分类' }
+                this.treeData = this.treeData.filter(i => {
+                    const authorityName = JSON.parse(i.authorityName)
+                    if (authorityName) {
+                        if (authorityName.fenLei) {
+                            return authorityName.fenLei === routeMap[routeName] || authorityName.fenLei === '通用'
+                        } else {
+                            return true
+                        }
+                    }
+                    return true
+                })
+                this.$emit('treeData', res1.data)
+            })
+        },
+        handleTreeAction (command, position, selection, data) {
+            switch (command) {
+                case 'refresh': // 查询
+                    this.loadTreeData()
+                    break
+                case 'add': // 添加
+                    this.typeData = data // { 'name': data.name, 'id': data.id }
+                    this.isPrivate = false
+                    this.editTitle = '添加分类'
+                    this.handTreeEdit()
+                    break
+                case 'addPrivate': // 添加
+                    this.typeData = data // { 'name': data.name, 'id': data.id }
+                    this.isPrivate = true
+                    this.editTitle = '添加私有分类'
+                    this.handTreeEdit()
+                    break
+                case 'edit': // 编辑
+                    this.typeData = data
+                    this.isPrivate = true
+                    this.editTitle = '编辑分类'
+                    this.handTreeEdit(data.id)
+                    break
+                case 'moveNode': // 移动节点
+                    this.typeData = data
+                    this.isPrivate = true
+                    this.editTitle = '移动节点'
+                    this.handTreeMove(data.id)
+                    break
+                case 'remove': // 删除
+                    this.handleTreeRemove(data.id)
+                    break
+                case 'sort': // 排序
+                    this.handleTreeSort(data)
+                    break
+                case 'addConfig': // 添加模板
+                    this.handleTemplateConfig({ parentId: data.id })
+                    break
+                case 'editConfig': // 模板配置
+                    this.handleTemplateConfig(data)
+                    break
+                case 'revoke': // 废除模板
+                    this.handleTemplateRevoke(data)
+                    break
+            }
+        },
+        handleNodeClick (data) {
+            this.$emit('node-click', data.parentId === '-1' ? '' : data.id, data, this.treeData)
+        },
+        handleExpandCollapse (isExpand) {
+            this.$emit('expand-collapse', isExpand)
+        },
+        handTreeEdit (editId) {
+            this.editId = editId || ''
+            this.typeFormVisible = true
+        },
+        handTreeMove (editId) {
+            this.editId = editId || ''
+            this.moveFormVisible = true
+        },
+        handleTreeSort (data) {
+            const children = data.children
+            if (children && children.length > 0) {
+                if (children.length === 1) {
+                    ActionUtils.warning('只有一个节点无需排序')
+                } else {
+                    this.editId = data.id || ''
+                    this.sortFormVisible = true
+                }
+            } else {
+                ActionUtils.warning('无子节点排序')
+            }
+        },
+        handleTreeRemove (ids) {
+            ActionUtils.removeRecord(ids).then((ids) => {
+                remove({ typeId: ids }).then((response) => {
+                    ActionUtils.removeSuccessMessage()
+                    this.loadTreeData()
+                }).catch((err) => {
+                    console.error(err)
+                })
+            }).catch(() => { })
+        },
+        showTree () {
+            this.$nextTick(() => {
+                this.$refs.treeIndex.handleExpandCollapse()
+            })
+        },
+        handleTemplateConfig (data) {
+            this.configFormVisible = true
+            this.fileOption = data.fileOption ? JSON.parse(data.fileOption) : {}
+            this.configParams = {
+                id_: data.id,
+                di_dian_: data.location,
+                bian_zhi_bu_men_: data.submitDept,
+                biao_dan_ming_che: data.name,
+                gui_dang_lu_jing_: data.parentId,
+                biao_dan_mo_ban_: data.templateFile,
+                cun_fang_lu_jing_: data.filePath,
+                liu_cheng_shu_ju_: data.configData,
+                bu_men_fen_zu_: data.deptGroup,
+                ban_ben_: data.version,
+                bei_zhu_: data.remark
+            }
+        },
+        /**
+         * 废除模板,软删除
+         */
+        handleTemplateRevoke (data) {
+            this.$confirm('废除后该模板不可使用,且模板对应的数据将无法查看,是否确认操作', '提示', {
+                confirmButtonText: '确认',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                const updateParams = {
+                    tableName: 't_bdmbpzb',
+                    updList: [{
+                        where: {
+                            id_: data.id
+                        },
+                        param: {
+                            shi_fou_guo_shen_: '已废除'
+                        }
+                    }]
+                }
+                this.$common.request('update', updateParams).then(() => {
+                    this.$message.success('操作成功!')
+                    this.$emit('refresh')
+                    this.loadTreeData()
+                    // 删除文件模板
+                    // deleteTemplateFile({ id: data.id }).then(res => {})
+                }).catch(error => {
+                    this.$message.warning(error.message)
+                })
+            })
+        }
+    }
+}
+</script>

+ 45 - 0
src/views/viewFile/template.vue

@@ -0,0 +1,45 @@
+<!--onlyoffice 编辑器-->
+<template>
+    <div>
+        <div id="editorDiv" ref="editor" class="nav" />
+    </div>
+</template>
+
+<script>
+
+export default {
+    name: 'editor',
+    data () {
+        return {
+            configKey: '',
+            option: {}
+        }
+    },
+    watch: {
+        option: {
+            handler (n, o) {
+                this.setEditor(n)
+            },
+            deep: true
+        }
+    },
+    mounted () {
+        const temp = localStorage.getItem('templateOption')
+        this.option = temp ? JSON.parse(temp) : {}
+        if (this.$utils.isNotEmpty(this.option)) {
+            this.setEditor(this.option)
+        }
+    },
+    methods: {
+        setEditor (option) {
+            const config = {
+                ...option,
+                width: '100%',
+                height: document.body.clientHeight + 'px'
+            }
+            const docEditor = new DocsAPI.DocEditor('editorDiv', config)
+            this.configKey = config.document.key
+        }
+    }
+}
+</script>