@neatui/vue 2.6.2 → 2.7.1

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.1",
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-case 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-case 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,192 +178,397 @@
154
178
  </div>
155
179
  </label>
156
180
  </slot>
157
- </li>
158
- </ul>
159
- <Layer :id="layerId" am="as"></Layer>
181
+ </div>
182
+ </div>
160
183
  </template>
161
184
  <script setup lang="ts">
162
- import { reactive, ref, watch } from 'vue';
163
- import { Image } from '../display';
164
- import draggable from 'vuedraggable';
185
+ import { reactive, watch, onUnmounted, computed } from 'vue';
165
186
  import Compressor from 'compressorjs';
166
- import { Layer, LayerById } from '../basic';
167
- import { isArray, isFunction } from '@fekit/utils';
168
-
169
- const layerId = Math.random().toString(36).slice(-6);
170
187
 
171
188
  interface Props {
172
189
  modelValue?: string | any[];
173
- // 文件限制数量
174
- limit?: number;
190
+ max?: number;
175
191
  attrs?: object;
176
192
  uploadClass?: string;
177
193
  uploadStyle?: string;
178
- id?: string;
179
- tips?: string; // 提示文案
180
- // 文件类型
181
- accept?: string;
182
- // 是否可以上传多个文件
183
- multiple?: boolean;
184
- // 是否需要抠图处理
185
- crop?: boolean;
186
- // 上传路径
187
- action?: string;
188
- // 请求上传接口
194
+ tips?: string;
195
+ accept?: string | 'image/*' | 'video/*' | 'audio/*';
189
196
  upload?: (formData: FormData) => Promise<any>;
197
+ maxSize?: number;
198
+ compress?: boolean;
190
199
  }
191
200
 
192
201
  const props = withDefaults(defineProps<Props>(), {
202
+ modelValue: '',
193
203
  uploadClass: '',
194
204
  uploadStyle: '',
195
- id: '',
196
205
  attrs: () => ({}),
197
- tips: '',
198
- modelValue: '',
199
- accept: 'image/*',
200
- multiple: false,
201
- crop: false,
202
- action: '/common/updateFile',
203
- upload: async () => null
206
+ tips: '', // 提示文案
207
+ accept: '*', // 文件类型
208
+ upload: async () => null, // 请求上传接口
209
+ maxSize: 10, // 文件大小限制(MB)
210
+ compress: true, // 是否启用图片压缩
211
+ max: 0 // 文件数量限制
212
+ });
213
+
214
+ interface UploadFile {
215
+ name: string;
216
+ type: string;
217
+ path: string;
218
+ status: 'uploading' | 'success' | 'error';
219
+ size?: number;
220
+ progress?: number;
221
+ result?: any; // 直接保存上传函数的返回值
222
+ }
223
+
224
+ const state = reactive<{
225
+ files: UploadFile[];
226
+ isInternalUpdate: boolean; // 标记是否为内部更新
227
+ }>({
228
+ files: [],
229
+ isInternalUpdate: false
204
230
  });
205
231
 
206
- const state: any = reactive({
207
- files: []
232
+ const emit = defineEmits(['update:modelValue', 'change', 'error']);
233
+
234
+ // 获取图片URL
235
+ const getImageUrl = (file: UploadFile): string => {
236
+ if (file.status === 'success' && file.result) {
237
+ // 如果上传成功,优先使用上传返回的URL
238
+ if (typeof file.result === 'string') {
239
+ return file.result;
240
+ }
241
+ // 如果是对象,尝试获取url或path
242
+ return file.result.url || file.result.path || file.path;
243
+ }
244
+ // 否则使用预览URL
245
+ return file.path;
246
+ };
247
+
248
+ // 计算属性
249
+ const showUploadButton = computed(() => {
250
+ const currentCount = state.files.length;
251
+
252
+ if (!props.max) {
253
+ return true; // 无限制
254
+ }
255
+
256
+ return currentCount < props.max;
208
257
  });
209
258
 
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';
259
+ // 获取文件类型
260
+ const getFileType = (file: File): string => {
261
+ if (file.type === 'application/msword' || file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
262
+ return 'docx';
263
+ } else if (file.type === 'application/vnd.ms-excel' || file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
264
+ return 'xlsx';
265
+ } else if (file.type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
266
+ return 'pptx';
267
+ } else if (file.type === 'application/pdf') {
268
+ return 'pdf';
269
+ } else if (file.type === 'text/html') {
270
+ return 'html';
271
+ } else if (file.type.startsWith('image/') && file.type !== 'image/vnd.adobe.photoshop') {
272
+ return 'image';
273
+ }
274
+ return 'file';
275
+ };
276
+
277
+ // 验证文件
278
+ const validateFile = (file: File): string | null => {
279
+ // 检查文件大小
280
+ if (props.maxSize && file.size > props.maxSize * 1024 * 1024) {
281
+ return `文件大小不能超过 ${props.maxSize}MB`;
282
+ }
283
+
284
+ // 检查文件类型
285
+ if (props.accept && props.accept !== '*') {
286
+ const acceptTypes = props.accept.split(',').map((type) => type.trim());
287
+ const isValidType = acceptTypes.some((type) => {
288
+ if (type.endsWith('/*')) {
289
+ return file.type.startsWith(type.replace('/*', ''));
290
+ }
291
+ return file.type === type;
292
+ });
293
+
294
+ if (!isValidType) {
295
+ return `不支持的文件类型: ${file.type}`;
296
+ }
297
+ }
298
+
299
+ return null;
300
+ };
301
+
302
+ // 压缩图片
303
+ const compressImage = async (file: File): Promise<File> => {
304
+ if (!props.compress || !file.type.startsWith('image/')) {
305
+ return file;
306
+ }
307
+
308
+ return new Promise((resolve, reject) => {
309
+ new Compressor(file, {
310
+ quality: 0.8,
311
+ maxWidth: 1920,
312
+ maxHeight: 1080,
313
+ checkOrientation: true,
314
+ success: (result) => {
315
+ resolve(new File([result], file.name, { type: result.type }));
316
+ },
317
+ error: reject
318
+ });
319
+ });
320
+ };
321
+
322
+ // 删除文件函数
323
+ const remove = (index: number) => {
324
+ // 释放对象URL,防止内存泄漏
325
+ if (state.files[index] && state.files[index].path) {
326
+ URL.revokeObjectURL(state.files[index].path);
327
+ }
328
+
329
+ // 从数组中移除文件
330
+ state.files.splice(index, 1);
331
+
332
+ // 发射更新事件
333
+ emitUpdateEvents();
334
+ };
335
+
336
+ // 发射更新事件
337
+ const emitUpdateEvents = () => {
338
+ // 设置内部更新标志
339
+ state.isInternalUpdate = true;
340
+
341
+ const successFiles = state.files.filter((file) => file.status === 'success');
342
+
343
+ // 如果没有成功文件,返回空值
344
+ if (successFiles.length === 0) {
345
+ const emptyValue = props.max === 1 ? '' : [];
346
+ emit('update:modelValue', emptyValue);
347
+ emit('change', emptyValue);
348
+ } else {
349
+ // 直接使用上传函数的返回值
350
+ const filesForModel = successFiles.map((file) => file.result);
351
+ const modelValue = props.max === 1 ? filesForModel[0] || '' : filesForModel;
352
+
353
+ emit('update:modelValue', modelValue);
354
+ emit('change', modelValue);
355
+ }
356
+
357
+ // 重置标志
358
+ setTimeout(() => {
359
+ state.isInternalUpdate = false;
360
+ }, 0);
361
+ };
362
+
363
+ // 文件上传处理
364
+ const change = (e: Event) => {
365
+ const target = e.target as HTMLInputElement;
366
+ const selectedFiles = target.files || [];
367
+
368
+ // 检查文件数量限制
369
+ if (props.max && state.files.length + selectedFiles.length > props.max) {
370
+ const errorMsg = `文件数量超过限制: ${props.max}`;
371
+ console.warn(errorMsg);
372
+ emit('error', errorMsg);
373
+ return;
374
+ }
375
+
376
+ // 步骤1:立即添加所有文件到UI,显示占位图
377
+ const validFiles: { file: File; uploadFile: UploadFile }[] = [];
378
+
379
+ for (let i = 0; i < selectedFiles.length; i++) {
380
+ const file = selectedFiles[i];
381
+
382
+ // 验证文件
383
+ const validationError = validateFile(file);
384
+ if (validationError) {
385
+ emit('error', validationError);
386
+ continue;
248
387
  }
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
- // }
388
+
389
+ // 创建预览URL
390
+ const previewUrl = URL.createObjectURL(file);
391
+
392
+ // 添加到文件列表
393
+ const uploadFile: UploadFile = {
394
+ name: file.name,
395
+ type: getFileType(file),
396
+ path: previewUrl,
397
+ status: 'uploading',
398
+ size: file.size
399
+ };
400
+ state.files.push(uploadFile);
401
+ validFiles.push({ file, uploadFile });
259
402
  }
260
403
 
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
- // }
404
+ // 步骤2:异步上传所有文件
405
+ validFiles.forEach(({ file, uploadFile }) => {
406
+ uploadSingleFile(file, uploadFile);
407
+ });
408
+
409
+ // 清空input值,允许重复选择同一文件
410
+ target.value = '';
411
+ };
412
+
413
+ // 单个文件上传函数
414
+ const uploadSingleFile = async (file: File, uploadFile: UploadFile) => {
415
+ try {
416
+ // 压缩图片(如果需要)
417
+ let processedFile = file;
418
+ if (props.compress && file.type.startsWith('image/')) {
419
+ try {
420
+ processedFile = await compressImage(file);
421
+ } catch (error) {
422
+ console.warn('图片压缩失败,使用原文件:', error);
423
+ }
424
+ }
425
+
426
+ // 上传到服务器
427
+ if (typeof props.upload !== 'function') {
428
+ throw new Error('upload 函数未定义');
429
+ }
430
+
431
+ const formData = new FormData();
432
+ formData.append('file', processedFile);
433
+ const result = await props.upload(formData);
434
+
435
+ // 更新文件状态
436
+ const fileIndex = state.files.findIndex((f) => f.path === uploadFile.path);
437
+ if (fileIndex !== -1) {
438
+ if (result) {
439
+ // 直接保存上传函数的返回值
440
+ state.files[fileIndex].result = result;
441
+ state.files[fileIndex].status = 'success';
442
+ } else {
443
+ state.files[fileIndex].status = 'error';
444
+ }
445
+ // 每个文件完成后更新外部数据源
446
+ emitUpdateEvents();
447
+ }
448
+ } catch (error) {
449
+ console.error('文件处理失败:', error);
450
+
451
+ // 更新失败文件状态
452
+ const fileIndex = state.files.findIndex((f) => f.path === uploadFile.path);
453
+ if (fileIndex !== -1) {
454
+ state.files[fileIndex].status = 'error';
455
+ }
456
+
457
+ emit('error', `文件 ${file.name} 上传失败: ${error}`);
458
+ }
295
459
  };
296
460
 
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 });
461
+ // 监听外部传入的modelValue变化,同步到内部状态
462
+ watch(
463
+ () => props.modelValue,
464
+ (newValue) => {
465
+ // 如果是内部更新,跳过处理
466
+ if (state.isInternalUpdate) {
467
+ return;
468
+ }
469
+ if (newValue && Array.isArray(newValue)) {
470
+ state.files = newValue.map((item) => {
471
+ if (typeof item === 'string') {
472
+ // 如果是字符串URL
473
+ return {
474
+ name: 'file',
475
+ type: 'image',
476
+ path: item,
477
+ status: 'success' as const,
478
+ result: item
479
+ };
480
+ } else {
481
+ // 如果是对象
482
+ return {
483
+ name: item.name || 'file',
484
+ type: item.type || 'file',
485
+ path: item.url || item.path || '',
486
+ status: 'success' as const,
487
+ result: item
488
+ };
489
+ }
490
+ });
491
+ } else if (newValue && typeof newValue === 'string') {
492
+ state.files = [
493
+ {
494
+ name: 'file',
495
+ type: 'image',
496
+ path: newValue,
497
+ status: 'success',
498
+ result: newValue
499
+ }
500
+ ];
501
+ } else if (newValue && typeof newValue === 'object') {
502
+ const objValue = newValue as any;
503
+ state.files = [
504
+ {
505
+ name: objValue.name || 'file',
506
+ type: objValue.type || 'file',
507
+ path: objValue.url || objValue.path || '',
508
+ status: 'success',
509
+ result: newValue
510
+ }
511
+ ];
512
+ } else {
513
+ state.files = [];
514
+ }
515
+ },
516
+ { immediate: true, deep: true }
517
+ );
518
+
519
+ // 组件销毁时清理资源
520
+ onUnmounted(() => {
521
+ state.files.forEach((file) => {
522
+ if (file.path && file.path.startsWith('blob:')) {
523
+ URL.revokeObjectURL(file.path);
524
+ }
525
+ });
526
+ });
311
527
  </script>
312
528
  <style lang="scss">
313
- .mc-upload {
529
+ .neatui-upload {
314
530
  flex-wrap: wrap;
315
-
316
- .upload {
317
- width: 8em;
318
- height: 8em;
531
+ // 上传状态样式
532
+ .upload-status {
533
+ position: absolute;
534
+ top: 0;
535
+ left: 0;
536
+ right: 0;
537
+ bottom: 0;
538
+ background: rgba(0, 0, 0, 0.6);
539
+ display: flex;
540
+ flex-direction: column;
541
+ align-items: center;
542
+ justify-content: center;
543
+ color: white;
544
+ font-size: 0.75em;
319
545
  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%;
546
+
547
+ &.error {
548
+ background: rgba(255, 0, 0, 0.6);
330
549
  }
331
- .input-icon {
332
- line-height: 1.3;
550
+
551
+ .status-text {
552
+ margin-top: 0.5em;
333
553
  }
334
554
  }
335
555
 
336
- &-img {
337
- display: flex;
338
- flex-direction: row;
556
+ // 加载动画
557
+ .loading-spinner {
558
+ width: 1.5em;
559
+ height: 1.5em;
560
+ border: 2px solid rgba(255, 255, 255, 0.3);
561
+ border-top: 2px solid white;
562
+ border-radius: 50%;
563
+ animation: spin 1s linear infinite;
564
+ }
339
565
 
340
- &-item {
341
- margin-right: 0.75rem;
342
- margin-bottom: 0.75rem;
566
+ @keyframes spin {
567
+ 0% {
568
+ transform: rotate(0deg);
569
+ }
570
+ 100% {
571
+ transform: rotate(360deg);
343
572
  }
344
573
  }
345
574
  }
@@ -43,6 +43,7 @@
43
43
  </template>
44
44
  </IFollowView>
45
45
  <button class="bg-fore" ui-btn="@a s case :border" @click="fResetLimit">{{ words['common.reset'] || '重置' }}</button>
46
+ <button class="bg-fore" ui-btn="@a s case :border" @click="fSearch">{{ words['common.search'] || '搜索' }}</button>
46
47
  </div>
47
48
  </div>
48
49
  </template>
@@ -91,6 +92,11 @@
91
92
  tools: false
92
93
  });
93
94
 
95
+ // 定义emit事件
96
+ const emits = defineEmits<{
97
+ search: [param: any];
98
+ }>();
99
+
94
100
  // 内部数据
95
101
  const state: any = reactive({
96
102
  name: computed(() => 'confs' + route.path?.replace(/\//g, '_')),
@@ -173,5 +179,16 @@
173
179
  delete state.param[key];
174
180
  }
175
181
  }
182
+ // 触发搜索事件
183
+ emits('search', state.param);
184
+ };
185
+
186
+ // 搜索功能
187
+ const fSearch = () => {
188
+ // 重置页码
189
+ if (state.param.page) {
190
+ state.param.page = 1;
191
+ }
192
+ emits('search', state.param);
176
193
  };
177
194
  </script>