feat(dashboard): 完善仪表盘监控主页面功能实现

This commit is contained in:
Leo 2025-09-23 22:30:48 +08:00
parent c299ff2e6a
commit 61b8494839

View File

@ -1,212 +1,131 @@
<template> <template>
<div> <div class="dashboard-container">
<n-grid <!-- 页面头部操作区 -->
:x-gap="16" <div class="page-header mb-6">
:y-gap="16" <div class="header-left">
> <h2 class="page-title">
<n-gi :span="6"> 仪表盘概览
<n-card> </h2>
<n-space <p class="page-subtitle">
justify="space-between" 实时监控系统运行状态和核心数据
align="center" </p>
> </div>
<n-statistic label="访问量">
<n-number-animation <!-- 实时时间显示 -->
:from="0" <div class="header-center">
:to="12039" <div class="realtime-clock">
show-separator <div class="clock-content">
/> <div class="date-section">
</n-statistic> <span class="year">{{ currentTime.year }}</span>
<n-icon <span class="month-day">{{ currentTime.month }}{{ currentTime.day }}</span>
color="#de4307" <span class="weekday">{{ currentTime.weekday }}</span>
size="42" </div>
> <div class="time-section">
<icon-park-outline-chart-histogram /> <span class="time">{{ currentTime.time }}</span>
</n-icon> </div>
</n-space> </div>
<template #footer> </div>
<n-space justify="space-between"> </div>
<span>累计访问数</span>
<span><n-number-animation <div class="header-right">
:from="0" <n-button
:to="322039" type="primary"
show-separator :loading="loading"
/></span> @click="refreshAllData"
</n-space>
</template>
</n-card>
</n-gi>
<n-gi :span="6">
<n-card>
<n-space
justify="space-between"
align="center"
>
<n-statistic label="下载量">
<n-number-animation
:from="0"
:to="12039"
show-separator
/>
</n-statistic>
<n-icon
color="#ffb549"
size="42"
>
<icon-park-outline-chart-graph />
</n-icon>
</n-space>
<template #footer>
<n-space justify="space-between">
<span>累计下载量</span>
<span><n-number-animation
:from="0"
:to="322039"
show-separator
/></span>
</n-space>
</template>
</n-card>
</n-gi>
<n-gi :span="6">
<n-card>
<n-space
justify="space-between"
align="center"
>
<n-statistic label="浏览量">
<n-number-animation
:from="0"
:to="12039"
show-separator
/>
</n-statistic>
<n-icon
color="#1687a7"
size="42"
>
<icon-park-outline-average />
</n-icon>
</n-space>
<template #footer>
<n-space justify="space-between">
<span>累计浏览量</span>
<span><n-number-animation
:from="0"
:to="322039"
show-separator
/></span>
</n-space>
</template>
</n-card>
</n-gi>
<n-gi :span="6">
<n-card>
<n-space
justify="space-between"
align="center"
>
<n-statistic label="注册量">
<n-number-animation
:from="0"
:to="12039"
show-separator
/>
</n-statistic>
<n-icon
color="#42218E"
size="42"
>
<icon-park-outline-chart-pie />
</n-icon>
</n-space>
<template #footer>
<n-space justify="space-between">
<span>累计注册量</span>
<span><n-number-animation
:from="0"
:to="322039"
show-separator
/></span>
</n-space>
</template>
</n-card>
</n-gi>
<n-gi :span="24">
<n-card content-style="padding: 0;">
<n-tabs
type="line"
size="large"
:tabs-padding="20"
pane-style="padding: 20px;"
>
<n-tab-pane name="流量趋势">
<Chart />
</n-tab-pane>
<n-tab-pane name="访问量趋势">
<Chart2 />
</n-tab-pane>
</n-tabs>
</n-card>
</n-gi>
<n-gi :span="8">
<n-card
title="访问来源"
:segmented="{
content: true,
}"
> >
<Chart3 /> <template #icon>
</n-card> <n-icon><IconParkOutline:refresh /></n-icon>
</template>
刷新数据
</n-button>
</div>
</div>
<!-- 第一行核心数据统计卡片 -->
<n-grid :x-gap="16" :y-gap="16" class="mb-4">
<n-gi :span="6">
<DashboardStatCard
title="用户统计"
:value="dashboardData.userStats.totalUsers"
subtitle="总用户数"
:extra-info="`今日新增: ${dashboardData.userStats.todayNewUsers}`"
trend="+12%"
:trend-up="true"
icon="user"
color="var(--primary-color)"
:loading="loading"
/>
</n-gi> </n-gi>
<n-gi :span="16"> <n-gi :span="6">
<DashboardStatCard
title="登录统计"
:value="dashboardData.loginStats.todayLogins"
subtitle="今日登录"
:extra-info="`累计登录: ${dashboardData.loginStats.totalLogins.toLocaleString()}`"
trend="+8%"
:trend-up="true"
icon="data"
color="var(--success-color)"
:loading="loading"
/>
</n-gi>
<n-gi :span="6">
<DashboardStatCard
title="存储统计"
:value="dashboardData.storageStats.totalFiles"
subtitle="总文件数"
:extra-info="`总大小: ${dashboardData.storageStats.totalSize}`"
trend="+15%"
:trend-up="true"
icon="storage"
color="var(--warning-color)"
:loading="loading"
/>
</n-gi>
<n-gi :span="6">
<DashboardStatCard
title="今日活跃"
:value="dashboardData.dailyActivityStats.todayVisits"
subtitle="今日访问"
:extra-info="`活跃用户: ${dashboardData.dailyActivityStats.activeUsers}人`"
trend="+8%"
:trend-up="true"
icon="activity"
color="var(--info-color)"
:loading="loading"
/>
</n-gi>
</n-grid>
<!-- 第二行登录趋势分析 - 独占一行大气展示 -->
<n-grid :x-gap="20" :y-gap="20" class="mb-4">
<n-gi :span="24">
<n-card <n-card
title="成交记录" title="登录趋势分析"
:segmented="{ :segmented="{ content: true }"
content: true, class="enhanced-card login-trend-card full-width"
}"
> >
<template #header-extra> <template #header-extra>
<n-button <n-space>
type="primary" <n-button
quaternary type="primary"
> quaternary
更多 size="small"
</n-button> @click="refreshLoginTrend"
</template>
<n-table
:bordered="false"
:single-line="false"
>
<thead>
<tr>
<th>交易名称</th>
<th>开始时间</th>
<th>结束时间</th>
<th>进度</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in tableData"
:key="item.id"
> >
<td>{{ item.name }}</td> <template #icon>
<td>{{ item.start }}</td> <n-icon><IconParkOutline:refresh /></n-icon>
<td>{{ item.end }}</td> </template>
<td>{{ item.prograss }}%</td> 刷新数据
<td> </n-button>
<n-tag </n-space>
:bordered="false" </template>
type="info" <div class="chart-container full-chart">
> <LoginTrendChart
{{ item.status }} :data="dashboardData.loginStats.loginTrend"
</n-tag> :loading="loading"
</td> />
</tr> </div>
</tbody>
</n-table>
</n-card> </n-card>
</n-gi> </n-gi>
</n-grid> </n-grid>
@ -214,36 +133,566 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Chart from './components/chart.vue' import { onMounted, onUnmounted, reactive, ref } from 'vue'
import Chart2 from './components/chart2.vue' import { coiMsgError, coiMsgSuccess } from '@/utils/coi'
import Chart3 from './components/chart3.vue' import DashboardStatCard from './components/DashboardStatCard.vue'
import LoginTrendChart from './components/LoginTrendChart.vue'
import { getAllDashboardData, getLoginTrend } from '@/service/api/dashboard'
import type { DashboardData } from './types'
const tableData = [ //
{ const currentTime = reactive({
id: 0, year: '',
name: '商品名称1', month: '',
start: '2022-02-02', day: '',
end: '2022-02-02', weekday: '',
prograss: '100', time: '',
status: '已完成', })
let timeInterval: NodeJS.Timeout | null = null
//
function updateCurrentTime() {
const now = new Date()
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
currentTime.year = now.getFullYear().toString()
currentTime.month = (now.getMonth() + 1).toString().padStart(2, '0')
currentTime.day = now.getDate().toString().padStart(2, '0')
currentTime.weekday = weekdays[now.getDay()]
currentTime.time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
}
//
function startTimeUpdate() {
updateCurrentTime() //
timeInterval = setInterval(updateCurrentTime, 1000) //
}
//
function stopTimeUpdate() {
if (timeInterval) {
clearInterval(timeInterval)
timeInterval = null
}
}
//
const dashboardData = ref<DashboardData>({
userStats: {
totalUsers: 0,
todayNewUsers: 0,
activeUsers: 0,
onlineUsers: 0,
}, },
{ loginStats: {
id: 0, todayLogins: 0,
name: '商品名称2', totalLogins: 0,
start: '2022-02-02', loginTrend: [],
end: '2022-02-02',
prograss: '50',
status: '交易中',
}, },
{ storageStats: {
id: 0, totalFiles: 0,
name: '商品名称3', totalImages: 0,
start: '2022-02-02', totalSize: '0 MB',
end: '2022-02-02', todayUploads: 0,
prograss: '100', storageUsage: 0,
status: '已完成', availableSpace: '0 MB',
}, },
] dailyActivityStats: {
todayVisits: 0,
todayOperations: 0,
activeUsers: 0,
newContent: 0,
apiCalls: 0,
avgResponseTime: 0,
},
})
//
const loading = ref(false)
//
async function loadDashboardData() {
loading.value = true
try {
const { isSuccess, data } = await getAllDashboardData({
includeTrend: true,
trendDays: 7,
})
if (isSuccess && data) {
dashboardData.value = {
userStats: data.userStats || dashboardData.value.userStats,
loginStats: data.loginStats || dashboardData.value.loginStats,
storageStats: data.storageStats || dashboardData.value.storageStats,
dailyActivityStats: data.dailyActivityStats || dashboardData.value.dailyActivityStats,
}
}
else {
coiMsgError('获取仪表盘数据失败,请稍后重试')
}
}
catch (error) {
coiMsgError('获取仪表盘数据失败,请检查网络连接')
console.error('加载仪表盘数据失败:', error)
}
finally {
loading.value = false
}
}
//
async function refreshLoginTrend() {
try {
const { isSuccess, data } = await getLoginTrend(7)
if (isSuccess && data && data.loginTrend) {
dashboardData.value.loginStats.loginTrend = data.loginTrend
coiMsgSuccess('登录趋势数据已刷新')
}
else {
coiMsgError('刷新登录趋势数据失败,请稍后重试')
}
}
catch (error) {
coiMsgError('刷新登录趋势数据失败')
console.error('刷新登录趋势数据失败:', error)
}
}
//
async function refreshAllData() {
await loadDashboardData()
coiMsgSuccess('仪表盘数据已刷新')
}
//
onMounted(() => {
loadDashboardData()
startTimeUpdate()
})
//
onUnmounted(() => {
stopTimeUpdate()
})
</script> </script>
<style scoped></style> <style scoped>
.dashboard-container {
padding: 16px;
background-color: var(--body-color);
min-height: calc(100vh - 120px);
}
/* 页面头部样式 */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 0 4px;
position: relative;
}
.header-left {
flex: 1;
}
.header-center {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: var(--text-color-1);
line-height: 1.3;
}
.page-subtitle {
margin: 4px 0 0 0;
font-size: 14px;
color: var(--text-color-3);
line-height: 1.4;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
/* 实时时间组件样式 */
.realtime-clock {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border-radius: 12px;
padding: 8px 18px;
box-shadow:
0 4px 16px rgba(59, 130, 246, 0.25),
0 2px 8px rgba(59, 130, 246, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
min-width: 240px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.realtime-clock::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.1) 0%,
transparent 50%,
rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
.realtime-clock:hover {
transform: translateY(-1px);
box-shadow:
0 6px 20px rgba(59, 130, 246, 0.3),
0 4px 12px rgba(59, 130, 246, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.clock-content {
position: relative;
z-index: 2;
color: white;
text-align: center;
}
.date-section {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-bottom: 2px;
font-size: 13px;
font-weight: 500;
opacity: 0.95;
}
.year {
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.month-day {
font-weight: 600;
color: white;
}
.weekday {
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.12);
padding: 1px 6px;
border-radius: 8px;
font-size: 11px;
}
.time-section {
font-family: 'Monaco', 'Consolas', 'Ubuntu Mono', monospace;
}
.time {
font-size: 18px;
font-weight: 700;
color: white;
letter-spacing: 1px;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
display: inline-block;
transition: all 0.3s ease;
}
/* 增强卡片样式 - 美观大气 */
.enhanced-card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--border-color);
overflow: hidden;
}
.enhanced-card:hover {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.login-trend-card {
background: linear-gradient(
135deg,
var(--card-color) 0%,
rgba(24, 160, 88, 0.02) 100%
);
}
/* 图表容器样式 */
.chart-container {
padding: 8px 0;
min-height: 320px;
position: relative;
}
.chart-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(
90deg,
var(--primary-color) 0%,
var(--success-color) 50%,
var(--primary-color) 100%
);
opacity: 0.3;
}
/* 全宽卡片样式增强 */
.full-width {
min-height: 400px;
}
.full-width :deep(.n-card-header) {
padding: 24px 32px 20px 32px;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 100%
);
backdrop-filter: blur(12px);
}
.full-width :deep(.n-card__content) {
padding: 28px 32px 32px 32px;
}
/* 全宽图表容器增强 */
.full-chart {
min-height: 380px;
padding: 12px 0;
}
.full-chart::before {
height: 3px;
background: linear-gradient(
90deg,
var(--primary-color) 0%,
var(--success-color) 25%,
var(--warning-color) 50%,
var(--success-color) 75%,
var(--primary-color) 100%
);
opacity: 0.4;
}
/* 卡片标题增强 */
.enhanced-card :deep(.n-card-header) {
padding: 20px 24px 16px 24px;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--divider-color);
}
.enhanced-card :deep(.n-card-header .n-card-header__main) {
font-size: 16px;
font-weight: 600;
color: var(--text-color-1);
position: relative;
}
.enhanced-card :deep(.n-card-header .n-card-header__main)::after {
content: "";
position: absolute;
bottom: -8px;
left: 0;
width: 24px;
height: 3px;
background: var(--primary-color);
border-radius: 2px;
}
/* 卡片内容区域 */
.enhanced-card :deep(.n-card__content) {
padding: 24px;
}
/* 按钮样式增强 */
.enhanced-card :deep(.n-button) {
border-radius: 8px;
font-weight: 500;
transition: all 0.3s ease;
}
.enhanced-card :deep(.n-button:hover) {
transform: translateY(-1px);
}
/* 响应式布局 */
@media (max-width: 1200px) {
.dashboard-container {
padding: 12px;
}
.enhanced-card :deep(.n-card-header) {
padding: 16px 20px 12px 20px;
}
.enhanced-card :deep(.n-card__content) {
padding: 20px;
}
.chart-container,
.table-container {
min-height: 280px;
}
/* 时间组件平板适配 */
.realtime-clock {
min-width: 220px;
padding: 6px 16px;
}
.date-section {
font-size: 12px;
gap: 5px;
}
.time {
font-size: 16px;
}
}
@media (max-width: 768px) {
.dashboard-container {
padding: 8px;
}
.enhanced-card {
border-radius: 8px;
}
.enhanced-card :deep(.n-card-header) {
padding: 12px 16px 8px 16px;
}
.enhanced-card :deep(.n-card__content) {
padding: 16px;
}
.chart-container,
.table-container {
min-height: 240px;
}
/* 移动端头部布局调整 */
.page-header {
flex-direction: column;
align-items: center;
gap: 16px;
padding: 0 8px;
}
.header-center {
position: static;
transform: none;
order: 2;
}
.header-left {
text-align: center;
order: 1;
}
.header-right {
order: 3;
}
/* 时间组件移动端适配 */
.realtime-clock {
min-width: 200px;
padding: 6px 14px;
border-radius: 10px;
}
.date-section {
font-size: 11px;
gap: 3px;
flex-wrap: wrap;
}
.weekday {
font-size: 10px;
padding: 1px 4px;
}
.time {
font-size: 15px;
letter-spacing: 0.5px;
}
}
@media (max-width: 480px) {
.dashboard-container {
padding: 8px;
}
.enhanced-card {
border-radius: 8px;
}
.enhanced-card :deep(.n-card-header) {
padding: 12px 16px 8px 16px;
}
.enhanced-card :deep(.n-card__content) {
padding: 16px;
}
.chart-container,
.table-container {
min-height: 240px;
}
/* 超小屏幕时间组件适配 */
.realtime-clock {
min-width: 180px;
padding: 5px 12px;
border-radius: 8px;
}
.date-section {
font-size: 10px;
gap: 2px;
}
.time {
font-size: 14px;
letter-spacing: 0.5px;
}
.page-title {
font-size: 20px;
}
.page-subtitle {
font-size: 13px;
}
}
</style>