feat(riskAssessment): 新增基于SSE的风险评估异步任务功能
Some checks failed
Lint Code / Lint Code (push) Failing after 2m35s
Some checks failed
Lint Code / Lint Code (push) Failing after 2m35s
- 支持提交风险评估异步任务接口,返回任务ID和状态信息 - 提供查询我的风险评估任务列表、单个任务状态及结果的API封装 - 风险评估数据类型扩展,新增任务详情、结果、统计等VO定义 - 项目ID支持string类型,防止精度丢失,相关接口参数做兼容处理 - 风险列表增加项目筛选,实现项目列表加载及选择功能 - 风险统计数据处理兼容字符串字段,确保数据准确展示 - 风险等级命名调整:高风险细分为严重、高风险,中低风险合并处理 - 风险界面新增提交评估按钮,支持任务提交及结果加载提示 - 列表操作按钮更新,增加风险工单分配及标记完成功能 - 调整路由枚举,新增system路由rank,避免冲突
This commit is contained in:
@@ -90,7 +90,7 @@ export type CreateRiskRequest = {
|
|||||||
|
|
||||||
/** 风险查询参数 */
|
/** 风险查询参数 */
|
||||||
export type RiskQueryParams = {
|
export type RiskQueryParams = {
|
||||||
projectId?: number;
|
projectId?: number | string;
|
||||||
pageNum: number;
|
pageNum: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
category?: string;
|
category?: string;
|
||||||
@@ -379,3 +379,88 @@ export const getMyWorkOrderStatistics = () => {
|
|||||||
"/api/v1/workorder/my/statistics"
|
"/api/v1/workorder/my/statistics"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== SSE风险评估API ====================
|
||||||
|
|
||||||
|
/** 风险评估任务VO */
|
||||||
|
export type RiskAssessmentTaskVO = {
|
||||||
|
taskId: string;
|
||||||
|
userId?: number;
|
||||||
|
status: string; // pending, processing, completed, failed
|
||||||
|
statusDesc: string;
|
||||||
|
progress: number;
|
||||||
|
progressMessage: string;
|
||||||
|
projectId: number;
|
||||||
|
projectName: string;
|
||||||
|
createTime: string;
|
||||||
|
startTime?: string;
|
||||||
|
completeTime?: string;
|
||||||
|
result?: RiskAssessmentResultVO;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 风险评估结果VO */
|
||||||
|
export type RiskAssessmentResultVO = {
|
||||||
|
projectId: number;
|
||||||
|
projectName: string;
|
||||||
|
overallRiskLevel: string;
|
||||||
|
overallRiskScore: number;
|
||||||
|
assessmentSummary: string;
|
||||||
|
riskAreas: string[];
|
||||||
|
identifiedRisks: Array<{
|
||||||
|
riskName: string;
|
||||||
|
category: string;
|
||||||
|
probability: number;
|
||||||
|
impact: number;
|
||||||
|
mitigationPlan: string;
|
||||||
|
}>;
|
||||||
|
recommendations: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 任务统计信息 */
|
||||||
|
export type RiskTaskStats = {
|
||||||
|
total: number;
|
||||||
|
processing: number;
|
||||||
|
completed: number;
|
||||||
|
failed: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交异步风险评估任务 */
|
||||||
|
export const submitRiskAssessment = (projectId: number) => {
|
||||||
|
return http.request<Result<string>>(
|
||||||
|
"post",
|
||||||
|
`/api/v1/risk/sse/assess/${projectId}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询我的风险评估任务列表 */
|
||||||
|
export const getMyRiskAssessmentTasks = () => {
|
||||||
|
return http.request<Result<RiskAssessmentTaskVO[]>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/risk/sse/my-tasks"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询任务统计信息 */
|
||||||
|
export const getMyRiskTaskStats = () => {
|
||||||
|
return http.request<Result<RiskTaskStats>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/risk/sse/my-tasks/stats"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询单个任务状态 */
|
||||||
|
export const getRiskTaskStatus = (taskId: string) => {
|
||||||
|
return http.request<Result<RiskAssessmentTaskVO>>(
|
||||||
|
"get",
|
||||||
|
`/api/v1/risk/sse/task/${taskId}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取任务评估结果 */
|
||||||
|
export const getRiskTaskResult = (taskId: string) => {
|
||||||
|
return http.request<Result<RiskAssessmentResultVO>>(
|
||||||
|
"get",
|
||||||
|
`/api/v1/risk/sse/task/${taskId}/result`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以
|
|||||||
frame = 12,
|
frame = 12,
|
||||||
nested = 13,
|
nested = 13,
|
||||||
permission = 14,
|
permission = 14,
|
||||||
system = 15,
|
|
||||||
monitor = 16,
|
monitor = 16,
|
||||||
tabs = 17,
|
tabs = 17,
|
||||||
about = 18,
|
about = 18,
|
||||||
@@ -29,7 +28,8 @@ const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以
|
|||||||
mind = 26,
|
mind = 26,
|
||||||
guide = 27,
|
guide = 27,
|
||||||
menuoverflow = 28,
|
menuoverflow = 28,
|
||||||
riskWorkorder = 29;
|
riskWorkorder = 29,
|
||||||
|
system = 99;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
home,
|
home,
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
getRiskList,
|
getRiskList,
|
||||||
getRiskStatistics,
|
getRiskStatistics,
|
||||||
deleteRisk,
|
deleteRisk,
|
||||||
|
submitRiskAssessment,
|
||||||
type RiskVO,
|
type RiskVO,
|
||||||
type RiskStatisticsVO,
|
type RiskStatisticsVO,
|
||||||
type RiskQueryParams
|
type RiskQueryParams
|
||||||
} from "@/api/risk-workorder";
|
} from "@/api/risk-workorder";
|
||||||
|
import { getProjectList, type ProjectItem } from "@/api/project";
|
||||||
import { message } from "@/utils/message";
|
import { message } from "@/utils/message";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
@@ -62,6 +64,9 @@ const queryParams = ref<RiskQueryParams>({
|
|||||||
// 趋势周期
|
// 趋势周期
|
||||||
const trendPeriod = ref("month");
|
const trendPeriod = ref("month");
|
||||||
|
|
||||||
|
// 项目列表
|
||||||
|
const projectList = ref<ProjectItem[]>([]);
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
@@ -87,7 +92,7 @@ const statCards = computed(() => [
|
|||||||
bgColor: "#ecf5ff"
|
bgColor: "#ecf5ff"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "高风险",
|
title: "严重风险",
|
||||||
value: statistics.value.criticalCount || 0,
|
value: statistics.value.criticalCount || 0,
|
||||||
trend: "↑ 5% 较上月",
|
trend: "↑ 5% 较上月",
|
||||||
trendUp: true,
|
trendUp: true,
|
||||||
@@ -96,7 +101,7 @@ const statCards = computed(() => [
|
|||||||
bgColor: "#fef0f0"
|
bgColor: "#fef0f0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "中风险",
|
title: "高风险",
|
||||||
value: statistics.value.highCount || 0,
|
value: statistics.value.highCount || 0,
|
||||||
trend: "↓ 3% 较上月",
|
trend: "↓ 3% 较上月",
|
||||||
trendUp: false,
|
trendUp: false,
|
||||||
@@ -105,8 +110,9 @@ const statCards = computed(() => [
|
|||||||
bgColor: "#fdf6ec"
|
bgColor: "#fdf6ec"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "低风险",
|
title: "中低风险",
|
||||||
value: statistics.value.mediumCount + statistics.value.lowCount || 0,
|
value:
|
||||||
|
(statistics.value.mediumCount || 0) + (statistics.value.lowCount || 0),
|
||||||
trend: "↓ 2% 较上月",
|
trend: "↓ 2% 较上月",
|
||||||
trendUp: false,
|
trendUp: false,
|
||||||
icon: "ri:checkbox-circle-line",
|
icon: "ri:checkbox-circle-line",
|
||||||
@@ -202,16 +208,28 @@ function getStatusLabel(status?: string): string {
|
|||||||
async function loadRiskList() {
|
async function loadRiskList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getRiskList({
|
const params: any = {
|
||||||
...queryParams.value,
|
|
||||||
pageNum: pagination.value.currentPage,
|
pageNum: pagination.value.currentPage,
|
||||||
pageSize: pagination.value.pageSize
|
pageSize: pagination.value.pageSize,
|
||||||
});
|
keyword: queryParams.value.keyword,
|
||||||
const responseData = res.data as any;
|
category: queryParams.value.category,
|
||||||
if (responseData.code === 200 && responseData.data) {
|
riskLevel: queryParams.value.riskLevel,
|
||||||
const tableData = responseData.data;
|
status: queryParams.value.status
|
||||||
dataList.value = tableData.rows || [];
|
};
|
||||||
pagination.value.total = tableData.total || 0;
|
// projectId 转换为字符串避免精度丢失
|
||||||
|
if (queryParams.value.projectId) {
|
||||||
|
params.projectId = String(queryParams.value.projectId);
|
||||||
|
}
|
||||||
|
const res = await getRiskList(params);
|
||||||
|
// res.data 直接是业务数据 { total, rows, code, msg }
|
||||||
|
const businessData = res.data as any;
|
||||||
|
console.log("API业务数据:", businessData);
|
||||||
|
if (businessData.code === 200) {
|
||||||
|
const rows = businessData.rows || [];
|
||||||
|
dataList.value = rows;
|
||||||
|
// 处理字符串类型的total
|
||||||
|
pagination.value.total = parseInt(businessData.total) || rows.length || 0;
|
||||||
|
console.log("风险列表数据:", rows);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message("加载风险列表失败", { type: "error" });
|
message("加载风险列表失败", { type: "error" });
|
||||||
@@ -220,13 +238,46 @@ async function loadRiskList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载项目列表
|
||||||
|
async function loadProjectList() {
|
||||||
|
try {
|
||||||
|
const res = await getProjectList({ pageNum: 1, pageSize: 100 });
|
||||||
|
// res.data 直接是业务数据 { total, rows, code, msg }
|
||||||
|
const businessData = res.data as any;
|
||||||
|
if (businessData.code === 200) {
|
||||||
|
projectList.value = businessData.rows || [];
|
||||||
|
console.log("项目列表:", projectList.value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载项目列表失败", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载统计数据
|
// 加载统计数据
|
||||||
async function loadStatistics() {
|
async function loadStatistics() {
|
||||||
try {
|
try {
|
||||||
const res = await getRiskStatistics();
|
const res = await getRiskStatistics();
|
||||||
const statsResponse = res.data as any;
|
const statsResponse = res.data as any;
|
||||||
if (statsResponse.code === 200 && statsResponse.data) {
|
if (statsResponse.code === 200 && statsResponse.data) {
|
||||||
statistics.value = statsResponse.data;
|
const data = statsResponse.data;
|
||||||
|
// 处理字符串类型的统计数据
|
||||||
|
statistics.value = {
|
||||||
|
totalCount: parseInt(data.totalCount) || 0,
|
||||||
|
identifiedCount: parseInt(data.identifiedCount) || 0,
|
||||||
|
assignedCount: parseInt(data.assignedCount) || 0,
|
||||||
|
mitigatingCount: parseInt(data.mitigatingCount) || 0,
|
||||||
|
resolvedCount: parseInt(data.resolvedCount) || 0,
|
||||||
|
closedCount: parseInt(data.closedCount) || 0,
|
||||||
|
criticalCount: parseInt(data.criticalCount) || 0,
|
||||||
|
highCount: parseInt(data.highCount) || 0,
|
||||||
|
mediumCount: parseInt(data.mediumCount) || 0,
|
||||||
|
lowCount: parseInt(data.lowCount) || 0,
|
||||||
|
categoryStats: data.categoryStats || {},
|
||||||
|
levelStats: data.levelStats || {},
|
||||||
|
trendData: data.trendData || {},
|
||||||
|
averageRiskScore: parseFloat(data.averageRiskScore) || 0,
|
||||||
|
unresolvedHighCount: parseInt(data.unresolvedHighCount) || 0
|
||||||
|
};
|
||||||
updateCharts();
|
updateCharts();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -247,21 +298,25 @@ function updatePieChart() {
|
|||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
value: statistics.value.criticalCount || 0,
|
value: statistics.value.criticalCount || 0,
|
||||||
name: "高风险",
|
name: "严重",
|
||||||
itemStyle: { color: "#f56c6c" }
|
itemStyle: { color: "#f56c6c" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: statistics.value.highCount || 0,
|
value: statistics.value.highCount || 0,
|
||||||
name: "中风险",
|
name: "高",
|
||||||
itemStyle: { color: "#e6a23c" }
|
itemStyle: { color: "#e6a23c" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value:
|
value: statistics.value.mediumCount || 0,
|
||||||
(statistics.value.mediumCount || 0) + (statistics.value.lowCount || 0),
|
name: "中",
|
||||||
name: "低风险",
|
itemStyle: { color: "#409eff" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: statistics.value.lowCount || 0,
|
||||||
|
name: "低",
|
||||||
itemStyle: { color: "#67c23a" }
|
itemStyle: { color: "#67c23a" }
|
||||||
}
|
}
|
||||||
];
|
].filter(item => item.value > 0);
|
||||||
const option = {
|
const option = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "item",
|
trigger: "item",
|
||||||
@@ -412,9 +467,24 @@ function handleCurrentChange(val: number) {
|
|||||||
loadRiskList();
|
loadRiskList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新建风险
|
// 新建风险评估 - 使用SSE异步评估
|
||||||
function handleCreate() {
|
async function handleCreate() {
|
||||||
message("新建风险功能开发中", { type: "info" });
|
if (!queryParams.value.projectId) {
|
||||||
|
message("请先选择项目", { type: "warning" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await submitRiskAssessment(queryParams.value.projectId);
|
||||||
|
const responseData = res.data as any;
|
||||||
|
if (responseData.code === 200) {
|
||||||
|
message("风险评估任务已提交,AI正在分析中...", { type: "success" });
|
||||||
|
// 可以在这里启动SSE连接监听进度
|
||||||
|
} else {
|
||||||
|
message(responseData.message || "提交失败", { type: "error" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message("提交风险评估任务失败", { type: "error" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看风险详情
|
// 查看风险详情
|
||||||
@@ -440,6 +510,16 @@ async function handleDelete(row: RiskVO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分配工单
|
||||||
|
function handleAssignWorkOrder(row: RiskVO) {
|
||||||
|
message(`为风险分配工单: ${row.riskName}`, { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记已完成
|
||||||
|
function handleComplete(row: RiskVO) {
|
||||||
|
message(`标记风险已完成: ${row.riskName}`, { type: "success" });
|
||||||
|
}
|
||||||
|
|
||||||
// 窗口大小变化时重新渲染图表
|
// 窗口大小变化时重新渲染图表
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
pieChart?.resize();
|
pieChart?.resize();
|
||||||
@@ -447,6 +527,7 @@ function handleResize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadProjectList();
|
||||||
loadRiskList();
|
loadRiskList();
|
||||||
loadStatistics();
|
loadStatistics();
|
||||||
initPieChart();
|
initPieChart();
|
||||||
@@ -468,11 +549,17 @@ onMounted(() => {
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.projectId"
|
v-model="queryParams.projectId"
|
||||||
placeholder="所有项目"
|
placeholder="选择项目"
|
||||||
clearable
|
clearable
|
||||||
style="width: 140px"
|
style="width: 200px"
|
||||||
|
@change="onSearch"
|
||||||
>
|
>
|
||||||
<el-option label="所有项目" :value="undefined" />
|
<el-option
|
||||||
|
v-for="project in projectList"
|
||||||
|
:key="project.id"
|
||||||
|
:label="project.projectName"
|
||||||
|
:value="project.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select placeholder="最近30天" style="width: 120px">
|
<el-select placeholder="最近30天" style="width: 120px">
|
||||||
<el-option label="最近7天" value="7" />
|
<el-option label="最近7天" value="7" />
|
||||||
@@ -543,18 +630,27 @@ onMounted(() => {
|
|||||||
<div class="risk-legend">
|
<div class="risk-legend">
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<span class="legend-dot" style="background-color: #f56c6c" />
|
<span class="legend-dot" style="background-color: #f56c6c" />
|
||||||
<span>高风险</span>
|
<span>严重</span>
|
||||||
<span class="legend-value">19%</span>
|
<span class="legend-value">{{
|
||||||
|
statistics.criticalCount || 0
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<span class="legend-dot" style="background-color: #e6a23c" />
|
<span class="legend-dot" style="background-color: #e6a23c" />
|
||||||
<span>中风险</span>
|
<span>高</span>
|
||||||
<span class="legend-value">38%</span>
|
<span class="legend-value">{{ statistics.highCount || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #409eff" />
|
||||||
|
<span>中</span>
|
||||||
|
<span class="legend-value">{{
|
||||||
|
statistics.mediumCount || 0
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<span class="legend-dot" style="background-color: #67c23a" />
|
<span class="legend-dot" style="background-color: #67c23a" />
|
||||||
<span>低风险</span>
|
<span>低</span>
|
||||||
<span class="legend-value">43%</span>
|
<span class="legend-value">{{ statistics.lowCount || 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -657,14 +753,29 @@ onMounted(() => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="handleView(row)">
|
<el-button
|
||||||
<component :is="useRenderIcon(ViewIcon)" />
|
link
|
||||||
|
type="primary"
|
||||||
|
title="查看详情"
|
||||||
|
@click="handleView(row)"
|
||||||
|
>
|
||||||
|
<component :is="useRenderIcon('ri:eye-line')" />
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button link type="primary" @click="handleEdit(row)">
|
<el-button
|
||||||
<component :is="useRenderIcon(EditPenIcon)" />
|
link
|
||||||
|
type="primary"
|
||||||
|
title="分配工单"
|
||||||
|
@click="handleAssignWorkOrder(row)"
|
||||||
|
>
|
||||||
|
<component :is="useRenderIcon('ri:ticket-line')" />
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button link type="danger" @click="handleDelete(row)">
|
<el-button
|
||||||
<component :is="useRenderIcon(DeleteIcon)" />
|
link
|
||||||
|
type="success"
|
||||||
|
title="已完成"
|
||||||
|
@click="handleComplete(row)"
|
||||||
|
>
|
||||||
|
<component :is="useRenderIcon('ri:check-double-line')" />
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|||||||
Reference in New Issue
Block a user