feat: 优化仪表盘界面样式和交互细节

This commit is contained in:
Ray.Hao
2026-02-24 09:28:34 +08:00
parent eb4c4b7462
commit c617d2943f
8 changed files with 152 additions and 125 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div class="dashboard-container">
<div class="relative p-6">
<!-- github 角标 -->
<github-corner class="github-corner" />
<github-corner class="absolute top-0 right-0 z-1 border-0" />
<el-card shadow="never" class="mt-2">
<div class="flex flex-wrap">
@@ -15,7 +15,9 @@
/>
</div>
<div class="ml-5">
<p>{{ greetings }}</p>
<p class="text-base font-semibold text-[--el-text-color-primary] leading-tight">
{{ greetings }}
</p>
<p class="text-sm text-gray">今日天气晴朗气温在15至25之间东南风</p>
</div>
</div>
@@ -121,34 +123,45 @@
<el-row :gutter="10" class="mt-5">
<!-- 在线用户数量 -->
<el-col :span="8" :xs="24" class="mb-xs-3">
<el-card shadow="never" class="h-full flex flex-col">
<el-card
shadow="never"
class="h-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
>
<template #header>
<div class="flex-x-between">
<span class="text-gray">在线用户</span>
<el-tag type="danger" size="small">实时</el-tag>
<span class="text-xs font-medium text-[--el-text-color-secondary]">在线用户</span>
<div class="flex items-center gap-2">
<span
class="inline-flex items-center gap-1.5 px-2.5 py-0.5 text-xs leading-5 rounded-full border select-none"
:class="wsStatusClass"
>
<el-icon class="text-sm">
<Loading
v-if="
!isConnected &&
(connectionState === 'CONNECTING' || connectionState === 'RECONNECTING')
"
/>
<CircleCheck v-else-if="isConnected" />
<CircleClose v-else />
</el-icon>
<span class="text-[--el-text-color-secondary]">WebSocket</span>
<span class="font-medium">{{ wsStatusText }}</span>
</span>
</div>
</div>
</template>
<div class="flex-x-between mt-2 flex-1">
<div class="flex-y-center">
<span class="text-lg transition-all duration-300 hover:scale-110">
{{ onlineUserCount }}
</span>
<span v-if="isConnected" class="ml-2 text-xs text-[#67c23a]">
<el-icon><Connection /></el-icon>
已连接
</span>
<span v-else class="ml-2 text-xs text-[#f56c6c]">
<el-icon><Failed /></el-icon>
未连接
</span>
<div class="mt-2 flex-1 flex items-end">
<div class="flex items-baseline gap-1.5">
<span class="text-xl font-semibold tracking-wide">{{ onlineUserCount }}</span>
<span class="text-xs text-[--el-text-color-secondary]">人</span>
</div>
<div class="i-svg:people w-8 h-8 animate-[pulse_2s_infinite]" />
</div>
<div class="flex-x-between mt-2 text-sm text-gray">
<span>更新时间</span>
<span>{{ formattedTime }}</span>
<div class="mt-2 flex justify-between items-center">
<span class="text-sm text-gray">更新时间</span>
<span class="text-sm">{{ formattedTime }}</span>
</div>
</el-card>
</el-col>
@@ -176,37 +189,45 @@
</el-card>
</template>
<template v-if="!visitStatsLoading">
<el-card shadow="never" class="h-full flex flex-col">
<el-card
shadow="never"
class="h-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
>
<template #header>
<div class="flex-x-between">
<span class="text-gray">访客数(UV)</span>
<span class="text-xs font-medium text-[--el-text-color-secondary]">
访客数 UV
</span>
<el-tag type="success" size="small">日</el-tag>
</div>
</template>
<div class="flex-x-between mt-2 flex-1">
<div class="flex-y-center">
<span class="text-lg">{{ displayTransitionUvCount }}</span>
<div class="mt-2 flex-1 flex items-end">
<div class="flex items-baseline gap-1.5">
<span class="text-xl font-semibold tracking-wide">
{{ displayTransitionUvCount }}
</span>
<span
:class="[
'text-xs',
'ml-2',
computeGrowthRateClass(visitStatsData.uvGrowthRate),
]"
v-if="uvGrowthText !== null"
:class="['text-xs', computeGrowthRateClass(visitStatsData.uvGrowthRate)]"
>
<el-icon>
<el-icon
v-if="
visitStatsData.uvGrowthRate !== undefined &&
visitStatsData.uvGrowthRate !== null
"
>
<Top v-if="visitStatsData.uvGrowthRate > 0" />
<Bottom v-else-if="visitStatsData.uvGrowthRate < 0" />
</el-icon>
{{ formatGrowthRate(visitStatsData.uvGrowthRate) }}
{{ uvGrowthText }}
</span>
</div>
<div class="i-svg:visitor w-8 h-8" />
</div>
<div class="flex-x-between mt-2 text-sm text-gray">
<span>总访客数</span>
<span>{{ displayTransitionTotalUvCount }}</span>
<div class="mt-2 flex justify-between items-center">
<span class="text-sm text-gray">总访客数</span>
<span class="text-sm">{{ displayTransitionTotalUvCount }}</span>
</div>
</el-card>
</template>
@@ -236,37 +257,45 @@
</el-card>
</template>
<template v-if="!visitStatsLoading">
<el-card shadow="never" class="h-full flex flex-col">
<el-card
shadow="never"
class="h-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
>
<template #header>
<div class="flex-x-between">
<span class="text-gray">浏览量(PV)</span>
<span class="text-xs font-medium text-[--el-text-color-secondary]">
浏览量 PV
</span>
<el-tag type="primary" size="small">日</el-tag>
</div>
</template>
<div class="flex-x-between mt-2 flex-1">
<div class="flex-y-center">
<span class="text-lg">{{ displayTransitionPvCount }}</span>
<div class="mt-2 flex-1 flex items-end">
<div class="flex items-baseline gap-1.5">
<span class="text-xl font-semibold tracking-wide">
{{ displayTransitionPvCount }}
</span>
<span
:class="[
'text-xs',
'ml-2',
computeGrowthRateClass(visitStatsData.pvGrowthRate),
]"
v-if="pvGrowthText !== null"
:class="['text-xs', computeGrowthRateClass(visitStatsData.pvGrowthRate)]"
>
<el-icon>
<el-icon
v-if="
visitStatsData.pvGrowthRate !== undefined &&
visitStatsData.pvGrowthRate !== null
"
>
<Top v-if="visitStatsData.pvGrowthRate > 0" />
<Bottom v-else-if="visitStatsData.pvGrowthRate < 0" />
</el-icon>
{{ formatGrowthRate(visitStatsData.pvGrowthRate) }}
{{ pvGrowthText }}
</span>
</div>
<div class="i-svg:browser w-8 h-8" />
</div>
<div class="flex-x-between mt-2 text-sm text-gray">
<span>总浏览量</span>
<span>{{ displayTransitionTotalPvCount }}</span>
<div class="mt-2 flex justify-between items-center">
<span class="text-sm text-gray">总浏览量</span>
<span class="text-sm">{{ displayTransitionTotalPvCount }}</span>
</div>
</el-card>
</template>
@@ -295,7 +324,7 @@
<el-card>
<template #header>
<div class="flex-x-between">
<span class="header-title">最新动态</span>
<span class="font-semibold">最新动态</span>
<el-link
type="primary"
underline="never"
@@ -303,7 +332,7 @@
target="_blank"
>
完整记录
<el-icon class="link-icon"><TopRight /></el-icon>
<el-icon class="ml-0.5"><TopRight /></el-icon>
</el-link>
</div>
</template>
@@ -319,15 +348,23 @@
:hollow="index !== 0"
size="large"
>
<div class="version-item" :class="{ 'latest-item': index === 0 }">
<div>
<div
class="p-4 mb-3 bg-[--el-fill-color-lighter] rounded-lg transition-all duration-200 hover:translate-x-1"
:class="{
'bg-[--el-color-primary-light-9]! border border-[--el-color-primary-light-5]':
index === 0,
}"
>
<div class="flex items-center gap-2">
<el-text tag="strong">{{ item.title }}</el-text>
<el-tag v-if="item.tag" :type="index === 0 ? 'success' : 'info'" size="small">
{{ item.tag }}
</el-tag>
</div>
<el-text class="version-content">{{ item.content }}</el-text>
<el-text class="mb-3 text-xs leading-relaxed text-[--el-text-color-secondary]">
{{ item.content }}
</el-text>
<div v-if="item.link">
<el-link
@@ -337,7 +374,7 @@
underline="never"
>
详情
<el-icon class="link-icon"><TopRight /></el-icon>
<el-icon class="ml-0.5"><TopRight /></el-icon>
</el-link>
</div>
</div>
@@ -363,21 +400,11 @@ import type { VisitStatsDetail, VisitTrendDetail } from "@/types/api";
import { useUserStore } from "@/store/modules/user";
import { formatGrowthRate } from "@/utils";
import { useTransition, useDateFormat } from "@vueuse/core";
import { Connection, Failed } from "@element-plus/icons-vue";
import { CircleCheck, CircleClose, Loading } from "@element-plus/icons-vue";
import { useOnlineCount } from "@/composables";
// 在线用户数量组件相关
const { onlineUserCount, lastUpdateTime, isConnected } = useOnlineCount();
// 记录上一次的用户数量用于计算趋势
const previousCount = ref(0);
// 监听用户数量变化,计算趋势
watch(onlineUserCount, (newCount, oldCount) => {
if (oldCount > 0) {
previousCount.value = oldCount;
}
});
const { onlineUserCount, lastUpdateTime, isConnected, connectionState } = useOnlineCount();
// 格式化时间戳
const formattedTime = computed(() => {
@@ -385,6 +412,23 @@ const formattedTime = computed(() => {
return useDateFormat(lastUpdateTime, "HH:mm:ss").value;
});
const wsStatusText = computed(() => {
if (!isConnected.value) {
return connectionState.value === "CONNECTING" || connectionState.value === "RECONNECTING"
? "连接中"
: "未连接";
}
return "已连接";
});
const wsStatusClass = computed(() => {
if (isConnected.value)
return "text-[--el-color-success] bg-[--el-color-success-light-9] border-[--el-color-success-light-7]";
return connectionState.value === "CONNECTING" || connectionState.value === "RECONNECTING"
? "text-[--el-color-warning] bg-[--el-color-warning-light-9] border-[--el-color-warning-light-7]"
: "text-[--el-color-danger] bg-[--el-color-danger-light-9] border-[--el-color-danger-light-7]";
});
interface VersionItem {
id: string;
title: string; // 版本标题v2.4.0
@@ -456,6 +500,26 @@ const visitStatsData = ref<VisitStatsDetail>({
totalPvCount: 0,
});
const uvGrowthText = computed(() => {
if (
visitStatsData.value.uvGrowthRate === undefined ||
visitStatsData.value.uvGrowthRate === null
) {
return "--";
}
return formatGrowthRate(visitStatsData.value.uvGrowthRate);
});
const pvGrowthText = computed(() => {
if (
visitStatsData.value.pvGrowthRate === undefined ||
visitStatsData.value.pvGrowthRate === null
) {
return "--";
}
return formatGrowthRate(visitStatsData.value.pvGrowthRate);
});
// 数字过渡动画
const transitionUvCount = useTransition(
computed(() => visitStatsData.value.todayUvCount),
@@ -640,38 +704,5 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.dashboard-container {
position: relative;
padding: 24px;
.github-corner {
position: absolute;
top: 0;
right: 0;
z-index: 1;
border: 0;
}
.version-item {
padding: 16px;
margin-bottom: 12px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
transition: all 0.2s;
&.latest-item {
background: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-5);
}
&:hover {
transform: translateX(5px);
}
.version-content {
margin-bottom: 12px;
font-size: 13px;
line-height: 1.5;
color: var(--el-text-color-secondary);
}
}
}
// 暂无自定义样式
</style>