coder-common-thin-frontend/src/views/dashboard/monitor/index.vue

699 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dashboard-container">
<!-- 页面头部操作区 -->
<div class="page-header mb-6">
<div class="header-left">
<h2 class="page-title">
仪表盘概览
</h2>
<p class="page-subtitle">
实时监控系统运行状态和核心数据
</p>
</div>
<!-- 实时时间显示 -->
<div class="header-center">
<div class="realtime-clock">
<div class="clock-content">
<div class="date-section">
<span class="year">{{ currentTime.year }}</span>
<span class="month-day">{{ currentTime.month }}{{ currentTime.day }}</span>
<span class="weekday">{{ currentTime.weekday }}</span>
</div>
<div class="time-section">
<span class="time">{{ currentTime.time }}</span>
</div>
</div>
</div>
</div>
<div class="header-right">
<n-button
type="primary"
:loading="loading"
@click="refreshAllData"
>
<template #icon>
<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 :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
title="登录趋势分析"
:segmented="{ content: true }"
class="enhanced-card login-trend-card full-width"
>
<template #header-extra>
<n-space>
<n-button
type="primary"
quaternary
size="small"
@click="refreshLoginTrend"
>
<template #icon>
<n-icon><IconParkOutline:refresh /></n-icon>
</template>
刷新数据
</n-button>
</n-space>
</template>
<div class="chart-container full-chart">
<LoginTrendChart
:data="dashboardData.loginStats.loginTrend"
:loading="loading"
/>
</div>
</n-card>
</n-gi>
</n-grid>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import { coiMsgError, coiMsgSuccess } from '@/utils/coi'
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 currentTime = reactive({
year: '',
month: '',
day: '',
weekday: '',
time: '',
})
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: {
todayLogins: 0,
totalLogins: 0,
loginTrend: [],
},
storageStats: {
totalFiles: 0,
totalImages: 0,
totalSize: '0 MB',
todayUploads: 0,
storageUsage: 0,
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>
<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>