| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140 |
- <template>
- <div ref="morningEveningCallPageRef" :class="prefixCls" class="morning-evening-call-page">
- <div class="page-header">
- <div class="page-title">晨晚通话</div>
- <div class="page-subtitle">围绕晨间和夜间时间窗口统计通话地点与记录,辅助识别异常生活轨迹和活动区域。</div>
- </div>
- <div class="filter-bar">
- <div class="filter-item">
- <span class="filter-label">选择对象:</span>
- <Select
- v-model:value="filters.target"
- mode="multiple"
- allow-clear
- max-tag-count="responsive"
- class="filter-input"
- :options="targetOptions"
- placeholder="请选择对象"
- />
- </div>
- <div class="filter-item">
- <span class="filter-label">通话时间:</span>
- <RangePicker
- v-model:value="filters.dateRange"
- class="filter-date"
- :format="DATE_FORMAT"
- :value-format="DATE_FORMAT"
- :placeholder="['开始日期', '结束日期']"
- />
- </div>
- <div class="filter-item">
- <span class="filter-label">早上时间:</span>
- <Input :value="'6点'" class="filter-time filter-time--fixed" disabled />
- <span class="filter-label">至</span>
- <Select v-model:value="filters.morningEnd" class="filter-time" :options="morningEndOptions" />
- </div>
- <div class="filter-item">
- <span class="filter-label">晚上时间:</span>
- <Select v-model:value="filters.eveningStart" class="filter-time" :options="eveningStartOptions" />
- <span class="filter-label">至</span>
- <Input :value="'6点'" class="filter-time filter-time--fixed" disabled />
- </div>
- <div class="filter-item">
- <span class="filter-label">通话次数:</span>
- <Select v-model:value="filters.countOp" class="filter-op" :options="opOptions" />
- <Input v-model:value="filters.countValue" class="filter-micro" :status="countValueStatus" />
- <span class="filter-unit">次</span>
- </div>
- <div class="filter-actions">
- <Button type="primary" :loading="queryLoading" @click="handleSearch">查询</Button>
- <Button @click="handleReset">重置</Button>
- </div>
- </div>
- <div v-if="targetList.length" class="target-tags">
- <div
- v-for="target in targetList"
- :key="target.id"
- class="target-tag"
- :class="{ active: selectedTarget === target.id }"
- @click="handleSelectTarget(target.id)"
- >
- {{ target.name }}
- </div>
- </div>
- <div class="content-panel">
- <div class="left-panel">
- <div class="panel-header">
- <div>
- <div class="panel-title">通话地点</div>
- <div class="panel-desc">{{ leftPanelDesc }}</div>
- </div>
- </div>
- <div v-if="!hasSearched" class="panel-empty panel-empty--state">
- <EmptyState
- variant="query"
- title="请点击查询"
- description="设置条件精细查询,也可点击查询展示所有信息"
- :min-height="220"
- />
- </div>
- <div v-else-if="queryLoading && !locationList.length" class="panel-empty">地点数据加载中...</div>
- <div v-else-if="locationList.length" class="location-list">
- <div
- v-for="location in locationList"
- :key="location.id"
- class="location-item"
- :class="{ active: selectedLocation === location.id }"
- @click="handleSelectLocation(location.id)"
- >
- <div class="location-name">{{ location.name }}</div>
- <div class="location-lac">LAC:{{ location.lac }}</div>
- <div class="location-cid">CID:{{ location.cid }}</div>
- <div class="location-count">通话:{{ location.count }}次</div>
- </div>
- </div>
- <div v-else class="panel-empty panel-empty--state">
- <EmptyState
- variant="compact"
- title="暂无地点数据"
- description="当前筛选条件下暂无早晚通话地统计结果"
- :min-height="220"
- />
- </div>
- </div>
- <div class="right-panel">
- <div class="panel-header panel-header--actions">
- <div>
- <div class="panel-title">通话记录</div>
- <div class="panel-desc">{{ rightPanelDesc }}</div>
- </div>
- </div>
- <div v-if="activeLocation" class="table-wrapper table-wrapper--detail">
- <BasicTable
- :columns="columns"
- :data-source="detailTableData"
- :loading="detailLoading"
- :pagination="false"
- :show-table-setting="false"
- :table-setting="{ setting: true, size: true, redo: false, fullScreen: false }"
- :index-column-props="indexColumnProps"
- :can-resize="true"
- :is-can-resize-parent="true"
- :resize-height-offset="32"
- row-key="rowKey"
- @register="handleMorningEveningCallTableRegister"
- />
- </div>
- <div v-else class="panel-empty panel-empty--state panel-empty--record">
- <EmptyState
- variant="compact"
- title="请选择地点"
- description="点击左侧通话地点后,右侧将使用通话底层接口展示对应记录。"
- :min-height="240"
- />
- </div>
- <div v-if="activeLocation" class="pagination-bar">
- <AnalysisPagination
- :total="pagination.total"
- :current="pagination.current"
- :page-size="pagination.pageSize"
- :page-size-options="pageSizeOptions"
- :loading="detailLoading"
- @change="handleDetailPaginationChange"
- />
- </div>
- </div>
- </div>
- </div>
- </template>
- <script lang="ts" setup name="ViewsMorningEveningCallIndex">
- import { computed, onMounted, ref } from 'vue';
- import { Select, Input, Button, DatePicker } from 'ant-design-vue';
- import AnalysisPagination from '@jeesite/core/components/AnalysisPagination/src/AnalysisPagination.vue';
- import { EmptyState } from '@jeesite/core/components/EmptyState';
- import { BasicTable } from '@jeesite/core/components/Table';
- import { createTableContext } from '@jeesite/core/components/Table/src/hooks/useTableContext';
- import type { BasicColumn } from '@jeesite/core/components/Table';
- import { useAnalysisRefreshPrompt } from '@jeesite/core/hooks/web/useAnalysisRefreshPrompt';
- import { useDesign } from '@jeesite/core/hooks/web/useDesign';
- import { useMessage } from '@jeesite/core/hooks/web/useMessage';
- import { useTableSkill } from '@jeesite/core/hooks/web/useTableSkill';
- import type { CallRecord } from '@jeesite/call/api/types/dto';
- import componentSetting from '@jeesite/core/settings/componentSetting';
- import { getPersonOptions } from '@jeesite/person/api/personApi';
- import { statFirstTrackEnLocal, statSecondTrackEnLocal } from '@jeesite/track/api/trackApi';
- import type { TrackEnLocalDTO } from '@jeesite/track/api/types/dto';
- import type { TrackEnLocalQuery } from '@jeesite/track/api/types/query';
- interface MorningTargetItem {
- id: string;
- name: string;
- locations: MorningLocationItem[];
- }
- interface MorningLocationItem {
- id: string;
- targetName: string;
- name: string;
- lac: string;
- cid: string;
- count: number;
- raw: TrackEnLocalDTO;
- }
- interface MorningCallRecordRow {
- rowKey: string;
- id: number;
- personName: string;
- personPhone: string;
- personStationName: string;
- personRegionId: string;
- personCellId: string;
- callDateTime: string;
- duration: number | string;
- callDirection: string;
- callType: string;
- otherName: string;
- otherPhone: string;
- festivalName: string;
- raw: CallRecord;
- }
- const { prefixCls } = useDesign('morning-evening-call');
- const { createMessage } = useMessage();
- const { queryLoading, runWithQueryLoading } = useTableSkill();
- const RangePicker = DatePicker.RangePicker;
- const DATE_FORMAT = 'YYYY-MM-DD';
- const morningEveningCallPageRef = ref<HTMLElement | null>(null);
- const morningEveningCallTableAction = ref<any>(null);
- const filters = ref({
- target: [] as string[],
- dateRange: undefined as [string, string] | undefined,
- morningStart: '6',
- morningEnd: '11',
- eveningStart: '20',
- eveningEnd: '6',
- countOp: 'ge',
- countValue: '',
- });
- const selectedTarget = ref<string | null>(null);
- const selectedLocation = ref<string | null>(null);
- const targetOptions = ref<Array<{ label: string; value: string }>>([]);
- const targetGroups = ref<MorningTargetItem[]>([]);
- const hasSearched = ref(false);
- const { syncAnalysisDataRefreshState } = useAnalysisRefreshPrompt({
- refresh: () => fetchSummaryData(),
- hasData: () => hasSearched.value && targetGroups.value.length > 0,
- });
- const positiveIntegerPattern = /^[1-9]\d*$/;
- const opOptions = [
- { label: '>=', value: 'ge' },
- { label: '<=', value: 'le' },
- ];
- const timeOptions = [
- { label: '0点', value: '0' },
- { label: '1点', value: '1' },
- { label: '2点', value: '2' },
- { label: '3点', value: '3' },
- { label: '4点', value: '4' },
- { label: '5点', value: '5' },
- { label: '6点', value: '6' },
- { label: '7点', value: '7' },
- { label: '8点', value: '8' },
- { label: '9点', value: '9' },
- { label: '10点', value: '10' },
- { label: '11点', value: '11' },
- { label: '12点', value: '12' },
- { label: '13点', value: '13' },
- { label: '14点', value: '14' },
- { label: '15点', value: '15' },
- { label: '16点', value: '16' },
- { label: '17点', value: '17' },
- { label: '18点', value: '18' },
- { label: '19点', value: '19' },
- { label: '20点', value: '20' },
- { label: '21点', value: '21' },
- { label: '22点', value: '22' },
- { label: '23点', value: '23' },
- ];
- const morningEndOptions = timeOptions.filter((item) => {
- const hour = Number(item.value);
- return hour >= 7 && hour <= 12;
- });
- const eveningStartOptions = timeOptions.filter((item) => {
- const hour = Number(item.value);
- return hour >= 20 || hour <= 5;
- });
- const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
- label: value,
- value: Number(value),
- }));
- const targetList = computed(() => targetGroups.value.map((item) => ({ id: item.id, name: item.name })));
- const activeTarget = computed(() => targetGroups.value.find((item) => item.id === selectedTarget.value) || null);
- const locationList = computed(() => activeTarget.value?.locations || []);
- const activeLocation = computed(() => locationList.value.find((item) => item.id === selectedLocation.value) || null);
- const normalizedCountValue = computed(() => String(filters.value.countValue ?? '').trim());
- const countValueStatus = computed<'error' | undefined>(() => {
- if (!normalizedCountValue.value) return undefined;
- return positiveIntegerPattern.test(normalizedCountValue.value) ? undefined : 'error';
- });
- const leftPanelDesc = computed(() => {
- if (!targetGroups.value.length) {
- return '从左侧地点切换查看晨间和夜间高频通话区域,辅助做固定活动范围研判。';
- }
- if (!activeTarget.value) {
- return '请选择对象后查看对应地点统计。';
- }
- return `${activeTarget.value.name} 当前命中 ${locationList.value.length} 个早晚通话地点。`;
- });
- const rightPanelDesc = computed(() => {
- if (!targetGroups.value.length) {
- return '请选择对象并定位左侧地点后,右侧将展示底层通话记录。';
- }
- if (!activeLocation.value) {
- return '请选择左侧地点后查看通话记录。';
- }
- if (detailLoading.value) {
- return `${activeLocation.value.name} 的底层通话记录加载中...`;
- }
- if (!pagination.value.total) {
- return `${activeLocation.value.name} 暂未查询到命中的底层通话记录。`;
- }
- return `${activeLocation.value.name} 共命中 ${pagination.value.total} 条底层通话记录。`;
- });
- const columns: BasicColumn[] = [
- { title: '本方姓名', dataIndex: 'personName', key: 'personName', width: 120, align: 'center' },
- { title: '本方号码', dataIndex: 'personPhone', key: 'personPhone', width: 140, align: 'center' },
- { title: '对方姓名', dataIndex: 'otherName', key: 'otherName', width: 120, align: 'center' },
- { title: '对方号码', dataIndex: 'otherPhone', key: 'otherPhone', width: 140, align: 'center' },
- { title: '通话日期时间', dataIndex: 'callDateTime', key: 'callDateTime', width: 180, align: 'center' },
- { title: '通话时长(秒)', dataIndex: 'duration', key: 'duration', width: 110, align: 'center' },
- { title: '主被叫', dataIndex: 'callDirection', key: 'callDirection', width: 90, align: 'center' },
- { title: '通话类型', dataIndex: 'callType', key: 'callType', width: 90, align: 'center' },
- { title: '基站名称', dataIndex: 'personStationName', key: 'personStationName', width: 220 },
- { title: 'LAC', dataIndex: 'personRegionId', key: 'personRegionId', width: 100, align: 'center' },
- { title: 'CID', dataIndex: 'personCellId', key: 'personCellId', width: 100, align: 'center' },
- ];
- const indexColumnProps: BasicColumn = {
- width: 60,
- align: 'center',
- customRender: ({ index }: { index: number }) => {
- return (pagination.value.current - 1) * pagination.value.pageSize + index + 1;
- },
- };
- const handleMorningEveningCallTableRegister = (action: any) => {
- morningEveningCallTableAction.value = action;
- };
- const getMorningEveningCallTableColumns = (...args: any[]) =>
- morningEveningCallTableAction.value?.getColumns?.(...args) || [];
- createTableContext({
- wrapRef: morningEveningCallPageRef,
- getBindValues: computed(() => ({
- columns,
- showIndexColumn: true,
- rowSelection: undefined,
- isTreeTable: false,
- })),
- getDefaultRowSelection: () => morningEveningCallTableAction.value?.getDefaultRowSelection?.(),
- getColumns: getMorningEveningCallTableColumns,
- getCacheColumns: () => morningEveningCallTableAction.value?.getCacheColumns?.() || [],
- setColumns: (nextColumns: any) => morningEveningCallTableAction.value?.setColumns?.(nextColumns),
- setProps: (props: any) => morningEveningCallTableAction.value?.setProps?.(props),
- } as any);
- const detailLoading = ref(false);
- const recordList = ref<CallRecord[]>([]);
- const pagination = ref({
- current: 1,
- pageSize: componentSetting.table.defaultPageSize,
- total: 0,
- jumpPage: '1',
- });
- const detailRequestSerial = ref(0);
- const detailTotalPages = computed(() => {
- if (!pagination.value.total) return 1;
- return Math.max(1, Math.ceil(pagination.value.total / pagination.value.pageSize));
- });
- const detailTableData = computed<MorningCallRecordRow[]>(() => {
- const start = (pagination.value.current - 1) * pagination.value.pageSize;
- const pageList = recordList.value.slice(start, start + pagination.value.pageSize);
- return pageList.map((item, index) => mapDetailRow(item, start + index));
- });
- const fallbackText = (value: string | number | null | undefined) => {
- if (value === 0) return '0';
- if (value === null || value === undefined || value === '') return '-';
- return String(value);
- };
- const normalizeCallRecordList = (resp: any): CallRecord[] => {
- if (Array.isArray(resp)) return resp;
- if (Array.isArray(resp?.records)) return resp.records;
- if (Array.isArray(resp?.data?.records)) return resp.data.records;
- if (Array.isArray(resp?.list)) return resp.list;
- if (Array.isArray(resp?.data?.list)) return resp.data.list;
- if (Array.isArray(resp?.data)) return resp.data;
- return [];
- };
- const normalizeCallRecordTotal = (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 resolveCountValue = (showMessage = false) => {
- if (!normalizedCountValue.value) return undefined;
- if (positiveIntegerPattern.test(normalizedCountValue.value)) {
- return Number(normalizedCountValue.value);
- }
- if (showMessage) {
- createMessage.warning('通话次数请输入大于 0 的整数');
- }
- return null;
- };
- const parseTimeValue = (value?: string) => {
- const numericValue = Number(value);
- return Number.isFinite(numericValue) ? numericValue : undefined;
- };
- const normalizeTimeValue = (value?: string | number | null, endOfHour = false) => {
- if (value === null || value === undefined || value === '') return undefined;
- const numericValue = typeof value === 'number' ? value : Number(value);
- if (!Number.isFinite(numericValue)) return undefined;
- const hours = Math.max(0, Math.min(23, numericValue));
- return `${String(hours).padStart(2, '0')}:${endOfHour ? '59:59' : '00:00'}`;
- };
- const matchesCountFilter = (count: number, compareValue?: number) => {
- if (compareValue === undefined) return true;
- switch (filters.value.countOp) {
- case 'gt':
- return count > compareValue;
- case 'lt':
- return count < compareValue;
- case 'le':
- return count <= compareValue;
- case 'eq':
- return count === compareValue;
- case 'ge':
- default:
- return count >= compareValue;
- }
- };
- const normalizeMapPayload = (resp: any): Record<string, TrackEnLocalDTO[]> => {
- if (resp?.data && typeof resp.data === 'object' && !Array.isArray(resp.data)) return resp.data;
- if (resp && typeof resp === 'object' && !Array.isArray(resp)) return resp;
- return {};
- };
- const mapLocationItem = (item: TrackEnLocalDTO, fallbackName: string, index: number): MorningLocationItem => {
- const targetName = item.personName || fallbackName || '-';
- const stationName = fallbackText(item.personStationName);
- const lac = fallbackText(item.personRegionId);
- const cid = fallbackText(item.personCellId);
- const count = Number.isFinite(Number(item.dayNightTotal)) ? Number(item.dayNightTotal) : 0;
- return {
- id: [targetName, stationName, lac, cid, index].join('||'),
- targetName,
- name: stationName,
- lac,
- cid,
- count,
- raw: item,
- };
- };
- const normalizeTargetGroups = (resp: any, compareValue?: number): MorningTargetItem[] => {
- const mapPayload = normalizeMapPayload(resp);
- return Object.entries(mapPayload)
- .map(([personName, list], groupIndex) => {
- const locations = (Array.isArray(list) ? list : [])
- .map((item, index) => mapLocationItem(item, personName, groupIndex * 1000 + index))
- .filter((item) => matchesCountFilter(item.count, compareValue))
- .sort((prev, next) => next.count - prev.count);
- return {
- id: `${personName || 'unknown'}-${groupIndex}`,
- name: personName || locations[0]?.targetName || '-',
- locations,
- };
- })
- .filter((item) => item.locations.length);
- };
- const buildDetailTimeRanges = () => {
- const ranges: Array<{ startTime?: string; endTime?: string }> = [];
- const appendRange = (start?: string, end?: string) => {
- const startHour = parseTimeValue(start);
- const endHour = parseTimeValue(end);
- if (startHour === undefined || endHour === undefined) return;
- if (startHour <= endHour) {
- ranges.push({
- startTime: normalizeTimeValue(startHour),
- endTime: normalizeTimeValue(endHour, true),
- });
- return;
- }
- ranges.push(
- {
- startTime: normalizeTimeValue(startHour),
- endTime: '23:59:59',
- },
- {
- startTime: '00:00:00',
- endTime: normalizeTimeValue(endHour, true),
- },
- );
- };
- appendRange(filters.value.morningStart, filters.value.morningEnd);
- appendRange(filters.value.eveningStart, filters.value.eveningEnd);
- return Array.from(new Map(ranges.map((item) => [`${item.startTime || ''}-${item.endTime || ''}`, item])).values());
- };
- const buildDetailBaseQuery = (location: MorningLocationItem): Partial<TrackEnLocalQuery> => {
- return {
- personName: location.raw.personName || activeTarget.value?.name || undefined,
- personRegionId: location.raw.personRegionId || undefined,
- personCellId: location.raw.personCellId || undefined,
- personStationName: location.raw.personStationName || undefined,
- startDate: filters.value.dateRange?.[0] || undefined,
- endDate: filters.value.dateRange?.[1] || undefined,
- morningTime: filters.value.morningEnd || undefined,
- nightTime: filters.value.eveningEnd || undefined,
- orderKey: 'callDateTime',
- sort: 'DESC',
- } as Partial<TrackEnLocalQuery>;
- };
- const buildRecordKey = (item: CallRecord) => {
- if (item.id !== undefined && item.id !== null) return `id-${item.id}`;
- return [
- item.personPhone || '',
- item.otherPhone || '',
- item.callDateTime || `${item.callDate || ''} ${item.callTime || ''}`,
- item.personRegionId || '',
- item.personCellId || '',
- item.callDirection || '',
- ].join('||');
- };
- const sortRecordsDesc = (list: CallRecord[]) => {
- return [...list].sort((prev, next) => {
- const prevTime = prev.callDateTime || `${prev.callDate || ''} ${prev.callTime || ''}`;
- const nextTime = next.callDateTime || `${next.callDate || ''} ${next.callTime || ''}`;
- return String(nextTime).localeCompare(String(prevTime));
- });
- };
- const mapCallDirection = (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 '视频';
- return value;
- };
- const mapDetailRow = (item: CallRecord, index: number): MorningCallRecordRow => ({
- rowKey: String(item.id ?? buildRecordKey(item) ?? index),
- id: index + 1,
- personName: item.personName || '-',
- personPhone: item.personPhone || '-',
- personStationName: item.personStationName || '-',
- personRegionId: item.personRegionId || '-',
- personCellId: item.personCellId || '-',
- callDateTime: item.callDateTime || (item.callDate && item.callTime ? `${item.callDate} ${item.callTime}` : '-'),
- duration: item.duration ?? 0,
- callDirection: mapCallDirection(item.callDirection),
- callType: mapCallType(item.callType),
- otherName: item.otherName || '-',
- otherPhone: item.otherPhone || '-',
- festivalName: item.festivalName || '-',
- raw: item,
- });
- const resetDetailTable = () => {
- recordList.value = [];
- pagination.value.total = 0;
- pagination.value.current = 1;
- pagination.value.jumpPage = '1';
- };
- const fetchAllCallRecords = async (baseQuery: Partial<TrackEnLocalQuery>) => {
- const pageSize = 500;
- const maxPages = 100;
- const results: CallRecord[] = [];
- for (let page = 1; page <= maxPages; page += 1) {
- const resp = await statSecondTrackEnLocal({
- ...(baseQuery as TrackEnLocalQuery),
- page,
- limit: pageSize,
- });
- const list = normalizeCallRecordList(resp);
- results.push(...list);
- const total = normalizeCallRecordTotal(resp, list.length);
- if (!list.length || list.length < pageSize || results.length >= total) {
- break;
- }
- }
- return results;
- };
- const syncSelection = () => {
- if (!targetGroups.value.length) {
- selectedTarget.value = null;
- selectedLocation.value = null;
- return;
- }
- if (!targetGroups.value.some((item) => item.id === selectedTarget.value)) {
- selectedTarget.value = targetGroups.value[0].id;
- }
- const currentLocations = targetGroups.value.find((item) => item.id === selectedTarget.value)?.locations || [];
- if (!currentLocations.some((item) => item.id === selectedLocation.value)) {
- selectedLocation.value = currentLocations[0]?.id || null;
- }
- };
- const buildQuery = (countValue?: number): TrackEnLocalQuery => {
- const personNames = filters.value.target.length ? [...filters.value.target] : undefined;
- return {
- page: 1,
- limit: 200,
- startDate: filters.value.dateRange?.[0] || undefined,
- endDate: filters.value.dateRange?.[1] || undefined,
- personNames,
- dayNightTotal:
- countValue !== undefined && ['ge', 'gt', 'eq'].includes(filters.value.countOp) ? countValue : undefined,
- // Backend currently accepts morning/night threshold cutoffs, so the UI range maps to the end/start boundaries.
- morningTime: parseTimeValue(filters.value.morningEnd),
- nightTime: parseTimeValue(filters.value.eveningEnd),
- };
- };
- const fetchSummaryData = async () => {
- const countValue = resolveCountValue(true);
- if (countValue === null) return;
- try {
- await runWithQueryLoading(async () => {
- const resp = await statFirstTrackEnLocal(buildQuery(countValue));
- targetGroups.value = normalizeTargetGroups(resp, countValue);
- hasSearched.value = true;
- syncAnalysisDataRefreshState(targetGroups.value.length > 0);
- resetDetailTable();
- syncSelection();
- });
- await fetchDetailTable();
- } catch (error) {
- targetGroups.value = [];
- hasSearched.value = true;
- syncAnalysisDataRefreshState(false);
- selectedTarget.value = null;
- selectedLocation.value = null;
- resetDetailTable();
- createMessage.warning('早晚通话地第一层接口调用失败');
- }
- };
- const handleSearch = async () => {
- await fetchSummaryData();
- };
- const handleReset = async () => {
- filters.value = {
- target: [],
- dateRange: undefined,
- morningStart: '6',
- morningEnd: '11',
- eveningStart: '20',
- eveningEnd: '6',
- countOp: 'ge',
- countValue: '',
- };
- await fetchSummaryData();
- };
- const handleSelectTarget = async (targetId: string) => {
- selectedTarget.value = targetId;
- selectedLocation.value = activeTarget.value?.locations[0]?.id || null;
- pagination.value.current = 1;
- pagination.value.jumpPage = '1';
- await fetchDetailTable();
- };
- const handleSelectLocation = async (locationId: string) => {
- selectedLocation.value = locationId;
- pagination.value.current = 1;
- pagination.value.jumpPage = '1';
- await fetchDetailTable();
- };
- const fetchDetailTable = async () => {
- const currentLocation = activeLocation.value;
- if (!currentLocation) {
- resetDetailTable();
- return;
- }
- const requestSerial = ++detailRequestSerial.value;
- detailLoading.value = true;
- try {
- const baseQuery = buildDetailBaseQuery(currentLocation);
- // const timeRanges = buildDetailTimeRanges();
- // const queryList = timeRanges.length ? timeRanges : [{ startTime: undefined, endTime: undefined }];
- const responses = await fetchAllCallRecords({ ...baseQuery });
- if (requestSerial !== detailRequestSerial.value) return;
- const mergedMap = new Map<string, CallRecord>();
- responses.flat().forEach((item) => {
- mergedMap.set(buildRecordKey(item), item);
- });
- recordList.value = sortRecordsDesc(Array.from(mergedMap.values()));
- pagination.value.total = recordList.value.length;
- pagination.value.current = 1;
- pagination.value.jumpPage = '1';
- } catch (error) {
- if (requestSerial !== detailRequestSerial.value) return;
- resetDetailTable();
- createMessage.warning('通话底层接口调用失败');
- } finally {
- if (requestSerial === detailRequestSerial.value) {
- detailLoading.value = false;
- }
- }
- };
- const handleDetailPrevPage = () => {
- if (detailLoading.value || pagination.value.current <= 1) return;
- pagination.value.current -= 1;
- pagination.value.jumpPage = String(pagination.value.current);
- };
- const handleDetailNextPage = () => {
- if (detailLoading.value || pagination.value.current >= detailTotalPages.value) return;
- pagination.value.current += 1;
- pagination.value.jumpPage = String(pagination.value.current);
- };
- const handleDetailPaginationChange = (page: number, pageSize: number) => {
- if (detailLoading.value) return;
- if (pageSize !== pagination.value.pageSize) {
- pagination.value.pageSize = pageSize;
- handleDetailPageSizeChange();
- return;
- }
- if (page === pagination.value.current - 1) {
- handleDetailPrevPage();
- return;
- }
- if (page === pagination.value.current + 1) {
- handleDetailNextPage();
- return;
- }
- if (page === pagination.value.current) return;
- pagination.value.jumpPage = String(page);
- handleDetailJumpPage();
- };
- const handleDetailPageSizeChange = () => {
- pagination.value.current = 1;
- pagination.value.jumpPage = '1';
- };
- const handleDetailJumpPage = () => {
- const next = Number(pagination.value.jumpPage);
- if (!Number.isInteger(next) || next < 1) {
- createMessage.warning('请输入正确页码');
- pagination.value.jumpPage = String(pagination.value.current);
- return;
- }
- pagination.value.current = Math.min(next, detailTotalPages.value);
- pagination.value.jumpPage = String(pagination.value.current);
- };
- const fetchTargetOptions = async () => {
- try {
- targetOptions.value = await getPersonOptions(false);
- } catch (error) {
- targetOptions.value = [];
- }
- };
- onMounted(async () => {
- await fetchTargetOptions();
- });
- </script>
- <style lang="less">
- @import '../styles/trans-analysis-table-ui.less';
- @prefix-cls: ~'jeesite-morning-evening-call';
- .@{prefix-cls}.morning-evening-call-page {
- .analysis-page-frame();
- .filter-input {
- width: 180px;
- }
- .filter-micro {
- width: 50px;
- }
- .filter-op {
- width: 84px;
- min-width: 84px;
- }
- .filter-time {
- width: 70px;
- }
- .filter-time--fixed {
- :deep(.ant-input[disabled]) {
- color: #111827;
- background: #fff;
- cursor: default;
- }
- }
- .filter-unit {
- color: #6b7280;
- font-size: 12px;
- }
- .filter-date {
- width: 360px;
- }
- .target-tags {
- display: flex;
- gap: 12px;
- margin-top: 0;
- flex-shrink: 0;
- flex-wrap: wrap;
- padding: 8px;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
- background: #fff;
- box-sizing: border-box;
- }
- .target-tag {
- padding: 8px 16px;
- background: #fff;
- border: 1px solid #e5e7eb;
- border-radius: 999px;
- font-size: 13px;
- color: #4b5563;
- cursor: pointer;
- transition: all 0.2s ease;
- &:hover {
- border-color: #93c5fd;
- }
- &.active {
- background: #eef6ff;
- border-color: #93c5fd;
- color: #2563eb;
- }
- }
- .content-panel {
- display: flex;
- gap: 2px;
- }
- .left-panel {
- width: 260px;
- flex: 0 0 auto;
- .analysis-card-shell();
- }
- .right-panel {
- .analysis-card-shell();
- flex: 1;
- min-width: 0;
- }
- .panel-header {
- padding: 0 0 8px;
- border-bottom: 0;
- flex: 0 0 auto;
- width: 100%;
- box-sizing: border-box;
- }
- .panel-header--actions {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- > div:first-child {
- flex: 1 1 auto;
- min-width: 0;
- }
- }
- .panel-title {
- font-size: 14px;
- font-weight: 600;
- color: #111827;
- line-height: 22px;
- }
- .panel-desc {
- margin-top: 4px;
- color: #6b7280;
- font-size: 12px;
- line-height: 20px;
- }
- .location-list {
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- padding: 0;
- gap: 0;
- width: 100%;
- }
- .location-item {
- padding: 10px 0;
- cursor: pointer;
- transition: all 0.2s ease;
- border: 0;
- border-bottom: 1px solid #eef2f7;
- border-radius: 0;
- background: transparent;
- &:hover {
- background: rgba(245, 247, 255, 0.72);
- }
- &.active {
- background: rgba(239, 246, 255, 0.72);
- }
- .location-name {
- font-size: 12px;
- color: #374151;
- margin-bottom: 4px;
- }
- .location-lac,
- .location-cid {
- font-size: 11px;
- color: #6b7280;
- }
- .location-count {
- font-size: 11px;
- color: #6b7280;
- margin-top: 6px;
- }
- }
- .panel-empty {
- display: flex;
- align-items: center;
- justify-content: center;
- flex: 1 1 auto;
- min-height: 0;
- padding: 16px;
- color: #6b7280;
- font-size: 12px;
- }
- .panel-empty--state {
- padding: 20px 16px;
- }
- .panel-empty--record {
- border-top: 0;
- }
- .table-wrapper {
- flex: 1 1 auto;
- min-height: 0;
- margin: 0;
- border: 0;
- border-radius: 0;
- overflow: hidden;
- width: 100%;
- }
- .table-wrapper .jeesite-basic-table {
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- .table-wrapper .ant-table-wrapper {
- height: 100%;
- min-height: 0;
- }
- .table-wrapper .ant-spin-nested-loading,
- .table-wrapper .ant-spin-container {
- height: 100%;
- min-height: 0;
- }
- .table-wrapper .ant-spin-container,
- .table-wrapper .ant-table,
- .table-wrapper .ant-table-container {
- height: 100%;
- min-height: 0;
- }
- .table-wrapper .ant-spin-container,
- .table-wrapper .ant-table,
- .table-wrapper .ant-table-container {
- display: flex;
- flex-direction: column;
- }
- .table-wrapper .ant-table-body {
- max-height: 100% !important;
- }
- .table-wrapper .ant-table-body::-webkit-scrollbar {
- width: 8px;
- height: 8px;
- }
- .table-wrapper .ant-table-body::-webkit-scrollbar-thumb {
- background: rgba(148, 163, 184, 0.6);
- border-radius: 999px;
- }
- .table-wrapper .ant-table-body::-webkit-scrollbar-track {
- background: transparent;
- }
- .table-wrapper .ant-table-cell {
- font-size: 12px;
- }
- .pagination-bar {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 8px;
- color: #6b7280;
- font-size: 12px;
- flex: 0 0 auto;
- flex-wrap: wrap;
- padding: 0;
- background: transparent;
- }
- .pagination-total {
- margin-right: 0;
- white-space: nowrap;
- }
- .pagination-size {
- display: flex;
- align-items: center;
- gap: 6px;
- white-space: nowrap;
- }
- .pagination-actions {
- display: flex;
- gap: 6px;
- align-items: center;
- white-space: nowrap;
- }
- .pagination-jump {
- display: flex;
- align-items: center;
- gap: 6px;
- white-space: nowrap;
- }
- .pagination-input {
- width: 48px;
- }
- :deep(.ant-select-selector),
- :deep(.ant-input),
- :deep(.ant-picker),
- :deep(.ant-btn) {
- border-radius: 8px;
- }
- @media screen and (max-width: 900px) {
- .content-panel {
- flex-direction: column;
- }
- .left-panel {
- width: 100%;
- }
- }
- @media screen and (max-width: 992px) {
- .filter-actions {
- width: 100%;
- margin-left: 0;
- }
- .filter-input,
- .filter-date {
- width: 100%;
- }
- }
- }
- </style>
|