@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
  {
2
2
  "name": "@neatui/vue",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div>
3
- <div :class="`mc-img b-solid bk-case b-xs ${imgClass}`" :style="imgStyle">
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: 'imgUrl' },
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="mySwiper"
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: 'imgUrl'
35
+ default: 'url'
36
36
  },
37
37
  id: {
38
38
  type: String,
@@ -1,7 +1,18 @@
1
1
  <template>
2
- <ul ui-flex="row lm :wrap" class="mb-ss-sub mr-ss-sub" v-bind="{ ...attrs }">
3
- <li 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">
4
- <img v-if="file.type === 'image'" :src="file.path" alt="" />
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.291068 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"
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
- <p ui-omit="1" class="fs-ss ac">{{ file.name }}</p>
139
- </li>
140
- <li v-if="(!multiple && state.files && state.files.length === 0) || (multiple && (!limit || (limit && state.files && state.files.length < limit)))">
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="multiple" @change="change" />
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
- </li>
158
- </ul>
159
- <Layer :id="layerId" am="as"></Layer>
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
- limit?: number;
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
- modelValue: '',
199
- accept: 'image/*',
200
- multiple: false,
223
+ accept: '*',
201
224
  crop: false,
202
- action: '/common/updateFile',
203
- upload: async () => null
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 state: any = reactive({
207
- files: []
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
- const emit = defineEmits(['update:modelValue', 'change']);
211
-
212
- // const files = ref<any>(isArray(props.modelValue) ? props.modelValue : props.modelValue ? [{ imgUrl: props.modelValue }] : []);
213
-
214
- // 删除图片
215
- // const delImg = (src: any) => {
216
- // files.value = files.value.filter((item: any) => {
217
- // return item.imgUrl !== src.imgUrl;
218
- // });
219
- // };
220
-
221
- // const uploadClick = () => {
222
- // LayerById(layerId).show();
223
- // };
224
-
225
- // const update = () => {
226
- // emit('update:modelValue', isArray(props.modelValue) ? files.value : files.value.length > 0 ? files.value[0].imgUrl : '');
227
- // };
228
-
229
- const change = async (e: any = {}) => {
230
- const files = e?.target?.files || [];
231
- for (let i = 0; i < files.length; i++) {
232
- const _file: any = files[i];
233
-
234
- // 文件类型
235
- let type = '';
236
- if (_file.type === 'application/msword' || _file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
237
- type = 'docx';
238
- } else if (_file.type === 'application/vnd.ms-excel' || _file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
239
- type = 'xlsx';
240
- } else if (_file.type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
241
- type = 'pptx';
242
- } else if (_file.type === 'application/pdf') {
243
- type = 'pdf';
244
- } else if (_file.type === 'text/html') {
245
- type = 'html';
246
- } else if (_file.type.startsWith('image/') && _file.type !== 'image/vnd.adobe.photoshop') {
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
- // const _files = [];
262
- // for (let i = 0; i < files.length; i++) {
263
- // const file = files[i];
264
- // const blob = URL.createObjectURL(file);
265
- // _files.push({ blob, file });
266
- // }
267
- // console.log(98, _files);
268
- // for (let i = 0; i < _files.length; i++) {
269
- // let { file = '' } = _files[i];
270
- // // 若为图片,则进行压缩处理
271
- // if (/image/.test(file.type)) {
272
- // const _blob: any = await new Promise((resolve, reject) => {
273
- // new Compressor(file, {
274
- // quality: 1,
275
- // checkOrientation: false,
276
- // success: resolve,
277
- // maxWidth: 1000,
278
- // maxHeight: 700,
279
- // error: reject
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
- // watch(
298
- // () => [props.modelValue, state.files],
299
- // (n: any, o: any) => {
300
- // if (n[0] !== o[0]) {
301
- // files.value = isArray(props.modelValue) ? props.modelValue : props.modelValue ? [{ imgUrl: props.modelValue }] : [];
302
- // }
303
- // if (n[1] !== o[1]) {
304
- // emit('update:modelValue', isArray(props.modelValue) ? files.value : files.value.length > 0 ? files.value[0].imgUrl : '');
305
- // }
306
- // },
307
- // { deep: true }
308
- // );
309
-
310
- // defineExpose({ change, uploadClick });
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
- .mc-upload {
546
+ .neatui-upload {
314
547
  flex-wrap: wrap;
315
-
316
- .upload {
317
- width: 8em;
318
- height: 8em;
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
- transition:
321
- all 0.35s,
322
- background-color 0s;
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
- .input-icon {
332
- line-height: 1.3;
567
+
568
+ .status-text {
569
+ margin-top: 0.5em;
333
570
  }
334
571
  }
335
572
 
336
- &-img {
337
- display: flex;
338
- flex-direction: row;
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
- &-item {
341
- margin-right: 0.75rem;
342
- margin-bottom: 0.75rem;
583
+ @keyframes spin {
584
+ 0% {
585
+ transform: rotate(0deg);
586
+ }
587
+ 100% {
588
+ transform: rotate(360deg);
343
589
  }
344
590
  }
345
591
  }