feat(ops): 优化仪表盘图表与客流统计交互

- 工单仪表盘:趋势图改为当月vs上月对比,漏斗图改为状态分布环形饼图,新增工牌队列统计图表,移除无用的 hourly/heatmap 死代码
- 客流统计:小时趋势和趋势图支持日期选择器,移除昨日对比线,API 支持日期参数
- 工作台:紧急任务过滤已取消和已完成状态,在岗人员仅显示在岗数

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-01 00:05:12 +08:00
parent bac318aa46
commit 482d892b24
5 changed files with 339 additions and 300 deletions

View File

@@ -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天客流趋势响应 */

View File

@@ -76,13 +76,17 @@ export interface TrafficTrendQuery {
// ==================== 迁移接口(路径变更) ====================
/** 获取全局实时客流数据 */
export function getTrafficRealtime() {
return requestClient.get<TrafficRealtimeResp>('/ops/traffic/realtime');
export function getTrafficRealtime(date?: string) {
return requestClient.get<TrafficRealtimeResp>('/ops/traffic/realtime', {
params: date ? { date } : undefined,
});
}
/** 获取全局近7天客流趋势 */
export function getTrafficTrend() {
return requestClient.get<TrafficTrendResp>('/ops/traffic/trend');
export function getTrafficTrend(params?: TrafficTrendQuery) {
return requestClient.get<TrafficTrendResp>('/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<TrafficRealtimeResp>('/ops/traffic/area/realtime', {
params: { areaIds: areaIds.join(',') },
params: { areaIds: areaIds.join(','), ...(date ? { date } : {}) },
});
}

View File

@@ -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',
},

View File

@@ -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<DashboardStats>({
pendingCount: 0,
@@ -129,6 +143,8 @@ const statsData = ref<DashboardStats>({
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}<br/>工单数: ${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<string, string> = {
待处理: '#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 = `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${params.color};margin-right:6px;"></span>`;
return `${marker}${params.name}<br/><span style="font-weight:600">${params.value}</span>个 · ${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}<br/>队列数量: ${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);
</Col>
</Row>
<!-- 第二行工单趋势 + 工单时段分布 -->
<!-- 第二行工单趋势当月vs上月 + 时段热力图近7天 -->
<Row :gutter="[12, 12]" class="mb-3">
<Col :xs="24" :lg="16">
<Card class="chart-card" title="工单趋势(近7天">
<Card class="chart-card" title="工单趋势(当月 vs 上月">
<template #extra>
<Tooltip title="展示最近7天的工单新增与完成情况">
<Tooltip title="展示当月与上月的新增工单对比">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
@@ -913,9 +904,9 @@ onUnmounted(stopPolling);
</Col>
<Col :xs="24" :lg="8">
<Card class="chart-card" title="今日工单时段分布">
<Card class="chart-card" title="时段热力图近7天">
<template #extra>
<Tooltip title="展示今日24小时各时段的工单数量分布">
<Tooltip title="展示近7天各时段的工单数量热力分布">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
@@ -929,9 +920,9 @@ onUnmounted(stopPolling);
</Col>
</Row>
<!-- 第三行平均耗时趋势 + 工单处理漏斗 -->
<!-- 第三行平均耗时趋势 + 工单状态分布 -->
<Row :gutter="[12, 12]" class="mb-3">
<Col :xs="24" :lg="12">
<Col :xs="24" :lg="16">
<Card class="chart-card" title="平均耗时趋势近7天">
<template #extra>
<Tooltip title="展示平均响应时间和平均完成时间的变化趋势">
@@ -950,10 +941,10 @@ onUnmounted(stopPolling);
</Card>
</Col>
<Col :xs="24" :lg="12">
<Card class="chart-card" title="工单处理漏斗">
<Col :xs="24" :lg="8">
<Card class="chart-card" title="工单状态分布近7天">
<template #extra>
<Tooltip title="展示工单从创建到完成各环节的转化情况">
<Tooltip title="展示近7天工单的状态占比分布">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
@@ -970,16 +961,16 @@ onUnmounted(stopPolling);
</Col>
</Row>
<!-- 第四行时段热力图近7天 + 功能类型排行 -->
<!-- 第四行工牌队列统计近7天 + 功能类型排行 -->
<Row :gutter="[12, 12]">
<!-- 时段热力图近7天 -->
<!-- 工牌队列统计近7天 -->
<Col :xs="24" :md="12" :lg="12">
<Card class="modern-card modern-card--heatmap">
<div class="modern-header">
<span class="modern-title">时段热力图近7天</span>
<span class="modern-title">工牌队列统计近7天</span>
</div>
<Spin :spinning="chartLoading">
<EchartsUI ref="heatmapChartRef" class="modern-chart" />
<EchartsUI ref="badgeQueueChartRef" class="modern-chart" />
</Spin>
</Card>
</Col>

View File

@@ -12,21 +12,28 @@ import {
onMounted,
onUnmounted,
ref,
watch,
} from 'vue';
import { IconifyIcon } from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { useDebounceFn } from '@vueuse/core';
import {
Alert,
Card,
Col,
DatePicker,
RangePicker,
Row,
Spin,
Statistic,
Tooltip,
} from 'ant-design-vue';
import dayjs from 'dayjs';
import {
getAreaRealtime,
getAreaTrend,
@@ -65,6 +72,18 @@ function handleAreaSelect(area: null | OpsAreaApi.BusArea) {
const loading = ref(true);
const chartLoading = ref(true);
// 小时趋势日期选择(默认今日)
const hourlyDate = ref<dayjs.Dayjs>(dayjs());
// 趋势图日期范围选择默认近7天
const trendDateRange = ref<[dayjs.Dayjs, dayjs.Dayjs]>([
dayjs().subtract(6, 'day'),
dayjs(),
]);
// 限制趋势范围最多1个月
function disabledTrendDate(current: dayjs.Dayjs) {
return current && current > dayjs().endOf('day');
}
const realtimeData = ref<TrafficRealtimeResp>({
totalIn: 0,
totalOut: 0,
@@ -96,12 +115,9 @@ function getHourlyChartOptions(): ECOption {
if (!hourly || hourly.hours.length === 0) return {};
// 昨日数据
const yesterdayHourly = realtimeData.value.yesterdayHourlyTrend;
const series: any[] = [
{
name: '今日进入',
name: '进入',
type: 'line',
smooth: true,
symbol: 'circle',
@@ -125,7 +141,7 @@ function getHourlyChartOptions(): ECOption {
},
},
{
name: '今日离开',
name: '离开',
type: 'line',
smooth: true,
symbol: 'circle',
@@ -150,32 +166,7 @@ function getHourlyChartOptions(): ECOption {
},
];
const legendData = ['今日进入', '今日离开'];
// 昨日对比线
if (yesterdayHourly && yesterdayHourly.hours.length > 0) {
legendData.push('昨日进入', '昨日离开');
series.push(
{
name: '昨日进入',
type: 'line',
smooth: true,
symbol: 'none',
data: yesterdayHourly.inData,
lineStyle: { width: 1.5, color: '#b5f5ec', type: 'dashed' },
itemStyle: { color: '#b5f5ec' },
},
{
name: '昨日离开',
type: 'line',
smooth: true,
symbol: 'none',
data: yesterdayHourly.outData,
lineStyle: { width: 1.5, color: '#adc6ff', type: 'dashed' },
itemStyle: { color: '#adc6ff' },
},
);
}
const legendData = ['进入', '离开'];
return {
tooltip: { trigger: 'axis' },
@@ -302,9 +293,14 @@ function getTrendChartOptions(): ECOption {
async function loadGlobalData() {
loading.value = true;
try {
const dateStr = hourlyDate.value.format('YYYY-MM-DD');
const [startDate, endDate] = trendDateRange.value;
const [realtime, trend] = await Promise.all([
getTrafficRealtime(),
getTrafficTrend(),
getTrafficRealtime(dateStr),
getTrafficTrend({
startDate: startDate.format('YYYY-MM-DD'),
endDate: endDate.format('YYYY-MM-DD'),
}),
]);
realtimeData.value = realtime;
trendData.value = trend;
@@ -323,9 +319,15 @@ async function loadGlobalData() {
async function loadAreaData(areaIds: number[]) {
loading.value = true;
try {
const dateStr = hourlyDate.value.format('YYYY-MM-DD');
const [startDate, endDate] = trendDateRange.value;
const [realtime, trend] = await Promise.all([
getAreaRealtime(areaIds),
getAreaTrend({ areaIds: areaIds.join(',') }),
getAreaRealtime(areaIds, dateStr),
getAreaTrend({
areaIds: areaIds.join(','),
startDate: startDate.format('YYYY-MM-DD'),
endDate: endDate.format('YYYY-MM-DD'),
}),
]);
realtimeData.value = realtime;
trendData.value = trend;
@@ -422,6 +424,17 @@ const statDailyAvg = computed(() => {
return Math.round(totalAll / t.inData.length);
});
// ========== 日期变化时重新加载(防抖避免并发请求) ==========
const debouncedLoadData = useDebounceFn(loadData, 300);
watch(hourlyDate, () => {
debouncedLoadData();
});
watch(trendDateRange, () => {
debouncedLoadData();
});
// ========== 生命周期 ==========
let refreshTimer: null | ReturnType<typeof setInterval> = null;
@@ -566,14 +579,21 @@ onUnmounted(stopPolling);
<Col :xs="24" :lg="16">
<Card class="chart-card chart-card--full" title="小时客流趋势">
<template #extra>
<Tooltip
title="展示今日与昨日24小时各时段的进入和离开人次对比"
>
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
<div style="display: flex; align-items: center; gap: 8px">
<DatePicker
v-model:value="hourlyDate"
:allow-clear="false"
:disabled-date="disabledTrendDate"
size="small"
style="width: 130px"
/>
</Tooltip>
<Tooltip title="展示选中日期24小时各时段的进入和离开人次">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
/>
</Tooltip>
</div>
</template>
<Spin :spinning="chartLoading">
<EchartsUI ref="hourlyChartRef" class="chart-container" />
@@ -696,14 +716,23 @@ onUnmounted(stopPolling);
<!-- 7天趋势 -->
<Row :gutter="[12, 12]">
<Col :span="24">
<Card class="chart-card" title="客流趋势近7天">
<Card class="chart-card" title="客流趋势">
<template #extra>
<Tooltip title="展示最近7天的客流进入、离开和净流量趋势">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
<div style="display: flex; align-items: center; gap: 8px">
<RangePicker
v-model:value="trendDateRange"
:allow-clear="false"
:disabled-date="disabledTrendDate"
size="small"
style="width: 240px"
/>
</Tooltip>
<Tooltip title="展示选定时间范围内的客流进入和离开趋势">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
/>
</Tooltip>
</div>
</template>
<Spin :spinning="chartLoading">
<EchartsUI ref="trendChartRef" class="chart-container" />