ContinuousDetailModal.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <template>
  2. <QueryTableModal
  3. :open="open"
  4. :title="title"
  5. :width="width"
  6. :loading="loading"
  7. :filters="filters"
  8. :filter-schema="filterSchema"
  9. :columns="columns"
  10. :data-source="tableData"
  11. :pagination="pagination"
  12. :total-pages="totalPages"
  13. :page-size-options="pageSizeOptions"
  14. :show-table-setting="false"
  15. :table-setting="{ size: true, setting: true, redo: false, fullScreen: false }"
  16. :can-fullscreen="true"
  17. wrap-class-name="continuous-detail-modal"
  18. @update:open="(v) => emit('update:open', v)"
  19. @search="handleSearch"
  20. @reset="handleReset"
  21. @prev-page="handlePrevPage"
  22. @next-page="handleNextPage"
  23. @page-size-change="handlePageSizeChange"
  24. @page-change="handlePageChange"
  25. @sort-change="handleSortChange"
  26. />
  27. <CallRecordModal
  28. v-model:open="callRecordModalOpen"
  29. title="通信记录"
  30. :base-query="callRecordBaseQuery"
  31. @jump="emit('update:open', false)"
  32. />
  33. </template>
  34. <script lang="ts" setup>
  35. import { computed, h, nextTick, reactive, ref, watch } from 'vue';
  36. import { Button } from 'ant-design-vue';
  37. import QueryTableModal from '@jeesite/case/views/dm/components/QueryTableModal.vue';
  38. import componentSetting from '@jeesite/core/settings/componentSetting';
  39. import { mergeDetailDateRange, pickDetailValue } from '@jeesite/core/utils/detailQuery';
  40. import { statSecondCallContinuous } from '@jeesite/call/api/callApi';
  41. import type { CallContinuousDTO } from '@jeesite/call/api/types/dto';
  42. import type { CallContinuousQuery, CallRecordQuery } from '@jeesite/call/api/types/query';
  43. import CallRecordModal from './CallRecordModal.vue';
  44. const props = withDefaults(
  45. defineProps<{
  46. open: boolean;
  47. title?: string;
  48. width?: number;
  49. baseQuery?: Partial<CallContinuousQuery>;
  50. }>(),
  51. {
  52. title: '持续联系明细',
  53. width: 1320,
  54. baseQuery: () => ({}),
  55. },
  56. );
  57. const emit = defineEmits<{
  58. (e: 'update:open', v: boolean): void;
  59. }>();
  60. const loading = ref(false);
  61. const tableData = ref<any[]>([]);
  62. const callRecordModalOpen = ref(false);
  63. const callRecordBaseQuery = reactive<Partial<CallRecordQuery>>({});
  64. const filters = reactive({
  65. personName: '',
  66. otherName: '',
  67. otherPhone: '',
  68. startDate: '',
  69. endDate: '',
  70. orderKey: '',
  71. sort: '' as '' | 'ASC' | 'DESC',
  72. });
  73. const pagination = reactive({
  74. page: 1,
  75. limit: componentSetting.table.defaultPageSize,
  76. total: 0,
  77. });
  78. const totalPages = computed(() => {
  79. if (!pagination.total) return 1;
  80. return Math.max(1, Math.ceil(pagination.total / pagination.limit));
  81. });
  82. const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
  83. label: value,
  84. value,
  85. }));
  86. const filterSchema = [
  87. { field: 'personName', label: '本方姓名', type: 'input' as const, placeholder: '请输入本方姓名', width: 180 },
  88. { field: 'otherName', label: '对方姓名', type: 'input' as const, placeholder: '请输入对方姓名', width: 180 },
  89. { field: 'otherPhone', label: '对方号码', type: 'input' as const, placeholder: '请输入对方号码', width: 180 },
  90. ];
  91. const normalizePhoneList = (value?: string | string[] | null) => {
  92. if (Array.isArray(value)) return value.filter(Boolean);
  93. if (!value) return undefined;
  94. const list = String(value)
  95. .split(',')
  96. .map((item) => item.trim())
  97. .filter(Boolean);
  98. return list.length ? list : undefined;
  99. };
  100. const normalizeMonthDate = (monthLike?: string, isEnd = false) => {
  101. if (!monthLike) return undefined;
  102. const normalized = String(monthLike).slice(0, 7);
  103. if (!/^\d{4}-\d{2}$/.test(normalized)) return undefined;
  104. const [year, month] = normalized.split('-').map((item) => Number(item));
  105. if (!year || !month) return undefined;
  106. const date = new Date(year, month - 1, isEnd ? 0 : 1);
  107. if (isEnd) {
  108. date.setMonth(month, 0);
  109. }
  110. const yyyy = date.getFullYear();
  111. const mm = String(date.getMonth() + 1).padStart(2, '0');
  112. const dd = String(date.getDate()).padStart(2, '0');
  113. return `${yyyy}-${mm}-${dd}`;
  114. };
  115. const handleViewRecord = (record: any) => {
  116. const raw = record?.raw || {};
  117. const { startDate, endDate } = mergeDetailDateRange(
  118. {
  119. startDate: pickDetailValue(filters.startDate, props.baseQuery?.startDate),
  120. endDate: pickDetailValue(filters.endDate, props.baseQuery?.endDate),
  121. },
  122. {
  123. startDate: normalizeMonthDate(raw.continuousStartMonth, false),
  124. endDate: normalizeMonthDate(raw.continuousEndMonth, true),
  125. },
  126. );
  127. callRecordBaseQuery.personName = pickDetailValue(raw.personName, filters.personName, props.baseQuery?.personName);
  128. callRecordBaseQuery.personPhones = raw.personPhone ? [raw.personPhone] : undefined;
  129. callRecordBaseQuery.otherName = pickDetailValue(raw.otherName, filters.otherName, props.baseQuery?.otherName);
  130. callRecordBaseQuery.otherPhones = raw.otherPhone ? [raw.otherPhone] : undefined;
  131. callRecordBaseQuery.startDate = startDate;
  132. callRecordBaseQuery.endDate = endDate;
  133. callRecordModalOpen.value = true;
  134. };
  135. const columns = [
  136. { title: '本方姓名', dataIndex: 'personName', key: 'personName', width: 140, sorter: true },
  137. { title: '本方号码', dataIndex: 'personPhone', key: 'personPhone', width: 160, sorter: true },
  138. { title: '对方姓名', dataIndex: 'otherName', key: 'otherName', width: 140, sorter: true },
  139. { title: '对方号码', dataIndex: 'otherPhone', key: 'otherPhone', width: 220, sorter: true },
  140. { title: '持续联系月数', dataIndex: 'pairContMonthCount', key: 'pairContMonthCount', width: 120, sorter: true },
  141. { title: '共持续联系次数', dataIndex: 'pairTotalCalls', key: 'pairTotalCalls', width: 130, sorter: true },
  142. {
  143. title: '操作',
  144. dataIndex: 'action',
  145. key: 'action',
  146. width: 120,
  147. fixed: 'right' as const,
  148. align: 'center' as const,
  149. customRender: ({ record }: { record: any }) =>
  150. h(
  151. Button,
  152. {
  153. type: 'link',
  154. size: 'small',
  155. onClick: () => handleViewRecord(record),
  156. },
  157. { default: () => '查看详情' },
  158. ),
  159. },
  160. ];
  161. const normalizeList = (resp: any): CallContinuousDTO[] => {
  162. if (Array.isArray(resp)) return resp;
  163. if (Array.isArray(resp?.list)) return resp.list;
  164. if (Array.isArray(resp?.data)) return resp.data;
  165. if (Array.isArray(resp?.data?.list)) return resp.data.list;
  166. if (Array.isArray(resp?.records)) return resp.records;
  167. if (Array.isArray(resp?.data?.records)) return resp.data.records;
  168. return [];
  169. };
  170. const normalizeTotal = (resp: any, listLen: number) => {
  171. const total = resp?.total ?? resp?.data?.total ?? resp?.count ?? resp?.data?.count ?? listLen;
  172. return Number.isFinite(Number(total)) ? Number(total) : listLen;
  173. };
  174. const normalizeCurrent = (resp: any, fallback: number) => {
  175. const current = resp?.current ?? resp?.data?.current ?? fallback;
  176. return Number.isFinite(Number(current)) && Number(current) > 0 ? Number(current) : fallback;
  177. };
  178. const normalizeSize = (resp: any, fallback: number) => {
  179. const size = resp?.size ?? resp?.data?.size ?? fallback;
  180. return Number.isFinite(Number(size)) && Number(size) > 0 ? Number(size) : fallback;
  181. };
  182. const buildQuery = (): CallContinuousQuery => {
  183. const personName = filters.personName || props.baseQuery?.personName || '';
  184. const otherName = filters.otherName || props.baseQuery?.otherName || '';
  185. return {
  186. ...props.baseQuery,
  187. page: pagination.page,
  188. limit: pagination.limit,
  189. monthVal: Number(props.baseQuery?.monthVal || 1),
  190. personName: personName || undefined,
  191. personNames: personName ? [personName] : props.baseQuery?.personNames,
  192. otherName: otherName || undefined,
  193. otherPhone: filters.otherPhone || props.baseQuery?.otherPhone || undefined,
  194. startDate: filters.startDate || props.baseQuery?.startDate || undefined,
  195. endDate: filters.endDate || props.baseQuery?.endDate || undefined,
  196. orderKey: filters.orderKey || undefined,
  197. sort: filters.sort || undefined,
  198. };
  199. };
  200. const mapRow = (item: CallContinuousDTO, index: number) => ({
  201. rowKey: `${item.personName || '-'}-${item.otherName || '-'}-${item.continuousStartMonth || index}`,
  202. id: (pagination.page - 1) * pagination.limit + index + 1,
  203. personName: item.personName || '-',
  204. personPhone: item.personPhone || '-',
  205. otherName: item.otherName || '-',
  206. otherPhone: item.otherPhone || '-',
  207. pairContMonthCount: item.pairContMonthCount ?? 0,
  208. pairTotalCalls: item.pairTotalCalls ?? 0,
  209. continuousStartMonth: item.continuousStartMonth || '-',
  210. continuousEndMonth: item.continuousEndMonth || '-',
  211. raw: item,
  212. });
  213. const syncFiltersFromBaseQuery = () => {
  214. filters.personName = props.baseQuery?.personName || '';
  215. filters.otherName = props.baseQuery?.otherName || '';
  216. filters.otherPhone = props.baseQuery?.otherPhone || '';
  217. filters.startDate = props.baseQuery?.startDate || '';
  218. filters.endDate = props.baseQuery?.endDate || '';
  219. filters.orderKey = '';
  220. filters.sort = '';
  221. };
  222. const fetchTable = async () => {
  223. loading.value = true;
  224. try {
  225. const resp = await statSecondCallContinuous(buildQuery());
  226. const list = normalizeList(resp);
  227. tableData.value = list.map(mapRow);
  228. pagination.total = normalizeTotal(resp, list.length);
  229. pagination.page = normalizeCurrent(resp, pagination.page);
  230. pagination.limit = normalizeSize(resp, pagination.limit);
  231. } catch (error) {
  232. tableData.value = [];
  233. pagination.total = 0;
  234. } finally {
  235. loading.value = false;
  236. }
  237. };
  238. const handleSearch = async () => {
  239. pagination.page = 1;
  240. await fetchTable();
  241. };
  242. const handleReset = async () => {
  243. syncFiltersFromBaseQuery();
  244. pagination.page = 1;
  245. await fetchTable();
  246. };
  247. const handlePrevPage = async () => {
  248. if (loading.value || pagination.page <= 1) return;
  249. pagination.page -= 1;
  250. await fetchTable();
  251. };
  252. const handleNextPage = async () => {
  253. if (loading.value || pagination.page >= totalPages.value) return;
  254. pagination.page += 1;
  255. await fetchTable();
  256. };
  257. const handlePageChange = async (page: number, pageSize: number) => {
  258. if (loading.value) return;
  259. if (pageSize !== pagination.limit) {
  260. await handlePageSizeChange(pageSize);
  261. return;
  262. }
  263. if (page === pagination.page - 1) {
  264. await handlePrevPage();
  265. return;
  266. }
  267. if (page === pagination.page + 1) {
  268. await handleNextPage();
  269. return;
  270. }
  271. if (page === pagination.page) return;
  272. pagination.page = page;
  273. await fetchTable();
  274. };
  275. const handlePageSizeChange = async (size: number) => {
  276. pagination.limit = Number(size);
  277. pagination.page = 1;
  278. await fetchTable();
  279. };
  280. const handleSortChange = async (payload: { orderKey: string; sort: 'ASC' | 'DESC' }) => {
  281. filters.orderKey = payload.orderKey;
  282. filters.sort = payload.sort;
  283. pagination.page = 1;
  284. await fetchTable();
  285. };
  286. const initAndFetch = async () => {
  287. pagination.page = 1;
  288. syncFiltersFromBaseQuery();
  289. await fetchTable();
  290. };
  291. let initFetchScheduled = false;
  292. const scheduleInitAndFetch = () => {
  293. if (!props.open || initFetchScheduled) return;
  294. initFetchScheduled = true;
  295. nextTick(async () => {
  296. initFetchScheduled = false;
  297. if (!props.open) return;
  298. await initAndFetch();
  299. });
  300. };
  301. watch(
  302. () => props.open,
  303. (open) => {
  304. if (!open) {
  305. initFetchScheduled = false;
  306. return;
  307. }
  308. scheduleInitAndFetch();
  309. },
  310. );
  311. watch(
  312. () => props.baseQuery,
  313. () => {
  314. if (!props.open) return;
  315. scheduleInitAndFetch();
  316. },
  317. { deep: true },
  318. );
  319. defineExpose({
  320. reload: fetchTable,
  321. });
  322. </script>
  323. <style lang="less">
  324. .continuous-detail-modal {
  325. .ant-table-cell-fix-right,
  326. .ant-table-cell-fix-right-first {
  327. background: #fff;
  328. }
  329. }
  330. </style>