| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- <template>
- <div :class="prefixCls" class="express-delivery-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" />
- </div>
- <div class="filter-item">
- <span class="filter-label">地点:</span>
- <Input v-model:value="filters.location" 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} 条地点汇总记录,可继续查看对应快递收发详情。`
- : '展示对象在不同地点的收件、寄件次数,方便后续定位地址与物流轨迹。'
- }}
- </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"
- :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>
- </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>
- </div>
- <ExpressInfoDetailModal v-model:open="detailModalOpen" :base-query="detailBaseQuery" />
- </div>
- </template>
- <script lang="ts" setup name="ViewsExpressDeliveryIndex">
- import { computed, onMounted, ref } from 'vue';
- import dayjs, { type Dayjs } from 'dayjs';
- import { Button, DatePicker, Input, Select } 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 componentSetting from '@jeesite/core/settings/componentSetting';
- 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 { pickDetailValue } from '@jeesite/core/utils/detailQuery';
- import { getPersonOptions } from '@jeesite/person/api/personApi';
- import { statFirstExpressInfo } from '@jeesite/track/api/trackApi';
- import type { ExpressInfoDTO } from '@jeesite/track/api/types/dto';
- import type { ExpressInfoQuery } from '@jeesite/track/api/types/query';
- import ExpressInfoDetailModal from '../components/ExpressInfoDetailModal.vue';
- type RangeValue = [Dayjs, Dayjs] | undefined;
- interface ExpressSummaryRow {
- rowKey: string;
- id: number;
- personName: string;
- location: string;
- expressCount: number | string;
- recipientCount: number | string;
- senderCount: number | string;
- raw: ExpressInfoDTO & Record<string, any>;
- }
- const DEFAULT_SORT_KEY = 'expressCount';
- const DEFAULT_SORT_ORDER = 'DESC' as const;
- const { prefixCls } = useDesign('express-delivery');
- const { createMessage } = useMessage();
- const { queryLoading, runWithQueryLoading } = useTableSkill();
- const RangePicker = DatePicker.RangePicker;
- const filters = ref({
- target: [] as string[],
- dateRange: undefined as RangeValue,
- location: '',
- });
- const targetOptions = ref<Array<{ label: string; value: string }>>([]);
- const tableData = ref<ExpressSummaryRow[]>([]);
- const hasSearched = ref(false);
- const { syncAnalysisDataRefreshState } = useAnalysisRefreshPrompt({
- refresh: () => fetchTable(),
- hasData: () => hasSearched.value && tableData.value.length > 0,
- });
- const detailModalOpen = ref(false);
- const detailBaseQuery = ref<Partial<ExpressInfoQuery>>({});
- 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: 80, align: 'center' },
- { title: '被调查对象', dataIndex: 'personName', key: 'personName', width: 150, align: 'center' },
- { title: '地点', dataIndex: 'location', key: 'location', width: 320, align: 'center' },
- { title: '快递总数', dataIndex: 'expressCount', key: 'expressCount', width: 120, align: 'center' },
- { title: '收件次数', dataIndex: 'recipientCount', key: 'recipientCount', width: 120, align: 'center' },
- { title: '寄件次数', dataIndex: 'senderCount', key: 'senderCount', width: 120, align: 'center' },
- ];
- const actionColumn = {
- title: '操作',
- dataIndex: 'action',
- key: 'action',
- width: 120,
- align: 'center' as const,
- fixed: 'right' as const,
- slot: 'action',
- };
- const normalizeList = (resp: any): ExpressInfoDTO[] => {
- 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 fallbackText = (value: string | number | null | undefined) => {
- if (value === 0) return '0';
- if (value === null || value === undefined || value === '') return '-';
- return String(value);
- };
- const normalizeCount = (value: number | string | null | undefined) => {
- const numericValue = Number(value);
- return Number.isFinite(numericValue) ? numericValue : fallbackText(value);
- };
- const parseCount = (value: number | string | null | undefined) => {
- const numericValue = Number(value);
- return Number.isFinite(numericValue) ? numericValue : 0;
- };
- const normalizeIds = (value: unknown): string[] | undefined => {
- if (Array.isArray(value)) {
- const ids = value.map((item) => String(item).trim()).filter(Boolean);
- return ids.length ? ids : undefined;
- }
- if (typeof value === 'string') {
- const ids = value
- .split(',')
- .map((item) => item.trim())
- .filter(Boolean);
- return ids.length ? ids : undefined;
- }
- return undefined;
- };
- const mapSummaryRow = (item: ExpressInfoDTO, index: number): ExpressSummaryRow => ({
- rowKey: item.ids || `${item.personName || 'unknown'}-${item.location || 'unknown'}-${index}`,
- id: (pagination.value.current - 1) * pagination.value.pageSize + index + 1,
- personName: fallbackText(item.personName),
- location: fallbackText(item.location),
- expressCount: normalizeCount(item.expressCount),
- recipientCount: normalizeCount(item.recipientCount),
- senderCount: normalizeCount(item.senderCount),
- raw: item,
- });
- const sortSummaryList = (list: ExpressInfoDTO[]) =>
- [...list].sort((left, right) => {
- const countDiff = parseCount(right.expressCount) - parseCount(left.expressCount);
- if (countDiff !== 0) return countDiff;
- const recipientDiff = parseCount(right.recipientCount) - parseCount(left.recipientCount);
- if (recipientDiff !== 0) return recipientDiff;
- return String(left.personName || left.location || '').localeCompare(
- String(right.personName || right.location || ''),
- );
- });
- const buildQuery = (): ExpressInfoQuery => {
- const startDate =
- Array.isArray(filters.value.dateRange) && filters.value.dateRange[0]
- ? dayjs(filters.value.dateRange[0]).format('YYYY-MM-DD')
- : undefined;
- const endDate =
- Array.isArray(filters.value.dateRange) && filters.value.dateRange[1]
- ? dayjs(filters.value.dateRange[1]).format('YYYY-MM-DD')
- : undefined;
- const personNames = filters.value.target.length ? [...filters.value.target] : undefined;
- return {
- page: pagination.value.current,
- limit: pagination.value.pageSize,
- startDate,
- endDate,
- personNames,
- location: filters.value.location.trim() || undefined,
- orderKey: DEFAULT_SORT_KEY,
- sort: DEFAULT_SORT_ORDER,
- };
- };
- const buildDetailBaseQuery = (record: ExpressSummaryRow): Partial<ExpressInfoQuery> => {
- const currentQuery = buildQuery();
- const recordPersonName = String(record.raw.personName || '').trim();
- return {
- startDate: currentQuery.startDate,
- endDate: currentQuery.endDate,
- // 二层详情的地点固定继承外层筛选条件,不再带行内汇总地点。
- location: currentQuery.location,
- personName: pickDetailValue(recordPersonName, currentQuery.personNames?.[0]),
- personNames: recordPersonName ? [recordPersonName] : currentQuery.personNames,
- ids: normalizeIds(record.raw.ids),
- };
- };
- const fetchTable = async () => {
- await runWithQueryLoading(async () => {
- const resp = await statFirstExpressInfo(buildQuery());
- const list = sortSummaryList(normalizeList(resp));
- tableData.value = list.map(mapSummaryRow);
- pagination.value.total = normalizeTotal(resp, list.length);
- hasSearched.value = true;
- syncAnalysisDataRefreshState(list.length > 0);
- 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,
- location: '',
- };
- 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: ExpressSummaryRow) => {
- detailBaseQuery.value = buildDetailBaseQuery(record);
- detailModalOpen.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-express-delivery';
- .@{prefix-cls}.express-delivery-page {
- .analysis-page-frame();
- .filter-input {
- width: 180px;
- }
- .filter-date {
- width: 360px;
- }
- .content-panel {
- flex: 1 1 auto;
- min-height: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
- }
- .table-card {
- .analysis-card-shell();
- flex: 1 1 auto;
- }
- .query-state-panel {
- flex: 1 1 auto;
- min-height: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 12px 0 0;
- }
- .table-wrapper {
- flex: 1 1 auto;
- min-height: 0;
- border: 1px solid #eef2f7;
- border-radius: 10px;
- overflow: hidden;
- }
- .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;
- display: flex;
- flex-direction: column;
- }
- .table-wrapper .ant-table-body {
- max-height: 100% !important;
- }
- .table-wrapper .ant-table-cell {
- font-size: 12px;
- }
- .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-actions {
- width: 100%;
- margin-left: 0;
- }
- .filter-input,
- .filter-date {
- width: 100%;
- }
- }
- }
- </style>
|