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

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.
Files changed (111) hide show
  1. package/README.md +98 -98
  2. package/dist/@idooel/components.esm.js +960 -248
  3. package/dist/@idooel/components.umd.js +963 -251
  4. package/jsconfig.json +7 -7
  5. package/package.json +61 -61
  6. package/packages/alert/index.js +4 -4
  7. package/packages/alert/src/index.vue +45 -45
  8. package/packages/batch-export/index.js +4 -4
  9. package/packages/batch-export/src/index.vue +104 -104
  10. package/packages/business-components/modal-fsm/index.js +4 -4
  11. package/packages/business-components/modal-fsm/src/index.vue +163 -163
  12. package/packages/business-components/modal-import/index.js +4 -4
  13. package/packages/business-components/modal-import/src/index.vue +139 -139
  14. package/packages/business-components/modal-timeline/index.js +4 -4
  15. package/packages/business-components/modal-timeline/src/index.vue +77 -77
  16. package/packages/business-components/tabs-sub-center/index.js +4 -4
  17. package/packages/business-components/tabs-sub-center/src/index.vue +116 -116
  18. package/packages/button/index.js +4 -4
  19. package/packages/button/src/index.vue +65 -65
  20. package/packages/checkbox/index.js +4 -4
  21. package/packages/checkbox/src/index.vue +52 -52
  22. package/packages/composite-components/button-group/index.js +4 -4
  23. package/packages/composite-components/button-group/src/index.vue +151 -151
  24. package/packages/composite-components/form-attachment/src/index.vue +14 -14
  25. package/packages/composite-components/form-img-crop/index.js +4 -4
  26. package/packages/composite-components/form-img-crop/src/index.vue +131 -131
  27. package/packages/composite-components/modal-confirm/index.js +4 -4
  28. package/packages/composite-components/modal-confirm/src/index.vue +103 -103
  29. package/packages/composite-components/modal-form/index.js +4 -4
  30. package/packages/composite-components/modal-form/src/index.vue +230 -230
  31. package/packages/composite-components/modal-img-crop/index.js +4 -4
  32. package/packages/composite-components/modal-img-crop/src/index.vue +298 -298
  33. package/packages/composite-components/modal-table/index.js +4 -4
  34. package/packages/composite-components/modal-table/src/index.vue +155 -155
  35. package/packages/composite-components/modal-tree/index.js +4 -4
  36. package/packages/composite-components/modal-tree/src/index.vue +75 -75
  37. package/packages/composite-components/search-area/index.js +4 -4
  38. package/packages/composite-components/search-area/src/index.vue +238 -237
  39. package/packages/composite-components/search-area/src/label.vue +35 -35
  40. package/packages/composite-components/select-entity-modal-table/index.js +4 -4
  41. package/packages/composite-components/select-entity-modal-table/src/index.vue +171 -171
  42. package/packages/date/index.js +4 -4
  43. package/packages/date/src/index.vue +112 -112
  44. package/packages/date-range/index.js +4 -4
  45. package/packages/date-range/src/index.vue +47 -47
  46. package/packages/form/index.js +4 -4
  47. package/packages/form/src/index.vue +319 -319
  48. package/packages/icon/index.js +4 -4
  49. package/packages/icon/src/index.vue +31 -31
  50. package/packages/index.js +156 -153
  51. package/packages/input/index.js +4 -4
  52. package/packages/input/src/index.vue +35 -35
  53. package/packages/input-number/index.js +4 -4
  54. package/packages/input-number/src/index.vue +23 -23
  55. package/packages/loading/index.js +4 -4
  56. package/packages/loading/src/index.vue +36 -36
  57. package/packages/meta/provider.js +4 -0
  58. package/packages/modal/index.js +4 -4
  59. package/packages/modal/src/index.vue +184 -184
  60. package/packages/models/form-group-model/index.js +4 -4
  61. package/packages/models/form-group-model/src/index.vue +273 -273
  62. package/packages/models/form-model/index.js +4 -4
  63. package/packages/models/form-model/src/index.vue +236 -236
  64. package/packages/models/step-model/index.js +4 -4
  65. package/packages/models/step-model/src/index.vue +224 -224
  66. package/packages/models/tree-table-model/index.js +4 -4
  67. package/packages/models/tree-table-model/src/index.vue +806 -764
  68. package/packages/radio/index.js +4 -4
  69. package/packages/radio/src/index.vue +56 -56
  70. package/packages/select/index.js +4 -4
  71. package/packages/select/src/index.vue +105 -105
  72. package/packages/select-entity/index.js +4 -4
  73. package/packages/select-entity/src/index.vue +119 -119
  74. package/packages/table/index.js +4 -4
  75. package/packages/table/src/action.vue +172 -172
  76. package/packages/table/src/index.vue +339 -319
  77. package/packages/tabs/index.js +4 -4
  78. package/packages/tabs/src/index.vue +55 -55
  79. package/packages/text/index.js +4 -4
  80. package/packages/text/src/index.vue +47 -47
  81. package/packages/text-editor/index.js +4 -4
  82. package/packages/text-editor/src/index.vue +72 -72
  83. package/packages/textarea/index.js +4 -4
  84. package/packages/textarea/src/index.vue +57 -57
  85. package/packages/theme/form.scss +21 -21
  86. package/packages/theme/index.scss +43 -43
  87. package/packages/theme/overrid.scss +7 -7
  88. package/packages/theme/styleClass.scss +2 -2
  89. package/packages/theme/variables.scss +55 -55
  90. package/packages/timeline/index.js +4 -4
  91. package/packages/timeline/src/index.vue +257 -257
  92. package/packages/tpl/index.js +4 -4
  93. package/packages/tpl/src/index.vue +55 -55
  94. package/packages/tree/index.js +4 -4
  95. package/packages/tree/src/TreeNode.vue +29 -29
  96. package/packages/tree/src/index.vue +101 -101
  97. package/packages/tree-select/index.js +4 -4
  98. package/packages/tree-select/src/index.vue +142 -142
  99. package/packages/upload/index.js +4 -4
  100. package/packages/upload/src/index.vue +996 -996
  101. package/packages/utils/index.js +66 -62
  102. package/packages/utils/runtime-context/dataPoolAPI.js +501 -0
  103. package/packages/utils/runtime-context/globalDataPool.js +279 -0
  104. package/packages/utils/runtime-context/index.js +76 -0
  105. package/packages/utils/runtime-context/modelSchema.js +174 -0
  106. package/scripts/rollup.config.js +42 -42
  107. package/scripts/rollup.esm.config.js +11 -11
  108. package/scripts/rollup.umd.config.js +17 -17
  109. package/vitest.config.js +17 -17
  110. package/packages/utils/dataPool.js +0 -22
  111. package/packages/utils/treeTableDataPoolManager.js +0 -85
@@ -1,997 +1,997 @@
1
- <template>
2
- <div class="ele-upload__wrapper">
3
- <FileUpload
4
- class="ele-upload__inner"
5
- v-show="isShowUploadContainer"
6
- v-model="files"
7
- :ref="uploadRef"
8
- :drop="drop"
9
- :chunk-enabled="chunkEnabled"
10
- :chunk="chunkConfig"
11
- :accept="accept"
12
- :size="fileSizeLimit"
13
- :post-action="postAction"
14
- :multiple="multiple"
15
- :headers="headers"
16
- :maximum="getMaximum"
17
- :data="uploadParams"
18
- @input-file="onWatchInputFiles"
19
- @input="onWatchFiles"
20
- style="width: 100%;">
21
- <section class="ele-upload__area">
22
- <div class="ele-upload__area--icon">
23
- <template v-if="iconIsZhWrod">
24
- {{ icon }}
25
- </template>
26
- <template v-else>
27
- <ele-icon :type="icon"></ele-icon>
28
- </template>
29
- </div>
30
- <div class="ele-upload__area--text">
31
- <div class="ele-upload__message" v-if="message" v-html="message"></div>
32
- <div class="ele-upload__message" v-else>单击或拖动文件到该区域以上传</div>
33
- <div class="ele-upload__ext" v-if="ext" v-html="ext"></div>
34
- <div class="ele-upload__ext" v-else>文件小于{{ size }}M</div>
35
- </div>
36
- </section>
37
- </FileUpload>
38
- <section class="ele-files__wrapper">
39
- <!-- 显示正在上传的文件(有进度条) -->
40
- <div class="ele-file__item" v-for="(file, idx) in uploadingFiles" :key="`uploading-${idx}`">
41
- <div class="ele-file__suffix--icon">
42
- <ele-icon :type="fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'"></ele-icon>
43
- </div>
44
- <div class="ele-file__name">
45
- <div class="ele-file__inner">{{ file.name }}</div>
46
- <div v-if="file.progress !== undefined" class="ele-uplpad__progress">
47
- <a-progress :strokeWidth="2" :percent="Number(file.progress)" size="small" />
48
- </div>
49
- </div>
50
- <div class="ele-file__delete" v-if="file.success || file.error">
51
- <span class="ele-file__size">{{ (file.size / byteConversion).toFixed(2) }}M</span>
52
- <span class="ele-file__delete--icon" @click="handleClickDelete(file)">
53
- <ele-icon type="delete"></ele-icon>
54
- </span>
55
- </div>
56
- </div>
57
-
58
- <!-- 显示已上传完成的文件 -->
59
- <div class="ele-file__item" v-for="(file, idx) in completedFiles" :key="`completed-${idx}`">
60
- <div class="ele-file__suffix--icon">
61
- <ele-icon :type="fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'"></ele-icon>
62
- </div>
63
- <div class="ele-file__name">
64
- <div class="ele-file__inner" @click="handleClickDownload(file)">{{ file.name }}</div>
65
- </div>
66
- <div class="ele-file__delete">
67
- <span class="ele-file__size">{{ (file.size / byteConversion).toFixed(2) }}M</span>
68
- <span class="ele-file__delete--icon" @click="handleClickDelete(file)">
69
- <ele-icon type="delete"></ele-icon>
70
- </span>
71
- </div>
72
- </div>
73
- </section>
74
- </div>
75
- </template>
76
-
77
- <script>
78
- import FileUpload from 'vue-upload-component'
79
- import { v4 as uuidv4 } from 'uuid'
80
- import { route, net, type } from '@idooel/shared'
81
-
82
- // 常量定义
83
- const CONSTANTS = {
84
- DEFAULT_URL: 'zuul/api-file/workbench/file',
85
- DEFAULT_ICON: '上传',
86
- DEFAULT_SIZE: 100,
87
- DEFAULT_MESSAGE: '单击或拖动文件到该区域以上传',
88
- DEFAULT_MAXIMUM: 20,
89
- BYTE_CONVERSION: 1024 * 1024,
90
- CHUNK_MIN_SIZE: 3 * 1024 * 1024,
91
- CHUNK_MAX_ACTIVE: 3,
92
- CHUNK_MAX_RETRIES: 5,
93
- SAVE_INTERVAL: 2000
94
- }
95
-
96
- // 文件后缀图标映射
97
- const FILE_SUFFIX_ICONS = {
98
- 'doc': { name: 'icon-doc' },
99
- 'html': { name: 'icon-html' },
100
- 'mp4': { name: 'icon-mp' },
101
- 'pdf': { name: 'icon-pdf' },
102
- 'ppt': { name: 'icon-ppt' },
103
- 'psd': { name: 'icon-psd' },
104
- 'rtf': { name: 'icon-rtf' },
105
- 'txt': { name: 'icon-txt' },
106
- 'vis': { name: 'icon-vis' },
107
- 'xls': { name: 'icon-xls' },
108
- 'xml': { name: 'icon-xml' },
109
- 'zip': { name: 'icon-zip' },
110
- 'jpg': { name: 'icon-img' },
111
- 'mp3': { name: 'icon-mp1' }
112
- }
113
-
114
- export default {
115
- name: 'ele-upload',
116
- components: {
117
- FileUpload
118
- },
119
- model: {
120
- prop: 'value',
121
- event: 'change'
122
- },
123
- props: {
124
- url: {
125
- type: String,
126
- default: CONSTANTS.DEFAULT_URL
127
- },
128
- icon: {
129
- type: String,
130
- default: CONSTANTS.DEFAULT_ICON
131
- },
132
- size: {
133
- type: Number,
134
- default: CONSTANTS.DEFAULT_SIZE
135
- },
136
- message: {
137
- type: String,
138
- default: CONSTANTS.DEFAULT_MESSAGE
139
- },
140
- ext: {
141
- type: String
142
- },
143
- extensions: {
144
- type: String
145
- },
146
- accept: {
147
- type: String
148
- },
149
- maximum: {
150
- type: Number,
151
- default: CONSTANTS.DEFAULT_MAXIMUM
152
- },
153
- multiple: {
154
- type: Boolean,
155
- default: false
156
- },
157
- drop: {
158
- type: Boolean,
159
- default: true
160
- },
161
- value: {
162
- type: [String, Array]
163
- },
164
- querys: {
165
- type: Object,
166
- default: () => ({
167
- _csrf: localStorage.getItem('token')
168
- })
169
- },
170
- headers: {
171
- type: Object,
172
- default: () => ({
173
- 'X-XSRF-TOKEN': localStorage.getItem('token')
174
- })
175
- },
176
- byteConversion: {
177
- type: Number,
178
- default: CONSTANTS.BYTE_CONVERSION
179
- },
180
- chunkEnabled: {
181
- type: Boolean,
182
- default: true
183
- }
184
- },
185
- data() {
186
- return {
187
- // 文件状态管理
188
- files: [], // vue-upload-component 管理的文件
189
- buildedFiles: [], // 已构建完成的文件列表
190
-
191
- // 上传状态管理
192
- saveToServerAsyncPageTimer: null,
193
- uploadRefId: null,
194
- // 多选且外部无值时,预先生成 groupId,确保首次上传参数完整
195
- groupId: (this.multiple && !this.value) ? uuidv4() : null
196
- }
197
- },
198
- created() {
199
- // 多文件模式下,如果没有外部 groupId,则生成一个
200
- if (this.multiple && !this.value) {
201
- this.groupId = uuidv4()
202
- console.log('Created with new groupId:', this.groupId)
203
- }
204
- },
205
- watch: {
206
- value: {
207
- async handler(value) {
208
- if (type.isEmpty(value)) {
209
- this.resetFiles()
210
- } else if (this.multiple) {
211
- this.handleMultipleFileValue(value)
212
- } else {
213
- this.handleSingleFileValue()
214
- }
215
- },
216
- immediate: true
217
- }
218
- },
219
- computed: {
220
- // ==================== 基础配置 ====================
221
- prefixPath() {
222
- return window.prefixPath
223
- },
224
-
225
- uploadRef() {
226
- if (!this.uploadRefId) {
227
- this.uploadRefId = `uploadRef_${uuidv4()}`
228
- }
229
- return this.uploadRefId
230
- },
231
-
232
- iconIsZhWord() {
233
- return type.isZhWord(this.icon)
234
- },
235
-
236
- // ==================== 上传配置 ====================
237
- uploadParams() {
238
- return this.multiple ? { groupID: this.groupId } : {}
239
- },
240
-
241
- fileSizeLimit() {
242
- return this.size * this.byteConversion
243
- },
244
-
245
- postAction() {
246
- const queryString = route.toQueryString(this.querys)
247
- return `${this.prefixPath}${this.url}?${queryString}`
248
- },
249
-
250
- chunkConfig() {
251
- return {
252
- action: `${this.prefixPath}zuul/api-file/workbench/file/temp/chunk/vue`,
253
- headers: { ...this.headers },
254
- minSize: CONSTANTS.CHUNK_MIN_SIZE,
255
- maxActive: CONSTANTS.CHUNK_MAX_ACTIVE,
256
- maxRetries: CONSTANTS.CHUNK_MAX_RETRIES,
257
- startBody: { override: true, path: '/cw' },
258
- uploadBody: { override: true, path: '/cw' },
259
- finishBody: { override: true, path: '/cw' }
260
- }
261
- },
262
-
263
- getMaximum() {
264
- return this.multiple ? this.maximum : 1
265
- },
266
-
267
- // ==================== 文件状态 ====================
268
- uploadingFiles() {
269
- // 筛选条件:有进度信息 且 (未成功 或 成功但还没有fileID的切片上传文件)
270
- // 这样可以确保切片上传完成但异步保存未完成的文件继续显示,避免闪烁
271
- return this.files.filter(file => {
272
- if (file.progress === undefined) return false
273
- if (!file.success) return true
274
-
275
- // 检查文件是否已经有 fileID(可能在 file.fileID 或 file.response.data.fileID)
276
- const hasFileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)
277
-
278
- // 切片上传成功但还没有完成异步保存(没有fileID)的文件继续显示
279
- // 普通上传成功且已有 fileID 的文件不显示(会在 completedFiles 中显示)
280
- return file.success && !hasFileID
281
- })
282
- },
283
-
284
- completedFiles() {
285
- return this.buildedFiles.filter(file => file.fileID)
286
- },
287
-
288
- totalFiles() {
289
- return this.uploadingFiles.length + this.completedFiles.length
290
- },
291
-
292
- isFileUploadSuccessed() {
293
- const currentUploadingFiles = this.files.filter(file => file.response !== undefined)
294
- if (currentUploadingFiles.length === 0) {
295
- return this.buildedFiles.length > 0
296
- }
297
- return currentUploadingFiles.every(file => file.success)
298
- },
299
-
300
- isShowUploadContainer() {
301
- const maxFiles = this.multiple ? this.maximum : 1
302
- return this.totalFiles < maxFiles
303
- },
304
-
305
- // ==================== 文件信息 ====================
306
- fileSuffixIcon() {
307
- return FILE_SUFFIX_ICONS
308
- },
309
-
310
- fileIds() {
311
- if (this.multiple) {
312
- return this.groupId
313
- } else {
314
- const fileIds = this.buildedFiles.map(file => file.fileID)
315
- return fileIds[0]
316
- }
317
- },
318
-
319
- fileResponseData() {
320
- return this.multiple ? this.buildedFiles : this.buildedFiles[0]
321
- }
322
- },
323
- methods: {
324
- // ==================== 文件管理 ====================
325
-
326
- /**
327
- * 从多个数组中移除文件
328
- */
329
- removeFromArrays(file, arrays, key = 'fileID') {
330
- return arrays.map(arr =>
331
- arr.filter(item => {
332
- // 如果 fileID 都存在且相等,则移除
333
- if (item[key] && file[key] && item[key] === file[key]) {
334
- return false
335
- }
336
- // 如果 id 都存在且相等,则移除
337
- if (item.id && file.id && item.id === file.id) {
338
- return false
339
- }
340
- // 如果文件名相同,也考虑移除(额外的安全检查)
341
- if (item.name && file.name && item.name === file.name && item[key] === file[key]) {
342
- return false
343
- }
344
- // 否则保留
345
- return true
346
- })
347
- )
348
- },
349
-
350
- /**
351
- * 检查文件是否为新文件
352
- */
353
- isNewFile(newFile, existingFiles, key = 'fileID') {
354
- return newFile[key] && !existingFiles.some(existing => existing[key] === newFile[key])
355
- },
356
-
357
- /**
358
- * 合并文件数据
359
- */
360
- mergeFileData(uploadFile) {
361
- return {
362
- ...uploadFile.response.data,
363
- ...uploadFile
364
- }
365
- },
366
-
367
- /**
368
- * 初始化文件列表
369
- */
370
- async initializeFiles() {
371
- if (!this.value) return
372
-
373
- if (this.multiple) {
374
- await this.fetchFilesWithGroupId()
375
- } else {
376
- await this.fetchFileWithFileId()
377
- }
378
- },
379
-
380
- /**
381
- * 获取多文件组
382
- */
383
- async fetchFilesWithGroupId() {
384
- try {
385
- const response = await net.get(`/api-file/workbench/file/group/${this.value}`)
386
- const data = response.data || []
387
-
388
- // 只有在没有现有文件时才设置初始文件列表
389
- if (this.buildedFiles.length === 0) {
390
- this.buildedFiles = data
391
- console.log('Initial files loaded:', this.buildedFiles.length)
392
- } else {
393
- console.log('Keep existing files, skip initial load')
394
- }
395
- } catch (error) {
396
- console.log('fetchFilesWithGroupId error:', error)
397
- console.log('Keep current files, do not clear due to API error')
398
- }
399
- },
400
-
401
- /**
402
- * 获取单文件
403
- */
404
- async fetchFileWithFileId() {
405
- try {
406
- const response = await net.get(`/api-file/file/${this.value}`)
407
- const data = response.data
408
- this.buildedFiles = [data]
409
- this.files = [data]
410
- } catch (error) {
411
- console.log('fetchFileWithFileId error:', error)
412
- }
413
- },
414
-
415
- /**
416
- * 调用服务器删除文件 API
417
- */
418
- async deleteFiles (ids) {
419
- if (!ids) {
420
- console.warn('No fileID provided for deletion')
421
- return
422
- }
423
-
424
- if (type.isArray(ids)) {
425
- ids = ids.join(',')
426
- }
427
-
428
- try {
429
- await net.remove(`/api-file/workbench/file?ids=${ids}`)
430
- console.log('File deleted from server:', ids)
431
- } catch (error) {
432
- console.error('Failed to delete file from server:', error)
433
- throw error // 重新抛出错误,让调用方处理
434
- }
435
- },
436
-
437
- /**
438
- * 处理文件删除
439
- */
440
- async handleClickDelete(file) {
441
- const { fileID, name } = file
442
- console.log('Deleting file:', { name, fileID })
443
-
444
- // 先从上传组件中移除文件
445
- if (this.$refs[this.uploadRef]) {
446
- this.$refs[this.uploadRef].remove(file)
447
- }
448
-
449
- // 从所有数组中移除文件
450
- [this.files, this.buildedFiles] = this.removeFromArrays(file, [this.files, this.buildedFiles])
451
-
452
- console.log('After deletion - files:', this.files.length, 'buildedFiles:', this.buildedFiles.length)
453
-
454
- // 如果有 fileID,调用服务器删除 API
455
- if (fileID) {
456
- try {
457
- await this.deleteFiles(fileID)
458
- } catch (error) {
459
- console.error('Failed to delete file from server:', error)
460
- // 可以选择提示用户或回滚操作
461
- // 这里仅记录错误,不影响前端显示
462
- }
463
- }
464
-
465
- // 多文件模式下,如果删除最后一个文件,重置 groupId
466
- if (this.multiple && this.buildedFiles.length === 0) {
467
- this.groupId = null
468
- console.log('Reset groupId after deleting last file')
469
- }
470
-
471
- // 触发 change 事件
472
- this.$emit('change', this.fileIds)
473
- },
474
-
475
- /**
476
- * 处理文件下载
477
- */
478
- handleClickDownload(file) {
479
- const { fileID: fileId } = file
480
- window.open(`/api-file/workbench/file/stream/${fileId}?origin=true`)
481
- },
482
- // ==================== 上传处理 ====================
483
-
484
- /**
485
- * 处理文件上传状态变化
486
- */
487
- onWatchFiles(files) {
488
- console.log('onWatchFiles called with files:', files.length)
489
- console.log('Current buildedFiles:', this.buildedFiles.length)
490
-
491
- // 更新文件状态
492
- this.files = files
493
-
494
- // 处理已上传成功的文件
495
- this.processUploadedFiles(files)
496
-
497
- // 检查上传是否完成
498
- if (this.isFileUploadSuccessed) {
499
- this.$emit('change', this.fileIds)
500
- this.$emit('on-success', this.fileResponseData)
501
- }
502
- },
503
-
504
- /**
505
- * 处理已上传成功的文件
506
- */
507
- processUploadedFiles(files) {
508
- // 处理所有有响应的文件(包括正在上传和已完成的)
509
- const uploadedFiles = files.filter(file => file.response)
510
- const newBuildedFiles = uploadedFiles.map(file => this.mergeFileData(file))
511
-
512
- if (this.multiple) {
513
- this.processMultipleFiles(newBuildedFiles)
514
- } else {
515
- // 单文件模式:只保留最新的文件
516
- this.buildedFiles = newBuildedFiles
517
-
518
- // 从 files 数组中移除已完成的文件(普通上传),避免重复显示
519
- if (newBuildedFiles.length > 0 && newBuildedFiles[0].fileID) {
520
- this.$nextTick(() => {
521
- this.files = this.files.filter(file => {
522
- const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)
523
- return fileID !== newBuildedFiles[0].fileID
524
- })
525
- console.log('Removed completed file from upload list (single mode)')
526
- })
527
- }
528
- }
529
-
530
- this.logFileStatus()
531
- },
532
-
533
- /**
534
- * 处理多文件模式
535
- */
536
- processMultipleFiles(newBuildedFiles) {
537
- // 获取已存在的文件ID集合
538
- const existingFileIds = new Set(this.buildedFiles.map(f => f.fileID).filter(id => id))
539
-
540
- // 过滤出真正的新文件
541
- const trulyNewFiles = newBuildedFiles.filter(newFile =>
542
- this.isNewFile(newFile, this.buildedFiles)
543
- )
544
-
545
- console.log('Existing fileIDs:', Array.from(existingFileIds))
546
- console.log('New uploaded files:', newBuildedFiles.map(f => ({ name: f.name, fileID: f.fileID })))
547
- console.log('Truly new files:', trulyNewFiles.map(f => ({ name: f.name, fileID: f.fileID })))
548
-
549
- // 将新文件追加到现有文件列表
550
- if (trulyNewFiles.length > 0) {
551
- this.buildedFiles = [...this.buildedFiles, ...trulyNewFiles]
552
- console.log('Added new files, total buildedFiles:', this.buildedFiles.length)
553
-
554
- // 从 files 数组中移除已完成的文件(普通上传),避免重复显示
555
- this.$nextTick(() => {
556
- const completedFileIDs = new Set(trulyNewFiles.map(f => f.fileID))
557
- this.files = this.files.filter(file => {
558
- const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)
559
- return !completedFileIDs.has(fileID)
560
- })
561
- console.log('Removed completed files from upload list')
562
- })
563
- }
564
-
565
- // 更新现有文件的状态(包括新添加的文件)
566
- this.updateExistingFiles(newBuildedFiles)
567
- },
568
-
569
- /**
570
- * 更新现有文件状态
571
- */
572
- updateExistingFiles(newBuildedFiles) {
573
- this.buildedFiles = this.buildedFiles.map(existingFile => {
574
- const updatedFile = newBuildedFiles.find(newFile => newFile.fileID === existingFile.fileID)
575
- return updatedFile ? { ...existingFile, ...updatedFile } : existingFile
576
- })
577
- },
578
-
579
- /**
580
- * 记录文件状态日志
581
- */
582
- logFileStatus() {
583
- console.log('Final buildedFiles:', this.buildedFiles.length)
584
- console.log('buildedFiles details:', this.buildedFiles.map(f => ({ name: f.name, fileID: f.fileID, success: f.success })))
585
- console.log('Uploading files:', this.uploadingFiles.length)
586
- console.log('Completed files:', this.completedFiles.length)
587
- },
588
- // ==================== 异步处理 ====================
589
-
590
- /**
591
- * 异步保存文件到服务器
592
- * @param {FormData} payloads - 上传参数
593
- * @param {Object} uploadingFile - 正在上传的文件对象(可选)
594
- */
595
- async saveToServerAsyncPage(payloads = {}, uploadingFile = null) {
596
- try {
597
- const response = await net.post('zuul/api-file/workbench/file/temp/saveToServerAsyncPage', payloads, {
598
- headers: {
599
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
600
- }
601
- })
602
-
603
- const { data } = response
604
- if (data !== 'saveToServerAsyncPage') {
605
- clearInterval(this.saveToServerAsyncPageTimer)
606
- console.log('Chunk upload async save completed, file data:', data)
607
-
608
- const { fileID, groupID, name } = data
609
-
610
- // 根据上传模式处理文件信息
611
- if (this.multiple) {
612
- // 多文件模式:更新 groupID 并追加文件到列表
613
- if (groupID) {
614
- this.groupId = groupID
615
- console.log('Updated groupId from server:', groupID)
616
- }
617
-
618
- // 检查文件是否已存在,避免重复添加
619
- const fileExists = this.buildedFiles.some(file => file.fileID === fileID)
620
- if (!fileExists && fileID) {
621
- // 将服务器返回的完整文件信息追加到文件列表
622
- // 添加必要的显示属性
623
- const fileData = {
624
- ...data,
625
- success: true // 标记为上传成功
626
- }
627
-
628
- // 先添加到已完成列表
629
- this.buildedFiles.push(fileData)
630
- console.log('File added to list:', name, 'Total files:', this.buildedFiles.length)
631
-
632
- // 使用 nextTick 确保视图更新后再移除上传列表中的文件,避免闪烁
633
- this.$nextTick(() => {
634
- // 从上传列表中精确移除对应的文件
635
- if (uploadingFile) {
636
- // 如果有文件对象引用,直接移除该文件
637
- this.files = this.files.filter(file => file.id !== uploadingFile.id)
638
- console.log('Removed uploading file by id:', uploadingFile.id)
639
- } else {
640
- // 否则根据文件名移除
641
- this.files = this.files.filter(file => file.name !== name)
642
- console.log('Removed uploading file by name:', name)
643
- }
644
- })
645
-
646
- // 触发 change 事件通知父组件
647
- this.$emit('change', this.fileIds)
648
- this.$emit('on-success', this.fileResponseData)
649
- } else {
650
- console.log('File already exists or invalid fileID, skip adding')
651
- }
652
- } else {
653
- // 单文件模式:替换文件列表
654
- if (fileID) {
655
- const fileData = {
656
- ...data,
657
- success: true // 标记为上传成功
658
- }
659
- this.buildedFiles = [fileData]
660
- this.files = [] // 清空上传列表
661
- console.log('Single file updated:', name)
662
-
663
- // 触发 change 事件通知父组件更新 value
664
- this.$emit('change', fileID)
665
- this.$emit('on-success', data)
666
- }
667
- }
668
- }
669
- } catch (error) {
670
- console.error('saveToServerAsyncPage error:', error)
671
- clearInterval(this.saveToServerAsyncPageTimer)
672
- }
673
- },
674
-
675
- // ==================== 文件验证 ====================
676
-
677
- /**
678
- * 验证文件类型
679
- */
680
- validateFileType(file) {
681
- if (!file || !file.name) {
682
- console.log('文件或文件名不存在')
683
- return false
684
- }
685
-
686
- const fileExt = this.getFileExtension(file.name)
687
- console.log('文件扩展名:', fileExt)
688
-
689
- if (this.extensions) {
690
- const allowedExts = this.getAllowedExtensions()
691
- console.log('允许的扩展名:', allowedExts)
692
-
693
- if (!allowedExts.includes(fileExt)) {
694
- console.log('扩展名不在允许列表中')
695
- this.$message.error(`不支持的文件类型 "${fileExt}",请上传 ${this.extensions} 格式的文件`)
696
- return false
697
- }
698
- }
699
-
700
- console.log('文件类型验证通过')
701
- return true
702
- },
703
-
704
- /**
705
- * 获取文件扩展名
706
- */
707
- getFileExtension(fileName) {
708
- return fileName.toLowerCase().substring(fileName.lastIndexOf('.'))
709
- },
710
-
711
- /**
712
- * 获取允许的扩展名列表
713
- */
714
- getAllowedExtensions() {
715
- return this.extensions.toLowerCase()
716
- .split(',')
717
- .map(ext => ext.trim().startsWith('.') ? ext.trim() : `.${ext.trim()}`)
718
- },
719
- // ==================== 事件处理 ====================
720
-
721
- /**
722
- * 处理文件输入事件
723
- */
724
- onWatchInputFiles(newFile, oldFile) {
725
- if (newFile && !oldFile) {
726
- this.handleFileAdd(newFile)
727
- } else if (newFile && oldFile) {
728
- this.handleFileUpdate(newFile)
729
- } else if (!newFile && oldFile) {
730
- this.handleFileDelete()
731
- }
732
-
733
- // 激活上传组件
734
- this.activateUploadComponent(newFile, oldFile)
735
- },
736
-
737
- /**
738
- * 生成唯一的随机数
739
- */
740
- generateUniqueRandom() {
741
- // 使用 crypto.getRandomValues 生成更安全的随机数
742
- if (window.crypto && window.crypto.getRandomValues) {
743
- const array = new Uint32Array(1)
744
- window.crypto.getRandomValues(array)
745
- return array[0].toString()
746
- }
747
- // 降级方案:使用 Math.random 结合时间戳
748
- return Math.random().toString(36).substr(2, 9) + Date.now().toString(36)
749
- },
750
-
751
- /**
752
- * 处理文件添加
753
- */
754
- handleFileAdd(newFile) {
755
- // 为每个文件生成唯一的随机数
756
- const uniqueRandom = this.generateUniqueRandom()
757
- newFile.postAction = newFile.postAction + '&_t=' + uniqueRandom
758
- console.log('add file:', newFile)
759
- console.log('extensions:', this.extensions)
760
- console.log('accept:', this.accept)
761
-
762
- // 生成或使用 groupId
763
- this.ensureGroupId()
764
-
765
- // 验证文件类型
766
- if (!this.validateFileType(newFile)) {
767
- this.removeInvalidFile(newFile)
768
- return
769
- }
770
- console.log('文件类型验证通过,继续上传')
771
- },
772
-
773
- /**
774
- * 处理文件更新
775
- */
776
- handleFileUpdate(newFile) {
777
- console.log('update', newFile)
778
- const { success, active, chunk, response } = newFile
779
-
780
- if (chunk && success && !active) {
781
- console.log('chunk end')
782
- this.handleChunkComplete(response, newFile)
783
- }
784
- },
785
-
786
- /**
787
- * 处理文件删除
788
- */
789
- handleFileDelete() {
790
- console.log('delete')
791
- },
792
-
793
- /**
794
- * 确保 groupId 存在
795
- */
796
- ensureGroupId() {
797
- console.log('onWatchInputFiles - multiple:', this.multiple, 'current groupId:', this.groupId)
798
-
799
- if (this.multiple && !this.groupId) {
800
- this.groupId = uuidv4()
801
- console.log('Generated new groupId:', this.groupId)
802
- } else if (this.multiple && this.groupId) {
803
- console.log('Using existing groupId:', this.groupId)
804
- }
805
- },
806
-
807
- /**
808
- * 移除无效文件
809
- */
810
- removeInvalidFile(file) {
811
- console.log('文件类型验证失败,尝试移除文件')
812
- console.log('uploadRef:', this.uploadRef)
813
- console.log('$refs:', this.$refs)
814
-
815
- if (this.$refs[this.uploadRef]) {
816
- this.$refs[this.uploadRef].remove(file)
817
- } else {
818
- console.error('无法找到 uploadRef 引用')
819
- }
820
- },
821
-
822
- /**
823
- * 处理分片上传完成
824
- * @param {Object} response - 上传响应
825
- * @param {Object} uploadingFile - 正在上传的文件对象(可选)
826
- */
827
- handleChunkComplete(response, uploadingFile = null) {
828
- const { data: { file, type } } = response
829
- const payloads = {
830
- filePath: file.match(/\/cw(.*)/) ? file.match(/\/cw(.*)/)[0] : void 0,
831
- asyncID: uuidv4(),
832
- groupID: this.groupId,
833
- isDeleteOrigin: false,
834
- toImage: type === 'pdf',
835
- unzip: type === 'zip',
836
- _csrf: localStorage.getItem('token')
837
- }
838
-
839
- const formData = new FormData()
840
-
841
- Object.keys(payloads).forEach(key => {
842
- formData.append(key, payloads[key])
843
- })
844
-
845
- // 将上传文件对象传递给异步保存方法
846
- this.saveToServerAsyncPageTimer = setInterval(() => {
847
- this.saveToServerAsyncPage(formData, uploadingFile)
848
- }, CONSTANTS.SAVE_INTERVAL)
849
- },
850
-
851
- /**
852
- * 激活上传组件
853
- */
854
- activateUploadComponent(newFile, oldFile) {
855
- if (Boolean(newFile) !== Boolean(oldFile) || oldFile.error !== newFile.error) {
856
- if (!this.$refs[this.uploadRef].active) {
857
- this.$refs[this.uploadRef].active = true
858
- }
859
- }
860
- },
861
-
862
- // ==================== 值变化处理 ====================
863
-
864
- /**
865
- * 重置文件状态
866
- */
867
- resetFiles() {
868
- this.files = []
869
- this.buildedFiles = []
870
- // 多选模式下保留或生成 groupId,避免首次上传为空
871
- if (this.multiple) {
872
- if (!this.groupId) {
873
- this.groupId = uuidv4()
874
- console.log('Generated groupId in resetFiles:', this.groupId)
875
- } else {
876
- console.log('Preserve existing groupId in resetFiles:', this.groupId)
877
- }
878
- } else {
879
- this.groupId = null
880
- console.log('Reset groupId to null (single mode)')
881
- }
882
- },
883
-
884
- /**
885
- * 处理多文件模式的值变化
886
- */
887
- async handleMultipleFileValue(value) {
888
- // multiple - value 就是 groupId
889
- // 只有当 groupId 发生变化时才重新获取文件列表(初始化回显)
890
- if (this.groupId !== value) {
891
- this.groupId = value
892
- console.log('Set groupId from external value:', this.groupId)
893
- await this.fetchFilesWithGroupId()
894
- } else {
895
- console.log('GroupId unchanged, skip fetchFilesWithGroupId')
896
- }
897
- },
898
-
899
- /**
900
- * 处理单文件模式的值变化
901
- */
902
- async handleSingleFileValue() {
903
- await this.fetchFileWithFileId()
904
- }
905
- }
906
- }
907
- </script>
908
-
909
- <style lang="scss" scoped>
910
- ::v-deep .ele-upload__inner {
911
- opacity: 1 !important;
912
- cursor: pointer;
913
- border: 1px dashed var(--idooel-form-title-border-color);
914
- background: var(--idooel-form-upload-bg-color) !important;
915
- &:hover {
916
- border-color: var(--idooel-form-upload-border-hover-color);
917
- }
918
- border-radius: var(--idooel-form-border-radius);
919
- }
920
- .ele-upload__wrapper {
921
- width: 100%;
922
- .ele-upload__area {
923
- padding: 16px;
924
- width: 100%;
925
- height: 80px;
926
- display: flex;
927
- flex-direction: row;
928
- .ele-upload__area--icon {
929
- color: var(--idooel-primary-color);
930
- display: flex;
931
- flex-direction: row;
932
- align-items: center;
933
- font-size: 16x;
934
- .anticon-cloud-upload {
935
- font-size: 48px;
936
- color: var(--idooel-primary-color);
937
- }
938
- .anticon {
939
- font-size: 48px;
940
- color: var(--idooel-primary-color);
941
- }
942
- }
943
- .ele-upload__area--text {
944
- margin-left: 16px;
945
- .ele-upload__message {
946
- font-size: 16px;
947
- color: var(--idoole-black-088);
948
- text-align: left;
949
- }
950
- .ele-upload__ext {
951
- text-align: left;
952
- font-size: 14px;
953
- color: var(--idoole-black-06);
954
- }
955
- }
956
- }
957
- .ele-files__wrapper {
958
- .ele-file__item {
959
- width: 100%;
960
- margin-top: 8px;
961
- padding: 8px 12px;
962
- border-radius: var(--idooel-form-border-radius);
963
- background: var(--idooel-form-upload-bg-color);
964
- display: flex;
965
- flex-direction: row;
966
- align-items: center;
967
- .ele-file__suffix--icon {
968
- display: flex;
969
- align-items: center;
970
- justify-content: center;
971
- width: 24px;
972
- height: 24px;
973
- }
974
- .ele-file__name {
975
- flex: 1;
976
- text-align: left;
977
- white-space: nowrap;
978
- overflow: hidden;
979
- font-size: 14px;
980
- margin-left: 8px;
981
- cursor: pointer;
982
- .ele-file__inner {
983
- overflow: hidden;
984
- text-overflow: ellipsis;
985
- }
986
- }
987
- .ele-file__delete {
988
- margin-left: 8px;
989
- .ele-file__delete--icon {
990
- margin-left: 8px;
991
- cursor: pointer;
992
- }
993
- }
994
- }
995
- }
996
- }
1
+ <template>
2
+ <div class="ele-upload__wrapper">
3
+ <FileUpload
4
+ class="ele-upload__inner"
5
+ v-show="isShowUploadContainer"
6
+ v-model="files"
7
+ :ref="uploadRef"
8
+ :drop="drop"
9
+ :chunk-enabled="chunkEnabled"
10
+ :chunk="chunkConfig"
11
+ :accept="accept"
12
+ :size="fileSizeLimit"
13
+ :post-action="postAction"
14
+ :multiple="multiple"
15
+ :headers="headers"
16
+ :maximum="getMaximum"
17
+ :data="uploadParams"
18
+ @input-file="onWatchInputFiles"
19
+ @input="onWatchFiles"
20
+ style="width: 100%;">
21
+ <section class="ele-upload__area">
22
+ <div class="ele-upload__area--icon">
23
+ <template v-if="iconIsZhWrod">
24
+ {{ icon }}
25
+ </template>
26
+ <template v-else>
27
+ <ele-icon :type="icon"></ele-icon>
28
+ </template>
29
+ </div>
30
+ <div class="ele-upload__area--text">
31
+ <div class="ele-upload__message" v-if="message" v-html="message"></div>
32
+ <div class="ele-upload__message" v-else>单击或拖动文件到该区域以上传</div>
33
+ <div class="ele-upload__ext" v-if="ext" v-html="ext"></div>
34
+ <div class="ele-upload__ext" v-else>文件小于{{ size }}M</div>
35
+ </div>
36
+ </section>
37
+ </FileUpload>
38
+ <section class="ele-files__wrapper">
39
+ <!-- 显示正在上传的文件(有进度条) -->
40
+ <div class="ele-file__item" v-for="(file, idx) in uploadingFiles" :key="`uploading-${idx}`">
41
+ <div class="ele-file__suffix--icon">
42
+ <ele-icon :type="fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'"></ele-icon>
43
+ </div>
44
+ <div class="ele-file__name">
45
+ <div class="ele-file__inner">{{ file.name }}</div>
46
+ <div v-if="file.progress !== undefined" class="ele-uplpad__progress">
47
+ <a-progress :strokeWidth="2" :percent="Number(file.progress)" size="small" />
48
+ </div>
49
+ </div>
50
+ <div class="ele-file__delete" v-if="file.success || file.error">
51
+ <span class="ele-file__size">{{ (file.size / byteConversion).toFixed(2) }}M</span>
52
+ <span class="ele-file__delete--icon" @click="handleClickDelete(file)">
53
+ <ele-icon type="delete"></ele-icon>
54
+ </span>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- 显示已上传完成的文件 -->
59
+ <div class="ele-file__item" v-for="(file, idx) in completedFiles" :key="`completed-${idx}`">
60
+ <div class="ele-file__suffix--icon">
61
+ <ele-icon :type="fileSuffixIcon[file.suffix] ? fileSuffixIcon[file.suffix].name : 'icon-file'"></ele-icon>
62
+ </div>
63
+ <div class="ele-file__name">
64
+ <div class="ele-file__inner" @click="handleClickDownload(file)">{{ file.name }}</div>
65
+ </div>
66
+ <div class="ele-file__delete">
67
+ <span class="ele-file__size">{{ (file.size / byteConversion).toFixed(2) }}M</span>
68
+ <span class="ele-file__delete--icon" @click="handleClickDelete(file)">
69
+ <ele-icon type="delete"></ele-icon>
70
+ </span>
71
+ </div>
72
+ </div>
73
+ </section>
74
+ </div>
75
+ </template>
76
+
77
+ <script>
78
+ import FileUpload from 'vue-upload-component'
79
+ import { v4 as uuidv4 } from 'uuid'
80
+ import { route, net, type } from '@idooel/shared'
81
+
82
+ // 常量定义
83
+ const CONSTANTS = {
84
+ DEFAULT_URL: 'zuul/api-file/workbench/file',
85
+ DEFAULT_ICON: '上传',
86
+ DEFAULT_SIZE: 100,
87
+ DEFAULT_MESSAGE: '单击或拖动文件到该区域以上传',
88
+ DEFAULT_MAXIMUM: 20,
89
+ BYTE_CONVERSION: 1024 * 1024,
90
+ CHUNK_MIN_SIZE: 3 * 1024 * 1024,
91
+ CHUNK_MAX_ACTIVE: 3,
92
+ CHUNK_MAX_RETRIES: 5,
93
+ SAVE_INTERVAL: 2000
94
+ }
95
+
96
+ // 文件后缀图标映射
97
+ const FILE_SUFFIX_ICONS = {
98
+ 'doc': { name: 'icon-doc' },
99
+ 'html': { name: 'icon-html' },
100
+ 'mp4': { name: 'icon-mp' },
101
+ 'pdf': { name: 'icon-pdf' },
102
+ 'ppt': { name: 'icon-ppt' },
103
+ 'psd': { name: 'icon-psd' },
104
+ 'rtf': { name: 'icon-rtf' },
105
+ 'txt': { name: 'icon-txt' },
106
+ 'vis': { name: 'icon-vis' },
107
+ 'xls': { name: 'icon-xls' },
108
+ 'xml': { name: 'icon-xml' },
109
+ 'zip': { name: 'icon-zip' },
110
+ 'jpg': { name: 'icon-img' },
111
+ 'mp3': { name: 'icon-mp1' }
112
+ }
113
+
114
+ export default {
115
+ name: 'ele-upload',
116
+ components: {
117
+ FileUpload
118
+ },
119
+ model: {
120
+ prop: 'value',
121
+ event: 'change'
122
+ },
123
+ props: {
124
+ url: {
125
+ type: String,
126
+ default: CONSTANTS.DEFAULT_URL
127
+ },
128
+ icon: {
129
+ type: String,
130
+ default: CONSTANTS.DEFAULT_ICON
131
+ },
132
+ size: {
133
+ type: Number,
134
+ default: CONSTANTS.DEFAULT_SIZE
135
+ },
136
+ message: {
137
+ type: String,
138
+ default: CONSTANTS.DEFAULT_MESSAGE
139
+ },
140
+ ext: {
141
+ type: String
142
+ },
143
+ extensions: {
144
+ type: String
145
+ },
146
+ accept: {
147
+ type: String
148
+ },
149
+ maximum: {
150
+ type: Number,
151
+ default: CONSTANTS.DEFAULT_MAXIMUM
152
+ },
153
+ multiple: {
154
+ type: Boolean,
155
+ default: false
156
+ },
157
+ drop: {
158
+ type: Boolean,
159
+ default: true
160
+ },
161
+ value: {
162
+ type: [String, Array]
163
+ },
164
+ querys: {
165
+ type: Object,
166
+ default: () => ({
167
+ _csrf: localStorage.getItem('token')
168
+ })
169
+ },
170
+ headers: {
171
+ type: Object,
172
+ default: () => ({
173
+ 'X-XSRF-TOKEN': localStorage.getItem('token')
174
+ })
175
+ },
176
+ byteConversion: {
177
+ type: Number,
178
+ default: CONSTANTS.BYTE_CONVERSION
179
+ },
180
+ chunkEnabled: {
181
+ type: Boolean,
182
+ default: true
183
+ }
184
+ },
185
+ data() {
186
+ return {
187
+ // 文件状态管理
188
+ files: [], // vue-upload-component 管理的文件
189
+ buildedFiles: [], // 已构建完成的文件列表
190
+
191
+ // 上传状态管理
192
+ saveToServerAsyncPageTimer: null,
193
+ uploadRefId: null,
194
+ // 多选且外部无值时,预先生成 groupId,确保首次上传参数完整
195
+ groupId: (this.multiple && !this.value) ? uuidv4() : null
196
+ }
197
+ },
198
+ created() {
199
+ // 多文件模式下,如果没有外部 groupId,则生成一个
200
+ if (this.multiple && !this.value) {
201
+ this.groupId = uuidv4()
202
+ console.log('Created with new groupId:', this.groupId)
203
+ }
204
+ },
205
+ watch: {
206
+ value: {
207
+ async handler(value) {
208
+ if (type.isEmpty(value)) {
209
+ this.resetFiles()
210
+ } else if (this.multiple) {
211
+ this.handleMultipleFileValue(value)
212
+ } else {
213
+ this.handleSingleFileValue()
214
+ }
215
+ },
216
+ immediate: true
217
+ }
218
+ },
219
+ computed: {
220
+ // ==================== 基础配置 ====================
221
+ prefixPath() {
222
+ return window.prefixPath
223
+ },
224
+
225
+ uploadRef() {
226
+ if (!this.uploadRefId) {
227
+ this.uploadRefId = `uploadRef_${uuidv4()}`
228
+ }
229
+ return this.uploadRefId
230
+ },
231
+
232
+ iconIsZhWord() {
233
+ return type.isZhWord(this.icon)
234
+ },
235
+
236
+ // ==================== 上传配置 ====================
237
+ uploadParams() {
238
+ return this.multiple ? { groupID: this.groupId } : {}
239
+ },
240
+
241
+ fileSizeLimit() {
242
+ return this.size * this.byteConversion
243
+ },
244
+
245
+ postAction() {
246
+ const queryString = route.toQueryString(this.querys)
247
+ return `${this.prefixPath}${this.url}?${queryString}`
248
+ },
249
+
250
+ chunkConfig() {
251
+ return {
252
+ action: `${this.prefixPath}zuul/api-file/workbench/file/temp/chunk/vue`,
253
+ headers: { ...this.headers },
254
+ minSize: CONSTANTS.CHUNK_MIN_SIZE,
255
+ maxActive: CONSTANTS.CHUNK_MAX_ACTIVE,
256
+ maxRetries: CONSTANTS.CHUNK_MAX_RETRIES,
257
+ startBody: { override: true, path: '/cw' },
258
+ uploadBody: { override: true, path: '/cw' },
259
+ finishBody: { override: true, path: '/cw' }
260
+ }
261
+ },
262
+
263
+ getMaximum() {
264
+ return this.multiple ? this.maximum : 1
265
+ },
266
+
267
+ // ==================== 文件状态 ====================
268
+ uploadingFiles() {
269
+ // 筛选条件:有进度信息 且 (未成功 或 成功但还没有fileID的切片上传文件)
270
+ // 这样可以确保切片上传完成但异步保存未完成的文件继续显示,避免闪烁
271
+ return this.files.filter(file => {
272
+ if (file.progress === undefined) return false
273
+ if (!file.success) return true
274
+
275
+ // 检查文件是否已经有 fileID(可能在 file.fileID 或 file.response.data.fileID)
276
+ const hasFileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)
277
+
278
+ // 切片上传成功但还没有完成异步保存(没有fileID)的文件继续显示
279
+ // 普通上传成功且已有 fileID 的文件不显示(会在 completedFiles 中显示)
280
+ return file.success && !hasFileID
281
+ })
282
+ },
283
+
284
+ completedFiles() {
285
+ return this.buildedFiles.filter(file => file.fileID)
286
+ },
287
+
288
+ totalFiles() {
289
+ return this.uploadingFiles.length + this.completedFiles.length
290
+ },
291
+
292
+ isFileUploadSuccessed() {
293
+ const currentUploadingFiles = this.files.filter(file => file.response !== undefined)
294
+ if (currentUploadingFiles.length === 0) {
295
+ return this.buildedFiles.length > 0
296
+ }
297
+ return currentUploadingFiles.every(file => file.success)
298
+ },
299
+
300
+ isShowUploadContainer() {
301
+ const maxFiles = this.multiple ? this.maximum : 1
302
+ return this.totalFiles < maxFiles
303
+ },
304
+
305
+ // ==================== 文件信息 ====================
306
+ fileSuffixIcon() {
307
+ return FILE_SUFFIX_ICONS
308
+ },
309
+
310
+ fileIds() {
311
+ if (this.multiple) {
312
+ return this.groupId
313
+ } else {
314
+ const fileIds = this.buildedFiles.map(file => file.fileID)
315
+ return fileIds[0]
316
+ }
317
+ },
318
+
319
+ fileResponseData() {
320
+ return this.multiple ? this.buildedFiles : this.buildedFiles[0]
321
+ }
322
+ },
323
+ methods: {
324
+ // ==================== 文件管理 ====================
325
+
326
+ /**
327
+ * 从多个数组中移除文件
328
+ */
329
+ removeFromArrays(file, arrays, key = 'fileID') {
330
+ return arrays.map(arr =>
331
+ arr.filter(item => {
332
+ // 如果 fileID 都存在且相等,则移除
333
+ if (item[key] && file[key] && item[key] === file[key]) {
334
+ return false
335
+ }
336
+ // 如果 id 都存在且相等,则移除
337
+ if (item.id && file.id && item.id === file.id) {
338
+ return false
339
+ }
340
+ // 如果文件名相同,也考虑移除(额外的安全检查)
341
+ if (item.name && file.name && item.name === file.name && item[key] === file[key]) {
342
+ return false
343
+ }
344
+ // 否则保留
345
+ return true
346
+ })
347
+ )
348
+ },
349
+
350
+ /**
351
+ * 检查文件是否为新文件
352
+ */
353
+ isNewFile(newFile, existingFiles, key = 'fileID') {
354
+ return newFile[key] && !existingFiles.some(existing => existing[key] === newFile[key])
355
+ },
356
+
357
+ /**
358
+ * 合并文件数据
359
+ */
360
+ mergeFileData(uploadFile) {
361
+ return {
362
+ ...uploadFile.response.data,
363
+ ...uploadFile
364
+ }
365
+ },
366
+
367
+ /**
368
+ * 初始化文件列表
369
+ */
370
+ async initializeFiles() {
371
+ if (!this.value) return
372
+
373
+ if (this.multiple) {
374
+ await this.fetchFilesWithGroupId()
375
+ } else {
376
+ await this.fetchFileWithFileId()
377
+ }
378
+ },
379
+
380
+ /**
381
+ * 获取多文件组
382
+ */
383
+ async fetchFilesWithGroupId() {
384
+ try {
385
+ const response = await net.get(`/api-file/workbench/file/group/${this.value}`)
386
+ const data = response.data || []
387
+
388
+ // 只有在没有现有文件时才设置初始文件列表
389
+ if (this.buildedFiles.length === 0) {
390
+ this.buildedFiles = data
391
+ console.log('Initial files loaded:', this.buildedFiles.length)
392
+ } else {
393
+ console.log('Keep existing files, skip initial load')
394
+ }
395
+ } catch (error) {
396
+ console.log('fetchFilesWithGroupId error:', error)
397
+ console.log('Keep current files, do not clear due to API error')
398
+ }
399
+ },
400
+
401
+ /**
402
+ * 获取单文件
403
+ */
404
+ async fetchFileWithFileId() {
405
+ try {
406
+ const response = await net.get(`/api-file/file/${this.value}`)
407
+ const data = response.data
408
+ this.buildedFiles = [data]
409
+ this.files = [data]
410
+ } catch (error) {
411
+ console.log('fetchFileWithFileId error:', error)
412
+ }
413
+ },
414
+
415
+ /**
416
+ * 调用服务器删除文件 API
417
+ */
418
+ async deleteFiles (ids) {
419
+ if (!ids) {
420
+ console.warn('No fileID provided for deletion')
421
+ return
422
+ }
423
+
424
+ if (type.isArray(ids)) {
425
+ ids = ids.join(',')
426
+ }
427
+
428
+ try {
429
+ await net.remove(`/api-file/workbench/file?ids=${ids}`)
430
+ console.log('File deleted from server:', ids)
431
+ } catch (error) {
432
+ console.error('Failed to delete file from server:', error)
433
+ throw error // 重新抛出错误,让调用方处理
434
+ }
435
+ },
436
+
437
+ /**
438
+ * 处理文件删除
439
+ */
440
+ async handleClickDelete(file) {
441
+ const { fileID, name } = file
442
+ console.log('Deleting file:', { name, fileID })
443
+
444
+ // 先从上传组件中移除文件
445
+ if (this.$refs[this.uploadRef]) {
446
+ this.$refs[this.uploadRef].remove(file)
447
+ }
448
+
449
+ // 从所有数组中移除文件
450
+ [this.files, this.buildedFiles] = this.removeFromArrays(file, [this.files, this.buildedFiles])
451
+
452
+ console.log('After deletion - files:', this.files.length, 'buildedFiles:', this.buildedFiles.length)
453
+
454
+ // 如果有 fileID,调用服务器删除 API
455
+ if (fileID) {
456
+ try {
457
+ await this.deleteFiles(fileID)
458
+ } catch (error) {
459
+ console.error('Failed to delete file from server:', error)
460
+ // 可以选择提示用户或回滚操作
461
+ // 这里仅记录错误,不影响前端显示
462
+ }
463
+ }
464
+
465
+ // 多文件模式下,如果删除最后一个文件,重置 groupId
466
+ if (this.multiple && this.buildedFiles.length === 0) {
467
+ this.groupId = null
468
+ console.log('Reset groupId after deleting last file')
469
+ }
470
+
471
+ // 触发 change 事件
472
+ this.$emit('change', this.fileIds)
473
+ },
474
+
475
+ /**
476
+ * 处理文件下载
477
+ */
478
+ handleClickDownload(file) {
479
+ const { fileID: fileId } = file
480
+ window.open(`/api-file/workbench/file/stream/${fileId}?origin=true`)
481
+ },
482
+ // ==================== 上传处理 ====================
483
+
484
+ /**
485
+ * 处理文件上传状态变化
486
+ */
487
+ onWatchFiles(files) {
488
+ console.log('onWatchFiles called with files:', files.length)
489
+ console.log('Current buildedFiles:', this.buildedFiles.length)
490
+
491
+ // 更新文件状态
492
+ this.files = files
493
+
494
+ // 处理已上传成功的文件
495
+ this.processUploadedFiles(files)
496
+
497
+ // 检查上传是否完成
498
+ if (this.isFileUploadSuccessed) {
499
+ this.$emit('change', this.fileIds)
500
+ this.$emit('on-success', this.fileResponseData)
501
+ }
502
+ },
503
+
504
+ /**
505
+ * 处理已上传成功的文件
506
+ */
507
+ processUploadedFiles(files) {
508
+ // 处理所有有响应的文件(包括正在上传和已完成的)
509
+ const uploadedFiles = files.filter(file => file.response)
510
+ const newBuildedFiles = uploadedFiles.map(file => this.mergeFileData(file))
511
+
512
+ if (this.multiple) {
513
+ this.processMultipleFiles(newBuildedFiles)
514
+ } else {
515
+ // 单文件模式:只保留最新的文件
516
+ this.buildedFiles = newBuildedFiles
517
+
518
+ // 从 files 数组中移除已完成的文件(普通上传),避免重复显示
519
+ if (newBuildedFiles.length > 0 && newBuildedFiles[0].fileID) {
520
+ this.$nextTick(() => {
521
+ this.files = this.files.filter(file => {
522
+ const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)
523
+ return fileID !== newBuildedFiles[0].fileID
524
+ })
525
+ console.log('Removed completed file from upload list (single mode)')
526
+ })
527
+ }
528
+ }
529
+
530
+ this.logFileStatus()
531
+ },
532
+
533
+ /**
534
+ * 处理多文件模式
535
+ */
536
+ processMultipleFiles(newBuildedFiles) {
537
+ // 获取已存在的文件ID集合
538
+ const existingFileIds = new Set(this.buildedFiles.map(f => f.fileID).filter(id => id))
539
+
540
+ // 过滤出真正的新文件
541
+ const trulyNewFiles = newBuildedFiles.filter(newFile =>
542
+ this.isNewFile(newFile, this.buildedFiles)
543
+ )
544
+
545
+ console.log('Existing fileIDs:', Array.from(existingFileIds))
546
+ console.log('New uploaded files:', newBuildedFiles.map(f => ({ name: f.name, fileID: f.fileID })))
547
+ console.log('Truly new files:', trulyNewFiles.map(f => ({ name: f.name, fileID: f.fileID })))
548
+
549
+ // 将新文件追加到现有文件列表
550
+ if (trulyNewFiles.length > 0) {
551
+ this.buildedFiles = [...this.buildedFiles, ...trulyNewFiles]
552
+ console.log('Added new files, total buildedFiles:', this.buildedFiles.length)
553
+
554
+ // 从 files 数组中移除已完成的文件(普通上传),避免重复显示
555
+ this.$nextTick(() => {
556
+ const completedFileIDs = new Set(trulyNewFiles.map(f => f.fileID))
557
+ this.files = this.files.filter(file => {
558
+ const fileID = file.fileID || (file.response && file.response.data && file.response.data.fileID)
559
+ return !completedFileIDs.has(fileID)
560
+ })
561
+ console.log('Removed completed files from upload list')
562
+ })
563
+ }
564
+
565
+ // 更新现有文件的状态(包括新添加的文件)
566
+ this.updateExistingFiles(newBuildedFiles)
567
+ },
568
+
569
+ /**
570
+ * 更新现有文件状态
571
+ */
572
+ updateExistingFiles(newBuildedFiles) {
573
+ this.buildedFiles = this.buildedFiles.map(existingFile => {
574
+ const updatedFile = newBuildedFiles.find(newFile => newFile.fileID === existingFile.fileID)
575
+ return updatedFile ? { ...existingFile, ...updatedFile } : existingFile
576
+ })
577
+ },
578
+
579
+ /**
580
+ * 记录文件状态日志
581
+ */
582
+ logFileStatus() {
583
+ console.log('Final buildedFiles:', this.buildedFiles.length)
584
+ console.log('buildedFiles details:', this.buildedFiles.map(f => ({ name: f.name, fileID: f.fileID, success: f.success })))
585
+ console.log('Uploading files:', this.uploadingFiles.length)
586
+ console.log('Completed files:', this.completedFiles.length)
587
+ },
588
+ // ==================== 异步处理 ====================
589
+
590
+ /**
591
+ * 异步保存文件到服务器
592
+ * @param {FormData} payloads - 上传参数
593
+ * @param {Object} uploadingFile - 正在上传的文件对象(可选)
594
+ */
595
+ async saveToServerAsyncPage(payloads = {}, uploadingFile = null) {
596
+ try {
597
+ const response = await net.post('zuul/api-file/workbench/file/temp/saveToServerAsyncPage', payloads, {
598
+ headers: {
599
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
600
+ }
601
+ })
602
+
603
+ const { data } = response
604
+ if (data !== 'saveToServerAsyncPage') {
605
+ clearInterval(this.saveToServerAsyncPageTimer)
606
+ console.log('Chunk upload async save completed, file data:', data)
607
+
608
+ const { fileID, groupID, name } = data
609
+
610
+ // 根据上传模式处理文件信息
611
+ if (this.multiple) {
612
+ // 多文件模式:更新 groupID 并追加文件到列表
613
+ if (groupID) {
614
+ this.groupId = groupID
615
+ console.log('Updated groupId from server:', groupID)
616
+ }
617
+
618
+ // 检查文件是否已存在,避免重复添加
619
+ const fileExists = this.buildedFiles.some(file => file.fileID === fileID)
620
+ if (!fileExists && fileID) {
621
+ // 将服务器返回的完整文件信息追加到文件列表
622
+ // 添加必要的显示属性
623
+ const fileData = {
624
+ ...data,
625
+ success: true // 标记为上传成功
626
+ }
627
+
628
+ // 先添加到已完成列表
629
+ this.buildedFiles.push(fileData)
630
+ console.log('File added to list:', name, 'Total files:', this.buildedFiles.length)
631
+
632
+ // 使用 nextTick 确保视图更新后再移除上传列表中的文件,避免闪烁
633
+ this.$nextTick(() => {
634
+ // 从上传列表中精确移除对应的文件
635
+ if (uploadingFile) {
636
+ // 如果有文件对象引用,直接移除该文件
637
+ this.files = this.files.filter(file => file.id !== uploadingFile.id)
638
+ console.log('Removed uploading file by id:', uploadingFile.id)
639
+ } else {
640
+ // 否则根据文件名移除
641
+ this.files = this.files.filter(file => file.name !== name)
642
+ console.log('Removed uploading file by name:', name)
643
+ }
644
+ })
645
+
646
+ // 触发 change 事件通知父组件
647
+ this.$emit('change', this.fileIds)
648
+ this.$emit('on-success', this.fileResponseData)
649
+ } else {
650
+ console.log('File already exists or invalid fileID, skip adding')
651
+ }
652
+ } else {
653
+ // 单文件模式:替换文件列表
654
+ if (fileID) {
655
+ const fileData = {
656
+ ...data,
657
+ success: true // 标记为上传成功
658
+ }
659
+ this.buildedFiles = [fileData]
660
+ this.files = [] // 清空上传列表
661
+ console.log('Single file updated:', name)
662
+
663
+ // 触发 change 事件通知父组件更新 value
664
+ this.$emit('change', fileID)
665
+ this.$emit('on-success', data)
666
+ }
667
+ }
668
+ }
669
+ } catch (error) {
670
+ console.error('saveToServerAsyncPage error:', error)
671
+ clearInterval(this.saveToServerAsyncPageTimer)
672
+ }
673
+ },
674
+
675
+ // ==================== 文件验证 ====================
676
+
677
+ /**
678
+ * 验证文件类型
679
+ */
680
+ validateFileType(file) {
681
+ if (!file || !file.name) {
682
+ console.log('文件或文件名不存在')
683
+ return false
684
+ }
685
+
686
+ const fileExt = this.getFileExtension(file.name)
687
+ console.log('文件扩展名:', fileExt)
688
+
689
+ if (this.extensions) {
690
+ const allowedExts = this.getAllowedExtensions()
691
+ console.log('允许的扩展名:', allowedExts)
692
+
693
+ if (!allowedExts.includes(fileExt)) {
694
+ console.log('扩展名不在允许列表中')
695
+ this.$message.error(`不支持的文件类型 "${fileExt}",请上传 ${this.extensions} 格式的文件`)
696
+ return false
697
+ }
698
+ }
699
+
700
+ console.log('文件类型验证通过')
701
+ return true
702
+ },
703
+
704
+ /**
705
+ * 获取文件扩展名
706
+ */
707
+ getFileExtension(fileName) {
708
+ return fileName.toLowerCase().substring(fileName.lastIndexOf('.'))
709
+ },
710
+
711
+ /**
712
+ * 获取允许的扩展名列表
713
+ */
714
+ getAllowedExtensions() {
715
+ return this.extensions.toLowerCase()
716
+ .split(',')
717
+ .map(ext => ext.trim().startsWith('.') ? ext.trim() : `.${ext.trim()}`)
718
+ },
719
+ // ==================== 事件处理 ====================
720
+
721
+ /**
722
+ * 处理文件输入事件
723
+ */
724
+ onWatchInputFiles(newFile, oldFile) {
725
+ if (newFile && !oldFile) {
726
+ this.handleFileAdd(newFile)
727
+ } else if (newFile && oldFile) {
728
+ this.handleFileUpdate(newFile)
729
+ } else if (!newFile && oldFile) {
730
+ this.handleFileDelete()
731
+ }
732
+
733
+ // 激活上传组件
734
+ this.activateUploadComponent(newFile, oldFile)
735
+ },
736
+
737
+ /**
738
+ * 生成唯一的随机数
739
+ */
740
+ generateUniqueRandom() {
741
+ // 使用 crypto.getRandomValues 生成更安全的随机数
742
+ if (window.crypto && window.crypto.getRandomValues) {
743
+ const array = new Uint32Array(1)
744
+ window.crypto.getRandomValues(array)
745
+ return array[0].toString()
746
+ }
747
+ // 降级方案:使用 Math.random 结合时间戳
748
+ return Math.random().toString(36).substr(2, 9) + Date.now().toString(36)
749
+ },
750
+
751
+ /**
752
+ * 处理文件添加
753
+ */
754
+ handleFileAdd(newFile) {
755
+ // 为每个文件生成唯一的随机数
756
+ const uniqueRandom = this.generateUniqueRandom()
757
+ newFile.postAction = newFile.postAction + '&_t=' + uniqueRandom
758
+ console.log('add file:', newFile)
759
+ console.log('extensions:', this.extensions)
760
+ console.log('accept:', this.accept)
761
+
762
+ // 生成或使用 groupId
763
+ this.ensureGroupId()
764
+
765
+ // 验证文件类型
766
+ if (!this.validateFileType(newFile)) {
767
+ this.removeInvalidFile(newFile)
768
+ return
769
+ }
770
+ console.log('文件类型验证通过,继续上传')
771
+ },
772
+
773
+ /**
774
+ * 处理文件更新
775
+ */
776
+ handleFileUpdate(newFile) {
777
+ console.log('update', newFile)
778
+ const { success, active, chunk, response } = newFile
779
+
780
+ if (chunk && success && !active) {
781
+ console.log('chunk end')
782
+ this.handleChunkComplete(response, newFile)
783
+ }
784
+ },
785
+
786
+ /**
787
+ * 处理文件删除
788
+ */
789
+ handleFileDelete() {
790
+ console.log('delete')
791
+ },
792
+
793
+ /**
794
+ * 确保 groupId 存在
795
+ */
796
+ ensureGroupId() {
797
+ console.log('onWatchInputFiles - multiple:', this.multiple, 'current groupId:', this.groupId)
798
+
799
+ if (this.multiple && !this.groupId) {
800
+ this.groupId = uuidv4()
801
+ console.log('Generated new groupId:', this.groupId)
802
+ } else if (this.multiple && this.groupId) {
803
+ console.log('Using existing groupId:', this.groupId)
804
+ }
805
+ },
806
+
807
+ /**
808
+ * 移除无效文件
809
+ */
810
+ removeInvalidFile(file) {
811
+ console.log('文件类型验证失败,尝试移除文件')
812
+ console.log('uploadRef:', this.uploadRef)
813
+ console.log('$refs:', this.$refs)
814
+
815
+ if (this.$refs[this.uploadRef]) {
816
+ this.$refs[this.uploadRef].remove(file)
817
+ } else {
818
+ console.error('无法找到 uploadRef 引用')
819
+ }
820
+ },
821
+
822
+ /**
823
+ * 处理分片上传完成
824
+ * @param {Object} response - 上传响应
825
+ * @param {Object} uploadingFile - 正在上传的文件对象(可选)
826
+ */
827
+ handleChunkComplete(response, uploadingFile = null) {
828
+ const { data: { file, type } } = response
829
+ const payloads = {
830
+ filePath: file.match(/\/cw(.*)/) ? file.match(/\/cw(.*)/)[0] : void 0,
831
+ asyncID: uuidv4(),
832
+ groupID: this.groupId,
833
+ isDeleteOrigin: false,
834
+ toImage: type === 'pdf',
835
+ unzip: type === 'zip',
836
+ _csrf: localStorage.getItem('token')
837
+ }
838
+
839
+ const formData = new FormData()
840
+
841
+ Object.keys(payloads).forEach(key => {
842
+ formData.append(key, payloads[key])
843
+ })
844
+
845
+ // 将上传文件对象传递给异步保存方法
846
+ this.saveToServerAsyncPageTimer = setInterval(() => {
847
+ this.saveToServerAsyncPage(formData, uploadingFile)
848
+ }, CONSTANTS.SAVE_INTERVAL)
849
+ },
850
+
851
+ /**
852
+ * 激活上传组件
853
+ */
854
+ activateUploadComponent(newFile, oldFile) {
855
+ if (Boolean(newFile) !== Boolean(oldFile) || oldFile.error !== newFile.error) {
856
+ if (!this.$refs[this.uploadRef].active) {
857
+ this.$refs[this.uploadRef].active = true
858
+ }
859
+ }
860
+ },
861
+
862
+ // ==================== 值变化处理 ====================
863
+
864
+ /**
865
+ * 重置文件状态
866
+ */
867
+ resetFiles() {
868
+ this.files = []
869
+ this.buildedFiles = []
870
+ // 多选模式下保留或生成 groupId,避免首次上传为空
871
+ if (this.multiple) {
872
+ if (!this.groupId) {
873
+ this.groupId = uuidv4()
874
+ console.log('Generated groupId in resetFiles:', this.groupId)
875
+ } else {
876
+ console.log('Preserve existing groupId in resetFiles:', this.groupId)
877
+ }
878
+ } else {
879
+ this.groupId = null
880
+ console.log('Reset groupId to null (single mode)')
881
+ }
882
+ },
883
+
884
+ /**
885
+ * 处理多文件模式的值变化
886
+ */
887
+ async handleMultipleFileValue(value) {
888
+ // multiple - value 就是 groupId
889
+ // 只有当 groupId 发生变化时才重新获取文件列表(初始化回显)
890
+ if (this.groupId !== value) {
891
+ this.groupId = value
892
+ console.log('Set groupId from external value:', this.groupId)
893
+ await this.fetchFilesWithGroupId()
894
+ } else {
895
+ console.log('GroupId unchanged, skip fetchFilesWithGroupId')
896
+ }
897
+ },
898
+
899
+ /**
900
+ * 处理单文件模式的值变化
901
+ */
902
+ async handleSingleFileValue() {
903
+ await this.fetchFileWithFileId()
904
+ }
905
+ }
906
+ }
907
+ </script>
908
+
909
+ <style lang="scss" scoped>
910
+ ::v-deep .ele-upload__inner {
911
+ opacity: 1 !important;
912
+ cursor: pointer;
913
+ border: 1px dashed var(--idooel-form-title-border-color);
914
+ background: var(--idooel-form-upload-bg-color) !important;
915
+ &:hover {
916
+ border-color: var(--idooel-form-upload-border-hover-color);
917
+ }
918
+ border-radius: var(--idooel-form-border-radius);
919
+ }
920
+ .ele-upload__wrapper {
921
+ width: 100%;
922
+ .ele-upload__area {
923
+ padding: 16px;
924
+ width: 100%;
925
+ height: 80px;
926
+ display: flex;
927
+ flex-direction: row;
928
+ .ele-upload__area--icon {
929
+ color: var(--idooel-primary-color);
930
+ display: flex;
931
+ flex-direction: row;
932
+ align-items: center;
933
+ font-size: 16x;
934
+ .anticon-cloud-upload {
935
+ font-size: 48px;
936
+ color: var(--idooel-primary-color);
937
+ }
938
+ .anticon {
939
+ font-size: 48px;
940
+ color: var(--idooel-primary-color);
941
+ }
942
+ }
943
+ .ele-upload__area--text {
944
+ margin-left: 16px;
945
+ .ele-upload__message {
946
+ font-size: 16px;
947
+ color: var(--idoole-black-088);
948
+ text-align: left;
949
+ }
950
+ .ele-upload__ext {
951
+ text-align: left;
952
+ font-size: 14px;
953
+ color: var(--idoole-black-06);
954
+ }
955
+ }
956
+ }
957
+ .ele-files__wrapper {
958
+ .ele-file__item {
959
+ width: 100%;
960
+ margin-top: 8px;
961
+ padding: 8px 12px;
962
+ border-radius: var(--idooel-form-border-radius);
963
+ background: var(--idooel-form-upload-bg-color);
964
+ display: flex;
965
+ flex-direction: row;
966
+ align-items: center;
967
+ .ele-file__suffix--icon {
968
+ display: flex;
969
+ align-items: center;
970
+ justify-content: center;
971
+ width: 24px;
972
+ height: 24px;
973
+ }
974
+ .ele-file__name {
975
+ flex: 1;
976
+ text-align: left;
977
+ white-space: nowrap;
978
+ overflow: hidden;
979
+ font-size: 14px;
980
+ margin-left: 8px;
981
+ cursor: pointer;
982
+ .ele-file__inner {
983
+ overflow: hidden;
984
+ text-overflow: ellipsis;
985
+ }
986
+ }
987
+ .ele-file__delete {
988
+ margin-left: 8px;
989
+ .ele-file__delete--icon {
990
+ margin-left: 8px;
991
+ cursor: pointer;
992
+ }
993
+ }
994
+ }
995
+ }
996
+ }
997
997
  </style>