feat(api): 新增风险与工单管理接口及多语言支持
Some checks failed
Lint Code / Lint Code (push) Failing after 9m13s

- 新增风险与工单相关多语言菜单项(英文和中文)
- 定义风险相关类型,包括风险分类、风险等级和状态等
- 定义工单相关类型,包括工单类型、优先级及状态等
- 实现风险评估创建、更新、删除、查询及统计接口
- 实现工单创建、更新、删除、查询、处理和分配接口
- 支持批量更新风险状态接口
- 新增我的工单列表及统计接口
- 提供统一的响应结果类型定义
- 更新OpenAPI规范文件以支持新增接口
This commit is contained in:
2026-03-30 14:20:01 +08:00
parent 6f3192cf9a
commit 16f466f666
11 changed files with 3714 additions and 5 deletions

View File

@@ -70,6 +70,9 @@ panel:
menus:
pureHome: Home
pureProject: Project Management
pureRiskWorkorder: Risk & Workorder
pureRiskAssessment: Risk Assessment
pureWorkorderManagement: Workorder Management
pureLogin: Login
pureEmpty: Empty Page
pureTable: Table

View File

@@ -70,6 +70,9 @@ panel:
menus:
pureHome: 首页
pureProject: 项目管理
pureRiskWorkorder: 风险与工单
pureRiskAssessment: 风险评估
pureWorkorderManagement: 工单管理
pureLogin: 登录
pureEmpty: 无Layout页
pureTable: 表格

381
src/api/risk-workorder.ts Normal file
View 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"
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,8 @@ const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以
ppt = 25,
mind = 26,
guide = 27,
menuoverflow = 28;
menuoverflow = 28,
riskWorkorder = 29;
export {
home,
@@ -59,5 +60,6 @@ export {
ppt,
mind,
guide,
menuoverflow
menuoverflow,
riskWorkorder
};

View File

@@ -38,14 +38,15 @@ import {
multipleTabsKey
} from "@/utils/auth";
/** 导入项目管理、系统管理和剩余路由模块
* 其他路由模块已隐藏
/** 导入项目所需的路由模块
*/
const modules: Record<string, any> = import.meta.glob(
[
"./modules/project.ts",
"./modules/system.ts",
"./modules/home.ts",
"./modules/risk-assessment.ts",
"./modules/workorder-management.ts",
"!./modules/**/remaining.ts"
],
{

View 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;

View 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;

View File

@@ -14,6 +14,10 @@ import {
import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic";
import { message } from "@/utils/message";
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 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() {
if (!projectId.value) return;
@@ -609,7 +625,7 @@ onMounted(() => {
<g-gantt-chart
:chart-start="ganttDateRange.start"
:chart-end="ganttDateRange.end"
precision="day"
:precision="ganttPrecision"
date-format="YYYY-MM-DD"
bar-start="startDate"
bar-end="endDate"

View 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>

View 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>