diff --git a/apps/web-antd/src/api/ops/order-center/index.ts b/apps/web-antd/src/api/ops/order-center/index.ts index 125b1369a..5cef5cb9c 100644 --- a/apps/web-antd/src/api/ops/order-center/index.ts +++ b/apps/web-antd/src/api/ops/order-center/index.ts @@ -206,6 +206,16 @@ export interface DashboardStatsResp { functionType: string; rate: number; }>; + // 当月与上月工单趋势对比 + monthlyTrendData?: { + currentMonth: { createdData: number[]; dates: string[] }; + lastMonth: { createdData: number[]; dates: string[] }; + }; + // 工牌近7天队列数量统计 + badgeQueueStats?: { + dates: string[]; + queueCounts: number[]; + }; } /** 近7天客流趋势响应 */ diff --git a/apps/web-antd/src/api/ops/traffic/index.ts b/apps/web-antd/src/api/ops/traffic/index.ts index 06f8b2d0a..fea5815d5 100644 --- a/apps/web-antd/src/api/ops/traffic/index.ts +++ b/apps/web-antd/src/api/ops/traffic/index.ts @@ -76,13 +76,17 @@ export interface TrafficTrendQuery { // ==================== 迁移接口(路径变更) ==================== /** 获取全局实时客流数据 */ -export function getTrafficRealtime() { - return requestClient.get('/ops/traffic/realtime'); +export function getTrafficRealtime(date?: string) { + return requestClient.get('/ops/traffic/realtime', { + params: date ? { date } : undefined, + }); } /** 获取全局近7天客流趋势 */ -export function getTrafficTrend() { - return requestClient.get('/ops/traffic/trend'); +export function getTrafficTrend(params?: TrafficTrendQuery) { + return requestClient.get('/ops/traffic/trend', { + params, + }); } // ==================== 新增接口 ==================== @@ -96,9 +100,9 @@ export function getDeviceRealtime(deviceId: number) { } /** 获取区域实时客流(汇总,返回与全局一致的结构) */ -export function getAreaRealtime(areaIds: number[]) { +export function getAreaRealtime(areaIds: number[], date?: string) { return requestClient.get('/ops/traffic/area/realtime', { - params: { areaIds: areaIds.join(',') }, + params: { areaIds: areaIds.join(','), ...(date ? { date } : {}) }, }); } diff --git a/apps/web-antd/src/views/dashboard/workspace/index.vue b/apps/web-antd/src/views/dashboard/workspace/index.vue index e318403f8..994ecdd30 100644 --- a/apps/web-antd/src/views/dashboard/workspace/index.vue +++ b/apps/web-antd/src/views/dashboard/workspace/index.vue @@ -113,22 +113,27 @@ async function loadData() { 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, - })); + // 紧急任务(过滤掉已取消和已完成的) + urgentTasks.value = (workspaceResp.urgentTasks || []) + .filter( + (item) => + item.status !== 'CANCELLED' && item.status !== 'COMPLETED', + ) + .map((item) => ({ + id: item.id, + title: item.title, + location: item.location, + priority: PRIORITY_MAP[item.priority] || 'P2', + status: 'pending', + createTime: item.createTime, + assignee: item.assigneeName, + })); // 统计指标 stats.value = [ { label: '在岗人员', - value: `${workspaceResp.onlineStaffCount || 0}/${workspaceResp.totalStaffCount || 0}`, + value: `${workspaceResp.onlineStaffCount || 0}`, icon: 'ant-design:user-outlined', color: 'indigo', }, diff --git a/apps/web-antd/src/views/ops/cleaning/work-order/dashboard/index.vue b/apps/web-antd/src/views/ops/cleaning/work-order/dashboard/index.vue index da549f856..660ef896b 100644 --- a/apps/web-antd/src/views/ops/cleaning/work-order/dashboard/index.vue +++ b/apps/web-antd/src/views/ops/cleaning/work-order/dashboard/index.vue @@ -63,7 +63,7 @@ interface DashboardStats { responseTimeData: number[]; }; - // 工单处理漏斗 + // 工单状态分布 funnelData: Array<{ name: string; value: number }>; // 时段热力图数据(近7天 x 24小时) @@ -80,6 +80,18 @@ interface DashboardStats { functionType: string; rate: number; }>; + + // 当月与上月工单趋势对比 + monthlyTrendData?: { + currentMonth: { createdData: number[]; dates: string[] }; + lastMonth: { createdData: number[]; dates: string[] }; + }; + + // 工牌近7天队列数量统计 + badgeQueueStats?: { + dates: string[]; + queueCounts: number[]; + }; } // ========== 响应式数据 ========== @@ -91,17 +103,19 @@ const trendChartRef = ref(); const hourlyChartRef = ref(); const timeTrendChartRef = ref(); const funnelChartRef = ref(); -const heatmapChartRef = ref(); const functionTypeRankingChartRef = ref(); +const badgeQueueChartRef = ref(); const { renderEcharts: renderTrendChart } = useEcharts(trendChartRef); const { renderEcharts: renderHourlyChart } = useEcharts(hourlyChartRef); const { renderEcharts: renderTimeTrendChart } = useEcharts(timeTrendChartRef); const { renderEcharts: renderFunnelChart } = useEcharts(funnelChartRef); -const { renderEcharts: renderHeatmapChart } = useEcharts(heatmapChartRef); const { renderEcharts: renderFunctionTypeRankingChart } = useEcharts( functionTypeRankingChartRef, ); +const { renderEcharts: renderBadgeQueueChart } = useEcharts( + badgeQueueChartRef, +); const statsData = ref({ pendingCount: 0, @@ -129,6 +143,8 @@ const statsData = ref({ data: [], }, functionTypeRanking: [], + monthlyTrendData: undefined, + badgeQueueStats: undefined, }); // ========== 空数据(API失败时的fallback) ========== @@ -148,73 +164,64 @@ const EMPTY_STATS: DashboardStats = { // ========== 图表配置 ========== /** - * 工单趋势图表配置(近7天) + * 工单趋势图表配置(当月 vs 上月新增工单对比) */ function getTrendChartOptions(): ECOption { - const { trendData } = statsData.value; + const monthly = statsData.value.monthlyTrendData; + // 如果没有月度对比数据,回退到原有7天数据 + if (!monthly) { + const { trendData } = statsData.value; + return { + tooltip: { trigger: 'axis' }, + legend: { data: ['新增工单'], top: '5%', textStyle: { color: '#595959' } }, + grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true }, + xAxis: [{ type: 'category', boundaryGap: false, data: trendData.dates, axisLine: { lineStyle: { color: '#d9d9d9' } }, axisLabel: { color: '#8c8c8c' } }], + yAxis: [{ type: 'value', name: '工单数量', nameTextStyle: { color: '#8c8c8c' }, axisLine: { show: false }, axisLabel: { color: '#8c8c8c' }, splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } } }], + series: [{ name: '新增工单', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: trendData.createdData, itemStyle: { color: '#1677ff' }, areaStyle: { opacity: 0.15, color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(22, 119, 255, 0.4)' }, { offset: 1, color: 'rgba(22, 119, 255, 0.05)' }] } } }], + }; + } + + // 使用日期中的"日"部分作为x轴(方便对比) + const currentDates = monthly.currentMonth.dates.map((d) => { + const parts = d.split('-'); + return parts[2] || d; + }); + const lastDates = monthly.lastMonth.dates.map((d) => { + const parts = d.split('-'); + return parts[2] || d; + }); + // 取最长的日期轴 + const xData = currentDates.length >= lastDates.length ? currentDates : lastDates; + return { tooltip: { trigger: 'axis', - axisPointer: { - type: 'cross', - label: { - backgroundColor: '#6a7985', - }, - }, + axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }, }, legend: { - data: ['新增工单', '完成工单'], + data: ['当月新增', '上月新增'], top: '5%', - textStyle: { - color: '#595959', - }, + textStyle: { color: '#595959' }, }, - grid: { - left: '3%', - right: '4%', - bottom: '3%', - top: '15%', - containLabel: true, - }, - xAxis: [ - { - type: 'category', - boundaryGap: false, - data: trendData.dates, - axisLine: { - lineStyle: { - color: '#d9d9d9', - }, - }, - axisLabel: { - color: '#8c8c8c', - }, - }, - ], - yAxis: [ - { - type: 'value', - name: '工单数量', - nameTextStyle: { - color: '#8c8c8c', - }, - axisLine: { - show: false, - }, - axisLabel: { - color: '#8c8c8c', - }, - splitLine: { - lineStyle: { - color: '#f0f0f0', - type: 'dashed', - }, - }, - }, - ], + grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true }, + xAxis: [{ + type: 'category', + boundaryGap: false, + data: xData, + axisLine: { lineStyle: { color: '#d9d9d9' } }, + axisLabel: { color: '#8c8c8c', formatter: (val: string) => `${val}日` }, + }], + yAxis: [{ + type: 'value', + name: '工单数量', + nameTextStyle: { color: '#8c8c8c' }, + axisLine: { show: false }, + axisLabel: { color: '#8c8c8c' }, + splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } }, + }], series: [ { - name: '新增工单', + name: '当月新增', type: 'line', smooth: true, symbol: 'circle', @@ -222,152 +229,37 @@ function getTrendChartOptions(): ECOption { areaStyle: { opacity: 0.15, color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, + type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(22, 119, 255, 0.4)' }, { offset: 1, color: 'rgba(22, 119, 255, 0.05)' }, ], }, }, - emphasis: { - focus: 'series', - }, - data: trendData.createdData, - itemStyle: { - color: '#1677ff', - }, + emphasis: { focus: 'series' }, + data: monthly.currentMonth.createdData, + itemStyle: { color: '#1677ff' }, }, { - name: '完成工单', + name: '上月新增', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, + lineStyle: { type: 'dashed' }, areaStyle: { - opacity: 0.15, + opacity: 0.1, color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, + type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ - { offset: 0, color: 'rgba(82, 196, 26, 0.4)' }, + { offset: 0, color: 'rgba(82, 196, 26, 0.3)' }, { offset: 1, color: 'rgba(82, 196, 26, 0.05)' }, ], }, }, - emphasis: { - focus: 'series', - }, - data: trendData.completedData, - itemStyle: { - color: '#52c41a', - }, - }, - ], - }; -} - -/** - * 工单时段分布图表配置(柱状图) - */ -function getHourlyChartOptions(): ECOption { - const { hourlyDistribution } = statsData.value; - return { - tooltip: { - trigger: 'axis', - axisPointer: { - type: 'shadow', - }, - formatter: (params: any) => { - const param = params[0]; - return `${param.name}
工单数: ${param.value}单`; - }, - }, - grid: { - left: '3%', - right: '4%', - bottom: '3%', - top: '10%', - containLabel: true, - }, - xAxis: [ - { - type: 'category', - data: hourlyDistribution.hours, - axisLine: { - lineStyle: { - color: '#d9d9d9', - }, - }, - axisLabel: { - color: '#8c8c8c', - fontSize: 11, - }, - }, - ], - yAxis: [ - { - type: 'value', - name: '工单数', - nameTextStyle: { - color: '#8c8c8c', - fontSize: 12, - }, - axisLine: { - show: false, - }, - axisLabel: { - color: '#8c8c8c', - }, - splitLine: { - lineStyle: { - color: '#f0f0f0', - type: 'dashed', - }, - }, - }, - ], - series: [ - { - name: '工单数量', - type: 'bar', - barWidth: '60%', - data: hourlyDistribution.data, - itemStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: '#1677ff' }, - { offset: 1, color: '#69b1ff' }, - ], - }, - borderRadius: [4, 4, 0, 0], - }, - emphasis: { - itemStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: '#1677ff' }, - { offset: 1, color: '#40a9ff' }, - ], - }, - }, - }, + emphasis: { focus: 'series' }, + data: monthly.lastMonth.createdData, + itemStyle: { color: '#52c41a' }, }, ], }; @@ -501,53 +393,95 @@ function getTimeTrendChartOptions(): ECOption { } /** - * 工单处理漏斗图表配置 + * 工单状态分布环形饼图配置 */ function getFunnelChartOptions(): ECOption { const { funnelData } = statsData.value; + const STATUS_COLORS: Record = { + 待处理: '#f5a623', + 排队中: '#8b5cf6', + 已派单: '#3b82f6', + 已到岗: '#06b6d4', + 已完成: '#10b981', + 已取消: '#f43f5e', + 已暂停: '#94a3b8', + }; + const total = funnelData.reduce((sum, item) => sum + item.value, 0); return { tooltip: { trigger: 'item', - formatter: '{b}: {c}个 ({d}%)', + backgroundColor: 'rgba(255, 255, 255, 0.96)', + borderColor: '#e5e7eb', + borderWidth: 1, + textStyle: { color: '#374151', fontSize: 13 }, + formatter: (params: any) => { + const marker = ``; + return `${marker}${params.name}
${params.value}个 · ${params.percent}%`; + }, }, + legend: { + orient: 'vertical', + right: '2%', + top: 'center', + itemWidth: 8, + itemHeight: 8, + itemGap: 12, + icon: 'circle', + textStyle: { fontSize: 12, color: '#6b7280' }, + }, + graphic: [ + { + type: 'text', + left: '28%', + top: '40%', + style: { + text: `${total}`, + fontSize: 22, + fontWeight: '600', + fill: '#1f2937', + textAlign: 'center', + }, + }, + { + type: 'text', + left: '28%', + top: '54%', + style: { + text: '近7天工单', + fontSize: 11, + fill: '#9ca3af', + textAlign: 'center', + }, + }, + ], series: [ { - name: '工单处理流程', - type: 'funnel', - left: '10%', - top: '5%', - bottom: '5%', - width: '80%', - minSize: '0%', - maxSize: '100%', - sort: 'descending', - gap: 4, - label: { - show: true, - position: 'inside', - formatter: '{b}\n{c}个', - fontSize: 13, - color: '#fff', - }, - labelLine: { - show: false, + name: '工单状态分布', + type: 'pie', + radius: ['50%', '72%'], + center: ['30%', '50%'], + avoidLabelOverlap: false, + label: { show: false }, + emphasis: { + scale: true, + scaleSize: 6, + label: { + show: true, + fontSize: 13, + fontWeight: '600', + color: '#374151', + formatter: '{b}\n{c}个', + }, }, itemStyle: { borderColor: '#fff', - borderWidth: 1, + borderWidth: 2, + borderRadius: 4, }, - emphasis: { - label: { - fontSize: 15, - fontWeight: 'bold', - }, - }, - data: funnelData.map((item, index) => ({ + data: funnelData.map((item) => ({ ...item, itemStyle: { - color: ['#1677ff', '#40a9ff', '#69b1ff', '#91caff', '#b3d4ff'][ - index - ], + color: STATUS_COLORS[item.name] || '#d1d5db', }, })), }, @@ -627,7 +561,7 @@ function getHeatmapChartOptions(): ECOption { }, visualMap: { min: 0, - max: 10, + max: Math.max(10, ...data.map((d: number[]) => d[2] || 0)), calculable: false, orient: 'horizontal', left: 'center', @@ -740,6 +674,61 @@ function getFunctionTypeRankingChartOptions(): ECOption { } } +/** + * 工牌近7天队列数量统计图表配置 + */ +function getBadgeQueueChartOptions(): ECOption { + const queue = statsData.value.badgeQueueStats; + if (!queue || queue.dates.length === 0) return {}; + return { + tooltip: { + trigger: 'axis', + formatter: (params: any) => { + const param = params[0]; + return `${param.name}
队列数量: ${param.value}`; + }, + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + top: '10%', + containLabel: true, + }, + xAxis: [{ + type: 'category', + data: queue.dates, + axisLine: { lineStyle: { color: '#d9d9d9' } }, + axisLabel: { color: '#8c8c8c', fontSize: 11 }, + }], + yAxis: [{ + type: 'value', + name: '队列数', + nameTextStyle: { color: '#8c8c8c', fontSize: 12 }, + axisLine: { show: false }, + axisLabel: { color: '#8c8c8c' }, + splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } }, + }], + series: [{ + name: '队列数量', + type: 'bar', + barWidth: '50%', + data: queue.queueCounts, + itemStyle: { + color: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: '#722ed1' }, + { offset: 1, color: '#b37feb' }, + ], + }, + borderRadius: [4, 4, 0, 0], + }, + }], + }; +} + // ========== 数据加载 ========== /** 加载统计数据 */ @@ -759,17 +748,19 @@ async function loadStats() { funnelData: resp.funnelData, heatmapData: resp.heatmapData, functionTypeRanking: resp.functionTypeRanking, + monthlyTrendData: resp.monthlyTrendData, + badgeQueueStats: resp.badgeQueueStats, }; // 渲染图表 chartLoading.value = false; await nextTick(); renderTrendChart(getTrendChartOptions()); - renderHourlyChart(getHourlyChartOptions()); + renderHourlyChart(getHeatmapChartOptions()); renderTimeTrendChart(getTimeTrendChartOptions()); renderFunnelChart(getFunnelChartOptions()); - renderHeatmapChart(getHeatmapChartOptions()); renderFunctionTypeRankingChart(getFunctionTypeRankingChartOptions()); + renderBadgeQueueChart(getBadgeQueueChartOptions()); } catch { // API失败时使用空数据作为fallback statsData.value = { ...EMPTY_STATS }; @@ -894,12 +885,12 @@ onUnmounted(stopPolling); - + - +