Skip to content

Commit 8cadd72

Browse files
committed
feat: 优化数据看板视觉效果,提升低数据量场景下的展示质量
- 添加指标卡片趋势标签和智能说明 - 添加图表智能洞察(访问趋势分析、设备分布总结) - 优化排名列表:添加可视化条形图、TOP3高亮显示 - 优化空状态设计:清晰的说明文字和引导提示 - 移除所有emoji图标,采用极简优雅设计风格 - 统一使用蓝色主题,符合无渐变设计规范
1 parent acdec02 commit 8cadd72

File tree

1 file changed

+239
-15
lines changed

1 file changed

+239
-15
lines changed

web/src/pages/DashboardPage.vue

Lines changed: 239 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,37 @@
1616
<div class="metric-card">
1717
<div class="metric-label">总短链数</div>
1818
<div class="metric-value">{{ globalStats?.totalUrls ?? '-' }}</div>
19+
<div class="metric-trend" v-if="globalStats?.totalUrls">
20+
<span class="trend-icon">•</span> 系统总计
21+
</div>
1922
</div>
2023
<div class="metric-card">
2124
<div class="metric-label">总点击量(PV)</div>
2225
<div class="metric-value">{{ globalStats?.totalClicks ?? '-' }}</div>
26+
<div class="metric-trend positive" v-if="globalStats?.totalClicks">
27+
<span class="trend-icon">↑</span> 累计访问
28+
</div>
2329
</div>
2430
<div class="metric-card">
2531
<div class="metric-label">独立访客(UV)</div>
2632
<div class="metric-value">{{ globalStats?.totalUniqueIps ?? '-' }}</div>
33+
<div class="metric-trend" v-if="globalStats?.totalUniqueIps">
34+
<span class="trend-icon">•</span> 独立 IP
35+
</div>
2736
</div>
28-
<div class="metric-card">
37+
<div class="metric-card highlight">
2938
<div class="metric-label">今日点击</div>
3039
<div class="metric-value">{{ globalStats?.todayClicks ?? '-' }}</div>
40+
<div class="metric-trend positive" v-if="globalStats?.todayClicks">
41+
<span class="trend-icon">•</span> 实时数据
42+
</div>
3143
</div>
3244
<div class="metric-card">
3345
<div class="metric-label">活跃短链</div>
3446
<div class="metric-value">{{ globalStats?.activeUrls ?? '-' }}</div>
47+
<div class="metric-trend" v-if="globalStats?.activeUrls">
48+
<span class="trend-icon">•</span> 有访问记录
49+
</div>
3550
</div>
3651
</div>
3752

@@ -48,11 +63,15 @@
4863
</div>
4964
</div>
5065
<div class="card-body">
51-
<div class="chart-container">
66+
<div class="chart-container-enhanced">
5267
<Suspense>
5368
<TrendChart :values="dailyTrendValues" :labels="dailyTrendLabels" :showValues="true" />
54-
<template #fallback><div class="chart-placeholder"></div></template>
69+
<template #fallback><div class="chart-placeholder">加载中...</div></template>
5570
</Suspense>
71+
<!-- 数据洞察提示 -->
72+
<div v-if="trendInsight" class="chart-insight">
73+
<span class="insight-text">{{ trendInsight }}</span>
74+
</div>
5675
</div>
5776
</div>
5877
</div>
@@ -61,24 +80,36 @@
6180
<div class="card">
6281
<div class="card-header">设备分布</div>
6382
<div class="card-body">
64-
<Suspense>
65-
<PieDonut :data="devicePieData" :colors="['#2563eb', '#60a5fa', '#93c5fd', '#bfdbfe']" />
66-
<template #fallback><div class="chart-placeholder"></div></template>
67-
</Suspense>
83+
<div class="chart-container-enhanced">
84+
<Suspense>
85+
<PieDonut :data="devicePieData" :colors="['#2563eb', '#60a5fa', '#93c5fd', '#bfdbfe']" />
86+
<template #fallback><div class="chart-placeholder">加载中...</div></template>
87+
</Suspense>
88+
<!-- 设备统计说明 -->
89+
<div v-if="deviceSummary" class="chart-insight">
90+
<span class="insight-text">{{ deviceSummary }}</span>
91+
</div>
92+
</div>
6893
</div>
6994
</div>
7095

7196
<!-- 城市TOP10 -->
7297
<div class="card">
7398
<div class="card-header">城市 TOP 10</div>
7499
<div class="card-body">
75-
<div class="rank-list">
76-
<div v-for="(item, idx) in cityTop10" :key="item.key" class="rank-item">
77-
<span class="rank-num">{{ idx + 1 }}</span>
100+
<div class="rank-list-enhanced">
101+
<div v-for="(item, idx) in cityTop10" :key="item.key" class="rank-item-enhanced">
102+
<span class="rank-badge" :class="{ 'rank-top3': idx < 3 }">{{ idx + 1 }}</span>
78103
<span class="rank-name">{{ item.key || '未知' }}</span>
104+
<div class="rank-bar">
105+
<div class="rank-bar-fill" :style="{ width: getBarWidth(item.count, cityTop10) + '%' }"></div>
106+
</div>
79107
<span class="rank-count">{{ item.count }}</span>
80108
</div>
81-
<div v-if="!cityTop10.length" class="empty-state">暂无数据</div>
109+
<div v-if="!cityTop10.length" class="empty-state-enhanced">
110+
<div class="empty-text">暂无城市数据</div>
111+
<div class="empty-hint">当有访问者点击短链时,此处将显示城市分布</div>
112+
</div>
82113
</div>
83114
</div>
84115
</div>
@@ -87,13 +118,19 @@
87118
<div class="card">
88119
<div class="card-header">来源域名 TOP 10</div>
89120
<div class="card-body">
90-
<div class="rank-list">
91-
<div v-for="(item, idx) in sourceTop10" :key="item.key" class="rank-item">
92-
<span class="rank-num">{{ idx + 1 }}</span>
121+
<div class="rank-list-enhanced">
122+
<div v-for="(item, idx) in sourceTop10" :key="item.key" class="rank-item-enhanced">
123+
<span class="rank-badge" :class="{ 'rank-top3': idx < 3 }">{{ idx + 1 }}</span>
93124
<span class="rank-name truncate">{{ item.key || '直接访问' }}</span>
125+
<div class="rank-bar">
126+
<div class="rank-bar-fill" :style="{ width: getBarWidth(item.count, sourceTop10) + '%' }"></div>
127+
</div>
94128
<span class="rank-count">{{ item.count }}</span>
95129
</div>
96-
<div v-if="!sourceTop10.length" class="empty-state">暂无数据</div>
130+
<div v-if="!sourceTop10.length" class="empty-state-enhanced">
131+
<div class="empty-text">暂无来源数据</div>
132+
<div class="empty-hint">系统将记录访问者来源,分析流量渠道</div>
133+
</div>
97134
</div>
98135
</div>
99136
</div>
@@ -258,6 +295,44 @@ const devicePieData = computed(() => {
258295
}))
259296
})
260297
298+
// 趋势洞察
299+
const trendInsight = computed(() => {
300+
const values = dailyTrendValues.value
301+
if (!values || values.length < 2) return ''
302+
303+
const total = values.reduce((a, b) => a + b, 0)
304+
const avg = Math.round(total / values.length)
305+
const lastValue = values[values.length - 1]
306+
const prevValue = values[values.length - 2]
307+
308+
if (lastValue > prevValue) {
309+
const growth = Math.round((lastValue - prevValue) / Math.max(prevValue, 1) * 100)
310+
return `${trendDays.value}天平均每天 ${avg} 次访问,最近一天增长 ${growth}%`
311+
} else if (lastValue < prevValue) {
312+
return `${trendDays.value}天平均每天 ${avg} 次访问,最近一天略有下降`
313+
} else {
314+
return `${trendDays.value}天平均每天 ${avg} 次访问,访问量保持稳定`
315+
}
316+
})
317+
318+
// 设备分布总结
319+
const deviceSummary = computed(() => {
320+
const devices = deviceDistribution.value
321+
if (!devices || devices.length === 0) return ''
322+
323+
const total = devices.reduce((a, b) => a + b.count, 0)
324+
const topDevice = devices.reduce((max, item) => item.count > max.count ? item : max, devices[0])
325+
const percent = Math.round(topDevice.count / total * 100)
326+
327+
const deviceName = {
328+
'desktop': '桌面设备',
329+
'mobile': '移动设备',
330+
'tablet': '平板设备'
331+
}[topDevice.key] || topDevice.key
332+
333+
return `${deviceName}访问占比最高,达 ${percent}%`
334+
})
335+
261336
const filteredList = computed(() => {
262337
const q = searchQuery.value.trim().toLowerCase()
263338
const list = urlList.value || []
@@ -286,6 +361,13 @@ function getSharePercent(url) {
286361
return totalClicks.value > 0 ? (Number(url.totalVisits || 0) / totalClicks.value * 100) : 0
287362
}
288363
364+
// 计算条形图宽度
365+
function getBarWidth(count, dataArray) {
366+
if (!dataArray || dataArray.length === 0) return 0
367+
const maxCount = Math.max(...dataArray.map(item => item.count))
368+
return maxCount > 0 ? (count / maxCount * 100) : 0
369+
}
370+
289371
// 复制状态
290372
const copyingCode = ref(null)
291373
@@ -334,11 +416,15 @@ async function fetchUrlList() {
334416
try {
335417
const res = await axios.get(`${API_BASE}/api/urls?page=${page.value - 1}&size=${pageSize}`)
336418
const data = res.data
419+
console.log('fetchUrlList response:', data) // 调试信息
337420
urlList.value = Array.isArray(data) ? data : (data.content || data.items || [])
338421
totalElements.value = data.totalElements || urlList.value.length
339422
totalPages.value = data.totalPages || Math.ceil(totalElements.value / pageSize) || 1
423+
console.log('urlList.value:', urlList.value) // 调试信息
424+
console.log('totalElements:', totalElements.value)
340425
} catch (e) {
341426
console.error('fetchUrlList error:', e)
427+
urlList.value = []
342428
} finally {
343429
loading.value = false
344430
}
@@ -748,4 +834,142 @@ tr:nth-child(n+4) .rank-badge {
748834
text-overflow: ellipsis;
749835
white-space: nowrap;
750836
}
837+
838+
/* ========== 新增:增强视觉样式 ========== */
839+
840+
/* 指标卡片增强 */
841+
.metric-card.highlight {
842+
border-color: #3b82f6;
843+
background: #eff6ff;
844+
}
845+
846+
.metric-trend {
847+
margin-top: 8px;
848+
font-size: 12px;
849+
color: #64748b;
850+
display: flex;
851+
align-items: center;
852+
justify-content: center;
853+
gap: 4px;
854+
}
855+
856+
.metric-trend.positive {
857+
color: #16a34a;
858+
}
859+
860+
.trend-icon {
861+
font-size: 14px;
862+
}
863+
864+
/* 图表容器增强 */
865+
.chart-container-enhanced {
866+
min-height: 320px;
867+
position: relative;
868+
}
869+
870+
.chart-placeholder {
871+
height: 280px;
872+
display: flex;
873+
align-items: center;
874+
justify-content: center;
875+
color: #94a3b8;
876+
font-size: 14px;
877+
background: #f8fafc;
878+
border: 1px dashed #e2e8f0;
879+
border-radius: 6px;
880+
}
881+
882+
/* 图表洞察提示 */
883+
.chart-insight {
884+
margin-top: 12px;
885+
padding: 12px 16px;
886+
background: #f0f9ff;
887+
border-left: 3px solid #3b82f6;
888+
border-radius: 4px;
889+
font-size: 13px;
890+
color: #1e40af;
891+
}
892+
893+
.insight-text {
894+
line-height: 1.5;
895+
}
896+
897+
/* 排名列表增强 */
898+
.rank-list-enhanced {
899+
display: flex;
900+
flex-direction: column;
901+
gap: 12px;
902+
min-height: 200px;
903+
}
904+
905+
.rank-item-enhanced {
906+
display: grid;
907+
grid-template-columns: 32px 1fr 100px 60px;
908+
align-items: center;
909+
gap: 12px;
910+
padding: 10px 0;
911+
border-bottom: 1px solid #f1f5f9;
912+
}
913+
914+
.rank-item-enhanced:last-child {
915+
border-bottom: none;
916+
}
917+
918+
.rank-badge {
919+
width: 28px;
920+
height: 28px;
921+
display: flex;
922+
align-items: center;
923+
justify-content: center;
924+
background: #cbd5e1;
925+
color: white;
926+
font-size: 13px;
927+
font-weight: 600;
928+
border-radius: 50%;
929+
flex-shrink: 0;
930+
}
931+
932+
.rank-badge.rank-top3 {
933+
background: #3b82f6;
934+
}
935+
936+
.rank-bar {
937+
height: 8px;
938+
background: #f1f5f9;
939+
border-radius: 4px;
940+
overflow: hidden;
941+
position: relative;
942+
}
943+
944+
.rank-bar-fill {
945+
height: 100%;
946+
background: #3b82f6;
947+
border-radius: 4px;
948+
transition: width 0.5s ease;
949+
}
950+
951+
/* 空状态增强 */
952+
.empty-state-enhanced {
953+
padding: 60px 20px;
954+
text-align: center;
955+
min-height: 200px;
956+
display: flex;
957+
flex-direction: column;
958+
align-items: center;
959+
justify-content: center;
960+
}
961+
962+
.empty-text {
963+
font-size: 16px;
964+
font-weight: 500;
965+
color: #64748b;
966+
margin-bottom: 8px;
967+
}
968+
969+
.empty-hint {
970+
font-size: 13px;
971+
color: #94a3b8;
972+
max-width: 300px;
973+
line-height: 1.6;
974+
}
751975
</style>

0 commit comments

Comments
 (0)