night.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. <template>
  2. <div class="app-container">
  3. <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
  4. <el-form-item label="查询方式" prop="queryType">
  5. <el-select v-model="queryParams.queryType" placeholder="请选择查询方式" clearable style="width: 150px;" @change="handleQuery">
  6. <el-option label="按年查询" value="year" />
  7. <el-option label="按月查询" value="month" />
  8. <el-option label="按时段查询" value="range" />
  9. </el-select>
  10. </el-form-item>
  11. <el-form-item v-if="queryParams.queryType === 'year'" label="年份" prop="year">
  12. <el-date-picker
  13. v-model="queryParams.year"
  14. type="year"
  15. value-format="YYYY"
  16. placeholder="请选择年份"
  17. @change="handleQuery">
  18. </el-date-picker>
  19. </el-form-item>
  20. <el-form-item v-if="queryParams.queryType === 'month'" label="月份" prop="month">
  21. <el-date-picker
  22. v-model="queryParams.month"
  23. type="month"
  24. value-format="YYYY-MM"
  25. placeholder="请选择月份"
  26. @change="handleQuery">
  27. </el-date-picker>
  28. </el-form-item>
  29. <el-form-item v-if="queryParams.queryType === 'range'" label="时段" prop="dateRange">
  30. <el-date-picker
  31. v-model="queryParams.dateRange"
  32. type="daterange"
  33. value-format="YYYY-MM-DD"
  34. range-separator="至"
  35. start-placeholder="开始日期"
  36. end-placeholder="结束日期"
  37. @change="handleQuery">
  38. </el-date-picker>
  39. </el-form-item>
  40. </el-form>
  41. <!-- 统计数据卡片 -->
  42. <el-row :gutter="20" class="mb20">
  43. <el-col :span="6">
  44. <el-card class="stat-card total-people">
  45. <div class="stat-item">
  46. <div class="stat-number">{{ statistics.totalPeople }}</div>
  47. <div class="stat-label">夜间出入境总人数</div>
  48. </div>
  49. </el-card>
  50. </el-col>
  51. <el-col :span="6">
  52. <el-card class="stat-card total-entries">
  53. <div class="stat-item">
  54. <div class="stat-number">{{ statistics.totalEntries }}</div>
  55. <div class="stat-label">夜间出入境总次数</div>
  56. </div>
  57. </el-card>
  58. </el-col>
  59. <el-col :span="6">
  60. <el-card class="stat-card avg-frequency">
  61. <div class="stat-item">
  62. <div class="stat-number">{{ statistics.avgFrequency }}</div>
  63. <div class="stat-label">平均频次</div>
  64. </div>
  65. </el-card>
  66. </el-col>
  67. <el-col :span="6">
  68. <el-card class="stat-card max-frequency">
  69. <div class="stat-item">
  70. <div class="stat-number">{{ statistics.maxFrequency }}</div>
  71. <div class="stat-label">最高频次</div>
  72. </div>
  73. </el-card>
  74. </el-col>
  75. </el-row>
  76. <!-- 趋势图 -->
  77. <el-row :gutter="10" style="margin-bottom: 20px;">
  78. <div style="height: 500px; width: 100%;">
  79. <div class="table_caption" style="height: 30px; line-height: 30px;">夜间出入境人员数量趋势图</div>
  80. <div style="height: 470px; width: 100%; border: 1px solid #ededed; background: #fff;" ref="trendChartContainer">
  81. <div id="trendChart" style="height: 100%; width: 100%;"></div>
  82. </div>
  83. </div>
  84. </el-row>
  85. <!-- 明细数据表格 -->
  86. <el-table v-loading="loading" :data="detailsList" style="margin-top: 20px;" @row-click="handleRowClick">
  87. <el-table-column label="序号" type="index" width="50" align="center" />
  88. <el-table-column label="姓名" prop="fullName" align="center" width="300"/>
  89. <el-table-column label="性别" align="center" prop="genderCn"/>
  90. <el-table-column label="出生日期" align="center" prop="birthDate" width="100">
  91. <template #default="scope">
  92. <span>{{ parseTime(scope.row.birthDate, '{y}-{m}-{d}') }}</span>
  93. </template>
  94. </el-table-column>
  95. <el-table-column label="国家/地区" align="center" prop="countryName"/>
  96. <el-table-column label="民族" align="center" prop="ethnicityName"/>
  97. <el-table-column label="出入标识" align="center" prop="inOutFlag"/>
  98. <el-table-column label="出入时间" align="center" prop="inOutTime" width="180">
  99. <template #default="scope">
  100. <span>{{ parseTime(scope.row.inOutTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  101. </template>
  102. </el-table-column>
  103. <el-table-column label="出入口岸" align="center" prop="portCode">
  104. <template #default="scope">
  105. <span>{{ scope.row.portCode}}-{{scope.row.portName}}</span>
  106. </template>
  107. </el-table-column>
  108. <el-table-column label="前往地/出发地" align="center" prop="destinationName"/>
  109. <el-table-column label="导入时间" align="center" prop="createTime" width="150">
  110. <template #default="scope">
  111. <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  112. </template>
  113. </el-table-column>
  114. </el-table>
  115. <div style="margin-top: 10px;">
  116. <div v-if="detailsList.length === 0 && !loading" style="margin-top: 10px; color: #f56c6c;">
  117. <el-icon><Warning /></el-icon> 暂无数据,请检查API返回结果
  118. </div>
  119. </div>
  120. <!-- 出入境记录明细对话框 -->
  121. <el-dialog :title="detailTitle" v-model="recordDetailOpen" width="900px" append-to-body>
  122. <el-form :model="detailForm" label-width="120px" disabled>
  123. <!-- 个人信息 -->
  124. <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">个人信息</div>
  125. <el-row>
  126. <el-col :span="8">
  127. <el-form-item label="人员类别:">
  128. <span>{{ detailForm.personnelCategoryName }}</span>
  129. </el-form-item>
  130. </el-col>
  131. <el-col :span="8">
  132. <el-form-item label="姓名:">
  133. <span>{{ detailForm.fullName }}</span>
  134. </el-form-item>
  135. </el-col>
  136. <el-col :span="8">
  137. <el-form-item label="性别:">
  138. <span>{{ detailForm.genderCn }}</span>
  139. </el-form-item>
  140. </el-col>
  141. <el-col :span="8">
  142. <el-form-item label="出生日期:">
  143. <span>{{ parseTime(detailForm.birthDate, '{y}-{m}-{d}') }}</span>
  144. </el-form-item>
  145. </el-col>
  146. <el-col :span="8">
  147. <el-form-item label="国家/地区:">
  148. <span>{{ detailForm.countryCode }}-{{ detailForm.countryName }}</span>
  149. </el-form-item>
  150. </el-col>
  151. <el-col :span="8">
  152. <el-form-item label="民族:">
  153. <span>{{ detailForm.ethnicityName }}</span>
  154. </el-form-item>
  155. </el-col>
  156. </el-row>
  157. <!-- 证件信息 -->
  158. <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">证件信息</div>
  159. <el-row>
  160. <el-col :span="8">
  161. <el-form-item label="证件类别:">
  162. <span>{{ detailForm.idTypeName }}</span>
  163. </el-form-item>
  164. </el-col>
  165. <el-col :span="8">
  166. <el-form-item label="证件号码:">
  167. <span>{{ detailForm.idNumber }}</span>
  168. </el-form-item>
  169. </el-col>
  170. <el-col :span="8">
  171. <el-form-item label="签证类型:">
  172. <span>{{ detailForm.visaTypeName }}</span>
  173. </el-form-item>
  174. </el-col>
  175. <el-col :span="8">
  176. <el-form-item label="签证号码:">
  177. <span>{{ detailForm.visaNumber }}</span>
  178. </el-form-item>
  179. </el-col>
  180. <el-col :span="8">
  181. <el-form-item label="停留期:">
  182. <span>{{ detailForm.stayDuration }}</span>
  183. </el-form-item>
  184. </el-col>
  185. </el-row>
  186. <!-- 出入境信息 -->
  187. <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">出入境信息</div>
  188. <el-row>
  189. <el-col :span="8">
  190. <el-form-item label="出入标识:">
  191. <span>{{ detailForm.inOutFlag }}</span>
  192. </el-form-item>
  193. </el-col>
  194. <el-col :span="8">
  195. <el-form-item label="出入时间:">
  196. <span>{{ parseTime(detailForm.inOutTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  197. </el-form-item>
  198. </el-col>
  199. <el-col :span="8">
  200. <el-form-item label="出入口岸:">
  201. <span>{{ detailForm.portCode }}-{{ detailForm.portName }}</span>
  202. </el-form-item>
  203. </el-col>
  204. <el-col :span="8">
  205. <el-form-item label="交通方式:">
  206. <span>{{ detailForm.transportMode }}</span>
  207. </el-form-item>
  208. </el-col>
  209. <el-col :span="8">
  210. <el-form-item label="交通工具:">
  211. <span>{{ detailForm.transportVehicle }}</span>
  212. </el-form-item>
  213. </el-col>
  214. <el-col :span="8">
  215. <el-form-item label="前往地/出发地:">
  216. <span>{{ detailForm.destinationCode }}-{{ detailForm.destinationName }}</span>
  217. </el-form-item>
  218. </el-col>
  219. <el-col :span="8">
  220. <el-form-item label="出入境事由:">
  221. <span>{{ detailForm.reasonName }}</span>
  222. </el-form-item>
  223. </el-col>
  224. <el-col :span="8">
  225. <el-form-item label="发证机关:">
  226. <span>{{ detailForm.issuingAuthorityCode }}-{{ detailForm.issuingAuthorityName }}</span>
  227. </el-form-item>
  228. </el-col>
  229. </el-row>
  230. <!-- 其他信息 -->
  231. <div style="font-weight: bold; margin: 10px 0 15px 0; border-left: 4px solid #409eff; padding-left: 8px; color: #409eff;">其他信息</div>
  232. <el-row>
  233. <el-col :span="8">
  234. <el-form-item label="自助通道标记:">
  235. <span>{{ detailForm.selfServiceFlag }}</span>
  236. </el-form-item>
  237. </el-col>
  238. <el-col :span="8">
  239. <el-form-item label="后台补录标记:">
  240. <span>{{ detailForm.backfillFlag }}</span>
  241. </el-form-item>
  242. </el-col>
  243. <el-col :span="8">
  244. <el-form-item label="创建时间:">
  245. <span>{{ parseTime(detailForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  246. </el-form-item>
  247. </el-col>
  248. <el-col :span="8">
  249. <el-form-item label="疑难字说明:">
  250. <span>{{ detailForm.remark }}</span>
  251. </el-form-item>
  252. </el-col>
  253. </el-row>
  254. </el-form>
  255. <template #footer>
  256. <div class="dialog-footer">
  257. <el-button @click="recordDetailOpen = false">关 闭</el-button>
  258. </div>
  259. </template>
  260. </el-dialog>
  261. <Pagination
  262. v-show="total>0"
  263. :total="total"
  264. v-model:page="queryParams.pageNum"
  265. v-model:limit="queryParams.pageSize"
  266. @pagination="handlePagination"
  267. />
  268. <div v-if="total === 0 && !loading" style="text-align: center; padding: 20px; color: #909399;">
  269. 暂无数据
  270. </div>
  271. </div>
  272. </template>
  273. <script setup name="Night">
  274. import * as echarts from 'echarts'
  275. import { getNightInOutStats, getNightInOutDetail } from "@/api/biz/anal";
  276. import { getCurrentInstance, nextTick, onMounted, onUnmounted, reactive, ref, toRefs, watch } from 'vue';
  277. import Pagination from "@/components/Pagination/index.vue";
  278. import { parseTime, resetForm } from "@/utils/ruoyi";
  279. import { Pointer, Warning } from "@element-plus/icons-vue";
  280. const { proxy } = getCurrentInstance();
  281. const detailsList = ref([]);
  282. const loading = ref(false);
  283. const total = ref(0);
  284. const trendChartContainer = ref(null);
  285. const statistics = ref({
  286. totalPeople: 0,
  287. totalEntries: 0,
  288. avgFrequency: 0,
  289. maxFrequency: 0
  290. });
  291. // 出入境记录明细相关
  292. const recordDetailOpen = ref(false);
  293. const recordDetailLoading = ref(false);
  294. const detailTitle = ref("详细信息");
  295. const detailForm = ref({});
  296. const data = reactive({
  297. queryParams: {
  298. pageNum: 1,
  299. pageSize: 10,
  300. queryType: 'year', // 默认查询类型为年
  301. year: new Date().getFullYear().toString(), // 默认年份
  302. month: null, // 初始不设置月份
  303. dateRange: null // 初始不设置日期范围
  304. }
  305. });
  306. const { queryParams } = toRefs(data);
  307. // 监听查询方式变化,设置默认值
  308. watch(() => queryParams.value.queryType, (newVal) => {
  309. if (newVal === 'month' && (!queryParams.value.month || queryParams.value.month === null)) {
  310. // 当选择按月查询时,默认选中当月
  311. const now = new Date();
  312. queryParams.value.month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
  313. // 立即查询
  314. handleQuery();
  315. } else if (newVal === 'range' && (!queryParams.value.dateRange || queryParams.value.dateRange.length === 0)) {
  316. // 当选择按时段查询时,默认最近7天
  317. const now = new Date();
  318. const startDate = new Date();
  319. startDate.setDate(now.getDate() - 6); // 最近7天
  320. const startStr = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`;
  321. const endStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
  322. queryParams.value.dateRange = [startStr, endStr];
  323. // 立即查询
  324. handleQuery();
  325. } else if (newVal === 'year') {
  326. // 当选择按年查询时,也立即查询
  327. handleQuery();
  328. }
  329. }, { immediate: true });
  330. /** 查询夜间出入境数据 */
  331. function getList() {
  332. loading.value = true;
  333. // 准备查询参数
  334. const params = {};
  335. if (queryParams.value.queryType === 'year') {
  336. params.year = queryParams.value.year;
  337. } else if (queryParams.value.queryType === 'month') {
  338. params.month = queryParams.value.month;
  339. } else if (queryParams.value.queryType === 'range') {
  340. if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
  341. params.startDate = queryParams.value.dateRange[0];
  342. params.endDate = queryParams.value.dateRange[1];
  343. }
  344. }
  345. // 同时调用统计数据接口和明细数据接口
  346. Promise.all([
  347. getNightInOutStats(params),
  348. getNightInOutDetail({ ...params, pageNum: queryParams.value.pageNum, pageSize: queryParams.value.pageSize })
  349. ]).then(responses => {
  350. // 处理统计数据和趋势数据
  351. const statsResponse = responses[0];
  352. // 处理明细数据
  353. const detailResponse = responses[1];
  354. if (statsResponse.code === 200 && statsResponse.data) {
  355. const data = statsResponse.data;
  356. // 处理趋势数据
  357. const trendData = data.trendData || [];
  358. // 直接使用后端返回的统计数据
  359. statistics.value = {
  360. totalPeople: data.totalPeople || 0,
  361. totalEntries: data.totalCount || 0,
  362. avgFrequency: Number(data.avgFrequency).toFixed(2) || 0,
  363. maxFrequency: data.maxFrequency || 0
  364. };
  365. // 确保在DOM更新后再渲染图表
  366. nextTick(() => {
  367. renderTrendChart(trendData, queryParams.value.queryType);
  368. });
  369. } else {
  370. // 如果统计数据API调用失败,初始化空数据
  371. statistics.value = {
  372. totalPeople: 0,
  373. totalEntries: 0,
  374. avgFrequency: 0,
  375. maxFrequency: 0
  376. };
  377. // 即使统计数据失败,也尝试渲染空的图表
  378. nextTick(() => {
  379. renderTrendChart([], queryParams.value.queryType);
  380. });
  381. }
  382. if (detailResponse.code === 200) {
  383. // 处理分页数据 - 使用rows和total字段
  384. // 根据API响应结构,数据直接在响应对象中,而非data字段下
  385. detailsList.value = detailResponse.rows || [];
  386. total.value = detailResponse.total || 0;
  387. } else {
  388. // 如果明细数据API调用失败,初始化空数据
  389. detailsList.value = [];
  390. total.value = 0;
  391. }
  392. loading.value = false;
  393. }).catch(error => {
  394. console.error('获取夜间出入境数据失败:', error);
  395. // 初始化空数据
  396. detailsList.value = [];
  397. total.value = 0;
  398. statistics.value = {
  399. totalPeople: 0,
  400. totalEntries: 0,
  401. avgFrequency: 0,
  402. maxFrequency: 0
  403. };
  404. // 确保即使出错也渲染空图表
  405. nextTick(() => {
  406. renderTrendChart([], queryParams.value.queryType);
  407. });
  408. loading.value = false;
  409. });
  410. }
  411. /** 处理分页事件 */
  412. function handlePagination() {
  413. // 分页只更新表格数据,不重新获取统计和图表数据
  414. loading.value = true;
  415. // 准备查询参数(仅用于表格数据)
  416. const params = {};
  417. if (queryParams.value.queryType === 'year') {
  418. params.year = queryParams.value.year;
  419. } else if (queryParams.value.queryType === 'month') {
  420. params.month = queryParams.value.month;
  421. } else if (queryParams.value.queryType === 'range') {
  422. if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
  423. params.startDate = queryParams.value.dateRange[0];
  424. params.endDate = queryParams.value.dateRange[1];
  425. }
  426. }
  427. // 添加分页参数
  428. params.pageNum = queryParams.value.pageNum;
  429. params.pageSize = queryParams.value.pageSize;
  430. getNightInOutDetail(params).then(response => {
  431. if (response.code === 200) {
  432. // 处理分页数据 - 使用rows和total字段
  433. // 根据API响应结构,数据直接在响应对象中,而非data字段下
  434. detailsList.value = response.rows || [];
  435. total.value = response.total || 0;
  436. } else {
  437. detailsList.value = [];
  438. total.value = 0;
  439. }
  440. loading.value = false;
  441. }).catch((error) => {
  442. console.error('分页API调用失败:', error); // 添加错误调试信息
  443. detailsList.value = [];
  444. total.value = 0;
  445. loading.value = false;
  446. });
  447. }
  448. /** 根据趋势数据计算统计数据 */
  449. function calculateStatisticsFromTrendData(trendData, details) {
  450. let totalPeople = 0;
  451. let totalEntries = details.length || 0;
  452. let maxFreq = 0;
  453. let avgFrequency = 0;
  454. // 计算总人数(去重后的证件号码数量)
  455. const uniquePersons = new Set();
  456. details.forEach(item => {
  457. if (item.idNumber) {
  458. uniquePersons.add(item.idNumber);
  459. }
  460. });
  461. totalPeople = uniquePersons.size;
  462. // 计算每个证件号码的出入境次数,找出最高频次
  463. const personCountMap = {};
  464. details.forEach(item => {
  465. if (item.idNumber) {
  466. personCountMap[item.idNumber] = (personCountMap[item.idNumber] || 0) + 1;
  467. }
  468. });
  469. if (Object.keys(personCountMap).length > 0) {
  470. maxFreq = Math.max(...Object.values(personCountMap));
  471. avgFrequency = totalEntries / totalPeople;
  472. }
  473. // 如果趋势数据包含总数信息,也可以使用
  474. if (trendData && trendData.length > 0) {
  475. // 累加趋势数据中的人数
  476. const trendTotal = trendData.reduce((sum, item) => sum + (item.personCount || item.count || 0), 0);
  477. // 使用较大的值作为总人数(可能趋势数据和明细数据统计方式不同)
  478. totalPeople = Math.max(totalPeople, trendTotal);
  479. }
  480. return {
  481. totalPeople: totalPeople,
  482. totalEntries: totalEntries,
  483. avgFrequency: Number(avgFrequency).toFixed(2) || 0,
  484. maxFrequency: maxFreq
  485. };
  486. }
  487. /** 渲染趋势图 */
  488. function renderTrendChart(trendData, queryType) {
  489. if (!trendChartContainer.value) return;
  490. // 如果已有图表实例,先销毁
  491. if (trendChartContainer.value.__echarts_instance) {
  492. echarts.getInstanceByDom(trendChartContainer.value)?.dispose();
  493. }
  494. const myChart = echarts.init(trendChartContainer.value);
  495. // 保存图表实例引用
  496. trendChartContainer.value.__echarts_instance = myChart;
  497. let xAxisData = [];
  498. let seriesData = [];
  499. if (trendData && trendData.length > 0) {
  500. // 根据查询类型确定X轴标签
  501. if (queryType === 'year') {
  502. // 按年查询,显示每月趋势,使用period字段
  503. xAxisData = trendData.map(item => {
  504. // period格式可能是 "2023-01" 格式
  505. if (item.period) {
  506. // 检查是否包含日期分隔符,提取月份
  507. if (item.period.includes('-')) {
  508. const parts = item.period.split('-');
  509. if (parts.length === 2) {
  510. return parts[1] + '月'; // 如 "01月"
  511. }
  512. }
  513. return item.period + '月';
  514. } else {
  515. return '未知';
  516. }
  517. });
  518. } else if (queryType === 'month') {
  519. // 按月查询,显示当月每天趋势
  520. xAxisData = trendData.map(item => {
  521. if (item.period) {
  522. // 如果是完整日期格式 "YYYY-MM-DD",取日部分
  523. if (item.period.includes('-')) {
  524. const parts = item.period.split('-');
  525. if (parts.length === 3) {
  526. return parts[2] + '日'; // 如 "01日"
  527. }
  528. }
  529. return item.period + '日';
  530. } else {
  531. return '未知';
  532. }
  533. });
  534. } else {
  535. // 按时段查询,显示按天趋势
  536. xAxisData = trendData.map(item => {
  537. if (item.period) {
  538. // 如果是完整日期格式 "YYYY-MM-DD",显示 MM-DD
  539. if (item.period.includes('-')) {
  540. const parts = item.period.split('-');
  541. if (parts.length === 3) {
  542. return parts[1] + '-' + parts[2]; // 如 "05-15"
  543. }
  544. }
  545. return item.period;
  546. } else {
  547. return '未知';
  548. }
  549. });
  550. }
  551. // 获取人数或次数数据 - 使用inOutCount字段
  552. seriesData = trendData.map(item => {
  553. // 使用新的字段名inOutCount
  554. return item.inOutCount !== undefined ? item.inOutCount : 0;
  555. });
  556. } else {
  557. // 如果没有趋势数据,显示提示信息
  558. xAxisData = ['暂无数据'];
  559. seriesData = [0];
  560. }
  561. const option = {
  562. title: {
  563. text: getTrendChartTitle(queryType),
  564. left: 'center'
  565. },
  566. tooltip: {
  567. trigger: 'axis',
  568. axisPointer: {
  569. type: 'shadow'
  570. },
  571. formatter: (params) => {
  572. const param = params[0];
  573. if (param.name === '暂无数据') {
  574. return '暂无数据';
  575. }
  576. const date = xAxisData[param.dataIndex];
  577. const value = param.value;
  578. return `${date}<br/>夜间出入境人数:${value}`;
  579. }
  580. },
  581. xAxis: {
  582. type: 'category',
  583. data: xAxisData,
  584. axisLabel: {
  585. rotate: 45,
  586. fontSize: 12
  587. }
  588. },
  589. yAxis: {
  590. type: 'value',
  591. name: '人数'
  592. },
  593. series: [{
  594. data: seriesData,
  595. type: 'line',
  596. smooth: true,
  597. itemStyle: {
  598. color: '#5470c6'
  599. },
  600. areaStyle: {
  601. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  602. { offset: 0, color: 'rgba(84, 112, 198, 0.4)' },
  603. { offset: 1, color: 'rgba(84, 112, 198, 0.1)' }
  604. ])
  605. }
  606. }]
  607. };
  608. myChart.setOption(option);
  609. }
  610. /** 获取趋势图标题 */
  611. function getTrendChartTitle(queryType) {
  612. if (queryType === 'year') {
  613. return `年度夜间出入境人员月度趋势图 (年: ${queryParams.value.year})`;
  614. } else if (queryType === 'month') {
  615. return `月度夜间出入境人员每日趋势图 (月: ${queryParams.value.month})`;
  616. } else {
  617. const start = queryParams.value.dateRange ? queryParams.value.dateRange[0] : '';
  618. const end = queryParams.value.dateRange ? queryParams.value.dateRange[1] : '';
  619. return `指定时段夜间出入境人员趋势图 (${start} 至 ${end})`;
  620. }
  621. }
  622. /** 搜索按钮操作 */
  623. function handleQuery() {
  624. queryParams.value.pageNum = 1;
  625. getList();
  626. }
  627. /** 重置按钮操作 */
  628. function resetQuery() {
  629. proxy.resetForm("queryRef");
  630. queryParams.value.queryType = 'year';
  631. queryParams.value.year = new Date().getFullYear().toString();
  632. queryParams.value.month = null;
  633. queryParams.value.dateRange = null;
  634. handleQuery();
  635. }
  636. /** 处理表格行点击事件 */
  637. function handleRowClick(row) {
  638. // 显示详细信息
  639. detailForm.value = { ...row };
  640. detailTitle.value = `${row.fullName || '未知'} 详细信息`;
  641. recordDetailOpen.value = true;
  642. }
  643. // 在组件挂载后,根据默认查询类型执行查询
  644. onMounted(() => {
  645. // 确保在DOM更新后再执行查询
  646. nextTick(() => {
  647. handleQuery();
  648. });
  649. });
  650. // 在组件卸载时清理图表实例
  651. onUnmounted(() => {
  652. if (trendChartContainer.value && trendChartContainer.value.__echarts_instance) {
  653. echarts.getInstanceByDom(trendChartContainer.value)?.dispose();
  654. trendChartContainer.value.__echarts_instance = null;
  655. }
  656. });
  657. </script>
  658. <style lang="scss" scoped>
  659. .app-container {
  660. padding: 20px;
  661. }
  662. .stat-card {
  663. height: 80px;
  664. text-align: center;
  665. .stat-item {
  666. .stat-number {
  667. font-size: 24px;
  668. font-weight: bold;
  669. color: #409eff;
  670. margin-bottom: 5px;
  671. }
  672. .stat-label {
  673. font-size: 14px;
  674. color: #606266;
  675. }
  676. }
  677. &.total-people { .stat-number { color: #409eff; } }
  678. &.total-entries { .stat-number { color: #67c21a; } }
  679. &.avg-frequency { .stat-number { color: #e6a23c; } }
  680. &.max-frequency { .stat-number { color: #f56c6c; } }
  681. }
  682. .mb20 {
  683. margin-bottom: 20px;
  684. }
  685. .table_caption {
  686. font-weight: bold;
  687. text-align: left;
  688. padding-left: 10px;
  689. background-color: #f5f7fa;
  690. }
  691. </style>