refactor: 工作台、工单统计看板接口对接
This commit is contained in:
@@ -27,6 +27,7 @@ export namespace OpsOrderCenterApi {
|
||||
DEEP = 'DEEP', // 深度
|
||||
EMERGENCY = 'EMERGENCY', // 应急
|
||||
ROUTINE = 'ROUTINE', // 日常
|
||||
SPOT = 'SPOT', // 专项
|
||||
}
|
||||
|
||||
/** 优先级枚举 (0=P0紧急, 1=P1重要, 2=P2普通) */
|
||||
@@ -171,6 +172,102 @@ export function getQuickStats() {
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 统计接口 ====================
|
||||
|
||||
/** 看板完整统计请求参数 */
|
||||
export interface DashboardStatsQuery {
|
||||
orderType?: string; // 默认 CLEAN
|
||||
startDate?: string; // yyyy-MM-dd
|
||||
endDate?: string; // yyyy-MM-dd
|
||||
}
|
||||
|
||||
/** 看板完整统计响应 */
|
||||
export interface DashboardStatsResp {
|
||||
pendingCount: number;
|
||||
inProgressCount: number;
|
||||
completedTodayCount: number;
|
||||
completedTotalCount: number;
|
||||
trendData: {
|
||||
dates: string[];
|
||||
createdData: number[];
|
||||
completedData: number[];
|
||||
};
|
||||
hourlyDistribution: { hours: string[]; data: number[] };
|
||||
timeTrendData: {
|
||||
dates: string[];
|
||||
responseTimeData: number[];
|
||||
completionTimeData: number[];
|
||||
};
|
||||
funnelData: Array<{ name: string; value: number }>;
|
||||
heatmapData: { days: string[]; hours: string[]; data: number[][] };
|
||||
areaRanking: Array<{
|
||||
area: string;
|
||||
count: number;
|
||||
completed: number;
|
||||
rate: number;
|
||||
}>;
|
||||
durationStats: Array<{ type: string; avgDuration: number }>;
|
||||
}
|
||||
|
||||
/** 实时客流响应 */
|
||||
export interface TrafficRealtimeResp {
|
||||
totalIn: number;
|
||||
totalOut: number;
|
||||
currentOccupancy: number;
|
||||
areas: Array<{
|
||||
areaId: number;
|
||||
areaName: string;
|
||||
todayIn: number;
|
||||
todayOut: number;
|
||||
currentOccupancy: number;
|
||||
deviceCount: number;
|
||||
}>;
|
||||
hourlyTrend: { hours: string[]; inData: number[]; outData: number[] };
|
||||
}
|
||||
|
||||
/** 工作台统计响应 */
|
||||
export interface WorkspaceStatsResp {
|
||||
onlineStaffCount: number;
|
||||
totalStaffCount: number;
|
||||
pendingCount: number;
|
||||
avgResponseMinutes: number;
|
||||
satisfactionRate: number;
|
||||
todayOrderCount: number;
|
||||
newOrderCount: number;
|
||||
urgentTasks: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
location: string;
|
||||
priority: number;
|
||||
status: string;
|
||||
createTime: string;
|
||||
assigneeName: string;
|
||||
}>;
|
||||
workOrderTrend: { hours: string[]; data: number[] };
|
||||
}
|
||||
|
||||
/** 获取看板完整统计 */
|
||||
export function getDashboardStats(params?: DashboardStatsQuery) {
|
||||
return requestClient.get<DashboardStatsResp>(
|
||||
'/ops/order-center/dashboard-stats',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取实时客流数据 */
|
||||
export function getTrafficRealtime() {
|
||||
return requestClient.get<TrafficRealtimeResp>(
|
||||
'/ops/order-center/traffic-realtime',
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取工作台统计数据 */
|
||||
export function getWorkspaceStats() {
|
||||
return requestClient.get<WorkspaceStatsResp>(
|
||||
'/ops/order-center/workspace-stats',
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 工单操作接口 ====================
|
||||
|
||||
/** 重新分配/派单 */
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { GlassCard } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Dropdown, message, Modal, Tag } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getTrafficRealtime,
|
||||
getWorkspaceStats,
|
||||
} from '#/api/ops/order-center';
|
||||
|
||||
import { BackgroundChart } from '../../../components/background-chart';
|
||||
import { createBackgroundChartOptions } from './utils/chart-options';
|
||||
import {
|
||||
@@ -40,123 +45,112 @@ interface StatItem {
|
||||
}
|
||||
|
||||
// --- 响应式数据 ---
|
||||
const urgentTasks = ref<Task[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '大堂地面湿滑',
|
||||
location: '1F 大堂',
|
||||
priority: 'P0',
|
||||
status: 'pending',
|
||||
createTime: '09:15',
|
||||
assignee: '张工',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '人员超限 (>200)',
|
||||
location: '3F 洗手间',
|
||||
priority: 'P0',
|
||||
status: 'pending',
|
||||
createTime: '09:30',
|
||||
assignee: '李工',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '补充纸巾',
|
||||
location: '2F 贵宾室',
|
||||
priority: 'P1',
|
||||
status: 'pending',
|
||||
createTime: '10:05',
|
||||
assignee: '王工',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '传感器离线',
|
||||
location: 'B1 停车场',
|
||||
priority: 'P1',
|
||||
status: 'pending',
|
||||
createTime: '10:20',
|
||||
assignee: '赵工',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '垃圾桶满溢',
|
||||
location: '1F 出口闸机',
|
||||
priority: 'P2',
|
||||
status: 'pending',
|
||||
createTime: '10:45',
|
||||
assignee: '钱工',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '通道受阻',
|
||||
location: '3F 楼梯间',
|
||||
priority: 'P1',
|
||||
status: 'pending',
|
||||
createTime: '11:00',
|
||||
assignee: '孙工',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: '灯光闪烁',
|
||||
location: '2F 走廊',
|
||||
priority: 'P2',
|
||||
status: 'pending',
|
||||
createTime: '11:15',
|
||||
assignee: '周工',
|
||||
},
|
||||
]);
|
||||
const urgentTasks = ref<Task[]>([]);
|
||||
|
||||
const flowData = [
|
||||
{ time: '09:00', value: 120 },
|
||||
{ time: '10:00', value: 180 },
|
||||
{ time: '11:00', value: 250 },
|
||||
{ time: '12:00', value: 190 },
|
||||
{ time: '13:00', value: 150 },
|
||||
{ time: '14:00', value: 210 },
|
||||
{ time: '15:00', value: 310 },
|
||||
{ time: '16:00', value: 280 },
|
||||
{ time: '17:00', value: 350 },
|
||||
{ time: '18:00', value: 200 },
|
||||
];
|
||||
const flowChartHours = ref<string[]>([]);
|
||||
const flowChartInData = ref<number[]>([]);
|
||||
const workOrderTrendHours = ref<string[]>([]);
|
||||
const workOrderTrendData = ref<number[]>([]);
|
||||
|
||||
const workOrderTrend = [
|
||||
{ time: '09:00', value: 5 },
|
||||
{ time: '10:00', value: 12 },
|
||||
{ time: '11:00', value: 8 },
|
||||
{ time: '12:00', value: 15 },
|
||||
{ time: '13:00', value: 24 },
|
||||
{ time: '14:00', value: 18 },
|
||||
{ time: '15:00', value: 28 },
|
||||
{ time: '16:00', value: 22 },
|
||||
{ time: '17:00', value: 14 },
|
||||
];
|
||||
const trafficTotalIn = ref(0);
|
||||
const todayOrderCount = ref(0);
|
||||
const newOrderCount = ref(0);
|
||||
|
||||
const stats: StatItem[] = [
|
||||
const stats = ref<StatItem[]>([
|
||||
{
|
||||
label: '在岗人员',
|
||||
value: '12/15',
|
||||
value: '-/-',
|
||||
icon: 'ant-design:user-outlined',
|
||||
color: 'indigo',
|
||||
},
|
||||
{
|
||||
label: '待处理',
|
||||
value: '5',
|
||||
value: '0',
|
||||
icon: 'ant-design:file-text-outlined',
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
label: '平均响应',
|
||||
value: '3.2m',
|
||||
value: '-',
|
||||
icon: 'ant-design:thunderbolt-outlined',
|
||||
color: 'teal',
|
||||
},
|
||||
{
|
||||
label: '满意度',
|
||||
value: '85%',
|
||||
value: '-',
|
||||
icon: 'ant-design:bar-chart-outlined',
|
||||
color: 'pink',
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
// --- 优先级映射(后端返回数字 0/1/2) ---
|
||||
const PRIORITY_MAP: Record<number, 'P0' | 'P1' | 'P2'> = {
|
||||
0: 'P0',
|
||||
1: 'P1',
|
||||
2: 'P2',
|
||||
};
|
||||
|
||||
// --- 数据加载 ---
|
||||
async function loadData() {
|
||||
try {
|
||||
const [trafficResp, workspaceResp] = await Promise.all([
|
||||
getTrafficRealtime(),
|
||||
getWorkspaceStats(),
|
||||
]);
|
||||
|
||||
// 客流数据
|
||||
trafficTotalIn.value = trafficResp.totalIn || 0;
|
||||
flowChartHours.value = trafficResp.hourlyTrend?.hours || [];
|
||||
flowChartInData.value = trafficResp.hourlyTrend?.inData || [];
|
||||
|
||||
// 工作台数据
|
||||
todayOrderCount.value = workspaceResp.todayOrderCount || 0;
|
||||
newOrderCount.value = workspaceResp.newOrderCount || 0;
|
||||
workOrderTrendHours.value = workspaceResp.workOrderTrend?.hours || [];
|
||||
workOrderTrendData.value = workspaceResp.workOrderTrend?.data || [];
|
||||
|
||||
// 紧急任务
|
||||
urgentTasks.value = (workspaceResp.urgentTasks || []).map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
location: item.location,
|
||||
priority: PRIORITY_MAP[item.priority] || 'P2',
|
||||
status: item.status === 'COMPLETED' ? 'completed' : 'pending',
|
||||
createTime: item.createTime,
|
||||
assignee: item.assigneeName,
|
||||
}));
|
||||
|
||||
// 统计指标
|
||||
stats.value = [
|
||||
{
|
||||
label: '在岗人员',
|
||||
value: `${workspaceResp.onlineStaffCount || 0}/${workspaceResp.totalStaffCount || 0}`,
|
||||
icon: 'ant-design:user-outlined',
|
||||
color: 'indigo',
|
||||
},
|
||||
{
|
||||
label: '待处理',
|
||||
value: `${workspaceResp.pendingCount || 0}`,
|
||||
icon: 'ant-design:file-text-outlined',
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
label: '平均响应',
|
||||
value: `${workspaceResp.avgResponseMinutes || 0}m`,
|
||||
icon: 'ant-design:thunderbolt-outlined',
|
||||
color: 'teal',
|
||||
},
|
||||
{
|
||||
label: '满意度',
|
||||
value: `${workspaceResp.satisfactionRate || 0}%`,
|
||||
icon: 'ant-design:bar-chart-outlined',
|
||||
color: 'pink',
|
||||
},
|
||||
];
|
||||
} catch {
|
||||
// API失败时保持现有数据不变
|
||||
}
|
||||
}
|
||||
|
||||
// --- 筛选和交互状态 ---
|
||||
const priorityFilter = ref<'all' | 'P0' | 'P1' | 'P2'>('all');
|
||||
@@ -184,8 +178,8 @@ const filteredTasks = computed(() => {
|
||||
// --- 图表配置 ---
|
||||
const flowChartOptions = computed(() =>
|
||||
createBackgroundChartOptions({
|
||||
xAxisData: flowData.map((item) => item.time),
|
||||
yAxisData: flowData.map((item) => item.value),
|
||||
xAxisData: flowChartHours.value,
|
||||
yAxisData: flowChartInData.value,
|
||||
seriesName: '客流',
|
||||
lineColor: '#FFA00A',
|
||||
areaColor: [
|
||||
@@ -204,8 +198,8 @@ const flowChartOptions = computed(() =>
|
||||
|
||||
const workOrderChartOptions = computed(() =>
|
||||
createBackgroundChartOptions({
|
||||
xAxisData: workOrderTrend.map((item) => item.time),
|
||||
yAxisData: workOrderTrend.map((item) => item.value),
|
||||
xAxisData: workOrderTrendHours.value,
|
||||
yAxisData: workOrderTrendData.value,
|
||||
seriesName: '工单',
|
||||
lineColor: '#3B82F6',
|
||||
areaColor: [
|
||||
@@ -216,6 +210,11 @@ const workOrderChartOptions = computed(() =>
|
||||
}),
|
||||
);
|
||||
|
||||
// --- 格式化数字(千位分隔) ---
|
||||
function formatNumber(num: number): string {
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
// --- 交互方法 ---
|
||||
function handleTaskComplete(taskId: number) {
|
||||
const task = urgentTasks.value.find((t) => t.id === taskId);
|
||||
@@ -246,15 +245,13 @@ function handleTaskDelete(taskId: number) {
|
||||
|
||||
function handleRefreshStats() {
|
||||
message.loading('刷新中...', 0.5);
|
||||
// 这里可以添加实际的刷新逻辑
|
||||
setTimeout(() => {
|
||||
loadData().then(() => {
|
||||
message.success('刷新成功');
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
function handleStatCardClick(stat: StatItem) {
|
||||
message.info(`查看${stat.label}详情`);
|
||||
// 这里可以添加跳转到详情页面的逻辑
|
||||
}
|
||||
|
||||
function handleFilterChange(value: 'all' | 'P0' | 'P1' | 'P2') {
|
||||
@@ -294,6 +291,20 @@ function getColorClass(color: string, type: 'bg' | 'text') {
|
||||
};
|
||||
return colors[color]?.[type] || '';
|
||||
}
|
||||
|
||||
// --- 生命周期 ---
|
||||
let refreshTimer: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
refreshTimer = setInterval(loadData, 30_000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -306,7 +317,7 @@ function getColorClass(color: string, type: 'bg' | 'text') {
|
||||
<div :class="cardContentClasses.container">
|
||||
<h3 :class="cardContentClasses.title">实时客流监测</h3>
|
||||
<div :class="cardContentClasses.numberContainer">
|
||||
<span :class="cardContentClasses.largeNumber">2,450</span>
|
||||
<span :class="cardContentClasses.largeNumber">{{ formatNumber(trafficTotalIn) }}</span>
|
||||
<Tag color="success" :class="`text-xs font-bold ${textShadowClasses.tag}`">+12%</Tag>
|
||||
</div>
|
||||
<p :class="cardContentClasses.description">预计高峰时间: 14:00</p>
|
||||
@@ -447,8 +458,8 @@ function getColorClass(color: string, type: 'bg' | 'text') {
|
||||
<div :class="cardContentClasses.container">
|
||||
<h3 :class="cardContentClasses.title">工单趋势分析</h3>
|
||||
<div :class="cardContentClasses.numberContainer">
|
||||
<span :class="cardContentClasses.largeNumber">89</span>
|
||||
<Tag color="processing" :class="`text-xs font-bold ${textShadowClasses.tag}`">+5 新增</Tag>
|
||||
<span :class="cardContentClasses.largeNumber">{{ todayOrderCount }}</span>
|
||||
<Tag color="processing" :class="`text-xs font-bold ${textShadowClasses.tag}`">+{{ newOrderCount }} 新增</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="text" size="small" :class="buttonClasses.viewAll" @click="message.info('查看全部工单')">
|
||||
|
||||
@@ -8,12 +8,24 @@ import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { Card, Col, Row, Spin, Statistic, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { getQuickStats } from '#/api/ops/order-center';
|
||||
import { getDashboardStats } from '#/api/ops/order-center';
|
||||
|
||||
defineOptions({ name: 'CleaningWorkOrderDashboard' });
|
||||
|
||||
// ========== 模拟数据开关 ==========
|
||||
const USE_MOCK_DATA = false;
|
||||
// ========== 保洁类型映射 ==========
|
||||
const CLEANING_TYPE_MAP: Record<
|
||||
string,
|
||||
{ type: string; icon: string; color: string }
|
||||
> = {
|
||||
ROUTINE: { type: '日常保洁', icon: 'solar:broom-bold', color: '#1677ff' },
|
||||
DEEP: { type: '深度保洁', icon: 'solar:multiply-bold', color: '#52c41a' },
|
||||
SPOT: {
|
||||
type: '专项清洁',
|
||||
icon: 'solar:spray-bottle-bold',
|
||||
color: '#faad14',
|
||||
},
|
||||
EMERGENCY: { type: '应急处理', icon: 'solar:flame-bold', color: '#ff4d4f' },
|
||||
};
|
||||
|
||||
// ========== 数据类型定义 ==========
|
||||
interface DashboardStats {
|
||||
@@ -22,8 +34,6 @@ interface DashboardStats {
|
||||
inProgressCount: number;
|
||||
completedTodayCount: number;
|
||||
completedTotalCount: number;
|
||||
onlineBadgeCount: number;
|
||||
totalBadgeCount: number;
|
||||
|
||||
// 趋势数据(7天)
|
||||
trendData: {
|
||||
@@ -99,8 +109,6 @@ const statsData = ref<DashboardStats>({
|
||||
inProgressCount: 0,
|
||||
completedTodayCount: 0,
|
||||
completedTotalCount: 0,
|
||||
onlineBadgeCount: 0,
|
||||
totalBadgeCount: 0,
|
||||
trendData: {
|
||||
dates: [],
|
||||
createdData: [],
|
||||
@@ -125,120 +133,19 @@ const statsData = ref<DashboardStats>({
|
||||
durationStats: [],
|
||||
});
|
||||
|
||||
// ========== 模拟数据 ==========
|
||||
const MOCK_STATS: DashboardStats = {
|
||||
pendingCount: 8,
|
||||
inProgressCount: 15,
|
||||
completedTodayCount: 42,
|
||||
completedTotalCount: 1586,
|
||||
onlineBadgeCount: 12,
|
||||
totalBadgeCount: 18,
|
||||
trendData: {
|
||||
dates: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
createdData: [45, 52, 38, 65, 48, 55, 42],
|
||||
completedData: [42, 48, 35, 60, 45, 52, 40],
|
||||
},
|
||||
hourlyDistribution: {
|
||||
hours: [
|
||||
'00:00',
|
||||
'02:00',
|
||||
'04:00',
|
||||
'06:00',
|
||||
'08:00',
|
||||
'10:00',
|
||||
'12:00',
|
||||
'14:00',
|
||||
'16:00',
|
||||
'18:00',
|
||||
'20:00',
|
||||
'22:00',
|
||||
],
|
||||
data: [2, 1, 0, 3, 15, 28, 22, 25, 18, 12, 8, 4],
|
||||
},
|
||||
timeTrendData: {
|
||||
dates: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
responseTimeData: [10, 9, 12, 8, 7, 9, 8.5],
|
||||
completionTimeData: [38, 35, 42, 32, 30, 36, 35],
|
||||
},
|
||||
funnelData: [
|
||||
{ name: '创建工单', value: 100 },
|
||||
{ name: '已派单', value: 95 },
|
||||
{ name: '已接单', value: 88 },
|
||||
{ name: '已到达', value: 82 },
|
||||
{ name: '已完成', value: 78 },
|
||||
],
|
||||
// 时段热力图数据(7天 x 24小时)
|
||||
heatmapData: {
|
||||
days: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
hours: Array.from({ length: 24 }, (_, i) => `${i}:00`),
|
||||
// 7天 x 24小时的数据(0-10表示工单数量)
|
||||
data: [
|
||||
[
|
||||
0, 0, 0, 1, 2, 8, 12, 18, 20, 16, 12, 6, 2, 0, 0, 0, 0, 0, 0, 1, 3, 5,
|
||||
4, 2,
|
||||
], // 周一
|
||||
[
|
||||
0, 0, 1, 1, 3, 10, 15, 20, 22, 18, 14, 8, 3, 1, 0, 0, 0, 0, 1, 2, 4, 6,
|
||||
5, 3,
|
||||
], // 周二
|
||||
[
|
||||
0, 0, 0, 0, 2, 8, 14, 18, 20, 16, 10, 6, 2, 1, 0, 0, 0, 0, 1, 3, 5, 4,
|
||||
3, 1,
|
||||
], // 周三
|
||||
[
|
||||
0, 0, 0, 2, 4, 12, 18, 22, 24, 20, 14, 8, 2, 1, 0, 0, 0, 0, 1, 2, 5, 6,
|
||||
4, 2,
|
||||
], // 周四
|
||||
[
|
||||
0, 0, 0, 1, 3, 9, 14, 19, 21, 18, 12, 7, 2, 0, 0, 0, 0, 0, 1, 3, 4, 5,
|
||||
3, 2,
|
||||
], // 周五
|
||||
[
|
||||
0, 0, 0, 0, 2, 10, 16, 22, 24, 20, 14, 10, 4, 1, 0, 0, 0, 0, 1, 2, 4, 6,
|
||||
5, 3,
|
||||
], // 周六
|
||||
[
|
||||
0, 0, 0, 1, 1, 6, 12, 16, 18, 15, 10, 6, 2, 0, 0, 0, 0, 0, 1, 2, 3, 4,
|
||||
3, 1,
|
||||
], // 周日
|
||||
],
|
||||
},
|
||||
// 区域工单排行
|
||||
areaRanking: [
|
||||
{ area: 'A区', count: 156, completed: 142, rate: 91 },
|
||||
{ area: 'B区', count: 128, completed: 110, rate: 85.9 },
|
||||
{ area: 'C区', count: 95, completed: 88, rate: 92.6 },
|
||||
{ area: 'D区', count: 78, completed: 65, rate: 83.3 },
|
||||
{ area: 'E区', count: 62, completed: 58, rate: 93.5 },
|
||||
{ area: 'F区', count: 45, completed: 42, rate: 93.3 },
|
||||
],
|
||||
// 工单作业时长统计
|
||||
durationStats: [
|
||||
{
|
||||
type: '日常保洁',
|
||||
icon: 'solar:broom-bold',
|
||||
avgDuration: 35,
|
||||
color: '#1677ff',
|
||||
},
|
||||
{
|
||||
type: '深度保洁',
|
||||
icon: 'solar:multiply-bold',
|
||||
avgDuration: 68,
|
||||
color: '#52c41a',
|
||||
},
|
||||
{
|
||||
type: '专项清洁',
|
||||
icon: 'solar:spray-bottle-bold',
|
||||
avgDuration: 52,
|
||||
color: '#faad14',
|
||||
},
|
||||
{
|
||||
type: '应急处理',
|
||||
icon: 'solar:flame-bold',
|
||||
avgDuration: 25,
|
||||
color: '#ff4d4f',
|
||||
},
|
||||
],
|
||||
// ========== 空数据(API失败时的fallback) ==========
|
||||
const EMPTY_STATS: DashboardStats = {
|
||||
pendingCount: 0,
|
||||
inProgressCount: 0,
|
||||
completedTodayCount: 0,
|
||||
completedTotalCount: 0,
|
||||
trendData: { dates: [], createdData: [], completedData: [] },
|
||||
hourlyDistribution: { hours: [], data: [] },
|
||||
timeTrendData: { dates: [], responseTimeData: [], completionTimeData: [] },
|
||||
funnelData: [],
|
||||
heatmapData: { days: [], hours: [], data: [] },
|
||||
areaRanking: [],
|
||||
durationStats: [],
|
||||
};
|
||||
|
||||
// ========== 图表配置 ==========
|
||||
@@ -903,20 +810,35 @@ function getDurationChartOptions(): ECOption {
|
||||
async function loadStats() {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (USE_MOCK_DATA) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
statsData.value = { ...MOCK_STATS };
|
||||
} else {
|
||||
const quickStats = await getQuickStats();
|
||||
// TODO: 根据 API 返回数据构建统计
|
||||
statsData.value = {
|
||||
...MOCK_STATS,
|
||||
pendingCount: quickStats.pendingCount,
|
||||
inProgressCount: quickStats.inProgressCount,
|
||||
completedTodayCount: quickStats.completedTodayCount,
|
||||
onlineBadgeCount: quickStats.onlineBadgeCount,
|
||||
const resp = await getDashboardStats();
|
||||
// 映射 durationStats: 后端返回 type(枚举key) + avgDuration,前端补充显示名、图标、颜色
|
||||
const mappedDurationStats = (resp.durationStats || []).map((item) => {
|
||||
const mapping = CLEANING_TYPE_MAP[item.type] || {
|
||||
type: item.type,
|
||||
icon: 'solar:broom-bold',
|
||||
color: '#8c8c8c',
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: mapping.type,
|
||||
icon: mapping.icon,
|
||||
color: mapping.color,
|
||||
avgDuration: item.avgDuration,
|
||||
};
|
||||
});
|
||||
|
||||
statsData.value = {
|
||||
pendingCount: resp.pendingCount,
|
||||
inProgressCount: resp.inProgressCount,
|
||||
completedTodayCount: resp.completedTodayCount,
|
||||
completedTotalCount: resp.completedTotalCount,
|
||||
trendData: resp.trendData,
|
||||
hourlyDistribution: resp.hourlyDistribution,
|
||||
timeTrendData: resp.timeTrendData,
|
||||
funnelData: resp.funnelData,
|
||||
heatmapData: resp.heatmapData,
|
||||
areaRanking: resp.areaRanking,
|
||||
durationStats: mappedDurationStats,
|
||||
};
|
||||
|
||||
// 渲染图表
|
||||
chartLoading.value = false;
|
||||
@@ -929,8 +851,8 @@ async function loadStats() {
|
||||
renderAreaRankingChart(getAreaRankingChartOptions());
|
||||
renderDurationChart(getDurationChartOptions());
|
||||
} catch {
|
||||
// 使用默认数据
|
||||
statsData.value = { ...MOCK_STATS };
|
||||
// API失败时使用空数据作为fallback
|
||||
statsData.value = { ...EMPTY_STATS };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export const STATUS_COLOR_MAP: Record<string, string> = {
|
||||
CONFIRMED: '#13c2c2', // 青色 - 已确认
|
||||
ARRIVED: '#52c41a', // 绿色 - 已到岗
|
||||
PAUSED: '#fa8c16', // 橙色 - 已暂停
|
||||
RESUMED: '#52c41a', // 绿色 - 已恢复
|
||||
COMPLETED: '#389e0d', // 深绿 - 已完成
|
||||
CANCELLED: '#ff4d4f', // 红色 - 已取消
|
||||
};
|
||||
@@ -23,6 +24,7 @@ export const STATUS_TEXT_MAP: Record<string, string> = {
|
||||
CONFIRMED: '已确认',
|
||||
ARRIVED: '作业中',
|
||||
PAUSED: '已暂停',
|
||||
RESUMED: '已恢复',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
};
|
||||
@@ -35,6 +37,7 @@ export const STATUS_ICON_MAP: Record<string, string> = {
|
||||
CONFIRMED: 'solar:check-circle-bold-duotone', // 已确认
|
||||
ARRIVED: 'solar:play-circle-bold-duotone', // 作业中
|
||||
PAUSED: 'solar:pause-circle-bold-duotone', // 已暂停
|
||||
RESUMED: 'solar:play-circle-bold-duotone', // 已恢复
|
||||
COMPLETED: 'solar:check-read-bold-duotone', // 已完成
|
||||
CANCELLED: 'solar:close-circle-bold-duotone', // 已取消
|
||||
};
|
||||
|
||||
@@ -78,6 +78,12 @@ const STATUS_STEPS = [
|
||||
icon: 'solar:inbox-line-bold-duotone',
|
||||
desc: '工单已创建,等待分配',
|
||||
},
|
||||
{
|
||||
key: 'QUEUED',
|
||||
title: '排队中',
|
||||
icon: 'solar:clock-circle-bold-duotone',
|
||||
desc: '执行人忙碌,任务排队',
|
||||
},
|
||||
{
|
||||
key: 'DISPATCHED',
|
||||
title: '已推送',
|
||||
@@ -330,14 +336,40 @@ const showLeaveWarning = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
/** 动态生成应该显示的状态步骤(根据 timeline 过滤) */
|
||||
const visibleSteps = computed(() => {
|
||||
// 必须显示的节点(主流程)
|
||||
const requiredSteps = ['PENDING', 'DISPATCHED', 'CONFIRMED', 'ARRIVED', 'COMPLETED'];
|
||||
|
||||
// timeline 中存在的状态
|
||||
const timelineStatuses = new Set(timeline.value.map(t => t.status));
|
||||
|
||||
// 当前状态
|
||||
const currentStatus = order.value.status;
|
||||
|
||||
// 过滤出要显示的节点
|
||||
return STATUS_STEPS.filter(step => {
|
||||
// 必须显示的节点
|
||||
if (requiredSteps.includes(step.key)) return true;
|
||||
|
||||
// QUEUED 节点:只有在 timeline 中明确存在时才显示
|
||||
if (step.key === 'QUEUED') {
|
||||
return timelineStatuses.has('QUEUED');
|
||||
}
|
||||
|
||||
// 其他节点:timeline 中存在,或者是当前状态
|
||||
return timelineStatuses.has(step.key) || currentStatus === step.key;
|
||||
});
|
||||
});
|
||||
|
||||
/** 计算当前状态步骤索引 */
|
||||
const currentStepIndex = computed(() => {
|
||||
if (order.value.status === 'CANCELLED') return -1;
|
||||
if (order.value.status === 'PAUSED') {
|
||||
// 暂停状态显示在到岗后
|
||||
return STATUS_STEPS.findIndex((s) => s.key === 'ARRIVED');
|
||||
return visibleSteps.value.findIndex((s) => s.key === 'ARRIVED');
|
||||
}
|
||||
const index = STATUS_STEPS.findIndex((s) => s.key === order.value.status);
|
||||
const index = visibleSteps.value.findIndex((s) => s.key === order.value.status);
|
||||
return Math.max(index, 0);
|
||||
});
|
||||
|
||||
@@ -673,13 +705,13 @@ onUnmounted(() => {
|
||||
<div
|
||||
class="progress-line-fill"
|
||||
:style="{
|
||||
width: `${Math.max(0, (currentStepIndex / (STATUS_STEPS.length - 1)) * 100)}%`,
|
||||
width: `${Math.max(0, (currentStepIndex / (visibleSteps.length - 1)) * 100)}%`,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="progress-nodes">
|
||||
<div
|
||||
v-for="(step, index) in STATUS_STEPS"
|
||||
v-for="(step, index) in visibleSteps"
|
||||
:key="step.key"
|
||||
class="progress-node"
|
||||
:class="{
|
||||
|
||||
Reference in New Issue
Block a user