|
|
@@ -68,11 +68,13 @@ import { ref, computed, inject, watch, nextTick } from "vue";
|
|
|
import { Plus, Picture } from "@element-plus/icons-vue";
|
|
|
import { uploadImg } from "@/api/modules/upload";
|
|
|
import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
|
|
|
-import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
|
|
|
+import { ElMessage, ElNotification, formContextKey, formItemContextKey } from "element-plus";
|
|
|
+import { useSimpleUploadOverlayStore } from "@/stores/modules/simpleUploadOverlay";
|
|
|
|
|
|
interface UploadFileProps {
|
|
|
fileList: UploadUserFile[];
|
|
|
- api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
|
|
|
+ /** 上传 api;第二参为透传选项(如 skipSimpleUploadOverlay),与默认 uploadImg 一致 */
|
|
|
+ api?: (params: FormData, options?: Record<string, unknown>) => Promise<any>;
|
|
|
drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
|
|
|
disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
|
|
|
limit?: number; // 最大图片上传数 ==> 非必传(默认为 5张)
|
|
|
@@ -151,6 +153,34 @@ watch(
|
|
|
const uploadingFiles = new Set<string | number>();
|
|
|
// 标记是否已经显示过成功提示,防止重复提示
|
|
|
let hasShownSuccessNotification = false;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 多选时 el-upload 会对每个文件并发触发 http-request,易导致 /upload/simple 并发报错。
|
|
|
+ * 通过 Promise 链串行化,同一时间只发起一次上传请求。
|
|
|
+ */
|
|
|
+let sequentialUploadTail = Promise.resolve();
|
|
|
+
|
|
|
+/** 当前这一批(一次多选)里待上传的文件数;归零时关闭全局上传弹层 */
|
|
|
+let pendingBatchUploadCount = 0;
|
|
|
+let batchHadUploadError = false;
|
|
|
+
|
|
|
+function sleep(ms: number) {
|
|
|
+ return new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
|
+}
|
|
|
+
|
|
|
+async function finishBatchUploadOverlay() {
|
|
|
+ const overlay = useSimpleUploadOverlayStore();
|
|
|
+ if (batchHadUploadError) {
|
|
|
+ overlay.dismiss();
|
|
|
+ } else {
|
|
|
+ overlay.bumpToComplete();
|
|
|
+ await sleep(280);
|
|
|
+ overlay.dismiss();
|
|
|
+ if (props.showSuccessNotification) {
|
|
|
+ ElMessage.success("上传成功");
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
// 记录图片加载失败的 UID
|
|
|
const imageLoadError = ref<Set<string | number>>(new Set());
|
|
|
|
|
|
@@ -196,70 +226,89 @@ const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
|
|
|
* @description 图片上传
|
|
|
* @param options upload 所有配置项
|
|
|
* */
|
|
|
-const handleHttpUpload = async (options: UploadRequestOptions) => {
|
|
|
- // 开始上传,记录文件 UID
|
|
|
+const handleHttpUpload = (options: UploadRequestOptions): Promise<unknown> => {
|
|
|
const fileUid = options.file.uid;
|
|
|
- // 如果这是新的一批上传(Set 为空),重置成功提示标志
|
|
|
if (uploadingFiles.size === 0) {
|
|
|
hasShownSuccessNotification = false;
|
|
|
}
|
|
|
uploadingFiles.add(fileUid);
|
|
|
- let formData = new FormData();
|
|
|
- formData.append("file", options.file);
|
|
|
- try {
|
|
|
- const api = props.api ?? uploadImg;
|
|
|
- const response = await api(formData);
|
|
|
- // 从 response.fileUrl / response.data.fileUrl 取值
|
|
|
- const fileUrl = response?.fileUrl || response?.data?.fileUrl || "";
|
|
|
-
|
|
|
- if (fileUrl) {
|
|
|
- const coverUrlRaw =
|
|
|
- (response as any)?.coverUrl ?? (response as any)?.data?.coverUrl ?? (response as any)?.data?.cover ?? "";
|
|
|
- const coverUrl = typeof coverUrlRaw === "string" && coverUrlRaw.trim() ? coverUrlRaw.trim() : "";
|
|
|
-
|
|
|
- // 更新 options.file(Element Plus 传入的文件对象)
|
|
|
- (options.file as unknown as UploadFile).url = fileUrl;
|
|
|
- (options.file as unknown as UploadFile).status = "success";
|
|
|
- (options.file as unknown as UploadFile).response = response;
|
|
|
- if (coverUrl) {
|
|
|
- (options.file as any).coverUrl = coverUrl;
|
|
|
- }
|
|
|
|
|
|
- // 同步更新 _fileList 中对应的文件
|
|
|
- const fileIndex = _fileList.value.findIndex(item => item.uid === fileUid);
|
|
|
- if (fileIndex !== -1) {
|
|
|
- _fileList.value[fileIndex].url = fileUrl;
|
|
|
- _fileList.value[fileIndex].status = "success";
|
|
|
- (_fileList.value[fileIndex] as any).response = response;
|
|
|
+ if (pendingBatchUploadCount === 0) {
|
|
|
+ batchHadUploadError = false;
|
|
|
+ }
|
|
|
+ pendingBatchUploadCount++;
|
|
|
+ if (pendingBatchUploadCount === 1) {
|
|
|
+ useSimpleUploadOverlayStore().beginUpload();
|
|
|
+ }
|
|
|
+
|
|
|
+ const runOne = async (): Promise<void> => {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("file", options.file);
|
|
|
+ const simpleOpts = { skipSimpleUploadOverlay: true as const };
|
|
|
+ try {
|
|
|
+ const response = props.api ? await props.api(formData, simpleOpts) : await uploadImg(formData, simpleOpts);
|
|
|
+ const fileUrl = response?.fileUrl || response?.data?.fileUrl || "";
|
|
|
+
|
|
|
+ if (fileUrl) {
|
|
|
+ const coverUrlRaw =
|
|
|
+ (response as any)?.coverUrl ?? (response as any)?.data?.coverUrl ?? (response as any)?.data?.cover ?? "";
|
|
|
+ const coverUrl = typeof coverUrlRaw === "string" && coverUrlRaw.trim() ? coverUrlRaw.trim() : "";
|
|
|
+
|
|
|
+ (options.file as unknown as UploadFile).url = fileUrl;
|
|
|
+ (options.file as unknown as UploadFile).status = "success";
|
|
|
+ (options.file as unknown as UploadFile).response = response;
|
|
|
if (coverUrl) {
|
|
|
- (_fileList.value[fileIndex] as any).coverUrl = coverUrl;
|
|
|
+ (options.file as any).coverUrl = coverUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ const fileIndex = _fileList.value.findIndex(item => item.uid === fileUid);
|
|
|
+ if (fileIndex !== -1) {
|
|
|
+ _fileList.value[fileIndex].url = fileUrl;
|
|
|
+ _fileList.value[fileIndex].status = "success";
|
|
|
+ (_fileList.value[fileIndex] as any).response = response;
|
|
|
+ if (coverUrl) {
|
|
|
+ (_fileList.value[fileIndex] as any).coverUrl = coverUrl;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- options.onSuccess(response);
|
|
|
- // 传递更新后的 _fileList 给父组件
|
|
|
- emit("update:fileList", _fileList.value);
|
|
|
- if (props.onSuccess) {
|
|
|
- try {
|
|
|
- const result = props.onSuccess(fileUrl);
|
|
|
- // 如果回调返回 Promise,等待它完成(但不影响上传成功状态)
|
|
|
- if (result !== undefined && result !== null && typeof result === "object" && typeof (result as any).then === "function") {
|
|
|
- (result as Promise<any>).catch((callbackError: any) => {
|
|
|
- // 回调失败不影响上传成功状态,只记录错误
|
|
|
- console.error("onSuccess callback error:", callbackError);
|
|
|
- });
|
|
|
+ options.onSuccess(response);
|
|
|
+ emit("update:fileList", _fileList.value);
|
|
|
+ if (props.onSuccess) {
|
|
|
+ try {
|
|
|
+ const result = props.onSuccess(fileUrl);
|
|
|
+ if (
|
|
|
+ result !== undefined &&
|
|
|
+ result !== null &&
|
|
|
+ typeof result === "object" &&
|
|
|
+ typeof (result as any).then === "function"
|
|
|
+ ) {
|
|
|
+ (result as Promise<any>).catch((callbackError: any) => {
|
|
|
+ console.error("onSuccess callback error:", callbackError);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (callbackError) {
|
|
|
+ console.error("onSuccess callback error:", callbackError);
|
|
|
}
|
|
|
- } catch (callbackError) {
|
|
|
- // 回调失败不影响上传成功状态,只记录错误
|
|
|
- console.error("onSuccess callback error:", callbackError);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ batchHadUploadError = true;
|
|
|
+ uploadingFiles.delete(fileUid);
|
|
|
+ options.onError(error as any);
|
|
|
+ throw error;
|
|
|
+ } finally {
|
|
|
+ pendingBatchUploadCount--;
|
|
|
+ if (pendingBatchUploadCount === 0) {
|
|
|
+ await finishBatchUploadOverlay();
|
|
|
}
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
- // 上传失败,移除文件 UID
|
|
|
- uploadingFiles.delete(fileUid);
|
|
|
- options.onError(error as any);
|
|
|
- }
|
|
|
+ };
|
|
|
+
|
|
|
+ const p = sequentialUploadTail.then(runOne);
|
|
|
+ sequentialUploadTail = p.catch(() => {
|
|
|
+ /* 保持队列继续,不因单张失败阻断后续文件 */
|
|
|
+ });
|
|
|
+ return p;
|
|
|
};
|
|
|
|
|
|
/**
|