| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668 |
- <template>
- <QueryTableModal
- :open="open"
- :title="title"
- :width="width"
- :z-index="zIndex"
- :loading="loading"
- :filters="filters"
- :filter-schema="visibleFilterSchema"
- :columns="visibleColumns"
- :action-column="actionColumn"
- :data-source="tableData"
- :pagination="pagination"
- :total-pages="totalPages"
- :page-size-options="pageSizeOptions"
- :show-table-setting="true"
- :table-setting="{ size: true, setting: true, redo: false, fullScreen: false }"
- :can-fullscreen="true"
- wrap-class-name="call-record-modal"
- @update:open="(v) => emit('update:open', v)"
- @search="handleSearch"
- @reset="handleReset"
- @prev-page="handlePrevPage"
- @next-page="handleNextPage"
- @page-size-change="handlePageSizeChange"
- @page-change="handlePageChange"
- @sort-change="handleSortChange"
- />
- </template>
- <script lang="ts" setup>
- import { computed, nextTick, reactive, ref, watch } from 'vue';
- import QueryTableModal from '@/case/views/dm/components/QueryTableModal.vue';
- import componentSetting from '@/core/settings/componentSetting';
- import { useMessage } from '@/core/hooks/web/useMessage';
- import {
- downloadOfficeFileById,
- getOpenOfficeFileErrorMessage,
- resolveOfficeFileId,
- } from '@/core/utils/openOfficeFile';
- import { maskCertNo } from '@/core/utils/sensitive';
- import { useTimeSeriesJump } from '@/otg/hooks/useTimeSeriesJump';
- import { getCallRecord } from '@/call/api/callApi';
- import type { CallRecord } from '@/call/api/types/dto';
- import type { CallRecordQuery } from '@/call/api/types/query';
- type CallRecordModalQuery = Partial<CallRecordQuery> & Record<string, any>;
- const props = withDefaults(
- defineProps<{
- open: boolean;
- title?: string;
- width?: number;
- zIndex?: number;
- baseQuery?: CallRecordModalQuery;
- fetchApi?: (query: CallRecordModalQuery) => Promise<any>;
- hideTechnicalIdColumns?: boolean;
- filterFields?: string[];
- }>(),
- {
- title: '通信记录',
- width: 1240,
- baseQuery: () => ({}),
- hideTechnicalIdColumns: true,
- filterFields: () => [
- 'personName',
- 'personPhone',
- 'otherName',
- 'otherPhone',
- 'callDirection',
- 'startDate',
- 'endDate',
- ],
- },
- );
- const emit = defineEmits<{
- (e: 'update:open', v: boolean): void;
- (e: 'jump'): void;
- }>();
- const loading = ref(false);
- const tableData = ref<any[]>([]);
- const { jumping, jumpToBehaviorSpacetime } = useTimeSeriesJump();
- const { createMessage } = useMessage();
- const filters = reactive({
- personName: '',
- personPhone: '',
- otherName: '',
- otherPhone: '',
- callDirection: '',
- startDate: '',
- endDate: '',
- orderKey: '',
- sort: '' as '' | 'ASC' | 'DESC',
- });
- const pagination = reactive({
- page: 1,
- limit: componentSetting.table.defaultPageSize,
- total: 0,
- });
- const totalPages = computed(() => {
- if (!pagination.total) return 1;
- return Math.max(1, Math.ceil(pagination.total / pagination.limit));
- });
- const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
- label: value,
- value,
- }));
- const filterSchema = [
- { field: 'personName', label: '本方姓名', type: 'input', placeholder: '请输入本方姓名', width: 180 },
- { field: 'personPhone', label: '本方号码', type: 'input', placeholder: '请输入本方号码', width: 180 },
- { field: 'otherName', label: '对方姓名', type: 'input', placeholder: '请输入对方姓名', width: 180 },
- { field: 'otherPhone', label: '对方号码', type: 'input', placeholder: '请输入对方号码', width: 180 },
- {
- field: 'callDirection',
- label: '主被叫',
- type: 'select',
- width: 160,
- options: [
- { label: '全部', value: '' },
- { label: '主叫', value: '主叫' },
- { label: '被叫', value: '被叫' },
- ],
- },
- { field: 'startDate', label: '开始日期', type: 'date', width: 170 },
- { field: 'endDate', label: '结束日期', type: 'date', width: 170 },
- {
- field: 'contactDateRange',
- label: '联系日期',
- type: 'date-range',
- startField: 'startDate',
- endField: 'endDate',
- width: 320,
- maxWidth: 360,
- rangePlaceholder: ['开始日期', '结束日期'],
- valueFormat: 'YYYY-MM-DD',
- },
- ] as const;
- const visibleFilterSchema = computed(() => {
- const visibleFields = new Set(props.filterFields);
- const showDateRange = visibleFields.has('startDate') && visibleFields.has('endDate');
- return filterSchema.filter((item) => {
- if (item.field === 'personName') return false;
- if (item.field === 'contactDateRange') return showDateRange;
- if (item.field === 'startDate' || item.field === 'endDate')
- return !showDateRange && visibleFields.has(item.field);
- return visibleFields.has(item.field);
- });
- });
- const technicalIdColumnKeys = ['personLibId'];
- const columns = [
- { title: '本方姓名', dataIndex: 'personName', key: 'personName', width: 120 },
- { title: '本方号码', dataIndex: 'personPhone', key: 'personPhone', width: 140 },
- { title: '对方姓名', dataIndex: 'otherName', key: 'otherName', width: 140 },
- { title: '对方号码', dataIndex: 'otherPhone', key: 'otherPhone', width: 140 },
- { title: '主被叫', dataIndex: 'callDirection', key: 'callDirection', width: 100 },
- { title: '通话日期时间', dataIndex: 'callDateTime', key: 'callDateTime', width: 180, sorter: true },
- { title: '本方基站名称', dataIndex: 'personStationName', key: 'personStationName', width: 220 },
- { title: '本方基站大区号', dataIndex: 'personRegionId', key: 'personRegionId', width: 140 },
- { title: '本方基站小区号', dataIndex: 'personCellId', key: 'personCellId', width: 140 },
- { title: '通话时长(秒)', dataIndex: 'duration', key: 'duration', width: 120, sorter: true },
- { title: '运营商', dataIndex: 'operatorId', key: 'operatorId', width: 120 },
- { title: '本方号码归属地', dataIndex: 'personPhoneArea', key: 'personPhoneArea', width: 160 },
- { title: '本方证件号码', dataIndex: 'personCertNo', key: 'personCertNo', width: 180 },
- { title: '本方基站经度', dataIndex: 'personStationLongitude', key: 'personStationLongitude', width: 140 },
- { title: '本方基站纬度', dataIndex: 'personStationLatitude', key: 'personStationLatitude', width: 140 },
- { title: '通话类型', dataIndex: 'callType', key: 'callType', width: 100 },
- { title: '对方号码归属地', dataIndex: 'otherPhoneArea', key: 'otherPhoneArea', width: 160 },
- { title: '对方证件号码', dataIndex: 'otherCertNo', key: 'otherCertNo', width: 180 },
- { title: '备注', dataIndex: 'remark', key: 'remark', width: 180 },
- ];
- const visibleColumns = computed(() => {
- if (!props.hideTechnicalIdColumns) return columns;
- return columns.filter((column) => !technicalIdColumnKeys.includes(String(column.key || column.dataIndex || '')));
- });
- const normalizeActionRecordId = (value: unknown) => {
- const text = String(value ?? '').trim();
- if (!text || text === '-' || text === 'null' || text === 'undefined') return undefined;
- return text;
- };
- const buildJumpQuery = (record?: CallRecord) => {
- const id = normalizeActionRecordId(record?.id);
- const personName = String(record?.personName || props.baseQuery?.personName || '').trim();
- return {
- hasLocal: 'N',
- tableName: 'call_record',
- tableNameEn: 'call_record',
- tableNameCn: '通信记录',
- id,
- personName: personName || undefined,
- personNames: personName ? [personName] : undefined,
- };
- };
- const handleJump = async (record?: CallRecord) => {
- await jumpToBehaviorSpacetime(buildJumpQuery(record), {
- onBeforeNavigate: () => {
- emit('jump');
- emit('update:open', false);
- },
- });
- };
- /** 下载关联的归档表格(Web 化:浏览器另存,不再调起本地应用打开) */
- const handleDownloadFile = async (record?: CallRecord) => {
- const fileId = resolveOfficeFileId(record);
- if (!fileId) {
- createMessage.warning('当前记录没有关联的源文件,无法下载');
- return;
- }
- try {
- await downloadOfficeFileById(fileId);
- } catch (error: any) {
- createMessage.error(getOpenOfficeFileErrorMessage(error, '文件下载失败'));
- }
- };
- const actionColumn = computed(() => ({
- title: '操作',
- dataIndex: 'action',
- key: 'action',
- width: 160,
- fixed: 'right',
- align: 'center',
- actions: (record: { raw?: CallRecord }) => {
- const rawRecord = record.raw;
- const query = buildJumpQuery(rawRecord);
- return [
- {
- label: '下载文件',
- // 不再按 fileId 置灰:没有来源文件时由 handleDownloadFile 明确提示原因
- onClick: () => void handleDownloadFile(rawRecord),
- },
- {
- label: '跳转',
- disabled: jumping.value || !query.id,
- onClick: () => void handleJump(rawRecord),
- },
- ];
- },
- }));
- const normalizeList = (resp: any): CallRecord[] => {
- if (Array.isArray(resp)) return resp;
- if (Array.isArray(resp?.list)) return resp.list;
- if (Array.isArray(resp?.data)) return resp.data;
- if (Array.isArray(resp?.data?.list)) return resp.data.list;
- if (Array.isArray(resp?.records)) return resp.records;
- if (Array.isArray(resp?.data?.records)) return resp.data.records;
- return [];
- };
- const normalizeTotal = (resp: any, listLen: number) => {
- const total = resp?.total ?? resp?.data?.total ?? resp?.count ?? resp?.data?.count ?? listLen;
- return Number.isFinite(Number(total)) ? Number(total) : listLen;
- };
- const normalizeCurrent = (resp: any, fallback: number) => {
- const current = resp?.current ?? resp?.data?.current ?? fallback;
- return Number.isFinite(Number(current)) && Number(current) > 0 ? Number(current) : fallback;
- };
- const normalizeSize = (resp: any, fallback: number) => {
- const size = resp?.size ?? resp?.data?.size ?? fallback;
- return Number.isFinite(Number(size)) && Number(size) > 0 ? Number(size) : fallback;
- };
- const toArrayByComma = (value?: string) => {
- const raw = String(value || '').trim();
- if (!raw) return undefined;
- return raw
- .split(',')
- .map((item) => item.trim())
- .filter(Boolean);
- };
- const toCommaText = (value?: string[]) => {
- return Array.isArray(value) && value.length ? value.join(',') : '';
- };
- const mapDirection = (value?: string) => {
- if (!value) return '-';
- if (value === 'CALL_IN') return '被叫';
- if (value === 'CALL_OUT') return '主叫';
- return value;
- };
- const mapCallType = (value?: string) => {
- if (!value) return '-';
- if (value === 'VOICE') return '语音';
- if (value === 'VIDEO') return '视频';
- if (value === 'SMSIN') return '接收';
- if (value === 'SMSOUT') return '发送';
- return value;
- };
- const buildQuery = (): CallRecordModalQuery => {
- const baseQuery = { ...(props.baseQuery || {}) };
- delete baseQuery.keyword;
- return {
- ...baseQuery,
- page: pagination.page,
- limit: pagination.limit,
- personName: filters.personName || props.baseQuery?.personName,
- personPhones: toArrayByComma(filters.personPhone) || props.baseQuery?.personPhones,
- otherName: filters.otherName || props.baseQuery?.otherName,
- otherPhones: toArrayByComma(filters.otherPhone) || props.baseQuery?.otherPhones,
- callDirection: filters.callDirection || props.baseQuery?.callDirection,
- callType: props.baseQuery?.callType,
- startDate: filters.startDate || props.baseQuery?.startDate,
- endDate: filters.endDate || props.baseQuery?.endDate,
- startTime: props.baseQuery?.startTime,
- endTime: props.baseQuery?.endTime,
- monthVal: props.baseQuery?.monthVal,
- orderKey: filters.orderKey || undefined,
- sort: filters.sort || undefined,
- };
- };
- const mapRow = (item: CallRecord, index: number) => ({
- rowKey: item.id ?? `${pagination.page}-${index}`,
- id: (pagination.page - 1) * pagination.limit + index + 1,
- recordId: item.id ?? '-',
- fileId: item.fileId ?? '-',
- sheetId: item.sheetId ?? '-',
- personName: item.personName || '-',
- personPhone: item.personPhone || '-',
- operatorId: item.operatorId || '-',
- personPhoneArea: item.personPhoneArea || '-',
- personCertNo: maskCertNo(item.personCertNo),
- personRegionId: item.personRegionId || '-',
- personCellId: item.personCellId || '-',
- personStationName: item.personStationName || '-',
- personStationLongitude: item.personStationLongitude || '-',
- personStationLatitude: item.personStationLatitude || '-',
- callDateTime: item.callDateTime || (item.callDate && item.callTime ? `${item.callDate} ${item.callTime}` : '-'),
- callDate: item.callDate || '-',
- callTime: item.callTime || '-',
- duration: item.duration ?? 0,
- callDirection: mapDirection(item.callDirection),
- callType: mapCallType(item.callType),
- otherName: item.otherName || '-',
- otherPhone: item.otherPhone || '-',
- otherPhoneArea: item.otherPhoneArea || '-',
- otherCertNo: maskCertNo(item.otherCertNo),
- remark: item.remark || '-',
- personLibId: item.personLibId || '-',
- festivalName: item.festivalName || '-',
- raw: item,
- });
- const syncFiltersFromBaseQuery = () => {
- filters.personName = props.baseQuery?.personName || '';
- filters.personPhone = toCommaText(props.baseQuery?.personPhones);
- filters.otherName = props.baseQuery?.otherName || '';
- filters.otherPhone = toCommaText(props.baseQuery?.otherPhones);
- filters.callDirection = props.baseQuery?.callDirection || '';
- filters.startDate = props.baseQuery?.startDate || '';
- filters.endDate = props.baseQuery?.endDate || '';
- filters.orderKey = '';
- filters.sort = '';
- };
- const fetchTable = async () => {
- loading.value = true;
- try {
- const query = buildQuery();
- const resp = props.fetchApi ? await props.fetchApi(query) : await getCallRecord(query as CallRecordQuery);
- const list = normalizeList(resp);
- tableData.value = list.map(mapRow);
- pagination.total = normalizeTotal(resp, list.length);
- pagination.page = normalizeCurrent(resp, pagination.page);
- pagination.limit = normalizeSize(resp, pagination.limit);
- } catch (error) {
- tableData.value = [];
- pagination.total = 0;
- } finally {
- loading.value = false;
- }
- };
- const handleSearch = async () => {
- pagination.page = 1;
- await fetchTable();
- };
- const handleReset = async () => {
- syncFiltersFromBaseQuery();
- pagination.page = 1;
- await fetchTable();
- };
- const handlePrevPage = async () => {
- if (loading.value || pagination.page <= 1) return;
- pagination.page -= 1;
- await fetchTable();
- };
- const handleNextPage = async () => {
- if (loading.value || pagination.page >= totalPages.value) return;
- pagination.page += 1;
- await fetchTable();
- };
- const handlePageChange = async (page: number, pageSize: number) => {
- if (loading.value) return;
- if (pageSize !== pagination.limit) {
- await handlePageSizeChange(pageSize);
- return;
- }
- if (page === pagination.page - 1) {
- await handlePrevPage();
- return;
- }
- if (page === pagination.page + 1) {
- await handleNextPage();
- return;
- }
- if (page === pagination.page) return;
- pagination.page = page;
- await fetchTable();
- };
- const handlePageSizeChange = async (size: number) => {
- pagination.limit = Number(size);
- pagination.page = 1;
- await fetchTable();
- };
- const handleSortChange = async (payload: { orderKey: string; sort: 'ASC' | 'DESC' }) => {
- filters.orderKey = payload.orderKey;
- filters.sort = payload.sort;
- pagination.page = 1;
- await fetchTable();
- };
- const initAndFetch = async () => {
- pagination.page = 1;
- syncFiltersFromBaseQuery();
- await fetchTable();
- };
- let initFetchScheduled = false;
- const scheduleInitAndFetch = () => {
- if (!props.open || initFetchScheduled) return;
- initFetchScheduled = true;
- nextTick(async () => {
- initFetchScheduled = false;
- if (!props.open) return;
- await initAndFetch();
- });
- };
- watch(
- () => props.open,
- (open) => {
- if (!open) {
- initFetchScheduled = false;
- return;
- }
- scheduleInitAndFetch();
- },
- );
- watch(
- () => props.baseQuery,
- () => {
- if (!props.open) return;
- scheduleInitAndFetch();
- },
- { deep: true },
- );
- defineExpose({
- reload: fetchTable,
- });
- </script>
- <style lang="less">
- .call-record-modal {
- .ant-modal {
- padding-bottom: 0;
- }
- .ant-modal-body > .scroll-container,
- .ant-modal-body > .scroll-container > .scrollbar__wrap,
- .ant-modal-body > .scroll-container > .scrollbar__wrap > .scrollbar__view,
- .ant-modal-body > .scroll-container > .scrollbar__wrap > .scrollbar__view > div {
- height: 100%;
- min-height: 0;
- }
- .ant-modal-body > .scroll-container > .scrollbar__wrap {
- overflow: hidden;
- }
- .ant-modal-body > .scroll-container > .scrollbar__wrap > .scrollbar__view {
- margin: 0 !important;
- }
- .ant-modal-body > .scroll-container > .scrollbar__bar.is-vertical,
- .ant-modal-body > .scroll-container > .is-vertical {
- display: none !important;
- opacity: 0 !important;
- pointer-events: none;
- }
- .ant-modal-content {
- border-radius: 14px;
- background: #ffffff;
- box-shadow: 0 18px 46px rgba(15, 23, 42, 0.16);
- }
- .ant-modal-header {
- padding: 12px 16px;
- background: @modal-header-bg-color;
- border-bottom-color: #e5e7eb;
- }
- .ant-modal-title {
- color: #0f172a;
- font-size: 16px;
- font-weight: 600;
- line-height: 24px;
- letter-spacing: 0.2px;
- }
- .ant-modal-close {
- top: 14px;
- right: 16px;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- width: auto !important;
- height: auto !important;
- border-radius: 8px;
- transition: background-color 0.2s ease;
- .jeesite-basic-modal-close {
- display: flex;
- align-items: center;
- gap: 2px;
- margin-right: 0;
- }
- &:hover {
- background: rgba(37, 99, 235, 0.08);
- }
- }
- .ant-modal-body {
- padding: 16px;
- }
- .ant-modal-footer {
- padding: 8px 16px 10px;
- border-top-color: #e5e7eb;
- background: #fff;
- }
- .query-table-wrap {
- gap: 12px;
- }
- .query-table-search {
- margin: 0;
- padding: 14px 16px;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
- background: #ffffff;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
- .label {
- color: #475569;
- font-size: 12px;
- font-weight: 600;
- }
- .ant-input,
- .ant-input-affix-wrapper,
- .ant-picker,
- .ant-select-selector {
- border-radius: 8px !important;
- }
- .search-actions {
- gap: 12px;
- .ant-btn {
- height: 36px;
- min-width: 76px;
- padding: 0 18px;
- border-radius: 18px;
- font-size: 15px;
- font-weight: 500;
- box-shadow: none;
- }
- .ant-btn-primary {
- border-color: #315efb;
- background: #315efb;
- }
- .ant-btn:not(.ant-btn-primary) {
- color: #1f2937;
- border-color: #d6dbe6;
- background: #ffffff;
- }
- }
- }
- .query-table-content {
- margin: 0;
- border-color: #dbe3ee;
- border-radius: 12px;
- background: #fff;
- box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
- .ant-table-thead > tr > th {
- background: #f8fafc;
- color: #334155;
- font-weight: 600;
- }
- .ant-table-tbody > tr:nth-child(even) > td {
- background: #fcfdff;
- }
- .ant-table-tbody > tr:hover > td {
- background: #eff6ff;
- }
- }
- .query-table-empty {
- color: #94a3b8;
- background: #ffffff;
- }
- .pagination-bar {
- .pagination-total {
- color: #64748b;
- }
- .pagination-size .ant-select-selector,
- .pagination-actions .ant-btn {
- border-radius: 6px;
- }
- .pagination-actions .ant-btn-primary {
- min-width: 36px;
- }
- }
- }
- </style>
|