refactor: 工作台、工单统计看板接口对接

This commit is contained in:
lzh
2026-02-04 11:14:06 +08:00
parent d4118123c1
commit 71fc0d0fad
3216 changed files with 4843 additions and 4778 deletions

View File

@@ -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',
);
}
// ==================== 工单操作接口 ====================
/** 重新分配/派单 */

View File

@@ -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('查看全部工单')">

View File

@@ -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;
}

View File

@@ -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', // 已取消
};

View File

@@ -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="{