@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
|
<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-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.
|
|
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-
|
|
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="
|
|
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
|
-
</
|
|
158
|
-
</
|
|
159
|
-
<Layer :id="layerId" am="as"></Layer>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
160
183
|
</template>
|
|
161
184
|
<script setup lang="ts">
|
|
162
|
-
import { reactive,
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
.
|
|
529
|
+
.neatui-upload {
|
|
314
530
|
flex-wrap: wrap;
|
|
315
|
-
|
|
316
|
-
.upload {
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
background
|
|
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
|
-
|
|
332
|
-
|
|
550
|
+
|
|
551
|
+
.status-text {
|
|
552
|
+
margin-top: 0.5em;
|
|
333
553
|
}
|
|
334
554
|
}
|
|
335
555
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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>
|