@neatui/vue 2.6.2 → 2.7.0
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<div :class="`mc-img
|
|
3
|
+
<div :class="`mc-img ${imgClass}`" :style="imgStyle">
|
|
4
4
|
<img :src="src[srcName]" :style="`object-fit: ${fit}`" @click.stop="showPreview" />
|
|
5
5
|
<div v-if="tools" class="mc-img-tools">
|
|
6
6
|
<slot name="tools">
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
const props: any = defineProps({
|
|
21
21
|
tools: { type: Boolean, default: true },
|
|
22
|
-
srcName: { type: String, default: '
|
|
22
|
+
srcName: { type: String, default: 'url' },
|
|
23
23
|
theme: { type: String, default: '@a' },
|
|
24
24
|
co: { type: String, default: 'note' },
|
|
25
25
|
imgClass: { type: String, default: '' },
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}"
|
|
13
13
|
:navigation="true"
|
|
14
14
|
:modules="[Keyboard, Pagination, Navigation]"
|
|
15
|
-
class="
|
|
15
|
+
class="full"
|
|
16
16
|
>
|
|
17
17
|
<SwiperSlide v-for="item in files" :key="item.id" class="ac" ui-flex="col cm">
|
|
18
18
|
<img :src="item[srcName]" class="vc" />
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
const props: any = defineProps({
|
|
33
33
|
srcName: {
|
|
34
34
|
type: String,
|
|
35
|
-
default: '
|
|
35
|
+
default: 'url'
|
|
36
36
|
},
|
|
37
37
|
id: {
|
|
38
38
|
type: String,
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
2
|
+
<div ui-flex="row lm :wrap" class="neatui-upload mb-sm-sub mr-sm-sub" v-bind="{ ...attrs }">
|
|
3
|
+
<div v-for="(file, idx) in state.files" :key="idx" ui-flex="col cm" class="flex-fixed bg-fore n-sl w-ms h-ms b-solid bk-line b-xs r-sm pr">
|
|
4
|
+
<!-- 删除按钮 -->
|
|
5
|
+
<div class="pa ot-ss or-ss bg-fore co-risk lh-xs r-xl parent-hover:show" @click="remove(idx)">
|
|
6
|
+
<svg class="block" viewBox="0 0 1024 1024" style="width: 1em; height: 1em">
|
|
7
|
+
<path
|
|
8
|
+
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66 0.3L512 563.4l-99.3 118.4-66.1-0.3c-4.4 0-8-3.5-8-8 0-1.9 0.7-3.7 1.9-5.2l130.1-155L340.5 359c-1.2-1.5-1.9-3.3-1.9-5.2 0-4.4 3.6-8 8-8l66.1 0.3L512 464.6l99.3-118.4 66-0.3c4.4 0 8 3.5 8 8 0 1.9-0.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
|
|
9
|
+
fill="currentColor"
|
|
10
|
+
/>
|
|
11
|
+
</svg>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- 文件预览 -->
|
|
15
|
+
<img v-if="file.type === 'image'" :src="getImageUrl(file)" alt="" />
|
|
5
16
|
<template v-else-if="file.type === 'pdf'">
|
|
6
17
|
<svg t="1705645350800" class="w-ss h-ss" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1913">
|
|
7
18
|
<path
|
|
@@ -66,7 +77,7 @@
|
|
|
66
77
|
p-id="2068"
|
|
67
78
|
></path>
|
|
68
79
|
<path
|
|
69
|
-
d="M242.569116 353.23904l89.224866 84.502478c4.337764 4.107988 4.523582 10.954302 0.415595 15.291067-4.107988 4.337764-10.954302 4.523582-15.
|
|
80
|
+
d="M242.569116 353.23904l89.224866 84.502478c4.337764 4.107988 4.523582 10.954302 0.415595 15.291067-4.107988 4.337764-10.954302 4.523582-15.291067 0.415595l-89.224866-84.502479-84.502478 89.224867c-4.107988 4.337764-10.954302 4.523582-15.291067 0.415594-4.337764-4.107988-4.523582-10.954302-0.415595-15.291067l84.502478-89.224867-89.224866-84.502478c-4.337764-4.107988-4.523582-10.954302-0.415594-15.291067 4.107988-4.337764 10.954302-4.523582 15.291067-0.415594l89.224867 84.502478 84.502478-89.224867c4.107988-4.337764 10.954302-4.523582 15.291067-0.415594 4.337764 4.107988 4.523582 10.954302 0.415594 15.291068l-84.502478 89.224866z"
|
|
70
81
|
fill="#FFFFFF"
|
|
71
82
|
p-id="2069"
|
|
72
83
|
></path>
|
|
@@ -135,15 +146,28 @@
|
|
|
135
146
|
></path>
|
|
136
147
|
</svg>
|
|
137
148
|
</template>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
|
|
150
|
+
<!-- 上传状态显示 -->
|
|
151
|
+
<div v-if="file.status === 'uploading'" class="upload-status">
|
|
152
|
+
<div class="loading-spinner"></div>
|
|
153
|
+
<span class="status-text">上传中...</span>
|
|
154
|
+
</div>
|
|
155
|
+
<div v-else-if="file.status === 'error'" class="upload-status error">
|
|
156
|
+
<span class="status-text">上传失败</span>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- 文件名 -->
|
|
160
|
+
<!-- <p ui-omit="1" class="fs-ss ac">{{ file.name }}</p> -->
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- 上传按钮 -->
|
|
164
|
+
<div v-if="showUploadButton">
|
|
141
165
|
<slot>
|
|
142
|
-
<label ui-flex="row cm" ui-form="@a type:upload tips:hover" :class="`upload w-ms h-ms n-sl bg-fore b-solid bk-line b-xs ${uploadClass}`" :style="uploadStyle">
|
|
166
|
+
<label ui-flex="row cm" ui-form="@a type:upload tips:hover" :class="`upload w-ms h-ms n-sl bg-fore b-solid bk-line b-xs r-sm ${uploadClass}`" :style="uploadStyle">
|
|
143
167
|
<div v-if="tips" ui-form-tips>
|
|
144
168
|
{{ tips }}
|
|
145
169
|
</div>
|
|
146
|
-
<input style="display: none" type="file" class="pa input" :multiple="
|
|
170
|
+
<input style="display: none" type="file" class="pa input" :multiple="!max || max > 1" :accept="accept" @change="change" />
|
|
147
171
|
<div class="co-note ac input-icon">
|
|
148
172
|
<svg t="1705642180833" style="width: 1em; height: 1em" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4215">
|
|
149
173
|
<path
|
|
@@ -154,12 +178,12 @@
|
|
|
154
178
|
</div>
|
|
155
179
|
</label>
|
|
156
180
|
</slot>
|
|
157
|
-
</
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
</div>
|
|
182
|
+
<Layer :id="layerId" am="as"></Layer>
|
|
183
|
+
</div>
|
|
160
184
|
</template>
|
|
161
185
|
<script setup lang="ts">
|
|
162
|
-
import { reactive, ref, watch } from 'vue';
|
|
186
|
+
import { reactive, ref, watch, onUnmounted, computed } from 'vue';
|
|
163
187
|
import { Image } from '../display';
|
|
164
188
|
import draggable from 'vuedraggable';
|
|
165
189
|
import Compressor from 'compressorjs';
|
|
@@ -170,176 +194,398 @@
|
|
|
170
194
|
|
|
171
195
|
interface Props {
|
|
172
196
|
modelValue?: string | any[];
|
|
173
|
-
//
|
|
174
|
-
|
|
197
|
+
// 文件数量限制
|
|
198
|
+
max?: number;
|
|
175
199
|
attrs?: object;
|
|
176
200
|
uploadClass?: string;
|
|
177
201
|
uploadStyle?: string;
|
|
178
202
|
id?: string;
|
|
179
203
|
tips?: string; // 提示文案
|
|
180
204
|
// 文件类型
|
|
181
|
-
accept?: string;
|
|
182
|
-
// 是否可以上传多个文件
|
|
183
|
-
multiple?: boolean;
|
|
205
|
+
accept?: string | 'image/*' | 'video/*' | 'audio/*';
|
|
184
206
|
// 是否需要抠图处理
|
|
185
207
|
crop?: boolean;
|
|
186
|
-
// 上传路径
|
|
187
|
-
action?: string;
|
|
188
208
|
// 请求上传接口
|
|
189
209
|
upload?: (formData: FormData) => Promise<any>;
|
|
210
|
+
// 文件大小限制(MB)
|
|
211
|
+
maxSize?: number;
|
|
212
|
+
// 是否启用图片压缩
|
|
213
|
+
compress?: boolean;
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
const props = withDefaults(defineProps<Props>(), {
|
|
217
|
+
modelValue: '',
|
|
193
218
|
uploadClass: '',
|
|
194
219
|
uploadStyle: '',
|
|
195
220
|
id: '',
|
|
196
221
|
attrs: () => ({}),
|
|
197
222
|
tips: '',
|
|
198
|
-
|
|
199
|
-
accept: 'image/*',
|
|
200
|
-
multiple: false,
|
|
223
|
+
accept: '*',
|
|
201
224
|
crop: false,
|
|
202
|
-
|
|
203
|
-
|
|
225
|
+
upload: async () => null,
|
|
226
|
+
maxSize: 10, // 默认10MB
|
|
227
|
+
compress: true,
|
|
228
|
+
max: 0
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
interface UploadFile {
|
|
232
|
+
name: string;
|
|
233
|
+
type: string;
|
|
234
|
+
path: string;
|
|
235
|
+
status: 'uploading' | 'success' | 'error';
|
|
236
|
+
size?: number;
|
|
237
|
+
progress?: number;
|
|
238
|
+
result?: any; // 直接保存上传函数的返回值
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const state = reactive<{
|
|
242
|
+
files: UploadFile[];
|
|
243
|
+
isInternalUpdate: boolean; // 标记是否为内部更新
|
|
244
|
+
}>({
|
|
245
|
+
files: [],
|
|
246
|
+
isInternalUpdate: false
|
|
204
247
|
});
|
|
205
248
|
|
|
206
|
-
const
|
|
207
|
-
|
|
249
|
+
const emit = defineEmits(['update:modelValue', 'change', 'error']);
|
|
250
|
+
|
|
251
|
+
// 获取图片URL
|
|
252
|
+
const getImageUrl = (file: UploadFile): string => {
|
|
253
|
+
if (file.status === 'success' && file.result) {
|
|
254
|
+
// 如果上传成功,优先使用上传返回的URL
|
|
255
|
+
if (typeof file.result === 'string') {
|
|
256
|
+
return file.result;
|
|
257
|
+
}
|
|
258
|
+
// 如果是对象,尝试获取url或path
|
|
259
|
+
return file.result.url || file.result.path || file.path;
|
|
260
|
+
}
|
|
261
|
+
// 否则使用预览URL
|
|
262
|
+
return file.path;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// 计算属性
|
|
266
|
+
const showUploadButton = computed(() => {
|
|
267
|
+
const currentCount = state.files.length;
|
|
268
|
+
|
|
269
|
+
if (!props.max) {
|
|
270
|
+
return true; // 无限制
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return currentCount < props.max;
|
|
208
274
|
});
|
|
209
275
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
type
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
type = 'image';
|
|
276
|
+
// 获取文件类型
|
|
277
|
+
const getFileType = (file: File): string => {
|
|
278
|
+
if (file.type === 'application/msword' || file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
|
279
|
+
return 'docx';
|
|
280
|
+
} else if (file.type === 'application/vnd.ms-excel' || file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
|
|
281
|
+
return 'xlsx';
|
|
282
|
+
} else if (file.type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
|
|
283
|
+
return 'pptx';
|
|
284
|
+
} else if (file.type === 'application/pdf') {
|
|
285
|
+
return 'pdf';
|
|
286
|
+
} else if (file.type === 'text/html') {
|
|
287
|
+
return 'html';
|
|
288
|
+
} else if (file.type.startsWith('image/') && file.type !== 'image/vnd.adobe.photoshop') {
|
|
289
|
+
return 'image';
|
|
290
|
+
}
|
|
291
|
+
return 'file';
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// 验证文件
|
|
295
|
+
const validateFile = (file: File): string | null => {
|
|
296
|
+
// 检查文件大小
|
|
297
|
+
if (props.maxSize && file.size > props.maxSize * 1024 * 1024) {
|
|
298
|
+
return `文件大小不能超过 ${props.maxSize}MB`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 检查文件类型
|
|
302
|
+
if (props.accept && props.accept !== '*') {
|
|
303
|
+
const acceptTypes = props.accept.split(',').map((type) => type.trim());
|
|
304
|
+
const isValidType = acceptTypes.some((type) => {
|
|
305
|
+
if (type.endsWith('/*')) {
|
|
306
|
+
return file.type.startsWith(type.replace('/*', ''));
|
|
307
|
+
}
|
|
308
|
+
return file.type === type;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (!isValidType) {
|
|
312
|
+
return `不支持的文件类型: ${file.type}`;
|
|
248
313
|
}
|
|
249
|
-
const file: any = { name: _file.name, type, path: URL.createObjectURL(_file), url: await props.upload(_file) };
|
|
250
|
-
// 上传文件
|
|
251
|
-
state.files.push(file);
|
|
252
|
-
|
|
253
|
-
// if (isFunction(props.upload)) {
|
|
254
|
-
// const src: any = await props.upload(_file);
|
|
255
|
-
// if (src) {
|
|
256
|
-
// file.src = src;
|
|
257
|
-
// }
|
|
258
|
-
// }
|
|
259
314
|
}
|
|
260
315
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// const { name = '' }: any = _blob || {};
|
|
283
|
-
// file = new File([_blob], name);
|
|
284
|
-
// }
|
|
285
|
-
// const formData = new FormData();
|
|
286
|
-
// formData.append('file', file);
|
|
287
|
-
// if (isFunction(props.upload)) {
|
|
288
|
-
// const url: any = await props.upload(formData);
|
|
289
|
-
// console.log(126, url);
|
|
290
|
-
// if (url) {
|
|
291
|
-
// files.value = [...files.value, { url }];
|
|
292
|
-
// }
|
|
293
|
-
// }
|
|
294
|
-
// }
|
|
316
|
+
return null;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// 压缩图片
|
|
320
|
+
const compressImage = async (file: File): Promise<File> => {
|
|
321
|
+
if (!props.compress || !file.type.startsWith('image/')) {
|
|
322
|
+
return file;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
new Compressor(file, {
|
|
327
|
+
quality: 0.8,
|
|
328
|
+
maxWidth: 1920,
|
|
329
|
+
maxHeight: 1080,
|
|
330
|
+
checkOrientation: true,
|
|
331
|
+
success: (result) => {
|
|
332
|
+
resolve(new File([result], file.name, { type: result.type }));
|
|
333
|
+
},
|
|
334
|
+
error: reject
|
|
335
|
+
});
|
|
336
|
+
});
|
|
295
337
|
};
|
|
296
338
|
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
339
|
+
// 删除文件函数
|
|
340
|
+
const remove = (index: number) => {
|
|
341
|
+
// 释放对象URL,防止内存泄漏
|
|
342
|
+
if (state.files[index] && state.files[index].path) {
|
|
343
|
+
URL.revokeObjectURL(state.files[index].path);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 从数组中移除文件
|
|
347
|
+
state.files.splice(index, 1);
|
|
348
|
+
|
|
349
|
+
// 发射更新事件
|
|
350
|
+
emitUpdateEvents();
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// 发射更新事件
|
|
354
|
+
const emitUpdateEvents = () => {
|
|
355
|
+
// 设置内部更新标志
|
|
356
|
+
state.isInternalUpdate = true;
|
|
357
|
+
|
|
358
|
+
const successFiles = state.files.filter((file) => file.status === 'success');
|
|
359
|
+
|
|
360
|
+
// 如果没有成功文件,返回空值
|
|
361
|
+
if (successFiles.length === 0) {
|
|
362
|
+
const emptyValue = props.max === 1 ? '' : [];
|
|
363
|
+
emit('update:modelValue', emptyValue);
|
|
364
|
+
emit('change', emptyValue);
|
|
365
|
+
} else {
|
|
366
|
+
// 直接使用上传函数的返回值
|
|
367
|
+
const filesForModel = successFiles.map((file) => file.result);
|
|
368
|
+
const modelValue = props.max === 1 ? filesForModel[0] || '' : filesForModel;
|
|
369
|
+
|
|
370
|
+
emit('update:modelValue', modelValue);
|
|
371
|
+
emit('change', modelValue);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 重置标志
|
|
375
|
+
setTimeout(() => {
|
|
376
|
+
state.isInternalUpdate = false;
|
|
377
|
+
}, 0);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// 文件上传处理
|
|
381
|
+
const change = (e: Event) => {
|
|
382
|
+
const target = e.target as HTMLInputElement;
|
|
383
|
+
const selectedFiles = target.files || [];
|
|
384
|
+
|
|
385
|
+
// 检查文件数量限制
|
|
386
|
+
if (props.max && state.files.length + selectedFiles.length > props.max) {
|
|
387
|
+
const errorMsg = `文件数量超过限制: ${props.max}`;
|
|
388
|
+
console.warn(errorMsg);
|
|
389
|
+
emit('error', errorMsg);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 步骤1:立即添加所有文件到UI,显示占位图
|
|
394
|
+
const validFiles: { file: File; uploadFile: UploadFile }[] = [];
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < selectedFiles.length; i++) {
|
|
397
|
+
const file = selectedFiles[i];
|
|
398
|
+
|
|
399
|
+
// 验证文件
|
|
400
|
+
const validationError = validateFile(file);
|
|
401
|
+
if (validationError) {
|
|
402
|
+
emit('error', validationError);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 创建预览URL
|
|
407
|
+
const previewUrl = URL.createObjectURL(file);
|
|
408
|
+
|
|
409
|
+
// 添加到文件列表
|
|
410
|
+
const uploadFile: UploadFile = {
|
|
411
|
+
name: file.name,
|
|
412
|
+
type: getFileType(file),
|
|
413
|
+
path: previewUrl,
|
|
414
|
+
status: 'uploading',
|
|
415
|
+
size: file.size
|
|
416
|
+
};
|
|
417
|
+
state.files.push(uploadFile);
|
|
418
|
+
validFiles.push({ file, uploadFile });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 步骤2:异步上传所有文件
|
|
422
|
+
validFiles.forEach(({ file, uploadFile }) => {
|
|
423
|
+
uploadSingleFile(file, uploadFile);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// 清空input值,允许重复选择同一文件
|
|
427
|
+
target.value = '';
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// 单个文件上传函数
|
|
431
|
+
const uploadSingleFile = async (file: File, uploadFile: UploadFile) => {
|
|
432
|
+
try {
|
|
433
|
+
// 压缩图片(如果需要)
|
|
434
|
+
let processedFile = file;
|
|
435
|
+
if (props.compress && file.type.startsWith('image/')) {
|
|
436
|
+
try {
|
|
437
|
+
processedFile = await compressImage(file);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.warn('图片压缩失败,使用原文件:', error);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 上传到服务器
|
|
444
|
+
if (typeof props.upload !== 'function') {
|
|
445
|
+
throw new Error('upload 函数未定义');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const formData = new FormData();
|
|
449
|
+
formData.append('file', processedFile);
|
|
450
|
+
const result = await props.upload(formData);
|
|
451
|
+
|
|
452
|
+
// 更新文件状态
|
|
453
|
+
const fileIndex = state.files.findIndex((f) => f.path === uploadFile.path);
|
|
454
|
+
if (fileIndex !== -1) {
|
|
455
|
+
if (result) {
|
|
456
|
+
// 直接保存上传函数的返回值
|
|
457
|
+
state.files[fileIndex].result = result;
|
|
458
|
+
state.files[fileIndex].status = 'success';
|
|
459
|
+
} else {
|
|
460
|
+
state.files[fileIndex].status = 'error';
|
|
461
|
+
}
|
|
462
|
+
// 每个文件完成后更新外部数据源
|
|
463
|
+
emitUpdateEvents();
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error('文件处理失败:', error);
|
|
467
|
+
|
|
468
|
+
// 更新失败文件状态
|
|
469
|
+
const fileIndex = state.files.findIndex((f) => f.path === uploadFile.path);
|
|
470
|
+
if (fileIndex !== -1) {
|
|
471
|
+
state.files[fileIndex].status = 'error';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
emit('error', `文件 ${file.name} 上传失败: ${error}`);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// 监听外部传入的modelValue变化,同步到内部状态
|
|
479
|
+
watch(
|
|
480
|
+
() => props.modelValue,
|
|
481
|
+
(newValue) => {
|
|
482
|
+
// 如果是内部更新,跳过处理
|
|
483
|
+
if (state.isInternalUpdate) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (newValue && Array.isArray(newValue)) {
|
|
487
|
+
state.files = newValue.map((item) => {
|
|
488
|
+
if (typeof item === 'string') {
|
|
489
|
+
// 如果是字符串URL
|
|
490
|
+
return {
|
|
491
|
+
name: 'file',
|
|
492
|
+
type: 'image',
|
|
493
|
+
path: item,
|
|
494
|
+
status: 'success' as const,
|
|
495
|
+
result: item
|
|
496
|
+
};
|
|
497
|
+
} else {
|
|
498
|
+
// 如果是对象
|
|
499
|
+
return {
|
|
500
|
+
name: item.name || 'file',
|
|
501
|
+
type: item.type || 'file',
|
|
502
|
+
path: item.url || item.path || '',
|
|
503
|
+
status: 'success' as const,
|
|
504
|
+
result: item
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
} else if (newValue && typeof newValue === 'string') {
|
|
509
|
+
state.files = [
|
|
510
|
+
{
|
|
511
|
+
name: 'file',
|
|
512
|
+
type: 'image',
|
|
513
|
+
path: newValue,
|
|
514
|
+
status: 'success',
|
|
515
|
+
result: newValue
|
|
516
|
+
}
|
|
517
|
+
];
|
|
518
|
+
} else if (newValue && typeof newValue === 'object') {
|
|
519
|
+
const objValue = newValue as any;
|
|
520
|
+
state.files = [
|
|
521
|
+
{
|
|
522
|
+
name: objValue.name || 'file',
|
|
523
|
+
type: objValue.type || 'file',
|
|
524
|
+
path: objValue.url || objValue.path || '',
|
|
525
|
+
status: 'success',
|
|
526
|
+
result: newValue
|
|
527
|
+
}
|
|
528
|
+
];
|
|
529
|
+
} else {
|
|
530
|
+
state.files = [];
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
{ immediate: true, deep: true }
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// 组件销毁时清理资源
|
|
537
|
+
onUnmounted(() => {
|
|
538
|
+
state.files.forEach((file) => {
|
|
539
|
+
if (file.path && file.path.startsWith('blob:')) {
|
|
540
|
+
URL.revokeObjectURL(file.path);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
});
|
|
311
544
|
</script>
|
|
312
545
|
<style lang="scss">
|
|
313
|
-
.
|
|
546
|
+
.neatui-upload {
|
|
314
547
|
flex-wrap: wrap;
|
|
315
|
-
|
|
316
|
-
.upload {
|
|
317
|
-
|
|
318
|
-
|
|
548
|
+
// 上传状态样式
|
|
549
|
+
.upload-status {
|
|
550
|
+
position: absolute;
|
|
551
|
+
top: 0;
|
|
552
|
+
left: 0;
|
|
553
|
+
right: 0;
|
|
554
|
+
bottom: 0;
|
|
555
|
+
background: rgba(0, 0, 0, 0.6);
|
|
556
|
+
display: flex;
|
|
557
|
+
flex-direction: column;
|
|
558
|
+
align-items: center;
|
|
559
|
+
justify-content: center;
|
|
560
|
+
color: white;
|
|
561
|
+
font-size: 0.75em;
|
|
319
562
|
border-radius: 0.25em;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
background
|
|
323
|
-
// background-color: #ffffff;
|
|
324
|
-
position: relative;
|
|
325
|
-
|
|
326
|
-
.input {
|
|
327
|
-
opacity: 0;
|
|
328
|
-
height: 100%;
|
|
329
|
-
width: 100%;
|
|
563
|
+
|
|
564
|
+
&.error {
|
|
565
|
+
background: rgba(255, 0, 0, 0.6);
|
|
330
566
|
}
|
|
331
|
-
|
|
332
|
-
|
|
567
|
+
|
|
568
|
+
.status-text {
|
|
569
|
+
margin-top: 0.5em;
|
|
333
570
|
}
|
|
334
571
|
}
|
|
335
572
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
573
|
+
// 加载动画
|
|
574
|
+
.loading-spinner {
|
|
575
|
+
width: 1.5em;
|
|
576
|
+
height: 1.5em;
|
|
577
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
578
|
+
border-top: 2px solid white;
|
|
579
|
+
border-radius: 50%;
|
|
580
|
+
animation: spin 1s linear infinite;
|
|
581
|
+
}
|
|
339
582
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
583
|
+
@keyframes spin {
|
|
584
|
+
0% {
|
|
585
|
+
transform: rotate(0deg);
|
|
586
|
+
}
|
|
587
|
+
100% {
|
|
588
|
+
transform: rotate(360deg);
|
|
343
589
|
}
|
|
344
590
|
}
|
|
345
591
|
}
|