From ba94ceae1315174798ae1967ef62268c6d16cd5b Mon Sep 17 00:00:00 2001 From: Codex Assistant <codex@example.com> Date: 星期一, 06 十月 2025 22:07:06 +0800 Subject: [PATCH] feat: 评审与活动相关改动 - backend(GraphQL): Activity schema 增加 updateActivityState(id, state);实现 resolver/service 仅更新 state=2 作为逻辑删除 - backend(GraphQL): region.graphqls 新增 Query leafRegions - backend(GraphQL): player.graphqls 的 projectReviewApplications 增加可选参数 regionId - backend(Service): listProjectReviewApplications 绑定 regionId 参数,修复 QueryParameterException - frontend(web): 新增 api/activity.js 的 updateActivityState 并接入 activity-list 删除逻辑 - frontend(web): review-list.vue 权限仅校验登录,移除角色限制;查询参数修正为 name/regionId - frontend(web): 删除未引用的 ActivityList.vue - frontend(web): projectReviewNew.js GraphQL 查询增加 name 参数 --- 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