feat(sse): 集成服务器推送事件实现异步任务进度推送
Some checks failed
Lint Code / Lint Code (push) Failing after 1m34s

- 新增SSE客户端类,实现基于fetch API的事件流连接和自动重连
- 增加sse状态管理Pinia模块,支持连接管理、任务进度和状态跟踪
- 登录状态管理增加userId字段,完善用户信息结构
- 登录接口修改,支持接收和存储用户ID、角色和权限信息
- 登录mock禁用,切换为真实后端接口调用
- 主布局组件增加SSE连接初始化与关闭生命周期钩子
- 项目创建向导中改用SSE方式上传文件及监听解析进度和结果
- 文件上传界面增加上传及任务进度展示,包括状态提示和进度条
- token处理函数更新,支持后端多种token字段并正确存储用户信息
- 调整本地存储结构,适应新增的用户ID和权限字段管理
This commit is contained in:
2026-03-28 17:24:29 +08:00
parent 87bdef6416
commit ac4d43fd01
10 changed files with 633 additions and 90 deletions

View File

@@ -1,44 +1,5 @@
// 根据角色动态生成路由
import { defineFakeRoute } from "vite-plugin-fake-server/client";
export default defineFakeRoute([
{
url: "/login",
method: "post",
response: ({ body }) => {
if (body.username === "admin") {
return {
code: 0,
message: "操作成功",
data: {
avatar: "https://avatars.githubusercontent.com/u/44761321",
username: "admin",
nickname: "小铭",
// 一个用户可能有多个角色
roles: ["admin"],
// 按钮级别权限
permissions: ["*:*:*"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
expires: "2030/10/30 00:00:00"
}
};
} else {
return {
code: 0,
message: "操作成功",
data: {
avatar: "https://avatars.githubusercontent.com/u/52823142",
username: "common",
nickname: "小林",
roles: ["common"],
permissions: ["permission:btn:add", "permission:btn:edit"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
expires: "2030/10/30 00:00:00"
}
};
}
}
}
]);
// 禁用登录 mock使用真实后端接口
export default defineFakeRoute([]);

View File

@@ -11,13 +11,19 @@ export type FeishuLoginData = {
/** 头像 */
avatar: string;
/** 用户ID */
userId: number;
userId: string;
/** 邮箱 */
email: string;
/** token */
token: string;
/** 用户名 */
username: string;
/** 是否管理员 */
isAdmin?: boolean;
/** 角色列表 */
roles?: string[];
/** 权限列表 */
permissions?: string[];
};
/** 飞书登录响应结果 */

View File

@@ -7,6 +7,8 @@ import { useI18n } from "vue-i18n";
import { useLayout } from "./hooks/useLayout";
import { useAppStoreHook } from "@/store/modules/app";
import { useSettingStoreHook } from "@/store/modules/settings";
import { useUserStoreHook } from "@/store/modules/user";
import { useSseStoreHook } from "@/store/modules/sse";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import {
h,
@@ -15,6 +17,7 @@ import {
computed,
onMounted,
onBeforeMount,
onBeforeUnmount,
defineComponent
} from "vue";
import {
@@ -120,8 +123,39 @@ onMounted(() => {
if (isMobile) {
toggle("mobile", false);
}
// 初始化 SSE 连接
initSse();
});
onBeforeUnmount(() => {
// 关闭 SSE 连接
useSseStoreHook().closeSse();
});
// 初始化 SSE 连接
function initSse() {
const userStore = useUserStoreHook();
const sseStore = useSseStoreHook();
const userId = userStore.userId;
console.log(
"[SSE] initSse called, userId:",
userId,
"isConnected:",
sseStore.getIsConnected
);
if (userId && !sseStore.getIsConnected) {
console.log("[SSE] 正在建立连接...");
sseStore.initSse(userId);
} else {
console.log(
"[SSE] 跳过连接 - userId:",
userId,
"isConnected:",
sseStore.getIsConnected
);
}
}
onBeforeMount(() => {
useDataThemeChange().dataThemeChange($storage.layout?.themeMode);
});

148
src/store/modules/sse.ts Normal file
View File

@@ -0,0 +1,148 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { SseClient, type ProjectInitTaskVO } from "@/utils/sse/SseClient";
import { store } from "../utils";
export const useSseStore = defineStore("sse", () => {
// State
const sseClient = ref<SseClient | null>(null);
const isConnected = ref(false);
const currentTask = ref<ProjectInitTaskVO | null>(null);
const taskProgress = ref(0);
const taskStatus = ref<
"idle" | "submitted" | "processing" | "completed" | "error"
>("idle");
const errorMessage = ref("");
// Getters
const getIsConnected = computed(() => isConnected.value);
const getCurrentTask = computed(() => currentTask.value);
const getTaskProgress = computed(() => taskProgress.value);
const getTaskStatus = computed(() => taskStatus.value);
// Actions
/**
* 初始化 SSE 连接
* @param userId 用户ID
*/
async function initSse(userId: string) {
// 如果已有连接,先关闭
if (sseClient.value) {
sseClient.value.close();
}
sseClient.value = new SseClient(userId);
// 监听连接成功
sseClient.value.on("connected", () => {
isConnected.value = true;
console.log("SSE Store: 连接成功");
});
// 监听项目初始化进度
sseClient.value.on("project-init-progress", (data: ProjectInitTaskVO) => {
currentTask.value = data;
taskProgress.value = data.progress;
taskStatus.value = "processing";
console.log(
`SSE Store: 任务进度 ${data.progress}% - ${data.progressMessage}`
);
});
// 监听任务完成
sseClient.value.on("project-init-complete", (data: ProjectInitTaskVO) => {
currentTask.value = data;
taskProgress.value = 100;
taskStatus.value = "completed";
console.log("SSE Store: 任务完成", data.result);
});
// 监听任务提交
sseClient.value.on(
"project-init-submitted",
(data: { taskId: string; message: string }) => {
taskStatus.value = "submitted";
console.log("SSE Store: 任务已提交", data.taskId);
}
);
// 监听错误
sseClient.value.on("project-init-error", (data: { error: string }) => {
taskStatus.value = "error";
errorMessage.value = data.error;
console.error("SSE Store: 任务错误", data.error);
});
// 监听连接错误
sseClient.value.on("connection-error", () => {
isConnected.value = false;
});
// 建立连接(异步,在后台运行)
sseClient.value.connect().catch(err => {
console.error("SSE Store: 连接失败", err);
});
}
/**
* 关闭 SSE 连接
*/
function closeSse() {
if (sseClient.value) {
sseClient.value.close();
sseClient.value = null;
isConnected.value = false;
currentTask.value = null;
taskProgress.value = 0;
taskStatus.value = "idle";
errorMessage.value = "";
}
}
/**
* 提交项目初始化任务
*/
async function submitProjectInitTask(file: File) {
if (!sseClient.value) {
throw new Error("SSE 未连接");
}
taskStatus.value = "submitted";
taskProgress.value = 0;
errorMessage.value = "";
return sseClient.value.submitProjectInitTask(file);
}
/**
* 重置任务状态
*/
function resetTaskStatus() {
currentTask.value = null;
taskProgress.value = 0;
taskStatus.value = "idle";
errorMessage.value = "";
}
return {
// State
sseClient,
isConnected,
currentTask,
taskProgress,
taskStatus,
errorMessage,
// Getters
getIsConnected,
getCurrentTask,
getTaskProgress,
getTaskStatus,
// Actions
initSse,
closeSse,
submitProjectInitTask,
resetTaskStatus
};
});
export function useSseStoreHook() {
return useSseStore(store);
}

View File

@@ -17,27 +17,32 @@ import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth";
export const useUserStore = defineStore("pure-user", {
state: (): userType => ({
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
// 用户名
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "",
// 昵称
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "",
// 页面级别权限
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
// 按钮级别权限
permissions:
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [],
// 前端生成的验证码(按实际需求替换)
verifyCode: "",
// 判断登录页面显示哪个组件0登录默认、1手机登录、2二维码登录、3注册、4忘记密码
currentPage: 0,
// 是否勾选了登录页的免登录
isRemembered: false,
// 登录页的免登录存储几天默认7天
loginDay: 7
}),
state: (): userType => {
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey);
console.log("[UserStore] 从 localStorage 读取用户信息:", userInfo);
return {
// 头像
avatar: userInfo?.avatar ?? "",
// 用户名
username: userInfo?.username ?? "",
// 昵称
nickname: userInfo?.nickname ?? "",
// 用户ID
userId: userInfo?.userId ?? "",
// 页面级别权限
roles: userInfo?.roles ?? [],
// 按钮级别权限
permissions: userInfo?.permissions ?? [],
// 前端生成的验证码(按实际需求替换)
verifyCode: "",
// 判断登录页面显示哪个组件0登录默认、1手机登录、2二维码登录、3注册、4忘记密码
currentPage: 0,
// 是否勾选了登录页的免登录
isRemembered: false,
// 登录页的免登录存储几天默认7天
loginDay: 7
};
},
actions: {
/** 存储头像 */
SET_AVATAR(avatar: string) {
@@ -51,6 +56,10 @@ export const useUserStore = defineStore("pure-user", {
SET_NICKNAME(nickname: string) {
this.nickname = nickname;
},
/** 存储用户ID */
SET_USERID(userId: string) {
this.userId = userId;
},
/** 存储角色 */
SET_ROLES(roles: Array<string>) {
this.roles = roles;
@@ -80,7 +89,8 @@ export const useUserStore = defineStore("pure-user", {
return new Promise<UserResult>((resolve, reject) => {
getLogin(data)
.then(data => {
if (data.code === 0) {
// 后端返回 code: 200 表示成功
if (data.code === 200 || data.code === 0) {
setToken(data.data);
resolve(data);
} else {
@@ -95,6 +105,8 @@ export const useUserStore = defineStore("pure-user", {
/** 前端登出(不调用接口) */
logOut() {
this.username = "";
this.nickname = "";
this.userId = "";
this.roles = [];
this.permissions = [];
removeToken();

View File

@@ -41,6 +41,7 @@ export type userType = {
avatar?: string;
username?: string;
nickname?: string;
userId?: string;
roles?: Array<string>;
permissions?: Array<string>;
verifyCode?: string;

View File

@@ -15,6 +15,8 @@ export interface DataInfo<T> {
username?: string;
/** 昵称 */
nickname?: string;
/** 用户ID */
userId?: string;
/** 当前登录用户的角色 */
roles?: Array<string>;
/** 当前登录用户的按钮级别权限 */
@@ -45,11 +47,22 @@ export function getToken(): DataInfo<number> {
* 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里过期自动销毁
* 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
*/
export function setToken(data: DataInfo<Date>) {
export function setToken(data: any) {
let expires = 0;
const { accessToken, refreshToken } = data;
// 适配后端返回的数据结构
// 后端返回: token, userId, realName, avatar, roles, permissions
// 内部使用: accessToken, refreshToken, expires
const accessToken = data.accessToken || data.token || "";
const refreshToken = data.refreshToken || "";
const { isRemembered, loginDay } = useUserStoreHook();
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳将此处代码改为expires = data.expires然后把上面的DataInfo<Date>改成DataInfo<number>即可
// 处理过期时间如果后端没有返回则默认7天
if (data.expires) {
expires = new Date(data.expires).getTime();
} else {
expires = Date.now() + 7 * 24 * 60 * 60 * 1000; // 默认7天
}
const cookieString = JSON.stringify({ accessToken, expires, refreshToken });
expires > 0
@@ -68,10 +81,18 @@ export function setToken(data: DataInfo<Date>) {
: {}
);
function setUserKey({ avatar, username, nickname, roles, permissions }) {
function setUserKey({
avatar,
username,
nickname,
userId,
roles,
permissions
}) {
useUserStoreHook().SET_AVATAR(avatar);
useUserStoreHook().SET_USERNAME(username);
useUserStoreHook().SET_NICKNAME(nickname);
useUserStoreHook().SET_USERID(userId);
useUserStoreHook().SET_ROLES(roles);
useUserStoreHook().SET_PERMS(permissions);
storageLocal().setItem(userKey, {
@@ -80,18 +101,19 @@ export function setToken(data: DataInfo<Date>) {
avatar,
username,
nickname,
userId,
roles,
permissions
});
}
if (data.username && data.roles) {
const { username, roles } = data;
setUserKey({
avatar: data?.avatar ?? "",
username,
nickname: data?.nickname ?? "",
roles,
username: data.username,
nickname: data?.nickname || data?.realName || "",
userId: data?.userId ?? "",
roles: data.roles,
permissions: data?.permissions ?? []
});
} else {
@@ -101,6 +123,8 @@ export function setToken(data: DataInfo<Date>) {
storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "";
const nickname =
storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "";
const userId =
storageLocal().getItem<DataInfo<number>>(userKey)?.userId ?? "";
const roles =
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
const permissions =
@@ -109,6 +133,7 @@ export function setToken(data: DataInfo<Date>) {
avatar,
username,
nickname,
userId,
roles,
permissions
});

297
src/utils/sse/SseClient.ts Normal file
View File

@@ -0,0 +1,297 @@
/**
* SSE 客户端类
* 用于与后端 SSE 服务进行对接,实现异步任务的实时进度推送
* 使用 fetch API 实现,支持自定义 Header包括鉴权
*/
import { getToken, formatToken } from "@/utils/auth";
export interface SseMessage {
type: string;
event: string;
userId: string;
data: any;
timestamp: string;
}
export interface ProjectInitTaskVO {
taskId: string;
status: string;
statusDesc: string;
progress: number;
progressMessage: string;
originalFilename: string;
createTime: string;
startTime: string;
completeTime: string;
result?: any;
errorMessage?: string;
}
type SseEventCallback = (data: any, message?: SseMessage) => void;
export class SseClient {
private userId: string;
private abortController: AbortController | null = null;
private listeners: Map<string, SseEventCallback[]> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 3;
private reconnectDelay = 3000;
private isManualClose = false;
private _isConnected = false;
constructor(userId: string) {
this.userId = userId;
}
/**
* 获取鉴权 Header
*/
private getAuthHeaders(): Record<string, string> {
const headers: Record<string, string> = {
Accept: "text/event-stream",
"Cache-Control": "no-cache"
};
const tokenData = getToken();
if (tokenData?.accessToken) {
headers["Authorization"] = formatToken(tokenData.accessToken);
}
return headers;
}
/**
* 建立 SSE 连接(使用 fetch API 支持自定义 Header
*/
async connect(): Promise<void> {
if (this.abortController) {
console.warn("SSE 连接已存在");
return;
}
this.isManualClose = false;
this.abortController = new AbortController();
const url = `/api/v1/sse/connect/${this.userId}`;
console.log("正在建立 SSE 连接...", url);
try {
const response = await fetch(url, {
method: "GET",
headers: this.getAuthHeaders(),
signal: this.abortController.signal
});
if (!response.ok) {
throw new Error(
`SSE 连接失败: ${response.status} ${response.statusText}`
);
}
if (!response.body) {
throw new Error("SSE 响应无 body");
}
this._isConnected = true;
this.reconnectAttempts = 0;
// 读取 SSE 流
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("SSE 流结束");
break;
}
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 消息(以双换行分隔)
const messages = buffer.split("\n\n");
buffer = messages.pop() || ""; // 保留未完成的消息
for (const msg of messages) {
if (msg.trim()) {
this.parseAndDispatch(msg);
}
}
}
} catch (error: any) {
if (error.name === "AbortError") {
console.log("SSE 连接已取消");
return;
}
console.error("SSE 连接错误:", error);
this._isConnected = false;
this.emit("connection-error", error);
// 自动重连
if (
!this.isManualClose &&
this.reconnectAttempts < this.maxReconnectAttempts
) {
this.reconnectAttempts++;
console.log(
`SSE 将在 ${this.reconnectDelay}ms 后重连 (第 ${this.reconnectAttempts} 次)`
);
setTimeout(() => {
this.close();
this.connect();
}, this.reconnectDelay);
} else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error("SSE 重连次数已达上限");
this.emit("max-reconnect-reached", null);
}
}
}
/**
* 解析并分发 SSE 消息
*/
private parseAndDispatch(rawMessage: string): void {
const lines = rawMessage.split("\n");
let eventName = "message";
let data = "";
for (const line of lines) {
if (line.startsWith("event:")) {
eventName = line.slice(6).trim();
} else if (line.startsWith("data:")) {
data = line.slice(5).trim();
}
}
if (!data) return;
try {
const message = JSON.parse(data) as SseMessage;
// 根据事件名称分发
switch (eventName) {
case "connected":
console.log("SSE 连接成功:", message);
this.emit("connected", message.data, message);
break;
case "submitted":
console.log("任务已提交:", message);
if (message.type === "project-init") {
this.emit("project-init-submitted", message.data, message);
}
break;
case "progress":
if (message.type === "project-init") {
this.emit("project-init-progress", message.data, message);
}
break;
case "complete":
if (message.type === "project-init") {
this.emit("project-init-complete", message.data, message);
}
break;
case "error":
console.error("SSE 错误事件:", message);
if (message.type === "project-init") {
this.emit("project-init-error", message.data, message);
}
this.emit("error", message.data, message);
break;
default:
console.log("SSE 未知事件:", eventName, message);
}
} catch (e) {
console.error("SSE 消息解析失败:", data, e);
}
}
/**
* 提交项目初始化任务SSE 方式)
*/
async submitProjectInitTask(
file: File
): Promise<{ code: number; data?: { taskId: string }; message?: string }> {
const formData = new FormData();
formData.append("userId", this.userId);
formData.append("file", file);
const tokenData = getToken();
const headers: Record<string, string> = {};
if (tokenData?.accessToken) {
headers["Authorization"] = formatToken(tokenData.accessToken);
}
const response = await fetch("/api/v1/project-init/sse/submit-task", {
method: "POST",
headers,
body: formData
});
return response.json();
}
/**
* 关闭 SSE 连接
*/
close(): void {
this.isManualClose = true;
this._isConnected = false;
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
console.log("SSE 连接已关闭");
}
}
/**
* 注册事件监听
*/
on(event: string, callback: SseEventCallback): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
/**
* 移除事件监听
*/
off(event: string, callback: SseEventCallback): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
/**
* 触发事件
*/
private emit(event: string, data: any, message?: SseMessage): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach(cb => cb(data, message));
}
}
/**
* 获取连接状态
*/
get isConnected(): boolean {
return this._isConnected;
}
/**
* 获取用户ID
*/
getUserId(): string {
return this.userId;
}
}
export default SseClient;

View File

@@ -155,8 +155,9 @@ const handleFeishuCallback = async (code: string) => {
avatar: data.avatar,
username: data.username,
nickname: data.realName,
roles: ["admin"], // 根据实际业务调整
permissions: []
userId: data.userId,
roles: data.roles || ["admin"],
permissions: data.permissions || []
});
// 保存用户信息到本地存储
@@ -166,8 +167,9 @@ const handleFeishuCallback = async (code: string) => {
avatar: data.avatar,
username: data.username,
nickname: data.realName,
roles: ["admin"],
permissions: []
userId: data.userId,
roles: data.roles || ["admin"],
permissions: data.permissions || []
});
// 更新全局状态
@@ -175,8 +177,9 @@ const handleFeishuCallback = async (code: string) => {
userStore.SET_AVATAR(data.avatar);
userStore.SET_USERNAME(data.username);
userStore.SET_NICKNAME(data.realName);
userStore.SET_ROLES(["admin"]);
userStore.SET_PERMS([]);
userStore.SET_USERID(data.userId);
userStore.SET_ROLES(data.roles || ["admin"]);
userStore.SET_PERMS(data.permissions || []);
// 初始化路由
await initRouter();

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, reactive, computed } from "vue";
import { ref, reactive, computed, watch } from "vue";
import { message } from "@/utils/message";
import { previewProjectInit, confirmProjectInit } from "@/api/project";
import { confirmProjectInit } from "@/api/project";
import type { ProjectInitResult } from "@/api/project";
import {
WizardStep,
@@ -9,11 +9,13 @@ import {
ProjectTypeOptions
} from "../utils/types";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { useSseStoreHook } from "@/store/modules/sse";
import UploadIcon from "~icons/ri/upload-cloud-line";
import FileIcon from "~icons/ri/file-line";
import CheckIcon from "~icons/ri/check-line";
import DeleteIcon from "~icons/ri/delete-bin-line";
import AddIcon from "~icons/ri/add-line";
import LoadingIcon from "~icons/ri/loader-4-line";
const props = defineProps<{
visible: boolean;
@@ -35,6 +37,31 @@ const saving = ref(false);
const uploadRef = ref();
const fileList = ref<File[]>([]);
// SSE Store
const sseStore = useSseStoreHook();
const taskProgress = computed(() => sseStore.taskProgress);
const taskStatus = computed(() => sseStore.taskStatus);
const currentTask = computed(() => sseStore.currentTask);
// 监听任务完成
watch(
() => sseStore.taskStatus,
newStatus => {
if (newStatus === "completed" && currentTask.value?.result) {
Object.assign(projectData, currentTask.value.result);
currentStep.value = WizardStep.Preview;
uploading.value = false;
message("文件解析成功", { type: "success" });
sseStore.resetTaskStatus();
} else if (newStatus === "error") {
uploading.value = false;
message(sseStore.errorMessage || "文件解析失败", { type: "error" });
sseStore.resetTaskStatus();
}
},
{ immediate: false }
);
// 项目初始化数据
const projectData = reactive<ProjectInitResult>({
project: {
@@ -117,24 +144,28 @@ function handleRemove() {
fileList.value = [];
}
// 上传并预览
// 上传并预览(使用 SSE
async function handleUploadAndPreview() {
if (fileList.value.length === 0) {
message("请先选择文件", { type: "warning" });
return;
}
if (!sseStore.getIsConnected) {
message("SSE 连接未建立,请刷新页面重试", { type: "warning" });
return;
}
uploading.value = true;
try {
const { code, data } = await previewProjectInit(fileList.value[0]);
if (code === 200 && data) {
Object.assign(projectData, data);
currentStep.value = WizardStep.Preview;
message("文件解析成功", { type: "success" });
const result = await sseStore.submitProjectInitTask(fileList.value[0]);
if (result.code !== 200) {
message(result.message || "提交任务失败", { type: "error" });
uploading.value = false;
}
// 进度和结果将通过 SSE 推送,由 watch 监听处理
} catch (error) {
message("文件解析失败,请检查文件格式", { type: "error" });
} finally {
message("文件上传失败,请检查文件格式", { type: "error" });
uploading.value = false;
}
}
@@ -300,14 +331,39 @@ function removeTag(tag: string) {
</template>
</el-upload>
<!-- 进度显示 -->
<div v-if="uploading" class="progress-container mt-6">
<div class="flex-c gap-2 mb-2">
<el-icon class="is-loading">
<component :is="useRenderIcon(LoadingIcon)" />
</el-icon>
<span class="text-sm text-gray-600">
{{
taskStatus === "submitted"
? "任务已提交,等待处理..."
: currentTask?.progressMessage || "正在解析..."
}}
</span>
</div>
<el-progress
:percentage="taskProgress"
:stroke-width="8"
:status="taskStatus === 'error' ? 'exception' : undefined"
class="w-full max-w-md mx-auto"
/>
<div class="text-center mt-2 text-xs text-gray-400">
进度: {{ taskProgress }}%
</div>
</div>
<div class="flex justify-center mt-6">
<el-button
type="primary"
:loading="uploading"
:disabled="fileList.length === 0"
:disabled="fileList.length === 0 || uploading"
@click="handleUploadAndPreview"
>
解析文件
{{ uploading ? "解析中..." : "解析文件" }}
</el-button>
</div>
</div>