- 新增风险与工单相关多语言菜单项(英文和中文) - 定义风险相关类型,包括风险分类、风险等级和状态等 - 定义工单相关类型,包括工单类型、优先级及状态等 - 实现风险评估创建、更新、删除、查询及统计接口 - 实现工单创建、更新、删除、查询、处理和分配接口 - 支持批量更新风险状态接口 - 新增我的工单列表及统计接口 - 提供统一的响应结果类型定义 - 更新OpenAPI规范文件以支持新增接口
This commit is contained in:
@@ -70,6 +70,9 @@ panel:
|
|||||||
menus:
|
menus:
|
||||||
pureHome: Home
|
pureHome: Home
|
||||||
pureProject: Project Management
|
pureProject: Project Management
|
||||||
|
pureRiskWorkorder: Risk & Workorder
|
||||||
|
pureRiskAssessment: Risk Assessment
|
||||||
|
pureWorkorderManagement: Workorder Management
|
||||||
pureLogin: Login
|
pureLogin: Login
|
||||||
pureEmpty: Empty Page
|
pureEmpty: Empty Page
|
||||||
pureTable: Table
|
pureTable: Table
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ panel:
|
|||||||
menus:
|
menus:
|
||||||
pureHome: 首页
|
pureHome: 首页
|
||||||
pureProject: 项目管理
|
pureProject: 项目管理
|
||||||
|
pureRiskWorkorder: 风险与工单
|
||||||
|
pureRiskAssessment: 风险评估
|
||||||
|
pureWorkorderManagement: 工单管理
|
||||||
pureLogin: 登录
|
pureLogin: 登录
|
||||||
pureEmpty: 无Layout页
|
pureEmpty: 无Layout页
|
||||||
pureTable: 表格
|
pureTable: 表格
|
||||||
|
|||||||
381
src/api/risk-workorder.ts
Normal file
381
src/api/risk-workorder.ts
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
import { http } from "@/utils/http";
|
||||||
|
|
||||||
|
/** 通用响应结果 */
|
||||||
|
type Result<T = any> = {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 风险相关类型定义 ====================
|
||||||
|
|
||||||
|
/** 风险分类 */
|
||||||
|
export type RiskCategory =
|
||||||
|
| "technical"
|
||||||
|
| "schedule"
|
||||||
|
| "cost"
|
||||||
|
| "quality"
|
||||||
|
| "resource"
|
||||||
|
| "external"
|
||||||
|
| "other";
|
||||||
|
|
||||||
|
/** 风险来源 */
|
||||||
|
export type RiskSource = "internal" | "external" | "manual";
|
||||||
|
|
||||||
|
/** 风险等级 */
|
||||||
|
export type RiskLevel = "critical" | "high" | "medium" | "low";
|
||||||
|
|
||||||
|
/** 风险状态 */
|
||||||
|
export type RiskStatus =
|
||||||
|
| "identified"
|
||||||
|
| "assigned"
|
||||||
|
| "mitigating"
|
||||||
|
| "resolved"
|
||||||
|
| "closed";
|
||||||
|
|
||||||
|
/** 风险VO */
|
||||||
|
export type RiskVO = {
|
||||||
|
id?: number;
|
||||||
|
riskCode?: string;
|
||||||
|
projectId?: number;
|
||||||
|
projectName?: string;
|
||||||
|
category?: RiskCategory;
|
||||||
|
riskName?: string;
|
||||||
|
description?: string;
|
||||||
|
riskSource?: RiskSource;
|
||||||
|
probability?: number;
|
||||||
|
impact?: number;
|
||||||
|
riskScore?: number;
|
||||||
|
riskLevel?: RiskLevel;
|
||||||
|
status?: RiskStatus;
|
||||||
|
ownerId?: number;
|
||||||
|
ownerName?: string;
|
||||||
|
ownerAvatar?: string;
|
||||||
|
workOrderCount?: number;
|
||||||
|
mitigationPlan?: string;
|
||||||
|
contingencyPlan?: string;
|
||||||
|
triggerCondition?: string;
|
||||||
|
discoverTime?: string;
|
||||||
|
dueDate?: string;
|
||||||
|
resolvedTime?: string;
|
||||||
|
tags?: string[];
|
||||||
|
createTime?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分页数据结构 */
|
||||||
|
export type TableDataInfo<T> = {
|
||||||
|
total: number;
|
||||||
|
rows: T[];
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 创建风险请求 */
|
||||||
|
export type CreateRiskRequest = {
|
||||||
|
projectId?: number;
|
||||||
|
category?: RiskCategory;
|
||||||
|
riskName?: string;
|
||||||
|
description?: string;
|
||||||
|
riskSource?: RiskSource;
|
||||||
|
probability?: number;
|
||||||
|
impact?: number;
|
||||||
|
ownerId?: number;
|
||||||
|
mitigationPlan?: string;
|
||||||
|
contingencyPlan?: string;
|
||||||
|
triggerCondition?: string;
|
||||||
|
dueDate?: string;
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 风险查询参数 */
|
||||||
|
export type RiskQueryParams = {
|
||||||
|
projectId?: number;
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
category?: string;
|
||||||
|
riskLevel?: string;
|
||||||
|
status?: string;
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 风险统计VO */
|
||||||
|
export type RiskStatisticsVO = {
|
||||||
|
totalCount: number;
|
||||||
|
identifiedCount: number;
|
||||||
|
assignedCount: number;
|
||||||
|
mitigatingCount: number;
|
||||||
|
resolvedCount: number;
|
||||||
|
closedCount: number;
|
||||||
|
criticalCount: number;
|
||||||
|
highCount: number;
|
||||||
|
mediumCount: number;
|
||||||
|
lowCount: number;
|
||||||
|
categoryStats: Record<string, number>;
|
||||||
|
levelStats: Record<string, number>;
|
||||||
|
trendData: Record<string, number[]>;
|
||||||
|
averageRiskScore: number;
|
||||||
|
unresolvedHighCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 工单相关类型定义 ====================
|
||||||
|
|
||||||
|
/** 工单类型 */
|
||||||
|
export type WorkOrderType =
|
||||||
|
| "bug"
|
||||||
|
| "feature"
|
||||||
|
| "task"
|
||||||
|
| "incident"
|
||||||
|
| "risk_handle"
|
||||||
|
| "other";
|
||||||
|
|
||||||
|
/** 工单优先级 */
|
||||||
|
export type WorkOrderPriority = "critical" | "high" | "medium" | "low";
|
||||||
|
|
||||||
|
/** 工单状态 */
|
||||||
|
export type WorkOrderStatus =
|
||||||
|
| "pending"
|
||||||
|
| "assigned"
|
||||||
|
| "processing"
|
||||||
|
| "resolved"
|
||||||
|
| "closed"
|
||||||
|
| "reopened";
|
||||||
|
|
||||||
|
/** 工单来源 */
|
||||||
|
export type WorkOrderSource = "web" | "mobile" | "api" | "system" | "risk";
|
||||||
|
|
||||||
|
/** 工单VO */
|
||||||
|
export type WorkOrderVO = {
|
||||||
|
id?: number;
|
||||||
|
orderCode?: string;
|
||||||
|
orderType?: WorkOrderType;
|
||||||
|
projectId?: number;
|
||||||
|
projectName?: string;
|
||||||
|
riskId?: number;
|
||||||
|
riskName?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
creatorId?: number;
|
||||||
|
creatorName?: string;
|
||||||
|
handlerId?: number;
|
||||||
|
handlerName?: string;
|
||||||
|
handlerAvatar?: string;
|
||||||
|
handlerGroupId?: number;
|
||||||
|
priority?: WorkOrderPriority;
|
||||||
|
status?: WorkOrderStatus;
|
||||||
|
source?: WorkOrderSource;
|
||||||
|
deadline?: string;
|
||||||
|
assignedTime?: string;
|
||||||
|
firstResponseTime?: string;
|
||||||
|
resolvedTime?: string;
|
||||||
|
closedTime?: string;
|
||||||
|
satisfactionScore?: number;
|
||||||
|
isOverdue?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
createTime?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 创建工单请求 */
|
||||||
|
export type CreateWorkOrderRequest = {
|
||||||
|
projectId?: number;
|
||||||
|
riskId?: number;
|
||||||
|
orderType?: WorkOrderType;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: WorkOrderPriority;
|
||||||
|
handlerId?: number;
|
||||||
|
handlerGroupId?: number;
|
||||||
|
deadline?: string;
|
||||||
|
source?: WorkOrderSource;
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 工单查询参数 */
|
||||||
|
export type WorkOrderQueryParams = {
|
||||||
|
projectId?: number;
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
orderType?: string;
|
||||||
|
status?: string;
|
||||||
|
priority?: string;
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 我的工单查询参数 */
|
||||||
|
export type MyWorkOrderQueryParams = {
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
status?: string;
|
||||||
|
orderType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理工单请求 */
|
||||||
|
export type ProcessWorkOrderRequest = {
|
||||||
|
workOrderId?: number;
|
||||||
|
status?: "processing" | "resolved" | "closed" | "reopened";
|
||||||
|
satisfactionScore?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 工单统计VO */
|
||||||
|
export type WorkOrderStatisticsVO = {
|
||||||
|
totalCount: number;
|
||||||
|
pendingCount: number;
|
||||||
|
assignedCount: number;
|
||||||
|
processingCount: number;
|
||||||
|
completedCount: number;
|
||||||
|
closedCount: number;
|
||||||
|
rejectedCount: number;
|
||||||
|
overdueCount: number;
|
||||||
|
aboutToExpireCount: number;
|
||||||
|
typeStats: Record<string, number>;
|
||||||
|
priorityStats: Record<string, number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 风险API ====================
|
||||||
|
|
||||||
|
/** 创建风险评估 */
|
||||||
|
export const createRisk = (data: CreateRiskRequest) => {
|
||||||
|
return http.request<Result<number>>("post", "/api/v1/risk", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 更新风险 */
|
||||||
|
export const updateRisk = (riskId: number, data: CreateRiskRequest) => {
|
||||||
|
return http.request<Result<boolean>>("put", `/api/v1/risk/${riskId}`, {
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除风险 */
|
||||||
|
export const deleteRisk = (riskId: number) => {
|
||||||
|
return http.request<Result<boolean>>("delete", `/api/v1/risk/${riskId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取风险详情 */
|
||||||
|
export const getRiskDetail = (riskId: number) => {
|
||||||
|
return http.request<Result<RiskVO>>("get", `/api/v1/risk/${riskId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分页查询风险列表 */
|
||||||
|
export const getRiskList = (params: RiskQueryParams) => {
|
||||||
|
return http.request<Result<TableDataInfo<RiskVO>>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/risk/list",
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取风险统计信息 */
|
||||||
|
export const getRiskStatistics = (projectId?: number) => {
|
||||||
|
return http.request<Result<RiskStatisticsVO>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/risk/statistics",
|
||||||
|
{ params: projectId ? { projectId } : undefined }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 为风险分配工单 */
|
||||||
|
export const assignWorkOrderToRisk = (
|
||||||
|
riskId: number,
|
||||||
|
data: CreateWorkOrderRequest
|
||||||
|
) => {
|
||||||
|
return http.request<Result<number>>(
|
||||||
|
"post",
|
||||||
|
`/api/v1/risk/${riskId}/assign-workorder`,
|
||||||
|
{ data }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 批量更新风险状态 */
|
||||||
|
export const batchUpdateRiskStatus = (riskIds: number[], status: string) => {
|
||||||
|
return http.request<Result<boolean>>("put", "/api/v1/risk/batch-status", {
|
||||||
|
params: { status },
|
||||||
|
data: riskIds
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 工单API ====================
|
||||||
|
|
||||||
|
/** 创建工单 */
|
||||||
|
export const createWorkOrder = (data: CreateWorkOrderRequest) => {
|
||||||
|
return http.request<Result<number>>("post", "/api/v1/workorder", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 更新工单 */
|
||||||
|
export const updateWorkOrder = (
|
||||||
|
workOrderId: number,
|
||||||
|
data: CreateWorkOrderRequest
|
||||||
|
) => {
|
||||||
|
return http.request<Result<boolean>>(
|
||||||
|
"put",
|
||||||
|
`/api/v1/workorder/${workOrderId}`,
|
||||||
|
{ data }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除工单 */
|
||||||
|
export const deleteWorkOrder = (workOrderId: number) => {
|
||||||
|
return http.request<Result<boolean>>(
|
||||||
|
"delete",
|
||||||
|
`/api/v1/workorder/${workOrderId}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取工单详情 */
|
||||||
|
export const getWorkOrderDetail = (workOrderId: number) => {
|
||||||
|
return http.request<Result<WorkOrderVO>>(
|
||||||
|
"get",
|
||||||
|
`/api/v1/workorder/${workOrderId}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分页查询工单列表 */
|
||||||
|
export const getWorkOrderList = (params: WorkOrderQueryParams) => {
|
||||||
|
return http.request<Result<TableDataInfo<WorkOrderVO>>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/workorder/list",
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取我的工单列表 */
|
||||||
|
export const getMyWorkOrderList = (params: MyWorkOrderQueryParams) => {
|
||||||
|
return http.request<Result<TableDataInfo<WorkOrderVO>>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/workorder/my",
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理工单 */
|
||||||
|
export const processWorkOrder = (data: ProcessWorkOrderRequest) => {
|
||||||
|
return http.request<Result<boolean>>("post", "/api/v1/workorder/process", {
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分配工单给处理人 */
|
||||||
|
export const assignWorkOrder = (workOrderId: number, handlerId: number) => {
|
||||||
|
return http.request<Result<boolean>>(
|
||||||
|
"put",
|
||||||
|
`/api/v1/workorder/${workOrderId}/assign`,
|
||||||
|
{ params: { handlerId } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取工单统计信息 */
|
||||||
|
export const getWorkOrderStatistics = (projectId?: number) => {
|
||||||
|
return http.request<Result<WorkOrderStatisticsVO>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/workorder/statistics",
|
||||||
|
{ params: projectId ? { projectId } : undefined }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取我的工单统计信息 */
|
||||||
|
export const getMyWorkOrderStatistics = () => {
|
||||||
|
return http.request<Result<WorkOrderStatisticsVO>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/workorder/my/statistics"
|
||||||
|
);
|
||||||
|
};
|
||||||
1657
src/api/风险与工单.openapi.json
Normal file
1657
src/api/风险与工单.openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,8 @@ const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以
|
|||||||
ppt = 25,
|
ppt = 25,
|
||||||
mind = 26,
|
mind = 26,
|
||||||
guide = 27,
|
guide = 27,
|
||||||
menuoverflow = 28;
|
menuoverflow = 28,
|
||||||
|
riskWorkorder = 29;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
home,
|
home,
|
||||||
@@ -59,5 +60,6 @@ export {
|
|||||||
ppt,
|
ppt,
|
||||||
mind,
|
mind,
|
||||||
guide,
|
guide,
|
||||||
menuoverflow
|
menuoverflow,
|
||||||
|
riskWorkorder
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,14 +38,15 @@ import {
|
|||||||
multipleTabsKey
|
multipleTabsKey
|
||||||
} from "@/utils/auth";
|
} from "@/utils/auth";
|
||||||
|
|
||||||
/** 只导入项目管理、系统管理和剩余路由模块
|
/** 导入项目所需的路由模块
|
||||||
* 其他路由模块已隐藏
|
|
||||||
*/
|
*/
|
||||||
const modules: Record<string, any> = import.meta.glob(
|
const modules: Record<string, any> = import.meta.glob(
|
||||||
[
|
[
|
||||||
"./modules/project.ts",
|
"./modules/project.ts",
|
||||||
"./modules/system.ts",
|
"./modules/system.ts",
|
||||||
"./modules/home.ts",
|
"./modules/home.ts",
|
||||||
|
"./modules/risk-assessment.ts",
|
||||||
|
"./modules/workorder-management.ts",
|
||||||
"!./modules/**/remaining.ts"
|
"!./modules/**/remaining.ts"
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|||||||
23
src/router/modules/risk-assessment.ts
Normal file
23
src/router/modules/risk-assessment.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { $t } from "@/plugins/i18n";
|
||||||
|
import { riskWorkorder } from "@/router/enums";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: "/risk-assessment",
|
||||||
|
redirect: "/risk-assessment/index",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:alert-line",
|
||||||
|
title: $t("menus.pureRiskAssessment"),
|
||||||
|
rank: riskWorkorder
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/risk-assessment/index",
|
||||||
|
name: "RiskAssessment",
|
||||||
|
component: () => import("@/views/risk-assessment/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: $t("menus.pureRiskAssessment"),
|
||||||
|
icon: "ri:alert-line"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} satisfies RouteConfigsTable;
|
||||||
23
src/router/modules/workorder-management.ts
Normal file
23
src/router/modules/workorder-management.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { $t } from "@/plugins/i18n";
|
||||||
|
import { riskWorkorder } from "@/router/enums";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: "/workorder-management",
|
||||||
|
redirect: "/workorder-management/index",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:ticket-line",
|
||||||
|
title: $t("menus.pureWorkorderManagement"),
|
||||||
|
rank: riskWorkorder + 1
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/workorder-management/index",
|
||||||
|
name: "WorkorderManagement",
|
||||||
|
component: () => import("@/views/workorder-management/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: $t("menus.pureWorkorderManagement"),
|
||||||
|
icon: "ri:ticket-line"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} satisfies RouteConfigsTable;
|
||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic";
|
import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic";
|
||||||
import { message } from "@/utils/message";
|
import { message } from "@/utils/message";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import isoWeek from "dayjs/plugin/isoWeek";
|
||||||
|
|
||||||
|
// 启用 isoWeek 插件,vue-ganttastic 周精度需要
|
||||||
|
dayjs.extend(isoWeek);
|
||||||
|
|
||||||
import ArrowLeftIcon from "~icons/ri/arrow-left-line";
|
import ArrowLeftIcon from "~icons/ri/arrow-left-line";
|
||||||
import DownloadIcon from "~icons/ri/download-line";
|
import DownloadIcon from "~icons/ri/download-line";
|
||||||
@@ -306,6 +310,18 @@ const ganttDateRange = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算项目时间跨度(天数)
|
||||||
|
const projectDays = computed(() => {
|
||||||
|
const start = dayjs(ganttDateRange.value.start);
|
||||||
|
const end = dayjs(ganttDateRange.value.end);
|
||||||
|
return end.diff(start, "day");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动态计算甘特图精度:超过两个月(60天)使用周精度,否则使用日精度
|
||||||
|
const ganttPrecision = computed(() => {
|
||||||
|
return projectDays.value > 60 ? "week" : "day";
|
||||||
|
});
|
||||||
|
|
||||||
// 获取项目详情
|
// 获取项目详情
|
||||||
async function fetchProjectDetail() {
|
async function fetchProjectDetail() {
|
||||||
if (!projectId.value) return;
|
if (!projectId.value) return;
|
||||||
@@ -609,7 +625,7 @@ onMounted(() => {
|
|||||||
<g-gantt-chart
|
<g-gantt-chart
|
||||||
:chart-start="ganttDateRange.start"
|
:chart-start="ganttDateRange.start"
|
||||||
:chart-end="ganttDateRange.end"
|
:chart-end="ganttDateRange.end"
|
||||||
precision="day"
|
:precision="ganttPrecision"
|
||||||
date-format="YYYY-MM-DD"
|
date-format="YYYY-MM-DD"
|
||||||
bar-start="startDate"
|
bar-start="startDate"
|
||||||
bar-end="endDate"
|
bar-end="endDate"
|
||||||
|
|||||||
738
src/views/risk-assessment/index.vue
Normal file
738
src/views/risk-assessment/index.vue
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import {
|
||||||
|
getRiskList,
|
||||||
|
getRiskStatistics,
|
||||||
|
deleteRisk,
|
||||||
|
type RiskVO,
|
||||||
|
type RiskStatisticsVO,
|
||||||
|
type RiskQueryParams
|
||||||
|
} from "@/api/risk-workorder";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
|
||||||
|
import AddIcon from "~icons/ri/add-line";
|
||||||
|
import SearchIcon from "~icons/ri/search-line";
|
||||||
|
import RefreshIcon from "~icons/ri/refresh-line";
|
||||||
|
import FilterIcon from "~icons/ri/filter-3-line";
|
||||||
|
import MoreIcon from "~icons/ep/more-filled";
|
||||||
|
import DeleteIcon from "~icons/ep/delete";
|
||||||
|
import EditPenIcon from "~icons/ep/edit-pen";
|
||||||
|
import ViewIcon from "~icons/ri/eye-line";
|
||||||
|
import WarningIcon from "~icons/ri/alert-line";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "RiskAssessmentIndex"
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const dataList = ref<RiskVO[]>([]);
|
||||||
|
const statistics = ref<RiskStatisticsVO>({
|
||||||
|
totalCount: 0,
|
||||||
|
identifiedCount: 0,
|
||||||
|
assignedCount: 0,
|
||||||
|
mitigatingCount: 0,
|
||||||
|
resolvedCount: 0,
|
||||||
|
closedCount: 0,
|
||||||
|
criticalCount: 0,
|
||||||
|
highCount: 0,
|
||||||
|
mediumCount: 0,
|
||||||
|
lowCount: 0,
|
||||||
|
categoryStats: {},
|
||||||
|
levelStats: {},
|
||||||
|
trendData: {},
|
||||||
|
averageRiskScore: 0,
|
||||||
|
unresolvedHighCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = ref<RiskQueryParams>({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: "",
|
||||||
|
category: "",
|
||||||
|
riskLevel: "",
|
||||||
|
status: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// 趋势周期
|
||||||
|
const trendPeriod = ref("month");
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = ref({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图表引用
|
||||||
|
const pieChartRef = ref<HTMLDivElement>();
|
||||||
|
const trendChartRef = ref<HTMLDivElement>();
|
||||||
|
let pieChart: echarts.ECharts | null = null;
|
||||||
|
let trendChart: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
// 统计卡片数据
|
||||||
|
const statCards = computed(() => [
|
||||||
|
{
|
||||||
|
title: "总风险数",
|
||||||
|
value: statistics.value.totalCount || 0,
|
||||||
|
trend: "↑ 12% 较上月",
|
||||||
|
trendUp: true,
|
||||||
|
icon: "ri:information-line",
|
||||||
|
color: "#409eff",
|
||||||
|
bgColor: "#ecf5ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "高风险",
|
||||||
|
value: statistics.value.criticalCount || 0,
|
||||||
|
trend: "↑ 5% 较上月",
|
||||||
|
trendUp: true,
|
||||||
|
icon: "ri:alert-line",
|
||||||
|
color: "#f56c6c",
|
||||||
|
bgColor: "#fef0f0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "中风险",
|
||||||
|
value: statistics.value.highCount || 0,
|
||||||
|
trend: "↓ 3% 较上月",
|
||||||
|
trendUp: false,
|
||||||
|
icon: "ri:error-warning-line",
|
||||||
|
color: "#e6a23c",
|
||||||
|
bgColor: "#fdf6ec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "低风险",
|
||||||
|
value: statistics.value.mediumCount + statistics.value.lowCount || 0,
|
||||||
|
trend: "↓ 2% 较上月",
|
||||||
|
trendUp: false,
|
||||||
|
icon: "ri:checkbox-circle-line",
|
||||||
|
color: "#67c23a",
|
||||||
|
bgColor: "#f0f9eb"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 风险等级选项
|
||||||
|
const riskLevelOptions = [
|
||||||
|
{ label: "严重", value: "critical", color: "#f56c6c" },
|
||||||
|
{ label: "高", value: "high", color: "#e6a23c" },
|
||||||
|
{ label: "中", value: "medium", color: "#409eff" },
|
||||||
|
{ label: "低", value: "low", color: "#67c23a" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 风险分类选项
|
||||||
|
const categoryOptions = [
|
||||||
|
{ label: "技术风险", value: "technical" },
|
||||||
|
{ label: "进度风险", value: "schedule" },
|
||||||
|
{ label: "成本风险", value: "cost" },
|
||||||
|
{ label: "质量风险", value: "quality" },
|
||||||
|
{ label: "资源风险", value: "resource" },
|
||||||
|
{ label: "外部风险", value: "external" },
|
||||||
|
{ label: "其他", value: "other" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: "已识别", value: "identified" },
|
||||||
|
{ label: "已分派", value: "assigned" },
|
||||||
|
{ label: "缓解中", value: "mitigating" },
|
||||||
|
{ label: "已解决", value: "resolved" },
|
||||||
|
{ label: "已关闭", value: "closed" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取风险等级标签类型
|
||||||
|
function getRiskLevelType(
|
||||||
|
level?: string
|
||||||
|
): "danger" | "warning" | "info" | "success" {
|
||||||
|
switch (level) {
|
||||||
|
case "critical":
|
||||||
|
return "danger";
|
||||||
|
case "high":
|
||||||
|
return "warning";
|
||||||
|
case "medium":
|
||||||
|
return "info";
|
||||||
|
case "low":
|
||||||
|
return "success";
|
||||||
|
default:
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取风险等级显示文本
|
||||||
|
function getRiskLevelLabel(level?: string): string {
|
||||||
|
const option = riskLevelOptions.find(o => o.value === level);
|
||||||
|
return option?.label || level || "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分类显示文本
|
||||||
|
function getCategoryLabel(category?: string): string {
|
||||||
|
const option = categoryOptions.find(o => o.value === category);
|
||||||
|
return option?.label || category || "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
function getStatusType(
|
||||||
|
status?: string
|
||||||
|
): "success" | "warning" | "info" | "primary" | "danger" {
|
||||||
|
switch (status) {
|
||||||
|
case "resolved":
|
||||||
|
case "closed":
|
||||||
|
return "success";
|
||||||
|
case "mitigating":
|
||||||
|
return "warning";
|
||||||
|
case "assigned":
|
||||||
|
return "primary";
|
||||||
|
case "identified":
|
||||||
|
return "info";
|
||||||
|
default:
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态显示文本
|
||||||
|
function getStatusLabel(status?: string): string {
|
||||||
|
const option = statusOptions.find(o => o.value === status);
|
||||||
|
return option?.label || status || "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载风险列表
|
||||||
|
async function loadRiskList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getRiskList({
|
||||||
|
...queryParams.value,
|
||||||
|
pageNum: pagination.value.currentPage,
|
||||||
|
pageSize: pagination.value.pageSize
|
||||||
|
});
|
||||||
|
const responseData = res.data as any;
|
||||||
|
if (responseData.code === 200 && responseData.data) {
|
||||||
|
const tableData = responseData.data;
|
||||||
|
dataList.value = tableData.rows || [];
|
||||||
|
pagination.value.total = tableData.total || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message("加载风险列表失败", { type: "error" });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载统计数据
|
||||||
|
async function loadStatistics() {
|
||||||
|
try {
|
||||||
|
const res = await getRiskStatistics();
|
||||||
|
const statsResponse = res.data as any;
|
||||||
|
if (statsResponse.code === 200 && statsResponse.data) {
|
||||||
|
statistics.value = statsResponse.data;
|
||||||
|
updateCharts();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载统计数据失败", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化饼图
|
||||||
|
function initPieChart() {
|
||||||
|
if (!pieChartRef.value) return;
|
||||||
|
pieChart = echarts.init(pieChartRef.value);
|
||||||
|
updatePieChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新饼图
|
||||||
|
function updatePieChart() {
|
||||||
|
if (!pieChart) return;
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: statistics.value.criticalCount || 0,
|
||||||
|
name: "高风险",
|
||||||
|
itemStyle: { color: "#f56c6c" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: statistics.value.highCount || 0,
|
||||||
|
name: "中风险",
|
||||||
|
itemStyle: { color: "#e6a23c" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
(statistics.value.mediumCount || 0) + (statistics.value.lowCount || 0),
|
||||||
|
name: "低风险",
|
||||||
|
itemStyle: { color: "#67c23a" }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: "{b}: {c} ({d}%)"
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
bottom: "5%",
|
||||||
|
left: "center",
|
||||||
|
itemWidth: 10,
|
||||||
|
itemHeight: 10,
|
||||||
|
textStyle: { fontSize: 12 }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "风险分布",
|
||||||
|
type: "pie",
|
||||||
|
radius: ["50%", "70%"],
|
||||||
|
center: ["50%", "45%"],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 6,
|
||||||
|
borderColor: "#fff",
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "bold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
pieChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化趋势图
|
||||||
|
function initTrendChart() {
|
||||||
|
if (!trendChartRef.value) return;
|
||||||
|
trendChart = echarts.init(trendChartRef.value);
|
||||||
|
updateTrendChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新趋势图
|
||||||
|
function updateTrendChart() {
|
||||||
|
if (!trendChart) return;
|
||||||
|
const months = ["1月", "2月", "3月", "4月", "5月", "6月"];
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
axisPointer: { type: "shadow" }
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ["高风险", "中风险", "低风险"],
|
||||||
|
right: 10,
|
||||||
|
top: 10,
|
||||||
|
itemWidth: 12,
|
||||||
|
itemHeight: 12
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: "3%",
|
||||||
|
right: "4%",
|
||||||
|
bottom: "3%",
|
||||||
|
top: "15%",
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: months,
|
||||||
|
axisLine: { lineStyle: { color: "#dcdfe6" } },
|
||||||
|
axisLabel: { color: "#606266" }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
axisLine: { show: false },
|
||||||
|
splitLine: { lineStyle: { color: "#ebeef5" } },
|
||||||
|
axisLabel: { color: "#606266" }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "高风险",
|
||||||
|
type: "bar",
|
||||||
|
stack: "total",
|
||||||
|
data: [5, 8, 6, 10, 12, 8],
|
||||||
|
itemStyle: { color: "#f56c6c", borderRadius: [0, 0, 4, 4] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "中风险",
|
||||||
|
type: "bar",
|
||||||
|
stack: "total",
|
||||||
|
data: [8, 12, 10, 15, 18, 16],
|
||||||
|
itemStyle: { color: "#e6a23c" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "低风险",
|
||||||
|
type: "bar",
|
||||||
|
stack: "total",
|
||||||
|
data: [10, 15, 12, 18, 20, 22],
|
||||||
|
itemStyle: { color: "#67c23a", borderRadius: [4, 4, 0, 0] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
trendChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
function updateCharts() {
|
||||||
|
updatePieChart();
|
||||||
|
updateTrendChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
function onSearch() {
|
||||||
|
pagination.value.currentPage = 1;
|
||||||
|
loadRiskList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
function resetForm() {
|
||||||
|
queryParams.value = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: "",
|
||||||
|
category: "",
|
||||||
|
riskLevel: "",
|
||||||
|
status: ""
|
||||||
|
};
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
pagination.value.pageSize = val;
|
||||||
|
loadRiskList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
function handleCurrentChange(val: number) {
|
||||||
|
pagination.value.currentPage = val;
|
||||||
|
loadRiskList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建风险
|
||||||
|
function handleCreate() {
|
||||||
|
message("新建风险功能开发中", { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看风险详情
|
||||||
|
function handleView(row: RiskVO) {
|
||||||
|
message(`查看风险: ${row.riskName}`, { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑风险
|
||||||
|
function handleEdit(row: RiskVO) {
|
||||||
|
message(`编辑风险: ${row.riskName}`, { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除风险
|
||||||
|
async function handleDelete(row: RiskVO) {
|
||||||
|
if (!row.id) return;
|
||||||
|
try {
|
||||||
|
await deleteRisk(row.id);
|
||||||
|
message("删除成功", { type: "success" });
|
||||||
|
loadRiskList();
|
||||||
|
loadStatistics();
|
||||||
|
} catch (error) {
|
||||||
|
message("删除失败", { type: "error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口大小变化时重新渲染图表
|
||||||
|
function handleResize() {
|
||||||
|
pieChart?.resize();
|
||||||
|
trendChart?.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadRiskList();
|
||||||
|
loadStatistics();
|
||||||
|
initPieChart();
|
||||||
|
initTrendChart();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="risk-assessment w-full">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="flex-bc mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold">风险评估</h2>
|
||||||
|
<p class="text-gray-500 text-sm mt-1">
|
||||||
|
根据项目资产资料进行风险评估,推送告警给相关人员
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.projectId"
|
||||||
|
placeholder="所有项目"
|
||||||
|
clearable
|
||||||
|
style="width: 140px"
|
||||||
|
>
|
||||||
|
<el-option label="所有项目" :value="undefined" />
|
||||||
|
</el-select>
|
||||||
|
<el-select placeholder="最近30天" style="width: 120px">
|
||||||
|
<el-option label="最近7天" value="7" />
|
||||||
|
<el-option label="最近30天" value="30" />
|
||||||
|
<el-option label="最近90天" value="90" />
|
||||||
|
</el-select>
|
||||||
|
<el-button>
|
||||||
|
<template #icon>
|
||||||
|
<component :is="useRenderIcon(FilterIcon)" />
|
||||||
|
</template>
|
||||||
|
筛选
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="handleCreate">
|
||||||
|
<template #icon>
|
||||||
|
<component :is="useRenderIcon(AddIcon)" />
|
||||||
|
</template>
|
||||||
|
新建评估
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col
|
||||||
|
v-for="(card, index) in statCards"
|
||||||
|
:key="index"
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
:md="6"
|
||||||
|
>
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="flex-bc">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500 text-sm">{{ card.title }}</p>
|
||||||
|
<p class="text-2xl font-bold mt-1" :style="{ color: card.color }">
|
||||||
|
{{ card.value }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-xs mt-1"
|
||||||
|
:class="card.trendUp ? 'text-red-500' : 'text-green-500'"
|
||||||
|
>
|
||||||
|
{{ card.trend }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon" :style="{ backgroundColor: card.bgColor }">
|
||||||
|
<el-icon :size="24" :color="card.color">
|
||||||
|
<component :is="useRenderIcon(card.icon)" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :xs="24" :md="8">
|
||||||
|
<el-card shadow="hover" class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<span class="font-medium">风险分布</span>
|
||||||
|
<el-button link>
|
||||||
|
<component :is="useRenderIcon(MoreIcon)" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="pieChartRef" class="chart-container" />
|
||||||
|
<div class="risk-legend">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #f56c6c" />
|
||||||
|
<span>高风险</span>
|
||||||
|
<span class="legend-value">19%</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #e6a23c" />
|
||||||
|
<span>中风险</span>
|
||||||
|
<span class="legend-value">38%</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #67c23a" />
|
||||||
|
<span>低风险</span>
|
||||||
|
<span class="legend-value">43%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :md="16">
|
||||||
|
<el-card shadow="hover" class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<span class="font-medium">风险趋势</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-radio-group v-model="trendPeriod" size="small">
|
||||||
|
<el-radio-button label="月度" value="month" />
|
||||||
|
<el-radio-button label="季度" value="quarter" />
|
||||||
|
<el-radio-button label="年度" value="year" />
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="trendChartRef" class="chart-container" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 风险告警列表 -->
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<span class="font-medium">风险告警列表</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.keyword"
|
||||||
|
placeholder="搜索风险..."
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="onSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<component :is="useRenderIcon(SearchIcon)" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="dataList" style="width: 100%">
|
||||||
|
<el-table-column label="风险名称" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-icon :size="20" color="#f56c6c">
|
||||||
|
<component :is="useRenderIcon(WarningIcon)" />
|
||||||
|
</el-icon>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ row.riskName }}</div>
|
||||||
|
<div class="text-xs text-gray-400">{{ row.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="项目" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.projectName || "-" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="风险等级" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getRiskLevelType(row.riskLevel)" size="small">
|
||||||
|
{{ getRiskLevelLabel(row.riskLevel) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="发现时间" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{
|
||||||
|
row.discoverTime
|
||||||
|
? dayjs(row.discoverTime).format("YYYY-MM-DD")
|
||||||
|
: "-"
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="负责人" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-avatar :size="24" :src="row.ownerAvatar">
|
||||||
|
{{ row.ownerName?.charAt(0) || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
<span>{{ row.ownerName || "未分配" }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="getStatusType(row.status)"
|
||||||
|
size="small"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
|
{{ getStatusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="handleView(row)">
|
||||||
|
<component :is="useRenderIcon(ViewIcon)" />
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="primary" @click="handleEdit(row)">
|
||||||
|
<component :is="useRenderIcon(EditPenIcon)" />
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" @click="handleDelete(row)">
|
||||||
|
<component :is="useRenderIcon(DeleteIcon)" />
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.currentPage"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.risk-assessment {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
.stat-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
.chart-container {
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-legend {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding-top: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
862
src/views/workorder-management/index.vue
Normal file
862
src/views/workorder-management/index.vue
Normal file
@@ -0,0 +1,862 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from "vue";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import {
|
||||||
|
getWorkOrderList,
|
||||||
|
getWorkOrderStatistics,
|
||||||
|
deleteWorkOrder,
|
||||||
|
type WorkOrderVO,
|
||||||
|
type WorkOrderStatisticsVO,
|
||||||
|
type WorkOrderQueryParams
|
||||||
|
} from "@/api/risk-workorder";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
|
||||||
|
import AddIcon from "~icons/ri/add-line";
|
||||||
|
import SearchIcon from "~icons/ri/search-line";
|
||||||
|
import RefreshIcon from "~icons/ri/refresh-line";
|
||||||
|
import FilterIcon from "~icons/ri/filter-3-line";
|
||||||
|
import MoreIcon from "~icons/ep/more-filled";
|
||||||
|
import DeleteIcon from "~icons/ep/delete";
|
||||||
|
import EditPenIcon from "~icons/ep/edit-pen";
|
||||||
|
import ViewIcon from "~icons/ri/eye-line";
|
||||||
|
import DownloadIcon from "~icons/ri/download-line";
|
||||||
|
import TicketIcon from "~icons/ri/ticket-line";
|
||||||
|
import TimeIcon from "~icons/ri/time-line";
|
||||||
|
import CheckLineIcon from "~icons/ri/check-line";
|
||||||
|
import LoaderIcon from "~icons/ri/loader-4-line";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "WorkorderManagementIndex"
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const dataList = ref<WorkOrderVO[]>([]);
|
||||||
|
const statistics = ref<WorkOrderStatisticsVO>({
|
||||||
|
totalCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
assignedCount: 0,
|
||||||
|
processingCount: 0,
|
||||||
|
completedCount: 0,
|
||||||
|
closedCount: 0,
|
||||||
|
rejectedCount: 0,
|
||||||
|
overdueCount: 0,
|
||||||
|
aboutToExpireCount: 0,
|
||||||
|
typeStats: {},
|
||||||
|
priorityStats: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = ref<WorkOrderQueryParams>({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: "",
|
||||||
|
orderType: "",
|
||||||
|
status: "",
|
||||||
|
priority: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = ref({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图表引用
|
||||||
|
const statusChartRef = ref<HTMLDivElement>();
|
||||||
|
const priorityChartRef = ref<HTMLDivElement>();
|
||||||
|
const durationChartRef = ref<HTMLDivElement>();
|
||||||
|
let statusChart: echarts.ECharts | null = null;
|
||||||
|
let priorityChart: echarts.ECharts | null = null;
|
||||||
|
let durationChart: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
// 统计卡片数据
|
||||||
|
const statCards = computed(() => [
|
||||||
|
{
|
||||||
|
title: "总工单",
|
||||||
|
value: statistics.value.totalCount || 0,
|
||||||
|
subText: "↑ 12% 较上月",
|
||||||
|
subTextColor: "text-green-500",
|
||||||
|
icon: "ri:ticket-line",
|
||||||
|
color: "#409eff",
|
||||||
|
bgColor: "#ecf5ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "待处理",
|
||||||
|
value: statistics.value.pendingCount || 0,
|
||||||
|
subText: `${statistics.value.aboutToExpireCount || 0} 个即将超期`,
|
||||||
|
subTextColor: "text-orange-500",
|
||||||
|
icon: "ri:time-line",
|
||||||
|
color: "#e6a23c",
|
||||||
|
bgColor: "#fdf6ec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "处理中",
|
||||||
|
value: statistics.value.processingCount || 0,
|
||||||
|
subText: "进行中",
|
||||||
|
subTextColor: "text-blue-500",
|
||||||
|
icon: "ri:loader-4-line",
|
||||||
|
color: "#409eff",
|
||||||
|
bgColor: "#ecf5ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "已完成",
|
||||||
|
value: statistics.value.completedCount || 0,
|
||||||
|
subText: `完成率 ${statistics.value.totalCount ? Math.round((statistics.value.completedCount / statistics.value.totalCount) * 100) : 0}%`,
|
||||||
|
subTextColor: "text-green-500",
|
||||||
|
icon: "ri:check-line",
|
||||||
|
color: "#67c23a",
|
||||||
|
bgColor: "#f0f9eb"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 工单类型选项
|
||||||
|
const orderTypeOptions = [
|
||||||
|
{ label: "缺陷", value: "bug" },
|
||||||
|
{ label: "需求", value: "feature" },
|
||||||
|
{ label: "任务", value: "task" },
|
||||||
|
{ label: "事件", value: "incident" },
|
||||||
|
{ label: "风险处理", value: "risk_handle" },
|
||||||
|
{ label: "其他", value: "other" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 优先级选项
|
||||||
|
const priorityOptions = [
|
||||||
|
{ label: "紧急", value: "critical", color: "#f56c6c" },
|
||||||
|
{ label: "高", value: "high", color: "#e6a23c" },
|
||||||
|
{ label: "中", value: "medium", color: "#409eff" },
|
||||||
|
{ label: "低", value: "low", color: "#909399" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: "待处理", value: "pending" },
|
||||||
|
{ label: "已分派", value: "assigned" },
|
||||||
|
{ label: "处理中", value: "processing" },
|
||||||
|
{ label: "已解决", value: "resolved" },
|
||||||
|
{ label: "已关闭", value: "closed" },
|
||||||
|
{ label: "已重开", value: "reopened" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取优先级标签类型
|
||||||
|
function getPriorityType(
|
||||||
|
priority?: string
|
||||||
|
): "danger" | "warning" | "info" | "success" {
|
||||||
|
switch (priority) {
|
||||||
|
case "critical":
|
||||||
|
return "danger";
|
||||||
|
case "high":
|
||||||
|
return "warning";
|
||||||
|
case "medium":
|
||||||
|
return "info";
|
||||||
|
case "low":
|
||||||
|
return "success";
|
||||||
|
default:
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取优先级显示文本
|
||||||
|
function getPriorityLabel(priority?: string): string {
|
||||||
|
const option = priorityOptions.find(o => o.value === priority);
|
||||||
|
return option?.label || priority || "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
function getStatusType(
|
||||||
|
status?: string
|
||||||
|
): "success" | "warning" | "info" | "primary" | "danger" {
|
||||||
|
switch (status) {
|
||||||
|
case "resolved":
|
||||||
|
case "closed":
|
||||||
|
return "success";
|
||||||
|
case "processing":
|
||||||
|
return "warning";
|
||||||
|
case "assigned":
|
||||||
|
return "primary";
|
||||||
|
case "pending":
|
||||||
|
return "info";
|
||||||
|
case "reopened":
|
||||||
|
return "danger";
|
||||||
|
default:
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态显示文本
|
||||||
|
function getStatusLabel(status?: string): string {
|
||||||
|
const option = statusOptions.find(o => o.value === status);
|
||||||
|
return option?.label || status || "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取类型显示文本
|
||||||
|
function getTypeLabel(type?: string): string {
|
||||||
|
const option = orderTypeOptions.find(o => o.value === type);
|
||||||
|
return option?.label || type || "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载工单列表
|
||||||
|
async function loadWorkOrderList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getWorkOrderList({
|
||||||
|
...queryParams.value,
|
||||||
|
pageNum: pagination.value.currentPage,
|
||||||
|
pageSize: pagination.value.pageSize
|
||||||
|
});
|
||||||
|
const responseData = res.data as any;
|
||||||
|
if (responseData.code === 200 && responseData.data) {
|
||||||
|
const tableData = responseData.data;
|
||||||
|
dataList.value = tableData.rows || [];
|
||||||
|
pagination.value.total = tableData.total || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message("加载工单列表失败", { type: "error" });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载统计数据
|
||||||
|
async function loadStatistics() {
|
||||||
|
try {
|
||||||
|
const res = await getWorkOrderStatistics();
|
||||||
|
const statsResponse = res.data as any;
|
||||||
|
if (statsResponse.code === 200 && statsResponse.data) {
|
||||||
|
statistics.value = statsResponse.data;
|
||||||
|
updateCharts();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载统计数据失败", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化状态分布饼图
|
||||||
|
function initStatusChart() {
|
||||||
|
if (!statusChartRef.value) return;
|
||||||
|
statusChart = echarts.init(statusChartRef.value);
|
||||||
|
updateStatusChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态分布饼图
|
||||||
|
function updateStatusChart() {
|
||||||
|
if (!statusChart) return;
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: statistics.value.pendingCount || 0,
|
||||||
|
name: "待处理",
|
||||||
|
itemStyle: { color: "#409eff" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: statistics.value.processingCount || 0,
|
||||||
|
name: "处理中",
|
||||||
|
itemStyle: { color: "#e6a23c" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: statistics.value.completedCount || 0,
|
||||||
|
name: "已完成",
|
||||||
|
itemStyle: { color: "#67c23a" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: statistics.value.closedCount || 0,
|
||||||
|
name: "已关闭",
|
||||||
|
itemStyle: { color: "#909399" }
|
||||||
|
}
|
||||||
|
].filter(item => item.value > 0);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: "{b}: {c} ({d}%)"
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "工单状态",
|
||||||
|
type: "pie",
|
||||||
|
radius: "65%",
|
||||||
|
center: ["50%", "50%"],
|
||||||
|
data,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: "rgba(0, 0, 0, 0.5)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
statusChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化优先级分布饼图
|
||||||
|
function initPriorityChart() {
|
||||||
|
if (!priorityChartRef.value) return;
|
||||||
|
priorityChart = echarts.init(priorityChartRef.value);
|
||||||
|
updatePriorityChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新优先级分布饼图
|
||||||
|
function updatePriorityChart() {
|
||||||
|
if (!priorityChart) return;
|
||||||
|
const priorityStats = statistics.value.priorityStats || {};
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: priorityStats.critical || 0,
|
||||||
|
name: "紧急",
|
||||||
|
itemStyle: { color: "#f56c6c" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: priorityStats.high || 0,
|
||||||
|
name: "高",
|
||||||
|
itemStyle: { color: "#e6a23c" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: priorityStats.medium || 0,
|
||||||
|
name: "中",
|
||||||
|
itemStyle: { color: "#409eff" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: priorityStats.low || 0,
|
||||||
|
name: "低",
|
||||||
|
itemStyle: { color: "#909399" }
|
||||||
|
}
|
||||||
|
].filter(item => item.value > 0);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: "{b}: {c} ({d}%)"
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "优先级",
|
||||||
|
type: "pie",
|
||||||
|
radius: "65%",
|
||||||
|
center: ["50%", "50%"],
|
||||||
|
data,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: "rgba(0, 0, 0, 0.5)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
priorityChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化平均处理时长折线图
|
||||||
|
function initDurationChart() {
|
||||||
|
if (!durationChartRef.value) return;
|
||||||
|
durationChart = echarts.init(durationChartRef.value);
|
||||||
|
updateDurationChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新平均处理时长折线图
|
||||||
|
function updateDurationChart() {
|
||||||
|
if (!durationChart) return;
|
||||||
|
const months = ["1月", "2月", "3月", "4月", "5月", "6月"];
|
||||||
|
const data = [4.5, 4.2, 3.8, 3.5, 3.2, 2.8];
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
formatter: "{b}: {c} 天"
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: "3%",
|
||||||
|
right: "4%",
|
||||||
|
bottom: "3%",
|
||||||
|
top: "10%",
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: months,
|
||||||
|
axisLine: { lineStyle: { color: "#dcdfe6" } },
|
||||||
|
axisLabel: { color: "#606266" }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
name: "天",
|
||||||
|
min: 0,
|
||||||
|
max: 5,
|
||||||
|
axisLine: { show: false },
|
||||||
|
splitLine: { lineStyle: { color: "#ebeef5" } },
|
||||||
|
axisLabel: { color: "#606266" }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "平均处理时长",
|
||||||
|
type: "line",
|
||||||
|
data,
|
||||||
|
smooth: true,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#409eff",
|
||||||
|
width: 2
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: "#409eff"
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "rgba(64, 158, 255, 0.3)" },
|
||||||
|
{ offset: 1, color: "rgba(64, 158, 255, 0.05)" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
durationChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
function updateCharts() {
|
||||||
|
updateStatusChart();
|
||||||
|
updatePriorityChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
function onSearch() {
|
||||||
|
pagination.value.currentPage = 1;
|
||||||
|
loadWorkOrderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
function resetForm() {
|
||||||
|
queryParams.value = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: "",
|
||||||
|
orderType: "",
|
||||||
|
status: "",
|
||||||
|
priority: ""
|
||||||
|
};
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
pagination.value.pageSize = val;
|
||||||
|
loadWorkOrderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
function handleCurrentChange(val: number) {
|
||||||
|
pagination.value.currentPage = val;
|
||||||
|
loadWorkOrderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建工单
|
||||||
|
function handleCreate() {
|
||||||
|
message("新建工单功能开发中", { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出工单
|
||||||
|
function handleExport() {
|
||||||
|
message("导出功能开发中", { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看工单详情
|
||||||
|
function handleView(row: WorkOrderVO) {
|
||||||
|
message(`查看工单: ${row.title}`, { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑工单
|
||||||
|
function handleEdit(row: WorkOrderVO) {
|
||||||
|
message(`编辑工单: ${row.title}`, { type: "info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除工单
|
||||||
|
async function handleDelete(row: WorkOrderVO) {
|
||||||
|
if (!row.id) return;
|
||||||
|
try {
|
||||||
|
await deleteWorkOrder(row.id);
|
||||||
|
message("删除成功", { type: "success" });
|
||||||
|
loadWorkOrderList();
|
||||||
|
loadStatistics();
|
||||||
|
} catch (error) {
|
||||||
|
message("删除失败", { type: "error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口大小变化时重新渲染图表
|
||||||
|
function handleResize() {
|
||||||
|
statusChart?.resize();
|
||||||
|
priorityChart?.resize();
|
||||||
|
durationChart?.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadWorkOrderList();
|
||||||
|
loadStatistics();
|
||||||
|
initStatusChart();
|
||||||
|
initPriorityChart();
|
||||||
|
initDurationChart();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="workorder-management w-full">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="flex-bc mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold">工单管理</h2>
|
||||||
|
<p class="text-gray-500 text-sm mt-1">
|
||||||
|
分发工单给相关人员,跟踪整改进度
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-button type="primary" @click="handleCreate">
|
||||||
|
<template #icon>
|
||||||
|
<component :is="useRenderIcon(AddIcon)" />
|
||||||
|
</template>
|
||||||
|
创建工单
|
||||||
|
</el-button>
|
||||||
|
<el-button>
|
||||||
|
<template #icon>
|
||||||
|
<component :is="useRenderIcon(FilterIcon)" />
|
||||||
|
</template>
|
||||||
|
筛选
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleExport">
|
||||||
|
<template #icon>
|
||||||
|
<component :is="useRenderIcon(DownloadIcon)" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col
|
||||||
|
v-for="(card, index) in statCards"
|
||||||
|
:key="index"
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
:md="6"
|
||||||
|
>
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="flex-bc">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500 text-sm">{{ card.title }}</p>
|
||||||
|
<p class="text-2xl font-bold mt-1" :style="{ color: card.color }">
|
||||||
|
{{ card.value }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mt-1" :class="card.subTextColor">
|
||||||
|
{{ card.subText }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon" :style="{ backgroundColor: card.bgColor }">
|
||||||
|
<el-icon :size="24" :color="card.color">
|
||||||
|
<component :is="useRenderIcon(card.icon)" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :xs="24" :md="8">
|
||||||
|
<el-card shadow="hover" class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<span class="font-medium">工单状态分布</span>
|
||||||
|
<el-button link>
|
||||||
|
<component :is="useRenderIcon(MoreIcon)" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="statusChartRef" class="chart-container" />
|
||||||
|
<div class="chart-legend">
|
||||||
|
<div class="legend-row">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #409eff" />
|
||||||
|
<span>待处理</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #e6a23c" />
|
||||||
|
<span>处理中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="legend-row">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #67c23a" />
|
||||||
|
<span>已完成</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #909399" />
|
||||||
|
<span>已逾期</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :md="8">
|
||||||
|
<el-card shadow="hover" class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<span class="font-medium">工单优先级分布</span>
|
||||||
|
<el-button link>
|
||||||
|
<component :is="useRenderIcon(MoreIcon)" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="priorityChartRef" class="chart-container" />
|
||||||
|
<div class="chart-legend">
|
||||||
|
<div class="legend-row">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #f56c6c" />
|
||||||
|
<span>紧急</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #e6a23c" />
|
||||||
|
<span>高</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="legend-row">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #409eff" />
|
||||||
|
<span>中</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #909399" />
|
||||||
|
<span>低</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :md="8">
|
||||||
|
<el-card shadow="hover" class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<span class="font-medium">平均处理时长</span>
|
||||||
|
<el-button link>
|
||||||
|
<component :is="useRenderIcon(MoreIcon)" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="durationChartRef" class="chart-container" />
|
||||||
|
<div class="duration-info">
|
||||||
|
<p class="text-gray-500 text-sm">平均处理时长</p>
|
||||||
|
<p class="text-3xl font-bold text-blue-500">2.8 天</p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 工单列表 -->
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<span class="font-medium">工单列表</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.keyword"
|
||||||
|
placeholder="搜索工单..."
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="onSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<component :is="useRenderIcon(SearchIcon)" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="dataList" style="width: 100%">
|
||||||
|
<el-table-column label="工单编号" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-mono">{{ row.orderCode || "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="项目名称" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="size-8 rounded bg-blue-100 flex-c">
|
||||||
|
<el-icon color="#409eff"
|
||||||
|
><component :is="useRenderIcon('ri:building-line')"
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
<span>{{ row.projectName || "-" }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="工单标题" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="font-medium">{{ row.title }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="负责人" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-avatar :size="24" :src="row.handlerAvatar">
|
||||||
|
{{ row.handlerName?.charAt(0) || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
<span>{{ row.handlerName || "未分配" }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="优先级" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="getPriorityType(row.priority)"
|
||||||
|
size="small"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
|
{{ getPriorityLabel(row.priority) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="getStatusType(row.status)"
|
||||||
|
size="small"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
|
{{ getStatusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{
|
||||||
|
row.createTime ? dayjs(row.createTime).format("YYYY-MM-DD") : "-"
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="截止时间" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="{ 'text-red-500': row.isOverdue }">
|
||||||
|
{{
|
||||||
|
row.deadline ? dayjs(row.deadline).format("YYYY-MM-DD") : "-"
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="handleView(row)">
|
||||||
|
<component :is="useRenderIcon(ViewIcon)" />
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="primary" @click="handleEdit(row)">
|
||||||
|
<component :is="useRenderIcon(EditPenIcon)" />
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<el-button link>
|
||||||
|
<component :is="useRenderIcon(MoreIcon)" />
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="handleDelete(row)">
|
||||||
|
<span class="text-red-500">删除</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.currentPage"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
background
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.workorder-management {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
.stat-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
.chart-container {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-legend {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.legend-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-info {
|
||||||
|
padding-top: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user