??????????????????????????????? H5/????? Co-authored-by: Cursor <cursoragent@cursor.com>
2202 lines
72 KiB
Vue
2202 lines
72 KiB
Vue
<script lang="ts" setup>
|
||
import { ref, computed, onMounted, nextTick, watch } from 'vue';
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
import { IconDown } from '@arco-design/web-vue/es/icon';
|
||
import { Message, Modal } from '@arco-design/web-vue';
|
||
import dayjs from 'dayjs';
|
||
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
||
import { useI18n } from 'vue-i18n';
|
||
import {
|
||
postOrderList,
|
||
getOrderStatusCounts,
|
||
getOrderDetail,
|
||
postOrderPreDeclare,
|
||
postOrderIntercept,
|
||
postOrderCancelIntercept,
|
||
postOrderDelete,
|
||
postOrderPrint,
|
||
postOrderExport,
|
||
postOrderTrackUpsertSmallBag,
|
||
postOrderDownstreamForwardCreateOne,
|
||
postOrderBatchJob,
|
||
waitOrderBatchJob,
|
||
formatBatchJobTrackUpsertResultText,
|
||
formatBatchJobDownstreamForwardResultText,
|
||
isOrderBatchQueueDisabledError,
|
||
formatBatchJobFailureMessage,
|
||
formatBatchJobSummaryMessage,
|
||
resolveBatchJobItemRowLabel,
|
||
parseBatchJobPrintUrl,
|
||
postOrderHandleStatusUpdate,
|
||
type OrderTrackUpsertResult,
|
||
type OrderDownstreamForwardResult,
|
||
} from '@/api/order';
|
||
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
|
||
import { buildFailGuideFromReason } from '@/utils/parcelCreateFailGuide';
|
||
import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
|
||
import OrderPayConfirmModal from './components/OrderPayConfirmModal.vue';
|
||
import MarkProblematicModal from './components/MarkProblematicModal.vue';
|
||
import MarkClaimedModal from './components/MarkClaimedModal.vue';
|
||
import ColumnConfigModal from '../components/ColumnConfigModal.vue';
|
||
import {
|
||
buildHotColumns,
|
||
getActiveScheme,
|
||
loadColumnConfigState,
|
||
mergeDiscoveredColumns,
|
||
saveColumnConfigState,
|
||
type ColumnScheme,
|
||
type OrderListColumnDef,
|
||
} from '../myOrders/columnConfig';
|
||
import {
|
||
WAYBILL_MANAGE_COLUMN_STORAGE_KEY,
|
||
WAYBILL_MANAGE_DEFAULT_KEYS,
|
||
getWaybillBaseCatalog,
|
||
} from './columnConfig';
|
||
import OrderPrintModal from '@/components/PrintTemplate/OrderPrintModal.vue';
|
||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
||
import {
|
||
ORDER_STATUS_TABS,
|
||
ORDER_STATUS_ALL,
|
||
resolveOrderStatusCode,
|
||
isOrderDeletable,
|
||
getOrderStatusMeta,
|
||
} from '@/constants/orderStatus';
|
||
import {
|
||
ORDER_HANDLE_STATUS_ALL,
|
||
ORDER_HANDLE_STATUS_LIST,
|
||
isOrderHandleSubTabParent,
|
||
resolveOrderHandleStatusCode,
|
||
getOrderHandleStatusMeta,
|
||
type OrderHandleStatusCode,
|
||
} from '@/constants/orderHandleStatus';
|
||
import { hasPermission } from '@/utils/rbac-permission';
|
||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer';
|
||
import { renderWarehouseActionBtn } from '@/utils/warehouseActionRenderer';
|
||
import { buildWaybillContextFromRow, openAskAdvisor } from '@/composables/useAskAdvisor';
|
||
|
||
defineOptions({
|
||
name: 'WaybillManage',
|
||
});
|
||
|
||
const { t } = useI18n();
|
||
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
||
const {
|
||
customerId: adminCustomerId,
|
||
customerOptions,
|
||
withCustomerId,
|
||
} = useWarehouseAdminCustomer();
|
||
|
||
// 订单详情抽屉
|
||
const detailDrawerRef = ref<InstanceType<typeof OrderDetailDrawer>>();
|
||
const currentOrder = ref<any>(null);
|
||
|
||
/** 兼容后端 camelCase / PascalCase */
|
||
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
||
if (!raw) return undefined;
|
||
for (const k of keys) {
|
||
const v = raw[k];
|
||
if (v !== undefined && v !== null && v !== '') return v;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
/** 列表行与表格列对齐:接口用 serviceNo/clientNo/trackNo,表格用运单号/客户单号/跟踪号 */
|
||
function normalizeOrderRow(raw: any) {
|
||
const r = raw?.receiver || raw?.Receiver || {};
|
||
const s = raw?.sender || raw?.Sender || {};
|
||
const wl = raw?.weightList || raw?.WeightList;
|
||
const lastW =
|
||
Array.isArray(wl) && wl.length ? wl[wl.length - 1] : undefined;
|
||
|
||
const serviceNo = pick(raw, 'serviceNo', 'ServiceNo', 'waybillNo', 'WaybillNo');
|
||
const clientNo = pick(raw, 'clientNo', 'ClientNo', 'customerOrderNo', 'CustomerOrderNo');
|
||
const trackNo = pick(raw, 'trackNo', 'TrackNo', 'trackingNo', 'TrackingNo');
|
||
const downstreamServiceNo = pick(raw, 'downstreamServiceNo', 'DownstreamServiceNo');
|
||
const productCode = pick(raw, 'productCode', 'ProductCode');
|
||
const productName = pick(raw, 'productName', 'ProductName');
|
||
const channelCode = pick(raw, 'channelCode', 'ChannelCode');
|
||
const channelName = pick(raw, 'channelName', 'ChannelName');
|
||
const isPaidRaw = pick(raw, 'isPaid', 'IsPaid');
|
||
const customerId = pick(raw, 'customerId', 'CustomerId') ?? raw?.customerId;
|
||
const customerFromOptions = customerOptions.value.find(
|
||
(opt) => Number(opt.value) === Number(customerId),
|
||
);
|
||
|
||
const boolish = (v: any) => v === true || v === 1 || v === '1';
|
||
|
||
return {
|
||
...raw,
|
||
// 展平寄件人(便于列配置直接选接口字段)
|
||
senderName: pick(s, 'senderName', 'SenderName') ?? '',
|
||
senderCompany: pick(s, 'company', 'Company') ?? '',
|
||
senderPhone: pick(s, 'phone', 'Phone') ?? '',
|
||
senderTelePhone: pick(s, 'telePhone', 'TelePhone') ?? '',
|
||
senderEmail: pick(s, 'email', 'Email') ?? '',
|
||
senderCountryCode: pick(s, 'countryCode', 'CountryCode') ?? '',
|
||
senderProvince: pick(s, 'province', 'Province') ?? '',
|
||
senderCity: pick(s, 'city', 'City') ?? '',
|
||
senderZipCode: pick(s, 'zipCode', 'ZipCode') ?? '',
|
||
senderStreetAddress1: pick(s, 'streetAddress1', 'StreetAddress1') ?? '',
|
||
senderStreetAddress2: pick(s, 'streetAddress2', 'StreetAddress2') ?? '',
|
||
senderStreetAddress3: pick(s, 'streetAddress3', 'StreetAddress3') ?? '',
|
||
senderHouseNumber: pick(s, 'houseNumber', 'HouseNumber') ?? '',
|
||
// 展平收件人原始字段
|
||
receiverRecipientsName: pick(r, 'recipientsName', 'RecipientsName') ?? '',
|
||
receiverCompanyName: pick(r, 'companyName', 'CompanyName') ?? '',
|
||
receiverPhone: pick(r, 'phone', 'Phone') ?? '',
|
||
receiverTelePhone: pick(r, 'telePhone', 'TelePhone') ?? '',
|
||
receiverEmail: pick(r, 'email', 'Email') ?? '',
|
||
receiverCountryCode: pick(r, 'countryCode', 'CountryCode') ?? '',
|
||
receiverProvince: pick(r, 'province', 'Province') ?? '',
|
||
receiverCity: pick(r, 'city', 'City') ?? '',
|
||
receiverStreetNumber: pick(r, 'streetNumber', 'StreetNumber') ?? '',
|
||
receiverStreetAddress1: pick(r, 'streetAddress1', 'StreetAddress1') ?? '',
|
||
receiverStreetAddress2: pick(r, 'streetAddress2', 'StreetAddress2') ?? '',
|
||
receiverStreetAddress3: pick(r, 'streetAddress3', 'StreetAddress3') ?? '',
|
||
receiverZipCode: pick(r, 'zipCode', 'ZipCode') ?? '',
|
||
receiverTaxType: pick(r, 'taxType', 'TaxType') ?? '',
|
||
receiverTaxNo: pick(r, 'taxNo', 'TaxNo') ?? '',
|
||
receiverCertificateType: pick(r, 'certificateType', 'CertificateType') ?? '',
|
||
receiverCertificateNo: pick(r, 'certificateNo', 'CertificateNo') ?? '',
|
||
receiverEori: pick(r, 'eori', 'Eori') ?? '',
|
||
receiverVat: pick(r, 'vat', 'Vat') ?? '',
|
||
|
||
waybillNo: serviceNo ?? '',
|
||
customerOrderNo: clientNo ?? '',
|
||
trackingNo: trackNo ?? '',
|
||
downstreamServiceNo: downstreamServiceNo ?? '',
|
||
productCode: productCode ?? '',
|
||
productName: productName ?? productCode ?? '',
|
||
channelCode: channelCode ?? '',
|
||
channelName: channelName ?? channelCode ?? '',
|
||
isPaid: boolish(isPaidRaw),
|
||
isCod: boolish(pick(raw, 'isCod', 'IsCod')),
|
||
isIntercept: boolish(pick(raw, 'isIntercept', 'IsIntercept')),
|
||
isRemote: boolish(pick(raw, 'isRemote', 'IsRemote')),
|
||
consignee: pick(raw, 'consignee', 'Consignee') ?? pick(r, 'recipientsName', 'RecipientsName') ?? '',
|
||
consigneePhone: pick(raw, 'consigneePhone', 'ConsigneePhone') ?? pick(r, 'phone', 'Phone') ?? '',
|
||
consigneeAddress: pick(raw, 'consigneeAddress', 'ConsigneeAddress') ?? pick(r, 'streetAddress1', 'StreetAddress1') ?? '',
|
||
consigneePostcode: pick(raw, 'consigneePostcode', 'ConsigneePostcode') ?? pick(r, 'zipCode', 'ZipCode') ?? '',
|
||
consigneeState: pick(raw, 'consigneeState', 'ConsigneeState') ?? pick(r, 'province', 'Province') ?? '',
|
||
consigneeCity: pick(raw, 'consigneeCity', 'ConsigneeCity') ?? pick(r, 'city', 'City') ?? '',
|
||
consigneeCountry: pick(raw, 'consigneeCountry', 'ConsigneeCountry') ?? pick(r, 'country', 'Country') ?? '',
|
||
consigneeCountryName: pick(raw, 'consigneeCountryName', 'ConsigneeCountryName') ?? '',
|
||
ioss: pick(raw, 'ioss', 'Ioss', 'IOSS') ?? '',
|
||
eori: pick(raw, 'eori', 'Eori', 'EORI') ?? pick(r, 'eori', 'Eori') ?? '',
|
||
shippingMethod: productName ?? productCode ?? '',
|
||
serviceChannel: channelName ?? channelCode ?? '',
|
||
chargeableWeight:
|
||
pick(raw, 'chargeableWeight', 'ChargeableWeight') ??
|
||
pick(lastW as any, 'chargeableWeight', 'ChargeableWeight') ??
|
||
'',
|
||
receivableAmount: pick(raw, 'receivableAmount', 'ReceivableAmount') ?? '',
|
||
orderStatus: pick(raw, 'orderStatus', 'OrderStatus') ?? raw?.orderStatus,
|
||
predeclareFailReason: pick(raw, 'predeclareFailReason', 'PredeclareFailReason') ?? '',
|
||
allocationPendingReason: pick(raw, 'allocationPendingReason', 'AllocationPendingReason') ?? '',
|
||
status: resolveOrderStatusCode(raw) || pick(raw, 'status', 'Status') || raw?.status,
|
||
handleStatus: pick(raw, 'handleStatus', 'HandleStatus') ?? raw?.handleStatus,
|
||
handleStatusName: pick(raw, 'handleStatusName', 'HandleStatusName') ?? raw?.handleStatusName,
|
||
cargoAttribute: pick(raw, 'cargoAttribute', 'CargoAttribute') ?? raw?.cargoAttribute,
|
||
createTime: pick(raw, 'createTime', 'CreateTime') ?? raw?.createTime,
|
||
receiveTime: pick(raw, 'receiveTime', 'ReceiveTime') ?? raw?.receiveTime,
|
||
shipTime: pick(raw, 'shipTime', 'ShipTime') ?? raw?.shipTime,
|
||
payTime: pick(raw, 'payTime', 'PayTime') ?? raw?.payTime,
|
||
signTime: pick(raw, 'signTime', 'SignTime') ?? raw?.signTime,
|
||
warehouse: pick(raw, 'warehouse', 'Warehouse') ?? raw?.warehouse,
|
||
customerId,
|
||
customerName:
|
||
pick(raw, 'customerName', 'CustomerName') ||
|
||
customerFromOptions?.label?.replace(/\s*\([^)]*\)\s*$/, '') ||
|
||
'',
|
||
customerCode:
|
||
pick(raw, 'customerCode', 'CustomerCode') ||
|
||
(customerFromOptions?.label?.match(/\(([^)]+)\)\s*$/)?.[1] ?? '') ||
|
||
'',
|
||
};
|
||
}
|
||
|
||
async function openDetailWithOptionalFetch(
|
||
row: any,
|
||
opts?: { editBasic?: boolean; tab?: 'basic' | 'track' },
|
||
) {
|
||
let nextRow = row;
|
||
const id = row?.id;
|
||
if (id != null && id !== '') {
|
||
try {
|
||
const detail = await getOrderDetail(id);
|
||
nextRow = normalizeOrderRow({ ...row, ...detail });
|
||
} catch {
|
||
/* 保留列表行数据 */
|
||
}
|
||
}
|
||
currentOrder.value = nextRow;
|
||
detailDrawerRef.value?.openDrawer({ tab: opts?.tab || 'basic' });
|
||
if (opts?.editBasic) {
|
||
await nextTick();
|
||
detailDrawerRef.value?.startBasicEdit?.();
|
||
}
|
||
}
|
||
|
||
function handleOrderDetailUpdated(row: any) {
|
||
currentOrder.value = normalizeOrderRow(row);
|
||
const idx = orderList.value.findIndex((it) => Number(it.id) === Number(row?.id));
|
||
if (idx >= 0) {
|
||
orderList.value[idx] = normalizeOrderRow({ ...orderList.value[idx], ...row });
|
||
}
|
||
}
|
||
|
||
const handleOrderDetailClick = (row: number) => {
|
||
if (row >= 0 && row < orderList.value.length) {
|
||
openDetailWithOptionalFetch(orderList.value[row]);
|
||
}
|
||
};
|
||
|
||
// 当前激活的标签页(与后端 OrderStatusCodes 一致;支持 ?status= 从工作台跳入)
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const validStatusSet = new Set(ORDER_STATUS_TABS.map((meta) => meta.value));
|
||
|
||
function resolveStatusFromQuery(): string {
|
||
const raw = route.query.status;
|
||
const status = Array.isArray(raw) ? raw[0] : raw;
|
||
if (typeof status === 'string' && validStatusSet.has(status as any)) {
|
||
return status;
|
||
}
|
||
return ORDER_STATUS_ALL;
|
||
}
|
||
|
||
const activeTab = ref<string>(resolveStatusFromQuery());
|
||
const activeHandleTab = ref<OrderHandleStatusCode>(ORDER_HANDLE_STATUS_ALL);
|
||
|
||
const tabCounts = ref<Record<string, number>>({});
|
||
const handleCounts = ref<Record<string, { pending: number; processed: number }>>({});
|
||
|
||
const showHandleSubTabs = computed(() => isOrderHandleSubTabParent(activeTab.value));
|
||
|
||
const showExceptionCreateActions = computed(
|
||
() => !['problematic', 'claimed', 'deleted'].includes(activeTab.value),
|
||
);
|
||
|
||
const showMarkProcessedAction = computed(
|
||
() => showHandleSubTabs.value && activeHandleTab.value === 'pending',
|
||
);
|
||
|
||
function isActiveTab(...keys: string[]) {
|
||
return keys.includes(activeTab.value);
|
||
}
|
||
|
||
/** 按当前状态收敛工具栏,避免客户在无关 Tab 看到一排无效按钮 */
|
||
const showPayAction = computed(() => isActiveTab('all', 'pendingPayment'));
|
||
const showPredeclareAction = computed(() => isActiveTab('all', 'draft', 'predeclared'));
|
||
const showDownstreamAction = computed(() =>
|
||
isActiveTab(
|
||
'all',
|
||
'draft',
|
||
'predeclared',
|
||
'received',
|
||
'pendingPayment',
|
||
'pendingShip',
|
||
'pendingOnline',
|
||
),
|
||
);
|
||
const showPrintAction = computed(() =>
|
||
isActiveTab(
|
||
'all',
|
||
'predeclared',
|
||
'received',
|
||
'pendingPayment',
|
||
'pendingShip',
|
||
'pendingOnline',
|
||
'shipping',
|
||
'deliverySuccess',
|
||
'deliveryFailed',
|
||
),
|
||
);
|
||
const showModifyOrderAction = computed(() =>
|
||
isActiveTab('all', 'draft', 'predeclared', 'received', 'pendingAllocation', 'pendingPayment', 'pendingShip'),
|
||
);
|
||
const showInterceptActions = computed(() =>
|
||
isActiveTab('all', 'pendingShip', 'pendingOnline', 'shipping'),
|
||
);
|
||
const showDeleteAction = computed(() => isActiveTab('all', 'draft', 'predeclared'));
|
||
const showCreateTrackAction = computed(() =>
|
||
isActiveTab('all', 'pendingOnline', 'shipping', 'deliverySuccess', 'deliveryFailed'),
|
||
);
|
||
|
||
const problematicModalVisible = ref(false);
|
||
const claimedModalVisible = ref(false);
|
||
const exceptionOrderIds = ref<number[]>([]);
|
||
const payConfirmVisible = ref(false);
|
||
const payConfirmOrders = ref<Array<{ id: number | string; waybillNo?: string; serviceNo?: string }>>([]);
|
||
|
||
const handleContextLabel = computed(() => {
|
||
if (activeTab.value === 'problematic') {
|
||
return t('order.myOrders.handleStatus.context.problematic');
|
||
}
|
||
if (activeTab.value === 'claimed') {
|
||
return t('order.myOrders.handleStatus.context.claimed');
|
||
}
|
||
return '';
|
||
});
|
||
|
||
const handleSegments = computed(() => {
|
||
if (!showHandleSubTabs.value) return [];
|
||
const bucket = handleCounts.value[activeTab.value] ?? { pending: 0, processed: 0 };
|
||
const total = (tabCounts.value[activeTab.value] ?? 0) || bucket.pending + bucket.processed;
|
||
return ORDER_HANDLE_STATUS_LIST.map((meta) => {
|
||
let count = total;
|
||
if (meta.value === 'pending') count = bucket.pending;
|
||
if (meta.value === 'processed') count = bucket.processed;
|
||
return {
|
||
key: meta.value,
|
||
labelKey: meta.labelKey,
|
||
count,
|
||
tagColor: meta.tagColor,
|
||
tagBgColor: meta.tagBgColor,
|
||
};
|
||
});
|
||
});
|
||
|
||
const tabs = computed(() =>
|
||
ORDER_STATUS_TABS.map((meta) => ({
|
||
key: meta.value,
|
||
labelKey: meta.labelKey,
|
||
showCount: meta.showTabCount !== false,
|
||
count: tabCounts.value[meta.value] ?? null,
|
||
})),
|
||
);
|
||
|
||
async function loadTabCounts() {
|
||
try {
|
||
const { total, counts, handleCounts: handleCountsRaw } = await getOrderStatusCounts();
|
||
const next: Record<string, number> = {
|
||
[ORDER_STATUS_ALL]: total,
|
||
};
|
||
for (const meta of ORDER_STATUS_TABS) {
|
||
if (meta.value === ORDER_STATUS_ALL) continue;
|
||
next[meta.value] = counts[meta.value] ?? 0;
|
||
}
|
||
tabCounts.value = next;
|
||
handleCounts.value = handleCountsRaw ?? {};
|
||
} catch {
|
||
/* 角标失败不影响列表 */
|
||
}
|
||
}
|
||
|
||
async function refreshListAndTabCounts() {
|
||
await Promise.all([loadOrderList(), loadTabCounts()]);
|
||
}
|
||
|
||
// 分页相关数据
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(30);
|
||
const totalCount = ref(0);
|
||
|
||
/** 列表筛选:单号关键词 + 创建时间 */
|
||
const listKeyword = ref('');
|
||
const createTimeRange = ref<(Date | string)[] | undefined>();
|
||
|
||
function buildListFilterParams() {
|
||
const keyword = listKeyword.value.trim();
|
||
let createTimeStart = '';
|
||
let createTimeEnd = '';
|
||
if (createTimeRange.value?.length === 2 && createTimeRange.value[0] && createTimeRange.value[1]) {
|
||
createTimeStart = dayjs(createTimeRange.value[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||
createTimeEnd = dayjs(createTimeRange.value[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||
}
|
||
return { keyword, createTimeStart, createTimeEnd };
|
||
}
|
||
|
||
const orderList = ref<any[]>([]);
|
||
const listLoading = ref(false);
|
||
let listRequestSeq = 0;
|
||
|
||
function escapeHtml(text: string) {
|
||
return String(text)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
const selectedRowCount = ref(0);
|
||
/** 表格选中行(用于批量操作) */
|
||
const selectedOrderRows = ref<any[]>([]);
|
||
/** 当前正在执行的 toolbar 操作(仅对应按钮显示 loading,避免全部按钮联动) */
|
||
const actionLoadingKey = ref<string | null>(null);
|
||
|
||
function isActionLoading(key: string) {
|
||
return actionLoadingKey.value === key;
|
||
}
|
||
|
||
function isActionBusy(exceptKey?: string) {
|
||
const current = actionLoadingKey.value;
|
||
return current != null && current !== exceptKey;
|
||
}
|
||
|
||
async function withOrderActionLoading<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
||
if (actionLoadingKey.value) {
|
||
return Promise.reject(new Error('busy'));
|
||
}
|
||
actionLoadingKey.value = key;
|
||
try {
|
||
return await fn();
|
||
} finally {
|
||
if (actionLoadingKey.value === key) {
|
||
actionLoadingKey.value = null;
|
||
}
|
||
}
|
||
}
|
||
const printConnectionState = useHiprintClientState();
|
||
const singlePrintVisible = ref(false);
|
||
const batchPrintVisible = ref(false);
|
||
const singlePrintItems = ref<Array<{ id: number | string; label: string; url: string }>>([]);
|
||
const batchPrintItems = ref<Array<{ id: number | string; label: string; url: string }>>([]);
|
||
const trackUpsertModalVisible = ref(false);
|
||
const trackUpsertResultText = ref('');
|
||
const downstreamForwardModalVisible = ref(false);
|
||
const downstreamForwardResultText = ref('');
|
||
|
||
function formatTrackUpsertResult(waybillNo: string, result: OrderTrackUpsertResult): string {
|
||
const lines = [`【${waybillNo || '-'}】`];
|
||
if (result.skipped) {
|
||
lines.push(`跳过:${result.skipReason || '未知原因'}`);
|
||
return lines.join('\n');
|
||
}
|
||
lines.push(`HTTP:${result.statusCode ?? '-'}`);
|
||
lines.push(`成功:${result.success ? '是' : '否'}`);
|
||
if (result.responseBody) {
|
||
lines.push(`响应:${result.responseBody}`);
|
||
}
|
||
if (result.curlCommand) {
|
||
lines.push('curl:');
|
||
lines.push(result.curlCommand);
|
||
} else if (result.requestJson) {
|
||
lines.push(`请求:${result.requestJson}`);
|
||
}
|
||
return lines.join('\n');
|
||
}
|
||
|
||
async function runSelectedOrderBatch(options: {
|
||
action: string;
|
||
rows: any[];
|
||
payload?: Record<string, unknown>;
|
||
loadingKey: string;
|
||
syncRunner: () => Promise<void>;
|
||
successMessage?: string;
|
||
onAsyncComplete?: (ctx: {
|
||
job: { successCount: number; failCount: number };
|
||
items: Array<{ orderId: number; serviceNo?: string; status: string; failReason?: string; resultJson?: string }>;
|
||
rows: any[];
|
||
}) => void | Promise<void>;
|
||
}) {
|
||
const orderIds = options.rows.map((row) => Number(getOrderRowId(row)!));
|
||
try {
|
||
const job = await postOrderBatchJob({
|
||
action: options.action,
|
||
orderIds,
|
||
payloadJson: options.payload ? JSON.stringify(options.payload) : undefined,
|
||
});
|
||
const result = await waitOrderBatchJob(job.jobId);
|
||
if (result.timedOut) {
|
||
Message.warning(formatBatchJobSummaryMessage(result.job, { timedOut: true, jobId: job.jobId }));
|
||
await refreshListAndTabCounts();
|
||
return;
|
||
}
|
||
if (options.onAsyncComplete) {
|
||
await options.onAsyncComplete({ ...result, rows: options.rows });
|
||
} else if (result.job.failCount > 0) {
|
||
Message.warning(
|
||
formatBatchJobFailureMessage(result.job, result.items, options.rows, (row, item) =>
|
||
resolveBatchJobItemRowLabel(row, item)
|
||
)
|
||
);
|
||
} else if (options.successMessage) {
|
||
Message.success(options.successMessage);
|
||
}
|
||
await refreshListAndTabCounts();
|
||
return;
|
||
} catch (e: any) {
|
||
if (!isOrderBatchQueueDisabledError(e)) {
|
||
throw e;
|
||
}
|
||
}
|
||
await options.syncRunner();
|
||
}
|
||
|
||
async function runTrackUpsertBatch(rows: any[]) {
|
||
const orderIds = rows.map((row) => Number(getOrderRowId(row)!));
|
||
try {
|
||
const job = await postOrderBatchJob({ action: 'track-upsert-small-bag', orderIds });
|
||
const result = await waitOrderBatchJob(job.jobId);
|
||
if (result.timedOut) {
|
||
trackUpsertResultText.value = `${formatBatchJobSummaryMessage(result.job, { timedOut: true, jobId: job.jobId })}\n\n${formatBatchJobTrackUpsertResultText('创建轨迹', result.job, result.items, rows)}`;
|
||
trackUpsertModalVisible.value = true;
|
||
return;
|
||
}
|
||
const { job: finishedJob, items } = result;
|
||
trackUpsertResultText.value = formatBatchJobTrackUpsertResultText('创建轨迹', finishedJob, items, rows);
|
||
trackUpsertModalVisible.value = true;
|
||
return;
|
||
} catch (e: any) {
|
||
if (!isOrderBatchQueueDisabledError(e)) {
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
const resultBlocks: string[] = [];
|
||
for (const row of rows) {
|
||
const id = getOrderRowId(row)!;
|
||
const waybillNo = row.waybillNo || row.serviceNo || String(id);
|
||
try {
|
||
const result = await postOrderTrackUpsertSmallBag({ id });
|
||
resultBlocks.push(formatTrackUpsertResult(waybillNo, result));
|
||
} catch (err: any) {
|
||
resultBlocks.push(`【${waybillNo}】失败:${err?.message || '请求异常'}`);
|
||
}
|
||
}
|
||
trackUpsertResultText.value = resultBlocks.join('\n\n');
|
||
trackUpsertModalVisible.value = true;
|
||
}
|
||
|
||
const handleCreateTrackOrder = async () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
await withOrderActionLoading('track', async () => {
|
||
await runTrackUpsertBatch(rows);
|
||
}).catch(() => undefined);
|
||
};
|
||
|
||
function formatDownstreamForwardResult(
|
||
waybillNo: string,
|
||
result: OrderDownstreamForwardResult
|
||
): string {
|
||
const lines = [
|
||
`【${waybillNo || '-'}】`,
|
||
result.success ? '成功' : '失败',
|
||
`standardCode:createOrderOne`,
|
||
];
|
||
if (result.supplierCode) {
|
||
lines.push(`supplierCode:${result.supplierCode}`);
|
||
}
|
||
if (result.downstreamServiceNo) {
|
||
lines.push(`服务商单号:${result.downstreamServiceNo}`);
|
||
}
|
||
if (result.trackNo) {
|
||
lines.push(`跟踪号:${result.trackNo}`);
|
||
}
|
||
lines.push(`请求:${result.url || '-'}`);
|
||
if (result.requestJson) {
|
||
lines.push(`请求体:\n${result.requestJson}`);
|
||
}
|
||
lines.push(`HTTP:${result.statusCode ?? '-'}`);
|
||
if (result.responseBody) {
|
||
lines.push(`响应:\n${result.responseBody}`);
|
||
}
|
||
return lines.join('\n');
|
||
}
|
||
|
||
async function runDownstreamForwardBatch(rows: any[]) {
|
||
const orderIds = rows.map((row) => Number(getOrderRowId(row)!));
|
||
try {
|
||
const job = await postOrderBatchJob({ action: 'downstream-forward-create-one', orderIds });
|
||
const result = await waitOrderBatchJob(job.jobId);
|
||
if (result.timedOut) {
|
||
downstreamForwardResultText.value = `${formatBatchJobSummaryMessage(result.job, { timedOut: true, jobId: job.jobId })}\n\n${formatBatchJobDownstreamForwardResultText('预报下游', result.job, result.items, rows)}`;
|
||
downstreamForwardModalVisible.value = true;
|
||
return;
|
||
}
|
||
const { job: finishedJob, items } = result;
|
||
downstreamForwardResultText.value = formatBatchJobDownstreamForwardResultText('预报下游', finishedJob, items, rows);
|
||
downstreamForwardModalVisible.value = true;
|
||
return;
|
||
} catch (e: any) {
|
||
if (!isOrderBatchQueueDisabledError(e)) {
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
const resultBlocks: string[] = [];
|
||
for (const row of rows) {
|
||
const id = getOrderRowId(row)!;
|
||
const waybillNo = row.waybillNo || row.serviceNo || String(id);
|
||
try {
|
||
const result = await postOrderDownstreamForwardCreateOne({ id });
|
||
resultBlocks.push(formatDownstreamForwardResult(waybillNo, result));
|
||
} catch (err: any) {
|
||
resultBlocks.push(`【${waybillNo}】失败:${err?.message || '请求异常'}`);
|
||
}
|
||
}
|
||
downstreamForwardResultText.value = resultBlocks.join('\n\n');
|
||
downstreamForwardModalVisible.value = true;
|
||
}
|
||
|
||
const handleForwardDownstreamCreate = async () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
await withOrderActionLoading('downstream', async () => {
|
||
await runDownstreamForwardBatch(rows);
|
||
}).catch(() => undefined);
|
||
};
|
||
|
||
function getOrderRowId(row: any): number | string | undefined {
|
||
if (row == null) return undefined;
|
||
const id = row.id ?? row.Id;
|
||
if (id === undefined || id === null || id === '') return undefined;
|
||
return id;
|
||
}
|
||
|
||
function requireSelectedRows(): any[] {
|
||
const rows = selectedOrderRows.value.filter((r) => getOrderRowId(r) != null);
|
||
if (!rows.length) {
|
||
Message.warning('请先选择订单');
|
||
return [];
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
const loadOrderList = async () => {
|
||
const requestSeq = ++listRequestSeq;
|
||
const status = activeTab.value === ORDER_STATUS_ALL ? ORDER_STATUS_ALL : activeTab.value;
|
||
const handleStatus =
|
||
showHandleSubTabs.value && activeHandleTab.value !== ORDER_HANDLE_STATUS_ALL
|
||
? activeHandleTab.value
|
||
: undefined;
|
||
const { keyword, createTimeStart, createTimeEnd } = buildListFilterParams();
|
||
listLoading.value = true;
|
||
try {
|
||
const { items, total } = await postOrderList(
|
||
withCustomerId({
|
||
page: currentPage.value,
|
||
pageSize: pageSize.value,
|
||
status,
|
||
handleStatus,
|
||
keyword,
|
||
createTimeStart: createTimeStart || undefined,
|
||
createTimeEnd: createTimeEnd || undefined,
|
||
}),
|
||
);
|
||
if (requestSeq !== listRequestSeq) {
|
||
return;
|
||
}
|
||
const mapped = (items || []).map((row) => normalizeOrderRow(row));
|
||
orderList.value = mapped;
|
||
totalCount.value = total;
|
||
tabCounts.value[status] = total;
|
||
syncColumnCatalogFromRows(mapped);
|
||
} catch (e: any) {
|
||
if (requestSeq !== listRequestSeq) {
|
||
return;
|
||
}
|
||
orderList.value = [];
|
||
totalCount.value = 0;
|
||
Message.error(e?.message || '加载订单列表失败');
|
||
} finally {
|
||
if (requestSeq === listRequestSeq) {
|
||
listLoading.value = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleSearch = () => {
|
||
currentPage.value = 1;
|
||
selectedOrderRows.value = [];
|
||
selectedRowCount.value = 0;
|
||
loadOrderList();
|
||
};
|
||
|
||
const handleResetSearch = () => {
|
||
listKeyword.value = '';
|
||
createTimeRange.value = undefined;
|
||
currentPage.value = 1;
|
||
selectedOrderRows.value = [];
|
||
selectedRowCount.value = 0;
|
||
loadOrderList();
|
||
};
|
||
|
||
// 当前页数据(HandsontablePro 用)
|
||
const processedTableData = computed(() => orderList.value);
|
||
|
||
// 标签页切换
|
||
const handleTabChange = (key: string) => {
|
||
if (activeTab.value === key) {
|
||
return;
|
||
}
|
||
activeTab.value = key;
|
||
activeHandleTab.value = ORDER_HANDLE_STATUS_ALL;
|
||
currentPage.value = 1;
|
||
orderList.value = [];
|
||
selectedOrderRows.value = [];
|
||
selectedRowCount.value = 0;
|
||
loadOrderList();
|
||
};
|
||
|
||
const handleHandleTabChange = (key: OrderHandleStatusCode) => {
|
||
if (activeHandleTab.value === key) {
|
||
return;
|
||
}
|
||
activeHandleTab.value = key;
|
||
currentPage.value = 1;
|
||
orderList.value = [];
|
||
selectedOrderRows.value = [];
|
||
selectedRowCount.value = 0;
|
||
loadOrderList();
|
||
};
|
||
|
||
// 操作按钮处理(依赖表格多选行;导出按当前 tab 条件,无需选行)
|
||
function openPayConfirm(rows: any[]) {
|
||
payConfirmOrders.value = rows
|
||
.filter((row) => getOrderRowId(row) != null)
|
||
.map((row) => ({
|
||
id: getOrderRowId(row)!,
|
||
waybillNo: row.waybillNo || row.serviceNo,
|
||
serviceNo: row.serviceNo,
|
||
}));
|
||
if (!payConfirmOrders.value.length) return;
|
||
payConfirmVisible.value = true;
|
||
}
|
||
|
||
const handlePay = () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
openPayConfirm(rows);
|
||
};
|
||
|
||
const handleRowPay = (row: any) => {
|
||
if (!getOrderRowId(row)) return;
|
||
openPayConfirm([row]);
|
||
};
|
||
|
||
async function handlePayConfirmSuccess() {
|
||
notifyCustomerBalanceChanged();
|
||
await refreshListAndTabCounts();
|
||
}
|
||
|
||
const handlePreDeclare = async () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
try {
|
||
await withOrderActionLoading('predeclare', async () => {
|
||
await runSelectedOrderBatch({
|
||
action: 'pre-declare',
|
||
rows,
|
||
loadingKey: 'predeclare',
|
||
successMessage: '预报已提交',
|
||
syncRunner: async () => {
|
||
for (const row of rows) {
|
||
await postOrderPreDeclare({ id: getOrderRowId(row)! });
|
||
}
|
||
Message.success('预报已提交');
|
||
await refreshListAndTabCounts();
|
||
},
|
||
});
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || '操作失败');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleIntercept = async () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
try {
|
||
await withOrderActionLoading('intercept', async () => {
|
||
await runSelectedOrderBatch({
|
||
action: 'intercept',
|
||
rows,
|
||
loadingKey: 'intercept',
|
||
successMessage: '拦截已提交',
|
||
syncRunner: async () => {
|
||
for (const row of rows) {
|
||
await postOrderIntercept({ id: getOrderRowId(row)! });
|
||
}
|
||
Message.success('拦截已提交');
|
||
await refreshListAndTabCounts();
|
||
},
|
||
});
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || '操作失败');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleCancelIntercept = async () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
try {
|
||
await withOrderActionLoading('cancelIntercept', async () => {
|
||
await runSelectedOrderBatch({
|
||
action: 'cancel-intercept',
|
||
rows,
|
||
loadingKey: 'cancelIntercept',
|
||
successMessage: '已取消拦截',
|
||
syncRunner: async () => {
|
||
for (const row of rows) {
|
||
await postOrderCancelIntercept({ id: getOrderRowId(row)! });
|
||
}
|
||
Message.success('已取消拦截');
|
||
await refreshListAndTabCounts();
|
||
},
|
||
});
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || '操作失败');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleDelete = () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
const blocked = rows.filter((row) => !isOrderDeletable(resolveOrderStatusCode(row)));
|
||
if (blocked.length) {
|
||
Message.warning('仅草稿或已预报订单可删除');
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: `确定删除选中的 ${rows.length} 条订单吗?`,
|
||
onOk: async () => {
|
||
try {
|
||
await withOrderActionLoading('delete', async () => {
|
||
await runSelectedOrderBatch({
|
||
action: 'delete',
|
||
rows,
|
||
loadingKey: 'delete',
|
||
successMessage: '已删除',
|
||
syncRunner: async () => {
|
||
for (const row of rows) {
|
||
await postOrderDelete({ id: getOrderRowId(row)! });
|
||
}
|
||
Message.success('已删除');
|
||
await refreshListAndTabCounts();
|
||
},
|
||
});
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || '删除失败');
|
||
}
|
||
throw e;
|
||
}
|
||
},
|
||
});
|
||
};
|
||
|
||
function buildPrintItem(row: any, printUrl: string) {
|
||
const id = getOrderRowId(row)!;
|
||
const label = row?.waybillNo || row?.trackingNo || row?.customerOrderNo || String(id);
|
||
return { id, label, url: printUrl };
|
||
}
|
||
|
||
function isUsablePrintUrl(raw: unknown) {
|
||
const value = String(raw ?? '').trim();
|
||
if (!value) return false;
|
||
if (value.includes('获取中')) return false;
|
||
return true;
|
||
}
|
||
|
||
const handleSinglePrint = async () => {
|
||
singlePrintVisible.value = false;
|
||
singlePrintItems.value = [];
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
if (rows.length > 1) {
|
||
Message.warning('单个打印仅支持选择 1 条订单');
|
||
return;
|
||
}
|
||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||
Message.warning('请先在顶部打开并连接打印开关');
|
||
return;
|
||
}
|
||
try {
|
||
await withOrderActionLoading('singlePrint', async () => {
|
||
const row = rows[0];
|
||
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
|
||
if (!isUsablePrintUrl(printUrl)) {
|
||
Message.info('未获取到打印文件,已打开打印弹窗供预览布局');
|
||
singlePrintVisible.value = true;
|
||
return;
|
||
}
|
||
singlePrintItems.value = [buildPrintItem(row, printUrl)];
|
||
singlePrintVisible.value = true;
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || '打印失败');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleBatchPrint = async () => {
|
||
batchPrintVisible.value = false;
|
||
batchPrintItems.value = [];
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||
Message.warning('请先在顶部打开并连接打印开关');
|
||
return;
|
||
}
|
||
try {
|
||
await withOrderActionLoading('batchPrint', async () => {
|
||
await runSelectedOrderBatch({
|
||
action: 'print',
|
||
rows,
|
||
loadingKey: 'batchPrint',
|
||
onAsyncComplete: async ({ items, rows: selectedRows, job }) => {
|
||
const itemsByOrderId = new Map(items.map((item) => [item.orderId, item]));
|
||
const printItems: Array<{ id: number | string; label: string; url: string }> = [];
|
||
for (const row of selectedRows) {
|
||
const orderId = Number(getOrderRowId(row)!);
|
||
const item = itemsByOrderId.get(orderId);
|
||
if (item?.status !== 'success') continue;
|
||
const printUrl = parseBatchJobPrintUrl(item);
|
||
if (isUsablePrintUrl(printUrl)) {
|
||
printItems.push(buildPrintItem(row, printUrl));
|
||
}
|
||
}
|
||
if (!printItems.length) {
|
||
if (job.failCount > 0) {
|
||
Message.warning(formatBatchJobFailureMessage(job, items, selectedRows));
|
||
} else {
|
||
Message.info('未获取到可打印文件,已打开打印弹窗供预览布局');
|
||
}
|
||
batchPrintVisible.value = true;
|
||
return;
|
||
}
|
||
batchPrintItems.value = printItems;
|
||
batchPrintVisible.value = true;
|
||
if (job.failCount > 0) {
|
||
Message.warning(formatBatchJobFailureMessage(job, items, selectedRows));
|
||
}
|
||
},
|
||
syncRunner: async () => {
|
||
const printItems: Array<{ id: number | string; label: string; url: string }> = [];
|
||
for (const row of rows) {
|
||
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
|
||
if (isUsablePrintUrl(printUrl)) {
|
||
printItems.push(buildPrintItem(row, printUrl));
|
||
}
|
||
}
|
||
if (!printItems.length) {
|
||
Message.info('未获取到可打印文件,已打开打印弹窗供预览布局');
|
||
batchPrintVisible.value = true;
|
||
return;
|
||
}
|
||
batchPrintItems.value = printItems;
|
||
batchPrintVisible.value = true;
|
||
},
|
||
});
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || '批量打印失败');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleExport = async () => {
|
||
try {
|
||
await withOrderActionLoading('export', async () => {
|
||
const status = activeTab.value === ORDER_STATUS_ALL ? ORDER_STATUS_ALL : activeTab.value;
|
||
const { keyword, createTimeStart, createTimeEnd } = buildListFilterParams();
|
||
const { downloadUrl } = await postOrderExport(
|
||
withCustomerId({
|
||
status,
|
||
keyword,
|
||
createTimeStart,
|
||
createTimeEnd,
|
||
}),
|
||
);
|
||
if (downloadUrl) window.open(downloadUrl, '_blank');
|
||
Message.success(downloadUrl ? '导出链接已打开' : '导出已提交');
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || '导出失败');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleModifyOrder = async () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
if (rows.length > 1) {
|
||
Message.warning(t('order.myOrders.modify.singleOnly'));
|
||
return;
|
||
}
|
||
await openDetailWithOptionalFetch(rows[0], { editBasic: true });
|
||
};
|
||
|
||
const openProblematicModal = () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
exceptionOrderIds.value = rows.map((row) => Number(getOrderRowId(row)!));
|
||
problematicModalVisible.value = true;
|
||
};
|
||
|
||
const openClaimedModal = () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
exceptionOrderIds.value = rows.map((row) => Number(getOrderRowId(row)!));
|
||
claimedModalVisible.value = true;
|
||
};
|
||
|
||
const handleExceptionActionSuccess = async () => {
|
||
await refreshListAndTabCounts();
|
||
};
|
||
|
||
const handleMarkProcessed = async () => {
|
||
const rows = requireSelectedRows();
|
||
if (!rows.length) return;
|
||
try {
|
||
await withOrderActionLoading('markProcessed', async () => {
|
||
await runSelectedOrderBatch({
|
||
action: 'handle-status-update',
|
||
rows,
|
||
loadingKey: 'markProcessed',
|
||
payload: { handleStatus: 'processed' },
|
||
successMessage: t('order.myOrders.handleStatus.success'),
|
||
syncRunner: async () => {
|
||
const result = await postOrderHandleStatusUpdate({
|
||
orderIds: rows.map((row) => Number(getOrderRowId(row)!)),
|
||
handleStatus: 'processed',
|
||
});
|
||
if (result.failures?.length) {
|
||
const lines = result.failures
|
||
.slice(0, 5)
|
||
.map((f) => `${f.serviceNo || f.orderId}: ${f.reason}`)
|
||
.join('\n');
|
||
if (result.successCount > 0) {
|
||
Message.warning(
|
||
t('order.myOrders.batch.partialSuccess', {
|
||
success: result.successCount,
|
||
fail: result.failures.length,
|
||
detail: lines,
|
||
}),
|
||
);
|
||
} else {
|
||
Message.error(lines || t('order.myOrders.handleStatus.failed'));
|
||
return;
|
||
}
|
||
} else {
|
||
Message.success(t('order.myOrders.handleStatus.success'));
|
||
}
|
||
await refreshListAndTabCounts();
|
||
},
|
||
});
|
||
});
|
||
} catch (e: any) {
|
||
if (e?.message !== 'busy') {
|
||
Message.error(e?.message || t('order.myOrders.handleStatus.failed'));
|
||
}
|
||
}
|
||
};
|
||
|
||
const onMoreActionClick = (key: string) => {
|
||
if (key === 'createProblematic') return openProblematicModal();
|
||
if (key === 'createClaimed') return openClaimedModal();
|
||
if (key === 'createTrackOrder') return handleCreateTrackOrder();
|
||
if (key === 'intercept') return handleIntercept();
|
||
if (key === 'cancelIntercept') return handleCancelIntercept();
|
||
if (key === 'delete') return handleDelete();
|
||
if (key === 'export') return handleExport();
|
||
if (key === 'modifyOrder') return handleModifyOrder();
|
||
};
|
||
|
||
// 分页处理函数
|
||
const handlePageChange = (page: number) => {
|
||
currentPage.value = page;
|
||
loadOrderList();
|
||
};
|
||
|
||
const handlePageSizeChange = (size: number) => {
|
||
pageSize.value = size;
|
||
currentPage.value = 1;
|
||
loadOrderList();
|
||
};
|
||
|
||
// HandsontablePro 列定义(支持命名方案;目录覆盖接口全量标量字段,并随列表动态发现)
|
||
const columnCatalog = ref<OrderListColumnDef[]>(getWaybillBaseCatalog());
|
||
const columnConfigState = ref(
|
||
loadColumnConfigState(
|
||
WAYBILL_MANAGE_COLUMN_STORAGE_KEY,
|
||
columnCatalog.value,
|
||
WAYBILL_MANAGE_DEFAULT_KEYS,
|
||
t('order.myOrders.columnConfig.defaultScheme'),
|
||
),
|
||
);
|
||
const columnConfigModalRef = ref<InstanceType<typeof ColumnConfigModal>>();
|
||
|
||
const columnSchemeOptions = computed(() =>
|
||
columnConfigState.value.schemes.map((s) => ({ label: s.name, value: s.id })),
|
||
);
|
||
|
||
const activeColumnKeys = computed(() => getActiveScheme(columnConfigState.value).keys);
|
||
|
||
const hotColumns = computed(() =>
|
||
buildHotColumns(columnCatalog.value, activeColumnKeys.value, t),
|
||
);
|
||
|
||
const tableColumnsKey = computed(() => activeColumnKeys.value.join('|'));
|
||
|
||
function syncColumnCatalogFromRows(rows: Record<string, any>[]) {
|
||
columnCatalog.value = mergeDiscoveredColumns(getWaybillBaseCatalog(), rows);
|
||
}
|
||
|
||
function handleColumnSchemeChange(id: string) {
|
||
if (!columnConfigState.value.schemes.some((s) => s.id === id)) return;
|
||
columnConfigState.value = {
|
||
...columnConfigState.value,
|
||
activeId: id,
|
||
};
|
||
saveColumnConfigState(WAYBILL_MANAGE_COLUMN_STORAGE_KEY, columnConfigState.value);
|
||
}
|
||
|
||
function openColumnConfig() {
|
||
columnConfigModalRef.value?.open();
|
||
}
|
||
|
||
function handleColumnConfigApply(payload: { activeId: string; schemes: ColumnScheme[] }) {
|
||
columnConfigState.value = {
|
||
activeId: payload.activeId,
|
||
schemes: payload.schemes,
|
||
};
|
||
saveColumnConfigState(WAYBILL_MANAGE_COLUMN_STORAGE_KEY, columnConfigState.value);
|
||
Message.success(t('order.myOrders.columnConfig.applySuccess'));
|
||
}
|
||
|
||
const showRowPayAction = computed(
|
||
() => activeTab.value === 'pendingPayment' && hasPermission('Order.WaybillManage.Pay'),
|
||
);
|
||
|
||
const actionColumnWidth = computed(() => {
|
||
let width = 52 + 48; // 详情 + 轨迹
|
||
if (showRowPayAction.value) width += 48;
|
||
return width;
|
||
});
|
||
|
||
const actionRenderer = () => {
|
||
const parts = [
|
||
renderWarehouseActionBtn('view', t('common.viewDetail'), 'view'),
|
||
renderWarehouseActionBtn('track', t('order.myOrders.actions.viewTrack'), 'track'),
|
||
renderWarehouseActionBtn('advisor', t('nanaAssistant.actionAdvisor'), 'advisor'),
|
||
];
|
||
if (showRowPayAction.value) {
|
||
parts.push(renderWarehouseActionBtn('pay', t('order.myOrders.actions.pay'), 'pay'));
|
||
}
|
||
return `<div class="wh-action-group" style="display:inline-flex;flex-direction:row;flex-wrap:nowrap;align-items:center;justify-content:flex-start;gap:10px;white-space:nowrap;">${parts.join('')}</div>`;
|
||
};
|
||
|
||
const customCellRenderer = (row: number, col: number, value: any, rowData: any) => {
|
||
const colOffset = 2; // action + serialNo
|
||
const dataColIndex = col - colOffset;
|
||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null;
|
||
const colKey = hotColumns.value[dataColIndex].key;
|
||
|
||
if (colKey === 'orderStatus') {
|
||
const code = resolveOrderStatusCode(rowData);
|
||
const meta = getOrderStatusMeta(code);
|
||
const tagColor = meta?.tagColor ?? '#86909c';
|
||
const tagBgColor = meta?.tagBgColor ?? '#f2f3f5';
|
||
const statusText = meta ? t(meta.labelKey) : (rowData?.orderStatus || code || '--');
|
||
const handleCode = resolveOrderHandleStatusCode(rowData);
|
||
const handleMeta = getOrderHandleStatusMeta(handleCode);
|
||
const handleText =
|
||
rowData?.handleStatusName ||
|
||
(handleMeta ? t(handleMeta.labelKey) : '');
|
||
const showHandleTag = isOrderHandleSubTabParent(code) && handleText;
|
||
const handleTagHtml = showHandleTag
|
||
? `<span style="display:inline-flex;align-items:center;margin-left:6px;padding:1px 6px;border-radius:10px;font-size:11px;line-height:18px;color:${handleMeta?.tagColor ?? '#4e5969'};background:${handleMeta?.tagBgColor ?? '#f2f3f5'};border:1px solid ${handleMeta?.tagBgColor ?? '#e5e6eb'};">${handleText}</span>`
|
||
: '';
|
||
return `<span style="display:inline-flex;align-items:center;flex-wrap:wrap;gap:4px;max-width:100%;"><span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;color:${tagColor};background-color:${tagBgColor};white-space:nowrap;">${statusText}</span>${handleTagHtml}</span>`;
|
||
}
|
||
if (colKey === 'predeclareFailReason') {
|
||
const reason = rowData?.predeclareFailReason || value || '';
|
||
if (!reason) return null;
|
||
const guide = buildFailGuideFromReason(String(reason));
|
||
const display = guide.reason || String(reason);
|
||
const escaped = escapeHtml(display);
|
||
const title = escapeHtml(guide.rawReason || String(reason));
|
||
return `<span title="${title}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#f53f3f;font-size:12px;">${escaped}</span>`;
|
||
}
|
||
if (colKey === 'allocationPendingReason') {
|
||
const reason = rowData?.allocationPendingReason || value || '';
|
||
if (!reason) return null;
|
||
const escaped = escapeHtml(String(reason));
|
||
return `<span title="${escaped}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#ff7d00;font-size:12px;">${escaped}</span>`;
|
||
}
|
||
if (['isPaid', 'isCod', 'isIntercept', 'isRemote'].includes(colKey)) {
|
||
const rawVal = rowData?.[colKey] ?? value;
|
||
const yes = rawVal === true || rawVal === 1 || rawVal === '1';
|
||
const tagColor = yes ? '#00b42a' : '#86909c';
|
||
const tagBgColor = yes ? '#e8f8e8' : '#f2f3f5';
|
||
const text = yes ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no');
|
||
return `<span style="display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; color: ${tagColor}; background-color: ${tagBgColor}; white-space: nowrap;">${text}</span>`;
|
||
}
|
||
if (colKey === 'cargoAttribute') {
|
||
const raw = String(rowData?.cargoAttribute || value || '').trim().toLowerCase();
|
||
if (!raw) return null;
|
||
const map: Record<string, string> = {
|
||
general: 'order.myOrders.drawer.cargoAttribute.general',
|
||
electronics: 'order.myOrders.drawer.cargoAttribute.electronics',
|
||
'3c': 'order.myOrders.drawer.cargoAttribute.electronics',
|
||
sensitive: 'order.myOrders.drawer.cargoAttribute.sensitive',
|
||
};
|
||
const labelKey = map[raw];
|
||
return labelKey ? t(labelKey) : String(rowData?.cargoAttribute || value);
|
||
}
|
||
if (colKey === 'consigneeCountry') {
|
||
const code = rowData?.consigneeCountry || value || '';
|
||
const name = rowData?.consigneeCountryName || '';
|
||
if (code && name) return `${code} ${name}`;
|
||
return name || code || null;
|
||
}
|
||
if (['chargeableWeight', 'receivableAmount', 'declaredSum', 'codSum', 'claimAmount'].includes(colKey)) {
|
||
return value != null && value !== '' ? Number(value).toLocaleString() : null;
|
||
}
|
||
if (colKey === 'trackingNo') {
|
||
const no = String(value || '').trim();
|
||
if (!no) return '--';
|
||
const escaped = escapeHtml(no);
|
||
return `<span title="${t('order.myOrders.table.copyHint')}: ${escaped}" style="font-family:ui-monospace,Consolas,monospace;font-size:12px;">${escaped}</span>`;
|
||
}
|
||
// 首列添加 data-row-status 用于行背景样式
|
||
if (colKey === 'waybillNo') {
|
||
const statusCode = resolveOrderStatusCode(rowData);
|
||
const escaped = escapeHtml(String(value || '--'));
|
||
return `<span data-row-status="${statusCode}">${escaped}</span>`;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const handleActionClick = (row: any, _rowIndex: number, action?: string) => {
|
||
if (action === 'pay') {
|
||
handleRowPay(row);
|
||
return;
|
||
}
|
||
if (action === 'advisor') {
|
||
openAskAdvisor(router, buildWaybillContextFromRow(row, 'waybillManage'));
|
||
return;
|
||
}
|
||
if (action === 'track') {
|
||
void openDetailWithOptionalFetch(row, { tab: 'track' });
|
||
return;
|
||
}
|
||
openDetailWithOptionalFetch(row);
|
||
};
|
||
|
||
const handleSelectionChange = (rows: any[]) => {
|
||
selectedOrderRows.value = rows ?? [];
|
||
selectedRowCount.value = selectedOrderRows.value.length;
|
||
};
|
||
|
||
onMounted(() => {
|
||
nextTick(async () => {
|
||
await loadOrderList();
|
||
loadTabCounts();
|
||
await tryOpenOrderFromQuery();
|
||
});
|
||
});
|
||
|
||
async function tryOpenOrderFromQuery() {
|
||
const rawId = route.query.orderId;
|
||
const idStr = Array.isArray(rawId) ? rawId[0] : rawId;
|
||
const id = Number(idStr);
|
||
if (!Number.isFinite(id) || id <= 0) return;
|
||
const editRaw = route.query.edit;
|
||
const editStr = Array.isArray(editRaw) ? editRaw[0] : editRaw;
|
||
const editBasic = String(editStr || '') === '1';
|
||
try {
|
||
await openDetailWithOptionalFetch({ id }, { editBasic });
|
||
} finally {
|
||
const nextQuery = { ...route.query } as Record<string, any>;
|
||
delete nextQuery.orderId;
|
||
delete nextQuery.edit;
|
||
router.replace({ query: nextQuery });
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => route.query.status,
|
||
() => {
|
||
const next = resolveStatusFromQuery();
|
||
if (next === activeTab.value) return;
|
||
activeTab.value = next;
|
||
activeHandleTab.value = ORDER_HANDLE_STATUS_ALL;
|
||
currentPage.value = 1;
|
||
orderList.value = [];
|
||
selectedOrderRows.value = [];
|
||
selectedRowCount.value = 0;
|
||
loadOrderList();
|
||
loadTabCounts();
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => route.query.orderId,
|
||
(orderId) => {
|
||
if (!orderId) return;
|
||
void tryOpenOrderFromQuery();
|
||
},
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="waybill-manage-container">
|
||
<!-- 标签页导航和操作按钮 -->
|
||
<div class="tabs-actions-section">
|
||
<div class="tabs-container">
|
||
<div
|
||
v-for="tab in tabs"
|
||
:key="tab.key"
|
||
:class="['tab-item', { active: activeTab === tab.key }]"
|
||
@click="handleTabChange(tab.key)"
|
||
>
|
||
<span class="tab-label">{{ tab.label || $t(tab.labelKey) }}</span>
|
||
<span v-if="tab.showCount && tab.count != null" class="tab-count">({{ tab.count }})</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="actions-right">
|
||
<a-button
|
||
v-if="showMarkProcessedAction"
|
||
type="primary"
|
||
:loading="isActionLoading('markProcessed')"
|
||
:disabled="isActionBusy('markProcessed')"
|
||
@click="handleMarkProcessed"
|
||
>{{ t('order.myOrders.actions.markProcessed') }}</a-button>
|
||
<a-button
|
||
v-if="showPayAction"
|
||
class="btn-pay"
|
||
:loading="isActionLoading('pay')"
|
||
:disabled="isActionBusy('pay')"
|
||
@click="handlePay"
|
||
>{{ t('order.myOrders.actions.pay') }}</a-button>
|
||
<a-button
|
||
v-if="showPredeclareAction"
|
||
type="primary"
|
||
:loading="isActionLoading('predeclare')"
|
||
:disabled="isActionBusy('predeclare')"
|
||
@click="handlePreDeclare"
|
||
>{{ t('order.myOrders.actions.predeclare') }}</a-button>
|
||
<a-button
|
||
v-if="showDownstreamAction"
|
||
type="primary"
|
||
:loading="isActionLoading('downstream')"
|
||
:disabled="isActionBusy('downstream')"
|
||
@click="handleForwardDownstreamCreate"
|
||
>{{ t('order.myOrders.actions.forwardDownstreamCreate') }}</a-button>
|
||
<a-button
|
||
v-if="showPrintAction"
|
||
type="primary"
|
||
:loading="isActionLoading('singlePrint')"
|
||
:disabled="isActionBusy('singlePrint')"
|
||
@click="handleSinglePrint"
|
||
>{{ t('order.myOrders.actions.singlePrint') }}</a-button>
|
||
<a-button
|
||
v-if="showPrintAction"
|
||
type="primary"
|
||
:loading="isActionLoading('batchPrint')"
|
||
:disabled="isActionBusy('batchPrint')"
|
||
@click="handleBatchPrint"
|
||
>{{ t('order.myOrders.actions.batchPrint') }}</a-button>
|
||
<a-dropdown trigger="click" position="bl" :disabled="!!actionLoadingKey">
|
||
<a-button type="text" class="btn-more" :disabled="!!actionLoadingKey">
|
||
{{ t('order.myOrders.actions.more') }}
|
||
<icon-down class="btn-more-arrow" />
|
||
</a-button>
|
||
<template #content>
|
||
<a-doption
|
||
v-if="showExceptionCreateActions"
|
||
:disabled="isActionBusy('markProblematic')"
|
||
@click="onMoreActionClick('createProblematic')"
|
||
>{{ t('order.myOrders.actions.createProblematic') }}</a-doption>
|
||
<a-doption
|
||
v-if="showExceptionCreateActions"
|
||
:disabled="isActionBusy('markClaimed')"
|
||
@click="onMoreActionClick('createClaimed')"
|
||
>{{ t('order.myOrders.actions.createClaimed') }}</a-doption>
|
||
<a-doption
|
||
v-if="showCreateTrackAction"
|
||
:disabled="isActionBusy('track')"
|
||
@click="onMoreActionClick('createTrackOrder')"
|
||
>{{ t('order.myOrders.actions.createTrackOrder') }}</a-doption>
|
||
<a-doption
|
||
v-if="showInterceptActions"
|
||
@click="onMoreActionClick('intercept')"
|
||
>{{ t('order.myOrders.actions.intercept') }}</a-doption>
|
||
<a-doption
|
||
v-if="showInterceptActions"
|
||
@click="onMoreActionClick('cancelIntercept')"
|
||
>{{ t('order.myOrders.actions.cancelIntercept') }}</a-doption>
|
||
<a-doption
|
||
v-if="showDeleteAction"
|
||
@click="onMoreActionClick('delete')"
|
||
>{{ t('order.myOrders.actions.delete') }}</a-doption>
|
||
<a-doption @click="onMoreActionClick('export')">{{ t('order.myOrders.actions.export') }}</a-doption>
|
||
<a-doption
|
||
v-if="showModifyOrderAction"
|
||
@click="onMoreActionClick('modifyOrder')"
|
||
>{{ t('order.myOrders.actions.modifyOrder') }}</a-doption>
|
||
</template>
|
||
</a-dropdown>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="showHandleSubTabs"
|
||
:class="['handle-status-bar', `handle-status-bar--${activeTab}`]"
|
||
>
|
||
<div class="handle-status-bar__intro">
|
||
<span class="handle-status-bar__dot" />
|
||
<span class="handle-status-bar__title">{{ handleContextLabel }}</span>
|
||
</div>
|
||
<div class="handle-status-segments" role="tablist">
|
||
<button
|
||
v-for="seg in handleSegments"
|
||
:key="seg.key"
|
||
type="button"
|
||
role="tab"
|
||
:class="['handle-status-segment', { active: activeHandleTab === seg.key }]"
|
||
:aria-selected="activeHandleTab === seg.key"
|
||
@click="handleHandleTabChange(seg.key)"
|
||
>
|
||
<span class="handle-status-segment__label">{{ t(seg.labelKey) }}</span>
|
||
<span class="handle-status-segment__count">{{ seg.count }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 订单表格区域 -->
|
||
<div class="table-section">
|
||
<!-- 表格操作按钮和分页控制 -->
|
||
<div class="table-toolbar">
|
||
<div class="toolbar-left">
|
||
<a-space wrap :size="8" class="list-filters">
|
||
<a-select
|
||
v-model="adminCustomerId"
|
||
allow-clear
|
||
allow-search
|
||
:placeholder="t('warehouse.common.selectCustomer')"
|
||
:options="customerOptions"
|
||
style="width: 200px"
|
||
@change="handleSearch"
|
||
/>
|
||
<a-input
|
||
v-model="listKeyword"
|
||
allow-clear
|
||
:placeholder="t('order.myOrders.search.keywordPlaceholder')"
|
||
style="width: 240px"
|
||
@press-enter="handleSearch"
|
||
/>
|
||
<a-range-picker
|
||
v-model="createTimeRange"
|
||
style="width: 260px"
|
||
:placeholder="[
|
||
t('order.myOrders.search.dateStart'),
|
||
t('order.myOrders.search.dateEnd'),
|
||
]"
|
||
/>
|
||
<a-button type="primary" @click="handleSearch">
|
||
{{ t('order.myOrders.search.actions.search') }}
|
||
</a-button>
|
||
<a-button @click="handleResetSearch">
|
||
{{ t('order.myOrders.search.actions.reset') }}
|
||
</a-button>
|
||
</a-space>
|
||
<span class="selected-count">
|
||
{{ t('order.myOrders.selectedCount', { count: selectedRowCount }) }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="toolbar-right">
|
||
<div class="column-config-controls">
|
||
<a-select
|
||
:model-value="columnConfigState.activeId"
|
||
:options="columnSchemeOptions"
|
||
:placeholder="t('order.myOrders.columnConfig.scheme')"
|
||
style="width: 140px"
|
||
@change="handleColumnSchemeChange"
|
||
/>
|
||
<a-button @click="openColumnConfig">
|
||
{{ t('order.myOrders.columnConfig.open') }}
|
||
</a-button>
|
||
</div>
|
||
<div class="pagination-controls">
|
||
<PaginationPro
|
||
:total-count="totalCount"
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
@page-change="handlePageChange"
|
||
@page-size-change="handlePageSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="!listLoading && totalCount === 0"
|
||
class="list-empty-hint"
|
||
>
|
||
<span>{{ t('order.waybillManage.empty.hint') }}</span>
|
||
</div>
|
||
|
||
<div ref="tableContainerRef" class="list-wrap">
|
||
<a-spin :loading="listLoading" class="table-spin">
|
||
<HandsontablePro
|
||
:key="`${activeTab}-${currentPage}-${orderList.length}-${tableColumnsKey}`"
|
||
:columns="hotColumns"
|
||
:data="processedTableData"
|
||
:total="totalCount"
|
||
:current-page="currentPage"
|
||
:page-size="pageSize"
|
||
:show-action-column="true"
|
||
:action-column-position="'start'"
|
||
:action-column-width="actionColumnWidth"
|
||
:action-renderer="actionRenderer"
|
||
:custom-cell-renderer="customCellRenderer"
|
||
:stretch-columns="true"
|
||
:height="tableHeight"
|
||
@action-click="handleActionClick"
|
||
@selection-change="handleSelectionChange"
|
||
/>
|
||
</a-spin>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 订单详情抽屉(左侧) -->
|
||
<OrderDetailDrawer
|
||
ref="detailDrawerRef"
|
||
:order="currentOrder"
|
||
@updated="handleOrderDetailUpdated"
|
||
/>
|
||
<ColumnConfigModal
|
||
ref="columnConfigModalRef"
|
||
:catalog="columnCatalog"
|
||
:schemes="columnConfigState.schemes"
|
||
:active-id="columnConfigState.activeId"
|
||
:default-keys="WAYBILL_MANAGE_DEFAULT_KEYS"
|
||
@apply="handleColumnConfigApply"
|
||
/>
|
||
<OrderPrintModal v-model:visible="singlePrintVisible" mode="single" :items="singlePrintItems" />
|
||
<OrderPrintModal v-model:visible="batchPrintVisible" mode="batch" :items="batchPrintItems" />
|
||
|
||
<a-modal
|
||
v-model:visible="trackUpsertModalVisible"
|
||
title="轨迹同步结果"
|
||
:width="720"
|
||
:footer="false"
|
||
>
|
||
<pre class="track-upsert-result">{{ trackUpsertResultText }}</pre>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:visible="downstreamForwardModalVisible"
|
||
:title="t('order.myOrders.modal.downstreamForwardTitle')"
|
||
:width="800"
|
||
:footer="false"
|
||
>
|
||
<pre class="track-upsert-result">{{ downstreamForwardResultText }}</pre>
|
||
</a-modal>
|
||
|
||
<MarkProblematicModal
|
||
v-model:visible="problematicModalVisible"
|
||
:order-ids="exceptionOrderIds"
|
||
@success="handleExceptionActionSuccess"
|
||
/>
|
||
<MarkClaimedModal
|
||
v-model:visible="claimedModalVisible"
|
||
:order-ids="exceptionOrderIds"
|
||
@success="handleExceptionActionSuccess"
|
||
/>
|
||
<OrderPayConfirmModal
|
||
v-model:visible="payConfirmVisible"
|
||
:orders="payConfirmOrders"
|
||
@success="handlePayConfirmSuccess"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="less" scoped>
|
||
.waybill-manage-container {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 16px 16px 24px;
|
||
background-color: #f0f3f4;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-x: hidden;
|
||
overflow-y: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
|
||
h2 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #262626;
|
||
}
|
||
}
|
||
|
||
.tabs-actions-section {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 0 16px;
|
||
margin-bottom: 16px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
|
||
&:has(+ .handle-status-bar) {
|
||
margin-bottom: 0;
|
||
border-radius: 8px 8px 0 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
}
|
||
|
||
.handle-status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
padding: 12px 16px;
|
||
background: #fff;
|
||
border-radius: 0 0 8px 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
border: 1px solid #f0f0f0;
|
||
border-top: none;
|
||
flex-shrink: 0;
|
||
|
||
&--problematic {
|
||
border-left: 3px solid #f53f3f;
|
||
background: linear-gradient(90deg, #fff8f7 0%, #fff 28%);
|
||
}
|
||
|
||
&--claimed {
|
||
border-left: 3px solid #722ed1;
|
||
background: linear-gradient(90deg, #faf5ff 0%, #fff 28%);
|
||
}
|
||
|
||
&__intro {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
min-width: 120px;
|
||
}
|
||
|
||
&__dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--app-primary-color);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&--problematic &__dot {
|
||
background: #f53f3f;
|
||
}
|
||
|
||
&--claimed &__dot {
|
||
background: #722ed1;
|
||
}
|
||
|
||
&__title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.handle-status-segments {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 4px;
|
||
background: #f2f3f5;
|
||
border-radius: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
flex: 1;
|
||
}
|
||
|
||
.handle-status-segment {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
height: 32px;
|
||
padding: 0 14px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: #4e5969;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
color: #1d2129;
|
||
background: rgba(255, 255, 255, 0.72);
|
||
}
|
||
|
||
&.active {
|
||
color: var(--app-primary-color);
|
||
background: #fff;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__count {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 22px;
|
||
height: 20px;
|
||
padding: 0 6px;
|
||
border-radius: 10px;
|
||
background: rgba(0, 0, 0, 0.06);
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
color: inherit;
|
||
}
|
||
|
||
&.active &__count {
|
||
background: rgba(45, 140, 140, 0.12);
|
||
color: var(--app-primary-color);
|
||
}
|
||
}
|
||
|
||
.tabs-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 16px 20px;
|
||
cursor: pointer;
|
||
border-bottom: 3px solid transparent;
|
||
transition: all 0.3s;
|
||
position: relative;
|
||
|
||
&:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
&.active {
|
||
border-bottom-color: var(--app-primary-color);
|
||
|
||
.tab-label {
|
||
color: var(--app-primary-color);
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.tab-label {
|
||
font-size: 14px;
|
||
color: #262626;
|
||
transition: color 0.3s;
|
||
}
|
||
|
||
.tab-count {
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
|
||
.actions-right {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--app-button-gap, 8px);
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
flex-shrink: 0;
|
||
|
||
:deep(.arco-btn) {
|
||
height: 32px;
|
||
padding: 0 12px;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
line-height: 30px;
|
||
}
|
||
|
||
:deep(.arco-btn.btn-pay) {
|
||
color: #d46b08;
|
||
background: #fffbe6;
|
||
border-color: #ffd666;
|
||
|
||
&:hover:not(:disabled) {
|
||
color: #d46b08;
|
||
background: #fff7d1;
|
||
border-color: #ffc53d;
|
||
}
|
||
}
|
||
|
||
:deep(.arco-btn.btn-track) {
|
||
color: #fff;
|
||
background: #000;
|
||
border-color: #000;
|
||
|
||
&:hover:not(:disabled) {
|
||
color: #fff;
|
||
background: #262626;
|
||
border-color: #262626;
|
||
}
|
||
|
||
&:active:not(:disabled) {
|
||
color: #fff;
|
||
background: #141414;
|
||
border-color: #141414;
|
||
}
|
||
}
|
||
|
||
:deep(.arco-btn.btn-more) {
|
||
height: 32px;
|
||
padding: 0 4px;
|
||
color: #4e5969;
|
||
background: transparent !important;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
|
||
.btn-more-arrow {
|
||
font-size: 12px;
|
||
margin-left: 2px;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
&:hover:not(:disabled) {
|
||
color: var(--app-primary-color);
|
||
background: transparent !important;
|
||
border: none !important;
|
||
}
|
||
|
||
&:active:not(:disabled) {
|
||
color: var(--app-primary-color-hover);
|
||
background: transparent !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.table-section {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 0;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table-toolbar {
|
||
padding: 16px 16px 0;
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
|
||
.toolbar-left {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
|
||
.list-filters {
|
||
width: 100%;
|
||
}
|
||
|
||
.selected-count {
|
||
font-size: 13px;
|
||
color: #86909c;
|
||
|
||
.count-number {
|
||
color: #f53f3f;
|
||
font-weight: 600;
|
||
margin: 0 2px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
|
||
.column-config-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pagination-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
.list-empty-hint {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
margin: 0 16px 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
background: #f7f8fa;
|
||
color: #4e5969;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.table-spin {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
:deep(.arco-spin) {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
:deep(.arco-spin-children) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
.list-wrap {
|
||
width: 100%;
|
||
overflow: hidden;
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 0 16px 16px;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overscroll-behavior: contain;
|
||
touch-action: pan-x pan-y;
|
||
|
||
:deep(.handsontable-pro-container) {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.handsontable-wrapper {
|
||
width: 100%;
|
||
|
||
:deep(.handsontable) {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
border: none !important;
|
||
|
||
// 覆盖 Handsontable 默认的 table 边框
|
||
table {
|
||
border: none !important;
|
||
}
|
||
|
||
.ht_master,
|
||
.ht_clone_top,
|
||
.ht_clone_left,
|
||
.ht_clone_corner {
|
||
border: none !important;
|
||
}
|
||
|
||
.wtHolder {
|
||
border: none !important;
|
||
}
|
||
|
||
.htCore {
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
border: none !important;
|
||
|
||
td {
|
||
border-right: 0.5px solid #f0f0f0 !important;
|
||
border-bottom: 0.5px solid #f0f0f0 !important;
|
||
border-top: none !important;
|
||
border-left: none !important;
|
||
padding: 7px 16px;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
color: #262626;
|
||
transition: all 0.2s ease;
|
||
background: #fff;
|
||
text-align: center !important;
|
||
vertical-align: middle !important;
|
||
|
||
&:hover {
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
&:first-child {
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
}
|
||
}
|
||
|
||
tr {
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
td {
|
||
// hover时,如果单元格已经有背景色(状态背景),则稍微加深
|
||
// 否则使用默认hover颜色
|
||
&:not([style*="background-color"]) {
|
||
background-color: #fafafa;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
th {
|
||
background: #f5f5f5 !important; // 表头背景颜色改为淡灰色
|
||
border-right: 0.5px solid #f0f0f0 !important;
|
||
border-bottom: 0.5px solid #f0f0f0 !important;
|
||
border-top: none !important;
|
||
border-left: none !important;
|
||
}
|
||
|
||
// 根据订单状态设置行背景
|
||
tbody tr:has([data-row-status="problematic"]) td {
|
||
background-color: #fff1f0 !important;
|
||
}
|
||
tbody tr:has([data-row-status="pendingPayment"]) td {
|
||
background-color: #fffbe6 !important;
|
||
}
|
||
tbody tr:has([data-row-status="pendingAllocation"]) td {
|
||
background-color: #fff7e6 !important;
|
||
}
|
||
|
||
:deep(.wh-action-group) {
|
||
display: inline-flex !important;
|
||
flex-direction: row !important;
|
||
flex-wrap: nowrap !important;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 10px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
:deep(.wh-action-btn) {
|
||
display: inline-flex;
|
||
flex-shrink: 0;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
color: var(--color-text-3);
|
||
transition: color 0.2s ease, background-color 0.2s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
:deep(.wh-action-btn--view:hover),
|
||
:deep(.wh-action-btn--pay:hover),
|
||
:deep(.wh-action-btn--track:hover) {
|
||
color: var(--app-primary-color);
|
||
background: var(--app-primary-color-light, #e6f7f7);
|
||
}
|
||
|
||
:deep(.wh-action-btn--advisor:hover) {
|
||
color: #ffb400;
|
||
background: rgba(255, 180, 0, 0.14);
|
||
}
|
||
|
||
// 隐藏筛选图标的边框
|
||
.changeType {
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
// 隐藏筛选图标容器的边框
|
||
.ht_master .ht_clone_top th .changeType,
|
||
.ht_clone_top th .changeType,
|
||
.htCore th .changeType,
|
||
th .changeType {
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
background: transparent !important;
|
||
}
|
||
}
|
||
|
||
.ht_master .wtHolder {
|
||
overflow: auto;
|
||
}
|
||
|
||
.handsontableInput {
|
||
display: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 响应式设计
|
||
@media (max-width: 768px) {
|
||
.tabs-container {
|
||
overflow-x: auto;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.actions-right {
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
|
||
.action-btn {
|
||
font-size: 12px;
|
||
padding: 0 8px;
|
||
height: 28px;
|
||
}
|
||
}
|
||
|
||
.track-upsert-result {
|
||
margin: 0;
|
||
max-height: 480px;
|
||
overflow: auto;
|
||
padding: 12px;
|
||
background: #f7f8fa;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
</style> |