@idooel/components 0.0.2-beta.13 → 0.0.2-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3411,6 +3411,29 @@ var script$A = {
3411
3411
  this.model.cleanup();
3412
3412
  }
3413
3413
  }
3414
+ },
3415
+ async activated() {
3416
+ // 重新计算表格高度(应对窗口大小变化)
3417
+ this.$nextTick(() => {
3418
+ this.calculateTableHeight();
3419
+ if (this.showTree) {
3420
+ this.calculateTreeHeight();
3421
+ }
3422
+ });
3423
+
3424
+ // 刷新列表数据
3425
+ const {
3426
+ overrideInit = false
3427
+ } = this.tableMeta;
3428
+ if (overrideInit) {
3429
+ // 如果使用自定义初始化模式,触发 INIT 事件
3430
+ this.$emit(RESERVE_EVENT_NAMES.INIT, {
3431
+ ...this.exposed
3432
+ });
3433
+ } else {
3434
+ // 使用当前查询参数刷新表格数据
3435
+ await this.requestTableData();
3436
+ }
3414
3437
  }
3415
3438
  };
3416
3439
 
@@ -3604,11 +3627,11 @@ __vue_render__$A._withStripped = true;
3604
3627
  /* style */
3605
3628
  const __vue_inject_styles__$A = function (inject) {
3606
3629
  if (!inject) return
3607
- inject("data-v-0f7afe7a_0", { source: ".ele.model__tree-table[data-v-0f7afe7a] {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper[data-v-0f7afe7a] {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n}\n.ele.model__tree-table .model__table--container[data-v-0f7afe7a] {\n width: 100%;\n min-width: 0;\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar[data-v-0f7afe7a] {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text[data-v-0f7afe7a] {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper[data-v-0f7afe7a] {\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area[data-v-0f7afe7a] {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm[data-v-0f7afe7a] {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["C:\\Users\\ZOU\\Code\\OnlineStudy\\base-elearning-frontend-model\\packages\\components\\packages\\models\\tree-table-model\\src\\index.vue","index.vue"],"names":[],"mappings":"AAyuBA;EACA,uBAAA;EACA,aAAA;EACA,mBAAA;EACA,WAAA;ACxuBA;AD0uBA;EACA,YAAA;EACA,gBAAA;EACA,cAAA;EACA,aAAA;EACA,sBAAA;EACA,kBAAA;EACA,gBAAA;ACxuBA;AD2uBA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;ACzuBA;AD2uBA;EACA,WAAA;EACA,WAAA;EACA,uCAAA;EACA,2BAAA;EACA,4BAAA;ACzuBA;AD2uBA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,qCAAA;ACzuBA;AD4uBA;EACA,gBAAA;AC1uBA;AD2uBA;EACA,WAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,8BAAA;EACA,iBAAA;EACA,mBAAA;EACA,mBAAA;ACzuBA;AD4uBA;EACA,eAAA;EACA,kCAAA;AC1uBA;;AAEA,oCAAoC","file":"index.vue","sourcesContent":["<template>\r\n <section class=\"ele model__tree-table\">\r\n <section class=\"model__tree-table--container\" v-if=\"showTree\">\r\n <div class=\"model__tree--title\"></div>\r\n <section :ref=\"modelTreeWrapper\" class=\"model__tree--wrapper\" :style=\"{height: `${treeWrapperHeight}px`}\">\r\n <ele-tree\r\n :tree-data=\"treeData\"\r\n :defaultExpandedKeys=\"defaultExpandedKeys\"\r\n :defaultSelectedKeys=\"defaultSelectedKeys\"\r\n @select=\"selectTreeNode\"\r\n :replace-fields=\"mapFields\">\r\n </ele-tree>\r\n </section>\r\n </section>\r\n <section class=\"model__table--container\" :ref=\"modelTableContainerRef\">\r\n <div class=\"model__table--title\" v-if=\"title\">\r\n <template v-if=\"titleMode\">\r\n <div :class=\"[`model__table-title--${titleMode}`]\"></div>\r\n </template>\r\n <template v-else>\r\n <div class=\"model__table-title--text\">{{ title }}</div>\r\n </template>\r\n </div>\r\n <section :ref=\"modelTableWrapper\" class=\"model__table--wrapper\">\r\n <ele-search-area :ref=\"searchArea\" @search=\"onSearch\" :data-source=\"searchMeta.elements\"></ele-search-area>\r\n <div class=\"button-row__area\">\r\n <ele-button-group class=\"model-table__button-group\" v-on=\"overrideButtonGroupEvent\" :ref=\"buttonGroup\" @click=\"handleClickButtonGroup\" :data-source=\"getButtonGroupElements\"></ele-button-group>\r\n <slot name=\"tags\"></slot>\r\n <slot v-if=\"$slots['sub-center']\" name=\"sub-center\"></slot>\r\n </div>\r\n <ele-table\r\n v-on=\"overrideTableEvent\"\r\n :ref=\"tableRef\"\r\n :row-selection=\"rowSelection\"\r\n :loading=\"loading\" \r\n :columns=\"columns\"\r\n :total=\"total\"\r\n :x=\"x\"\r\n :y=\"y\"\r\n :bordered=\"setBorder\"\r\n :height=\"tableHeight\"\r\n :width=\"tableWidth\"\r\n :actions=\"actions\"\r\n :pageSize=\"pageSize\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n :data-source=\"tableData\"\r\n @change-page=\"onChangePage\"\r\n ></ele-table>\r\n </section>\r\n </section>\r\n <ele-modal-form v-model=\"modalFormValue\" v-on=\"overrideModalFormEvent\" :meta=\"modalFormMeta\"></ele-modal-form>\r\n <ele-modal-fsm v-model=\"showFsmModal\" :contextProp=\"fsmContextProp\" :meta=\"fsmMeta\" @cancel=\"handleCloseFsmModal\"></ele-modal-fsm>\r\n <ele-modal-table\r\n :meta=\"modalTableMeta\"\r\n v-model=\"modalTableValue\"\r\n v-on=\"overrideModalTableEvent\"\r\n ></ele-modal-table>\r\n </section>\r\n</template>\r\n\r\n<script>\r\nimport { type, net } from '@idooel/shared'\r\nimport { v4 as uuidv4 } from 'uuid'\r\nimport { BUILT_IN_EVENT_NAMES, RESERVE_EVENT_NAMES, parseFieldMap, BUILT_IN_TRIGGER, CONTEXT } from '../../../utils'\r\nimport { createTreeTableModel } from '../../../utils/runtime-context'\r\nexport default {\r\n name: 'ele-tree-table-model',\r\n props: {\r\n title: {\r\n type: [Object, String]\r\n },\r\n overHeight: {\r\n type: Number,\r\n default: 0\r\n },\r\n treeMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n searchMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n buttonGroupMeta: {\r\n typeof: Object,\r\n default: () => ({})\r\n },\r\n tableMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n createMeta: {\r\n type: Object\r\n },\r\n editMeta: {\r\n type: Object\r\n }\r\n },\r\n provide () {\r\n return {\r\n requestTreeData: this.requestTreeData,\r\n requestTableData: this.requestTableData,\r\n [CONTEXT]: () => {\r\n return {\r\n exposed: this.exposed\r\n }\r\n }\r\n }\r\n },\r\n data () {\r\n return {\r\n tableHeight: 0,\r\n tableWidth: 0,\r\n modalFormMeta: {},\r\n modalFormValue: false,\r\n treeData: [],\r\n tableData: [],\r\n defaultExpandedKeys: [],\r\n defaultSelectedKeys: [],\r\n replaceFields: {\r\n title: 'title',\r\n children: 'children',\r\n key: 'id'\r\n },\r\n loading: false,\r\n total: 0,\r\n tableQuerys: {},\r\n resizeObserverModelTableWrapper: null,\r\n modelTableWrapperHeight: 0,\r\n currentTreeNodeData: {},\r\n currentRowData: {},\r\n treeWrapperHeight: 0,\r\n currentTableSelection: this.currentTableMode == 'radio' ? {} : [],\r\n showFsmModal: false,\r\n fsmMeta: {},\r\n fsmContextProp: {},\r\n modalTableValue: false,\r\n modalTableMeta: {},\r\n dataPoolManager: null\r\n }\r\n },\r\n computed: {\r\n setBorder () {\r\n return this.tableMeta.bordered === false ? false : true\r\n },\r\n rowSelection () {\r\n if (!this.currentTableMode) return void 0\r\n return {\r\n columnTitle: this.currentSelectionColumn.columnTitle,\r\n fixed: true,\r\n type: this.currentTableMode,\r\n onChange: this.onChangeTableSelection\r\n }\r\n },\r\n currentSelectionColumn () {\r\n const { multiple } = this.tableMeta\r\n const target = this.columns.find(item => Object.keys(item).includes('multiple'))\r\n const isGlobalExistMultiple = Object.keys(this.tableMeta).includes('multiple')\r\n if (target) {\r\n return target\r\n } else if (isGlobalExistMultiple) {\r\n return { multiple }\r\n }\r\n return void 0\r\n },\r\n x () {\r\n const { x } = this.tableMeta\r\n return x\r\n },\r\n y () {\r\n const { y } = this.tableMeta\r\n return y\r\n },\r\n currentTableMode () {\r\n if (!this.currentSelectionColumn) return void 0\r\n const { multiple } = this.currentSelectionColumn\r\n if (type.isBool(multiple)) {\r\n if (multiple) {\r\n return 'checkbox'\r\n } else {\r\n return 'radio'\r\n }\r\n } else {\r\n return void 0\r\n }\r\n },\r\n modelTableContainerRef () {\r\n return uuidv4()\r\n },\r\n titleMode () {\r\n if (type.isObject(this.title)) {\r\n const { mode = '' } = this.title\r\n return mode\r\n }\r\n return void 0\r\n },\r\n tableRef () {\r\n return uuidv4()\r\n },\r\n exposed () {\r\n return {\r\n showModalForm: this.showModalForm,\r\n closeModalForm: this.closeModalForm,\r\n showModalTable: this.showModalTable,\r\n closeModalTable: this.closeModalTable,\r\n currentTableSelection: this.currentTableSelection,\r\n currentTreeNode: this.currentTreeNodeData,\r\n requestTableData: this.requestTableData,\r\n refreshTreeData: this.refreshTreeData,\r\n querys: this.tableQuerys,\r\n currentRowData: this.getCurrentRowData(),\r\n getCurrentRowData: this.getCurrentRowData,\r\n setCurrentRowData: this.setCurrentRowData,\r\n setCurrentTableSelection: this.setCurrentTableSelection,\r\n getCurrentTableSelection: this.getCurrentTableSelection,\r\n cleanCurrentModelEffect: this.cleanCurrentModelEffect,\r\n route: this.$route,\r\n _route: this.$route.query,\r\n _routeMeta: this.$route.meta,\r\n dataPool: this.model ? this.model.getDataPool() : null,\r\n dataPoolManager: this.model || null\r\n }\r\n },\r\n overrideTableEvent () {\r\n const events = this.actions.reduce((ret, action) => {\r\n ret[action.eventName || action.key] = (e) => {\r\n this.setCurrentRowData(e.exposed.currentRowData)\r\n const { target } = action\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, record: e.exposed.currentRowData, modeMeta: targetMeta })\r\n this.$emit(action.eventName || action.key, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: { ...this.exposed, ...e.exposed } })\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.EDIT]: this[BUILT_IN_EVENT_NAMES.EDIT],\r\n [BUILT_IN_EVENT_NAMES.SUBMIT]: this[BUILT_IN_EVENT_NAMES.SUBMIT]\r\n }\r\n },\r\n overrideModalFormEvent () {\r\n const { footerMeta } = this.modalFormMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalForm()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events\r\n }\r\n },\r\n overrideModalTableEvent () {\r\n const { footerMeta } = this.modalTableMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalTable()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events,\r\n exposed: this.exposed\r\n }\r\n },\r\n overrideButtonGroupEvent () {\r\n const events = this.getButtonGroupElements.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e) => {\r\n this.$emit(ele.eventName || 'click', { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, e.exposed || {}, this.exposed)})\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.CREATE]: this[BUILT_IN_EVENT_NAMES.CREATE],\r\n exposed: this.exposed\r\n }\r\n },\r\n showTree () {\r\n return !!Object.keys(this.treeMeta).length\r\n },\r\n buttonGroup () {\r\n return uuidv4()\r\n },\r\n searchArea () {\r\n return uuidv4()\r\n },\r\n modelTreeWrapper () {\r\n return uuidv4()\r\n },\r\n modelTableWrapper () {\r\n return uuidv4()\r\n },\r\n actions () {\r\n const { operations } = this.tableMeta\r\n if (operations) {\r\n return operations.elements\r\n } else {\r\n return []\r\n }\r\n },\r\n pageSize () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSize || 10\r\n },\r\n pageSizeOptions () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSizeOptions || ['10', '20', '30', '40']\r\n },\r\n columns () {\r\n const { columns, operations } = this.tableMeta\r\n if (type.get(columns) === 'array') {\r\n const columnsOptions = columns.map(item => {\r\n const { mode = 'text' } = item\r\n if (item.render) {\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n const { $createElement } = this\r\n return item.render.call(this, \r\n { h: $createElement, ctx: this },\r\n text ? typeof text == 'object' ? text[item.dataIndex] : text : '', \r\n record, index)\r\n }\r\n }\r\n } else if (mode !== BUILT_IN_TRIGGER.TEXT) {\r\n const { [`${mode}Meta`]: modeMeta } = item\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n return <span onClick={() => this.dispatchTrigger({ mode, record, modeMeta, index })} class={ mode }>{ text }</span>\r\n }\r\n }\r\n }\r\n return {\r\n ...item\r\n }\r\n })\r\n if (operations) {\r\n return [\r\n ...columnsOptions,\r\n {\r\n title: '操作',\r\n width: operations.width,\r\n key: 'action',\r\n fixed: 'right',\r\n scopedSlots: { customRender: 'action' }\r\n }\r\n ]\r\n }\r\n return columnsOptions\r\n } else {\r\n console.error('Error: columns is invalid, please check it')\r\n return []\r\n }\r\n },\r\n getButtonGroupElements () {\r\n const { elements } = this.buttonGroupMeta\r\n if (type.get(elements) === 'function') {\r\n return elements.call(this)\r\n } else if (type.get(elements) === 'array') {\r\n return elements\r\n } else {\r\n return []\r\n }\r\n },\r\n mapFields () {\r\n const { replaceFields = {} } = this.treeMeta\r\n const mapFields = type.isEmpty(replaceFields) ? this.replaceFields : replaceFields\r\n return mapFields\r\n }\r\n },\r\n async created () {\r\n // 确保全局数据池已初始化\r\n if (!window.__idooel_data_pool__) {\r\n console.error('Global data pool not initialized. Please check if runtime-context/globalDataPool.js is properly imported.')\r\n return\r\n }\r\n \r\n // 初始化数据池管理器(使用新的 runtime-context)\r\n try {\r\n this.model = createTreeTableModel('treeTableModel')\r\n \r\n if (!this.model) {\r\n throw new Error('Failed to create tree table model')\r\n }\r\n } catch (error) {\r\n console.error('Error creating tree table model:', error)\r\n this.model = null\r\n return\r\n }\r\n \r\n // 初始化 currentRowData(尝试从共享命名空间获取)\r\n this.initializeCurrentRowData()\r\n \r\n // 订阅数据池变化\r\n this.unsubscribe = this.model.subscribe('currentRowData', (event) => {\r\n this.currentRowData = event.value || {}\r\n this.$forceUpdate()\r\n })\r\n \r\n // 订阅共享数据变化\r\n this.unsubscribeShared = this.model.subscribeShared((event) => {\r\n // 当有新的共享数据时,更新当前组件的 currentRowData\r\n if (event.value && Object.keys(event.value).length > 0) {\r\n this.setCurrentRowData(event.value)\r\n }\r\n })\r\n \r\n if (this.showTree) {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.currentTreeNodeData = defaultTreeNode\r\n }\r\n \r\n const { params = {}, fieldMap = {}, overrideInit = false } = this.tableMeta\r\n const currentRowData = this.getCurrentRowData()\r\n const ctx = { \r\n ...this.currentTreeNodeData, \r\n _route: this.$route.query, \r\n currentRowData: currentRowData\r\n }\r\n \r\n const initQuerys = Object.assign({}, params, parseFieldMap(fieldMap, ctx))\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.INIT, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(initQuerys)\r\n }\r\n },\r\n methods: {\r\n initializeCurrentRowData () {\r\n if (!this.model) {\r\n console.warn('Model not initialized, skipping currentRowData initialization')\r\n return\r\n }\r\n \r\n // 检查是否有来自父组件的共享数据(比如 modal table 场景)\r\n const parentData = this.model.getSharedData()\r\n \r\n if (parentData && Object.keys(parentData).length > 0) {\r\n this.setCurrentRowData(parentData)\r\n return\r\n }\r\n \r\n // 可以根据路由参数、props 或其他来源设置初始的 currentRowData\r\n const { query } = this.$route\r\n \r\n // 示例:如果路由中有特定参数,可以设置为 currentRowData\r\n if (query.rowId || query.selectedId) {\r\n // 这里可以根据实际需求从服务端或其他地方获取数据\r\n // const presetData = { id: query.rowId, /* 其他字段 */ }\r\n // this.setCurrentRowData(presetData)\r\n }\r\n \r\n // 目前保持空对象,等待用户选择行数据\r\n },\r\n async refreshTreeData () {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.currentTreeNodeData = defaultTreeNode\r\n },\r\n dispatchTrigger ({ mode, record = {}, modeMeta = { } }) {\r\n switch (mode) {\r\n case BUILT_IN_TRIGGER.FSM:\r\n this[`${BUILT_IN_TRIGGER.FSM}Trigger`](record, modeMeta = type.isEmpty(modeMeta) ? { \r\n url: 'api-fsm/workbench/fsm/auditFlow',\r\n requestType: 'GET',\r\n fieldMap: {\r\n modelCode: 'modelCode',\r\n businessId: 'businessId'\r\n }\r\n } : modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_FORM:\r\n this.modalFormMeta = modeMeta\r\n this.showModalForm(modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_TABLE:\r\n this.modalTableMeta = modeMeta\r\n // 将当前行的 record 数据传递给 modal table\r\n this.showModalTable(modeMeta, record)\r\n break\r\n default:\r\n break\r\n }\r\n },\r\n handleCloseFsmModal () {\r\n this.showFsmModal = false\r\n },\r\n [`${BUILT_IN_TRIGGER.FSM}Trigger`] (record, meta) {\r\n this.fsmMeta = meta\r\n this.fsmContextProp = record\r\n this.showFsmModal = true\r\n },\r\n onChangeTableSelection (_, selectedRows = []) {\r\n if (this.currentTableMode === 'radio') {\r\n this.setCurrentTableSelection(selectedRows)\r\n this.$emit('on-change-table-selection', this.currentTableSelection)\r\n this.$emit('x:refresh-exposed', { exposed: this.exposed })\r\n } else {\r\n this.setCurrentTableSelection(selectedRows)\r\n this.$emit('on-change-table-selection', this.currentTableSelection)\r\n this.$emit('x:refresh-exposed', { exposed: this.exposed })\r\n }\r\n },\r\n setCurrentTableSelection (props = {}) {\r\n if (this.currentTableMode === 'radio') {\r\n this.$set(this, 'currentTableSelection', (type.isArray(props) && props.length > 0) ? props[0] : type.isObject(props) ? props : {})\r\n } else {\r\n this.$set(this, 'currentTableSelection', type.isArray(props) ? props : [])\r\n }\r\n },\r\n getCurrentTableSelection () {\r\n return this.currentTableSelection\r\n },\r\n setCurrentRowData (props = {}) {\r\n this.currentRowData = props\r\n if (this.model) {\r\n this.model.setCurrentRowData(props)\r\n } else {\r\n console.warn('Model not initialized, cannot setCurrentRowData in model')\r\n }\r\n },\r\n getCurrentRowData () {\r\n if (this.model) {\r\n return this.model.getCurrentRowData()\r\n }\r\n console.warn('Model not initialized, getCurrentRowData returning local data')\r\n return this.currentRowData || {}\r\n },\r\n cleanCurrentModelEffect (clearRowData = true) {\r\n this.setCurrentTableSelection()\r\n if (clearRowData) {\r\n this.setCurrentRowData({})\r\n }\r\n },\r\n [BUILT_IN_EVENT_NAMES.SUBMIT] (props = {}) {\r\n this.cleanCurrentModelEffect()\r\n this.requestTableData()\r\n },\r\n [BUILT_IN_EVENT_NAMES.EDIT] (props = {}) {\r\n const { record = {} } = props\r\n this.setCurrentRowData(record)\r\n this.modalFormMeta = this.editMeta\r\n this.modalFormValue = true\r\n },\r\n [BUILT_IN_EVENT_NAMES.CREATE] () {\r\n this.modalFormMeta = this.createMeta\r\n this.modalFormValue = true\r\n },\r\n showModalForm (modeMeta = {}) {\r\n if (type.isStr(modeMeta)) {\r\n const targetMeta = this.findMetaByKey(modeMeta)\r\n this.modalFormMeta = targetMeta\r\n } else {\r\n this.modalFormMeta = modeMeta\r\n }\r\n this.modalFormValue = true\r\n },\r\n showModalTable (modeMeta = {}, record = null) {\r\n // 获取当前行数据并设置到共享命名空间\r\n const currentRowData = record || this.getCurrentRowData()\r\n if (this.model) {\r\n this.model.setSharedData(currentRowData)\r\n } else {\r\n console.warn('Model not initialized, cannot setSharedData')\r\n }\r\n \r\n let targetMeta = modeMeta\r\n if (type.isStr(modeMeta)) {\r\n targetMeta = this.findMetaByKey(modeMeta)\r\n }\r\n \r\n // 解析 fieldMap 参数,使用完整的上下文包括 currentRowData\r\n if (targetMeta && targetMeta.fieldMap) {\r\n const { fieldMap, params = {} } = targetMeta\r\n \r\n const ctx = { \r\n ...this.currentTreeNodeData, \r\n _route: this.$route.query, \r\n ...currentRowData \r\n }\r\n const parsedParams = parseFieldMap(fieldMap, ctx)\r\n \r\n // 将当前的 currentRowData 传递给 modal table,通过 params\r\n targetMeta = {\r\n ...targetMeta,\r\n params: {\r\n ...params,\r\n ...parsedParams\r\n }\r\n }\r\n }\r\n \r\n this.modalTableMeta = targetMeta\r\n this.modalTableValue = true\r\n },\r\n closeModalForm () {\r\n this.modalFormValue = false\r\n },\r\n closeModalTable () {\r\n this.modalTableValue = false\r\n },\r\n findMetaByKey (key) {\r\n return this.$attrs[key] || {}\r\n },\r\n handleClickButtonGroup (props) {\r\n const { eventName, target } = props\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, modeMeta: targetMeta })\r\n this.$emit(eventName || 'click', { currentTreeNode: this.currentTreeNodeData })\r\n },\r\n async onSearch (props) {\r\n const { overrideInit = false } = this.tableMeta\r\n this.tableQuerys = Object.assign(this.tableQuerys, props)\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData()\r\n }\r\n },\r\n async selectTreeNode (selectedKeys, e) {\r\n const { fieldMap } = this.tableMeta\r\n this.currentTreeNodeData = e.node.$vnode.data.props.dataRef || {}\r\n //@deprecated '_' namespace is deprecated, please use 'exposed' instead\r\n const execFieldMapRet = parseFieldMap(fieldMap, { ...this.currentTreeNodeData, exposed: this.exposed, _route: this.exposed._route })\r\n const { overrideInit = false } = this.tableMeta\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(execFieldMapRet)\r\n }\r\n },\r\n async requestTreeData () {\r\n const { url, requestType = 'GET', params = {}, fieldMap = {} } = this.treeMeta\r\n const fieldMapRet = parseFieldMap(fieldMap, { ...this.currentTreeNodeData, _route: this.$route.query })\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n { ...params, ...fieldMapRet }\r\n ).then(resp => {\r\n const { data } = resp || {}\r\n return data\r\n })\r\n return ret\r\n },\r\n async onChangePage (page, pageSize) {\r\n this.tableData = await this.requestTableData({ currentPage: page, pageSize })\r\n },\r\n async requestTableData (props = {}) {\r\n const { url, requestType = 'GET', page = {} } = this.tableMeta\r\n const { pageSize = 10 } = page\r\n this.tableQuerys = Object.assign(this.tableQuerys, { currentPage: 1, pageSize }, props)\r\n this.$emit(RESERVE_EVENT_NAMES.WATCH, { ...this.exposed })\r\n this.loading = true\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n this.tableQuerys\r\n ).then(resp => {\r\n const { data = [], count } = resp || {}\r\n this.total = count\r\n this.loading = false\r\n return (data || []).map(item => {\r\n delete item.children\r\n return {\r\n key: uuidv4(),\r\n ...item\r\n }\r\n })\r\n })\r\n this.cleanCurrentModelEffect(false) // 不清空 currentRowData,除非明确需要\r\n this.tableData = ret\r\n return ret\r\n },\r\n calculateTableHeight () {\r\n const currentViewportHeight = window.innerHeight\r\n const tableRef = this.$refs[this.tableRef]\r\n const { top: tableToTop, width } = tableRef.$el.getBoundingClientRect()\r\n this.tableWidth = width\r\n this.tableHeight = currentViewportHeight - tableToTop - this.overHeight\r\n },\r\n calculateTreeHeight () {\r\n if (!this.showTree) return\r\n const modelTableContainerRef = this.$refs[this.modelTableContainerRef]\r\n const { height } = modelTableContainerRef.getBoundingClientRect()\r\n this.treeWrapperHeight = height\r\n }\r\n },\r\n mounted () {\r\n this.calculateTableHeight()\r\n this.$nextTick(() => {\r\n this.calculateTreeHeight()\r\n })\r\n this.resizeObserverModelTableWrapper = new ResizeObserver(entries => {\r\n for (const _ of entries) {\r\n requestAnimationFrame(() => {\r\n this.calculateTableHeight()\r\n })\r\n }\r\n })\r\n this.resizeObserverModelTableWrapper.observe(this.$refs[this.modelTableWrapper])\r\n },\r\n destroyed () {\r\n this.resizeObserverModelTableWrapper.disconnect()\r\n if (this.model) {\r\n // 清理订阅\r\n if (this.unsubscribe) {\r\n this.unsubscribe()\r\n }\r\n if (this.unsubscribeShared) {\r\n this.unsubscribeShared()\r\n }\r\n // 清理模型数据\r\n if (this.model) {\r\n this.model.cleanup()\r\n }\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.ele {\r\n &.model__tree-table {\r\n background: transparent; \r\n display: flex;\r\n flex-direction: row;\r\n width: 100%;\r\n .model__tree-table--container {\r\n .model__tree--wrapper {\r\n width: 240px;\r\n background: #fff;\r\n flex-shrink: 0;\r\n padding: 16px;\r\n box-sizing: border-box;\r\n margin-right: 16px;\r\n overflow-y: auto;\r\n }\r\n }\r\n .model__table--container {\r\n width: 100%;\r\n min-width: 0;\r\n background: #fff;\r\n .model__table--title {\r\n .model__table-title--bar {\r\n width: 100%;\r\n height: 8px;\r\n background: var(--idooel-primary-color);\r\n border-top-left-radius: 4px;\r\n border-top-right-radius: 4px;\r\n }\r\n .model__table-title--text {\r\n text-align: left;\r\n padding: 16px;\r\n font-size: 16px;\r\n font-weight: bold;\r\n background: #fff;\r\n border-bottom: 1px solid;\r\n border-color: var(--idoole-black-016);\r\n }\r\n }\r\n .model__table--wrapper {\r\n background: #fff;\r\n .button-row__area {\r\n width: 100%;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding-top: 16px;\r\n padding-bottom: 8px;\r\n padding-right: 16px;\r\n }\r\n .g-table__wrapper {\r\n .fsm {\r\n cursor: pointer;\r\n color: var(--idooel-primary-color);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</style>\r\n",".ele.model__tree-table {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n}\n.ele.model__tree-table .model__table--container {\n width: 100%;\n min-width: 0;\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper {\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined });
3630
+ inject("data-v-306c600f_0", { source: ".ele.model__tree-table[data-v-306c600f] {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper[data-v-306c600f] {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n}\n.ele.model__tree-table .model__table--container[data-v-306c600f] {\n width: 100%;\n min-width: 0;\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar[data-v-306c600f] {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text[data-v-306c600f] {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper[data-v-306c600f] {\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area[data-v-306c600f] {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm[data-v-306c600f] {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["C:\\Users\\ZOU\\Code\\OnlineStudy\\base-elearning-frontend-model\\packages\\components\\packages\\models\\tree-table-model\\src\\index.vue","index.vue"],"names":[],"mappings":"AA4vBA;EACA,uBAAA;EACA,aAAA;EACA,mBAAA;EACA,WAAA;AC3vBA;AD6vBA;EACA,YAAA;EACA,gBAAA;EACA,cAAA;EACA,aAAA;EACA,sBAAA;EACA,kBAAA;EACA,gBAAA;AC3vBA;AD8vBA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;AC5vBA;AD8vBA;EACA,WAAA;EACA,WAAA;EACA,uCAAA;EACA,2BAAA;EACA,4BAAA;AC5vBA;AD8vBA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,qCAAA;AC5vBA;AD+vBA;EACA,gBAAA;AC7vBA;AD8vBA;EACA,WAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,8BAAA;EACA,iBAAA;EACA,mBAAA;EACA,mBAAA;AC5vBA;AD+vBA;EACA,eAAA;EACA,kCAAA;AC7vBA;;AAEA,oCAAoC","file":"index.vue","sourcesContent":["<template>\r\n <section class=\"ele model__tree-table\">\r\n <section class=\"model__tree-table--container\" v-if=\"showTree\">\r\n <div class=\"model__tree--title\"></div>\r\n <section :ref=\"modelTreeWrapper\" class=\"model__tree--wrapper\" :style=\"{height: `${treeWrapperHeight}px`}\">\r\n <ele-tree\r\n :tree-data=\"treeData\"\r\n :defaultExpandedKeys=\"defaultExpandedKeys\"\r\n :defaultSelectedKeys=\"defaultSelectedKeys\"\r\n @select=\"selectTreeNode\"\r\n :replace-fields=\"mapFields\">\r\n </ele-tree>\r\n </section>\r\n </section>\r\n <section class=\"model__table--container\" :ref=\"modelTableContainerRef\">\r\n <div class=\"model__table--title\" v-if=\"title\">\r\n <template v-if=\"titleMode\">\r\n <div :class=\"[`model__table-title--${titleMode}`]\"></div>\r\n </template>\r\n <template v-else>\r\n <div class=\"model__table-title--text\">{{ title }}</div>\r\n </template>\r\n </div>\r\n <section :ref=\"modelTableWrapper\" class=\"model__table--wrapper\">\r\n <ele-search-area :ref=\"searchArea\" @search=\"onSearch\" :data-source=\"searchMeta.elements\"></ele-search-area>\r\n <div class=\"button-row__area\">\r\n <ele-button-group class=\"model-table__button-group\" v-on=\"overrideButtonGroupEvent\" :ref=\"buttonGroup\" @click=\"handleClickButtonGroup\" :data-source=\"getButtonGroupElements\"></ele-button-group>\r\n <slot name=\"tags\"></slot>\r\n <slot v-if=\"$slots['sub-center']\" name=\"sub-center\"></slot>\r\n </div>\r\n <ele-table\r\n v-on=\"overrideTableEvent\"\r\n :ref=\"tableRef\"\r\n :row-selection=\"rowSelection\"\r\n :loading=\"loading\" \r\n :columns=\"columns\"\r\n :total=\"total\"\r\n :x=\"x\"\r\n :y=\"y\"\r\n :bordered=\"setBorder\"\r\n :height=\"tableHeight\"\r\n :width=\"tableWidth\"\r\n :actions=\"actions\"\r\n :pageSize=\"pageSize\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n :data-source=\"tableData\"\r\n @change-page=\"onChangePage\"\r\n ></ele-table>\r\n </section>\r\n </section>\r\n <ele-modal-form v-model=\"modalFormValue\" v-on=\"overrideModalFormEvent\" :meta=\"modalFormMeta\"></ele-modal-form>\r\n <ele-modal-fsm v-model=\"showFsmModal\" :contextProp=\"fsmContextProp\" :meta=\"fsmMeta\" @cancel=\"handleCloseFsmModal\"></ele-modal-fsm>\r\n <ele-modal-table\r\n :meta=\"modalTableMeta\"\r\n v-model=\"modalTableValue\"\r\n v-on=\"overrideModalTableEvent\"\r\n ></ele-modal-table>\r\n </section>\r\n</template>\r\n\r\n<script>\r\nimport { type, net } from '@idooel/shared'\r\nimport { v4 as uuidv4 } from 'uuid'\r\nimport { BUILT_IN_EVENT_NAMES, RESERVE_EVENT_NAMES, parseFieldMap, BUILT_IN_TRIGGER, CONTEXT } from '../../../utils'\r\nimport { createTreeTableModel } from '../../../utils/runtime-context'\r\nexport default {\r\n name: 'ele-tree-table-model',\r\n props: {\r\n title: {\r\n type: [Object, String]\r\n },\r\n overHeight: {\r\n type: Number,\r\n default: 0\r\n },\r\n treeMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n searchMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n buttonGroupMeta: {\r\n typeof: Object,\r\n default: () => ({})\r\n },\r\n tableMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n createMeta: {\r\n type: Object\r\n },\r\n editMeta: {\r\n type: Object\r\n }\r\n },\r\n provide () {\r\n return {\r\n requestTreeData: this.requestTreeData,\r\n requestTableData: this.requestTableData,\r\n [CONTEXT]: () => {\r\n return {\r\n exposed: this.exposed\r\n }\r\n }\r\n }\r\n },\r\n data () {\r\n return {\r\n tableHeight: 0,\r\n tableWidth: 0,\r\n modalFormMeta: {},\r\n modalFormValue: false,\r\n treeData: [],\r\n tableData: [],\r\n defaultExpandedKeys: [],\r\n defaultSelectedKeys: [],\r\n replaceFields: {\r\n title: 'title',\r\n children: 'children',\r\n key: 'id'\r\n },\r\n loading: false,\r\n total: 0,\r\n tableQuerys: {},\r\n resizeObserverModelTableWrapper: null,\r\n modelTableWrapperHeight: 0,\r\n currentTreeNodeData: {},\r\n currentRowData: {},\r\n treeWrapperHeight: 0,\r\n currentTableSelection: this.currentTableMode == 'radio' ? {} : [],\r\n showFsmModal: false,\r\n fsmMeta: {},\r\n fsmContextProp: {},\r\n modalTableValue: false,\r\n modalTableMeta: {},\r\n dataPoolManager: null\r\n }\r\n },\r\n computed: {\r\n setBorder () {\r\n return this.tableMeta.bordered === false ? false : true\r\n },\r\n rowSelection () {\r\n if (!this.currentTableMode) return void 0\r\n return {\r\n columnTitle: this.currentSelectionColumn.columnTitle,\r\n fixed: true,\r\n type: this.currentTableMode,\r\n onChange: this.onChangeTableSelection\r\n }\r\n },\r\n currentSelectionColumn () {\r\n const { multiple } = this.tableMeta\r\n const target = this.columns.find(item => Object.keys(item).includes('multiple'))\r\n const isGlobalExistMultiple = Object.keys(this.tableMeta).includes('multiple')\r\n if (target) {\r\n return target\r\n } else if (isGlobalExistMultiple) {\r\n return { multiple }\r\n }\r\n return void 0\r\n },\r\n x () {\r\n const { x } = this.tableMeta\r\n return x\r\n },\r\n y () {\r\n const { y } = this.tableMeta\r\n return y\r\n },\r\n currentTableMode () {\r\n if (!this.currentSelectionColumn) return void 0\r\n const { multiple } = this.currentSelectionColumn\r\n if (type.isBool(multiple)) {\r\n if (multiple) {\r\n return 'checkbox'\r\n } else {\r\n return 'radio'\r\n }\r\n } else {\r\n return void 0\r\n }\r\n },\r\n modelTableContainerRef () {\r\n return uuidv4()\r\n },\r\n titleMode () {\r\n if (type.isObject(this.title)) {\r\n const { mode = '' } = this.title\r\n return mode\r\n }\r\n return void 0\r\n },\r\n tableRef () {\r\n return uuidv4()\r\n },\r\n exposed () {\r\n return {\r\n showModalForm: this.showModalForm,\r\n closeModalForm: this.closeModalForm,\r\n showModalTable: this.showModalTable,\r\n closeModalTable: this.closeModalTable,\r\n currentTableSelection: this.currentTableSelection,\r\n currentTreeNode: this.currentTreeNodeData,\r\n requestTableData: this.requestTableData,\r\n refreshTreeData: this.refreshTreeData,\r\n querys: this.tableQuerys,\r\n currentRowData: this.getCurrentRowData(),\r\n getCurrentRowData: this.getCurrentRowData,\r\n setCurrentRowData: this.setCurrentRowData,\r\n setCurrentTableSelection: this.setCurrentTableSelection,\r\n getCurrentTableSelection: this.getCurrentTableSelection,\r\n cleanCurrentModelEffect: this.cleanCurrentModelEffect,\r\n route: this.$route,\r\n _route: this.$route.query,\r\n _routeMeta: this.$route.meta,\r\n dataPool: this.model ? this.model.getDataPool() : null,\r\n dataPoolManager: this.model || null\r\n }\r\n },\r\n overrideTableEvent () {\r\n const events = this.actions.reduce((ret, action) => {\r\n ret[action.eventName || action.key] = (e) => {\r\n this.setCurrentRowData(e.exposed.currentRowData)\r\n const { target } = action\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, record: e.exposed.currentRowData, modeMeta: targetMeta })\r\n this.$emit(action.eventName || action.key, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: { ...this.exposed, ...e.exposed } })\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.EDIT]: this[BUILT_IN_EVENT_NAMES.EDIT],\r\n [BUILT_IN_EVENT_NAMES.SUBMIT]: this[BUILT_IN_EVENT_NAMES.SUBMIT]\r\n }\r\n },\r\n overrideModalFormEvent () {\r\n const { footerMeta } = this.modalFormMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalForm()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events\r\n }\r\n },\r\n overrideModalTableEvent () {\r\n const { footerMeta } = this.modalTableMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalTable()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events,\r\n exposed: this.exposed\r\n }\r\n },\r\n overrideButtonGroupEvent () {\r\n const events = this.getButtonGroupElements.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e) => {\r\n this.$emit(ele.eventName || 'click', { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, e.exposed || {}, this.exposed)})\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.CREATE]: this[BUILT_IN_EVENT_NAMES.CREATE],\r\n exposed: this.exposed\r\n }\r\n },\r\n showTree () {\r\n return !!Object.keys(this.treeMeta).length\r\n },\r\n buttonGroup () {\r\n return uuidv4()\r\n },\r\n searchArea () {\r\n return uuidv4()\r\n },\r\n modelTreeWrapper () {\r\n return uuidv4()\r\n },\r\n modelTableWrapper () {\r\n return uuidv4()\r\n },\r\n actions () {\r\n const { operations } = this.tableMeta\r\n if (operations) {\r\n return operations.elements\r\n } else {\r\n return []\r\n }\r\n },\r\n pageSize () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSize || 10\r\n },\r\n pageSizeOptions () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSizeOptions || ['10', '20', '30', '40']\r\n },\r\n columns () {\r\n const { columns, operations } = this.tableMeta\r\n if (type.get(columns) === 'array') {\r\n const columnsOptions = columns.map(item => {\r\n const { mode = 'text' } = item\r\n if (item.render) {\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n const { $createElement } = this\r\n return item.render.call(this, \r\n { h: $createElement, ctx: this },\r\n text ? typeof text == 'object' ? text[item.dataIndex] : text : '', \r\n record, index)\r\n }\r\n }\r\n } else if (mode !== BUILT_IN_TRIGGER.TEXT) {\r\n const { [`${mode}Meta`]: modeMeta } = item\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n return <span onClick={() => this.dispatchTrigger({ mode, record, modeMeta, index })} class={ mode }>{ text }</span>\r\n }\r\n }\r\n }\r\n return {\r\n ...item\r\n }\r\n })\r\n if (operations) {\r\n return [\r\n ...columnsOptions,\r\n {\r\n title: '操作',\r\n width: operations.width,\r\n key: 'action',\r\n fixed: 'right',\r\n scopedSlots: { customRender: 'action' }\r\n }\r\n ]\r\n }\r\n return columnsOptions\r\n } else {\r\n console.error('Error: columns is invalid, please check it')\r\n return []\r\n }\r\n },\r\n getButtonGroupElements () {\r\n const { elements } = this.buttonGroupMeta\r\n if (type.get(elements) === 'function') {\r\n return elements.call(this)\r\n } else if (type.get(elements) === 'array') {\r\n return elements\r\n } else {\r\n return []\r\n }\r\n },\r\n mapFields () {\r\n const { replaceFields = {} } = this.treeMeta\r\n const mapFields = type.isEmpty(replaceFields) ? this.replaceFields : replaceFields\r\n return mapFields\r\n }\r\n },\r\n async created () {\r\n // 确保全局数据池已初始化\r\n if (!window.__idooel_data_pool__) {\r\n console.error('Global data pool not initialized. Please check if runtime-context/globalDataPool.js is properly imported.')\r\n return\r\n }\r\n \r\n // 初始化数据池管理器(使用新的 runtime-context)\r\n try {\r\n this.model = createTreeTableModel('treeTableModel')\r\n \r\n if (!this.model) {\r\n throw new Error('Failed to create tree table model')\r\n }\r\n } catch (error) {\r\n console.error('Error creating tree table model:', error)\r\n this.model = null\r\n return\r\n }\r\n \r\n // 初始化 currentRowData(尝试从共享命名空间获取)\r\n this.initializeCurrentRowData()\r\n \r\n // 订阅数据池变化\r\n this.unsubscribe = this.model.subscribe('currentRowData', (event) => {\r\n this.currentRowData = event.value || {}\r\n this.$forceUpdate()\r\n })\r\n \r\n // 订阅共享数据变化\r\n this.unsubscribeShared = this.model.subscribeShared((event) => {\r\n // 当有新的共享数据时,更新当前组件的 currentRowData\r\n if (event.value && Object.keys(event.value).length > 0) {\r\n this.setCurrentRowData(event.value)\r\n }\r\n })\r\n \r\n if (this.showTree) {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.currentTreeNodeData = defaultTreeNode\r\n }\r\n \r\n const { params = {}, fieldMap = {}, overrideInit = false } = this.tableMeta\r\n const currentRowData = this.getCurrentRowData()\r\n const ctx = { \r\n ...this.currentTreeNodeData, \r\n _route: this.$route.query, \r\n currentRowData: currentRowData\r\n }\r\n \r\n const initQuerys = Object.assign({}, params, parseFieldMap(fieldMap, ctx))\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.INIT, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(initQuerys)\r\n }\r\n },\r\n methods: {\r\n initializeCurrentRowData () {\r\n if (!this.model) {\r\n console.warn('Model not initialized, skipping currentRowData initialization')\r\n return\r\n }\r\n \r\n // 检查是否有来自父组件的共享数据(比如 modal table 场景)\r\n const parentData = this.model.getSharedData()\r\n \r\n if (parentData && Object.keys(parentData).length > 0) {\r\n this.setCurrentRowData(parentData)\r\n return\r\n }\r\n \r\n // 可以根据路由参数、props 或其他来源设置初始的 currentRowData\r\n const { query } = this.$route\r\n \r\n // 示例:如果路由中有特定参数,可以设置为 currentRowData\r\n if (query.rowId || query.selectedId) {\r\n // 这里可以根据实际需求从服务端或其他地方获取数据\r\n // const presetData = { id: query.rowId, /* 其他字段 */ }\r\n // this.setCurrentRowData(presetData)\r\n }\r\n \r\n // 目前保持空对象,等待用户选择行数据\r\n },\r\n async refreshTreeData () {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.currentTreeNodeData = defaultTreeNode\r\n },\r\n dispatchTrigger ({ mode, record = {}, modeMeta = { } }) {\r\n switch (mode) {\r\n case BUILT_IN_TRIGGER.FSM:\r\n this[`${BUILT_IN_TRIGGER.FSM}Trigger`](record, modeMeta = type.isEmpty(modeMeta) ? { \r\n url: 'api-fsm/workbench/fsm/auditFlow',\r\n requestType: 'GET',\r\n fieldMap: {\r\n modelCode: 'modelCode',\r\n businessId: 'businessId'\r\n }\r\n } : modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_FORM:\r\n this.modalFormMeta = modeMeta\r\n this.showModalForm(modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_TABLE:\r\n this.modalTableMeta = modeMeta\r\n // 将当前行的 record 数据传递给 modal table\r\n this.showModalTable(modeMeta, record)\r\n break\r\n default:\r\n break\r\n }\r\n },\r\n handleCloseFsmModal () {\r\n this.showFsmModal = false\r\n },\r\n [`${BUILT_IN_TRIGGER.FSM}Trigger`] (record, meta) {\r\n this.fsmMeta = meta\r\n this.fsmContextProp = record\r\n this.showFsmModal = true\r\n },\r\n onChangeTableSelection (_, selectedRows = []) {\r\n if (this.currentTableMode === 'radio') {\r\n this.setCurrentTableSelection(selectedRows)\r\n this.$emit('on-change-table-selection', this.currentTableSelection)\r\n this.$emit('x:refresh-exposed', { exposed: this.exposed })\r\n } else {\r\n this.setCurrentTableSelection(selectedRows)\r\n this.$emit('on-change-table-selection', this.currentTableSelection)\r\n this.$emit('x:refresh-exposed', { exposed: this.exposed })\r\n }\r\n },\r\n setCurrentTableSelection (props = {}) {\r\n if (this.currentTableMode === 'radio') {\r\n this.$set(this, 'currentTableSelection', (type.isArray(props) && props.length > 0) ? props[0] : type.isObject(props) ? props : {})\r\n } else {\r\n this.$set(this, 'currentTableSelection', type.isArray(props) ? props : [])\r\n }\r\n },\r\n getCurrentTableSelection () {\r\n return this.currentTableSelection\r\n },\r\n setCurrentRowData (props = {}) {\r\n this.currentRowData = props\r\n if (this.model) {\r\n this.model.setCurrentRowData(props)\r\n } else {\r\n console.warn('Model not initialized, cannot setCurrentRowData in model')\r\n }\r\n },\r\n getCurrentRowData () {\r\n if (this.model) {\r\n return this.model.getCurrentRowData()\r\n }\r\n console.warn('Model not initialized, getCurrentRowData returning local data')\r\n return this.currentRowData || {}\r\n },\r\n cleanCurrentModelEffect (clearRowData = true) {\r\n this.setCurrentTableSelection()\r\n if (clearRowData) {\r\n this.setCurrentRowData({})\r\n }\r\n },\r\n [BUILT_IN_EVENT_NAMES.SUBMIT] (props = {}) {\r\n this.cleanCurrentModelEffect()\r\n this.requestTableData()\r\n },\r\n [BUILT_IN_EVENT_NAMES.EDIT] (props = {}) {\r\n const { record = {} } = props\r\n this.setCurrentRowData(record)\r\n this.modalFormMeta = this.editMeta\r\n this.modalFormValue = true\r\n },\r\n [BUILT_IN_EVENT_NAMES.CREATE] () {\r\n this.modalFormMeta = this.createMeta\r\n this.modalFormValue = true\r\n },\r\n showModalForm (modeMeta = {}) {\r\n if (type.isStr(modeMeta)) {\r\n const targetMeta = this.findMetaByKey(modeMeta)\r\n this.modalFormMeta = targetMeta\r\n } else {\r\n this.modalFormMeta = modeMeta\r\n }\r\n this.modalFormValue = true\r\n },\r\n showModalTable (modeMeta = {}, record = null) {\r\n // 获取当前行数据并设置到共享命名空间\r\n const currentRowData = record || this.getCurrentRowData()\r\n if (this.model) {\r\n this.model.setSharedData(currentRowData)\r\n } else {\r\n console.warn('Model not initialized, cannot setSharedData')\r\n }\r\n \r\n let targetMeta = modeMeta\r\n if (type.isStr(modeMeta)) {\r\n targetMeta = this.findMetaByKey(modeMeta)\r\n }\r\n \r\n // 解析 fieldMap 参数,使用完整的上下文包括 currentRowData\r\n if (targetMeta && targetMeta.fieldMap) {\r\n const { fieldMap, params = {} } = targetMeta\r\n \r\n const ctx = { \r\n ...this.currentTreeNodeData, \r\n _route: this.$route.query, \r\n ...currentRowData \r\n }\r\n const parsedParams = parseFieldMap(fieldMap, ctx)\r\n \r\n // 将当前的 currentRowData 传递给 modal table,通过 params\r\n targetMeta = {\r\n ...targetMeta,\r\n params: {\r\n ...params,\r\n ...parsedParams\r\n }\r\n }\r\n }\r\n \r\n this.modalTableMeta = targetMeta\r\n this.modalTableValue = true\r\n },\r\n closeModalForm () {\r\n this.modalFormValue = false\r\n },\r\n closeModalTable () {\r\n this.modalTableValue = false\r\n },\r\n findMetaByKey (key) {\r\n return this.$attrs[key] || {}\r\n },\r\n handleClickButtonGroup (props) {\r\n const { eventName, target } = props\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, modeMeta: targetMeta })\r\n this.$emit(eventName || 'click', { currentTreeNode: this.currentTreeNodeData })\r\n },\r\n async onSearch (props) {\r\n const { overrideInit = false } = this.tableMeta\r\n this.tableQuerys = Object.assign(this.tableQuerys, props)\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData()\r\n }\r\n },\r\n async selectTreeNode (selectedKeys, e) {\r\n const { fieldMap } = this.tableMeta\r\n this.currentTreeNodeData = e.node.$vnode.data.props.dataRef || {}\r\n //@deprecated '_' namespace is deprecated, please use 'exposed' instead\r\n const execFieldMapRet = parseFieldMap(fieldMap, { ...this.currentTreeNodeData, exposed: this.exposed, _route: this.exposed._route })\r\n const { overrideInit = false } = this.tableMeta\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(execFieldMapRet)\r\n }\r\n },\r\n async requestTreeData () {\r\n const { url, requestType = 'GET', params = {}, fieldMap = {} } = this.treeMeta\r\n const fieldMapRet = parseFieldMap(fieldMap, { ...this.currentTreeNodeData, _route: this.$route.query })\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n { ...params, ...fieldMapRet }\r\n ).then(resp => {\r\n const { data } = resp || {}\r\n return data\r\n })\r\n return ret\r\n },\r\n async onChangePage (page, pageSize) {\r\n this.tableData = await this.requestTableData({ currentPage: page, pageSize })\r\n },\r\n async requestTableData (props = {}) {\r\n const { url, requestType = 'GET', page = {} } = this.tableMeta\r\n const { pageSize = 10 } = page\r\n this.tableQuerys = Object.assign(this.tableQuerys, { currentPage: 1, pageSize }, props)\r\n this.$emit(RESERVE_EVENT_NAMES.WATCH, { ...this.exposed })\r\n this.loading = true\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n this.tableQuerys\r\n ).then(resp => {\r\n const { data = [], count } = resp || {}\r\n this.total = count\r\n this.loading = false\r\n return (data || []).map(item => {\r\n delete item.children\r\n return {\r\n key: uuidv4(),\r\n ...item\r\n }\r\n })\r\n })\r\n this.cleanCurrentModelEffect(false) // 不清空 currentRowData,除非明确需要\r\n this.tableData = ret\r\n return ret\r\n },\r\n calculateTableHeight () {\r\n const currentViewportHeight = window.innerHeight\r\n const tableRef = this.$refs[this.tableRef]\r\n const { top: tableToTop, width } = tableRef.$el.getBoundingClientRect()\r\n this.tableWidth = width\r\n this.tableHeight = currentViewportHeight - tableToTop - this.overHeight\r\n },\r\n calculateTreeHeight () {\r\n if (!this.showTree) return\r\n const modelTableContainerRef = this.$refs[this.modelTableContainerRef]\r\n const { height } = modelTableContainerRef.getBoundingClientRect()\r\n this.treeWrapperHeight = height\r\n }\r\n },\r\n mounted () {\r\n this.calculateTableHeight()\r\n this.$nextTick(() => {\r\n this.calculateTreeHeight()\r\n })\r\n this.resizeObserverModelTableWrapper = new ResizeObserver(entries => {\r\n for (const _ of entries) {\r\n requestAnimationFrame(() => {\r\n this.calculateTableHeight()\r\n })\r\n }\r\n })\r\n this.resizeObserverModelTableWrapper.observe(this.$refs[this.modelTableWrapper])\r\n },\r\n destroyed () {\r\n this.resizeObserverModelTableWrapper.disconnect()\r\n if (this.model) {\r\n // 清理订阅\r\n if (this.unsubscribe) {\r\n this.unsubscribe()\r\n }\r\n if (this.unsubscribeShared) {\r\n this.unsubscribeShared()\r\n }\r\n // 清理模型数据\r\n if (this.model) {\r\n this.model.cleanup()\r\n }\r\n }\r\n },\r\n async activated () {\r\n // 重新计算表格高度(应对窗口大小变化)\r\n this.$nextTick(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n })\r\n \r\n // 刷新列表数据\r\n const { overrideInit = false } = this.tableMeta\r\n if (overrideInit) {\r\n // 如果使用自定义初始化模式,触发 INIT 事件\r\n this.$emit(RESERVE_EVENT_NAMES.INIT, { ...this.exposed })\r\n } else {\r\n // 使用当前查询参数刷新表格数据\r\n await this.requestTableData()\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.ele {\r\n &.model__tree-table {\r\n background: transparent; \r\n display: flex;\r\n flex-direction: row;\r\n width: 100%;\r\n .model__tree-table--container {\r\n .model__tree--wrapper {\r\n width: 240px;\r\n background: #fff;\r\n flex-shrink: 0;\r\n padding: 16px;\r\n box-sizing: border-box;\r\n margin-right: 16px;\r\n overflow-y: auto;\r\n }\r\n }\r\n .model__table--container {\r\n width: 100%;\r\n min-width: 0;\r\n background: #fff;\r\n .model__table--title {\r\n .model__table-title--bar {\r\n width: 100%;\r\n height: 8px;\r\n background: var(--idooel-primary-color);\r\n border-top-left-radius: 4px;\r\n border-top-right-radius: 4px;\r\n }\r\n .model__table-title--text {\r\n text-align: left;\r\n padding: 16px;\r\n font-size: 16px;\r\n font-weight: bold;\r\n background: #fff;\r\n border-bottom: 1px solid;\r\n border-color: var(--idoole-black-016);\r\n }\r\n }\r\n .model__table--wrapper {\r\n background: #fff;\r\n .button-row__area {\r\n width: 100%;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding-top: 16px;\r\n padding-bottom: 8px;\r\n padding-right: 16px;\r\n }\r\n .g-table__wrapper {\r\n .fsm {\r\n cursor: pointer;\r\n color: var(--idooel-primary-color);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</style>\r\n",".ele.model__tree-table {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n}\n.ele.model__tree-table .model__table--container {\n width: 100%;\n min-width: 0;\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper {\n background: #fff;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined });
3608
3631
 
3609
3632
  };
3610
3633
  /* scoped */
3611
- const __vue_scope_id__$A = "data-v-0f7afe7a";
3634
+ const __vue_scope_id__$A = "data-v-306c600f";
3612
3635
  /* module identifier */
3613
3636
  const __vue_module_identifier__$A = undefined;
3614
3637
  /* functional template */
@@ -5844,7 +5867,9 @@ var script$t = {
5844
5867
  saveToServerAsyncPageTimer: null,
5845
5868
  uploadRefId: null,
5846
5869
  // 多选且外部无值时,预先生成 groupId,确保首次上传参数完整
5847
- groupId: this.multiple && !this.value ? v4() : null
5870
+ groupId: this.multiple && !this.value ? v4() : null,
5871
+ //TODO
5872
+ iconIsZhWrod: false
5848
5873
  };
5849
5874
  },
5850
5875
  created() {
@@ -6772,11 +6797,11 @@ __vue_render__$t._withStripped = true;
6772
6797
  /* style */
6773
6798
  const __vue_inject_styles__$t = function (inject) {
6774
6799
  if (!inject) return
6775
- inject("data-v-1b3ec41c_0", { source: "[data-v-1b3ec41c] .ele-upload__inner {\n opacity: 1 !important;\n cursor: pointer;\n border: 1px dashed var(--idooel-form-title-border-color);\n background: var(--idooel-form-upload-bg-color) !important;\n}\n[data-v-1b3ec41c] .ele-upload__inner:hover {\n border-color: var(--idooel-form-upload-border-hover-color);\n}\n[data-v-1b3ec41c] .ele-upload__inner {\n border-radius: var(--idooel-form-border-radius);\n}\n.ele-upload__wrapper[data-v-1b3ec41c] {\n width: 100%;\n}\n.ele-upload__wrapper .ele-upload__area[data-v-1b3ec41c] {\n padding: 16px;\n width: 100%;\n height: 80px;\n display: flex;\n flex-direction: row;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon[data-v-1b3ec41c] {\n color: var(--idooel-primary-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n font-size: 16x;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload[data-v-1b3ec41c] {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon[data-v-1b3ec41c] {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text[data-v-1b3ec41c] {\n margin-left: 16px;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__message[data-v-1b3ec41c] {\n font-size: 16px;\n color: var(--idoole-black-088);\n text-align: left;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__ext[data-v-1b3ec41c] {\n text-align: left;\n font-size: 14px;\n color: var(--idoole-black-06);\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item[data-v-1b3ec41c] {\n width: 100%;\n margin-top: 8px;\n padding: 8px 12px;\n border-radius: var(--idooel-form-border-radius);\n background: var(--idooel-form-upload-bg-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__suffix--icon[data-v-1b3ec41c] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name[data-v-1b3ec41c] {\n flex: 1;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n font-size: 14px;\n margin-left: 8px;\n cursor: pointer;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name .ele-file__inner[data-v-1b3ec41c] {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete[data-v-1b3ec41c] {\n margin-left: 8px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete .ele-file__delete--icon[data-v-1b3ec41c] {\n margin-left: 8px;\n cursor: pointer;\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["C:\\Users\\ZOU\\Code\\OnlineStudy\\base-elearning-frontend-model\\packages\\components\\packages\\upload\\src\\index.vue","index.vue"],"names":[],"mappings":"AA64BA;EACA,qBAAA;EACA,eAAA;EACA,wDAAA;EACA,yDAAA;AC54BA;AD64BA;EACA,0DAAA;AC34BA;ADq4BA;EAQA,+CAAA;AC14BA;AD44BA;EACA,WAAA;ACz4BA;AD04BA;EACA,aAAA;EACA,WAAA;EACA,YAAA;EACA,aAAA;EACA,mBAAA;ACx4BA;ADy4BA;EACA,kCAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,cAAA;ACv4BA;ADw4BA;EACA,eAAA;EACA,kCAAA;ACt4BA;ADw4BA;EACA,eAAA;EACA,kCAAA;ACt4BA;ADy4BA;EACA,iBAAA;ACv4BA;ADw4BA;EACA,eAAA;EACA,8BAAA;EACA,gBAAA;ACt4BA;ADw4BA;EACA,gBAAA;EACA,eAAA;EACA,6BAAA;ACt4BA;AD24BA;EACA,WAAA;EACA,eAAA;EACA,iBAAA;EACA,+CAAA;EACA,8CAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;ACz4BA;AD04BA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;ACx4BA;AD04BA;EACA,OAAA;EACA,gBAAA;EACA,mBAAA;EACA,gBAAA;EACA,eAAA;EACA,gBAAA;EACA,eAAA;ACx4BA;ADy4BA;EACA,gBAAA;EACA,uBAAA;ACv4BA;AD04BA;EACA,gBAAA;ACx4BA;ADy4BA;EACA,gBAAA;EACA,eAAA;ACv4BA;;AAEA,oCAAoC","file":"index.vue","sourcesContent":["<template>\r\n <div class=\"ele-upload__wrapper\">\r\n <FileUpload\r\n class=\"ele-upload__inner\"\r\n v-show=\"isShowUploadContainer\"\r\n v-model=\"files\"\r\n :ref=\"uploadRef\"\r\n :drop=\"drop\"\r\n :chunk-enabled=\"chunkEnabled\"\r\n :chunk=\"chunkConfig\"\r\n :accept=\"accept\"\r\n :size=\"fileSizeLimit\"\r\n :post-action=\"postAction\"\r\n :multiple=\"multiple\"\r\n :headers=\"headers\"\r\n :maximum=\"getMaximum\"\r\n :data=\"uploadParams\"\r\n @input-file=\"onWatchInputFiles\"\r\n @input=\"onWatchFiles\"\r\n style=\"width: 100%;\">\r\n <section class=\"ele-upload__area\">\r\n <div class=\"ele-upload__area--icon\">\r\n <template v-if=\"iconIsZhWrod\">\r\n {{ icon }}\r\n </template>\r\n <template v-else>\r\n <ele-icon :type=\"icon\"></ele-icon>\r\n </template>\r\n </div>\r\n <div class=\"ele-upload__area--text\">\r\n <div class=\"ele-upload__message\" v-if=\"message\" v-html=\"message\"></div>\r\n <div class=\"ele-upload__message\" v-else>单击或拖动文件到该区域以上传</div>\r\n <div class=\"ele-upload__ext\" v-if=\"ext\" v-html=\"ext\"></div>\r\n <div class=\"ele-upload__ext\" v-else>文件小于{{ size }}M</div>\r\n </div>\r\n </section>\r\n </FileUpload>\r\n <section class=\"ele-files__wrapper\">\r\n <!-- 显示正在上传的文件(有进度条) -->\r\n <div class=\"ele-file__item\" v-for=\"(file, idx) in uploadingFiles\" :key=\"`uploading-${idx}`\">\r\n <div class=\"ele-file__suffix--icon\">\r\n <ele-icon :type=\"fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'\"></ele-icon>\r\n </div>\r\n <div class=\"ele-file__name\">\r\n <div class=\"ele-file__inner\">{{ file.name }}</div>\r\n <div v-if=\"file.progress !== undefined\" class=\"ele-uplpad__progress\">\r\n <a-progress :strokeWidth=\"2\" :percent=\"Number(file.progress)\" size=\"small\" />\r\n </div>\r\n </div>\r\n <div class=\"ele-file__delete\" v-if=\"file.success || file.error\">\r\n <span class=\"ele-file__size\">{{ (file.size / byteConversion).toFixed(2) }}M</span>\r\n <span class=\"ele-file__delete--icon\" @click=\"handleClickDelete(file)\">\r\n <ele-icon type=\"delete\"></ele-icon>\r\n </span>\r\n </div>\r\n </div>\r\n \r\n <!-- 显示已上传完成的文件 -->\r\n <div class=\"ele-file__item\" v-for=\"(file, idx) in completedFiles\" :key=\"`completed-${idx}`\">\r\n <div class=\"ele-file__suffix--icon\">\r\n <ele-icon :type=\"fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'\"></ele-icon>\r\n </div>\r\n <div class=\"ele-file__name\">\r\n <div class=\"ele-file__inner\" @click=\"handleClickDownload(file)\">{{ file.name }}</div>\r\n </div>\r\n <div class=\"ele-file__delete\">\r\n <span class=\"ele-file__size\">{{ (file.size / byteConversion).toFixed(2) }}M</span>\r\n <span class=\"ele-file__delete--icon\" @click=\"handleClickDelete(file)\">\r\n <ele-icon type=\"delete\"></ele-icon>\r\n </span>\r\n </div>\r\n </div>\r\n </section>\r\n </div>\r\n</template>\r\n\r\n<script>\r\nimport FileUpload from 'vue-upload-component'\r\nimport { v4 as uuidv4 } from 'uuid'\r\nimport { route, net, type } from '@idooel/shared'\r\n\r\n// 常量定义\r\nconst CONSTANTS = {\r\n DEFAULT_URL: 'zuul/api-file/workbench/file',\r\n DEFAULT_ICON: '上传',\r\n DEFAULT_SIZE: 100,\r\n DEFAULT_MESSAGE: '单击或拖动文件到该区域以上传',\r\n DEFAULT_MAXIMUM: 20,\r\n BYTE_CONVERSION: 1024 * 1024,\r\n CHUNK_MIN_SIZE: 3 * 1024 * 1024,\r\n CHUNK_MAX_ACTIVE: 3,\r\n CHUNK_MAX_RETRIES: 5,\r\n SAVE_INTERVAL: 2000\r\n}\r\n\r\n// 文件后缀图标映射\r\nconst FILE_SUFFIX_ICONS = {\r\n 'doc': { name: 'icon-doc' },\r\n 'html': { name: 'icon-html' },\r\n 'mp4': { name: 'icon-mp' },\r\n 'pdf': { name: 'icon-pdf' },\r\n 'ppt': { name: 'icon-ppt' },\r\n 'psd': { name: 'icon-psd' },\r\n 'rtf': { name: 'icon-rtf' },\r\n 'txt': { name: 'icon-txt' },\r\n 'vis': { name: 'icon-vis' },\r\n 'xls': { name: 'icon-xls' },\r\n 'xml': { name: 'icon-xml' },\r\n 'zip': { name: 'icon-zip' },\r\n 'jpg': { name: 'icon-img' },\r\n 'mp3': { name: 'icon-mp1' }\r\n}\r\n\r\nexport default {\r\n name: 'ele-upload',\r\n components: {\r\n FileUpload\r\n },\r\n model: {\r\n prop: 'value',\r\n event: 'change'\r\n },\r\n props: {\r\n url: {\r\n type: String,\r\n default: CONSTANTS.DEFAULT_URL\r\n },\r\n icon: {\r\n type: String,\r\n default: CONSTANTS.DEFAULT_ICON\r\n },\r\n size: {\r\n type: Number,\r\n default: CONSTANTS.DEFAULT_SIZE\r\n },\r\n message: {\r\n type: String,\r\n default: CONSTANTS.DEFAULT_MESSAGE\r\n },\r\n ext: {\r\n type: String\r\n },\r\n extensions: {\r\n type: String\r\n },\r\n accept: {\r\n type: String\r\n },\r\n maximum: {\r\n type: Number,\r\n default: CONSTANTS.DEFAULT_MAXIMUM\r\n },\r\n multiple: {\r\n type: Boolean,\r\n default: false\r\n },\r\n drop: {\r\n type: Boolean,\r\n default: true\r\n },\r\n value: {\r\n type: [String, Array]\r\n },\r\n querys: {\r\n type: Object,\r\n default: () => ({\r\n _csrf: localStorage.getItem('token')\r\n })\r\n },\r\n headers: {\r\n type: Object,\r\n default: () => ({\r\n 'X-XSRF-TOKEN': localStorage.getItem('token')\r\n })\r\n },\r\n byteConversion: {\r\n type: Number,\r\n default: CONSTANTS.BYTE_CONVERSION\r\n },\r\n chunkEnabled: {\r\n type: Boolean,\r\n default: true\r\n }\r\n },\r\n data() {\r\n return {\r\n // 文件状态管理\r\n files: [], // vue-upload-component 管理的文件\r\n buildedFiles: [], // 已构建完成的文件列表\r\n \r\n // 上传状态管理\r\n saveToServerAsyncPageTimer: null,\r\n uploadRefId: null,\r\n // 多选且外部无值时,预先生成 groupId,确保首次上传参数完整\r\n groupId: (this.multiple && !this.value) ? uuidv4() : null\r\n }\r\n },\r\n created() {\r\n // 多文件模式下,如果没有外部 groupId,则生成一个\r\n if (this.multiple && !this.value) {\r\n this.groupId = uuidv4()\r\n console.log('Created with new groupId:', this.groupId)\r\n }\r\n },\r\n watch: {\r\n value: {\r\n async handler(value) {\r\n if (type.isEmpty(value)) {\r\n this.resetFiles()\r\n } else if (this.multiple) {\r\n this.handleMultipleFileValue(value)\r\n } else {\r\n this.handleSingleFileValue()\r\n }\r\n },\r\n immediate: true\r\n }\r\n },\r\n computed: {\r\n // ==================== 基础配置 ====================\r\n prefixPath() {\r\n return window.prefixPath\r\n },\r\n \r\n uploadRef() {\r\n if (!this.uploadRefId) {\r\n this.uploadRefId = `uploadRef_${uuidv4()}`\r\n }\r\n return this.uploadRefId\r\n },\r\n \r\n iconIsZhWord() {\r\n return type.isZhWord(this.icon)\r\n },\r\n \r\n // ==================== 上传配置 ====================\r\n uploadParams() {\r\n return this.multiple ? { groupID: this.groupId } : {}\r\n },\r\n \r\n fileSizeLimit() {\r\n return this.size * this.byteConversion\r\n },\r\n \r\n postAction() {\r\n const queryString = route.toQueryString(this.querys)\r\n return `${this.prefixPath}${this.url}?${queryString}`\r\n },\r\n \r\n chunkConfig() {\r\n return {\r\n action: `${this.prefixPath}zuul/api-file/workbench/file/temp/chunk/vue`,\r\n headers: { ...this.headers },\r\n minSize: CONSTANTS.CHUNK_MIN_SIZE,\r\n maxActive: CONSTANTS.CHUNK_MAX_ACTIVE,\r\n maxRetries: CONSTANTS.CHUNK_MAX_RETRIES,\r\n startBody: { override: true, path: '/cw' },\r\n uploadBody: { override: true, path: '/cw' },\r\n finishBody: { override: true, path: '/cw' }\r\n }\r\n },\r\n \r\n getMaximum() {\r\n return this.multiple ? this.maximum : 1\r\n },\r\n \r\n // ==================== 文件状态 ====================\r\n uploadingFiles() {\r\n // 筛选条件:有进度信息 且 (未成功 或 成功但还没有fileID的切片上传文件)\r\n // 这样可以确保切片上传完成但异步保存未完成的文件继续显示,避免闪烁\r\n return this.files.filter(file => {\r\n if (file.progress === undefined) return false\r\n if (!file.success) return true\r\n \r\n // 检查文件是否已经有 fileID(可能在 file.fileID 或 file.response.data.fileID)\r\n const hasFileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)\r\n \r\n // 切片上传成功但还没有完成异步保存(没有fileID)的文件继续显示\r\n // 普通上传成功且已有 fileID 的文件不显示(会在 completedFiles 中显示)\r\n return file.success && !hasFileID\r\n })\r\n },\r\n \r\n completedFiles() {\r\n return this.buildedFiles.filter(file => file.fileID)\r\n },\r\n \r\n totalFiles() {\r\n return this.uploadingFiles.length + this.completedFiles.length\r\n },\r\n \r\n isFileUploadSuccessed() {\r\n const currentUploadingFiles = this.files.filter(file => file.response !== undefined)\r\n if (currentUploadingFiles.length === 0) {\r\n return this.buildedFiles.length > 0\r\n }\r\n return currentUploadingFiles.every(file => file.success)\r\n },\r\n \r\n isShowUploadContainer() {\r\n const maxFiles = this.multiple ? this.maximum : 1\r\n return this.totalFiles < maxFiles\r\n },\r\n \r\n // ==================== 文件信息 ====================\r\n fileSuffixIcon() {\r\n return FILE_SUFFIX_ICONS\r\n },\r\n \r\n fileIds() {\r\n if (this.multiple) {\r\n return this.groupId\r\n } else {\r\n const fileIds = this.buildedFiles.map(file => file.fileID)\r\n return fileIds[0]\r\n }\r\n },\r\n \r\n fileResponseData() {\r\n return this.multiple ? this.buildedFiles : this.buildedFiles[0]\r\n }\r\n },\r\n methods: {\r\n // ==================== 文件管理 ====================\r\n \r\n /**\r\n * 从多个数组中移除文件\r\n */\r\n removeFromArrays(file, arrays, key = 'fileID') {\r\n return arrays.map(arr => \r\n arr.filter(item => {\r\n // 如果 fileID 都存在且相等,则移除\r\n if (item[key] && file[key] && item[key] === file[key]) {\r\n return false\r\n }\r\n // 如果 id 都存在且相等,则移除\r\n if (item.id && file.id && item.id === file.id) {\r\n return false\r\n }\r\n // 如果文件名相同,也考虑移除(额外的安全检查)\r\n if (item.name && file.name && item.name === file.name && item[key] === file[key]) {\r\n return false\r\n }\r\n // 否则保留\r\n return true\r\n })\r\n )\r\n },\r\n \r\n /**\r\n * 检查文件是否为新文件\r\n */\r\n isNewFile(newFile, existingFiles, key = 'fileID') {\r\n return newFile[key] && !existingFiles.some(existing => existing[key] === newFile[key])\r\n },\r\n \r\n /**\r\n * 合并文件数据\r\n */\r\n mergeFileData(uploadFile) {\r\n return {\r\n ...uploadFile.response.data,\r\n ...uploadFile\r\n }\r\n },\r\n \r\n /**\r\n * 初始化文件列表\r\n */\r\n async initializeFiles() {\r\n if (!this.value) return\r\n \r\n if (this.multiple) {\r\n await this.fetchFilesWithGroupId()\r\n } else {\r\n await this.fetchFileWithFileId()\r\n }\r\n },\r\n \r\n /**\r\n * 获取多文件组\r\n */\r\n async fetchFilesWithGroupId() {\r\n try {\r\n const response = await net.get(`/api-file/workbench/file/group/${this.value}`)\r\n const data = response.data || []\r\n \r\n // 只有在没有现有文件时才设置初始文件列表\r\n if (this.buildedFiles.length === 0) {\r\n this.buildedFiles = data\r\n console.log('Initial files loaded:', this.buildedFiles.length)\r\n } else {\r\n console.log('Keep existing files, skip initial load')\r\n }\r\n } catch (error) {\r\n console.log('fetchFilesWithGroupId error:', error)\r\n console.log('Keep current files, do not clear due to API error')\r\n }\r\n },\r\n \r\n /**\r\n * 获取单文件\r\n */\r\n async fetchFileWithFileId() {\r\n try {\r\n const response = await net.get(`/api-file/file/${this.value}`)\r\n const data = response.data\r\n this.buildedFiles = [data]\r\n this.files = [data]\r\n } catch (error) {\r\n console.log('fetchFileWithFileId error:', error)\r\n }\r\n },\r\n\r\n /**\r\n * 调用服务器删除文件 API\r\n */\r\n async deleteFiles (ids) {\r\n if (!ids) {\r\n console.warn('No fileID provided for deletion')\r\n return\r\n }\r\n \r\n if (type.isArray(ids)) {\r\n ids = ids.join(',')\r\n }\r\n \r\n try {\r\n await net.remove(`/api-file/workbench/file?ids=${ids}`)\r\n console.log('File deleted from server:', ids)\r\n } catch (error) {\r\n console.error('Failed to delete file from server:', error)\r\n throw error // 重新抛出错误,让调用方处理\r\n }\r\n },\r\n \r\n /**\r\n * 处理文件删除\r\n */\r\n async handleClickDelete(file) {\r\n const { fileID, name } = file\r\n console.log('Deleting file:', { name, fileID })\r\n \r\n // 先从上传组件中移除文件\r\n if (this.$refs[this.uploadRef]) {\r\n this.$refs[this.uploadRef].remove(file)\r\n }\r\n \r\n // 从所有数组中移除文件\r\n [this.files, this.buildedFiles] = this.removeFromArrays(file, [this.files, this.buildedFiles])\r\n \r\n console.log('After deletion - files:', this.files.length, 'buildedFiles:', this.buildedFiles.length)\r\n \r\n // 如果有 fileID,调用服务器删除 API\r\n if (fileID) {\r\n try {\r\n await this.deleteFiles(fileID)\r\n } catch (error) {\r\n console.error('Failed to delete file from server:', error)\r\n // 可以选择提示用户或回滚操作\r\n // 这里仅记录错误,不影响前端显示\r\n }\r\n }\r\n \r\n // 多文件模式下,如果删除最后一个文件,重置 groupId\r\n if (this.multiple && this.buildedFiles.length === 0) {\r\n this.groupId = null\r\n console.log('Reset groupId after deleting last file')\r\n }\r\n \r\n // 触发 change 事件\r\n this.$emit('change', this.fileIds)\r\n },\r\n \r\n /**\r\n * 处理文件下载\r\n */\r\n handleClickDownload(file) {\r\n const { fileID: fileId } = file\r\n window.open(`/api-file/workbench/file/stream/${fileId}?origin=true`)\r\n },\r\n // ==================== 上传处理 ====================\r\n \r\n /**\r\n * 处理文件上传状态变化\r\n */\r\n onWatchFiles(files) {\r\n console.log('onWatchFiles called with files:', files.length)\r\n console.log('Current buildedFiles:', this.buildedFiles.length)\r\n \r\n // 更新文件状态\r\n this.files = files\r\n \r\n // 处理已上传成功的文件\r\n this.processUploadedFiles(files)\r\n \r\n // 检查上传是否完成\r\n if (this.isFileUploadSuccessed) {\r\n this.$emit('change', this.fileIds)\r\n this.$emit('on-success', this.fileResponseData)\r\n }\r\n },\r\n \r\n /**\r\n * 处理已上传成功的文件\r\n */\r\n processUploadedFiles(files) {\r\n // 处理所有有响应的文件(包括正在上传和已完成的)\r\n const uploadedFiles = files.filter(file => file.response)\r\n const newBuildedFiles = uploadedFiles.map(file => this.mergeFileData(file))\r\n \r\n if (this.multiple) {\r\n this.processMultipleFiles(newBuildedFiles)\r\n } else {\r\n // 单文件模式:只保留最新的文件\r\n this.buildedFiles = newBuildedFiles\r\n \r\n // 从 files 数组中移除已完成的文件(普通上传),避免重复显示\r\n if (newBuildedFiles.length > 0 && newBuildedFiles[0].fileID) {\r\n this.$nextTick(() => {\r\n this.files = this.files.filter(file => {\r\n const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)\r\n return fileID !== newBuildedFiles[0].fileID\r\n })\r\n console.log('Removed completed file from upload list (single mode)')\r\n })\r\n }\r\n }\r\n \r\n this.logFileStatus()\r\n },\r\n \r\n /**\r\n * 处理多文件模式\r\n */\r\n processMultipleFiles(newBuildedFiles) {\r\n // 获取已存在的文件ID集合\r\n const existingFileIds = new Set(this.buildedFiles.map(f => f.fileID).filter(id => id))\r\n \r\n // 过滤出真正的新文件\r\n const trulyNewFiles = newBuildedFiles.filter(newFile => \r\n this.isNewFile(newFile, this.buildedFiles)\r\n )\r\n \r\n console.log('Existing fileIDs:', Array.from(existingFileIds))\r\n console.log('New uploaded files:', newBuildedFiles.map(f => ({ name: f.name, fileID: f.fileID })))\r\n console.log('Truly new files:', trulyNewFiles.map(f => ({ name: f.name, fileID: f.fileID })))\r\n \r\n // 将新文件追加到现有文件列表\r\n if (trulyNewFiles.length > 0) {\r\n this.buildedFiles = [...this.buildedFiles, ...trulyNewFiles]\r\n console.log('Added new files, total buildedFiles:', this.buildedFiles.length)\r\n \r\n // 从 files 数组中移除已完成的文件(普通上传),避免重复显示\r\n this.$nextTick(() => {\r\n const completedFileIDs = new Set(trulyNewFiles.map(f => f.fileID))\r\n this.files = this.files.filter(file => {\r\n const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)\r\n return !completedFileIDs.has(fileID)\r\n })\r\n console.log('Removed completed files from upload list')\r\n })\r\n }\r\n \r\n // 更新现有文件的状态(包括新添加的文件)\r\n this.updateExistingFiles(newBuildedFiles)\r\n },\r\n \r\n /**\r\n * 更新现有文件状态\r\n */\r\n updateExistingFiles(newBuildedFiles) {\r\n this.buildedFiles = this.buildedFiles.map(existingFile => {\r\n const updatedFile = newBuildedFiles.find(newFile => newFile.fileID === existingFile.fileID)\r\n return updatedFile ? { ...existingFile, ...updatedFile } : existingFile\r\n })\r\n },\r\n \r\n /**\r\n * 记录文件状态日志\r\n */\r\n logFileStatus() {\r\n console.log('Final buildedFiles:', this.buildedFiles.length)\r\n console.log('buildedFiles details:', this.buildedFiles.map(f => ({ name: f.name, fileID: f.fileID, success: f.success })))\r\n console.log('Uploading files:', this.uploadingFiles.length)\r\n console.log('Completed files:', this.completedFiles.length)\r\n },\r\n // ==================== 异步处理 ====================\r\n \r\n /**\r\n * 异步保存文件到服务器\r\n * @param {FormData} payloads - 上传参数\r\n * @param {Object} uploadingFile - 正在上传的文件对象(可选)\r\n */\r\n async saveToServerAsyncPage(payloads = {}, uploadingFile = null) {\r\n try {\r\n const response = await net.post('zuul/api-file/workbench/file/temp/saveToServerAsyncPage', payloads, { \r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'\r\n }\r\n })\r\n \r\n const { data } = response\r\n if (data !== 'saveToServerAsyncPage') {\r\n clearInterval(this.saveToServerAsyncPageTimer)\r\n console.log('Chunk upload async save completed, file data:', data)\r\n \r\n const { fileID, groupID, name } = data\r\n \r\n // 根据上传模式处理文件信息\r\n if (this.multiple) {\r\n // 多文件模式:更新 groupID 并追加文件到列表\r\n if (groupID) {\r\n this.groupId = groupID\r\n console.log('Updated groupId from server:', groupID)\r\n }\r\n \r\n // 检查文件是否已存在,避免重复添加\r\n const fileExists = this.buildedFiles.some(file => file.fileID === fileID)\r\n if (!fileExists && fileID) {\r\n // 将服务器返回的完整文件信息追加到文件列表\r\n // 添加必要的显示属性\r\n const fileData = {\r\n ...data,\r\n success: true // 标记为上传成功\r\n }\r\n \r\n // 先添加到已完成列表\r\n this.buildedFiles.push(fileData)\r\n console.log('File added to list:', name, 'Total files:', this.buildedFiles.length)\r\n \r\n // 使用 nextTick 确保视图更新后再移除上传列表中的文件,避免闪烁\r\n this.$nextTick(() => {\r\n // 从上传列表中精确移除对应的文件\r\n if (uploadingFile) {\r\n // 如果有文件对象引用,直接移除该文件\r\n this.files = this.files.filter(file => file.id !== uploadingFile.id)\r\n console.log('Removed uploading file by id:', uploadingFile.id)\r\n } else {\r\n // 否则根据文件名移除\r\n this.files = this.files.filter(file => file.name !== name)\r\n console.log('Removed uploading file by name:', name)\r\n }\r\n })\r\n \r\n // 触发 change 事件通知父组件\r\n this.$emit('change', this.fileIds)\r\n this.$emit('on-success', this.fileResponseData)\r\n } else {\r\n console.log('File already exists or invalid fileID, skip adding')\r\n }\r\n } else {\r\n // 单文件模式:替换文件列表\r\n if (fileID) {\r\n const fileData = {\r\n ...data,\r\n success: true // 标记为上传成功\r\n }\r\n this.buildedFiles = [fileData]\r\n this.files = [] // 清空上传列表\r\n console.log('Single file updated:', name)\r\n \r\n // 触发 change 事件通知父组件更新 value\r\n this.$emit('change', fileID)\r\n this.$emit('on-success', data)\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error('saveToServerAsyncPage error:', error)\r\n clearInterval(this.saveToServerAsyncPageTimer)\r\n }\r\n },\r\n \r\n // ==================== 文件验证 ====================\r\n \r\n /**\r\n * 验证文件类型\r\n */\r\n validateFileType(file) {\r\n if (!file || !file.name) {\r\n console.log('文件或文件名不存在')\r\n return false\r\n }\r\n \r\n const fileExt = this.getFileExtension(file.name)\r\n console.log('文件扩展名:', fileExt)\r\n \r\n if (this.extensions) {\r\n const allowedExts = this.getAllowedExtensions()\r\n console.log('允许的扩展名:', allowedExts)\r\n \r\n if (!allowedExts.includes(fileExt)) {\r\n console.log('扩展名不在允许列表中')\r\n this.$message.error(`不支持的文件类型 \"${fileExt}\",请上传 ${this.extensions} 格式的文件`)\r\n return false\r\n }\r\n }\r\n \r\n console.log('文件类型验证通过')\r\n return true\r\n },\r\n \r\n /**\r\n * 获取文件扩展名\r\n */\r\n getFileExtension(fileName) {\r\n return fileName.toLowerCase().substring(fileName.lastIndexOf('.'))\r\n },\r\n \r\n /**\r\n * 获取允许的扩展名列表\r\n */\r\n getAllowedExtensions() {\r\n return this.extensions.toLowerCase()\r\n .split(',')\r\n .map(ext => ext.trim().startsWith('.') ? ext.trim() : `.${ext.trim()}`)\r\n },\r\n // ==================== 事件处理 ====================\r\n \r\n /**\r\n * 处理文件输入事件\r\n */\r\n onWatchInputFiles(newFile, oldFile) {\r\n if (newFile && !oldFile) {\r\n this.handleFileAdd(newFile)\r\n } else if (newFile && oldFile) {\r\n this.handleFileUpdate(newFile)\r\n } else if (!newFile && oldFile) {\r\n this.handleFileDelete()\r\n }\r\n \r\n // 激活上传组件\r\n this.activateUploadComponent(newFile, oldFile)\r\n },\r\n \r\n /**\r\n * 生成唯一的随机数\r\n */\r\n generateUniqueRandom() {\r\n // 使用 crypto.getRandomValues 生成更安全的随机数\r\n if (window.crypto && window.crypto.getRandomValues) {\r\n const array = new Uint32Array(1)\r\n window.crypto.getRandomValues(array)\r\n return array[0].toString()\r\n }\r\n // 降级方案:使用 Math.random 结合时间戳\r\n return Math.random().toString(36).substr(2, 9) + Date.now().toString(36)\r\n },\r\n\r\n /**\r\n * 处理文件添加\r\n */\r\n handleFileAdd(newFile) {\r\n // 为每个文件生成唯一的随机数\r\n const uniqueRandom = this.generateUniqueRandom()\r\n newFile.postAction = newFile.postAction + '&_t=' + uniqueRandom\r\n console.log('add file:', newFile)\r\n console.log('extensions:', this.extensions)\r\n console.log('accept:', this.accept)\r\n \r\n // 生成或使用 groupId\r\n this.ensureGroupId()\r\n \r\n // 验证文件类型\r\n if (!this.validateFileType(newFile)) {\r\n this.removeInvalidFile(newFile)\r\n return\r\n }\r\n console.log('文件类型验证通过,继续上传')\r\n },\r\n \r\n /**\r\n * 处理文件更新\r\n */\r\n handleFileUpdate(newFile) {\r\n console.log('update', newFile)\r\n const { success, active, chunk, response } = newFile\r\n \r\n if (chunk && success && !active) {\r\n console.log('chunk end')\r\n this.handleChunkComplete(response, newFile)\r\n }\r\n },\r\n \r\n /**\r\n * 处理文件删除\r\n */\r\n handleFileDelete() {\r\n console.log('delete')\r\n },\r\n \r\n /**\r\n * 确保 groupId 存在\r\n */\r\n ensureGroupId() {\r\n console.log('onWatchInputFiles - multiple:', this.multiple, 'current groupId:', this.groupId)\r\n \r\n if (this.multiple && !this.groupId) {\r\n this.groupId = uuidv4()\r\n console.log('Generated new groupId:', this.groupId)\r\n } else if (this.multiple && this.groupId) {\r\n console.log('Using existing groupId:', this.groupId)\r\n }\r\n },\r\n \r\n /**\r\n * 移除无效文件\r\n */\r\n removeInvalidFile(file) {\r\n console.log('文件类型验证失败,尝试移除文件')\r\n console.log('uploadRef:', this.uploadRef)\r\n console.log('$refs:', this.$refs)\r\n \r\n if (this.$refs[this.uploadRef]) {\r\n this.$refs[this.uploadRef].remove(file)\r\n } else {\r\n console.error('无法找到 uploadRef 引用')\r\n }\r\n },\r\n \r\n /**\r\n * 处理分片上传完成\r\n * @param {Object} response - 上传响应\r\n * @param {Object} uploadingFile - 正在上传的文件对象(可选)\r\n */\r\n handleChunkComplete(response, uploadingFile = null) {\r\n const { data: { file, type } } = response\r\n const payloads = {\r\n filePath: file.match(/\\/cw(.*)/) ? file.match(/\\/cw(.*)/)[0] : void 0,\r\n asyncID: uuidv4(),\r\n groupID: this.groupId,\r\n isDeleteOrigin: false,\r\n toImage: type === 'pdf',\r\n unzip: type === 'zip',\r\n _csrf: localStorage.getItem('token')\r\n }\r\n\r\n const formData = new FormData()\r\n\r\n Object.keys(payloads).forEach(key => {\r\n formData.append(key, payloads[key])\r\n })\r\n \r\n // 将上传文件对象传递给异步保存方法\r\n this.saveToServerAsyncPageTimer = setInterval(() => {\r\n this.saveToServerAsyncPage(formData, uploadingFile)\r\n }, CONSTANTS.SAVE_INTERVAL)\r\n },\r\n \r\n /**\r\n * 激活上传组件\r\n */\r\n activateUploadComponent(newFile, oldFile) {\r\n if (Boolean(newFile) !== Boolean(oldFile) || oldFile.error !== newFile.error) {\r\n if (!this.$refs[this.uploadRef].active) {\r\n this.$refs[this.uploadRef].active = true\r\n }\r\n }\r\n },\r\n \r\n // ==================== 值变化处理 ====================\r\n \r\n /**\r\n * 重置文件状态\r\n */\r\n resetFiles() {\r\n this.files = []\r\n this.buildedFiles = []\r\n // 多选模式下保留或生成 groupId,避免首次上传为空\r\n if (this.multiple) {\r\n if (!this.groupId) {\r\n this.groupId = uuidv4()\r\n console.log('Generated groupId in resetFiles:', this.groupId)\r\n } else {\r\n console.log('Preserve existing groupId in resetFiles:', this.groupId)\r\n }\r\n } else {\r\n this.groupId = null\r\n console.log('Reset groupId to null (single mode)')\r\n }\r\n },\r\n \r\n /**\r\n * 处理多文件模式的值变化\r\n */\r\n async handleMultipleFileValue(value) {\r\n // multiple - value 就是 groupId\r\n // 只有当 groupId 发生变化时才重新获取文件列表(初始化回显)\r\n if (this.groupId !== value) {\r\n this.groupId = value\r\n console.log('Set groupId from external value:', this.groupId)\r\n await this.fetchFilesWithGroupId()\r\n } else {\r\n console.log('GroupId unchanged, skip fetchFilesWithGroupId')\r\n }\r\n },\r\n \r\n /**\r\n * 处理单文件模式的值变化\r\n */\r\n async handleSingleFileValue() {\r\n await this.fetchFileWithFileId()\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n::v-deep .ele-upload__inner {\r\n opacity: 1 !important;\r\n cursor: pointer;\r\n border: 1px dashed var(--idooel-form-title-border-color);\r\n background: var(--idooel-form-upload-bg-color) !important;\r\n &:hover {\r\n border-color: var(--idooel-form-upload-border-hover-color);\r\n }\r\n border-radius: var(--idooel-form-border-radius);\r\n}\r\n.ele-upload__wrapper {\r\n width: 100%;\r\n .ele-upload__area {\r\n padding: 16px;\r\n width: 100%;\r\n height: 80px;\r\n display: flex;\r\n flex-direction: row;\r\n .ele-upload__area--icon {\r\n color: var(--idooel-primary-color);\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n font-size: 16x;\r\n .anticon-cloud-upload {\r\n font-size: 48px;\r\n color: var(--idooel-primary-color);\r\n }\r\n .anticon {\r\n font-size: 48px;\r\n color: var(--idooel-primary-color);\r\n }\r\n }\r\n .ele-upload__area--text {\r\n margin-left: 16px;\r\n .ele-upload__message {\r\n font-size: 16px;\r\n color: var(--idoole-black-088);\r\n text-align: left;\r\n }\r\n .ele-upload__ext {\r\n text-align: left;\r\n font-size: 14px;\r\n color: var(--idoole-black-06);\r\n }\r\n }\r\n }\r\n .ele-files__wrapper {\r\n .ele-file__item {\r\n width: 100%;\r\n margin-top: 8px;\r\n padding: 8px 12px;\r\n border-radius: var(--idooel-form-border-radius);\r\n background: var(--idooel-form-upload-bg-color);\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n .ele-file__suffix--icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n }\r\n .ele-file__name {\r\n flex: 1;\r\n text-align: left;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n font-size: 14px;\r\n margin-left: 8px;\r\n cursor: pointer;\r\n .ele-file__inner {\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n }\r\n .ele-file__delete {\r\n margin-left: 8px;\r\n .ele-file__delete--icon {\r\n margin-left: 8px;\r\n cursor: pointer;\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</style>","::v-deep .ele-upload__inner {\n opacity: 1 !important;\n cursor: pointer;\n border: 1px dashed var(--idooel-form-title-border-color);\n background: var(--idooel-form-upload-bg-color) !important;\n}\n::v-deep .ele-upload__inner:hover {\n border-color: var(--idooel-form-upload-border-hover-color);\n}\n::v-deep .ele-upload__inner {\n border-radius: var(--idooel-form-border-radius);\n}\n\n.ele-upload__wrapper {\n width: 100%;\n}\n.ele-upload__wrapper .ele-upload__area {\n padding: 16px;\n width: 100%;\n height: 80px;\n display: flex;\n flex-direction: row;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon {\n color: var(--idooel-primary-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n font-size: 16x;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text {\n margin-left: 16px;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__message {\n font-size: 16px;\n color: var(--idoole-black-088);\n text-align: left;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__ext {\n text-align: left;\n font-size: 14px;\n color: var(--idoole-black-06);\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item {\n width: 100%;\n margin-top: 8px;\n padding: 8px 12px;\n border-radius: var(--idooel-form-border-radius);\n background: var(--idooel-form-upload-bg-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__suffix--icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name {\n flex: 1;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n font-size: 14px;\n margin-left: 8px;\n cursor: pointer;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name .ele-file__inner {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete {\n margin-left: 8px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete .ele-file__delete--icon {\n margin-left: 8px;\n cursor: pointer;\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined });
6800
+ inject("data-v-3db5c928_0", { source: "[data-v-3db5c928] .ele-upload__inner {\n opacity: 1 !important;\n cursor: pointer;\n border: 1px dashed var(--idooel-form-title-border-color);\n background: var(--idooel-form-upload-bg-color) !important;\n}\n[data-v-3db5c928] .ele-upload__inner:hover {\n border-color: var(--idooel-form-upload-border-hover-color);\n}\n[data-v-3db5c928] .ele-upload__inner {\n border-radius: var(--idooel-form-border-radius);\n}\n.ele-upload__wrapper[data-v-3db5c928] {\n width: 100%;\n}\n.ele-upload__wrapper .ele-upload__area[data-v-3db5c928] {\n padding: 16px;\n width: 100%;\n height: 80px;\n display: flex;\n flex-direction: row;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon[data-v-3db5c928] {\n color: var(--idooel-primary-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n font-size: 16x;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload[data-v-3db5c928] {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon[data-v-3db5c928] {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text[data-v-3db5c928] {\n margin-left: 16px;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__message[data-v-3db5c928] {\n font-size: 16px;\n color: var(--idoole-black-088);\n text-align: left;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__ext[data-v-3db5c928] {\n text-align: left;\n font-size: 14px;\n color: var(--idoole-black-06);\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item[data-v-3db5c928] {\n width: 100%;\n margin-top: 8px;\n padding: 8px 12px;\n border-radius: var(--idooel-form-border-radius);\n background: var(--idooel-form-upload-bg-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__suffix--icon[data-v-3db5c928] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name[data-v-3db5c928] {\n flex: 1;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n font-size: 14px;\n margin-left: 8px;\n cursor: pointer;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name .ele-file__inner[data-v-3db5c928] {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete[data-v-3db5c928] {\n margin-left: 8px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete .ele-file__delete--icon[data-v-3db5c928] {\n margin-left: 8px;\n cursor: pointer;\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["C:\\Users\\ZOU\\Code\\OnlineStudy\\base-elearning-frontend-model\\packages\\components\\packages\\upload\\src\\index.vue","index.vue"],"names":[],"mappings":"AA+4BA;EACA,qBAAA;EACA,eAAA;EACA,wDAAA;EACA,yDAAA;AC94BA;AD+4BA;EACA,0DAAA;AC74BA;ADu4BA;EAQA,+CAAA;AC54BA;AD84BA;EACA,WAAA;AC34BA;AD44BA;EACA,aAAA;EACA,WAAA;EACA,YAAA;EACA,aAAA;EACA,mBAAA;AC14BA;AD24BA;EACA,kCAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,cAAA;ACz4BA;AD04BA;EACA,eAAA;EACA,kCAAA;ACx4BA;AD04BA;EACA,eAAA;EACA,kCAAA;ACx4BA;AD24BA;EACA,iBAAA;ACz4BA;AD04BA;EACA,eAAA;EACA,8BAAA;EACA,gBAAA;ACx4BA;AD04BA;EACA,gBAAA;EACA,eAAA;EACA,6BAAA;ACx4BA;AD64BA;EACA,WAAA;EACA,eAAA;EACA,iBAAA;EACA,+CAAA;EACA,8CAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;AC34BA;AD44BA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;AC14BA;AD44BA;EACA,OAAA;EACA,gBAAA;EACA,mBAAA;EACA,gBAAA;EACA,eAAA;EACA,gBAAA;EACA,eAAA;AC14BA;AD24BA;EACA,gBAAA;EACA,uBAAA;ACz4BA;AD44BA;EACA,gBAAA;AC14BA;AD24BA;EACA,gBAAA;EACA,eAAA;ACz4BA;;AAEA,oCAAoC","file":"index.vue","sourcesContent":["<template>\r\n <div class=\"ele-upload__wrapper\">\r\n <FileUpload\r\n class=\"ele-upload__inner\"\r\n v-show=\"isShowUploadContainer\"\r\n v-model=\"files\"\r\n :ref=\"uploadRef\"\r\n :drop=\"drop\"\r\n :chunk-enabled=\"chunkEnabled\"\r\n :chunk=\"chunkConfig\"\r\n :accept=\"accept\"\r\n :size=\"fileSizeLimit\"\r\n :post-action=\"postAction\"\r\n :multiple=\"multiple\"\r\n :headers=\"headers\"\r\n :maximum=\"getMaximum\"\r\n :data=\"uploadParams\"\r\n @input-file=\"onWatchInputFiles\"\r\n @input=\"onWatchFiles\"\r\n style=\"width: 100%;\">\r\n <section class=\"ele-upload__area\">\r\n <div class=\"ele-upload__area--icon\">\r\n <template v-if=\"iconIsZhWrod\">\r\n {{ icon }}\r\n </template>\r\n <template v-else>\r\n <ele-icon :type=\"icon\"></ele-icon>\r\n </template>\r\n </div>\r\n <div class=\"ele-upload__area--text\">\r\n <div class=\"ele-upload__message\" v-if=\"message\" v-html=\"message\"></div>\r\n <div class=\"ele-upload__message\" v-else>单击或拖动文件到该区域以上传</div>\r\n <div class=\"ele-upload__ext\" v-if=\"ext\" v-html=\"ext\"></div>\r\n <div class=\"ele-upload__ext\" v-else>文件小于{{ size }}M</div>\r\n </div>\r\n </section>\r\n </FileUpload>\r\n <section class=\"ele-files__wrapper\">\r\n <!-- 显示正在上传的文件(有进度条) -->\r\n <div class=\"ele-file__item\" v-for=\"(file, idx) in uploadingFiles\" :key=\"`uploading-${idx}`\">\r\n <div class=\"ele-file__suffix--icon\">\r\n <ele-icon :type=\"fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'\"></ele-icon>\r\n </div>\r\n <div class=\"ele-file__name\">\r\n <div class=\"ele-file__inner\">{{ file.name }}</div>\r\n <div v-if=\"file.progress !== undefined\" class=\"ele-uplpad__progress\">\r\n <a-progress :strokeWidth=\"2\" :percent=\"Number(file.progress)\" size=\"small\" />\r\n </div>\r\n </div>\r\n <div class=\"ele-file__delete\" v-if=\"file.success || file.error\">\r\n <span class=\"ele-file__size\">{{ (file.size / byteConversion).toFixed(2) }}M</span>\r\n <span class=\"ele-file__delete--icon\" @click=\"handleClickDelete(file)\">\r\n <ele-icon type=\"delete\"></ele-icon>\r\n </span>\r\n </div>\r\n </div>\r\n \r\n <!-- 显示已上传完成的文件 -->\r\n <div class=\"ele-file__item\" v-for=\"(file, idx) in completedFiles\" :key=\"`completed-${idx}`\">\r\n <div class=\"ele-file__suffix--icon\">\r\n <ele-icon :type=\"fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'\"></ele-icon>\r\n </div>\r\n <div class=\"ele-file__name\">\r\n <div class=\"ele-file__inner\" @click=\"handleClickDownload(file)\">{{ file.name }}</div>\r\n </div>\r\n <div class=\"ele-file__delete\">\r\n <span class=\"ele-file__size\">{{ (file.size / byteConversion).toFixed(2) }}M</span>\r\n <span class=\"ele-file__delete--icon\" @click=\"handleClickDelete(file)\">\r\n <ele-icon type=\"delete\"></ele-icon>\r\n </span>\r\n </div>\r\n </div>\r\n </section>\r\n </div>\r\n</template>\r\n\r\n<script>\r\nimport FileUpload from 'vue-upload-component'\r\nimport { v4 as uuidv4 } from 'uuid'\r\nimport { route, net, type } from '@idooel/shared'\r\n\r\n// 常量定义\r\nconst CONSTANTS = {\r\n DEFAULT_URL: 'zuul/api-file/workbench/file',\r\n DEFAULT_ICON: '上传',\r\n DEFAULT_SIZE: 100,\r\n DEFAULT_MESSAGE: '单击或拖动文件到该区域以上传',\r\n DEFAULT_MAXIMUM: 20,\r\n BYTE_CONVERSION: 1024 * 1024,\r\n CHUNK_MIN_SIZE: 3 * 1024 * 1024,\r\n CHUNK_MAX_ACTIVE: 3,\r\n CHUNK_MAX_RETRIES: 5,\r\n SAVE_INTERVAL: 2000\r\n}\r\n\r\n// 文件后缀图标映射\r\nconst FILE_SUFFIX_ICONS = {\r\n 'doc': { name: 'icon-doc' },\r\n 'html': { name: 'icon-html' },\r\n 'mp4': { name: 'icon-mp' },\r\n 'pdf': { name: 'icon-pdf' },\r\n 'ppt': { name: 'icon-ppt' },\r\n 'psd': { name: 'icon-psd' },\r\n 'rtf': { name: 'icon-rtf' },\r\n 'txt': { name: 'icon-txt' },\r\n 'vis': { name: 'icon-vis' },\r\n 'xls': { name: 'icon-xls' },\r\n 'xml': { name: 'icon-xml' },\r\n 'zip': { name: 'icon-zip' },\r\n 'jpg': { name: 'icon-img' },\r\n 'mp3': { name: 'icon-mp1' }\r\n}\r\n\r\nexport default {\r\n name: 'ele-upload',\r\n components: {\r\n FileUpload\r\n },\r\n model: {\r\n prop: 'value',\r\n event: 'change'\r\n },\r\n props: {\r\n url: {\r\n type: String,\r\n default: CONSTANTS.DEFAULT_URL\r\n },\r\n icon: {\r\n type: String,\r\n default: CONSTANTS.DEFAULT_ICON\r\n },\r\n size: {\r\n type: Number,\r\n default: CONSTANTS.DEFAULT_SIZE\r\n },\r\n message: {\r\n type: String,\r\n default: CONSTANTS.DEFAULT_MESSAGE\r\n },\r\n ext: {\r\n type: String\r\n },\r\n extensions: {\r\n type: String\r\n },\r\n accept: {\r\n type: String\r\n },\r\n maximum: {\r\n type: Number,\r\n default: CONSTANTS.DEFAULT_MAXIMUM\r\n },\r\n multiple: {\r\n type: Boolean,\r\n default: false\r\n },\r\n drop: {\r\n type: Boolean,\r\n default: true\r\n },\r\n value: {\r\n type: [String, Array]\r\n },\r\n querys: {\r\n type: Object,\r\n default: () => ({\r\n _csrf: localStorage.getItem('token')\r\n })\r\n },\r\n headers: {\r\n type: Object,\r\n default: () => ({\r\n 'X-XSRF-TOKEN': localStorage.getItem('token')\r\n })\r\n },\r\n byteConversion: {\r\n type: Number,\r\n default: CONSTANTS.BYTE_CONVERSION\r\n },\r\n chunkEnabled: {\r\n type: Boolean,\r\n default: true\r\n }\r\n },\r\n data() {\r\n return {\r\n // 文件状态管理\r\n files: [], // vue-upload-component 管理的文件\r\n buildedFiles: [], // 已构建完成的文件列表\r\n \r\n // 上传状态管理\r\n saveToServerAsyncPageTimer: null,\r\n uploadRefId: null,\r\n // 多选且外部无值时,预先生成 groupId,确保首次上传参数完整\r\n groupId: (this.multiple && !this.value) ? uuidv4() : null,\r\n //TODO\r\n iconIsZhWrod: false\r\n }\r\n },\r\n created() {\r\n // 多文件模式下,如果没有外部 groupId,则生成一个\r\n if (this.multiple && !this.value) {\r\n this.groupId = uuidv4()\r\n console.log('Created with new groupId:', this.groupId)\r\n }\r\n },\r\n watch: {\r\n value: {\r\n async handler(value) {\r\n if (type.isEmpty(value)) {\r\n this.resetFiles()\r\n } else if (this.multiple) {\r\n this.handleMultipleFileValue(value)\r\n } else {\r\n this.handleSingleFileValue()\r\n }\r\n },\r\n immediate: true\r\n }\r\n },\r\n computed: {\r\n // ==================== 基础配置 ====================\r\n prefixPath() {\r\n return window.prefixPath\r\n },\r\n \r\n uploadRef() {\r\n if (!this.uploadRefId) {\r\n this.uploadRefId = `uploadRef_${uuidv4()}`\r\n }\r\n return this.uploadRefId\r\n },\r\n \r\n iconIsZhWord() {\r\n return type.isZhWord(this.icon)\r\n },\r\n \r\n // ==================== 上传配置 ====================\r\n uploadParams() {\r\n return this.multiple ? { groupID: this.groupId } : {}\r\n },\r\n \r\n fileSizeLimit() {\r\n return this.size * this.byteConversion\r\n },\r\n \r\n postAction() {\r\n const queryString = route.toQueryString(this.querys)\r\n return `${this.prefixPath}${this.url}?${queryString}`\r\n },\r\n \r\n chunkConfig() {\r\n return {\r\n action: `${this.prefixPath}zuul/api-file/workbench/file/temp/chunk/vue`,\r\n headers: { ...this.headers },\r\n minSize: CONSTANTS.CHUNK_MIN_SIZE,\r\n maxActive: CONSTANTS.CHUNK_MAX_ACTIVE,\r\n maxRetries: CONSTANTS.CHUNK_MAX_RETRIES,\r\n startBody: { override: true, path: '/cw' },\r\n uploadBody: { override: true, path: '/cw' },\r\n finishBody: { override: true, path: '/cw' }\r\n }\r\n },\r\n \r\n getMaximum() {\r\n return this.multiple ? this.maximum : 1\r\n },\r\n \r\n // ==================== 文件状态 ====================\r\n uploadingFiles() {\r\n // 筛选条件:有进度信息 且 (未成功 或 成功但还没有fileID的切片上传文件)\r\n // 这样可以确保切片上传完成但异步保存未完成的文件继续显示,避免闪烁\r\n return this.files.filter(file => {\r\n if (file.progress === undefined) return false\r\n if (!file.success) return true\r\n \r\n // 检查文件是否已经有 fileID(可能在 file.fileID 或 file.response.data.fileID)\r\n const hasFileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)\r\n \r\n // 切片上传成功但还没有完成异步保存(没有fileID)的文件继续显示\r\n // 普通上传成功且已有 fileID 的文件不显示(会在 completedFiles 中显示)\r\n return file.success && !hasFileID\r\n })\r\n },\r\n \r\n completedFiles() {\r\n return this.buildedFiles.filter(file => file.fileID)\r\n },\r\n \r\n totalFiles() {\r\n return this.uploadingFiles.length + this.completedFiles.length\r\n },\r\n \r\n isFileUploadSuccessed() {\r\n const currentUploadingFiles = this.files.filter(file => file.response !== undefined)\r\n if (currentUploadingFiles.length === 0) {\r\n return this.buildedFiles.length > 0\r\n }\r\n return currentUploadingFiles.every(file => file.success)\r\n },\r\n \r\n isShowUploadContainer() {\r\n const maxFiles = this.multiple ? this.maximum : 1\r\n return this.totalFiles < maxFiles\r\n },\r\n \r\n // ==================== 文件信息 ====================\r\n fileSuffixIcon() {\r\n return FILE_SUFFIX_ICONS\r\n },\r\n \r\n fileIds() {\r\n if (this.multiple) {\r\n return this.groupId\r\n } else {\r\n const fileIds = this.buildedFiles.map(file => file.fileID)\r\n return fileIds[0]\r\n }\r\n },\r\n \r\n fileResponseData() {\r\n return this.multiple ? this.buildedFiles : this.buildedFiles[0]\r\n }\r\n },\r\n methods: {\r\n // ==================== 文件管理 ====================\r\n \r\n /**\r\n * 从多个数组中移除文件\r\n */\r\n removeFromArrays(file, arrays, key = 'fileID') {\r\n return arrays.map(arr => \r\n arr.filter(item => {\r\n // 如果 fileID 都存在且相等,则移除\r\n if (item[key] && file[key] && item[key] === file[key]) {\r\n return false\r\n }\r\n // 如果 id 都存在且相等,则移除\r\n if (item.id && file.id && item.id === file.id) {\r\n return false\r\n }\r\n // 如果文件名相同,也考虑移除(额外的安全检查)\r\n if (item.name && file.name && item.name === file.name && item[key] === file[key]) {\r\n return false\r\n }\r\n // 否则保留\r\n return true\r\n })\r\n )\r\n },\r\n \r\n /**\r\n * 检查文件是否为新文件\r\n */\r\n isNewFile(newFile, existingFiles, key = 'fileID') {\r\n return newFile[key] && !existingFiles.some(existing => existing[key] === newFile[key])\r\n },\r\n \r\n /**\r\n * 合并文件数据\r\n */\r\n mergeFileData(uploadFile) {\r\n return {\r\n ...uploadFile.response.data,\r\n ...uploadFile\r\n }\r\n },\r\n \r\n /**\r\n * 初始化文件列表\r\n */\r\n async initializeFiles() {\r\n if (!this.value) return\r\n \r\n if (this.multiple) {\r\n await this.fetchFilesWithGroupId()\r\n } else {\r\n await this.fetchFileWithFileId()\r\n }\r\n },\r\n \r\n /**\r\n * 获取多文件组\r\n */\r\n async fetchFilesWithGroupId() {\r\n try {\r\n const response = await net.get(`/api-file/workbench/file/group/${this.value}`)\r\n const data = response.data || []\r\n \r\n // 只有在没有现有文件时才设置初始文件列表\r\n if (this.buildedFiles.length === 0) {\r\n this.buildedFiles = data\r\n console.log('Initial files loaded:', this.buildedFiles.length)\r\n } else {\r\n console.log('Keep existing files, skip initial load')\r\n }\r\n } catch (error) {\r\n console.log('fetchFilesWithGroupId error:', error)\r\n console.log('Keep current files, do not clear due to API error')\r\n }\r\n },\r\n \r\n /**\r\n * 获取单文件\r\n */\r\n async fetchFileWithFileId() {\r\n try {\r\n const response = await net.get(`/api-file/file/${this.value}`)\r\n const data = response.data\r\n this.buildedFiles = [data]\r\n this.files = [data]\r\n } catch (error) {\r\n console.log('fetchFileWithFileId error:', error)\r\n }\r\n },\r\n\r\n /**\r\n * 调用服务器删除文件 API\r\n */\r\n async deleteFiles (ids) {\r\n if (!ids) {\r\n console.warn('No fileID provided for deletion')\r\n return\r\n }\r\n \r\n if (type.isArray(ids)) {\r\n ids = ids.join(',')\r\n }\r\n \r\n try {\r\n await net.remove(`/api-file/workbench/file?ids=${ids}`)\r\n console.log('File deleted from server:', ids)\r\n } catch (error) {\r\n console.error('Failed to delete file from server:', error)\r\n throw error // 重新抛出错误,让调用方处理\r\n }\r\n },\r\n \r\n /**\r\n * 处理文件删除\r\n */\r\n async handleClickDelete(file) {\r\n const { fileID, name } = file\r\n console.log('Deleting file:', { name, fileID })\r\n \r\n // 先从上传组件中移除文件\r\n if (this.$refs[this.uploadRef]) {\r\n this.$refs[this.uploadRef].remove(file)\r\n }\r\n \r\n // 从所有数组中移除文件\r\n [this.files, this.buildedFiles] = this.removeFromArrays(file, [this.files, this.buildedFiles])\r\n \r\n console.log('After deletion - files:', this.files.length, 'buildedFiles:', this.buildedFiles.length)\r\n \r\n // 如果有 fileID,调用服务器删除 API\r\n if (fileID) {\r\n try {\r\n await this.deleteFiles(fileID)\r\n } catch (error) {\r\n console.error('Failed to delete file from server:', error)\r\n // 可以选择提示用户或回滚操作\r\n // 这里仅记录错误,不影响前端显示\r\n }\r\n }\r\n \r\n // 多文件模式下,如果删除最后一个文件,重置 groupId\r\n if (this.multiple && this.buildedFiles.length === 0) {\r\n this.groupId = null\r\n console.log('Reset groupId after deleting last file')\r\n }\r\n \r\n // 触发 change 事件\r\n this.$emit('change', this.fileIds)\r\n },\r\n \r\n /**\r\n * 处理文件下载\r\n */\r\n handleClickDownload(file) {\r\n const { fileID: fileId } = file\r\n window.open(`/api-file/workbench/file/stream/${fileId}?origin=true`)\r\n },\r\n // ==================== 上传处理 ====================\r\n \r\n /**\r\n * 处理文件上传状态变化\r\n */\r\n onWatchFiles(files) {\r\n console.log('onWatchFiles called with files:', files.length)\r\n console.log('Current buildedFiles:', this.buildedFiles.length)\r\n \r\n // 更新文件状态\r\n this.files = files\r\n \r\n // 处理已上传成功的文件\r\n this.processUploadedFiles(files)\r\n \r\n // 检查上传是否完成\r\n if (this.isFileUploadSuccessed) {\r\n this.$emit('change', this.fileIds)\r\n this.$emit('on-success', this.fileResponseData)\r\n }\r\n },\r\n \r\n /**\r\n * 处理已上传成功的文件\r\n */\r\n processUploadedFiles(files) {\r\n // 处理所有有响应的文件(包括正在上传和已完成的)\r\n const uploadedFiles = files.filter(file => file.response)\r\n const newBuildedFiles = uploadedFiles.map(file => this.mergeFileData(file))\r\n \r\n if (this.multiple) {\r\n this.processMultipleFiles(newBuildedFiles)\r\n } else {\r\n // 单文件模式:只保留最新的文件\r\n this.buildedFiles = newBuildedFiles\r\n \r\n // 从 files 数组中移除已完成的文件(普通上传),避免重复显示\r\n if (newBuildedFiles.length > 0 && newBuildedFiles[0].fileID) {\r\n this.$nextTick(() => {\r\n this.files = this.files.filter(file => {\r\n const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)\r\n return fileID !== newBuildedFiles[0].fileID\r\n })\r\n console.log('Removed completed file from upload list (single mode)')\r\n })\r\n }\r\n }\r\n \r\n this.logFileStatus()\r\n },\r\n \r\n /**\r\n * 处理多文件模式\r\n */\r\n processMultipleFiles(newBuildedFiles) {\r\n // 获取已存在的文件ID集合\r\n const existingFileIds = new Set(this.buildedFiles.map(f => f.fileID).filter(id => id))\r\n \r\n // 过滤出真正的新文件\r\n const trulyNewFiles = newBuildedFiles.filter(newFile => \r\n this.isNewFile(newFile, this.buildedFiles)\r\n )\r\n \r\n console.log('Existing fileIDs:', Array.from(existingFileIds))\r\n console.log('New uploaded files:', newBuildedFiles.map(f => ({ name: f.name, fileID: f.fileID })))\r\n console.log('Truly new files:', trulyNewFiles.map(f => ({ name: f.name, fileID: f.fileID })))\r\n \r\n // 将新文件追加到现有文件列表\r\n if (trulyNewFiles.length > 0) {\r\n this.buildedFiles = [...this.buildedFiles, ...trulyNewFiles]\r\n console.log('Added new files, total buildedFiles:', this.buildedFiles.length)\r\n \r\n // 从 files 数组中移除已完成的文件(普通上传),避免重复显示\r\n this.$nextTick(() => {\r\n const completedFileIDs = new Set(trulyNewFiles.map(f => f.fileID))\r\n this.files = this.files.filter(file => {\r\n const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)\r\n return !completedFileIDs.has(fileID)\r\n })\r\n console.log('Removed completed files from upload list')\r\n })\r\n }\r\n \r\n // 更新现有文件的状态(包括新添加的文件)\r\n this.updateExistingFiles(newBuildedFiles)\r\n },\r\n \r\n /**\r\n * 更新现有文件状态\r\n */\r\n updateExistingFiles(newBuildedFiles) {\r\n this.buildedFiles = this.buildedFiles.map(existingFile => {\r\n const updatedFile = newBuildedFiles.find(newFile => newFile.fileID === existingFile.fileID)\r\n return updatedFile ? { ...existingFile, ...updatedFile } : existingFile\r\n })\r\n },\r\n \r\n /**\r\n * 记录文件状态日志\r\n */\r\n logFileStatus() {\r\n console.log('Final buildedFiles:', this.buildedFiles.length)\r\n console.log('buildedFiles details:', this.buildedFiles.map(f => ({ name: f.name, fileID: f.fileID, success: f.success })))\r\n console.log('Uploading files:', this.uploadingFiles.length)\r\n console.log('Completed files:', this.completedFiles.length)\r\n },\r\n // ==================== 异步处理 ====================\r\n \r\n /**\r\n * 异步保存文件到服务器\r\n * @param {FormData} payloads - 上传参数\r\n * @param {Object} uploadingFile - 正在上传的文件对象(可选)\r\n */\r\n async saveToServerAsyncPage(payloads = {}, uploadingFile = null) {\r\n try {\r\n const response = await net.post('zuul/api-file/workbench/file/temp/saveToServerAsyncPage', payloads, { \r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'\r\n }\r\n })\r\n \r\n const { data } = response\r\n if (data !== 'saveToServerAsyncPage') {\r\n clearInterval(this.saveToServerAsyncPageTimer)\r\n console.log('Chunk upload async save completed, file data:', data)\r\n \r\n const { fileID, groupID, name } = data\r\n \r\n // 根据上传模式处理文件信息\r\n if (this.multiple) {\r\n // 多文件模式:更新 groupID 并追加文件到列表\r\n if (groupID) {\r\n this.groupId = groupID\r\n console.log('Updated groupId from server:', groupID)\r\n }\r\n \r\n // 检查文件是否已存在,避免重复添加\r\n const fileExists = this.buildedFiles.some(file => file.fileID === fileID)\r\n if (!fileExists && fileID) {\r\n // 将服务器返回的完整文件信息追加到文件列表\r\n // 添加必要的显示属性\r\n const fileData = {\r\n ...data,\r\n success: true // 标记为上传成功\r\n }\r\n \r\n // 先添加到已完成列表\r\n this.buildedFiles.push(fileData)\r\n console.log('File added to list:', name, 'Total files:', this.buildedFiles.length)\r\n \r\n // 使用 nextTick 确保视图更新后再移除上传列表中的文件,避免闪烁\r\n this.$nextTick(() => {\r\n // 从上传列表中精确移除对应的文件\r\n if (uploadingFile) {\r\n // 如果有文件对象引用,直接移除该文件\r\n this.files = this.files.filter(file => file.id !== uploadingFile.id)\r\n console.log('Removed uploading file by id:', uploadingFile.id)\r\n } else {\r\n // 否则根据文件名移除\r\n this.files = this.files.filter(file => file.name !== name)\r\n console.log('Removed uploading file by name:', name)\r\n }\r\n })\r\n \r\n // 触发 change 事件通知父组件\r\n this.$emit('change', this.fileIds)\r\n this.$emit('on-success', this.fileResponseData)\r\n } else {\r\n console.log('File already exists or invalid fileID, skip adding')\r\n }\r\n } else {\r\n // 单文件模式:替换文件列表\r\n if (fileID) {\r\n const fileData = {\r\n ...data,\r\n success: true // 标记为上传成功\r\n }\r\n this.buildedFiles = [fileData]\r\n this.files = [] // 清空上传列表\r\n console.log('Single file updated:', name)\r\n \r\n // 触发 change 事件通知父组件更新 value\r\n this.$emit('change', fileID)\r\n this.$emit('on-success', data)\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error('saveToServerAsyncPage error:', error)\r\n clearInterval(this.saveToServerAsyncPageTimer)\r\n }\r\n },\r\n \r\n // ==================== 文件验证 ====================\r\n \r\n /**\r\n * 验证文件类型\r\n */\r\n validateFileType(file) {\r\n if (!file || !file.name) {\r\n console.log('文件或文件名不存在')\r\n return false\r\n }\r\n \r\n const fileExt = this.getFileExtension(file.name)\r\n console.log('文件扩展名:', fileExt)\r\n \r\n if (this.extensions) {\r\n const allowedExts = this.getAllowedExtensions()\r\n console.log('允许的扩展名:', allowedExts)\r\n \r\n if (!allowedExts.includes(fileExt)) {\r\n console.log('扩展名不在允许列表中')\r\n this.$message.error(`不支持的文件类型 \"${fileExt}\",请上传 ${this.extensions} 格式的文件`)\r\n return false\r\n }\r\n }\r\n \r\n console.log('文件类型验证通过')\r\n return true\r\n },\r\n \r\n /**\r\n * 获取文件扩展名\r\n */\r\n getFileExtension(fileName) {\r\n return fileName.toLowerCase().substring(fileName.lastIndexOf('.'))\r\n },\r\n \r\n /**\r\n * 获取允许的扩展名列表\r\n */\r\n getAllowedExtensions() {\r\n return this.extensions.toLowerCase()\r\n .split(',')\r\n .map(ext => ext.trim().startsWith('.') ? ext.trim() : `.${ext.trim()}`)\r\n },\r\n // ==================== 事件处理 ====================\r\n \r\n /**\r\n * 处理文件输入事件\r\n */\r\n onWatchInputFiles(newFile, oldFile) {\r\n if (newFile && !oldFile) {\r\n this.handleFileAdd(newFile)\r\n } else if (newFile && oldFile) {\r\n this.handleFileUpdate(newFile)\r\n } else if (!newFile && oldFile) {\r\n this.handleFileDelete()\r\n }\r\n \r\n // 激活上传组件\r\n this.activateUploadComponent(newFile, oldFile)\r\n },\r\n \r\n /**\r\n * 生成唯一的随机数\r\n */\r\n generateUniqueRandom() {\r\n // 使用 crypto.getRandomValues 生成更安全的随机数\r\n if (window.crypto && window.crypto.getRandomValues) {\r\n const array = new Uint32Array(1)\r\n window.crypto.getRandomValues(array)\r\n return array[0].toString()\r\n }\r\n // 降级方案:使用 Math.random 结合时间戳\r\n return Math.random().toString(36).substr(2, 9) + Date.now().toString(36)\r\n },\r\n\r\n /**\r\n * 处理文件添加\r\n */\r\n handleFileAdd(newFile) {\r\n // 为每个文件生成唯一的随机数\r\n const uniqueRandom = this.generateUniqueRandom()\r\n newFile.postAction = newFile.postAction + '&_t=' + uniqueRandom\r\n console.log('add file:', newFile)\r\n console.log('extensions:', this.extensions)\r\n console.log('accept:', this.accept)\r\n \r\n // 生成或使用 groupId\r\n this.ensureGroupId()\r\n \r\n // 验证文件类型\r\n if (!this.validateFileType(newFile)) {\r\n this.removeInvalidFile(newFile)\r\n return\r\n }\r\n console.log('文件类型验证通过,继续上传')\r\n },\r\n \r\n /**\r\n * 处理文件更新\r\n */\r\n handleFileUpdate(newFile) {\r\n console.log('update', newFile)\r\n const { success, active, chunk, response } = newFile\r\n \r\n if (chunk && success && !active) {\r\n console.log('chunk end')\r\n this.handleChunkComplete(response, newFile)\r\n }\r\n },\r\n \r\n /**\r\n * 处理文件删除\r\n */\r\n handleFileDelete() {\r\n console.log('delete')\r\n },\r\n \r\n /**\r\n * 确保 groupId 存在\r\n */\r\n ensureGroupId() {\r\n console.log('onWatchInputFiles - multiple:', this.multiple, 'current groupId:', this.groupId)\r\n \r\n if (this.multiple && !this.groupId) {\r\n this.groupId = uuidv4()\r\n console.log('Generated new groupId:', this.groupId)\r\n } else if (this.multiple && this.groupId) {\r\n console.log('Using existing groupId:', this.groupId)\r\n }\r\n },\r\n \r\n /**\r\n * 移除无效文件\r\n */\r\n removeInvalidFile(file) {\r\n console.log('文件类型验证失败,尝试移除文件')\r\n console.log('uploadRef:', this.uploadRef)\r\n console.log('$refs:', this.$refs)\r\n \r\n if (this.$refs[this.uploadRef]) {\r\n this.$refs[this.uploadRef].remove(file)\r\n } else {\r\n console.error('无法找到 uploadRef 引用')\r\n }\r\n },\r\n \r\n /**\r\n * 处理分片上传完成\r\n * @param {Object} response - 上传响应\r\n * @param {Object} uploadingFile - 正在上传的文件对象(可选)\r\n */\r\n handleChunkComplete(response, uploadingFile = null) {\r\n const { data: { file, type } } = response\r\n const payloads = {\r\n filePath: file.match(/\\/cw(.*)/) ? file.match(/\\/cw(.*)/)[0] : void 0,\r\n asyncID: uuidv4(),\r\n groupID: this.groupId,\r\n isDeleteOrigin: false,\r\n toImage: type === 'pdf',\r\n unzip: type === 'zip',\r\n _csrf: localStorage.getItem('token')\r\n }\r\n\r\n const formData = new FormData()\r\n\r\n Object.keys(payloads).forEach(key => {\r\n formData.append(key, payloads[key])\r\n })\r\n \r\n // 将上传文件对象传递给异步保存方法\r\n this.saveToServerAsyncPageTimer = setInterval(() => {\r\n this.saveToServerAsyncPage(formData, uploadingFile)\r\n }, CONSTANTS.SAVE_INTERVAL)\r\n },\r\n \r\n /**\r\n * 激活上传组件\r\n */\r\n activateUploadComponent(newFile, oldFile) {\r\n if (Boolean(newFile) !== Boolean(oldFile) || oldFile.error !== newFile.error) {\r\n if (!this.$refs[this.uploadRef].active) {\r\n this.$refs[this.uploadRef].active = true\r\n }\r\n }\r\n },\r\n \r\n // ==================== 值变化处理 ====================\r\n \r\n /**\r\n * 重置文件状态\r\n */\r\n resetFiles() {\r\n this.files = []\r\n this.buildedFiles = []\r\n // 多选模式下保留或生成 groupId,避免首次上传为空\r\n if (this.multiple) {\r\n if (!this.groupId) {\r\n this.groupId = uuidv4()\r\n console.log('Generated groupId in resetFiles:', this.groupId)\r\n } else {\r\n console.log('Preserve existing groupId in resetFiles:', this.groupId)\r\n }\r\n } else {\r\n this.groupId = null\r\n console.log('Reset groupId to null (single mode)')\r\n }\r\n },\r\n \r\n /**\r\n * 处理多文件模式的值变化\r\n */\r\n async handleMultipleFileValue(value) {\r\n // multiple - value 就是 groupId\r\n // 只有当 groupId 发生变化时才重新获取文件列表(初始化回显)\r\n if (this.groupId !== value) {\r\n this.groupId = value\r\n console.log('Set groupId from external value:', this.groupId)\r\n await this.fetchFilesWithGroupId()\r\n } else {\r\n console.log('GroupId unchanged, skip fetchFilesWithGroupId')\r\n }\r\n },\r\n \r\n /**\r\n * 处理单文件模式的值变化\r\n */\r\n async handleSingleFileValue() {\r\n await this.fetchFileWithFileId()\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n::v-deep .ele-upload__inner {\r\n opacity: 1 !important;\r\n cursor: pointer;\r\n border: 1px dashed var(--idooel-form-title-border-color);\r\n background: var(--idooel-form-upload-bg-color) !important;\r\n &:hover {\r\n border-color: var(--idooel-form-upload-border-hover-color);\r\n }\r\n border-radius: var(--idooel-form-border-radius);\r\n}\r\n.ele-upload__wrapper {\r\n width: 100%;\r\n .ele-upload__area {\r\n padding: 16px;\r\n width: 100%;\r\n height: 80px;\r\n display: flex;\r\n flex-direction: row;\r\n .ele-upload__area--icon {\r\n color: var(--idooel-primary-color);\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n font-size: 16x;\r\n .anticon-cloud-upload {\r\n font-size: 48px;\r\n color: var(--idooel-primary-color);\r\n }\r\n .anticon {\r\n font-size: 48px;\r\n color: var(--idooel-primary-color);\r\n }\r\n }\r\n .ele-upload__area--text {\r\n margin-left: 16px;\r\n .ele-upload__message {\r\n font-size: 16px;\r\n color: var(--idoole-black-088);\r\n text-align: left;\r\n }\r\n .ele-upload__ext {\r\n text-align: left;\r\n font-size: 14px;\r\n color: var(--idoole-black-06);\r\n }\r\n }\r\n }\r\n .ele-files__wrapper {\r\n .ele-file__item {\r\n width: 100%;\r\n margin-top: 8px;\r\n padding: 8px 12px;\r\n border-radius: var(--idooel-form-border-radius);\r\n background: var(--idooel-form-upload-bg-color);\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n .ele-file__suffix--icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n }\r\n .ele-file__name {\r\n flex: 1;\r\n text-align: left;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n font-size: 14px;\r\n margin-left: 8px;\r\n cursor: pointer;\r\n .ele-file__inner {\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n }\r\n .ele-file__delete {\r\n margin-left: 8px;\r\n .ele-file__delete--icon {\r\n margin-left: 8px;\r\n cursor: pointer;\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</style>","::v-deep .ele-upload__inner {\n opacity: 1 !important;\n cursor: pointer;\n border: 1px dashed var(--idooel-form-title-border-color);\n background: var(--idooel-form-upload-bg-color) !important;\n}\n::v-deep .ele-upload__inner:hover {\n border-color: var(--idooel-form-upload-border-hover-color);\n}\n::v-deep .ele-upload__inner {\n border-radius: var(--idooel-form-border-radius);\n}\n\n.ele-upload__wrapper {\n width: 100%;\n}\n.ele-upload__wrapper .ele-upload__area {\n padding: 16px;\n width: 100%;\n height: 80px;\n display: flex;\n flex-direction: row;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon {\n color: var(--idooel-primary-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n font-size: 16x;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--icon .anticon {\n font-size: 48px;\n color: var(--idooel-primary-color);\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text {\n margin-left: 16px;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__message {\n font-size: 16px;\n color: var(--idoole-black-088);\n text-align: left;\n}\n.ele-upload__wrapper .ele-upload__area .ele-upload__area--text .ele-upload__ext {\n text-align: left;\n font-size: 14px;\n color: var(--idoole-black-06);\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item {\n width: 100%;\n margin-top: 8px;\n padding: 8px 12px;\n border-radius: var(--idooel-form-border-radius);\n background: var(--idooel-form-upload-bg-color);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__suffix--icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name {\n flex: 1;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n font-size: 14px;\n margin-left: 8px;\n cursor: pointer;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__name .ele-file__inner {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete {\n margin-left: 8px;\n}\n.ele-upload__wrapper .ele-files__wrapper .ele-file__item .ele-file__delete .ele-file__delete--icon {\n margin-left: 8px;\n cursor: pointer;\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined });
6776
6801
 
6777
6802
  };
6778
6803
  /* scoped */
6779
- const __vue_scope_id__$t = "data-v-1b3ec41c";
6804
+ const __vue_scope_id__$t = "data-v-3db5c928";
6780
6805
  /* module identifier */
6781
6806
  const __vue_module_identifier__$t = undefined;
6782
6807
  /* functional template */
@@ -14860,9 +14885,29 @@ var script$3 = {
14860
14885
  event: 'change'
14861
14886
  },
14862
14887
  props: {
14888
+ title: {
14889
+ type: String,
14890
+ default: '导入'
14891
+ },
14892
+ mode: {
14893
+ type: String,
14894
+ default: 'async'
14895
+ },
14863
14896
  value: {
14864
14897
  type: Boolean,
14865
14898
  default: false
14899
+ },
14900
+ hooks: {
14901
+ type: Object,
14902
+ default: () => ({
14903
+ download: {},
14904
+ uploaded: {
14905
+ url: '',
14906
+ requestType: 'POST',
14907
+ params: {},
14908
+ fieldMap: {}
14909
+ }
14910
+ })
14866
14911
  }
14867
14912
  },
14868
14913
  data() {
@@ -14876,12 +14921,17 @@ var script$3 = {
14876
14921
  type: 'default',
14877
14922
  eventName: 'handleClose'
14878
14923
  }]
14879
- }
14924
+ },
14925
+ innerErrorList: []
14880
14926
  };
14881
14927
  },
14882
14928
  computed: {
14883
14929
  isFileEmpty() {
14884
14930
  return type.isEmpty(this.file);
14931
+ },
14932
+ statusList() {
14933
+ // 0 default
14934
+ return this.innerErrorList;
14885
14935
  }
14886
14936
  },
14887
14937
  watch: {
@@ -14907,7 +14957,59 @@ var script$3 = {
14907
14957
  action: 'jump'
14908
14958
  });
14909
14959
  },
14960
+ execFieldMap(fieldMap = {}, payloads = {}) {
14961
+ const ctx = {
14962
+ _route: this.$route.query,
14963
+ route: this.$route,
14964
+ ...payloads
14965
+ };
14966
+ return parseFieldMap(fieldMap, ctx);
14967
+ },
14910
14968
  uploadFileSuccess(props) {
14969
+ const {
14970
+ uploaded
14971
+ } = this.hooks;
14972
+ const {
14973
+ url,
14974
+ requestType = 'POST',
14975
+ params = {},
14976
+ fieldMap = {}
14977
+ } = uploaded;
14978
+ if (!url) {
14979
+ return this.emitEvent({
14980
+ action: 'uploaded',
14981
+ file: props
14982
+ });
14983
+ }
14984
+ const payloads = this.execFieldMap(fieldMap, {
14985
+ file: props.file
14986
+ });
14987
+ const formData = new FormData();
14988
+ Object.keys(payloads).forEach(key => {
14989
+ formData.append(key, payloads[key]);
14990
+ });
14991
+ net[requestType.toLowerCase()](url, formData, {
14992
+ headers: {
14993
+ 'Content-Type': 'multipart/form-data'
14994
+ }
14995
+ }).then(resp => {
14996
+ const {
14997
+ code,
14998
+ data
14999
+ } = resp;
15000
+ if (code !== '2000') {
15001
+ this.innerErrorList = data.map(item => {
15002
+ if (type.isStr(item)) {
15003
+ return {
15004
+ statusCode: '0',
15005
+ message: item
15006
+ };
15007
+ }
15008
+ });
15009
+ } else {
15010
+ this.innerErrorList = [];
15011
+ }
15012
+ });
14911
15013
  this.emitEvent({
14912
15014
  action: 'uploaded',
14913
15015
  file: props
@@ -14943,7 +15045,7 @@ var __vue_render__$3 = function () {
14943
15045
  attrs: {
14944
15046
  value: _vm.innerValue,
14945
15047
  buttonGroupMeta: _vm.buttonGroupMeta,
14946
- title: "导入",
15048
+ title: _vm.title,
14947
15049
  },
14948
15050
  on: { cancel: _vm.onCancel, handleClose: _vm.handleClose },
14949
15051
  },
@@ -14975,7 +15077,10 @@ var __vue_render__$3 = function () {
14975
15077
  ],
14976
15078
  1
14977
15079
  )
14978
- : _c("div", { staticClass: "ele-modal-import__hint" }, [
15080
+ : _vm._e(),
15081
+ _vm._v(" "),
15082
+ !_vm.isFileEmpty && _vm.mode === "async"
15083
+ ? _c("div", { staticClass: "ele-modal-import__hint" }, [
14979
15084
  _c("div", { staticClass: "import-hint__content" }, [
14980
15085
  _c(
14981
15086
  "div",
@@ -15009,7 +15114,21 @@ var __vue_render__$3 = function () {
15009
15114
  "\n 您可以请点击上方按钮查看任务进度或关闭弹框\n "
15010
15115
  ),
15011
15116
  ]),
15012
- ]),
15117
+ ])
15118
+ : _vm._e(),
15119
+ _vm._v(" "),
15120
+ !_vm.isFileEmpty && _vm.statusList.length > 0 && _vm.mode === "sync"
15121
+ ? _c(
15122
+ "div",
15123
+ { staticClass: "ele-modal-import__errors" },
15124
+ _vm._l(_vm.statusList, function (value, idx) {
15125
+ return _c("div", { key: idx }, [
15126
+ _vm._v("\n " + _vm._s(value.message) + "\n "),
15127
+ ])
15128
+ }),
15129
+ 0
15130
+ )
15131
+ : _vm._e(),
15013
15132
  ]
15014
15133
  )
15015
15134
  };
@@ -15019,12 +15138,12 @@ __vue_render__$3._withStripped = true;
15019
15138
  /* style */
15020
15139
  const __vue_inject_styles__$3 = function (inject) {
15021
15140
  if (!inject) return
15022
- inject("data-v-5d09c280_0", { source: ".has-error .ele-upload__wrapper .ele-upload__inner {\n border-color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__message {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__ext {\n color: #FFA39E;\n}\n.margin-t-8 {\n margin-top: 8px;\n}\n:root {\n --idooel-primary-color: #1890FF;\n --idoole-black-02: rgba(0, 0, 0, 0.04);\n --idoole-black-06: rgba(0, 0, 0, 0.44);\n --idoole-black-07: rgba(0, 0, 0, 0.64);\n --idoole-black-064: rgba(0, 0, 0, 0.64);\n --idoole-black-088: rgba(0, 0, 0, 0.88);\n --idoole-black-016: rgba(0, 0, 0, 0.16);\n --idooel-disabled-border-color: rgba(0, 0, 0, 0.16);\n --idoole-success-06: #52C41A;\n --idoole-warning-06: #FAAD14;\n --idoole-error-06: #F5222D;\n --idooel-border-width: 2;\n --idooel-border-color: #53a8ff;\n --idooel-column-border-width: 1;\n --idooel-column-border-color: #d9ecff;\n --idooel-row-odd-color: rgba(0, 0, 0, 0.04);\n --idooel-row-even-color: #FFF;\n --idooel-row-hover-color: #E6F7FF;\n --idooel-form-title-border-color: rgba(0, 0, 0, 0.16);\n --idooel-form-upload-bg-color: rgba(0, 0, 0, 0.02);\n --idooel-form-upload-border-hover-color: #40A9FF;\n --idooel-form-border-radius: 2px;\n --idooel-form-border-err-color: #F5222D;\n --idooel-link-06: #1890FF;\n --idooel-link-03: #91D5FF;\n --idooel-img-crop-err-color: #FFA39E;\n}\n.ant-input-disabled {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n.ant-select-disabled .ant-select-selection {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["index.vue"],"names":[],"mappings":"AAAA;EACE,qBAAqB;AACvB;AACA;EACE,cAAc;AAChB;AACA;EACE,cAAc;AAChB;AACA;EACE,cAAc;AAChB;AAEA;EACE,eAAe;AACjB;AAEA;EACE,+BAA+B;EAC/B,sCAAsC;EACtC,sCAAsC;EACtC,sCAAsC;EACtC,uCAAuC;EACvC,uCAAuC;EACvC,uCAAuC;EACvC,mDAAmD;EACnD,4BAA4B;EAC5B,4BAA4B;EAC5B,0BAA0B;EAC1B,wBAAwB;EACxB,8BAA8B;EAC9B,+BAA+B;EAC/B,qCAAqC;EACrC,2CAA2C;EAC3C,6BAA6B;EAC7B,iCAAiC;EACjC,qDAAqD;EACrD,kDAAkD;EAClD,gDAAgD;EAChD,gCAAgC;EAChC,uCAAuC;EACvC,yBAAyB;EACzB,yBAAyB;EACzB,oCAAoC;AACtC;AAEA;EACE,4DAA4D;AAC9D;AAEA;EACE,4DAA4D;AAC9D;;AAEA,oCAAoC","file":"index.vue","sourcesContent":[".has-error .ele-upload__wrapper .ele-upload__inner {\n border-color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__message {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__ext {\n color: #FFA39E;\n}\n\n.margin-t-8 {\n margin-top: 8px;\n}\n\n:root {\n --idooel-primary-color: #1890FF;\n --idoole-black-02: rgba(0, 0, 0, 0.04);\n --idoole-black-06: rgba(0, 0, 0, 0.44);\n --idoole-black-07: rgba(0, 0, 0, 0.64);\n --idoole-black-064: rgba(0, 0, 0, 0.64);\n --idoole-black-088: rgba(0, 0, 0, 0.88);\n --idoole-black-016: rgba(0, 0, 0, 0.16);\n --idooel-disabled-border-color: rgba(0, 0, 0, 0.16);\n --idoole-success-06: #52C41A;\n --idoole-warning-06: #FAAD14;\n --idoole-error-06: #F5222D;\n --idooel-border-width: 2;\n --idooel-border-color: #53a8ff;\n --idooel-column-border-width: 1;\n --idooel-column-border-color: #d9ecff;\n --idooel-row-odd-color: rgba(0, 0, 0, 0.04);\n --idooel-row-even-color: #FFF;\n --idooel-row-hover-color: #E6F7FF;\n --idooel-form-title-border-color: rgba(0, 0, 0, 0.16);\n --idooel-form-upload-bg-color: rgba(0, 0, 0, 0.02);\n --idooel-form-upload-border-hover-color: #40A9FF;\n --idooel-form-border-radius: 2px;\n --idooel-form-border-err-color: #F5222D;\n --idooel-link-06: #1890FF;\n --idooel-link-03: #91D5FF;\n --idooel-img-crop-err-color: #FFA39E;\n}\n\n.ant-input-disabled {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n\n.ant-select-disabled .ant-select-selection {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined })
15023
- ,inject("data-v-5d09c280_1", { source: ".import-download__link[data-v-5d09c280] {\n color: var(--idooel-link-06);\n font-size: 14px;\n cursor: pointer;\n line-height: 22px;\n}\n.ele-modal-import__hint .import-hint__content[data-v-5d09c280] {\n padding: 16px;\n border-radius: 2px;\n height: 82px;\n border: 1px dashed var(--idooel-link-06);\n background: var(--idoole-black-02);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__left .anticon-upload[data-v-5d09c280] {\n font-size: 48px;\n color: var(--idooel-link-06);\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right[data-v-5d09c280] {\n margin-left: 16px;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__title[data-v-5d09c280] {\n font-size: 16px;\n color: var(--idooel-link-06);\n cursor: pointer;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__sub-title[data-v-5d09c280] {\n font-size: 14px;\n color: var(--idooel-link-03);\n}\n.ele-modal-import__hint .import-footer__hint--text[data-v-5d09c280] {\n font-size: 14px;\n color: var(--idooel-link-06);\n line-height: 22px;\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["C:\\Users\\ZOU\\Code\\OnlineStudy\\base-elearning-frontend-model\\packages\\components\\packages\\business-components\\modal-import\\src\\index.vue","index.vue"],"names":[],"mappings":"AAkGA;EACA,4BAAA;EACA,eAAA;EACA,eAAA;EACA,iBAAA;ACjGA;ADoGA;EACA,aAAA;EACA,kBAAA;EACA,YAAA;EACA,wCAAA;EACA,kCAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;ACjGA;ADmGA;EACA,eAAA;EACA,4BAAA;ACjGA;ADoGA;EACA,iBAAA;AClGA;ADmGA;EACA,eAAA;EACA,4BAAA;EACA,eAAA;ACjGA;ADmGA;EACA,eAAA;EACA,4BAAA;ACjGA;ADqGA;EACA,eAAA;EACA,4BAAA;EACA,iBAAA;ACnGA;;AAEA,oCAAoC","file":"index.vue","sourcesContent":["<template>\r\n <ele-modal :value=\"innerValue\" :buttonGroupMeta=\"buttonGroupMeta\" @cancel=\"onCancel\" @handleClose=\"handleClose\" title=\"导入\">\r\n <div class=\"ele-modal-import__upload\" v-if=\"isFileEmpty\">\r\n <div class=\"import-download__link\" @click=\"handleClickDownloadTpl\">点击此链接下载导入模板</div>\r\n <ele-upload v-model=\"file\" @on-success=\"uploadFileSuccess\"></ele-upload>\r\n </div>\r\n <div v-else class=\"ele-modal-import__hint\">\r\n <div class=\"import-hint__content\">\r\n <div class=\"import-hint__left\">\r\n <ele-icon></ele-icon>\r\n </div>\r\n <div class=\"import-hint__right\">\r\n <div class=\"hint-right__title\" @click=\"handleClickJump\">文件上传成功,任务处理中..... 请点击此按钮跳转到“批处理管理”菜单查看任务进度</div>\r\n <div class=\"hint-right__sub-title\">批处理任务的准备工作会在后台运行</div>\r\n </div>\r\n </div>\r\n <div class=\"import-footer__hint--text\">\r\n 您可以请点击上方按钮查看任务进度或关闭弹框\r\n </div>\r\n </div>\r\n </ele-modal>\r\n</template>\r\n\r\n<script>\r\nimport { type } from '@idooel/shared'\r\nexport default {\r\n name: 'ele-modal-import',\r\n model: {\r\n prop: 'value',\r\n event: 'change'\r\n },\r\n props: {\r\n value: {\r\n type: Boolean,\r\n default: false\r\n }\r\n },\r\n data() {\r\n return {\r\n innerValue: false,\r\n file: [],\r\n buttonGroupMeta: {\r\n elements: [\r\n {\r\n label: '关闭',\r\n key: 'close',\r\n type: 'default',\r\n eventName: 'handleClose'\r\n }\r\n ]\r\n }\r\n }\r\n },\r\n computed: {\r\n isFileEmpty () {\r\n return type.isEmpty(this.file)\r\n }\r\n },\r\n watch: {\r\n value: {\r\n handler (value) {\r\n this.innerValue = value\r\n },\r\n immediate: true\r\n }\r\n },\r\n methods: {\r\n emitEvent (props = {}) {\r\n this.$emit('on-actions', { ...props })\r\n },\r\n onCancel () {\r\n this.innerValue = false\r\n this.$emit('change', false)\r\n },\r\n handleClickJump () {\r\n this.emitEvent({ action: 'jump' })\r\n },\r\n uploadFileSuccess (props) {\r\n this.emitEvent({ action: 'uploaded', file: props })\r\n },\r\n handleClickDownloadTpl () {\r\n this.emitEvent({ action: 'download' })\r\n },\r\n cleanFile () {\r\n this.file = []\r\n },\r\n handleClose () {\r\n this.cleanFile()\r\n this.innerValue = false\r\n this.$emit('change', false)\r\n }\r\n }\r\n}\r\n</script>\r\n<style lang=\"scss\">\r\n@import '../../../theme/index';\r\n</style>\r\n<style lang=\"scss\" scoped>\r\n.import-download__link {\r\n color: var(--idooel-link-06);\r\n font-size: 14px;\r\n cursor: pointer;\r\n line-height: 22px;\r\n}\r\n.ele-modal-import__hint {\r\n .import-hint__content {\r\n padding: 16px;\r\n border-radius: 2px;\r\n height: 82px;\r\n border: 1px dashed var(--idooel-link-06);\r\n background: var(--idoole-black-02);\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n .import-hint__left {\r\n .anticon-upload {\r\n font-size: 48px;\r\n color: var(--idooel-link-06);\r\n }\r\n }\r\n .import-hint__right {\r\n margin-left: 16px;\r\n .hint-right__title {\r\n font-size: 16px;\r\n color: var(--idooel-link-06);\r\n cursor: pointer;\r\n }\r\n .hint-right__sub-title {\r\n font-size: 14px;\r\n color: var(--idooel-link-03);\r\n }\r\n }\r\n }\r\n .import-footer__hint--text {\r\n font-size: 14px;\r\n color: var(--idooel-link-06);\r\n line-height: 22px;\r\n }\r\n}\r\n</style>",".import-download__link {\n color: var(--idooel-link-06);\n font-size: 14px;\n cursor: pointer;\n line-height: 22px;\n}\n\n.ele-modal-import__hint .import-hint__content {\n padding: 16px;\n border-radius: 2px;\n height: 82px;\n border: 1px dashed var(--idooel-link-06);\n background: var(--idoole-black-02);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__left .anticon-upload {\n font-size: 48px;\n color: var(--idooel-link-06);\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right {\n margin-left: 16px;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__title {\n font-size: 16px;\n color: var(--idooel-link-06);\n cursor: pointer;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__sub-title {\n font-size: 14px;\n color: var(--idooel-link-03);\n}\n.ele-modal-import__hint .import-footer__hint--text {\n font-size: 14px;\n color: var(--idooel-link-06);\n line-height: 22px;\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined });
15141
+ inject("data-v-55b81ade_0", { source: ".has-error .ele-upload__wrapper .ele-upload__inner {\n border-color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__message {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__ext {\n color: #FFA39E;\n}\n.margin-t-8 {\n margin-top: 8px;\n}\n:root {\n --idooel-primary-color: #1890FF;\n --idoole-black-02: rgba(0, 0, 0, 0.04);\n --idoole-black-06: rgba(0, 0, 0, 0.44);\n --idoole-black-07: rgba(0, 0, 0, 0.64);\n --idoole-black-064: rgba(0, 0, 0, 0.64);\n --idoole-black-088: rgba(0, 0, 0, 0.88);\n --idoole-black-016: rgba(0, 0, 0, 0.16);\n --idooel-disabled-border-color: rgba(0, 0, 0, 0.16);\n --idoole-success-06: #52C41A;\n --idoole-warning-06: #FAAD14;\n --idoole-error-06: #F5222D;\n --idooel-border-width: 2;\n --idooel-border-color: #53a8ff;\n --idooel-column-border-width: 1;\n --idooel-column-border-color: #d9ecff;\n --idooel-row-odd-color: rgba(0, 0, 0, 0.04);\n --idooel-row-even-color: #FFF;\n --idooel-row-hover-color: #E6F7FF;\n --idooel-form-title-border-color: rgba(0, 0, 0, 0.16);\n --idooel-form-upload-bg-color: rgba(0, 0, 0, 0.02);\n --idooel-form-upload-border-hover-color: #40A9FF;\n --idooel-form-border-radius: 2px;\n --idooel-form-border-err-color: #F5222D;\n --idooel-link-06: #1890FF;\n --idooel-link-03: #91D5FF;\n --idooel-img-crop-err-color: #FFA39E;\n}\n.ant-input-disabled {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n.ant-select-disabled .ant-select-selection {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["index.vue"],"names":[],"mappings":"AAAA;EACE,qBAAqB;AACvB;AACA;EACE,cAAc;AAChB;AACA;EACE,cAAc;AAChB;AACA;EACE,cAAc;AAChB;AAEA;EACE,eAAe;AACjB;AAEA;EACE,+BAA+B;EAC/B,sCAAsC;EACtC,sCAAsC;EACtC,sCAAsC;EACtC,uCAAuC;EACvC,uCAAuC;EACvC,uCAAuC;EACvC,mDAAmD;EACnD,4BAA4B;EAC5B,4BAA4B;EAC5B,0BAA0B;EAC1B,wBAAwB;EACxB,8BAA8B;EAC9B,+BAA+B;EAC/B,qCAAqC;EACrC,2CAA2C;EAC3C,6BAA6B;EAC7B,iCAAiC;EACjC,qDAAqD;EACrD,kDAAkD;EAClD,gDAAgD;EAChD,gCAAgC;EAChC,uCAAuC;EACvC,yBAAyB;EACzB,yBAAyB;EACzB,oCAAoC;AACtC;AAEA;EACE,4DAA4D;AAC9D;AAEA;EACE,4DAA4D;AAC9D;;AAEA,oCAAoC","file":"index.vue","sourcesContent":[".has-error .ele-upload__wrapper .ele-upload__inner {\n border-color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--icon .anticon-cloud-upload {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__message {\n color: #F5222D;\n}\n.has-error .ele-upload__wrapper .ele-upload__inner .ele-upload__area .ele-upload__area--text .ele-upload__ext {\n color: #FFA39E;\n}\n\n.margin-t-8 {\n margin-top: 8px;\n}\n\n:root {\n --idooel-primary-color: #1890FF;\n --idoole-black-02: rgba(0, 0, 0, 0.04);\n --idoole-black-06: rgba(0, 0, 0, 0.44);\n --idoole-black-07: rgba(0, 0, 0, 0.64);\n --idoole-black-064: rgba(0, 0, 0, 0.64);\n --idoole-black-088: rgba(0, 0, 0, 0.88);\n --idoole-black-016: rgba(0, 0, 0, 0.16);\n --idooel-disabled-border-color: rgba(0, 0, 0, 0.16);\n --idoole-success-06: #52C41A;\n --idoole-warning-06: #FAAD14;\n --idoole-error-06: #F5222D;\n --idooel-border-width: 2;\n --idooel-border-color: #53a8ff;\n --idooel-column-border-width: 1;\n --idooel-column-border-color: #d9ecff;\n --idooel-row-odd-color: rgba(0, 0, 0, 0.04);\n --idooel-row-even-color: #FFF;\n --idooel-row-hover-color: #E6F7FF;\n --idooel-form-title-border-color: rgba(0, 0, 0, 0.16);\n --idooel-form-upload-bg-color: rgba(0, 0, 0, 0.02);\n --idooel-form-upload-border-hover-color: #40A9FF;\n --idooel-form-border-radius: 2px;\n --idooel-form-border-err-color: #F5222D;\n --idooel-link-06: #1890FF;\n --idooel-link-03: #91D5FF;\n --idooel-img-crop-err-color: #FFA39E;\n}\n\n.ant-input-disabled {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n\n.ant-select-disabled .ant-select-selection {\n border-color: var(--idooel-disabled-border-color) !important;\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined })
15142
+ ,inject("data-v-55b81ade_1", { source: ".import-download__link[data-v-55b81ade] {\n color: var(--idooel-link-06);\n font-size: 14px;\n cursor: pointer;\n line-height: 22px;\n}\n.ele-modal-import__hint .import-hint__content[data-v-55b81ade] {\n padding: 16px;\n border-radius: 2px;\n height: 82px;\n border: 1px dashed var(--idooel-link-06);\n background: var(--idoole-black-02);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__left .anticon-upload[data-v-55b81ade] {\n font-size: 48px;\n color: var(--idooel-link-06);\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right[data-v-55b81ade] {\n margin-left: 16px;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__title[data-v-55b81ade] {\n font-size: 16px;\n color: var(--idooel-link-06);\n cursor: pointer;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__sub-title[data-v-55b81ade] {\n font-size: 14px;\n color: var(--idooel-link-03);\n}\n.ele-modal-import__hint .import-footer__hint--text[data-v-55b81ade] {\n font-size: 14px;\n color: var(--idooel-link-06);\n line-height: 22px;\n}\n\n/*# sourceMappingURL=index.vue.map */", map: {"version":3,"sources":["C:\\Users\\ZOU\\Code\\OnlineStudy\\base-elearning-frontend-model\\packages\\components\\packages\\business-components\\modal-import\\src\\index.vue","index.vue"],"names":[],"mappings":"AAgKA;EACA,4BAAA;EACA,eAAA;EACA,eAAA;EACA,iBAAA;AC/JA;ADkKA;EACA,aAAA;EACA,kBAAA;EACA,YAAA;EACA,wCAAA;EACA,kCAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;AC/JA;ADiKA;EACA,eAAA;EACA,4BAAA;AC/JA;ADkKA;EACA,iBAAA;AChKA;ADiKA;EACA,eAAA;EACA,4BAAA;EACA,eAAA;AC/JA;ADiKA;EACA,eAAA;EACA,4BAAA;AC/JA;ADmKA;EACA,eAAA;EACA,4BAAA;EACA,iBAAA;ACjKA;;AAEA,oCAAoC","file":"index.vue","sourcesContent":["<template>\r\n <ele-modal :value=\"innerValue\" :buttonGroupMeta=\"buttonGroupMeta\" @cancel=\"onCancel\" @handleClose=\"handleClose\" :title=\"title\">\r\n <div class=\"ele-modal-import__upload\" v-if=\"isFileEmpty\">\r\n <div class=\"import-download__link\" @click=\"handleClickDownloadTpl\">点击此链接下载导入模板</div>\r\n <ele-upload v-model=\"file\" @on-success=\"uploadFileSuccess\"></ele-upload>\r\n </div>\r\n <div v-if=\"(!isFileEmpty && mode === 'async')\" class=\"ele-modal-import__hint\">\r\n <div class=\"import-hint__content\">\r\n <div class=\"import-hint__left\">\r\n <ele-icon></ele-icon>\r\n </div>\r\n <div class=\"import-hint__right\">\r\n <div class=\"hint-right__title\" @click=\"handleClickJump\">文件上传成功,任务处理中..... 请点击此按钮跳转到“批处理管理”菜单查看任务进度</div>\r\n <div class=\"hint-right__sub-title\">批处理任务的准备工作会在后台运行</div>\r\n </div>\r\n </div>\r\n <div class=\"import-footer__hint--text\">\r\n 您可以请点击上方按钮查看任务进度或关闭弹框\r\n </div>\r\n </div>\r\n <div v-if=\"!isFileEmpty && statusList.length > 0 && mode === 'sync'\" class=\"ele-modal-import__errors\">\r\n <div v-for=\"(value, idx) in statusList\" :key=\"idx\">\r\n {{ value.message }}\r\n </div>\r\n </div>\r\n </ele-modal>\r\n</template>\r\n\r\n<script>\r\nimport { type, net } from '@idooel/shared'\r\nimport { parseFieldMap } from '../../../utils'\r\nexport default {\r\n name: 'ele-modal-import',\r\n model: {\r\n prop: 'value',\r\n event: 'change'\r\n },\r\n props: {\r\n title: {\r\n type: String,\r\n default: '导入'\r\n },\r\n mode: {\r\n type: String,\r\n default: 'async'\r\n },\r\n value: {\r\n type: Boolean,\r\n default: false\r\n },\r\n hooks: {\r\n type: Object,\r\n default: () => ({\r\n download: {},\r\n uploaded: {\r\n url: '',\r\n requestType: 'POST',\r\n params: {},\r\n fieldMap: {}\r\n }\r\n })\r\n }\r\n },\r\n data() {\r\n return {\r\n innerValue: false,\r\n file: [],\r\n buttonGroupMeta: {\r\n elements: [\r\n {\r\n label: '关闭',\r\n key: 'close',\r\n type: 'default',\r\n eventName: 'handleClose'\r\n }\r\n ]\r\n },\r\n innerErrorList: []\r\n }\r\n },\r\n computed: {\r\n isFileEmpty () {\r\n return type.isEmpty(this.file)\r\n },\r\n statusList () {\r\n // 0 default\r\n return this.innerErrorList\r\n }\r\n },\r\n watch: {\r\n value: {\r\n handler (value) {\r\n this.innerValue = value\r\n },\r\n immediate: true\r\n }\r\n },\r\n methods: {\r\n emitEvent (props = {}) {\r\n this.$emit('on-actions', { ...props })\r\n },\r\n onCancel () {\r\n this.innerValue = false\r\n this.$emit('change', false)\r\n },\r\n handleClickJump () {\r\n this.emitEvent({ action: 'jump' })\r\n },\r\n execFieldMap (fieldMap = {}, payloads = {}) {\r\n const ctx = { _route: this.$route.query, route: this.$route, ...payloads }\r\n return parseFieldMap(fieldMap, ctx)\r\n },\r\n uploadFileSuccess (props) {\r\n const { uploaded } = this.hooks\r\n const { url, requestType = 'POST', params = {}, fieldMap = {} } = uploaded\r\n if (!url) {\r\n return this.emitEvent({ action: 'uploaded', file: props })\r\n }\r\n const payloads = this.execFieldMap(fieldMap, { file: props.file })\r\n const formData = new FormData()\r\n Object.keys(payloads).forEach(key => {\r\n formData.append(key, payloads[key])\r\n })\r\n net[requestType.toLowerCase()](url, formData , { headers: { 'Content-Type': 'multipart/form-data' } }).then(resp => {\r\n const { code, data } = resp\r\n if (code !== '2000') {\r\n this.innerErrorList = data.map(item => {\r\n if (type.isStr(item)) {\r\n return {\r\n statusCode: '0',\r\n message: item\r\n }\r\n } else {\r\n // TODO\r\n }\r\n })\r\n } else {\r\n this.innerErrorList = []\r\n }\r\n })\r\n this.emitEvent({ action: 'uploaded', file: props })\r\n },\r\n handleClickDownloadTpl () {\r\n this.emitEvent({ action: 'download' })\r\n },\r\n cleanFile () {\r\n this.file = []\r\n },\r\n handleClose () {\r\n this.cleanFile()\r\n this.innerValue = false\r\n this.$emit('change', false)\r\n }\r\n }\r\n}\r\n</script>\r\n<style lang=\"scss\">\r\n@import '../../../theme/index';\r\n</style>\r\n<style lang=\"scss\" scoped>\r\n.import-download__link {\r\n color: var(--idooel-link-06);\r\n font-size: 14px;\r\n cursor: pointer;\r\n line-height: 22px;\r\n}\r\n.ele-modal-import__hint {\r\n .import-hint__content {\r\n padding: 16px;\r\n border-radius: 2px;\r\n height: 82px;\r\n border: 1px dashed var(--idooel-link-06);\r\n background: var(--idoole-black-02);\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n .import-hint__left {\r\n .anticon-upload {\r\n font-size: 48px;\r\n color: var(--idooel-link-06);\r\n }\r\n }\r\n .import-hint__right {\r\n margin-left: 16px;\r\n .hint-right__title {\r\n font-size: 16px;\r\n color: var(--idooel-link-06);\r\n cursor: pointer;\r\n }\r\n .hint-right__sub-title {\r\n font-size: 14px;\r\n color: var(--idooel-link-03);\r\n }\r\n }\r\n }\r\n .import-footer__hint--text {\r\n font-size: 14px;\r\n color: var(--idooel-link-06);\r\n line-height: 22px;\r\n }\r\n}\r\n</style>",".import-download__link {\n color: var(--idooel-link-06);\n font-size: 14px;\n cursor: pointer;\n line-height: 22px;\n}\n\n.ele-modal-import__hint .import-hint__content {\n padding: 16px;\n border-radius: 2px;\n height: 82px;\n border: 1px dashed var(--idooel-link-06);\n background: var(--idoole-black-02);\n display: flex;\n flex-direction: row;\n align-items: center;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__left .anticon-upload {\n font-size: 48px;\n color: var(--idooel-link-06);\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right {\n margin-left: 16px;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__title {\n font-size: 16px;\n color: var(--idooel-link-06);\n cursor: pointer;\n}\n.ele-modal-import__hint .import-hint__content .import-hint__right .hint-right__sub-title {\n font-size: 14px;\n color: var(--idooel-link-03);\n}\n.ele-modal-import__hint .import-footer__hint--text {\n font-size: 14px;\n color: var(--idooel-link-06);\n line-height: 22px;\n}\n\n/*# sourceMappingURL=index.vue.map */"]}, media: undefined });
15024
15143
 
15025
15144
  };
15026
15145
  /* scoped */
15027
- const __vue_scope_id__$3 = "data-v-5d09c280";
15146
+ const __vue_scope_id__$3 = "data-v-55b81ade";
15028
15147
  /* module identifier */
15029
15148
  const __vue_module_identifier__$3 = undefined;
15030
15149
  /* functional template */