Skip to content

Commit 7207d19

Browse files
committed
style(stats-page): 优化数据统计界面UI设计 - 添加关键指标卡片(总访问、今日访问、创建时间、转化率) - 改进短链信息展示(代码块、链接样式、原始URL) - 优化城市排名展示(数字排名、渐进式柱状图) - 添加Hover动画和视觉反馈 - 提升整体美观度和信息层级
1 parent 2831208 commit 7207d19

File tree

1 file changed

+187
-35
lines changed

1 file changed

+187
-35
lines changed

web/src/pages/StatsPage.vue

Lines changed: 187 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -113,37 +113,65 @@
113113
</div>
114114
</div>
115115

116-
<!-- 详情模块(精简版) -->
117-
<div class="q-card p-6">
118-
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 items-start">
119-
<div>
120-
<div class="q-muted text-[13px]">短码</div>
121-
<div class="mt-1 q-strong">{{ shortCode }}</div>
122-
</div>
123-
<div>
124-
<div class="q-muted text-[13px]">短链</div>
125-
<a :href="shortUrl" target="_blank" class="mt-1 q-link">{{ shortLabel }}</a>
126-
</div>
127-
<div>
128-
<div class="q-muted text-[13px]">创建时间</div>
129-
<div class="mt-1 q-strong">{{ formatDate(overview?.createdAt) }}</div>
130-
</div>
131-
<div>
132-
<div class="q-muted text-[13px]">总访问量</div>
133-
<div class="mt-1 q-nums">{{ overview?.totalVisits ?? '-' }}</div>
134-
</div>
135-
<div>
136-
<div class="q-muted text-[13px]">今日访问量</div>
137-
<div class="mt-1 q-nums">{{ overview?.todayVisits ?? '-' }}</div>
116+
<!-- 关键指标卡片 -->
117+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
118+
<div class="stat-card">
119+
<div class="stat-label">总访问量</div>
120+
<div class="stat-value">{{ overview?.totalVisits ?? '-' }}</div>
121+
<div class="stat-unit">次</div>
122+
</div>
123+
<div class="stat-card">
124+
<div class="stat-label">今日访问量</div>
125+
<div class="stat-value">{{ overview?.todayVisits ?? '-' }}</div>
126+
<div class="stat-unit">次</div>
127+
</div>
128+
<div class="stat-card">
129+
<div class="stat-label">创建时间</div>
130+
<div class="stat-value-text">{{ formatDate(overview?.createdAt) }}</div>
131+
</div>
132+
<div class="stat-card">
133+
<div class="stat-label">访问转化</div>
134+
<div class="stat-value">{{ overview?.totalVisits ? Math.round((overview.todayVisits / overview.totalVisits) * 100) : 0 }}%</div>
135+
<div class="stat-unit">今日占比</div>
136+
</div>
137+
</div>
138+
139+
<!-- 短链详情与Top城市 -->
140+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
141+
<div class="col-span-1 md:col-span-2">
142+
<div class="q-card p-6">
143+
<div class="q-card-title mb-4">短链信息</div>
144+
<div class="space-y-4">
145+
<div class="detail-row">
146+
<span class="detail-label">短码</span>
147+
<code class="detail-value code-block">{{ shortCode }}</code>
148+
</div>
149+
<div class="detail-row">
150+
<span class="detail-label">短链地址</span>
151+
<a :href="shortUrl" target="_blank" class="detail-value link-style">{{ shortLabel }}</a>
152+
</div>
153+
<div class="detail-row">
154+
<span class="detail-label">原始链接</span>
155+
<div class="mt-2 text-xs font-mono bg-gray-100 p-2 rounded break-all text-gray-600">{{ overview?.longUrl || '-' }}</div>
156+
</div>
157+
</div>
138158
</div>
139-
<div>
140-
<div class="q-muted text-[13px]">Top 城市</div>
141-
<div class="mt-2 space-y-1">
142-
<div v-for="c in cityData" :key="c.label" class="flex items-center justify-between">
143-
<span class="q-muted">{{ c.label }}</span>
144-
<span class="q-nums">{{ c.value }}</span>
159+
</div>
160+
<div>
161+
<div class="q-card p-6">
162+
<div class="q-card-title mb-4">城市 Top 5</div>
163+
<div class="space-y-2">
164+
<div v-for="(c, idx) in cityData" :key="c.label" class="city-item">
165+
<div class="flex items-center justify-between">
166+
<span class="city-rank">{{ idx + 1 }}</span>
167+
<span class="city-name flex-1 ml-3">{{ c.label }}</span>
168+
<span class="city-count">{{ c.value }}</span>
169+
</div>
170+
<div class="city-bar">
171+
<div class="city-bar-fill" :style="{ width: (c.value / (cityData[0]?.value || 1) * 100) + '%' }"></div>
172+
</div>
145173
</div>
146-
<div v-if="!cityData.length" class="q-muted">{{ $t('common.noData') }}</div>
174+
<div v-if="!cityData.length" class="text-center py-4 text-gray-400">{{ $t('common.noData') }}</div>
147175
</div>
148176
</div>
149177
</div>
@@ -296,6 +324,135 @@ svg { width: 20px; height: 20px; }
296324
.tf-modal { width: 680px; max-width: 92vw; border-radius: 16px; padding: 24px; background: linear-gradient(135deg,#ffffff 0%, #f3f7ff 100%); box-shadow: 0 12px 40px rgba(37,99,235,0.22); border: 1px solid rgba(37,99,235,0.18); }
297325
.tf-modal-header { font-size: 16px; font-weight: 600; color: #2563EB; margin-bottom: 16px; }
298326
327+
/* 统计卡片窗口 */
328+
.stat-card {
329+
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
330+
border: 1px solid rgba(226, 232, 240, 0.6);
331+
border-radius: 12px;
332+
padding: 20px;
333+
transition: all 0.3s ease;
334+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
335+
}
336+
.stat-card:hover {
337+
transform: translateY(-2px);
338+
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.15);
339+
border-color: rgba(37, 99, 235, 0.3);
340+
}
341+
.stat-label {
342+
font-size: 13px;
343+
color: #64748b;
344+
font-weight: 500;
345+
margin-bottom: 8px;
346+
text-transform: uppercase;
347+
letter-spacing: 0.5px;
348+
}
349+
.stat-value {
350+
font-size: 32px;
351+
font-weight: 700;
352+
color: #1e293b;
353+
line-height: 1;
354+
margin-bottom: 4px;
355+
}
356+
.stat-value-text {
357+
font-size: 14px;
358+
color: #334155;
359+
font-weight: 500;
360+
word-break: break-all;
361+
}
362+
.stat-unit {
363+
font-size: 12px;
364+
color: #94a3b8;
365+
font-weight: 400;
366+
}
367+
368+
/* 详情行样式 */
369+
.detail-row {
370+
display: flex;
371+
align-items: flex-start;
372+
gap: 16px;
373+
padding: 12px 0;
374+
border-bottom: 1px solid #f1f5f9;
375+
}
376+
.detail-row:last-child {
377+
border-bottom: none;
378+
}
379+
.detail-label {
380+
min-width: 80px;
381+
font-size: 13px;
382+
color: #64748b;
383+
font-weight: 500;
384+
flex-shrink: 0;
385+
}
386+
.detail-value {
387+
font-size: 14px;
388+
color: #1e293b;
389+
flex: 1;
390+
word-break: break-all;
391+
}
392+
.code-block {
393+
background: #f1f5f9;
394+
padding: 6px 10px;
395+
border-radius: 6px;
396+
color: #2563eb;
397+
font-weight: 500;
398+
font-size: 13px;
399+
}
400+
.link-style {
401+
color: #2563eb;
402+
text-decoration: none;
403+
border-bottom: 1px dashed #2563eb;
404+
transition: all 0.2s ease;
405+
}
406+
.link-style:hover {
407+
color: #1d4ed8;
408+
border-bottom-color: #1d4ed8;
409+
}
410+
411+
/* 城市接流样式 */
412+
.city-item {
413+
padding: 8px 0;
414+
}
415+
.city-rank {
416+
display: inline-flex;
417+
align-items: center;
418+
justify-content: center;
419+
width: 24px;
420+
height: 24px;
421+
border-radius: 50%;
422+
background: linear-gradient(135deg, #2563eb, #1d4ed8);
423+
color: white;
424+
font-size: 12px;
425+
font-weight: 600;
426+
flex-shrink: 0;
427+
}
428+
.city-name {
429+
font-size: 13px;
430+
color: #334155;
431+
font-weight: 500;
432+
}
433+
.city-count {
434+
font-size: 13px;
435+
color: #2563eb;
436+
font-weight: 600;
437+
min-width: 40px;
438+
text-align: right;
439+
flex-shrink: 0;
440+
}
441+
.city-bar {
442+
width: 100%;
443+
height: 4px;
444+
background: #e2e8f0;
445+
border-radius: 2px;
446+
margin-top: 4px;
447+
overflow: hidden;
448+
}
449+
.city-bar-fill {
450+
height: 100%;
451+
background: linear-gradient(90deg, #2563eb, #3b82f6);
452+
border-radius: 2px;
453+
transition: width 0.3s ease;
454+
}
455+
299456
.fx-btn { font-family: Arial, Helvetica, sans-serif; font-weight: bold; color: #0F172A; background-color: #F8FAFC; padding: 0.75em 1.4em; border: 1px solid rgba(148,163,184,0.6); border-radius: 0.6rem; position: relative; cursor: pointer; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; }
300457
.fx-btn span:not(.btn-text) { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); height: 26px; width: 26px; background-color: #2563EB; border-radius: 50%; transition: .6s ease; }
301458
.fx-btn span:nth-child(1) { transform: translate(-3.3em, -4em); }
@@ -309,9 +466,4 @@ svg { width: 20px; height: 20px; }
309466
.fx-gray { background-color: #EFF6FF; }
310467
.fx-sm { padding: 0.4em 0.9em; }
311468
.fx-sm span:not(.btn-text) { height: 18px; width: 18px; }
312-
</style>
313-
function selectDays(d){ selectedDays.value = d; refreshTrend() }
314-
watch(selectedDays, async () => {
315-
await refreshTrend()
316-
await refreshAll()
317-
})
469+
</style>

0 commit comments

Comments
 (0)