| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826 |
- <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>
|