¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container statistics-container"> |
| | | |
| | | <!-- æ»ä½ç»è®¡å¡ç --> |
| | | <el-row :gutter="20" class="statistics-cards"> |
| | | <el-col :span="6" v-for="(item, index) in overviewData" :key="index"> |
| | | <el-card class="statistics-card" :class="item.type"> |
| | | <div class="card-content"> |
| | | <div class="card-icon"> |
| | | <el-icon :size="32"> |
| | | <component :is="item.icon" /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="card-info"> |
| | | <div class="card-number"> |
| | | <el-skeleton-item v-if="loading" variant="text" style="width: 60px; height: 32px;" /> |
| | | <span v-else>{{ item.value }}</span> |
| | | </div> |
| | | <div class="card-label">{{ item.label }}</div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <el-row :gutter="20" class="charts-section"> |
| | | <el-col :span="12"> |
| | | <el-card class="chart-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>æ¡£æ¡åç±»ç»è®¡</span> |
| | | </div> |
| | | </template> |
| | | <div class="chart-container"> |
| | | <div ref="categoryChartRef" class="chart"></div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card class="chart-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>æ¡£æ¡ç¶æç»è®¡</span> |
| | | </div> |
| | | </template> |
| | | <div class="chart-container"> |
| | | <div ref="statusChartRef" class="chart"></div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, nextTick, onUnmounted } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { Refresh } from "@element-plus/icons-vue"; |
| | | import * as echarts from "echarts"; |
| | | import { |
| | | getDocumentationOverview, |
| | | getDocumentationCategoryStats, |
| | | getDocumentationStatusStats |
| | | } from "@/api/fileManagement/document"; |
| | | import { |
| | | Document, |
| | | Folder, |
| | | Tickets, |
| | | Calendar |
| | | } from "@element-plus/icons-vue"; |
| | | |
| | | // ååºå¼æ°æ® |
| | | const overviewData = ref([ |
| | | { |
| | | label: "æ»æ¡£æ¡æ°", |
| | | value: 0, |
| | | icon: "Document", |
| | | type: "primary", |
| | | }, |
| | | { |
| | | label: "åç±»æ°é", |
| | | value: 0, |
| | | icon: "Folder", |
| | | type: "success", |
| | | }, |
| | | { |
| | | label: "ååºæ¡£æ¡", |
| | | value: 0, |
| | | icon: "Tickets", |
| | | type: "warning", |
| | | }, |
| | | { |
| | | label: "æ¬ææ°å¢", |
| | | value: 0, |
| | | icon: "Calendar", |
| | | type: "info", |
| | | }, |
| | | ]); |
| | | |
| | | const categoryChartRef = ref(null); |
| | | const statusChartRef = ref(null); |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let categoryChart = null; |
| | | let statusChart = null; |
| | | |
| | | // å è½½ç¶æ |
| | | const loading = ref(false); |
| | | const autoRefreshInterval = ref(null); |
| | | |
| | | // èªå¨å·æ°å¼å
³ |
| | | const autoRefreshEnabled = ref(true); |
| | | |
| | | // èªå¨å·æ°é´éï¼5åéï¼ |
| | | const AUTO_REFRESH_INTERVAL = 5 * 60 * 1000; |
| | | |
| | | // å¯å¨èªå¨å·æ° |
| | | const startAutoRefresh = () => { |
| | | if (autoRefreshInterval.value) { |
| | | clearInterval(autoRefreshInterval.value); |
| | | } |
| | | if (autoRefreshEnabled.value) { |
| | | autoRefreshInterval.value = setInterval(() => { |
| | | refreshData(); |
| | | }, AUTO_REFRESH_INTERVAL); |
| | | } |
| | | }; |
| | | |
| | | // 忢èªå¨å·æ° |
| | | const stopAutoRefresh = () => { |
| | | if (autoRefreshInterval.value) { |
| | | clearInterval(autoRefreshInterval.value); |
| | | autoRefreshInterval.value = null; |
| | | } |
| | | }; |
| | | |
| | | // 忢èªå¨å·æ°ç¶æ |
| | | const toggleAutoRefresh = (value) => { |
| | | if (value) { |
| | | startAutoRefresh(); |
| | | } else { |
| | | stopAutoRefresh(); |
| | | } |
| | | }; |
| | | |
| | | // å è½½æ»ä½ç»è®¡æ°æ® |
| | | const loadOverviewData = async () => { |
| | | try { |
| | | const response = await getDocumentationOverview(); |
| | | if (response.code === 200) { |
| | | const data = response.data; |
| | | overviewData.value[0].value = data.totalDocsCount || 0; |
| | | overviewData.value[1].value = data.categoryNumCount || 0; |
| | | overviewData.value[2].value = data.borrowedDocsCount || 0; |
| | | overviewData.value[3].value = data.monthlyAddedDocsCount || 0; |
| | | } |
| | | } catch (error) { |
| | | console.error('å è½½æ»ä½ç»è®¡æ°æ®å¤±è´¥:', error); |
| | | ElMessage.error('å è½½æ»ä½ç»è®¡æ°æ®å¤±è´¥'); |
| | | } |
| | | }; |
| | | |
| | | // å è½½åç±»ç»è®¡æ°æ® |
| | | const loadCategoryData = async () => { |
| | | try { |
| | | const response = await getDocumentationCategoryStats(); |
| | | if (response.code === 200) { |
| | | renderCategoryChart(response.data); |
| | | } |
| | | } catch (error) { |
| | | console.error('å è½½åç±»ç»è®¡æ°æ®å¤±è´¥:', error); |
| | | ElMessage.error('å è½½åç±»ç»è®¡æ°æ®å¤±è´¥'); |
| | | } |
| | | }; |
| | | |
| | | // å è½½ç¶æç»è®¡æ°æ® |
| | | const loadStatusData = async () => { |
| | | try { |
| | | const response = await getDocumentationStatusStats(); |
| | | if (response.code === 200) { |
| | | renderStatusChart(response.data); |
| | | } |
| | | } catch (error) { |
| | | console.error('å è½½ç¶æç»è®¡æ°æ®å¤±è´¥:', error); |
| | | ElMessage.error('å è½½ç¶æç»è®¡æ°æ®å¤±è´¥'); |
| | | } |
| | | }; |
| | | |
| | | // å·æ°æ°æ® |
| | | const refreshData = async () => { |
| | | loading.value = true; |
| | | try { |
| | | await Promise.all([ |
| | | loadOverviewData(), |
| | | loadCategoryData(), |
| | | loadStatusData() |
| | | ]); |
| | | ElMessage.success('æ°æ®å·æ°æå'); |
| | | } catch (error) { |
| | | console.error('å·æ°æ°æ®å¤±è´¥:', error); |
| | | ElMessage.error('å·æ°æ°æ®å¤±è´¥'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | // å»¶è¿åå§åï¼ç¡®ä¿DOMå
ç´ å·²ç»æ¸²æ |
| | | setTimeout(() => { |
| | | if (categoryChartRef.value) { |
| | | categoryChart = echarts.init(categoryChartRef.value); |
| | | } |
| | | |
| | | if (statusChartRef.value) { |
| | | statusChart = echarts.init(statusChartRef.value); |
| | | } |
| | | |
| | | // åå§å宿åå è½½æ°æ® |
| | | loadCategoryData(); |
| | | loadStatusData(); |
| | | }, 300); |
| | | }; |
| | | |
| | | // 渲æåç±»ç»è®¡å¾è¡¨ |
| | | const renderCategoryChart = (data) => { |
| | | if (!categoryChart) return; |
| | | let newData = data.map(item => { |
| | | return { |
| | | name: item.category, |
| | | value: item.count |
| | | } |
| | | }) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: "æ¡£æ¡åç±»åå¸", |
| | | left: "center", |
| | | textStyle: { |
| | | fontSize: 16, |
| | | fontWeight: "normal", |
| | | }, |
| | | }, |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{a} <br/>{b}: {c} ({d}%)", |
| | | }, |
| | | legend: { |
| | | orient: "vertical", |
| | | left: "left", |
| | | top: "middle", |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "æ¡£æ¡æ°é", |
| | | type: "pie", |
| | | radius: ["40%", "70%"], |
| | | center: ["60%", "50%"], |
| | | data: newData || [ |
| | | { name: "ææ¯ææ¡£", value: 450 }, |
| | | { name: "ç®¡çææ¡£", value: 320 }, |
| | | { name: "è´¢å¡ææ¡£", value: 280 }, |
| | | { name: "äººäºææ¡£", value: 200 }, |
| | | ], |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | try { |
| | | categoryChart.setOption(option); |
| | | } catch (error) { |
| | | console.error('åç±»å¾è¡¨æ¸²æå¤±è´¥:', error); |
| | | } |
| | | }; |
| | | |
| | | // 渲æç¶æç»è®¡å¾è¡¨ |
| | | const renderStatusChart = (data) => { |
| | | if (!statusChart) return; |
| | | let newData = data.map(item => { |
| | | return { |
| | | name: item.docStatus, |
| | | value: item.count |
| | | } |
| | | }) |
| | | const option = { |
| | | title: { |
| | | text: "æ¡£æ¡ç¶æåå¸", |
| | | left: "center", |
| | | textStyle: { |
| | | fontSize: 16, |
| | | fontWeight: "normal", |
| | | }, |
| | | }, |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{a} <br/>{b}: {c} ({d}%)", |
| | | }, |
| | | legend: { |
| | | orient: "vertical", |
| | | left: "left", |
| | | top: "middle", |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "æ¡£æ¡æ°é", |
| | | type: "pie", |
| | | radius: ["40%", "70%"], |
| | | center: ["60%", "50%"], |
| | | roseType: false, |
| | | data: newData || [ |
| | | { name: "æ£å¸¸", value: 1150, itemStyle: { color: "#67C23A" } }, |
| | | { name: "ååº", value: 89, itemStyle: { color: "#E6A23C" } }, |
| | | { name: "丢失", value: 8, itemStyle: { color: "#F56C6C" } }, |
| | | { name: "æå", value: 4, itemStyle: { color: "#909399" } }, |
| | | ], |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | try { |
| | | statusChart.setOption(option); |
| | | } catch (error) { |
| | | console.error('ç¶æå¾è¡¨æ¸²æå¤±è´¥:', error); |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | loadOverviewData(); |
| | | initCharts(); |
| | | startAutoRefresh(); |
| | | }); |
| | | |
| | | // ç»ä»¶å¸è½½æ¶æ¸
ç宿¶å¨ |
| | | onUnmounted(() => { |
| | | stopAutoRefresh(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .statistics-container { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .page-header { |
| | | text-align: center; |
| | | margin-bottom: 30px; |
| | | padding: 20px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | border-radius: 12px; |
| | | color: white; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | color: white; |
| | | margin-bottom: 10px; |
| | | font-size: 28px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .page-header p { |
| | | color: rgba(255, 255, 255, 0.9); |
| | | font-size: 14px; |
| | | margin: 0 0 15px 0; |
| | | } |
| | | |
| | | .header-controls { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | margin-top: 10px; |
| | | gap: 20px; |
| | | } |
| | | |
| | | .refresh-btn { |
| | | margin-left: 20px; |
| | | } |
| | | |
| | | .statistics-cards { |
| | | margin-bottom: 30px; |
| | | } |
| | | |
| | | .statistics-card { |
| | | border-radius: 12px; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | transition: all 0.3s ease; |
| | | border: none; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .statistics-card:hover { |
| | | transform: translateY(-5px); |
| | | box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .statistics-card.primary { |
| | | border-left: 4px solid #409EFF; |
| | | background: linear-gradient(135deg, #409EFF 0%, #36a3f7 100%); |
| | | } |
| | | |
| | | .statistics-card.success { |
| | | border-left: 4px solid #67C23A; |
| | | background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%); |
| | | } |
| | | |
| | | .statistics-card.warning { |
| | | border-left: 4px solid #E6A23C; |
| | | background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%); |
| | | } |
| | | |
| | | .statistics-card.info { |
| | | border-left: 4px solid #909399; |
| | | background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%); |
| | | } |
| | | |
| | | .card-content { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .card-icon { |
| | | margin-right: 20px; |
| | | color: white; |
| | | } |
| | | |
| | | .card-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-number { |
| | | font-size: 32px; |
| | | font-weight: 600; |
| | | color: white; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .card-label { |
| | | font-size: 14px; |
| | | color: rgba(255, 255, 255, 0.9); |
| | | } |
| | | |
| | | .charts-section { |
| | | margin-bottom: 30px; |
| | | } |
| | | |
| | | .chart-card { |
| | | border-radius: 12px; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | border: none; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | padding: 15px 20px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 400px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .chart { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | /* ååºå¼è®¾è®¡ */ |
| | | @media (max-width: 768px) { |
| | | .statistics-container { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .page-header { |
| | | padding: 15px; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .header-controls { |
| | | flex-direction: column; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .refresh-btn { |
| | | margin-left: 0; |
| | | } |
| | | |
| | | .statistics-cards .el-col { |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .charts-section .el-col { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 300px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 480px) { |
| | | .page-header h2 { |
| | | font-size: 20px; |
| | | } |
| | | |
| | | .card-number { |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 250px; |
| | | } |
| | | } |
| | | </style> |