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 { defineStore } from "pinia";
import { ref, computed } from "vue"; 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 { store } from "../utils";
import { import {
getMyTasks as fetchTasksApi, getMyTasks as fetchTasksApi,
@@ -25,6 +29,14 @@ export const useSseStore = defineStore("sse", () => {
myTasks.value.some(t => t.status === "processing" || t.status === "pending") 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 // Getters
const getIsConnected = computed(() => isConnected.value); const getIsConnected = computed(() => isConnected.value);
const getCurrentTask = computed(() => currentTask.value); const getCurrentTask = computed(() => currentTask.value);
@@ -115,6 +127,56 @@ export const useSseStore = defineStore("sse", () => {
isConnected.value = false; 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 => { sseClient.value.connect().catch(err => {
console.error("SSE Store: 连接失败", err); console.error("SSE Store: 连接失败", err);
@@ -159,6 +221,16 @@ export const useSseStore = defineStore("sse", () => {
errorMessage.value = ""; 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, taskStatus,
errorMessage, errorMessage,
myTasks, myTasks,
// 风险评估状态
riskAssessTask,
riskAssessProgress,
riskAssessStatus,
riskAssessErrorMessage,
// Getters // Getters
getIsConnected, getIsConnected,
getCurrentTask, getCurrentTask,
@@ -194,6 +271,7 @@ export const useSseStore = defineStore("sse", () => {
closeSse, closeSse,
submitProjectInitTask, submitProjectInitTask,
resetTaskStatus, resetTaskStatus,
resetRiskAssessStatus,
fetchMyTasks fetchMyTasks
}; };
}); });

View File

@@ -28,6 +28,23 @@ export interface ProjectInitTaskVO {
errorMessage?: string; 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; type SseEventCallback = (data: any, message?: SseMessage) => void;
export class SseClient { export class SseClient {
@@ -181,22 +198,30 @@ export class SseClient {
console.log("任务已提交:", message); console.log("任务已提交:", message);
if (message.type === "project-init") { if (message.type === "project-init") {
this.emit("project-init-submitted", message.data, message); this.emit("project-init-submitted", message.data, message);
} else if (message.type === "risk-assess") {
this.emit("risk-assess-submitted", message.data, message);
} }
break; break;
case "progress": case "progress":
if (message.type === "project-init") { if (message.type === "project-init") {
this.emit("project-init-progress", message.data, message); this.emit("project-init-progress", message.data, message);
} else if (message.type === "risk-assess") {
this.emit("risk-assess-progress", message.data, message);
} }
break; break;
case "complete": case "complete":
if (message.type === "project-init") { if (message.type === "project-init") {
this.emit("project-init-complete", message.data, message); this.emit("project-init-complete", message.data, message);
} else if (message.type === "risk-assess") {
this.emit("risk-assess-complete", message.data, message);
} }
break; break;
case "error": case "error":
console.error("SSE 错误事件:", message); console.error("SSE 错误事件:", message);
if (message.type === "project-init") { if (message.type === "project-init") {
this.emit("project-init-error", message.data, message); 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); this.emit("error", message.data, message);
break; break;

View File

@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed, onUnmounted, watch } from "vue";
import { useRouter } from "vue-router"; 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 { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { import {
getRiskList, getRiskList,
@@ -67,6 +69,34 @@ const trendPeriod = ref("month");
// 项目列表 // 项目列表
const projectList = ref<ProjectItem[]>([]); 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({ const pagination = ref({
currentPage: 1, currentPage: 1,
@@ -473,16 +503,36 @@ async function handleCreate() {
message("请先选择项目", { type: "warning" }); message("请先选择项目", { type: "warning" });
return; return;
} }
const userId = userStore.userId;
if (!userId) {
message("用户信息不存在", { type: "error" });
return;
}
// 确保SSE连接已建立
if (!sseStore.getIsConnected) {
sseStore.initSse(String(userId));
}
try { 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; const responseData = res.data as any;
// 扁平化结构res.data 直接是 { code: 200, data: {...}, message: "..." }
if (responseData.code === 200) { if (responseData.code === 200) {
message("风险评估任务已提交AI正在分析中...", { type: "success" }); message("风险评估任务已提交AI正在分析中...", { type: "success" });
// 可以在这里启动SSE连接监听进度
} else { } else {
console.error(
"响应code不是200:",
responseData.code,
responseData.message
);
message(responseData.message || "提交失败", { type: "error" }); message(responseData.message || "提交失败", { type: "error" });
} }
} catch (error) { } catch (error) {
console.error("风险评估请求异常:", error);
message("提交风险评估任务失败", { type: "error" }); message("提交风险评估任务失败", { type: "error" });
} }
} }
@@ -534,6 +584,10 @@ onMounted(() => {
initTrendChart(); initTrendChart();
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
}); });
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
</script> </script>
<template> <template>
@@ -572,15 +626,38 @@ onMounted(() => {
</template> </template>
筛选 筛选
</el-button> </el-button>
<el-button type="primary" @click="handleCreate"> <el-button type="primary" :loading="isAssessing" @click="handleCreate">
<template #icon> <template #icon>
<component :is="useRenderIcon(AddIcon)" /> <component :is="useRenderIcon(AddIcon)" />
</template> </template>
新建评估 {{ isAssessing ? "评估中..." : "新建评估" }}
</el-button> </el-button>
</div> </div>
</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-row :gutter="16" class="mb-4">
<el-col <el-col