|
@@ -1,164 +1,726 @@
|
|
|
<template>
|
|
|
- <div class="app-container home">
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :sm="24" :lg="12" style="padding-left: 20px">
|
|
|
- <h2>RuoYi-Vue-Plus多租户管理系统</h2>
|
|
|
- <p>
|
|
|
- RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
|
|
|
- <br />
|
|
|
- * 前端开发框架 Vue3、TS、Element Plus<br />
|
|
|
- * 后端开发框架 Spring Boot<br />
|
|
|
- * 容器框架 Undertow 基于 Netty 的高性能容器<br />
|
|
|
- * 权限认证框架 Sa-Token 支持多终端认证系统<br />
|
|
|
- * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
|
|
|
- * 缓存数据库 Redis 适配 6.X 最低 4.X<br />
|
|
|
- * 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
|
|
|
- * 数据库框架 p6spy 更强劲的 SQL 分析<br />
|
|
|
- * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
|
|
|
- * 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
|
|
|
- * Redis客户端 Redisson 性能强劲、API丰富<br />
|
|
|
- * 分布式限流 Redisson 全局、请求IP、集群ID 多种限流<br />
|
|
|
- * 分布式锁 Lock4j 注解锁、工具锁 多种多样<br />
|
|
|
- * 分布式幂等 Lock4j 基于分布式锁实现<br />
|
|
|
- * 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化<br />
|
|
|
- * 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
|
|
|
- * 文件存储 Minio 本地存储<br />
|
|
|
- * 文件存储 七牛、阿里、腾讯 云存储<br />
|
|
|
- * 监控框架 SpringBoot-Admin 全方位服务监控<br />
|
|
|
- * 校验框架 Validation 增强接口安全性 严谨性<br />
|
|
|
- * Excel框架 FastExcel(原Alibaba EasyExcel) 性能优异 扩展性强<br />
|
|
|
- * 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释<br />
|
|
|
- * 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性<br />
|
|
|
- * 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码<br />
|
|
|
- * 部署方式 Docker 容器编排 一键部署业务集群<br />
|
|
|
- * 国际化 SpringMessage Spring标准国际化方案<br />
|
|
|
- </p>
|
|
|
- <p><b>当前版本:</b> <span>v5.4.1</span></p>
|
|
|
- <p>
|
|
|
- <el-tag type="danger">¥免费开源</el-tag>
|
|
|
- </p>
|
|
|
- <p>
|
|
|
- <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
|
|
|
- <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
|
|
|
- <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
|
|
|
- >更新日志</el-button
|
|
|
- >
|
|
|
- </p>
|
|
|
+ <div class="dashboard-container">
|
|
|
+ <!-- 顶部标题区域 -->
|
|
|
+ <div class="dashboard-header">
|
|
|
+ <h1 class="dashboard-title">
|
|
|
+ <el-icon class="title-icon">
|
|
|
+ <TrendCharts />
|
|
|
+ </el-icon>
|
|
|
+ 赛事管理系统仪表板
|
|
|
+ </h1>
|
|
|
+ <p class="dashboard-subtitle">实时监控赛事数据,洞察运营状况</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 统计卡片区域 -->
|
|
|
+ <div class="stats-grid">
|
|
|
+ <div class="stat-card" v-for="(item, index) in statsData" :key="index">
|
|
|
+ <div class="stat-icon" :style="{ backgroundColor: item.color }">
|
|
|
+ <el-icon :size="24">
|
|
|
+ <component :is="item.icon" />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="stat-content">
|
|
|
+ <div class="stat-value">{{ item.loading ? '-' : item.value }}</div>
|
|
|
+ <div class="stat-label">{{ item.label }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 快速操作区域 -->
|
|
|
+ <el-card class="quick-actions-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>快速操作</span>
|
|
|
+ <el-icon>
|
|
|
+ <Operation />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="quick-actions">
|
|
|
+ <el-button
|
|
|
+ v-for="action in quickActions"
|
|
|
+ :key="action.name"
|
|
|
+ :type="action.type"
|
|
|
+ :icon="action.icon"
|
|
|
+ @click="handleQuickAction(action.action)"
|
|
|
+ class="action-btn"
|
|
|
+ >
|
|
|
+ {{ action.name }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ <!-- 图表和详细信息区域 -->
|
|
|
+ <el-row :gutter="20" class="charts-section">
|
|
|
+ <!-- 数据分布饼图 -->
|
|
|
+ <el-col :xs="24" :sm="24" :md="12" :lg="8">
|
|
|
+ <el-card class="chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>数据分布</span>
|
|
|
+ <el-icon>
|
|
|
+ <PieChart />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div id="pieChart" class="chart-container"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 趋势图表 -->
|
|
|
+ <el-col :xs="24" :sm="24" :md="12" :lg="8">
|
|
|
+ <el-card class="chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>数据趋势</span>
|
|
|
+ <el-icon>
|
|
|
+ <TrendCharts />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div id="lineChart" class="chart-container"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 系统信息 -->
|
|
|
+ <el-col :xs="24" :sm="24" :md="24" :lg="8">
|
|
|
+ <el-card class="chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>系统信息</span>
|
|
|
+ <el-icon>
|
|
|
+ <Monitor />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="system-info">
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">系统版本</span>
|
|
|
+ <span class="info-value">v5.4.1</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">运行状态</span>
|
|
|
+ <el-tag type="success">正常运行</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">在线用户</span>
|
|
|
+ <span class="info-value">{{ onlineUsers }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">数据更新</span>
|
|
|
+ <span class="info-value">{{ lastUpdateTime }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 最新动态 -->
|
|
|
+ <el-row :gutter="20" class="activity-section">
|
|
|
+ <el-col :xs="24" :sm="24" :md="12">
|
|
|
+ <el-card class="activity-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>最新赛事</span>
|
|
|
+ <el-icon>
|
|
|
+ <Calendar />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="activity-list">
|
|
|
+ <div v-for="event in recentEvents" :key="event.id" class="activity-item">
|
|
|
+ <div class="activity-dot"></div>
|
|
|
+ <div class="activity-content">
|
|
|
+ <div class="activity-title">{{ event.eventName }}</div>
|
|
|
+ <div class="activity-time">{{ event.updateTime }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
</el-col>
|
|
|
|
|
|
- <el-col :sm="24" :lg="12" style="padding-left: 20px">
|
|
|
- <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
|
|
|
- <p>
|
|
|
- RuoYi-Cloud-Plus 微服务通用权限管理系统 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
|
|
|
- <br />
|
|
|
- * 前端开发框架 Vue3、TS、Element UI<br />
|
|
|
- * 后端开发框架 Spring Boot<br />
|
|
|
- * 微服务开发框架 Spring Cloud、Spring Cloud Alibaba<br />
|
|
|
- * 容器框架 Undertow 基于 XNIO 的高性能容器<br />
|
|
|
- * 权限认证框架 Sa-Token、Jwt 支持多终端认证系统<br />
|
|
|
- * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
|
|
|
- * 关系数据库 Oracle 适配 11g 12c<br />
|
|
|
- * 关系数据库 PostgreSQL 适配 13 14<br />
|
|
|
- * 关系数据库 SQLServer 适配 2017 2019<br />
|
|
|
- * 缓存数据库 Redis 适配 6.X 最低 5.X<br />
|
|
|
- * 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
|
|
|
- * 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
|
|
|
- * 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
|
|
|
- * 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
|
|
|
- * RPC远程调用 Apache Dubbo 原生态使用体验、高性能<br />
|
|
|
- * 分布式限流熔断 Alibaba Sentinel 无侵入、高扩展<br />
|
|
|
- * 分布式事务 Alibaba Seata 无侵入、高扩展 支持 四种模式<br />
|
|
|
- * 分布式消息队列 Apache Kafka 高性能高速度<br />
|
|
|
- * 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
|
|
|
- * 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
|
|
|
- * 分布式搜索引擎 ElasticSearch 业界知名<br />
|
|
|
- * 分布式链路追踪 Apache SkyWalking 链路追踪、网格分析、度量聚合、可视化<br />
|
|
|
- * 分布式日志中心 ELK 业界成熟解决方案<br />
|
|
|
- * 分布式监控 Prometheus、Grafana 全方位性能监控<br />
|
|
|
- * 其余与 Vue 版本一致<br />
|
|
|
- </p>
|
|
|
- <p><b>当前版本:</b> <span>v2.4.1</span></p>
|
|
|
- <p>
|
|
|
- <el-tag type="danger">¥免费开源</el-tag>
|
|
|
- </p>
|
|
|
- <p>
|
|
|
- <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
|
|
|
- <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
|
|
|
- <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
|
|
|
- >更新日志</el-button
|
|
|
- >
|
|
|
- </p>
|
|
|
+ <el-col :xs="24" :sm="24" :md="12">
|
|
|
+ <el-card class="activity-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>系统公告</span>
|
|
|
+ <el-icon>
|
|
|
+ <Bell />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="activity-list">
|
|
|
+ <div v-for="notice in systemNotices" :key="notice.id" class="activity-item">
|
|
|
+ <div class="activity-dot notice"></div>
|
|
|
+ <div class="activity-content">
|
|
|
+ <div class="activity-title">{{ notice.noticeTitle }}</div>
|
|
|
+ <div class="activity-time">{{ notice.createTime }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
- <el-divider />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="Index" lang="ts">
|
|
|
-const goTarget = (url: string) => {
|
|
|
- window.open(url, '__blank');
|
|
|
+import { ref, nextTick, onMounted } from 'vue';
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
+import { TrendCharts, PieChart, Monitor, Operation, Calendar, Bell } from '@element-plus/icons-vue';
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import { getArticleCount } from '@/api/system/article';
|
|
|
+import { getEventCount, listGameEvent } from '@/api/system/gameEvent';
|
|
|
+import { getEventProjectCount } from '@/api/system/gameEventProject';
|
|
|
+import { getRefereeCount } from '@/api/system/gameReferee';
|
|
|
+import { getTeamCount } from '@/api/system/gameTeam';
|
|
|
+import { listNotice } from '@/api/system/notice';
|
|
|
+import { countOnlineUser } from '@/api/monitor/online';
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const statsData = ref([
|
|
|
+ {
|
|
|
+ label: '文章数量',
|
|
|
+ value: 0,
|
|
|
+ icon: 'DataBoard',
|
|
|
+ color: '#409EFF',
|
|
|
+ loading: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '参赛队伍',
|
|
|
+ value: 0,
|
|
|
+ icon: 'User',
|
|
|
+ color: '#67C23A',
|
|
|
+ loading: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '赛事',
|
|
|
+ value: 0,
|
|
|
+ icon: 'Trophy',
|
|
|
+ color: '#E6A23C',
|
|
|
+ loading: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '赛事项目',
|
|
|
+ value: 0,
|
|
|
+ icon: 'Promotion',
|
|
|
+ color: '#F56C6C',
|
|
|
+ loading: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '裁判',
|
|
|
+ value: 0,
|
|
|
+ icon: 'Flag',
|
|
|
+ color: '#909399',
|
|
|
+ loading: true
|
|
|
+ }
|
|
|
+]);
|
|
|
+const router = useRouter();
|
|
|
+const onlineUsers = ref(1);
|
|
|
+const lastUpdateTime = ref('');
|
|
|
+
|
|
|
+const quickActions = ref([
|
|
|
+ { name: '新增赛事', type: 'primary' as const, icon: 'Plus', action: 'addEvent' },
|
|
|
+ { name: '参赛队伍管理', type: 'success' as const, icon: 'User', action: 'manageTeam' },
|
|
|
+ { name: '裁判管理', type: 'warning' as const, icon: 'Flag', action: 'manageReferees' },
|
|
|
+ { name: '数据导出', type: 'info' as const, icon: 'Download', action: 'exportData' },
|
|
|
+ { name: '刷新数据', type: 'default' as const, icon: 'Refresh', action: 'refresh' }
|
|
|
+]);
|
|
|
+
|
|
|
+const recentEvents = ref([]);
|
|
|
+
|
|
|
+const systemNotices = ref([]);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取最新赛事
|
|
|
+ */
|
|
|
+const loadRecentEvents = async () => {
|
|
|
+ try {
|
|
|
+ const res = await listGameEvent({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 4
|
|
|
+ });
|
|
|
+ if (res.code === 200 && res.total > 0) {
|
|
|
+ recentEvents.value = res.rows;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载最新赛事失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取公告
|
|
|
+ */
|
|
|
+const loadNotice = async () => {
|
|
|
+ try {
|
|
|
+ const res = await listNotice({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 4,
|
|
|
+ noticeTitle: undefined,
|
|
|
+ createByName: undefined,
|
|
|
+ status: undefined,
|
|
|
+ noticeType: undefined
|
|
|
+ });
|
|
|
+ if (res.code === 200 && res.total > 0) {
|
|
|
+ systemNotices.value = res.rows;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载公告失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取统计数量
|
|
|
+ */
|
|
|
+const loadStatistics = async () => {
|
|
|
+ try {
|
|
|
+ // 并行请求所有统计接口
|
|
|
+ const results = await Promise.allSettled([getArticleCount(), getTeamCount(), getEventCount(), getEventProjectCount(), getRefereeCount()]);
|
|
|
+
|
|
|
+ let hasError = false;
|
|
|
+
|
|
|
+ // 处理文章数量
|
|
|
+ if (results[0].status === 'fulfilled') {
|
|
|
+ statsData.value[0].value = results[0].value.data || 0;
|
|
|
+ } else {
|
|
|
+ console.error('获取文章数量失败:', results[0].reason);
|
|
|
+ statsData.value[0].value = 0;
|
|
|
+ hasError = true;
|
|
|
+ }
|
|
|
+ statsData.value[0].loading = false;
|
|
|
+
|
|
|
+ // 处理参赛队伍数量
|
|
|
+ if (results[1].status === 'fulfilled') {
|
|
|
+ statsData.value[1].value = results[1].value.data || 0;
|
|
|
+ } else {
|
|
|
+ console.error('获取参赛队伍数量失败:', results[1].reason);
|
|
|
+ statsData.value[1].value = 0;
|
|
|
+ hasError = true;
|
|
|
+ }
|
|
|
+ statsData.value[1].loading = false;
|
|
|
+
|
|
|
+ // 处理赛事数量
|
|
|
+ if (results[2].status === 'fulfilled') {
|
|
|
+ statsData.value[2].value = results[2].value.data || 0;
|
|
|
+ } else {
|
|
|
+ console.error('获取赛事数量失败:', results[2].reason);
|
|
|
+ statsData.value[2].value = 0;
|
|
|
+ hasError = true;
|
|
|
+ }
|
|
|
+ statsData.value[2].loading = false;
|
|
|
+
|
|
|
+ // 处理赛事项目数量
|
|
|
+ if (results[3].status === 'fulfilled') {
|
|
|
+ statsData.value[3].value = results[3].value.data || 0;
|
|
|
+ } else {
|
|
|
+ console.error('获取赛事项目数量失败:', results[3].reason);
|
|
|
+ statsData.value[3].value = 0;
|
|
|
+ hasError = true;
|
|
|
+ }
|
|
|
+ statsData.value[3].loading = false;
|
|
|
+
|
|
|
+ // 处理裁判数量
|
|
|
+ if (results[4].status === 'fulfilled') {
|
|
|
+ statsData.value[4].value = results[4].value.data || 0;
|
|
|
+ } else {
|
|
|
+ console.error('获取裁判数量失败:', results[4].reason);
|
|
|
+ statsData.value[4].value = 0;
|
|
|
+ hasError = true;
|
|
|
+ }
|
|
|
+ statsData.value[4].loading = false;
|
|
|
+
|
|
|
+ // 如果有接口错误,显示提示
|
|
|
+ if (hasError) {
|
|
|
+ ElMessage({
|
|
|
+ message: '部分统计数据获取失败,请检查网络连接或稍后重试',
|
|
|
+ type: 'warning',
|
|
|
+ duration: 3000
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化图表
|
|
|
+ nextTick(() => {
|
|
|
+ initCharts();
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载统计数据失败:', error);
|
|
|
+ ElMessage({
|
|
|
+ message: '统计数据加载失败,请检查网络连接',
|
|
|
+ type: 'error',
|
|
|
+ duration: 5000
|
|
|
+ });
|
|
|
+ // 设置所有统计数据为0并停止加载状态
|
|
|
+ statsData.value.forEach((item) => {
|
|
|
+ item.value = 0;
|
|
|
+ item.loading = false;
|
|
|
+ });
|
|
|
+ }
|
|
|
};
|
|
|
+
|
|
|
+const initCharts = () => {
|
|
|
+ initPieChart();
|
|
|
+ initLineChart();
|
|
|
+};
|
|
|
+
|
|
|
+const initPieChart = () => {
|
|
|
+ const chartDom = document.getElementById('pieChart');
|
|
|
+ if (!chartDom) return;
|
|
|
+
|
|
|
+ const myChart = echarts.init(chartDom);
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item'
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'vertical',
|
|
|
+ left: 'left'
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '数据分布',
|
|
|
+ type: 'pie',
|
|
|
+ radius: '50%',
|
|
|
+ data: [
|
|
|
+ { value: statsData.value[0].value, name: '文章' },
|
|
|
+ { value: statsData.value[1].value, name: '参赛队伍' },
|
|
|
+ { value: statsData.value[2].value, name: '赛事' },
|
|
|
+ { value: statsData.value[3].value, name: '项目' },
|
|
|
+ { value: statsData.value[4].value, name: '裁判' }
|
|
|
+ ],
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetX: 0,
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ myChart.setOption(option);
|
|
|
+};
|
|
|
+
|
|
|
+const initLineChart = () => {
|
|
|
+ const chartDom = document.getElementById('lineChart');
|
|
|
+ if (!chartDom) return;
|
|
|
+
|
|
|
+ const myChart = echarts.init(chartDom);
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis'
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: ['注册数量', '赛事数量']
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value'
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '注册数量',
|
|
|
+ type: 'line',
|
|
|
+ stack: 'Total',
|
|
|
+ data: [120, 132, 101, 134, 90, 230]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '赛事数量',
|
|
|
+ type: 'line',
|
|
|
+ stack: 'Total',
|
|
|
+ data: [220, 182, 191, 234, 290, 330]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ myChart.setOption(option);
|
|
|
+};
|
|
|
+
|
|
|
+const handleQuickAction = (action: string) => {
|
|
|
+ switch (action) {
|
|
|
+ case 'addEvent':
|
|
|
+ // 跳转到新增赛事页面
|
|
|
+ router.push(`/system/gameEvent/add`);
|
|
|
+ break;
|
|
|
+ case 'manageAthletes':
|
|
|
+ // 跳转到参赛队伍管理页面
|
|
|
+ router.push(`/game/gameTeam`);
|
|
|
+ break;
|
|
|
+ case 'manageReferees':
|
|
|
+ // 跳转到裁判管理页面
|
|
|
+ router.push(`/game/gameReferee`);
|
|
|
+ break;
|
|
|
+ case 'exportData':
|
|
|
+ // 导出数据
|
|
|
+ ElMessage({
|
|
|
+ message: '请到具体的管理页面进行导出',
|
|
|
+ type: 'info'
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ case 'refresh':
|
|
|
+ // 刷新统计数据和最新赛事
|
|
|
+ getAllData();
|
|
|
+ ElMessage({
|
|
|
+ message: '正在刷新数据...',
|
|
|
+ type: 'info'
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const getOnlineUser = async () => {
|
|
|
+ const res = await countOnlineUser();
|
|
|
+ onlineUsers.value = res.data;
|
|
|
+};
|
|
|
+
|
|
|
+const updateTime = () => {
|
|
|
+ const now = new Date();
|
|
|
+ lastUpdateTime.value = now.toLocaleTimeString();
|
|
|
+};
|
|
|
+
|
|
|
+const getAllData = async () => {
|
|
|
+ loadStatistics();
|
|
|
+ loadRecentEvents();
|
|
|
+ loadNotice();
|
|
|
+ getOnlineUser();
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getAllData();
|
|
|
+ updateTime();
|
|
|
+ // 每分钟更新一次时间
|
|
|
+ setInterval(updateTime, 60000);
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.home {
|
|
|
- blockquote {
|
|
|
- padding: 10px 20px;
|
|
|
- margin: 0 0 20px;
|
|
|
- font-size: 17.5px;
|
|
|
- border-left: 5px solid #eee;
|
|
|
+.dashboard-container {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-header {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 30px;
|
|
|
+
|
|
|
+ .dashboard-title {
|
|
|
+ font-size: 28px;
|
|
|
+ color: #303133;
|
|
|
+ margin: 0 0 10px 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10px;
|
|
|
+
|
|
|
+ .title-icon {
|
|
|
+ color: #409eff;
|
|
|
+ }
|
|
|
}
|
|
|
- hr {
|
|
|
- margin-top: 20px;
|
|
|
- margin-bottom: 20px;
|
|
|
- border: 0;
|
|
|
- border-top: 1px solid #eee;
|
|
|
+
|
|
|
+ .dashboard-subtitle {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #909399;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
- .col-item {
|
|
|
- margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.12);
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- ul {
|
|
|
- padding: 0;
|
|
|
- margin: 0;
|
|
|
+.stat-icon {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ border-radius: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 16px;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-content {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-trend {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+
|
|
|
+ &.up {
|
|
|
+ color: #67c23a;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.down {
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.charts-section {
|
|
|
+ margin-bottom: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-card {
|
|
|
+ height: 400px;
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ height: 320px;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
|
- font-size: 13px;
|
|
|
- color: #676a6c;
|
|
|
- overflow-x: hidden;
|
|
|
+.system-info {
|
|
|
+ .info-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
- ul {
|
|
|
- list-style-type: none;
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- h4 {
|
|
|
- margin-top: 0px;
|
|
|
+ .info-label {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
}
|
|
|
|
|
|
- h2 {
|
|
|
- margin-top: 10px;
|
|
|
- font-size: 26px;
|
|
|
- font-weight: 100;
|
|
|
+ .info-value {
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- p {
|
|
|
- margin-top: 10px;
|
|
|
+.quick-actions-card {
|
|
|
+ margin-bottom: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
|
- b {
|
|
|
- font-weight: 700;
|
|
|
+ .action-btn {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 120px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.activity-section {
|
|
|
+ .activity-card {
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-weight: bold;
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+.activity-list {
|
|
|
+ .activity-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .activity-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #409eff;
|
|
|
+ margin-top: 6px;
|
|
|
+ margin-right: 12px;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ &.notice {
|
|
|
+ background: #e6a23c;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .activity-content {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .activity-title {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .activity-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .dashboard-container {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
|
|
|
- .update-log {
|
|
|
- ol {
|
|
|
- display: block;
|
|
|
- list-style-type: decimal;
|
|
|
- margin-block-start: 1em;
|
|
|
- margin-block-end: 1em;
|
|
|
- margin-inline-start: 0;
|
|
|
- margin-inline-end: 0;
|
|
|
- padding-inline-start: 40px;
|
|
|
+ .quick-actions {
|
|
|
+ .action-btn {
|
|
|
+ min-width: 100px;
|
|
|
}
|
|
|
}
|
|
|
}
|