vue2-components-plus 1.0.11 → 1.0.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.
@@ -0,0 +1,882 @@
1
+ <template>
2
+ <div class="demo-page">
3
+ <el-card shadow="never" class="demo-card">
4
+ <div slot="header" class="demo-card__header">
5
+ <div>
6
+ <div class="demo-card__title">NsForm 动态表单预览(&lt;script setup&gt; 版)</div>
7
+ <div class="demo-card__desc">覆盖校验、回填、只读、级联、上传和动态表单项等常见场景,使用 &lt;script setup&gt; 写法。</div>
8
+
9
+ </div>
10
+ <el-tag size="small" :type="demoReadOnly ? 'info' : 'success'">
11
+ {{ demoReadOnly ? '只读模式' : '编辑模式' }}
12
+ </el-tag>
13
+ </div>
14
+
15
+ <el-alert
16
+ v-if="hintText"
17
+ :title="hintText"
18
+ type="info"
19
+ :closable="false"
20
+ class="demo-alert"
21
+ />
22
+
23
+ <div class="toolbar">
24
+ <el-button type="primary" @click="getFormData">获取表单数据</el-button>
25
+ <el-button @click="loadDetailData()">模拟详情回填</el-button>
26
+ <el-button @click="resetFormData()">重置表单</el-button>
27
+ <el-button @click="toggleReadOnly">切换只读</el-button>
28
+ <el-button @click="notifyInnerButton">触发自定义事件</el-button>
29
+ <el-button v-if="insideDialog" type="danger" plain @click="emit('close')">从内容区关闭弹窗</el-button>
30
+ </div>
31
+
32
+ <el-form ref="shellForm" :model="formState" label-position="top" class="shell-form">
33
+ <NsFormTitle title="模型参数">
34
+ <NsForm
35
+ ref="row1Ref"
36
+ :readOnly="demoReadOnly"
37
+ :model="demoReadOnly ? '' : 'vertical'"
38
+ :rows="formState.rows"
39
+ formPropKey="rows"
40
+ backgroundColor="#fff"
41
+ labelColor="#606266"
42
+ labelWidth="140"
43
+ gapH="20px"
44
+ gapV="10px"
45
+ />
46
+ </NsFormTitle>
47
+
48
+ <NsFormTitle title="视频配置">
49
+ <NsForm
50
+ ref="row2Ref"
51
+ :readOnly="demoReadOnly"
52
+ :model="demoReadOnly ? '' : 'vertical'"
53
+ :rows="formState.rows2"
54
+ formPropKey="rows2"
55
+ backgroundColor="#fff"
56
+ labelColor="#606266"
57
+ labelWidth="140"
58
+ gapH="20px"
59
+ gapV="10px"
60
+ />
61
+ </NsFormTitle>
62
+
63
+ <NsFormTitle title="结果保存">
64
+ <NsForm
65
+ ref="row3Ref"
66
+ :readOnly="demoReadOnly"
67
+ :model="demoReadOnly ? '' : 'vertical'"
68
+ :rows="formState.rows3"
69
+ formPropKey="rows3"
70
+ backgroundColor="#fff"
71
+ labelColor="#606266"
72
+ labelWidth="140"
73
+ gapH="20px"
74
+ gapV="10px"
75
+ />
76
+ </NsFormTitle>
77
+
78
+ <NsFormTitle title="级联选择器">
79
+ <NsForm
80
+ ref="row4Ref"
81
+ :readOnly="demoReadOnly"
82
+ :model="demoReadOnly ? '' : 'vertical'"
83
+ :rows="formState.rows4"
84
+ formPropKey="rows4"
85
+ backgroundColor="#fff"
86
+ labelColor="#606266"
87
+ labelWidth="140"
88
+ gapH="20px"
89
+ gapV="10px"
90
+ />
91
+ </NsFormTitle>
92
+
93
+ <NsFormTitle title="文件上传">
94
+ <NsForm
95
+ ref="rowUploadRef"
96
+ :readOnly="demoReadOnly"
97
+ :model="demoReadOnly ? '' : 'vertical'"
98
+ :rows="formState.rowsUpload"
99
+ formPropKey="rowsUpload"
100
+ backgroundColor="#fff"
101
+ labelColor="#606266"
102
+ labelWidth="140"
103
+ gapH="20px"
104
+ gapV="10px"
105
+ />
106
+ </NsFormTitle>
107
+ </el-form>
108
+ </el-card>
109
+
110
+ <el-card shadow="never" class="result-card">
111
+ <div slot="header" class="result-card__header">
112
+ <span>输出结果</span>
113
+ <el-button type="text" @click="showToast('已通过组件实例调用方法')">调用实例方法示例</el-button>
114
+ </div>
115
+ <pre class="result-content">{{ outputText || '点击“获取表单数据”后在此查看结果。' }}</pre>
116
+ </el-card>
117
+ </div>
118
+ </template>
119
+
120
+ <script setup>
121
+ import { getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from 'vue'
122
+
123
+ const props = defineProps({
124
+ readOnly: {
125
+ type: Boolean,
126
+ default: false,
127
+ },
128
+ insideDialog: {
129
+ type: Boolean,
130
+ default: false,
131
+ },
132
+ hintText: {
133
+ type: String,
134
+ default: '',
135
+ },
136
+ })
137
+
138
+ const emit = defineEmits(['close', 'btnClick'])
139
+
140
+ const CustomRegionEditor = {
141
+ name: 'CustomRegionEditor',
142
+ props: {
143
+ value: {
144
+ type: String,
145
+ default: '',
146
+ },
147
+ },
148
+ methods: {
149
+ handleInput(value) {
150
+ this.$emit('input', value)
151
+ },
152
+ },
153
+ render(h) {
154
+ return h('div', { class: 'custom-region-editor' }, [
155
+ h('div', { class: 'custom-region-editor__label' }, '模拟区域编辑器'),
156
+ h('el-input', {
157
+ props: {
158
+ type: 'textarea',
159
+ rows: 3,
160
+ value: this.value,
161
+ placeholder: '请输入区域 JSON 或坐标信息',
162
+ },
163
+ on: {
164
+ input: this.handleInput,
165
+ },
166
+ }),
167
+ ])
168
+ },
169
+ }
170
+
171
+ function createRows() {
172
+ return [
173
+ [
174
+ {
175
+ key: 'isEnable',
176
+ label: '是否启用',
177
+ value: false,
178
+ component: 'ElSwitch',
179
+ events: {},
180
+ params: {
181
+ activeText: '启用',
182
+ inactiveText: '禁用',
183
+ },
184
+ },
185
+ {
186
+ key: 'modelName',
187
+ label: '模型名称',
188
+ value: 'demo-detector',
189
+ component: 'ElInput',
190
+ params: {
191
+ clearable: true,
192
+ maxlength: 30,
193
+ rules: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
194
+ },
195
+ },
196
+ ],
197
+ [
198
+ {
199
+ key: 'confidence',
200
+ label: '置信度阈值',
201
+ value: '0.60',
202
+ component: 'ElInput',
203
+ params: {
204
+ clearable: true,
205
+ 'v-length.range': { min: 0, max: 1 },
206
+ rules: [{ required: true, message: '请输入置信度', trigger: 'blur' }],
207
+ },
208
+ },
209
+ {
210
+ key: 'iou',
211
+ label: 'IOU 阈值',
212
+ value: '0.45',
213
+ component: 'ElInput',
214
+ params: {
215
+ clearable: true,
216
+ 'v-length.range': { min: 0, max: 1 },
217
+ rules: [{ required: true, message: '请输入 IOU', trigger: 'blur' }],
218
+ },
219
+ },
220
+ ],
221
+ ]
222
+ }
223
+
224
+ function createRows2() {
225
+ return [
226
+ [
227
+ {
228
+ key: 'timeInterval',
229
+ label: '时间间隔(秒)',
230
+ value: '5',
231
+ component: 'ElInput',
232
+ params: {
233
+ clearable: true,
234
+ 'v-length.range': { min: 0, max: 6000, int: true },
235
+ rules: [{ required: true, message: '请输入时间间隔', trigger: 'blur' }],
236
+ },
237
+ },
238
+ {
239
+ key: 'stuck_threshold',
240
+ label: '所属工程',
241
+ value: ['component', 'form'],
242
+ component: 'ElCascader',
243
+ params: {
244
+ clearable: true,
245
+ props: {
246
+ checkStrictly: true,
247
+ emitPath: true,
248
+ },
249
+ options: [
250
+ {
251
+ value: 'guide',
252
+ label: 'Guide',
253
+ children: [
254
+ { value: 'disciplines', label: 'Disciplines' },
255
+ { value: 'navigation', label: 'Navigation' },
256
+ ],
257
+ },
258
+ {
259
+ value: 'component',
260
+ label: '组件库',
261
+ children: [
262
+ { value: 'form', label: 'Form' },
263
+ { value: 'table', label: 'Table' },
264
+ ],
265
+ },
266
+ ],
267
+ rules: [{ required: true, message: '请选择所属工程', trigger: 'change' }],
268
+ },
269
+ },
270
+ ],
271
+ [
272
+ {
273
+ key: 'maxRetries',
274
+ label: '最大重连次数',
275
+ value: '3',
276
+ component: 'ElInput',
277
+ params: {
278
+ clearable: true,
279
+ 'v-length.range': { min: 0, max: 20, int: true },
280
+ rules: [{ required: true, message: '请输入重连次数', trigger: 'blur' }],
281
+ },
282
+ },
283
+ {
284
+ key: 'streamUrl',
285
+ label: '视频流地址',
286
+ value: 'rtsp://example.com/live/001',
287
+ component: 'ElInput',
288
+ params: {
289
+ clearable: true,
290
+ rules: [{ required: true, message: '请输入视频流地址', trigger: 'blur' }],
291
+ },
292
+ },
293
+ ],
294
+ ]
295
+ }
296
+
297
+ function createRows3() {
298
+ return [
299
+ [
300
+ {
301
+ key: 'saveVideo',
302
+ label: '是否保存视频',
303
+ value: true,
304
+ component: 'ElRadioGroup',
305
+ params: {
306
+ options: [
307
+ { label: '是', value: true },
308
+ { label: '否', value: false },
309
+ ],
310
+ rules: [{ required: true, message: '请选择是否保存视频', trigger: 'change' }],
311
+ },
312
+ },
313
+ {
314
+ key: 'preBufferSecond',
315
+ label: '帧前缓存(秒)',
316
+ value: '10',
317
+ component: 'ElInput',
318
+ params: {
319
+ clearable: true,
320
+ 'v-length.range': { min: 0, max: 120, int: true },
321
+ rules: [{ required: true, message: '请输入缓存时长', trigger: 'blur' }],
322
+ },
323
+ },
324
+ ],
325
+ [
326
+ {
327
+ key: 'det_area_mode',
328
+ label: '检测区域模式',
329
+ value: 'normal',
330
+ component: 'ElRadioGroup',
331
+ events: {},
332
+ params: {
333
+ options: [
334
+ { label: '常规检测', value: 'normal' },
335
+ { label: '非常规检测', value: 'abnormal' },
336
+ ],
337
+ rules: [{ required: true, message: '请选择区域模式', trigger: 'change' }],
338
+ },
339
+ },
340
+ ],
341
+ ]
342
+ }
343
+
344
+ function createRows4() {
345
+ return [
346
+ [
347
+ {
348
+ key: 'region',
349
+ label: '地区选择',
350
+ value: ['beijing', 'chaoyang'],
351
+ component: 'ElCascader',
352
+ params: {
353
+ props: {
354
+ multiple: false,
355
+ checkStrictly: true,
356
+ emitPath: true,
357
+ },
358
+ showAllLevels: false,
359
+ options: [
360
+ {
361
+ value: 'beijing',
362
+ label: '北京市',
363
+ children: [
364
+ { value: 'chaoyang', label: '朝阳区' },
365
+ { value: 'haidian', label: '海淀区' },
366
+ ],
367
+ },
368
+ {
369
+ value: 'shanghai',
370
+ label: '上海市',
371
+ children: [{ value: 'pudong', label: '浦东新区' }],
372
+ },
373
+ ],
374
+ },
375
+ },
376
+ {
377
+ key: 'department',
378
+ label: '部门选择',
379
+ value: ['company', 'tech', 'frontend'],
380
+ component: 'ElCascader',
381
+ params: {
382
+ props: {
383
+ value: 'code',
384
+ label: 'name',
385
+ children: 'children',
386
+ checkStrictly: true,
387
+ emitPath: true,
388
+ },
389
+ separator: ' / ',
390
+ options: [
391
+ {
392
+ code: 'company',
393
+ name: '公司总部',
394
+ children: [
395
+ {
396
+ code: 'tech',
397
+ name: '技术部',
398
+ children: [
399
+ { code: 'frontend', name: '前端组' },
400
+ { code: 'backend', name: '后端组' },
401
+ ],
402
+ },
403
+ { code: 'sales', name: '销售部' },
404
+ ],
405
+ },
406
+ ],
407
+ },
408
+ },
409
+ ],
410
+ [
411
+ {
412
+ key: 'singleLevelCascader',
413
+ label: '单层级联',
414
+ value: 'shanghai',
415
+ component: 'ElCascader',
416
+ params: {
417
+ options: [
418
+ { value: 'beijing', label: '北京市' },
419
+ { value: 'shanghai', label: '上海市' },
420
+ { value: 'guangzhou', label: '广州市' },
421
+ ],
422
+ },
423
+ },
424
+ {
425
+ key: 'notifyEmails',
426
+ label: '通知邮箱',
427
+ value: 'demo@example.com',
428
+ component: 'ElInput',
429
+ params: {
430
+ clearable: true,
431
+ },
432
+ },
433
+ ],
434
+ ]
435
+ }
436
+
437
+ function createRowsUpload() {
438
+ return [
439
+ [
440
+ {
441
+ key: 'upload_file',
442
+ label: '上传模型文件',
443
+ value: [],
444
+ component: 'ElUpload',
445
+ events: {},
446
+ params: {
447
+ drag: true,
448
+ multiple: true,
449
+ action: '#',
450
+ limit: 2,
451
+ fileList: [],
452
+ accept: '.txt,.md,.json,.jpg,.png,.pdf',
453
+ autoUpload: true,
454
+ httpRequest: null,
455
+ rules: [{ required: true, message: '请上传至少一个文件', trigger: 'change' }],
456
+ },
457
+ slots: {},
458
+ },
459
+ ],
460
+ ]
461
+ }
462
+
463
+ function createDetailData() {
464
+ return {
465
+ isEnable: true,
466
+ modelName: 'helmet-detector-v2',
467
+ confidence: '0.72',
468
+ iou: '0.33',
469
+ timeInterval: '3',
470
+ stuck_threshold: ['component', 'table'],
471
+ maxRetries: '5',
472
+ streamUrl: 'rtsp://example.com/live/demo',
473
+ saveVideo: true,
474
+ preBufferSecond: '8',
475
+ det_area_mode: 'abnormal',
476
+ det_area_json: '{"x":10,"y":12,"width":320,"height":180}',
477
+ region: ['shanghai', 'pudong'],
478
+ department: ['company', 'tech', 'backend'],
479
+ singleLevelCascader: 'guangzhou',
480
+ notifyEmails: 'ops@example.com',
481
+ upload_file: [
482
+ {
483
+ name: '示例说明.md',
484
+ url: 'https://cdn.jsdelivr.net/gh/vuejs/art@master/logo.png',
485
+ fileName: '示例说明.md',
486
+ filePath: 'https://cdn.jsdelivr.net/gh/vuejs/art@master/logo.png',
487
+ fileSize: 2048,
488
+ },
489
+ ],
490
+ }
491
+ }
492
+
493
+ const demoReadOnly = ref(props.readOnly)
494
+ const outputText = ref('')
495
+ const uploadFileList = ref([])
496
+ const formState = reactive({
497
+ rows: createRows(),
498
+ rows2: createRows2(),
499
+ rows3: createRows3(),
500
+ rows4: createRows4(),
501
+ rowsUpload: createRowsUpload(),
502
+ })
503
+
504
+ const shellForm = ref(null)
505
+ const row1Ref = ref(null)
506
+ const row2Ref = ref(null)
507
+ const row3Ref = ref(null)
508
+ const row4Ref = ref(null)
509
+ const rowUploadRef = ref(null)
510
+
511
+ const { proxy } = getCurrentInstance()
512
+
513
+ const getUploadField = () => formState.rowsUpload[0][0]
514
+ const getFormRefs = () => [row1Ref.value, row2Ref.value, row3Ref.value, row4Ref.value, rowUploadRef.value].filter(Boolean)
515
+
516
+ const syncUploadDisabled = () => {
517
+ const uploadField = getUploadField()
518
+ if (uploadField && uploadField.params) {
519
+ proxy?.$set(uploadField.params, 'disabled', demoReadOnly.value)
520
+ }
521
+ }
522
+
523
+ watch(
524
+ () => props.readOnly,
525
+ (value) => {
526
+ demoReadOnly.value = value
527
+ syncUploadDisabled()
528
+ },
529
+ { immediate: true },
530
+ )
531
+
532
+ watch(demoReadOnly, () => {
533
+ syncUploadDisabled()
534
+ })
535
+
536
+ const bindFieldEvents = () => {
537
+ formState.rows[0][0].events.change = handleEnableChange
538
+ formState.rows3[1][0].events.change = detAreaModeChange
539
+
540
+ const uploadField = getUploadField()
541
+ uploadField.params.httpRequest = mockUploadRequest
542
+ uploadField.events = {
543
+ success: handleUploadSuccess,
544
+ change: handleUploadChange,
545
+ remove: handleUploadRemove,
546
+ }
547
+ syncUploadDisabled()
548
+ }
549
+
550
+ const setUploadSlots = () => {
551
+ const uploadField = getUploadField()
552
+ proxy?.$set(uploadField, 'slots', {
553
+ default: () =>
554
+ proxy?.$createElement('div', { class: 'upload-trigger' }, [
555
+ proxy.$createElement('i', { class: 'el-icon-upload upload-trigger__icon' }),
556
+ proxy.$createElement('div', { class: 'upload-trigger__title' }, '点击或拖拽文件到此处上传'),
557
+ proxy.$createElement('div', { class: 'upload-trigger__sub' }, '仅做本地模拟,不会真正请求后台'),
558
+ ]),
559
+ tip: () =>
560
+ proxy?.$createElement('div', { class: 'el-upload__tip' }, '支持 txt / md / json / jpg / png / pdf,最多 2 个文件'),
561
+ })
562
+ }
563
+
564
+ const toggleReadOnly = () => {
565
+ demoReadOnly.value = !demoReadOnly.value
566
+ }
567
+
568
+ const showToast = (message) => {
569
+ proxy?.$message?.info(message || '来自 FormDemo 的实例方法调用')
570
+ return true
571
+ }
572
+
573
+ const handleEnableChange = (value) => {
574
+ proxy?.$message?.info(value ? '已启用模型能力' : '已停用模型能力')
575
+ }
576
+
577
+ const ensureAbnormalField = (mode) => {
578
+ const lastRow = formState.rows3[formState.rows3.length - 1]
579
+ const lastKey = lastRow && lastRow[0] && lastRow[0].key
580
+ if (lastKey === 'det_area_json') {
581
+ formState.rows3.pop()
582
+ }
583
+ if (mode === 'abnormal') {
584
+ formState.rows3.push([
585
+ {
586
+ key: 'det_area_json',
587
+ label: '感兴趣区域',
588
+ value: '',
589
+ component: CustomRegionEditor,
590
+ span: 24,
591
+ params: {
592
+ rules: [{ required: true, message: '请输入感兴趣区域', trigger: 'blur' }],
593
+ },
594
+ },
595
+ ])
596
+ }
597
+ }
598
+
599
+ const detAreaModeChange = (value) => {
600
+ ensureAbnormalField(value)
601
+ }
602
+
603
+ const validateShellForm = () =>
604
+ new Promise((resolve, reject) => {
605
+ if (!shellForm.value) {
606
+ reject(new Error('表单未就绪'))
607
+ return
608
+ }
609
+ shellForm.value.validate((valid) => {
610
+ if (valid) {
611
+ resolve(true)
612
+ return
613
+ }
614
+ reject(new Error('表单校验失败'))
615
+ })
616
+ })
617
+
618
+ const collectFormData = () =>
619
+ getFormRefs().reduce((result, refInstance) => {
620
+ if (refInstance && typeof refInstance.getFormKvData === 'function') {
621
+ return Object.assign(result, refInstance.getFormKvData())
622
+ }
623
+ return result
624
+ }, {})
625
+
626
+ const getFormData = async () => {
627
+ try {
628
+ await validateShellForm()
629
+ const data = collectFormData()
630
+ outputText.value = JSON.stringify(data, null, 2)
631
+ proxy?.$message?.success('表单校验成功')
632
+ return data
633
+ } catch (error) {
634
+ outputText.value = ''
635
+ proxy?.$message?.error('表单校验失败,请先完善必填项')
636
+ return false
637
+ }
638
+ }
639
+
640
+ const resetFormData = (showMessage = true) => {
641
+ getFormRefs().forEach((refInstance) => {
642
+ if (refInstance && typeof refInstance.resetForm === 'function') {
643
+ refInstance.resetForm()
644
+ }
645
+ })
646
+ ensureAbnormalField('normal')
647
+ uploadFileList.value = []
648
+ const uploadField = getUploadField()
649
+ proxy?.$set(uploadField.params, 'fileList', [])
650
+ proxy?.$set(uploadField, 'value', [])
651
+ proxy?.$set(uploadField, 'delValue', [])
652
+ nextTick(() => {
653
+ shellForm.value?.clearValidate()
654
+ outputText.value = ''
655
+ if (showMessage) {
656
+ proxy?.$message?.success('表单已重置')
657
+ }
658
+ })
659
+ }
660
+
661
+ const loadDetailData = (showMessage = true) => {
662
+ if (showMessage) {
663
+ proxy?.$message?.info('开始模拟详情回填')
664
+ }
665
+ setTimeout(() => {
666
+ const detail = createDetailData()
667
+ resetFormData(false)
668
+ ensureAbnormalField(detail.det_area_mode)
669
+ nextTick(() => {
670
+ getFormRefs().forEach((refInstance) => {
671
+ if (refInstance && typeof refInstance.setFormData === 'function') {
672
+ refInstance.setFormData(detail)
673
+ }
674
+ })
675
+ uploadFileList.value = detail.upload_file.slice()
676
+ proxy?.$set(getUploadField().params, 'fileList', detail.upload_file.slice())
677
+ if (showMessage) {
678
+ proxy?.$message?.success('详情已回填')
679
+ }
680
+ })
681
+ }, 300)
682
+ }
683
+
684
+ const notifyInnerButton = () => {
685
+ emit('btnClick', collectFormData())
686
+ proxy?.$message?.success('已触发自定义事件')
687
+ }
688
+
689
+ const normalizeUploadList = (fileList) =>
690
+ (fileList || []).map((item) => {
691
+ const responseData = item && item.response && item.response.data ? item.response.data : item
692
+ const fileName = responseData.fileName || item.name || '未命名文件'
693
+ const filePath = responseData.filePath || item.url || ''
694
+ const fileSize = responseData.fileSize || item.size || (item.raw && item.raw.size) || 0
695
+ return {
696
+ name: fileName,
697
+ url: filePath,
698
+ fileName,
699
+ filePath,
700
+ fileSize,
701
+ }
702
+ })
703
+
704
+ const syncUploadFieldValue = (fileList, removedFile) => {
705
+ const uploadField = getUploadField()
706
+ const normalizedList = normalizeUploadList(fileList)
707
+ uploadFileList.value = (fileList || []).slice()
708
+ proxy?.$set(uploadField.params, 'fileList', (fileList || []).slice())
709
+ proxy?.$set(uploadField, 'value', normalizedList)
710
+ if (removedFile) {
711
+ const removed = normalizeUploadList([removedFile])[0]
712
+ const delValue = Array.isArray(uploadField.delValue) ? uploadField.delValue.slice() : []
713
+ delValue.push(Object.assign({}, removed, { isDelete: 1 }))
714
+ proxy?.$set(uploadField, 'delValue', delValue)
715
+ }
716
+ nextTick(() => {
717
+ shellForm.value?.validateField('rowsUpload.0.0.value', function () {})
718
+ })
719
+ }
720
+
721
+ const mockUploadRequest = (options) => {
722
+ const file = options.file
723
+ const timer = setTimeout(() => {
724
+ const filePath = URL.createObjectURL(file)
725
+ const response = {
726
+ code: 0,
727
+ data: {
728
+ fileName: file.name,
729
+ filePath,
730
+ fileSize: file.size || 0,
731
+ },
732
+ }
733
+ if (typeof options.onSuccess === 'function') {
734
+ options.onSuccess(response, file)
735
+ }
736
+ }, 400)
737
+
738
+ return {
739
+ abort() {
740
+ clearTimeout(timer)
741
+ if (typeof options.onError === 'function') {
742
+ options.onError(new Error('上传已取消'))
743
+ }
744
+ },
745
+ }
746
+ }
747
+
748
+ const handleUploadSuccess = (_response, _file, fileList) => {
749
+ syncUploadFieldValue(fileList)
750
+ proxy?.$message?.success('文件上传成功')
751
+ }
752
+
753
+ const handleUploadChange = (_file, fileList) => {
754
+ syncUploadFieldValue(fileList)
755
+ }
756
+
757
+ const handleUploadRemove = (file, fileList) => {
758
+ syncUploadFieldValue(fileList, file)
759
+ proxy?.$message?.warning('已移除文件')
760
+ }
761
+
762
+ onMounted(() => {
763
+ bindFieldEvents()
764
+ setUploadSlots()
765
+ loadDetailData(false)
766
+ })
767
+
768
+ defineExpose({
769
+ getFormData,
770
+ resetFormData,
771
+ loadDetailData,
772
+ showToast,
773
+ })
774
+ </script>
775
+
776
+ <style scoped>
777
+ .demo-page {
778
+ display: flex;
779
+ flex-direction: column;
780
+ gap: 20px;
781
+ }
782
+
783
+ .demo-card,
784
+ .result-card {
785
+ border-radius: 12px;
786
+ }
787
+
788
+ .demo-card__header,
789
+ .result-card__header {
790
+ display: flex;
791
+ align-items: center;
792
+ justify-content: space-between;
793
+ }
794
+
795
+ .demo-card__title {
796
+ font-size: 18px;
797
+ font-weight: 700;
798
+ color: #303133;
799
+ }
800
+
801
+ .demo-card__desc {
802
+ margin-top: 6px;
803
+ font-size: 13px;
804
+ color: #909399;
805
+ }
806
+
807
+ .demo-alert {
808
+ margin-bottom: 16px;
809
+ }
810
+
811
+ .toolbar {
812
+ display: flex;
813
+ flex-wrap: wrap;
814
+ gap: 12px;
815
+ margin-bottom: 20px;
816
+ }
817
+
818
+ .shell-form {
819
+ display: flex;
820
+ flex-direction: column;
821
+ gap: 16px;
822
+ }
823
+
824
+ .result-content {
825
+ margin: 0;
826
+ padding: 16px;
827
+ min-height: 180px;
828
+ max-height: 360px;
829
+ overflow: auto;
830
+ white-space: pre-wrap;
831
+ word-break: break-all;
832
+ background: #0f172a;
833
+ border-radius: 10px;
834
+ color: #e2e8f0;
835
+ font-size: 13px;
836
+ line-height: 1.6;
837
+ }
838
+
839
+ .custom-region-editor {
840
+ border: 1px dashed #dcdfe6;
841
+ border-radius: 8px;
842
+ padding: 12px;
843
+ background: #fafafa;
844
+ }
845
+
846
+ .custom-region-editor__label {
847
+ margin-bottom: 8px;
848
+ font-size: 12px;
849
+ font-weight: 600;
850
+ color: #409eff;
851
+ }
852
+
853
+ .upload-trigger {
854
+ padding: 12px;
855
+ text-align: center;
856
+ color: #606266;
857
+ }
858
+
859
+ .upload-trigger__icon {
860
+ font-size: 28px;
861
+ color: #409eff;
862
+ }
863
+
864
+ .upload-trigger__title {
865
+ margin-top: 8px;
866
+ font-size: 14px;
867
+ font-weight: 600;
868
+ }
869
+
870
+ .upload-trigger__sub {
871
+ margin-top: 6px;
872
+ font-size: 12px;
873
+ color: #909399;
874
+ }
875
+
876
+ @media (max-width: 960px) {
877
+ .toolbar {
878
+ flex-direction: column;
879
+ align-items: stretch;
880
+ }
881
+ }
882
+ </style>