Browse Source

搞定境外滞留

wukai 4 months ago
parent
commit
d6c718069f
3 changed files with 543 additions and 7 deletions
  1. 18 0
      src/api/biz/anal.js
  2. 2 7
      src/views/biz/anal/holiday.vue
  3. 523 0
      src/views/biz/anal/overstay.vue

+ 18 - 0
src/api/biz/anal.js

@@ -80,3 +80,21 @@ export function getRapid(params) {
         params: params
     })
 }
+
+// 查询境外滞留明细数据
+export function getOverseasStayDetails(params) {
+    return request({
+        url: '/biz/anal/overseas/details',
+        method: 'get',
+        params: params
+    })
+}
+
+// 查询境外滞留统计数据
+export function getOverseasStayStats(params) {
+    return request({
+        url: '/biz/anal/overseas/stats',
+        method: 'get',
+        params: params
+    })
+}

+ 2 - 7
src/views/biz/anal/holiday.vue

@@ -47,7 +47,7 @@
         </el-row>
       </div>
     </div>
-    
+
     <!-- 没有节假日数据时的提示 -->
     <div v-else class="no-data-container">
       <el-empty description="暂无节假日数据" :image-size="100"></el-empty>
@@ -85,11 +85,6 @@
         </el-table-column>
         <el-table-column label="前往地/出发地" align="center" prop="destinationName"/>
         <el-table-column label="节假日" align="center" prop="holidayName"/>
-        <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>
 
@@ -434,7 +429,7 @@ function getStats() {
       // 如果没有节假日数据,清空表格并弹出提示
       list.value = [];
       total.value = 0;
-      
+
       // 弹出模态框提示信息,阻止其他操作
       ElMessageBox.alert(`${queryParams.value.year}年度节假日未配置,请先到参数配置/节假日配置中配置 ${queryParams.value.year} 年度的节假日。`, '提示', {
         type: 'warning',

+ 523 - 0
src/views/biz/anal/overstay.vue

@@ -0,0 +1,523 @@
+,<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
+      <el-form-item 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 label="最小天数" prop="minDays">
+        <el-input-number v-model="queryParams.minDays" :min="15" :step="5" placeholder="最小天数" @change="handleQuery" />
+      </el-form-item>
+    </el-form>
+
+    <!-- 图表和统计卡片并排布局 -->
+    <el-row :gutter="10" style="margin-bottom: 20px;">
+      <div style="height: 400px; width: 49%; margin-right: 1%;">
+        <div class="table_caption" style="height: 30px; line-height: 30px;">境外滞留天数分布</div>
+        <div style="height: 370px; width: 100%; border: 1px solid #ededed; background: #fff;" ref="pieChartContainer">
+          <div id="pieChart" style="height: 100%; width: 100%;"></div>
+        </div>
+      </div>
+      <div style="width: 49%; margin-left: 1;">
+        <div class="table_caption" style="height: 30px; line-height: 30px;">统计信息</div>
+        <div style="height: 370px; width: 100%; border: 1px solid #ededed; background: #fff; padding: 20px;">
+          <!-- 统计数据卡片,3行均匀分布 -->
+          <el-row :gutter="20" class="mb20" style="height: 100%;">
+            <el-col :span="24" style="margin-bottom: 10px; height: calc(33.33% - 7px);">
+              <el-card class="stat-card total-people" style="height: 100%;">
+                <div class="stat-item" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">
+                  <div class="stat-number">{{ statistics.totalPeople }}</div>
+                  <div class="stat-label">境外滞留总人数</div>
+                </div>
+              </el-card>
+            </el-col>
+            <el-col :span="24" style="margin-bottom: 10px; height: calc(33.33% - 7px);">
+              <el-card class="stat-card avg-frequency" style="height: 100%;">
+                <div class="stat-item" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">
+                  <div class="stat-number">{{ statistics.avgFrequency }}</div>
+                  <div class="stat-label">平均滞留天数</div>
+                </div>
+              </el-card>
+            </el-col>
+            <el-col :span="24" style="height: calc(33.33% - 7px);">
+              <el-card class="stat-card max-frequency" style="height: 100%;">
+                <div class="stat-item" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">
+                  <div class="stat-number">{{ statistics.maxFrequency }}</div>
+                  <div class="stat-label">最长滞留天数</div>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+        </div>
+      </div>
+    </el-row>
+
+    <el-table v-loading="loading" :data="list" 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" />
+      <el-table-column label="证件号码" prop="idNumber" align="center" />
+      <el-table-column label="滞留天数" prop="absenceDays" align="center" />
+      <el-table-column label="出境时间" prop="departureTime" align="center" width="180" />
+      <el-table-column label="入境时间" prop="returnTime" align="center" width="180" />
+    </el-table>
+
+    <!-- 使用公共组件 -->
+    <RecordListDetailDialog
+      :visible="recordListOpen"
+      :person-info="selectedPersonInfo"
+      :date-range="getDateRangeForQuery()"
+      @update:visible="recordListOpen = $event"
+      @close="recordListOpen = false"
+    />
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      v-model:page="queryParams.pageNum"
+      v-model:limit="queryParams.pageSize"
+      @pagination="handlePagination"
+    />
+  </div>
+</template>
+
+<script setup name="Overstay">
+import * as echarts from 'echarts'
+import { listHighFrequencyInOut, getInOutStats, getRecords } from "@/api/biz/anal";
+import { getOverseasStayDetails, getOverseasStayStats } from "@/api/biz/anal";
+import RecordListDetailDialog from '@/components/RecordListDetailDialog.vue';
+
+const { proxy } = getCurrentInstance();
+
+const list = ref([]);
+const loading = ref(false);
+
+const total = ref(0);
+
+// 添加饼图容器引用
+const pieChartContainer = ref(null);
+
+const statistics = ref({
+  totalPeople: 0,
+  avgFrequency: 0,
+  maxFrequency: 0,
+  minFrequency: 0,
+  totalEntries: 0
+});
+
+// 出入境记录明细相关
+const recordDetailOpen = ref(false);
+const recordDetailLoading = ref(false);
+const recordDetailList = ref([]);
+const detailTitle = ref("详细信息");
+const detailForm = ref({});
+
+// 出入境记录列表相关
+const recordListOpen = ref(false);
+const recordListLoading = ref(false);
+const recordListTitle = ref("出入境记录列表");
+const recordList = ref([]);
+const selectedPersonInfo = ref({});
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    year: new Date().getFullYear().toString(),
+    minDays: 15  // 默认最小天数为15天
+  }
+});
+
+const { queryParams } = toRefs(data);
+
+/** 查询境外滞留列表 */
+function getList() {
+  loading.value = true;
+
+  // 准备查询参数
+  const params = {};
+  params.year = queryParams.value.year;
+  // 添加最小天数参数
+  params.minDays = queryParams.value.minDays;
+
+  // 同时调用列表接口和统计接口
+  Promise.all([
+    getOverseasStayDetails(params),
+    getOverseasStayStats(params)
+  ]).then(responses => {
+    // 处理列表数据
+    const listResponse = responses[0];
+    list.value = listResponse.rows || [];
+    total.value = listResponse.total || 0;
+
+    // 处理统计信息
+    const statsResponse = responses[1];
+    if (statsResponse.code === 200 && statsResponse.data) {
+      const stats = statsResponse.data;
+      statistics.value = {
+        totalPeople: stats.totalPeople || 0,
+        avgFrequency: Number(stats.avgStayDays).toFixed(2) || 0,
+        maxFrequency: stats.maxStayDays || 0,
+        minFrequency: stats.minStayDays || 0,
+        totalEntries: stats.totalCount || 0
+      };
+
+      // 渲染分布图
+      renderPieChart(stats.stayDaysDistribution || {});
+    } else {
+      // 如果新接口调用失败,使用原有方式计算统计数据
+      calculateStatistics();
+
+      // 渲染分布图(使用前端计算方式)
+      renderPieChart({});
+    }
+
+    loading.value = false;
+  }).catch(() => {
+    loading.value = false;
+  });
+}
+
+/** 处理分页事件 */
+function handlePagination() {
+  // 分页只更新表格数据,不重新获取统计和图表数据
+  loading.value = true;
+
+  // 准备查询参数(仅用于表格数据)
+  const params = {};
+  params.year = queryParams.value.year;
+
+  // 添加分页参数
+  params.pageNum = queryParams.value.pageNum;
+  params.pageSize = queryParams.value.pageSize;
+  params.minDays = queryParams.value.minDays;
+
+  getOverseasStayDetails(params).then(response => {
+    list.value = response.rows || [];
+    total.value = response.total || 0;
+
+    loading.value = false;
+  }).catch(() => {
+    loading.value = false;
+  });
+}
+
+/** 计算统计数据 */
+function calculateStatistics() {
+  if (!list.value || list.value.length === 0) {
+    statistics.value = {
+      totalPeople: 0,
+      avgFrequency: 0,
+      maxFrequency: 0,
+      minFrequency: 0,
+      totalEntries: 0
+    };
+    return;
+  }
+
+  let totalStayDays = 0;
+  let maxStayDays = 0;
+  let minStayDays = Infinity;
+
+  list.value.forEach(item => {
+    const stayDays = item.absenceDays || 0;
+    totalStayDays += stayDays;
+    if (stayDays > maxStayDays) {
+      maxStayDays = stayDays;
+    }
+    if (stayDays < minStayDays) {
+      minStayDays = stayDays;
+    }
+  });
+
+  statistics.value = {
+    totalPeople: list.value.length,
+    avgFrequency: list.value.length > 0 ? (totalStayDays / list.value.length).toFixed(2) : 0,
+    maxFrequency: maxStayDays,
+    minFrequency: minStayDays === Infinity ? 0 : minStayDays,
+    totalEntries: totalStayDays
+  };
+}
+
+/** 渲染饼图 */
+function renderPieChart(stayDaysDistribution = {}) {
+  if (!pieChartContainer.value) return;
+
+  // 销毁现有图表实例(如果存在)
+  const chartDom = pieChartContainer.value.querySelector('#pieChart');
+  if (chartDom && chartDom.__echarts_instance__) {
+    chartDom.__echarts_instance__.dispose();
+  }
+
+  const myChart = echarts.init(chartDom);
+
+  // 将分布数据转换为饼图所需格式
+  let distributionData = [];
+  if (Object.keys(stayDaysDistribution).length > 0) {
+    // 定义合理的排序顺序(按照天数递增)
+    const rangeOrder = ['0-15天', '15-30天', '30-60天', '60-90天', '90-120天', '120天以上'];
+    
+    // 标记是否已经遇到非零值
+    let foundNonZero = false;
+    
+    // 按照预定顺序处理数据
+    for (const range of rangeOrder) {
+      if (stayDaysDistribution.hasOwnProperty(range)) {
+        if (stayDaysDistribution[range] > 0) {
+          foundNonZero = true;
+          let color;
+          if (range.includes('0-15')) color = '#5470c6';
+          else if (range.includes('15-30')) color = '#91cc75';
+          else if (range.includes('30-60')) color = '#fac858';
+          else if (range.includes('60-90')) color = '#ee6666';
+          else if (range.includes('90')) color = '#73c0de'; // 90天以上
+          else color = '#9da4b0'; // 默认颜色
+
+          distributionData.push({
+            value: stayDaysDistribution[range],
+            name: range,
+            itemStyle: { color: color }
+          });
+        } else if (foundNonZero) {
+          // 如果已经遇到非零值,但当前值为0,则仍保留(中间的0值)
+          let color;
+          if (range.includes('0-15')) color = '#5470c6';
+          else if (range.includes('15-30')) color = '#91cc75';
+          else if (range.includes('30-60')) color = '#fac858';
+          else if (range.includes('60-90')) color = '#ee6666';
+          else if (range.includes('90')) color = '#73c0de'; // 90天以上
+          else color = '#9da4b0'; // 默认颜色
+
+          distributionData.push({
+            value: stayDaysDistribution[range],
+            name: range,
+            itemStyle: { color: color }
+          });
+        }
+        // 如果是前面的0值,则跳过不添加
+      }
+    }
+    
+    // 处理不在预定义区间中的其他区间
+    for (const [range, count] of Object.entries(stayDaysDistribution)) {
+      if (!rangeOrder.includes(range)) {
+        // 根据区间范围选择颜色
+        let color;
+        if (range.includes('0-15')) color = '#5470c6';
+        else if (range.includes('15-30')) color = '#91cc75';
+        else if (range.includes('30-60')) color = '#fac858';
+        else if (range.includes('60-90')) color = '#ee6666';
+        else if (range.includes('90')) color = '#73c0de'; // 90天以上
+        else color = '#9da4b0'; // 默认颜色
+
+        distributionData.push({
+          value: count,
+          name: range,
+          itemStyle: { color: color }
+        });
+      }
+    }
+  } else {
+    // 如果没有分布数据,使用默认值或从前端数据计算
+    // 这里可以根据实际数据进行计算,但为了简单起见,我们显示一个默认值
+    distributionData = [
+      { value: statistics.value.totalPeople, name: '滞留分布', itemStyle: { color: '#5470c6' } }
+    ];
+  }
+
+  const option = {
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}: {c}人 ({d}%)'
+    },
+    legend: {
+      orient: 'vertical',
+      left: 'right'
+    },
+    series: [{
+      name: '滞留天数分布',
+      type: 'pie',
+      radius: ['40%', '70%'], // 设置为环形图
+      avoidLabelOverlap: false,
+      itemStyle: {
+        borderRadius: 10,
+        borderColor: '#fff',
+        borderWidth: 2
+      },
+      label: {
+        show: true,
+        formatter: '{b}: {c}人'
+      },
+      emphasis: {
+        label: {
+          show: true,
+          fontSize: '16',
+          fontWeight: 'bold'
+        }
+      },
+      data: distributionData
+    }]
+  };
+
+  myChart.setOption(option);
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  proxy.resetForm("queryRef");
+  queryParams.value.year = new Date().getFullYear().toString();
+  queryParams.value.minDays = 15;
+  handleQuery();
+}
+
+/** 处理表格行点击事件 */
+function handleRowClick(row) {
+  // 设置选中人员信息
+  selectedPersonInfo.value = { ...row };
+  // 打开记录列表对话框
+  recordListOpen.value = true;
+}
+
+/** 获取用于查询的日期范围 */
+function getDateRangeForQuery() {
+  // 按年查询,返回该年的开始和结束日期
+  const year = queryParams.value.year;
+  return [`${year}-01-01`, `${year}-12-31`];
+}
+
+// 页面加载时获取数据
+onMounted(() => {
+  // 初始化时直接查询,使用reactive中设置的默认值
+  handleQuery();
+});
+</script>
+
+<style scoped>
+/* 统计卡片样式 */
+.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: 100%;
+}
+
+.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%);
+}
+
+.stat-card.total-entries {
+  background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
+}
+
+.stat-card.avg-frequency {
+  background: linear-gradient(135deg, #fffbe6 0%, #fff5a0 100%);
+}
+
+.stat-card.max-frequency {
+  background: linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%);
+}
+
+.stat-item {
+  padding: 15px 10px;
+}
+
+.stat-number {
+  font-size: 22px;
+  font-weight: bold;
+  margin-bottom: 5px;
+}
+
+/* 不同统计数字的颜色 */
+.stat-number {
+  color: #1890ff;
+}
+
+.stat-card:nth-child(2) .stat-number {
+  color: #52c41a;
+}
+
+.stat-card:nth-child(3) .stat-number {
+  color: #faad14;
+}
+
+.stat-card:nth-child(4) .stat-number {
+  color: #f5222d;
+}
+
+.stat-label {
+  font-size: 13px;
+  color: #606266;
+  font-weight: 500;
+}
+
+/* 新增统计信息项样式 */
+.stat-info-item {
+  margin: 10px 0;
+  padding: 8px 0;
+  border-bottom: 1px solid #eee;
+}
+
+.stat-info-label {
+  font-size: 14px;
+  color: #606266;
+  margin-bottom: 5px;
+}
+
+.stat-info-value {
+  font-size: 16px;
+  font-weight: bold;
+  color: #303133;
+}
+
+.chart-container {
+  margin-bottom: 20px;
+}
+
+#chart, #pieChart {
+  width: 100%;
+}
+
+.table_caption {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  text-align: center;
+  margin-bottom: 10px;
+}
+
+.app-container {
+  padding: 15px;
+}
+
+.el-row {
+  margin-bottom: 15px;
+}
+
+.el-table {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.el-card {
+  border-radius: 8px;
+  border: none;
+}
+</style>