feat(ops): 优化仪表盘图表与客流统计交互
- 工单仪表盘:趋势图改为当月vs上月对比,漏斗图改为状态分布环形饼图,新增工牌队列统计图表,移除无用的 hourly/heatmap 死代码 - 客流统计:小时趋势和趋势图支持日期选择器,移除昨日对比线,API 支持日期参数 - 工作台:紧急任务过滤已取消和已完成状态,在岗人员仅显示在岗数 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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天客流趋势响应 */
|
||||
|
||||
@@ -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 } : {}) },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user