feat(sse): 添加风险评估任务的SSE事件监听及状态管理
Some checks failed
Lint Code / Lint Code (push) Failing after 17m16s

- 在SseClient中新增RiskAssessTaskVO接口定义
- 在SseClient事件处理里支持风险评估相关事件推送处理
- 在sse模块中新增风险评估任务状态及进度等响应式变量
- 实现风险评估任务的事件监听逻辑,处理提交、进度、完成、错误事件
- 发送风险评估完成和错误的通知弹窗提示
- 添加重置风险评估状态的方法resetRiskAssessStatus
- 风险评估页面引入SSE Store,响应任务状态变化展示进度和提示信息
- 提交风险评估任务时确保SSE连接已建立
- 新增风险评估任务进行中的加载状态显示及进度条UI
- 监听风险评估完成和错误状态,自动加载数据并重置状态
- 优化风险评估接口调用参数类型转换和错误处理
- 生命周期钩子内移除窗口resize事件监听防止内存泄漏
This commit is contained in:
2026-03-30 15:23:19 +08:00
parent d75578b09a
commit 16e698ceab
3 changed files with 186 additions and 6 deletions

View File

@@ -1,6 +1,10 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { SseClient, type ProjectInitTaskVO } from "@/utils/sse/SseClient";
import {
SseClient,
type ProjectInitTaskVO,
type RiskAssessTaskVO
} from "@/utils/sse/SseClient";
import { store } from "../utils";
import {
getMyTasks as fetchTasksApi,
@@ -25,6 +29,14 @@ export const useSseStore = defineStore("sse", () => {
myTasks.value.some(t => t.status === "processing" || t.status === "pending")
);
// 风险评估任务状态
const riskAssessTask = ref<RiskAssessTaskVO | null>(null);
const riskAssessProgress = ref(0);
const riskAssessStatus = ref<
"idle" | "submitted" | "processing" | "completed" | "error"
>("idle");
const riskAssessErrorMessage = ref("");
// Getters
const getIsConnected = computed(() => isConnected.value);
const getCurrentTask = computed(() => currentTask.value);
@@ -115,6 +127,56 @@ export const useSseStore = defineStore("sse", () => {
isConnected.value = false;
});
// ============ 风险评估事件监听 ============
// 监听风险评估任务提交
sseClient.value.on("risk-assess-submitted", (data: any) => {
riskAssessStatus.value = "submitted";
console.log("SSE Store: 风险评估任务已提交", data);
});
// 监听风险评估进度
sseClient.value.on("risk-assess-progress", (data: RiskAssessTaskVO) => {
riskAssessTask.value = data;
riskAssessProgress.value = data.progress;
riskAssessStatus.value = "processing";
console.log(
`SSE Store: 风险评估进度 ${data.progress}% - ${data.progressMessage}`
);
});
// 监听风险评估完成
sseClient.value.on("risk-assess-complete", (data: RiskAssessTaskVO) => {
riskAssessTask.value = data;
riskAssessProgress.value = 100;
riskAssessStatus.value = "completed";
console.log("SSE Store: 风险评估完成", data.result);
// 发送通知
ElNotification({
title: "风险评估完成",
message: `项目风险评估已完成,已识别 ${data.result?.identifiedRisks?.length || 0} 个风险。`,
type: "success",
duration: 5000,
position: "top-right"
});
});
// 监听风险评估错误
sseClient.value.on("risk-assess-error", (data: { error: string }) => {
riskAssessStatus.value = "error";
riskAssessErrorMessage.value = data.error;
console.error("SSE Store: 风险评估错误", data.error);
// 发送错误通知
ElNotification({
title: "风险评估失败",
message: data.error || "风险评估失败,请重试",
type: "error",
duration: 5000,
position: "top-right"
});
});
// 建立连接(异步,在后台运行)
sseClient.value.connect().catch(err => {
console.error("SSE Store: 连接失败", err);
@@ -159,6 +221,16 @@ export const useSseStore = defineStore("sse", () => {
errorMessage.value = "";
}
/**
* 重置风险评估任务状态
*/
function resetRiskAssessStatus() {
riskAssessTask.value = null;
riskAssessProgress.value = 0;
riskAssessStatus.value = "idle";
riskAssessErrorMessage.value = "";
}
/**
* 查询我的任务列表
*/
@@ -182,6 +254,11 @@ export const useSseStore = defineStore("sse", () => {
taskStatus,
errorMessage,
myTasks,
// 风险评估状态
riskAssessTask,
riskAssessProgress,
riskAssessStatus,
riskAssessErrorMessage,
// Getters
getIsConnected,
getCurrentTask,
@@ -194,6 +271,7 @@ export const useSseStore = defineStore("sse", () => {
closeSse,
submitProjectInitTask,
resetTaskStatus,
resetRiskAssessStatus,
fetchMyTasks
};
});

View File

@@ -28,6 +28,23 @@ export interface ProjectInitTaskVO {
errorMessage?: string;
}
/** 风险评估任务VO */
export interface RiskAssessTaskVO {
taskId: string;
userId: string;
projectId: string;
projectName?: string;
status: string;
statusDesc: string;
progress: number;
progressMessage: string;
createTime: string;
startTime?: string;
completeTime?: string;
result?: any;
errorMessage?: string;
}
type SseEventCallback = (data: any, message?: SseMessage) => void;
export class SseClient {
@@ -181,22 +198,30 @@ export class SseClient {
console.log("任务已提交:", message);
if (message.type === "project-init") {
this.emit("project-init-submitted", message.data, message);
} else if (message.type === "risk-assess") {
this.emit("risk-assess-submitted", message.data, message);
}
break;
case "progress":
if (message.type === "project-init") {
this.emit("project-init-progress", message.data, message);
} else if (message.type === "risk-assess") {
this.emit("risk-assess-progress", message.data, message);
}
break;
case "complete":
if (message.type === "project-init") {
this.emit("project-init-complete", message.data, message);
} else if (message.type === "risk-assess") {
this.emit("risk-assess-complete", message.data, message);
}
break;
case "error":
console.error("SSE 错误事件:", message);
if (message.type === "project-init") {
this.emit("project-init-error", message.data, message);
} else if (message.type === "risk-assess") {
this.emit("risk-assess-error", message.data, message);
}
this.emit("error", message.data, message);
break;

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { ref, onMounted, computed, onUnmounted, watch } from "vue";
import { useRouter } from "vue-router";
import { useUserStoreHook } from "@/store/modules/user";
import { useSseStoreHook } from "@/store/modules/sse";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import {
getRiskList,
@@ -67,6 +69,34 @@ const trendPeriod = ref("month");
// 项目列表
const projectList = ref<ProjectItem[]>([]);
// SSE Store
const sseStore = useSseStoreHook();
const userStore = useUserStoreHook();
// 风险评估进度相关
const riskAssessProgress = computed(() => sseStore.riskAssessProgress);
const riskAssessStatus = computed(() => sseStore.riskAssessStatus);
const riskAssessTask = computed(() => sseStore.riskAssessTask);
const riskAssessErrorMessage = computed(() => sseStore.riskAssessErrorMessage);
const isAssessing = computed(
() =>
riskAssessStatus.value === "submitted" ||
riskAssessStatus.value === "processing"
);
// 监听风险评估完成
watch(riskAssessStatus, newStatus => {
if (newStatus === "completed") {
message("风险评估完成!", { type: "success" });
loadRiskList();
loadStatistics();
sseStore.resetRiskAssessStatus();
} else if (newStatus === "error") {
message(riskAssessErrorMessage.value || "风险评估失败", { type: "error" });
sseStore.resetRiskAssessStatus();
}
});
// 分页
const pagination = ref({
currentPage: 1,
@@ -473,16 +503,36 @@ async function handleCreate() {
message("请先选择项目", { type: "warning" });
return;
}
const userId = userStore.userId;
if (!userId) {
message("用户信息不存在", { type: "error" });
return;
}
// 确保SSE连接已建立
if (!sseStore.getIsConnected) {
sseStore.initSse(String(userId));
}
try {
const res = await submitRiskAssessment(queryParams.value.projectId);
const res = await submitRiskAssessment(Number(queryParams.value.projectId));
console.log("风险评估API响应:", res);
console.log("res.data:", res.data);
const responseData = res.data as any;
// 扁平化结构res.data 直接是 { code: 200, data: {...}, message: "..." }
if (responseData.code === 200) {
message("风险评估任务已提交AI正在分析中...", { type: "success" });
// 可以在这里启动SSE连接监听进度
} else {
console.error(
"响应code不是200:",
responseData.code,
responseData.message
);
message(responseData.message || "提交失败", { type: "error" });
}
} catch (error) {
console.error("风险评估请求异常:", error);
message("提交风险评估任务失败", { type: "error" });
}
}
@@ -534,6 +584,10 @@ onMounted(() => {
initTrendChart();
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
</script>
<template>
@@ -572,15 +626,38 @@ onMounted(() => {
</template>
筛选
</el-button>
<el-button type="primary" @click="handleCreate">
<el-button type="primary" :loading="isAssessing" @click="handleCreate">
<template #icon>
<component :is="useRenderIcon(AddIcon)" />
</template>
新建评估
{{ isAssessing ? "评估中..." : "新建评估" }}
</el-button>
</div>
</div>
<!-- AI风险评估进度显示 -->
<el-card v-if="isAssessing" class="mb-4" shadow="hover">
<div class="flex-c gap-4">
<el-icon class="is-loading text-primary" :size="24">
<component :is="useRenderIcon('ri:loader-4-line')" />
</el-icon>
<div class="flex-1">
<div class="flex-bc mb-2">
<span class="font-medium">AI 正在进行风险评估</span>
<span class="text-sm text-gray-500">{{ riskAssessProgress }}%</span>
</div>
<el-progress
:percentage="riskAssessProgress"
:stroke-width="8"
:show-text="false"
/>
<p class="text-sm text-gray-500 mt-2">
{{ riskAssessTask?.progressMessage || "正在分析项目数据..." }}
</p>
</div>
</div>
</el-card>
<!-- 统计卡片 -->
<el-row :gutter="16" class="mb-4">
<el-col