|
|
@@ -1,2 +1,826 @@
|
|
|
<template>
|
|
|
+ <div class="app-container">
|
|
|
+ <!-- 统计数据卡片 -->
|
|
|
+ <el-row :gutter="20" class="mb20">
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-card class="stat-card total-people">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-number">{{ statistics.totalPeople }}</div>
|
|
|
+ <div class="stat-label">总人数</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-card class="stat-card total-entries">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-number">{{ statistics.totalEntries }}</div>
|
|
|
+ <div class="stat-label">总次数</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-card class="stat-card avg-frequency">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-number">{{ statistics.avgFrequency }}</div>
|
|
|
+ <div class="stat-label">平均频次</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-card class="stat-card max-frequency">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-number">{{ statistics.maxFrequency }}</div>
|
|
|
+ <div class="stat-label">最高频次</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-card class="stat-card night-entries">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-number">{{ statistics.nightPeople }}</div>
|
|
|
+ <div class="stat-label">夜间出入境人数</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-card class="stat-card holiday-entries">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-number">{{ statistics.holidayPeople }}</div>
|
|
|
+ <div class="stat-label">节假日出入境人数</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 图表区域 -->
|
|
|
+ <el-row :gutter="20" class="mb20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-card class="chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="chart-header">
|
|
|
+ <span>出入境频次分布</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="chart-container" ref="pieChartContainer" style="height: 300px;"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-card class="chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="chart-header">
|
|
|
+ <span>节假日出入境</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="holiday-container" style="height: 300px; overflow-y: auto; padding: 20px;">
|
|
|
+ <div v-loading="holidayLoading" class="holiday-list">
|
|
|
+ <div v-for="(item, index) in holidayList" :key="index" class="holiday-item">
|
|
|
+ <div class="holiday-content">
|
|
|
+ <span class="holiday-name">{{ item.holidayName }}</span>
|
|
|
+ <span class="holiday-dates">({{ item.startDate }}至{{ item.endDate }})</span>
|
|
|
+ <span class="holiday-count">{{ item.inOutCount }}次</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="holidayList.length === 0 && holidayLoading" class="no-data">
|
|
|
+ <span>加载中...</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="holidayList.length === 0 && !holidayLoading" class="no-data">
|
|
|
+ <span>暂无数据</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-row :gutter="20" class="mb20">
|
|
|
+ <el-col :span="24">
|
|
|
+ <el-card class="chart-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="chart-header">
|
|
|
+ <span>夜间出入境趋势</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="chart-container" ref="nightChartContainer" style="height: 300px;"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 快捷入口 -->
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="24">
|
|
|
+ <el-card class="quick-access-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>快捷入口</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="quick-access-grid">
|
|
|
+ <div class="quick-item" @click="goToPage('/anal/high')">
|
|
|
+ <div class="quick-icon bg-blue">
|
|
|
+ <el-icon><TrendCharts /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="quick-text">高频出入境</div>
|
|
|
+ </div>
|
|
|
+ <div class="quick-item" @click="goToPage('/anal/night')">
|
|
|
+ <div class="quick-icon bg-purple">
|
|
|
+ <el-icon><Moon /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="quick-text">夜间出入境</div>
|
|
|
+ </div>
|
|
|
+ <div class="quick-item" @click="goToPage('/anal/holiday')">
|
|
|
+ <div class="quick-icon bg-green">
|
|
|
+ <el-icon><Calendar /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="quick-text">节假日出入境</div>
|
|
|
+ </div>
|
|
|
+ <div class="quick-item" @click="goToPage('/anal/rapid')">
|
|
|
+ <div class="quick-icon bg-orange">
|
|
|
+ <el-icon><Lightning /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="quick-text">短期往返分析</div>
|
|
|
+ </div>
|
|
|
+ <div class="quick-item" @click="goToPage('/anal/low')">
|
|
|
+ <div class="quick-icon bg-red">
|
|
|
+ <el-icon><Document /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="quick-text">低频出入境</div>
|
|
|
+ </div>
|
|
|
+ <div class="quick-item" @click="goToPage('/anal/overstay')">
|
|
|
+ <div class="quick-icon bg-cyan">
|
|
|
+ <el-icon><Timer /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="quick-text">境外滞留分析</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
+
|
|
|
+<script setup name="Index">
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import { onMounted, onUnmounted, ref, reactive, toRefs, nextTick } from 'vue'
|
|
|
+import { House, TrendCharts, Moon, Calendar, Lightning, Document, Timer } from '@element-plus/icons-vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import { getInOutStats, listHighFrequencyInOut, getNightInOutStats, getHolidayInOutStats } from "@/api/biz/anal";
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+
|
|
|
+const holidayList = ref([]);
|
|
|
+const holidayLoading = ref(false);
|
|
|
+
|
|
|
+const pieChartContainer = ref(null);
|
|
|
+const nightChartContainer = ref(null);
|
|
|
+
|
|
|
+const statistics = ref({
|
|
|
+ totalPeople: 0,
|
|
|
+ totalEntries: 0,
|
|
|
+ avgFrequency: 0,
|
|
|
+ maxFrequency: 0,
|
|
|
+ nightPeople: 0,
|
|
|
+ holidayPeople: 0
|
|
|
+});
|
|
|
+
|
|
|
+// 直接使用当年查询,不再需要查询表单
|
|
|
+const data = reactive({
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ year: new Date().getFullYear().toString() // 直接使用当前年份
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const { queryParams } = toRefs(data);
|
|
|
+
|
|
|
+/** 查询数据 */
|
|
|
+function getList() {
|
|
|
+ // 准备查询参数 - 直接使用当前年份
|
|
|
+ const params = {
|
|
|
+ year: queryParams.value.year
|
|
|
+ };
|
|
|
+
|
|
|
+ // 设置加载状态
|
|
|
+ holidayLoading.value = true;
|
|
|
+
|
|
|
+ // 并行调用所有需要的API
|
|
|
+ Promise.all([
|
|
|
+ getInOutStats(params), // 基础统计
|
|
|
+ getNightInOutStats(params), // 夜间统计
|
|
|
+ getHolidayInOutStats(params), // 节假日统计
|
|
|
+ listHighFrequencyInOut(params) // 高频数据用于频次分布
|
|
|
+ ]).then(responses => {
|
|
|
+ console.log('API响应数据:', responses); // 调试信息
|
|
|
+
|
|
|
+ // 处理基础统计信息
|
|
|
+ const statsResponse = responses[0];
|
|
|
+ if (statsResponse.code === 200 && statsResponse.data) {
|
|
|
+ const stats = statsResponse.data;
|
|
|
+ statistics.value.totalPeople = stats.totalPeople || 0;
|
|
|
+ statistics.value.totalEntries = stats.totalCount || 0;
|
|
|
+ statistics.value.avgFrequency = Number(stats.avgFrequency).toFixed(2) || 0;
|
|
|
+ statistics.value.maxFrequency = stats.maxFrequency || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理夜间统计信息
|
|
|
+ const nightResponse = responses[1];
|
|
|
+ if (nightResponse.code === 200 && nightResponse.data) {
|
|
|
+ const nightStats = nightResponse.data;
|
|
|
+ statistics.value.nightPeople = nightStats.totalPeople || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理节假日统计信息
|
|
|
+ const holidayResponse = responses[2];
|
|
|
+ console.log('节假日统计响应:', holidayResponse); // 调试信息
|
|
|
+ if (holidayResponse.code === 200) {
|
|
|
+ // 节假日统计数据结构为数组,包含节假日名称、出入境次数等信息
|
|
|
+ const holidayData = Array.isArray(holidayResponse.data) ? holidayResponse.data : [];
|
|
|
+ console.log('节假日统计数据:', holidayData); // 调试信息
|
|
|
+
|
|
|
+ // 计算总人数
|
|
|
+ const totalHolidayCount = holidayData.reduce((sum, item) => sum + (item.inOutCount || 0), 0);
|
|
|
+ statistics.value.holidayPeople = totalHolidayCount;
|
|
|
+
|
|
|
+ // 处理节假日列表数据
|
|
|
+ holidayList.value = holidayData.map(item => ({
|
|
|
+ holidayName: item.holidayName,
|
|
|
+ inOutCount: item.inOutCount,
|
|
|
+ startDate: item.startDate,
|
|
|
+ endDate: item.endDate
|
|
|
+ }));
|
|
|
+ } else {
|
|
|
+ console.error('节假日数据获取失败:', holidayResponse);
|
|
|
+ holidayList.value = [];
|
|
|
+ statistics.value.holidayPeople = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新图表
|
|
|
+ nextTick(() => {
|
|
|
+ renderPieChart(params); // 频次分布饼图
|
|
|
+ renderNightChart(params); // 夜间趋势图
|
|
|
+ });
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('获取统计数据失败:', error);
|
|
|
+ holidayList.value = [];
|
|
|
+ statistics.value.holidayPeople = 0;
|
|
|
+ }).finally(() => {
|
|
|
+ holidayLoading.value = false;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** 渲染出入境频次分布饼图 */
|
|
|
+function renderPieChart(params) {
|
|
|
+ if (!pieChartContainer.value) return;
|
|
|
+
|
|
|
+ const myChart = echarts.init(pieChartContainer.value);
|
|
|
+
|
|
|
+ // 调用后端接口获取频次分布数据
|
|
|
+ getInOutStats(params).then(response => {
|
|
|
+ let distributionData = [];
|
|
|
+
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ const stats = response.data;
|
|
|
+
|
|
|
+ // 检查是否有频次分布数据
|
|
|
+ if (stats.frequencyDistribution && typeof stats.frequencyDistribution === 'object') {
|
|
|
+ const dist = stats.frequencyDistribution;
|
|
|
+ distributionData = [];
|
|
|
+
|
|
|
+ // 将对象格式转换为饼图需要的数组格式,并按区间排序
|
|
|
+ const rangeOrder = ['1-5', '5-10', '10-20', '20-30', '30+'];
|
|
|
+
|
|
|
+ // 先处理预定义的区间
|
|
|
+ for (const range of rangeOrder) {
|
|
|
+ if (dist.hasOwnProperty(range)) {
|
|
|
+ // 根据区间范围选择颜色
|
|
|
+ let color;
|
|
|
+ if (range.startsWith('1-5')) color = '#5470c6';
|
|
|
+ else if (range.startsWith('5-10')) color = '#91cc75';
|
|
|
+ else if (range.startsWith('10-20')) color = '#fac858';
|
|
|
+ else if (range.startsWith('20-30')) color = '#ee6666';
|
|
|
+ else if (range.startsWith('30')) color = '#73c0de';
|
|
|
+ else color = '#9da4b0';
|
|
|
+
|
|
|
+ distributionData.push({
|
|
|
+ value: dist[range],
|
|
|
+ name: range,
|
|
|
+ itemStyle: { color: color }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理不在预定义区间中的其他区间
|
|
|
+ for (const [range, count] of Object.entries(dist)) {
|
|
|
+ if (!rangeOrder.includes(range)) {
|
|
|
+ let color;
|
|
|
+ if (range.startsWith('1-5')) color = '#5470c6';
|
|
|
+ else if (range.startsWith('5-10')) color = '#91cc75';
|
|
|
+ else if (range.startsWith('10-20')) color = '#fac858';
|
|
|
+ else if (range.startsWith('20-30')) color = '#ee6666';
|
|
|
+ else if (range.startsWith('30')) color = '#73c0de';
|
|
|
+ else color = '#9da4b0';
|
|
|
+
|
|
|
+ distributionData.push({
|
|
|
+ value: count,
|
|
|
+ name: range,
|
|
|
+ itemStyle: { color: color }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ formatter: '{a} <br/>{b}: {c}人 ({d}%)'
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'vertical',
|
|
|
+ left: 'right'
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name: '人数',
|
|
|
+ type: 'pie',
|
|
|
+ radius: [20, 120], // 设置内外半径,形成南丁格尔图效果
|
|
|
+ center: ['50%', '50%'],
|
|
|
+ roseType: 'area', // 启用南丁格尔图模式
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: 8
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ formatter: '{b}: {c}',
|
|
|
+ fontSize: 10,
|
|
|
+ position: 'outside'
|
|
|
+ },
|
|
|
+ data: distributionData,
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetX: 0,
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }]
|
|
|
+ };
|
|
|
+
|
|
|
+ myChart.setOption(option);
|
|
|
+ }).catch(() => {
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item'
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'vertical',
|
|
|
+ left: 'right'
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name: '人数',
|
|
|
+ type: 'pie',
|
|
|
+ radius: [20, 120],
|
|
|
+ center: ['50%', '50%'],
|
|
|
+ roseType: 'area',
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: 8
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ formatter: '{b}: {c}',
|
|
|
+ fontSize: 10,
|
|
|
+ position: 'outside'
|
|
|
+ },
|
|
|
+ data: [],
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetX: 0,
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }]
|
|
|
+ };
|
|
|
+
|
|
|
+ myChart.setOption(option);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** 渲染夜间出入境趋势图 */
|
|
|
+function renderNightChart(params) {
|
|
|
+ if (!nightChartContainer.value) return;
|
|
|
+
|
|
|
+ const myChart = echarts.init(nightChartContainer.value);
|
|
|
+
|
|
|
+ getNightInOutStats(params).then(response => {
|
|
|
+ let xAxisData = [];
|
|
|
+ let seriesData = [];
|
|
|
+
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ const stats = response.data;
|
|
|
+
|
|
|
+ // 使用趋势数据
|
|
|
+ if (stats.trendData && Array.isArray(stats.trendData)) {
|
|
|
+ // 按年查询,显示每月趋势
|
|
|
+ xAxisData = stats.trendData.map(item => {
|
|
|
+ if (item.period) {
|
|
|
+ const parts = item.period.split('-');
|
|
|
+ if (parts.length === 2) {
|
|
|
+ return parts[1] + '月';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return '未知';
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取人数或次数数据
|
|
|
+ seriesData = stats.trendData.map(item => item.inOutCount !== undefined ? item.inOutCount : 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ },
|
|
|
+ formatter: (params) => {
|
|
|
+ const param = params[0];
|
|
|
+ if (param.name === '暂无数据') {
|
|
|
+ return '暂无数据';
|
|
|
+ }
|
|
|
+ const date = xAxisData[param.dataIndex];
|
|
|
+ const value = param.value;
|
|
|
+ return `${date}<br/>夜间出入境人数:${value}`;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: xAxisData,
|
|
|
+ axisLabel: {
|
|
|
+ rotate: 45,
|
|
|
+ fontSize: 12
|
|
|
+ },
|
|
|
+ boundaryGap: false
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '人数',
|
|
|
+ show: true
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ data: seriesData,
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'emptyCircle',
|
|
|
+ symbolSize: 6,
|
|
|
+ lineStyle: {
|
|
|
+ color: '#73c0de',
|
|
|
+ width: 2
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: '#3ba272'
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: 'rgba(115, 192, 222, 0.3)' },
|
|
|
+ { offset: 1, color: 'rgba(115, 192, 222, 0.05)' }
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ }]
|
|
|
+ };
|
|
|
+
|
|
|
+ myChart.setOption(option);
|
|
|
+ }).catch(() => {
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: ['暂无数据'],
|
|
|
+ axisLabel: {
|
|
|
+ rotate: 45,
|
|
|
+ fontSize: 12
|
|
|
+ },
|
|
|
+ boundaryGap: false
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '人数',
|
|
|
+ show: true
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ data: [0],
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'emptyCircle',
|
|
|
+ symbolSize: 6,
|
|
|
+ lineStyle: {
|
|
|
+ color: '#73c0de',
|
|
|
+ width: 2
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: '#3ba272'
|
|
|
+ }
|
|
|
+ }]
|
|
|
+ };
|
|
|
+
|
|
|
+ myChart.setOption(option);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** 跳转到指定页面 */
|
|
|
+function goToPage(path) {
|
|
|
+ router.push(path)
|
|
|
+}
|
|
|
+
|
|
|
+// 页面加载时获取数据 - 直接查询当年数据
|
|
|
+onMounted(() => {
|
|
|
+ getList();
|
|
|
+});
|
|
|
+
|
|
|
+// 组件卸载时销毁图表
|
|
|
+onUnmounted(() => {
|
|
|
+ if (pieChartContainer.value) {
|
|
|
+ const pieChart = echarts.getInstanceByDom(pieChartContainer.value);
|
|
|
+ if (pieChart) pieChart.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nightChartContainer.value) {
|
|
|
+ const nightChart = echarts.getInstanceByDom(nightChartContainer.value);
|
|
|
+ if (nightChart) nightChart.dispose();
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.app-container {
|
|
|
+ padding: 20px;
|
|
|
+ background-color: #f5f7f9;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ height: 100px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card:hover {
|
|
|
+ transform: translateY(-3px);
|
|
|
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.total-people {
|
|
|
+ background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
|
|
+ color: #1890ff;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.total-entries {
|
|
|
+ background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
|
|
|
+ color: #52c41a;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.avg-frequency {
|
|
|
+ background: linear-gradient(135deg, #fffbe6 0%, #fff5a0 100%);
|
|
|
+ color: #faad14;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.max-frequency {
|
|
|
+ background: linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%);
|
|
|
+ color: #f5222d;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.night-entries {
|
|
|
+ background: linear-gradient(135deg, #f0f9ff 0%, #c0e8ff 100%);
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.holiday-entries {
|
|
|
+ background: linear-gradient(135deg, #fff0f5 0%, #ffd6e7 100%);
|
|
|
+ color: #f759ab;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-item {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-number {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-header {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.table-container {
|
|
|
+ width: 100%;
|
|
|
+ padding: 0 20px 20px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-container {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-item {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border: 1px solid #c0e8ff;
|
|
|
+ border-radius: 6px;
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ box-shadow: 0 1px 4px rgba(192, 232, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-item:hover {
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+ border-color: #c0c4cc;
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-name {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #1890ff;
|
|
|
+ margin-right: 6px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-dates {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #52c41a;
|
|
|
+ margin-right: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-count {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #f5222d;
|
|
|
+ margin-left: auto;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.no-data {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px 0;
|
|
|
+ color: #909399;
|
|
|
+ font-style: italic;
|
|
|
+}
|
|
|
+
|
|
|
+.data-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-access-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.quick-access-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(6, 1fr);
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-item {
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 20px 10px;
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ background-color: #fafafa;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-item:hover {
|
|
|
+ transform: translateY(-3px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ background-color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-icon {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin: 0 auto 10px;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 24px;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.bg-blue {
|
|
|
+ background: linear-gradient(135deg, #409eff 0%, #4a9eff 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.bg-purple {
|
|
|
+ background: linear-gradient(135deg, #7265e3 0%, #7a6de3 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.bg-green {
|
|
|
+ background: linear-gradient(135deg, #20c997 0%, #26d19c 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.bg-orange {
|
|
|
+ background: linear-gradient(135deg, #f56c6c 0%, #f67878 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.bg-red {
|
|
|
+ background: linear-gradient(135deg, #e6a23c 0%, #eca742 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.bg-cyan {
|
|
|
+ background: linear-gradient(135deg, #17b3a3 0%, #19c0b2 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.quick-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.mb20 {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .quick-access-grid {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .quick-access-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-col-4 {
|
|
|
+ width: 50%;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-col-12 {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-row {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|