From 308d3b3b7883a92a761dfaf4f607a9f4658213cf Mon Sep 17 00:00:00 2001
From: Codex Assistant <codex@example.com>
Date: 星期四, 06 十一月 2025 09:58:25 +0800
Subject: [PATCH] 修改页面
---
web/src/views/dashboard/index.vue | 324 ++++++++++++++++++++++++++++++++++++++++++++---------
1 files changed, 268 insertions(+), 56 deletions(-)
diff --git a/web/src/views/dashboard/index.vue b/web/src/views/dashboard/index.vue
index 05691f8..1254a5e 100644
--- a/web/src/views/dashboard/index.vue
+++ b/web/src/views/dashboard/index.vue
@@ -39,12 +39,31 @@
</el-col>
</el-row>
+ <div class="chart-section">
+ <div class="chart-card">
+ <div v-if="trendChartLoading" class="chart-mask">鍔犺浇涓�...</div>
+ <div class="chart-header">
+ <h3 class="chart-header-title">鎶ュ悕瓒嬪娍</h3>
+ <span class="chart-header-desc">鏈�杩� 15 澶�</span>
+ </div>
+ <div class="chart-container" ref="trendChartRef"></div>
+ </div>
+ <div class="chart-card">
+ <div v-if="regionChartLoading" class="chart-mask">鍔犺浇涓�...</div>
+ <div class="chart-header">
+ <h3 class="chart-header-title">鍖哄煙鎶ュ悕鍒嗗竷</h3>
+ <span class="chart-header-desc">鎸夊彾瀛愬湴鍖虹粺璁�</span>
+ </div>
+ <div class="chart-container" ref="regionChartRef"></div>
+ </div>
+ </div>
+
<div class="table-card">
<div class="table-header">
<h3 class="table-title">鏈�杩戞瘮璧�</h3>
<el-button type="primary" @click="$router.push('/activity')">鏌ョ湅鍏ㄩ儴</el-button>
</div>
-
+
<el-table :data="recentActivities" class="recent-table">
<el-table-column prop="name" label="姣旇禌鍚嶇О" width="180" />
<el-table-column prop="playerCount" label="鎶ュ悕浜烘暟" width="120" />
@@ -66,14 +85,39 @@
</template>
<script setup lang="ts">
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Trophy, UserFilled, Clock, User } from '@element-plus/icons-vue'
-import { getDashboardStats } from '@/api/dashboard'
+import * as echarts from 'echarts'
+import { getDashboardStats, getRegistrationTrend, getRegionRegistrationStats } from '@/api/dashboard'
import { getActivities } from '@/api/activity'
const router = useRouter()
+
+interface RegistrationTrendPoint {
+ date: string
+ count: number
+}
+
+interface RegionRegistrationStat {
+ regionId: number | null
+ regionName: string
+ leafFlag?: boolean | null
+ count: number
+}
+
+const UNASSIGNED_REGION_LABEL = '鏈�夋嫨鍖哄煙'
+
+const registrationTrend = ref<RegistrationTrendPoint[]>([])
+const regionStats = ref<RegionRegistrationStat[]>([])
+const trendChartRef = ref<HTMLDivElement | null>(null)
+const regionChartRef = ref<HTMLDivElement | null>(null)
+const trendChartLoading = ref(false)
+const regionChartLoading = ref(false)
+
+let trendChartInstance: echarts.ECharts | null = null
+let regionChartInstance: echarts.ECharts | null = null
// 缁熻鏁版嵁
const stats = ref({
@@ -84,7 +128,7 @@
})
// 鏈�杩戞椿鍔ㄦ暟鎹�
-const recentActivities = ref([])
+const recentActivities = ref<any[]>([])
// 鍔犺浇缁熻鏁版嵁
const loadStats = async () => {
@@ -100,8 +144,8 @@
// 鍔犺浇鏈�杩戞椿鍔�
const loadRecentActivities = async () => {
try {
- const data = await getActivities(0, 5) // 鑾峰彇鍓�5鏉℃椿鍔�
- const { content } = data || {};
+ const data = await getActivities(0, 5)
+ const { content } = data || {}
recentActivities.value = (content || []).map(item => ({
id: item.id,
name: item.name,
@@ -116,13 +160,137 @@
}
}
+const renderTrendChart = () => {
+ if (!trendChartRef.value) return
+ if (!trendChartInstance) {
+ trendChartInstance = echarts.init(trendChartRef.value)
+ }
+
+ const dates = registrationTrend.value.map(item => item.date)
+ const values = registrationTrend.value.map(item => Number(item.count || 0))
+
+ trendChartInstance.setOption({
+ grid: { left: 16, right: 16, top: 32, bottom: 24, containLabel: true },
+ tooltip: { trigger: 'axis' },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: dates
+ },
+ yAxis: {
+ type: 'value',
+ minInterval: 1
+ },
+ series: [
+ {
+ name: '鎶ュ悕鏁伴噺',
+ type: 'line',
+ smooth: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { width: 3, color: '#409EFF' },
+ areaStyle: { color: 'rgba(64, 158, 255, 0.2)' },
+ data: values
+ }
+ ]
+ })
+}
+
+const renderRegionChart = () => {
+ if (!regionChartRef.value) return
+ if (!regionChartInstance) {
+ regionChartInstance = echarts.init(regionChartRef.value)
+ }
+
+ const pieData = regionStats.value.map(item => ({
+ name: item.regionName || UNASSIGNED_REGION_LABEL,
+ value: Number(item.count || 0)
+ }))
+
+ regionChartInstance.setOption({
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
+ legend: { orient: 'vertical', right: 10, top: 'center' },
+ series: [
+ {
+ name: '鍖哄煙鎶ュ悕',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 8,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: { formatter: '{b}\n{c}浜�' },
+ data: pieData.length > 0 ? pieData : [{ name: UNASSIGNED_REGION_LABEL, value: 0 }]
+ }
+ ]
+ })
+}
+
+const handleResize = () => {
+ trendChartInstance?.resize()
+ regionChartInstance?.resize()
+}
+
+const loadRegistrationTrend = async () => {
+ trendChartLoading.value = true
+ try {
+ const data = await getRegistrationTrend(15)
+ registrationTrend.value = data || []
+ await nextTick()
+ renderTrendChart()
+ } catch (error) {
+ console.error('鍔犺浇鎶ュ悕瓒嬪娍澶辫触:', error)
+ ElMessage.error('鍔犺浇鎶ュ悕瓒嬪娍澶辫触')
+ } finally {
+ trendChartLoading.value = false
+ }
+}
+
+const loadRegionStats = async () => {
+ regionChartLoading.value = true
+ try {
+ const data = await getRegionRegistrationStats()
+ regionStats.value = (data || [])
+ .filter(item => item.leafFlag !== false)
+ .map(item => ({
+ regionId: item.regionId ?? null,
+ regionName: item.regionName || UNASSIGNED_REGION_LABEL,
+ leafFlag: item.leafFlag,
+ count: item.count ?? 0
+ }))
+ await nextTick()
+ renderRegionChart()
+ } catch (error) {
+ console.error('鍔犺浇鍖哄煙鎶ュ悕缁熻澶辫触:', error)
+ ElMessage.error('鍔犺浇鍖哄煙鎶ュ悕缁熻澶辫触')
+ } finally {
+ regionChartLoading.value = false
+ }
+}
+
// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
onMounted(() => {
loadStats()
loadRecentActivities()
+ loadRegistrationTrend()
+ loadRegionStats()
+ window.addEventListener('resize', handleResize)
})
-// 鏌ョ湅姣旇禌
+onBeforeUnmount(() => {
+ window.removeEventListener('resize', handleResize)
+ if (trendChartInstance) {
+ trendChartInstance.dispose()
+ trendChartInstance = null
+ }
+ if (regionChartInstance) {
+ regionChartInstance.dispose()
+ regionChartInstance = null
+ }
+})
+
const viewActivity = (activity: any) => {
router.push(`/activity/${activity.id}`)
}
@@ -130,13 +298,14 @@
// 鑾峰彇鐘舵�佹牱寮忕被
const getStatusClass = (status: string) => {
const statusMap: Record<string, string> = {
- '宸插彂甯�': 'status-published',
- '杩涜涓�': 'status-published',
- '鏈彂甯�': 'status-unpublished',
- '鎶ュ悕涓�': 'status-unpublished',
- '鍏抽棴': 'status-closed',
- '宸茬粨鏉�': 'status-closed',
- '寰呭紑濮�': 'status-unpublished'
+ 宸插彂甯�: 'status-published',
+ 鍙戝竷: 'status-published',
+ 杩涜涓�: 'status-published',
+ 鏈彂甯�: 'status-unpublished',
+ 鎶ュ悕涓�: 'status-unpublished',
+ 寰呭紑濮�: 'status-unpublished',
+ 鍏抽棴: 'status-closed',
+ 宸茬粨鏉�: 'status-closed'
}
return statusMap[status] || 'status-unpublished'
}
@@ -146,18 +315,18 @@
/* 椤甸潰鏁翠綋鏍峰紡 */
.dashboard {
padding: 24px;
- background-color: #FFFFFF;
+ background-color: #ffffff;
min-height: 100vh;
}
-/* 缁熻鍗$墖琛� */
+/* 缁熻鍗$墖鍖哄煙 */
.stats-row {
margin-bottom: 20px;
}
/* 缁熻鍗$墖鏍峰紡 */
.stat-card {
- background: #FFFFFF;
+ background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: none;
@@ -173,7 +342,6 @@
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
-/* 鍥炬爣瀹瑰櫒 */
.icon-container {
width: 48px;
height: 48px;
@@ -187,49 +355,98 @@
}
.icon-container.blue {
- background-color: #E0E7FF;
- color: #6366F1;
+ background-color: #e0e7ff;
+ color: #6366f1;
}
.icon-container.green {
- background-color: #D1FAE5;
- color: #10B981;
+ background-color: #d1fae5;
+ color: #10b981;
}
.icon-container.yellow {
- background-color: #FEF3C7;
- color: #F59E0B;
+ background-color: #fef3c7;
+ color: #f59e0b;
}
.icon-container.red {
- background-color: #FECACA;
- color: #EF4444;
+ background-color: #fecaca;
+ color: #ef4444;
}
-/* 缁熻鏁板瓧 */
.stat-number {
font-size: 32px;
font-weight: 700;
- color: #1F2937;
+ color: #1f2937;
position: absolute;
top: 24px;
right: 24px;
- line-height: 1;
}
-/* 缁熻鏍囬 */
.stat-title {
font-size: 14px;
font-weight: 500;
- color: #6B7280;
+ color: #6b7280;
position: absolute;
bottom: 24px;
left: 24px;
}
-/* 琛ㄦ牸鍗$墖 */
+.chart-section {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 20px;
+ margin-top: 24px;
+}
+
+.chart-card {
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ border: none;
+ padding: 20px;
+ min-height: 340px;
+ position: relative;
+}
+
+.chart-mask {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.8);
+ z-index: 1;
+ font-size: 14px;
+ color: #6b7280;
+}
+
+.chart-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.chart-header-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2937;
+ margin: 0;
+}
+
+.chart-header-desc {
+ font-size: 13px;
+ color: #9ca3af;
+}
+
+.chart-container {
+ width: 100%;
+ height: 280px;
+}
+
.table-card {
- background: #FFFFFF;
+ background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: none;
@@ -237,7 +454,6 @@
margin-top: 20px;
}
-/* 琛ㄦ牸澶撮儴 */
.table-header {
margin-bottom: 20px;
display: flex;
@@ -248,26 +464,25 @@
.table-title {
font-size: 18px;
font-weight: 600;
- color: #1F2937;
+ color: #1f2937;
margin: 0;
}
-/* 琛ㄦ牸鏍峰紡 */
.recent-table {
width: 100%;
}
:deep(.el-table__header) {
- background-color: #F9FAFB;
+ background-color: #f9fafb;
}
:deep(.el-table__header th) {
- background-color: #F9FAFB !important;
+ background-color: #f9fafb !important;
color: #374151;
font-size: 14px;
font-weight: 500;
height: 48px;
- border-bottom: 1px solid #E5E7EB;
+ border-bottom: 1px solid #e5e7eb;
}
:deep(.el-table__row) {
@@ -275,48 +490,46 @@
}
:deep(.el-table__row:nth-child(even)) {
- background-color: #F9FAFB;
+ background-color: #f9fafb;
}
:deep(.el-table__row:nth-child(odd)) {
- background-color: #FFFFFF;
+ background-color: #ffffff;
}
:deep(.el-table td) {
- color: #1F2937;
+ color: #1f2937;
font-size: 14px;
font-weight: 400;
- border-bottom: 1px solid #F3F4F6;
+ border-bottom: 1px solid #f3f4f6;
}
-/* 鐘舵�佹爣绛炬牱寮� */
.status-published {
- color: #67C23A;
- background-color: #F0F9FF;
+ color: #67c23a;
+ background-color: #f0f9ff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-unpublished {
- color: #E6A23C;
- background-color: #FDF6EC;
+ color: #e6a23c;
+ background-color: #fdf6ec;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-closed {
- color: #F56C6C;
- background-color: #FEF0F0;
+ color: #f56c6c;
+ background-color: #fef0f0;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
-/* 鎿嶄綔閾炬帴鏍峰紡 */
.action-link {
- color: #409EFF;
+ color: #409eff;
cursor: pointer;
font-size: 14px;
margin: 0 8px;
@@ -324,7 +537,7 @@
}
.action-link:hover {
- color: #66B1FF;
+ color: #66b1ff;
text-decoration: underline;
}
@@ -332,14 +545,13 @@
margin-left: 0;
}
-/* 鍝嶅簲寮忚璁� */
@media (max-width: 768px) {
.dashboard {
padding: 16px;
}
-
+
.stat-card {
margin-bottom: 16px;
}
}
-</style>
\ No newline at end of file
+</style>
--
Gitblit v1.8.0