| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- <template>
- <div :class="prefixCls" class="base-station-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 v-model:value="filters.stationName" class="filter-input" placeholder="请输入基站名称" />
- </div>
- <div class="filter-actions">
- <Button type="primary" :loading="queryLoading" @click="handleSearch">查询</Button>
- <Button @click="handleReset">重置</Button>
- </div>
- </div>
- <div class="content-panel">
- <div class="table-card">
- <div class="panel-header">
- <div>
- <div class="panel-title">基站命中列表</div>
- <div class="panel-desc">
- {{
- tableData.length
- ? `当前命中 ${pagination.total} 条基站记录,可查看坐标位置并继续下钻。`
- : '展示对象在不同基站的通信次数、LAC/CID 和时间区间,适合做高频位置聚类的前置筛查。'
- }}
- </div>
- </div>
- </div>
- <div class="table-panel">
- <div v-if="!hasSearched" class="query-state-panel">
- <EmptyState variant="query" title="请点击查询" description="设置条件精细查询,也可点击查询展示所有信息" />
- </div>
- <div v-else class="table-wrapper">
- <BasicTable
- :columns="columns"
- :action-column="actionColumn"
- :data-source="tableData"
- :loading="queryLoading"
- :pagination="false"
- :showIndexColumn="false"
- :table-setting-store="false"
- :show-table-setting="false"
- :table-setting="{ setting: true, size: true, redo: false, fullScreen: false }"
- :can-resize="true"
- :is-can-resize-parent="true"
- :resize-height-offset="32"
- row-key="rowKey"
- >
- <template #action="{ record }">
- <Button type="link" size="small" @click="handleViewDetail(record)">查看详情</Button>
- <Button type="link" size="small" @click="handleOpenMap(record)">地图位置</Button>
- </template>
- </BasicTable>
- </div>
- </div>
- <div v-if="hasSearched" class="pagination-bar">
- <AnalysisPagination
- :total="pagination.total"
- :current="pagination.current"
- :page-size="pagination.pageSize"
- :page-size-options="pageSizeOptions"
- :loading="queryLoading"
- @change="handlePaginationChange"
- />
- </div>
- </div>
- <BaseStationDetailModal v-model:open="detailModalOpen" :base-query="detailBaseQuery" />
- <BaseStationMapModal v-model:open="mapModalOpen" :title="mapModalTitle" :stations="mapModalStations" />
- </div>
- </div>
- </template>
- <script lang="ts" setup name="ViewsBaseStationIndex">
- 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 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 { mergeDetailDateRange, pickDetailValue } from '@jeesite/core/utils/detailQuery';
- import componentSetting from '@jeesite/core/settings/componentSetting';
- import { getPersonOptions } from '@jeesite/person/api/personApi';
- import { statFirstTrackCellTower } from '@jeesite/track/api/trackApi';
- import type { TrackCellTowerDTO } from '@jeesite/track/api/types/dto';
- import type { TrackCellTowerQuery } from '@jeesite/track/api/types/query';
- import BaseStationDetailModal from '../components/BaseStationDetailModal.vue';
- import BaseStationMapModal from '../components/BaseStationMapModal.vue';
- interface BaseStationRow {
- rowKey: string;
- id: number;
- stationName: string;
- target: string;
- callCount: number | string;
- lac: string;
- cid: string;
- timeRange: string;
- raw: TrackCellTowerDTO;
- }
- type RangeValue = [string, string] | undefined;
- const DEFAULT_SORT_KEY = 'callCount';
- const DEFAULT_SORT_ORDER = 'DESC' as const;
- const { prefixCls } = useDesign('base-station');
- const { createMessage } = useMessage();
- const { queryLoading, runWithQueryLoading } = useTableSkill();
- const RangePicker = DatePicker.RangePicker;
- const DATE_FORMAT = 'YYYY-MM-DD';
- const filters = ref({
- target: [] as string[],
- dateRange: undefined as RangeValue,
- stationName: '',
- });
- const targetOptions = ref<Array<{ label: string; value: string }>>([]);
- const tableData = ref<BaseStationRow[]>([]);
- const hasSearched = ref(false);
- const { syncAnalysisDataRefreshState } = useAnalysisRefreshPrompt({
- refresh: () => fetchTable(),
- hasData: () => hasSearched.value && tableData.value.length > 0,
- });
- const detailModalOpen = ref(false);
- const detailBaseQuery = ref<Partial<TrackCellTowerQuery>>({});
- const mapModalOpen = ref(false);
- const mapModalTitle = ref('基站地图位置');
- const mapModalStations = ref<TrackCellTowerDTO[]>([]);
- const pagination = ref({
- current: 1,
- pageSize: componentSetting.table.defaultPageSize,
- total: 0,
- jumpPage: '1',
- });
- const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
- label: value,
- value: Number(value),
- }));
- const totalPages = computed(() => {
- if (!pagination.value.total) return 1;
- return Math.max(1, Math.ceil(pagination.value.total / pagination.value.pageSize));
- });
- const columns: BasicColumn[] = [
- { title: '序号', dataIndex: 'id', key: 'id', width: 70, align: 'center' },
- { title: '基站名称', dataIndex: 'stationName', key: 'stationName', width: 320, align: 'center' },
- { title: '被调查对象', dataIndex: 'target', key: 'target', width: 120, align: 'center' },
- { title: '通信次数', dataIndex: 'callCount', key: 'callCount', width: 100, align: 'center' },
- { title: 'LAC', dataIndex: 'lac', key: 'lac', width: 100, align: 'center' },
- { title: 'CID', dataIndex: 'cid', key: 'cid', width: 100, align: 'center' },
- { title: '通信时间区间', dataIndex: 'timeRange', key: 'timeRange', width: 220, align: 'center' },
- ];
- const actionColumn = {
- title: '操作',
- dataIndex: 'action',
- key: 'action',
- width: 220,
- align: 'center' as const,
- fixed: 'right' as const,
- slot: 'action',
- };
- const normalizeList = (resp: any): TrackCellTowerDTO[] => {
- if (Array.isArray(resp)) return resp;
- if (Array.isArray(resp?.list)) return resp.list;
- if (Array.isArray(resp?.records)) return resp.records;
- if (Array.isArray(resp?.data)) return resp.data;
- if (Array.isArray(resp?.data?.list)) return resp.data.list;
- 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 fallbackText = (value: string | number | null | undefined) => {
- if (value === 0) return '0';
- if (value === null || value === undefined || value === '') return '-';
- return String(value);
- };
- const formatTimeRange = (startDate?: string, endDate?: string) => {
- if (startDate && endDate) return `${startDate} - ${endDate}`;
- return fallbackText(startDate || endDate);
- };
- const mapRow = (item: TrackCellTowerDTO, index: number): BaseStationRow => ({
- rowKey:
- [item.personName, item.personStationName, item.personRegionId, item.personCellId, index]
- .filter(Boolean)
- .join('||') || `${pagination.value.current}-${index}`,
- id: (pagination.value.current - 1) * pagination.value.pageSize + index + 1,
- stationName: fallbackText(item.personStationName),
- target: fallbackText(item.personName),
- callCount: Number.isFinite(Number(item.callCount)) ? Number(item.callCount) : fallbackText(item.callCount),
- lac: fallbackText(item.personRegionId),
- cid: fallbackText(item.personCellId),
- timeRange: formatTimeRange(item.startDate, item.endDate),
- raw: item,
- });
- const parseCount = (value: number | string | null | undefined) => {
- const numericValue = Number(value);
- return Number.isFinite(numericValue) ? numericValue : 0;
- };
- const sortList = (list: TrackCellTowerDTO[]) =>
- [...list].sort((left, right) => {
- const callCountDiff = parseCount(right.callCount) - parseCount(left.callCount);
- if (callCountDiff !== 0) return callCountDiff;
- return String(left.personName || left.personStationName || '').localeCompare(
- String(right.personName || right.personStationName || ''),
- );
- });
- const buildQuery = (): TrackCellTowerQuery => {
- const personNames = filters.value.target.length ? [...filters.value.target] : undefined;
- return {
- page: pagination.value.current,
- limit: pagination.value.pageSize,
- startDate: filters.value.dateRange?.[0] || undefined,
- endDate: filters.value.dateRange?.[1] || undefined,
- personNames,
- personStationName: filters.value.stationName.trim() || undefined,
- orderKey: DEFAULT_SORT_KEY,
- sort: DEFAULT_SORT_ORDER,
- };
- };
- const buildDetailBaseQuery = (record: BaseStationRow): Partial<TrackCellTowerQuery> => {
- const currentQuery = buildQuery();
- const { startDate, endDate } = mergeDetailDateRange(
- {
- startDate: currentQuery.startDate,
- endDate: currentQuery.endDate,
- },
- {
- startDate: record.raw.startDate,
- endDate: record.raw.endDate,
- },
- );
- return {
- startDate,
- endDate,
- personName: pickDetailValue(record.raw.personName, currentQuery.personNames?.[0]),
- personStationName: pickDetailValue(record.raw.personStationName, currentQuery.personStationName),
- personRegionId: pickDetailValue(record.raw.personRegionId),
- personCellId: pickDetailValue(record.raw.personCellId),
- };
- };
- const fetchTable = async () => {
- await runWithQueryLoading(async () => {
- const resp = await statFirstTrackCellTower(buildQuery());
- const list = sortList(normalizeList(resp));
- tableData.value = list.map(mapRow);
- hasSearched.value = true;
- syncAnalysisDataRefreshState(list.length > 0);
- pagination.value.total = normalizeTotal(resp, list.length);
- const current = Number((resp as any)?.current ?? (resp as any)?.data?.current ?? pagination.value.current);
- const size = Number((resp as any)?.size ?? (resp as any)?.data?.size ?? pagination.value.pageSize);
- if (Number.isFinite(current) && current > 0) pagination.value.current = current;
- if (Number.isFinite(size) && size > 0) pagination.value.pageSize = size;
- pagination.value.jumpPage = String(pagination.value.current);
- });
- };
- const handleSearch = async () => {
- pagination.value.current = 1;
- await fetchTable();
- };
- const handleReset = async () => {
- filters.value = {
- target: [],
- dateRange: undefined,
- stationName: '',
- };
- pagination.value.current = 1;
- await fetchTable();
- };
- const handlePrevPage = async () => {
- if (queryLoading.value || pagination.value.current <= 1) return;
- pagination.value.current -= 1;
- await fetchTable();
- };
- const handleNextPage = async () => {
- if (queryLoading.value || pagination.value.current >= totalPages.value) return;
- pagination.value.current += 1;
- await fetchTable();
- };
- const handlePaginationChange = async (page: number, pageSize: number) => {
- if (queryLoading.value) return;
- if (pageSize !== pagination.value.pageSize) {
- pagination.value.pageSize = pageSize;
- await handlePageSizeChange();
- return;
- }
- if (page === pagination.value.current - 1) {
- await handlePrevPage();
- return;
- }
- if (page === pagination.value.current + 1) {
- await handleNextPage();
- return;
- }
- if (page === pagination.value.current) return;
- pagination.value.jumpPage = String(page);
- await handleJumpPage();
- };
- const handlePageSizeChange = async () => {
- pagination.value.current = 1;
- await fetchTable();
- };
- const handleJumpPage = async () => {
- const nextPage = Number(pagination.value.jumpPage);
- if (!Number.isInteger(nextPage) || nextPage < 1) {
- createMessage.warning('请输入正确页码');
- pagination.value.jumpPage = String(pagination.value.current);
- return;
- }
- pagination.value.current = Math.min(nextPage, totalPages.value);
- await fetchTable();
- };
- const handleViewDetail = (record: BaseStationRow) => {
- detailBaseQuery.value = buildDetailBaseQuery(record);
- detailModalOpen.value = true;
- };
- const handleOpenMap = (record: BaseStationRow) => {
- const longitude = String(record.raw.personStationLongitude || '').trim();
- const latitude = String(record.raw.personStationLatitude || '').trim();
- if (!longitude || !latitude) {
- createMessage.warning('当前基站暂无坐标信息');
- return;
- }
- mapModalTitle.value = `${record.target === '-' ? '对象' : record.target} · ${
- record.stationName === '-' ? '基站位置' : record.stationName
- }`;
- mapModalStations.value = [record.raw];
- mapModalOpen.value = true;
- };
- 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-base-station';
- .@{prefix-cls}.base-station-page {
- .analysis-single-panel-frame();
- .filter-input {
- width: 180px;
- }
- .filter-date {
- width: 360px;
- }
- .table-wrapper .ant-table-cell {
- font-size: 12px;
- }
- .query-state-panel {
- flex: 1 1 auto;
- min-height: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 12px 0 0;
- }
- .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: 992px) {
- .filter-input,
- .filter-date {
- width: 100%;
- }
- }
- }
- </style>
|