|
|
@@ -0,0 +1,765 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container">
|
|
|
+ <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
|
|
|
+ <el-form-item label="查询方式" prop="queryType">
|
|
|
+ <el-select v-model="queryParams.queryType" placeholder="请选择查询方式" clearable style="width: 150px;" @change="handleQuery">
|
|
|
+ <el-option label="按年查询" value="year" />
|
|
|
+ <el-option label="按月查询" value="month" />
|
|
|
+ <el-option label="按时段查询" value="range" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="queryParams.queryType === 'year'" label="年份" prop="year">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="queryParams.year"
|
|
|
+ type="year"
|
|
|
+ value-format="YYYY"
|
|
|
+ placeholder="请选择年份"
|
|
|
+ @change="handleQuery">
|
|
|
+ </el-date-picker>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="queryParams.queryType === 'month'" label="月份" prop="month">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="queryParams.month"
|
|
|
+ type="month"
|
|
|
+ value-format="YYYY-MM"
|
|
|
+ placeholder="请选择月份"
|
|
|
+ @change="handleQuery">
|
|
|
+ </el-date-picker>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="queryParams.queryType === 'range'" label="时段" prop="dateRange">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="queryParams.dateRange"
|
|
|
+ type="daterange"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ @change="handleQuery">
|
|
|
+ </el-date-picker>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 统计数据卡片 -->
|
|
|
+ <el-row :gutter="20" class="mb20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <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="6">
|
|
|
+ <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="6">
|
|
|
+ <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="6">
|
|
|
+ <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-row>
|
|
|
+
|
|
|
+ <!-- 趋势图 -->
|
|
|
+ <el-row :gutter="10" style="margin-bottom: 20px;">
|
|
|
+ <div style="height: 500px; width: 100%;">
|
|
|
+ <div class="table_caption" style="height: 30px; line-height: 30px;">夜间出入境人员数量趋势图</div>
|
|
|
+ <div style="height: 470px; width: 100%; border: 1px solid #ededed; background: #fff;" ref="trendChartContainer">
|
|
|
+ <div id="trendChart" style="height: 100%; width: 100%;"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 明细数据表格 -->
|
|
|
+ <el-table v-loading="loading" :data="detailsList" style="margin-top: 20px;" @row-click="handleRowClick">
|
|
|
+ <el-table-column label="序号" type="index" width="50" align="center" />
|
|
|
+ <el-table-column label="姓名" prop="fullName" align="center" width="300"/>
|
|
|
+ <el-table-column label="性别" align="center" prop="genderCn"/>
|
|
|
+ <el-table-column label="出生日期" align="center" prop="birthDate" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ parseTime(scope.row.birthDate, '{y}-{m}-{d}') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="国家/地区" align="center" prop="countryName"/>
|
|
|
+ <el-table-column label="民族" align="center" prop="ethnicityName"/>
|
|
|
+ <el-table-column label="出入标识" align="center" prop="inOutFlag"/>
|
|
|
+ <el-table-column label="出入时间" align="center" prop="inOutTime" width="180">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ parseTime(scope.row.inOutTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="出入口岸" align="center" prop="portCode">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ scope.row.portCode}}-{{scope.row.portName}}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="前往地/出发地" align="center" prop="destinationName"/>
|
|
|
+ <el-table-column label="导入时间" align="center" prop="createTime" width="150">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div style="margin-top: 10px;">
|
|
|
+ <div v-if="detailsList.length === 0 && !loading" style="margin-top: 10px; color: #f56c6c;">
|
|
|
+ <el-icon><Warning /></el-icon> 暂无数据,请检查API返回结果
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 出入境记录明细对话框 -->
|
|
|
+ <el-dialog :title="detailTitle" v-model="recordDetailOpen" width="900px" append-to-body>
|
|
|
+ <el-form :model="detailForm" label-width="120px" disabled>
|
|
|
+ <!-- 个人信息 -->
|
|
|
+ <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">个人信息</div>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="人员类别:">
|
|
|
+ <span>{{ detailForm.personnelCategoryName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="姓名:">
|
|
|
+ <span>{{ detailForm.fullName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="性别:">
|
|
|
+ <span>{{ detailForm.genderCn }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="出生日期:">
|
|
|
+ <span>{{ parseTime(detailForm.birthDate, '{y}-{m}-{d}') }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="国家/地区:">
|
|
|
+ <span>{{ detailForm.countryCode }}-{{ detailForm.countryName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="民族:">
|
|
|
+ <span>{{ detailForm.ethnicityName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 证件信息 -->
|
|
|
+ <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">证件信息</div>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="证件类别:">
|
|
|
+ <span>{{ detailForm.idTypeName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="证件号码:">
|
|
|
+ <span>{{ detailForm.idNumber }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="签证类型:">
|
|
|
+ <span>{{ detailForm.visaTypeName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="签证号码:">
|
|
|
+ <span>{{ detailForm.visaNumber }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="停留期:">
|
|
|
+ <span>{{ detailForm.stayDuration }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 出入境信息 -->
|
|
|
+ <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">出入境信息</div>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="出入标识:">
|
|
|
+ <span>{{ detailForm.inOutFlag }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="出入时间:">
|
|
|
+ <span>{{ parseTime(detailForm.inOutTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="出入口岸:">
|
|
|
+ <span>{{ detailForm.portCode }}-{{ detailForm.portName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="交通方式:">
|
|
|
+ <span>{{ detailForm.transportMode }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="交通工具:">
|
|
|
+ <span>{{ detailForm.transportVehicle }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="前往地/出发地:">
|
|
|
+ <span>{{ detailForm.destinationCode }}-{{ detailForm.destinationName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="出入境事由:">
|
|
|
+ <span>{{ detailForm.reasonName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="发证机关:">
|
|
|
+ <span>{{ detailForm.issuingAuthorityCode }}-{{ detailForm.issuingAuthorityName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 其他信息 -->
|
|
|
+ <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">其他信息</div>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="自助通道标记:">
|
|
|
+ <span>{{ detailForm.selfServiceFlag }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="后台补录标记:">
|
|
|
+ <span>{{ detailForm.backfillFlag }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="创建时间:">
|
|
|
+ <span>{{ parseTime(detailForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="疑难字说明:">
|
|
|
+ <span>{{ detailForm.remark }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="recordDetailOpen = false">关 闭</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <Pagination
|
|
|
+ v-show="total>0"
|
|
|
+ :total="total"
|
|
|
+ v-model:page="queryParams.pageNum"
|
|
|
+ v-model:limit="queryParams.pageSize"
|
|
|
+ @pagination="handlePagination"
|
|
|
+ />
|
|
|
+ <div v-if="total === 0 && !loading" style="text-align: center; padding: 20px; color: #909399;">
|
|
|
+ 暂无数据
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup name="Night">
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import { getNightInOutStats, getNightInOutDetail } from "@/api/biz/anal";
|
|
|
+import { getCurrentInstance, nextTick, onMounted, onUnmounted, reactive, ref, toRefs, watch } from 'vue';
|
|
|
+import Pagination from "@/components/Pagination/index.vue";
|
|
|
+import { parseTime, resetForm } from "@/utils/ruoyi";
|
|
|
+import { Pointer, Warning } from "@element-plus/icons-vue";
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance();
|
|
|
+
|
|
|
+const detailsList = ref([]);
|
|
|
+const loading = ref(false);
|
|
|
+const total = ref(0);
|
|
|
+const trendChartContainer = ref(null);
|
|
|
+
|
|
|
+const statistics = ref({
|
|
|
+ totalPeople: 0,
|
|
|
+ totalEntries: 0,
|
|
|
+ avgFrequency: 0,
|
|
|
+ maxFrequency: 0
|
|
|
+});
|
|
|
+
|
|
|
+// 出入境记录明细相关
|
|
|
+const recordDetailOpen = ref(false);
|
|
|
+const recordDetailLoading = ref(false);
|
|
|
+const detailTitle = ref("详细信息");
|
|
|
+const detailForm = ref({});
|
|
|
+
|
|
|
+const data = reactive({
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ queryType: 'year', // 默认查询类型为年
|
|
|
+ year: new Date().getFullYear().toString(), // 默认年份
|
|
|
+ month: null, // 初始不设置月份
|
|
|
+ dateRange: null // 初始不设置日期范围
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const { queryParams } = toRefs(data);
|
|
|
+
|
|
|
+// 监听查询方式变化,设置默认值
|
|
|
+watch(() => queryParams.value.queryType, (newVal) => {
|
|
|
+ if (newVal === 'month' && (!queryParams.value.month || queryParams.value.month === null)) {
|
|
|
+ // 当选择按月查询时,默认选中当月
|
|
|
+ const now = new Date();
|
|
|
+ queryParams.value.month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
|
+ // 立即查询
|
|
|
+ handleQuery();
|
|
|
+ } else if (newVal === 'range' && (!queryParams.value.dateRange || queryParams.value.dateRange.length === 0)) {
|
|
|
+ // 当选择按时段查询时,默认最近7天
|
|
|
+ const now = new Date();
|
|
|
+ const startDate = new Date();
|
|
|
+ startDate.setDate(now.getDate() - 6); // 最近7天
|
|
|
+
|
|
|
+ const startStr = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`;
|
|
|
+ const endStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
|
+
|
|
|
+ queryParams.value.dateRange = [startStr, endStr];
|
|
|
+ // 立即查询
|
|
|
+ handleQuery();
|
|
|
+ } else if (newVal === 'year') {
|
|
|
+ // 当选择按年查询时,也立即查询
|
|
|
+ handleQuery();
|
|
|
+ }
|
|
|
+}, { immediate: true });
|
|
|
+
|
|
|
+/** 查询夜间出入境数据 */
|
|
|
+function getList() {
|
|
|
+ loading.value = true;
|
|
|
+
|
|
|
+ // 准备查询参数
|
|
|
+ const params = {};
|
|
|
+ if (queryParams.value.queryType === 'year') {
|
|
|
+ params.year = queryParams.value.year;
|
|
|
+ } else if (queryParams.value.queryType === 'month') {
|
|
|
+ params.month = queryParams.value.month;
|
|
|
+ } else if (queryParams.value.queryType === 'range') {
|
|
|
+ if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
|
|
|
+ params.startDate = queryParams.value.dateRange[0];
|
|
|
+ params.endDate = queryParams.value.dateRange[1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 同时调用统计数据接口和明细数据接口
|
|
|
+
|
|
|
+ Promise.all([
|
|
|
+ getNightInOutStats(params),
|
|
|
+ getNightInOutDetail({ ...params, pageNum: queryParams.value.pageNum, pageSize: queryParams.value.pageSize })
|
|
|
+ ]).then(responses => {
|
|
|
+ // 处理统计数据和趋势数据
|
|
|
+ const statsResponse = responses[0];
|
|
|
+ // 处理明细数据
|
|
|
+ const detailResponse = responses[1];
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if (statsResponse.code === 200 && statsResponse.data) {
|
|
|
+ const data = statsResponse.data;
|
|
|
+ // 处理趋势数据
|
|
|
+ const trendData = data.trendData || [];
|
|
|
+
|
|
|
+ // 直接使用后端返回的统计数据
|
|
|
+ statistics.value = {
|
|
|
+ totalPeople: data.totalPeople || 0,
|
|
|
+ totalEntries: data.totalCount || 0,
|
|
|
+ avgFrequency: Number(data.avgFrequency).toFixed(2) || 0,
|
|
|
+ maxFrequency: data.maxFrequency || 0
|
|
|
+ };
|
|
|
+
|
|
|
+ // 确保在DOM更新后再渲染图表
|
|
|
+ nextTick(() => {
|
|
|
+ renderTrendChart(trendData, queryParams.value.queryType);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 如果统计数据API调用失败,初始化空数据
|
|
|
+ statistics.value = {
|
|
|
+ totalPeople: 0,
|
|
|
+ totalEntries: 0,
|
|
|
+ avgFrequency: 0,
|
|
|
+ maxFrequency: 0
|
|
|
+ };
|
|
|
+
|
|
|
+ // 即使统计数据失败,也尝试渲染空的图表
|
|
|
+ nextTick(() => {
|
|
|
+ renderTrendChart([], queryParams.value.queryType);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (detailResponse.code === 200) {
|
|
|
+ // 处理分页数据 - 使用rows和total字段
|
|
|
+ // 根据API响应结构,数据直接在响应对象中,而非data字段下
|
|
|
+ detailsList.value = detailResponse.rows || [];
|
|
|
+ total.value = detailResponse.total || 0;
|
|
|
+
|
|
|
+
|
|
|
+ } else {
|
|
|
+ // 如果明细数据API调用失败,初始化空数据
|
|
|
+ detailsList.value = [];
|
|
|
+ total.value = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = false;
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('获取夜间出入境数据失败:', error);
|
|
|
+ // 初始化空数据
|
|
|
+ detailsList.value = [];
|
|
|
+ total.value = 0;
|
|
|
+ statistics.value = {
|
|
|
+ totalPeople: 0,
|
|
|
+ totalEntries: 0,
|
|
|
+ avgFrequency: 0,
|
|
|
+ maxFrequency: 0
|
|
|
+ };
|
|
|
+
|
|
|
+ // 确保即使出错也渲染空图表
|
|
|
+ nextTick(() => {
|
|
|
+ renderTrendChart([], queryParams.value.queryType);
|
|
|
+ });
|
|
|
+
|
|
|
+ loading.value = false;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** 处理分页事件 */
|
|
|
+function handlePagination() {
|
|
|
+ // 分页只更新表格数据,不重新获取统计和图表数据
|
|
|
+ loading.value = true;
|
|
|
+
|
|
|
+ // 准备查询参数(仅用于表格数据)
|
|
|
+ const params = {};
|
|
|
+ if (queryParams.value.queryType === 'year') {
|
|
|
+ params.year = queryParams.value.year;
|
|
|
+ } else if (queryParams.value.queryType === 'month') {
|
|
|
+ params.month = queryParams.value.month;
|
|
|
+ } else if (queryParams.value.queryType === 'range') {
|
|
|
+ if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
|
|
|
+ params.startDate = queryParams.value.dateRange[0];
|
|
|
+ params.endDate = queryParams.value.dateRange[1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加分页参数
|
|
|
+ params.pageNum = queryParams.value.pageNum;
|
|
|
+ params.pageSize = queryParams.value.pageSize;
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ getNightInOutDetail(params).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ // 处理分页数据 - 使用rows和total字段
|
|
|
+ // 根据API响应结构,数据直接在响应对象中,而非data字段下
|
|
|
+ detailsList.value = response.rows || [];
|
|
|
+ total.value = response.total || 0;
|
|
|
+ } else {
|
|
|
+ detailsList.value = [];
|
|
|
+ total.value = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = false;
|
|
|
+ }).catch((error) => {
|
|
|
+ console.error('分页API调用失败:', error); // 添加错误调试信息
|
|
|
+ detailsList.value = [];
|
|
|
+ total.value = 0;
|
|
|
+ loading.value = false;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** 根据趋势数据计算统计数据 */
|
|
|
+function calculateStatisticsFromTrendData(trendData, details) {
|
|
|
+ let totalPeople = 0;
|
|
|
+ let totalEntries = details.length || 0;
|
|
|
+ let maxFreq = 0;
|
|
|
+ let avgFrequency = 0;
|
|
|
+
|
|
|
+ // 计算总人数(去重后的证件号码数量)
|
|
|
+ const uniquePersons = new Set();
|
|
|
+ details.forEach(item => {
|
|
|
+ if (item.idNumber) {
|
|
|
+ uniquePersons.add(item.idNumber);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ totalPeople = uniquePersons.size;
|
|
|
+
|
|
|
+ // 计算每个证件号码的出入境次数,找出最高频次
|
|
|
+ const personCountMap = {};
|
|
|
+ details.forEach(item => {
|
|
|
+ if (item.idNumber) {
|
|
|
+ personCountMap[item.idNumber] = (personCountMap[item.idNumber] || 0) + 1;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (Object.keys(personCountMap).length > 0) {
|
|
|
+ maxFreq = Math.max(...Object.values(personCountMap));
|
|
|
+ avgFrequency = totalEntries / totalPeople;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果趋势数据包含总数信息,也可以使用
|
|
|
+ if (trendData && trendData.length > 0) {
|
|
|
+ // 累加趋势数据中的人数
|
|
|
+ const trendTotal = trendData.reduce((sum, item) => sum + (item.personCount || item.count || 0), 0);
|
|
|
+ // 使用较大的值作为总人数(可能趋势数据和明细数据统计方式不同)
|
|
|
+ totalPeople = Math.max(totalPeople, trendTotal);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ totalPeople: totalPeople,
|
|
|
+ totalEntries: totalEntries,
|
|
|
+ avgFrequency: Number(avgFrequency).toFixed(2) || 0,
|
|
|
+ maxFrequency: maxFreq
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/** 渲染趋势图 */
|
|
|
+function renderTrendChart(trendData, queryType) {
|
|
|
+ if (!trendChartContainer.value) return;
|
|
|
+
|
|
|
+ // 如果已有图表实例,先销毁
|
|
|
+ if (trendChartContainer.value.__echarts_instance) {
|
|
|
+ echarts.getInstanceByDom(trendChartContainer.value)?.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ const myChart = echarts.init(trendChartContainer.value);
|
|
|
+
|
|
|
+ // 保存图表实例引用
|
|
|
+ trendChartContainer.value.__echarts_instance = myChart;
|
|
|
+
|
|
|
+ let xAxisData = [];
|
|
|
+ let seriesData = [];
|
|
|
+
|
|
|
+ if (trendData && trendData.length > 0) {
|
|
|
+ // 根据查询类型确定X轴标签
|
|
|
+ if (queryType === 'year') {
|
|
|
+ // 按年查询,显示每月趋势,使用period字段
|
|
|
+ xAxisData = trendData.map(item => {
|
|
|
+ // period格式可能是 "2023-01" 格式
|
|
|
+ if (item.period) {
|
|
|
+ // 检查是否包含日期分隔符,提取月份
|
|
|
+ if (item.period.includes('-')) {
|
|
|
+ const parts = item.period.split('-');
|
|
|
+ if (parts.length === 2) {
|
|
|
+ return parts[1] + '月'; // 如 "01月"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return item.period + '月';
|
|
|
+ } else {
|
|
|
+ return '未知';
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else if (queryType === 'month') {
|
|
|
+ // 按月查询,显示当月每天趋势
|
|
|
+ xAxisData = trendData.map(item => {
|
|
|
+ if (item.period) {
|
|
|
+ // 如果是完整日期格式 "YYYY-MM-DD",取日部分
|
|
|
+ if (item.period.includes('-')) {
|
|
|
+ const parts = item.period.split('-');
|
|
|
+ if (parts.length === 3) {
|
|
|
+ return parts[2] + '日'; // 如 "01日"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return item.period + '日';
|
|
|
+ } else {
|
|
|
+ return '未知';
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 按时段查询,显示按天趋势
|
|
|
+ xAxisData = trendData.map(item => {
|
|
|
+ if (item.period) {
|
|
|
+ // 如果是完整日期格式 "YYYY-MM-DD",显示 MM-DD
|
|
|
+ if (item.period.includes('-')) {
|
|
|
+ const parts = item.period.split('-');
|
|
|
+ if (parts.length === 3) {
|
|
|
+ return parts[1] + '-' + parts[2]; // 如 "05-15"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return item.period;
|
|
|
+ } else {
|
|
|
+ return '未知';
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取人数或次数数据 - 使用inOutCount字段
|
|
|
+ seriesData = trendData.map(item => {
|
|
|
+ // 使用新的字段名inOutCount
|
|
|
+ return item.inOutCount !== undefined ? item.inOutCount : 0;
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 如果没有趋势数据,显示提示信息
|
|
|
+ xAxisData = ['暂无数据'];
|
|
|
+ seriesData = [0];
|
|
|
+ }
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ title: {
|
|
|
+ text: getTrendChartTitle(queryType),
|
|
|
+ left: 'center'
|
|
|
+ },
|
|
|
+ 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}`;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: xAxisData,
|
|
|
+ axisLabel: {
|
|
|
+ rotate: 45,
|
|
|
+ fontSize: 12
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '人数'
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ data: seriesData,
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ itemStyle: {
|
|
|
+ color: '#5470c6'
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: 'rgba(84, 112, 198, 0.4)' },
|
|
|
+ { offset: 1, color: 'rgba(84, 112, 198, 0.1)' }
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ }]
|
|
|
+ };
|
|
|
+
|
|
|
+ myChart.setOption(option);
|
|
|
+}
|
|
|
+
|
|
|
+/** 获取趋势图标题 */
|
|
|
+function getTrendChartTitle(queryType) {
|
|
|
+ if (queryType === 'year') {
|
|
|
+ return `年度夜间出入境人员月度趋势图 (年: ${queryParams.value.year})`;
|
|
|
+ } else if (queryType === 'month') {
|
|
|
+ return `月度夜间出入境人员每日趋势图 (月: ${queryParams.value.month})`;
|
|
|
+ } else {
|
|
|
+ const start = queryParams.value.dateRange ? queryParams.value.dateRange[0] : '';
|
|
|
+ const end = queryParams.value.dateRange ? queryParams.value.dateRange[1] : '';
|
|
|
+ return `指定时段夜间出入境人员趋势图 (${start} 至 ${end})`;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 搜索按钮操作 */
|
|
|
+function handleQuery() {
|
|
|
+
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+}
|
|
|
+
|
|
|
+/** 重置按钮操作 */
|
|
|
+function resetQuery() {
|
|
|
+ proxy.resetForm("queryRef");
|
|
|
+ queryParams.value.queryType = 'year';
|
|
|
+ queryParams.value.year = new Date().getFullYear().toString();
|
|
|
+ queryParams.value.month = null;
|
|
|
+ queryParams.value.dateRange = null;
|
|
|
+ handleQuery();
|
|
|
+}
|
|
|
+
|
|
|
+/** 处理表格行点击事件 */
|
|
|
+function handleRowClick(row) {
|
|
|
+ // 显示详细信息
|
|
|
+ detailForm.value = { ...row };
|
|
|
+ detailTitle.value = `${row.fullName || '未知'} 详细信息`;
|
|
|
+ recordDetailOpen.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+// 在组件挂载后,根据默认查询类型执行查询
|
|
|
+onMounted(() => {
|
|
|
+
|
|
|
+ // 确保在DOM更新后再执行查询
|
|
|
+ nextTick(() => {
|
|
|
+ handleQuery();
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// 在组件卸载时清理图表实例
|
|
|
+onUnmounted(() => {
|
|
|
+ if (trendChartContainer.value && trendChartContainer.value.__echarts_instance) {
|
|
|
+ echarts.getInstanceByDom(trendChartContainer.value)?.dispose();
|
|
|
+ trendChartContainer.value.__echarts_instance = null;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.app-container {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ height: 80px;
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ .stat-item {
|
|
|
+ .stat-number {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #409eff;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.total-people { .stat-number { color: #409eff; } }
|
|
|
+ &.total-entries { .stat-number { color: #67c21a; } }
|
|
|
+ &.avg-frequency { .stat-number { color: #e6a23c; } }
|
|
|
+ &.max-frequency { .stat-number { color: #f56c6c; } }
|
|
|
+}
|
|
|
+
|
|
|
+.mb20 {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.table_caption {
|
|
|
+ font-weight: bold;
|
|
|
+ text-align: left;
|
|
|
+ padding-left: 10px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+}
|
|
|
+</style>
|