- 新增风险与工单相关多语言菜单项(英文和中文) - 定义风险相关类型,包括风险分类、风险等级和状态等 - 定义工单相关类型,包括工单类型、优先级及状态等 - 实现风险评估创建、更新、删除、查询及统计接口 - 实现工单创建、更新、删除、查询、处理和分配接口 - 支持批量更新风险状态接口 - 新增我的工单列表及统计接口 - 提供统一的响应结果类型定义 - 更新OpenAPI规范文件以支持新增接口
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -70,6 +70,9 @@ panel:
|
||||
menus:
|
||||
pureHome: 首页
|
||||
pureProject: 项目管理
|
||||
pureRiskWorkorder: 风险与工单
|
||||
pureRiskAssessment: 风险评估
|
||||
pureWorkorderManagement: 工单管理
|
||||
pureLogin: 登录
|
||||
pureEmpty: 无Layout页
|
||||
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,
|
||||
mind = 26,
|
||||
guide = 27,
|
||||
menuoverflow = 28;
|
||||
menuoverflow = 28,
|
||||
riskWorkorder = 29;
|
||||
|
||||
export {
|
||||
home,
|
||||
@@ -59,5 +60,6 @@ export {
|
||||
ppt,
|
||||
mind,
|
||||
guide,
|
||||
menuoverflow
|
||||
menuoverflow,
|
||||
riskWorkorder
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
{
|
||||
|
||||
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 { 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"
|
||||
|
||||
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