CoResidenceDetailModal.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <template>
  2. <QueryTableModal
  3. :open="open"
  4. :title="title"
  5. :width="width"
  6. :loading="loading"
  7. :filters="filters"
  8. :filter-schema="activeFilterSchema"
  9. :columns="activeColumns"
  10. :action-column="actionColumn"
  11. :data-source="tableData"
  12. :pagination="pagination"
  13. :total-pages="totalPages"
  14. :page-size-options="pageSizeOptions"
  15. :show-table-setting="false"
  16. :table-setting="{ size: true, setting: true, redo: false, fullScreen: false }"
  17. :can-fullscreen="true"
  18. :switch-options="resolvedModeOptions"
  19. :active-switch="activeMode"
  20. wrap-class-name="co-residence-detail-modal"
  21. @update:open="(value) => emit('update:open', value)"
  22. @search="handleSearch"
  23. @reset="handleReset"
  24. @prev-page="handlePrevPage"
  25. @next-page="handleNextPage"
  26. @page-size-change="handlePageSizeChange"
  27. @page-change="handlePageChange"
  28. @sort-change="handleSortChange"
  29. @switch-change="handleModeChange"
  30. />
  31. </template>
  32. <script lang="ts" setup>
  33. import { computed, nextTick, reactive, ref, watch } from 'vue';
  34. import QueryTableModal from '@jeesite/case/views/dm/components/QueryTableModal.vue';
  35. import { useMessage } from '@jeesite/core/hooks/web/useMessage';
  36. import componentSetting from '@jeesite/core/settings/componentSetting';
  37. import { useTimeSeriesJump } from '@jeesite/otg/hooks/useTimeSeriesJump';
  38. import { hotelStayInfoDetail, togetherLiveInfoDetail } from '@jeesite/track/api/trackApi';
  39. import type { HotelStayInfo, TogetherLiveInfo } from '@jeesite/track/api/types/dto';
  40. import type { TogetherLiveDetailQuery } from '@jeesite/track/api/types/query';
  41. type DetailMode = 'hotelStay' | 'togetherLive';
  42. interface ModeOption {
  43. label: string;
  44. value: DetailMode;
  45. disabled?: boolean;
  46. }
  47. interface FilterSchemaItem {
  48. field: string;
  49. label: string;
  50. type: 'input' | 'date' | 'date-range' | 'select' | 'compare-input';
  51. placeholder?: string;
  52. width?: number;
  53. maxWidth?: number;
  54. startField?: string;
  55. endField?: string;
  56. rangePlaceholder?: [string, string];
  57. valueFormat?: string;
  58. }
  59. const props = withDefaults(
  60. defineProps<{
  61. open: boolean;
  62. title?: string;
  63. width?: number;
  64. baseQuery?: Partial<TogetherLiveDetailQuery>;
  65. initialMode?: DetailMode;
  66. modeOptions?: ReadonlyArray<ModeOption>;
  67. }>(),
  68. {
  69. title: '同住记录',
  70. width: 1460,
  71. baseQuery: () => ({}),
  72. initialMode: 'hotelStay',
  73. modeOptions: () => [],
  74. },
  75. );
  76. const emit = defineEmits<{
  77. (e: 'update:open', value: boolean): void;
  78. }>();
  79. const { createMessage } = useMessage();
  80. const { jumping, jumpToBehaviorSpacetime } = useTimeSeriesJump();
  81. const loading = ref(false);
  82. const tableData = ref<any[]>([]);
  83. const activeMode = ref<DetailMode>(props.initialMode);
  84. const filters = reactive({
  85. hotelName: '',
  86. inStartDate: '',
  87. inEndDate: '',
  88. outStartDate: '',
  89. outEndDate: '',
  90. orderKey: '',
  91. sort: '' as '' | 'ASC' | 'DESC',
  92. });
  93. const pagination = reactive({
  94. page: 1,
  95. limit: componentSetting.table.defaultPageSize,
  96. total: 0,
  97. });
  98. const totalPages = computed(() => {
  99. if (!pagination.total) return 1;
  100. return Math.max(1, Math.ceil(pagination.total / pagination.limit));
  101. });
  102. const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
  103. label: value,
  104. value: Number(value),
  105. }));
  106. const resolvedModeOptions = computed<ReadonlyArray<ModeOption>>(() => {
  107. if (props.modeOptions.length) return props.modeOptions;
  108. return [
  109. { label: '旅馆住宿信息', value: 'hotelStay' },
  110. { label: '同住服务', value: 'togetherLive' },
  111. ];
  112. });
  113. const commonFilterSchema: ReadonlyArray<FilterSchemaItem> = [
  114. { field: 'hotelName', label: '旅店名称', type: 'input', placeholder: '请输入旅店名称', width: 220 },
  115. {
  116. field: 'checkinRange',
  117. label: '入住时间',
  118. type: 'date-range',
  119. startField: 'inStartDate',
  120. endField: 'inEndDate',
  121. width: 280,
  122. maxWidth: 320,
  123. rangePlaceholder: ['开始日期', '结束日期'],
  124. valueFormat: 'YYYY-MM-DD',
  125. },
  126. {
  127. field: 'checkoutRange',
  128. label: '退房时间',
  129. type: 'date-range',
  130. startField: 'outStartDate',
  131. endField: 'outEndDate',
  132. width: 280,
  133. maxWidth: 320,
  134. rangePlaceholder: ['开始日期', '结束日期'],
  135. valueFormat: 'YYYY-MM-DD',
  136. },
  137. ];
  138. const hotelStayColumns = [
  139. { title: '序号', dataIndex: 'id', key: 'id', width: 70, align: 'center' as const },
  140. { title: '身份证明名称', dataIndex: 'personCertName', key: 'personCertName', width: 140, align: 'center' as const },
  141. {
  142. title: '身份证号码',
  143. dataIndex: 'personCertNo',
  144. key: 'personCertNo',
  145. width: 180,
  146. align: 'center' as const,
  147. },
  148. { title: '姓名', dataIndex: 'personName', key: 'personName', width: 120, align: 'center' as const },
  149. { title: '性别', dataIndex: 'gender', key: 'gender', width: 90, align: 'center' as const },
  150. { title: '住址省市县区', dataIndex: 'addressCity', key: 'addressCity', width: 180, align: 'center' as const },
  151. { title: '住址详情', dataIndex: 'addressDetail', key: 'addressDetail', width: 220, align: 'center' as const },
  152. { title: '旅店名称', dataIndex: 'hotelName', key: 'hotelName', width: 180, align: 'center' as const },
  153. { title: '旅店详细地址', dataIndex: 'hotelAddress', key: 'hotelAddress', width: 220, align: 'center' as const },
  154. { title: '入住房号', dataIndex: 'roomNo', key: 'roomNo', width: 120, align: 'center' as const },
  155. {
  156. title: '入住时间',
  157. dataIndex: 'inDateTime',
  158. key: 'inDateTime',
  159. width: 180,
  160. align: 'center' as const,
  161. sorter: true,
  162. },
  163. {
  164. title: '退房时间',
  165. dataIndex: 'outDateTime',
  166. key: 'outDateTime',
  167. width: 180,
  168. align: 'center' as const,
  169. sorter: true,
  170. },
  171. ];
  172. const togetherLiveColumns = [
  173. { title: '序号', dataIndex: 'id', key: 'id', width: 70, align: 'center' as const },
  174. { title: '被调查对象', dataIndex: 'queryName', key: 'queryName', width: 120, align: 'center' as const },
  175. {
  176. title: '被调查对象证件号',
  177. dataIndex: 'queryCertNo',
  178. key: 'queryCertNo',
  179. width: 180,
  180. align: 'center' as const,
  181. },
  182. { title: '同住人姓名', dataIndex: 'togetherName', key: 'togetherName', width: 120, align: 'center' as const },
  183. {
  184. title: '同住人证件号',
  185. dataIndex: 'togetherCertNo',
  186. key: 'togetherCertNo',
  187. width: 180,
  188. align: 'center' as const,
  189. },
  190. {
  191. title: '同住人证件类型',
  192. dataIndex: 'togetherCertType',
  193. key: 'togetherCertType',
  194. width: 140,
  195. align: 'center' as const,
  196. },
  197. { title: '同住人性别', dataIndex: 'togetherGender', key: 'togetherGender', width: 110, align: 'center' as const },
  198. { title: '同住人年龄', dataIndex: 'togetherAge', key: 'togetherAge', width: 110, align: 'center' as const },
  199. {
  200. title: '同住人民族',
  201. dataIndex: 'togetherEthnicity',
  202. key: 'togetherEthnicity',
  203. width: 120,
  204. align: 'center' as const,
  205. },
  206. {
  207. title: '同住人籍贯',
  208. dataIndex: 'togetherOrigin',
  209. key: 'togetherOrigin',
  210. width: 160,
  211. align: 'center' as const,
  212. },
  213. { title: '旅店名称', dataIndex: 'hotelName', key: 'hotelName', width: 180, align: 'center' as const },
  214. { title: '入住房号', dataIndex: 'roomNo', key: 'roomNo', width: 120, align: 'center' as const },
  215. { title: '同住次数', dataIndex: 'stayCount', key: 'stayCount', width: 110, align: 'center' as const },
  216. { title: '同住类型', dataIndex: 'coType', key: 'coType', width: 120, align: 'center' as const },
  217. {
  218. title: '入住时间',
  219. dataIndex: 'inDateTime',
  220. key: 'inDateTime',
  221. width: 180,
  222. align: 'center' as const,
  223. sorter: true,
  224. },
  225. {
  226. title: '退房时间',
  227. dataIndex: 'outDateTime',
  228. key: 'outDateTime',
  229. width: 180,
  230. align: 'center' as const,
  231. sorter: true,
  232. },
  233. { title: '备注', dataIndex: 'remark', key: 'remark', width: 160, align: 'center' as const },
  234. ];
  235. const activeFilterSchema = computed(() => commonFilterSchema);
  236. const activeColumns = computed(() => (activeMode.value === 'hotelStay' ? hotelStayColumns : togetherLiveColumns));
  237. const normalizeActionRecordId = (value: unknown) => {
  238. const text = String(value ?? '').trim();
  239. if (!text || text === '-' || text === 'null' || text === 'undefined') return undefined;
  240. return text;
  241. };
  242. const resolveJumpPersonName = (record?: HotelStayInfo | TogetherLiveInfo) => {
  243. const personName = pickFirstValue(
  244. record?.personName,
  245. (record as TogetherLiveInfo | undefined)?.queryName,
  246. props.baseQuery?.personName,
  247. props.baseQuery?.objName,
  248. );
  249. const normalizedName = String(personName ?? '').trim();
  250. return normalizedName || undefined;
  251. };
  252. const buildJumpQuery = (record?: HotelStayInfo | TogetherLiveInfo) => {
  253. const id = normalizeActionRecordId(record?.id);
  254. const personName = resolveJumpPersonName(record);
  255. const togetherName = pickFirstValue(
  256. (record as TogetherLiveInfo | undefined)?.togetherName,
  257. props.baseQuery?.liveWithName,
  258. );
  259. const keyword = activeMode.value === 'togetherLive' ? String(togetherName ?? '').trim() || undefined : undefined;
  260. const seriesDateTime = String(record?.inDateTime || record?.checkinTime || '').trim() || undefined;
  261. return {
  262. hasLocal: 'N',
  263. tableName: 'hotel_stay_info',
  264. tableNameEn: 'hotel_stay_info',
  265. tableNameCn: '住宿信息',
  266. id,
  267. rowId: id,
  268. personName,
  269. personNames: personName ? [personName] : undefined,
  270. keyword,
  271. seriesDateTime,
  272. };
  273. };
  274. const handleJump = async (record?: HotelStayInfo | TogetherLiveInfo) => {
  275. await jumpToBehaviorSpacetime(buildJumpQuery(record), {
  276. onBeforeNavigate: () => {
  277. emit('update:open', false);
  278. },
  279. });
  280. };
  281. const actionColumn = computed(() => ({
  282. title: '操作',
  283. dataIndex: 'action',
  284. key: 'action',
  285. width: 90,
  286. fixed: 'right',
  287. align: 'center',
  288. actions: (record: { raw?: HotelStayInfo | TogetherLiveInfo }) => {
  289. const rawRecord = record.raw;
  290. const query = buildJumpQuery(rawRecord);
  291. return [
  292. {
  293. label: '跳转',
  294. disabled: jumping.value || !query.id,
  295. onClick: () => void handleJump(rawRecord),
  296. },
  297. ];
  298. },
  299. }));
  300. const normalizeList = <T = any,>(resp: any): T[] => {
  301. if (Array.isArray(resp)) return resp;
  302. if (Array.isArray(resp?.list)) return resp.list;
  303. if (Array.isArray(resp?.data)) return resp.data;
  304. if (Array.isArray(resp?.data?.list)) return resp.data.list;
  305. if (Array.isArray(resp?.records)) return resp.records;
  306. if (Array.isArray(resp?.data?.records)) return resp.data.records;
  307. return [];
  308. };
  309. const normalizeTotal = (resp: any, listLen: number) => {
  310. const total = resp?.total ?? resp?.data?.total ?? resp?.count ?? resp?.data?.count ?? listLen;
  311. return Number.isFinite(Number(total)) ? Number(total) : listLen;
  312. };
  313. const normalizeCurrent = (resp: any, fallback: number) => {
  314. const current = resp?.current ?? resp?.data?.current ?? fallback;
  315. return Number.isFinite(Number(current)) && Number(current) > 0 ? Number(current) : fallback;
  316. };
  317. const normalizeSize = (resp: any, fallback: number) => {
  318. const size = resp?.size ?? resp?.data?.size ?? fallback;
  319. return Number.isFinite(Number(size)) && Number(size) > 0 ? Number(size) : fallback;
  320. };
  321. const fallbackValue = (value: string | number | null | undefined) => {
  322. if (value === 0) return '0';
  323. if (value === null || value === undefined || value === '') return '-';
  324. return String(value);
  325. };
  326. const pickFirstValue = (...values: Array<string | number | null | undefined>) => {
  327. for (const value of values) {
  328. if (value === 0) return value;
  329. if (value === null || value === undefined) continue;
  330. if (String(value).trim() === '') continue;
  331. return value;
  332. }
  333. return undefined;
  334. };
  335. const formatBoundary = (value?: string, isEnd = false) => {
  336. if (!value) return undefined;
  337. return `${value} ${isEnd ? '23:59:59' : '00:00:00'}`;
  338. };
  339. const buildQuery = (): TogetherLiveDetailQuery => {
  340. const hotelName = filters.hotelName || props.baseQuery?.hotelName || undefined;
  341. const query: TogetherLiveDetailQuery = {
  342. ...props.baseQuery,
  343. page: pagination.page,
  344. limit: pagination.limit,
  345. hotelName,
  346. search: hotelName || props.baseQuery?.search || undefined,
  347. inStartTime: formatBoundary(filters.inStartDate, false) || props.baseQuery?.inStartTime,
  348. inEndTime: formatBoundary(filters.inEndDate, true) || props.baseQuery?.inEndTime,
  349. outStartTime: formatBoundary(filters.outStartDate, false) || props.baseQuery?.outStartTime,
  350. outEndTime: formatBoundary(filters.outEndDate, true) || props.baseQuery?.outEndTime,
  351. orderKey: filters.orderKey || undefined,
  352. sort: filters.sort || undefined,
  353. };
  354. return query;
  355. };
  356. const mapHotelStayRow = (item: HotelStayInfo, index: number) => ({
  357. rowKey: item.id || `${pagination.page}-${index}-${item.personName || ''}-${item.hotelName || ''}`,
  358. id: (pagination.page - 1) * pagination.limit + index + 1,
  359. personCertName: fallbackValue(item.personCertName),
  360. personCertNo: fallbackValue(item.personCertNo),
  361. personName: fallbackValue(item.personName),
  362. gender: fallbackValue(item.gender),
  363. addressCity: fallbackValue(item.addressCity),
  364. addressDetail: fallbackValue(item.addressDetail),
  365. hotelName: fallbackValue(item.hotelName),
  366. hotelAddress: fallbackValue(item.hotelAddress),
  367. roomNo: fallbackValue(item.roomNo),
  368. inDateTime: fallbackValue(item.inDateTime || item.checkinTime),
  369. outDateTime: fallbackValue(item.outDateTime || item.checkoutTime),
  370. raw: item,
  371. });
  372. const mapTogetherLiveRow = (item: TogetherLiveInfo, index: number) => ({
  373. rowKey:
  374. item.id ||
  375. `${pagination.page}-${index}-${item.queryCertNo || item.personCertNo || ''}-${item.togetherName || ''}`,
  376. id: (pagination.page - 1) * pagination.limit + index + 1,
  377. queryName: fallbackValue(
  378. pickFirstValue(item.queryName, item.personName, props.baseQuery?.objName, props.baseQuery?.personName),
  379. ),
  380. queryCertNo: fallbackValue(
  381. pickFirstValue(item.queryCertNo, item.personCertNo, props.baseQuery?.objIdCard, props.baseQuery?.personCertNo),
  382. ),
  383. togetherName: fallbackValue(pickFirstValue(item.togetherName, props.baseQuery?.liveWithName)),
  384. togetherCertNo: fallbackValue(
  385. pickFirstValue(item.togetherCertNo, props.baseQuery?.liveWithCertNo, props.baseQuery?.liveWithIdCard),
  386. ),
  387. togetherCertType: fallbackValue(item.togetherCertType),
  388. togetherGender: fallbackValue(item.togetherGender),
  389. togetherAge: fallbackValue(item.togetherAge),
  390. togetherEthnicity: fallbackValue(item.togetherEthnicity),
  391. togetherOrigin: fallbackValue(item.togetherOrigin),
  392. hotelName: fallbackValue(item.hotelName),
  393. roomNo: fallbackValue(item.roomNo),
  394. stayCount: Number.isFinite(Number(item.stayCount)) ? Number(item.stayCount) : fallbackValue(item.stayCount),
  395. coType: fallbackValue(item.coType),
  396. inDateTime: fallbackValue(item.inDateTime || item.checkinTime),
  397. outDateTime: fallbackValue(item.outDateTime || item.checkoutTime),
  398. remark: fallbackValue(item.remark),
  399. raw: item,
  400. });
  401. const fetchTable = async () => {
  402. loading.value = true;
  403. try {
  404. const resp =
  405. activeMode.value === 'hotelStay'
  406. ? await hotelStayInfoDetail(buildQuery())
  407. : await togetherLiveInfoDetail(buildQuery());
  408. const list =
  409. activeMode.value === 'hotelStay' ? normalizeList<HotelStayInfo>(resp) : normalizeList<TogetherLiveInfo>(resp);
  410. tableData.value =
  411. activeMode.value === 'hotelStay'
  412. ? list.map((item, index) => mapHotelStayRow(item as HotelStayInfo, index))
  413. : list.map((item, index) => mapTogetherLiveRow(item as TogetherLiveInfo, index));
  414. pagination.total = normalizeTotal(resp, list.length);
  415. pagination.page = normalizeCurrent(resp, pagination.page);
  416. pagination.limit = normalizeSize(resp, pagination.limit);
  417. } catch (error) {
  418. tableData.value = [];
  419. pagination.total = 0;
  420. createMessage.warning('同住详情接口调用失败');
  421. } finally {
  422. loading.value = false;
  423. }
  424. };
  425. const syncFiltersFromBaseQuery = () => {
  426. filters.hotelName = props.baseQuery?.hotelName || '';
  427. filters.inStartDate = '';
  428. filters.inEndDate = '';
  429. filters.outStartDate = '';
  430. filters.outEndDate = '';
  431. filters.orderKey = '';
  432. filters.sort = '';
  433. };
  434. const handleSearch = async () => {
  435. pagination.page = 1;
  436. await fetchTable();
  437. };
  438. const handleReset = async () => {
  439. syncFiltersFromBaseQuery();
  440. pagination.page = 1;
  441. await fetchTable();
  442. };
  443. const handlePrevPage = async () => {
  444. if (loading.value || pagination.page <= 1) return;
  445. pagination.page -= 1;
  446. await fetchTable();
  447. };
  448. const handleNextPage = async () => {
  449. if (loading.value || pagination.page >= totalPages.value) return;
  450. pagination.page += 1;
  451. await fetchTable();
  452. };
  453. const handlePageChange = async (page: number, pageSize: number) => {
  454. if (loading.value) return;
  455. if (pageSize !== pagination.limit) {
  456. await handlePageSizeChange(pageSize);
  457. return;
  458. }
  459. if (page === pagination.page - 1) {
  460. await handlePrevPage();
  461. return;
  462. }
  463. if (page === pagination.page + 1) {
  464. await handleNextPage();
  465. return;
  466. }
  467. if (page === pagination.page) return;
  468. pagination.page = page;
  469. await fetchTable();
  470. };
  471. const handlePageSizeChange = async (size: number) => {
  472. pagination.limit = Number(size);
  473. pagination.page = 1;
  474. await fetchTable();
  475. };
  476. const handleSortChange = async (payload: { orderKey: string; sort: 'ASC' | 'DESC' }) => {
  477. filters.orderKey = payload.orderKey;
  478. filters.sort = payload.sort;
  479. pagination.page = 1;
  480. await fetchTable();
  481. };
  482. const handleModeChange = async (value: string | number) => {
  483. const nextMode = String(value) as DetailMode;
  484. if (nextMode === activeMode.value) return;
  485. activeMode.value = nextMode;
  486. filters.orderKey = '';
  487. filters.sort = '';
  488. pagination.page = 1;
  489. await fetchTable();
  490. };
  491. const initAndFetch = async () => {
  492. activeMode.value = props.initialMode;
  493. pagination.page = 1;
  494. syncFiltersFromBaseQuery();
  495. await fetchTable();
  496. };
  497. let initFetchScheduled = false;
  498. const scheduleInitAndFetch = () => {
  499. if (!props.open || initFetchScheduled) return;
  500. initFetchScheduled = true;
  501. nextTick(async () => {
  502. initFetchScheduled = false;
  503. if (!props.open) return;
  504. await initAndFetch();
  505. });
  506. };
  507. watch(
  508. () => props.open,
  509. (open) => {
  510. if (!open) {
  511. initFetchScheduled = false;
  512. return;
  513. }
  514. scheduleInitAndFetch();
  515. },
  516. );
  517. watch(
  518. () => props.baseQuery,
  519. () => {
  520. if (!props.open) return;
  521. scheduleInitAndFetch();
  522. },
  523. { deep: true },
  524. );
  525. watch(
  526. () => props.initialMode,
  527. (mode) => {
  528. if (!props.open) return;
  529. activeMode.value = mode;
  530. scheduleInitAndFetch();
  531. },
  532. );
  533. defineExpose({
  534. reload: fetchTable,
  535. });
  536. </script>
  537. <style lang="less">
  538. .co-residence-detail-modal {
  539. .query-table-search {
  540. row-gap: 12px;
  541. }
  542. }
  543. </style>