front/src/views/order/waybillManage/index.vue
zzh 3642d650db
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m59s
feat: 客户侧栏账户坞与运单发货后状态变更
侧栏改为 Cursor 式账户面板(仓库眼标预览、语言切换、假邮箱过滤),运单「更多」支持发货后四态变更且妥投成功仅可进入。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 11:46:32 +08:00

3098 lines
105 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import {
ref,
computed,
onMounted,
nextTick,
watch,
reactive,
defineAsyncComponent,
} from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { IconCalendar, IconDown, IconQuestionCircleFill, IconSettings } 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 SlidingTabs from '@/components/SlidingTabs/index.vue';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
import { useI18n } from 'vue-i18n';
import {
adaptOrderListHeaderFilters,
withServerFilterColumns,
withSortableColumns,
} from '@/utils/headerFilterAdapter';
import {
MAX_HEADER_BATCH_VALUES,
ORDER_EXPORT_MAX_ROWS,
ORDER_LIST_DEFAULT_DAYS,
buildDefaultCreateTimeRange,
formatCreateTimeRangeParam,
isDefaultCreateTimeRange,
normalizeBatchInputText,
} from '@/utils/headerFilterLimits';
import {
postOrderList,
getOrderStatusCounts,
getOrderDetail,
postOrderPreDeclare,
postOrderIntercept,
postOrderCancelIntercept,
postOrderDelete,
postOrderExport,
postOrderTrackUpsertSmallBag,
postOrderDownstreamForwardCreateOne,
postOrderDownstreamGetFile,
DOWNSTREAM_GET_FILE_TYPES,
postOrderBatchJob,
waitOrderBatchJob,
formatBatchJobTrackUpsertResultText,
formatBatchJobDownstreamForwardResultText,
isOrderBatchQueueDisabledError,
formatBatchJobFailureMessage,
formatBatchJobSummaryMessage,
resolveBatchJobItemRowLabel,
postOrderHandleStatusUpdate,
type OrderTrackUpsertResult,
type OrderDownstreamForwardResult,
type OrderDownstreamGetFileResult,
type DownstreamGetFileType,
} from '@/api/order';
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
import { buildFailGuideFromReason } from '@/utils/parcelCreateFailGuide';
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 {
fetchPrintUrlForOrder,
fetchPrintUrlsForOrders,
getCachedPrintUrl,
} from '@/utils/printSessionCache';
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
import {
ORDER_STATUS_TABS,
ORDER_STATUS_ALL,
resolveOrderStatusCode,
isOrderDeletable,
isOrderNotShipped,
getOrderStatusMeta,
getPostShipStatusChangeTargets,
buildOrderStatusTagHtml,
buildOrderStatusSubTagHtml,
buildBooleanTagHtml,
} from '@/constants/orderStatus';
import { buildStatusTagHtml } from '@/utils/statusTag';
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, wrapWarehouseActions } from '@/utils/warehouseActionRenderer';
import { buildWaybillContextFromRow, openAskAdvisor } from '@/composables/useAskAdvisor';
/** 详情/弹层按需分包,避免进页就解析 100KB+ 抽屉与打印组件 */
const OrderDetailDrawer = defineAsyncComponent(
() => import('./components/OrderDetailDrawer.vue'),
);
const OrderPayConfirmModal = defineAsyncComponent(
() => import('../myOrders/components/OrderPayConfirmModal.vue'),
);
const MarkProblematicModal = defineAsyncComponent(
() => import('./components/MarkProblematicModal.vue'),
);
const MarkClaimedModal = defineAsyncComponent(
() => import('./components/MarkClaimedModal.vue'),
);
const ReallocateModal = defineAsyncComponent(
() => import('./components/ReallocateModal.vue'),
);
const DownstreamWeightImportModal = defineAsyncComponent(
() => import('./components/DownstreamWeightImportModal.vue'),
);
const ColumnConfigModal = defineAsyncComponent(
() => import('../components/ColumnConfigModal.vue'),
);
const OrderPrintModal = defineAsyncComponent(
() => import('@/components/PrintTemplate/OrderPrintModal.vue'),
);
const StockBindModal = defineAsyncComponent(
() => import('../myOrders/components/StockBindModal.vue'),
);
const ChangePostShipStatusModal = defineAsyncComponent(
() => import('./components/ChangePostShipStatusModal.vue'),
);
defineOptions({
name: 'WaybillManage',
});
const { t, te } = useI18n();
const { tableContainerRef, tableHeight } = useTableContainerHeight();
const { customerOptions, withCustomerId } = useWarehouseAdminCustomer();
// 订单详情抽屉(首次打开再挂载,避免进页就拉抽屉分包)
const detailDrawerMounted = ref(false);
const detailDrawerRef = ref<{
openDrawer?: (opts?: { tab?: string }) => void;
startBasicEdit?: () => void;
} | null>(null);
const currentOrder = ref<any>(null);
const columnConfigMounted = ref(false);
const downstreamWeightImportMounted = ref(false);
const columnConfigModalRef = ref<{ open?: () => void } | null>(null);
const downstreamWeightImportModalRef = ref<{ open?: () => void } | null>(null);
async function waitForRef<T>(
get: () => T | null | undefined,
ready: (v: T) => boolean,
retries = 40,
): Promise<T | null> {
for (let i = 0; i < retries; i++) {
await nextTick();
const v = get();
if (v != null && ready(v)) return v;
await new Promise((r) => setTimeout(r, 40));
}
return null;
}
async function ensureDetailDrawer() {
detailDrawerMounted.value = true;
return waitForRef(
() => detailDrawerRef.value,
(v) => typeof v.openDrawer === 'function',
);
}
/** 兼容后端 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;
}
function escapeHtml(text: string) {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function buildCustomerLabelMap() {
const map = new Map<number, string>();
for (const opt of customerOptions.value || []) {
const id = Number(opt.value);
if (Number.isFinite(id)) map.set(id, String(opt.label || ''));
}
return map;
}
/** 列表行与表格列对齐:只保留标量/展平字段,不保留嵌套树(Items/FeeList 等) */
function normalizeOrderRow(raw: any, customerLabelMap?: Map<number, string>) {
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 customerLabel =
(customerId != null && customerLabelMap?.get(Number(customerId))) ||
customerOptions.value.find((opt) => Number(opt.value) === Number(customerId))?.label ||
'';
const boolish = (v: any) => v === true || v === 1 || v === '1';
const hasInsuranceRaw = pick(raw, 'hasInsurance', 'HasInsurance');
let hasInsurance: boolean | undefined;
if (hasInsuranceRaw === true || hasInsuranceRaw === 1 || hasInsuranceRaw === '1') hasInsurance = true;
else if (hasInsuranceRaw === false || hasInsuranceRaw === 0 || hasInsuranceRaw === '0') {
hasInsurance = false;
}
const statusCode =
resolveOrderStatusCode(raw) || pick(raw, 'status', 'Status') || raw?.status || '';
const orderStatusText = pick(raw, 'orderStatus', 'OrderStatus') ?? raw?.orderStatus;
const handleStatus = pick(raw, 'handleStatus', 'HandleStatus') ?? raw?.handleStatus;
const handleStatusName = pick(raw, 'handleStatusName', 'HandleStatusName') ?? raw?.handleStatusName;
const predeclareFailReason = pick(raw, 'predeclareFailReason', 'PredeclareFailReason') ?? '';
const allocationPendingReason =
pick(raw, 'allocationPendingReason', 'AllocationPendingReason') ?? '';
const isPaid = boolish(isPaidRaw);
const isCod = boolish(pick(raw, 'isCod', 'IsCod'));
const isIntercept = boolish(pick(raw, 'isIntercept', 'IsIntercept'));
const isRemote = boolish(pick(raw, 'isRemote', 'IsRemote'));
const cargoAttribute = pick(raw, 'cargoAttribute', 'CargoAttribute') ?? raw?.cargoAttribute;
const fulfillmentType = pick(raw, 'fulfillmentType', 'FulfillmentType') ?? 'direct';
const stockBindStatus = pick(raw, 'stockBindStatus', 'StockBindStatus') ?? 'none';
const fulfillProgress = pick(raw, 'fulfillProgress', 'FulfillProgress') ?? '';
const domesticTrackingNos = pick(raw, 'domesticTrackingNos', 'DomesticTrackingNos') ?? '';
const primaryBinCode = pick(raw, 'primaryBinCode', 'PrimaryBinCode') ?? '';
const waybillNo = serviceNo ?? '';
const consigneeCountry =
pick(raw, 'consigneeCountry', 'ConsigneeCountry') ?? pick(r, 'country', 'Country') ?? '';
const consigneeCountryName = pick(raw, 'consigneeCountryName', 'ConsigneeCountryName') ?? '';
const statusMeta = getOrderStatusMeta(String(statusCode || ''));
const statusDisplay = statusMeta
? t(statusMeta.labelKey)
: orderStatusText || statusCode || '--';
const handleCode = resolveOrderHandleStatusCode({ handleStatus, status: statusCode });
const handleMeta = getOrderHandleStatusMeta(handleCode);
const handleText =
handleStatusName || (handleMeta ? t(handleMeta.labelKey) : '');
const showHandleTag = isOrderHandleSubTabParent(String(statusCode || '')) && handleText;
const orderStatusHtml = `<span style="display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;max-width:100%;">${buildOrderStatusTagHtml(String(statusDisplay), statusMeta)}${
showHandleTag ? buildOrderStatusSubTagHtml(String(handleText), handleMeta) : ''
}</span>`;
let predeclareFailReasonHtml: string | null = null;
if (predeclareFailReason) {
const guide = buildFailGuideFromReason(String(predeclareFailReason));
const display = guide.reason || String(predeclareFailReason);
predeclareFailReasonHtml = `<span title="${escapeHtml(guide.rawReason || String(predeclareFailReason))}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8a5c58;font-size:12px;">${escapeHtml(display)}</span>`;
}
const allocationPendingReasonHtml = allocationPendingReason
? `<span title="${escapeHtml(String(allocationPendingReason))}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#7a6a52;font-size:12px;">${escapeHtml(String(allocationPendingReason))}</span>`
: null;
return {
id: pick(raw, 'id', 'Id') ?? raw?.id,
// 展平寄件人
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') ?? '',
serviceNo: serviceNo ?? '',
clientNo: clientNo ?? '',
trackNo: trackNo ?? '',
waybillNo,
customerOrderNo: clientNo ?? '',
trackingNo: trackNo ?? '',
downstreamServiceNo: downstreamServiceNo ?? '',
productCode: productCode ?? '',
productName: productName ?? productCode ?? '',
channelCode: channelCode ?? '',
channelName: channelName ?? channelCode ?? '',
downstreamSupplierName: pick(raw, 'downstreamSupplierName', 'DownstreamSupplierName') ?? '',
downstreamSupplierCode: pick(raw, 'downstreamSupplierCode', 'DownstreamSupplierCode') ?? '',
downstreamAppKey: pick(raw, 'downstreamAppKey', 'DownstreamAppKey') ?? '',
downstreamAppId: pick(raw, 'downstreamAppId', 'DownstreamAppId') ?? '',
channelForecastCode: pick(raw, 'channelForecastCode', 'ChannelForecastCode') ?? '',
maxTimeliness: pick(raw, 'maxTimeliness', 'MaxTimeliness') ?? null,
salesProductRulesVersion: pick(raw, 'salesProductRulesVersion', 'SalesProductRulesVersion') ?? '',
salesProductPriceVersion: pick(raw, 'salesProductPriceVersion', 'SalesProductPriceVersion') ?? '',
isPaid,
isCod,
isIntercept,
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,
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') ?? '',
declaredSum: pick(raw, 'declaredSum', 'DeclaredSum') ?? raw?.declaredSum ?? null,
declaredCurrency: pick(raw, 'declaredCurrency', 'DeclaredCurrency') ?? raw?.declaredCurrency ?? '',
quantity: pick(raw, 'quantity', 'Quantity') ?? null,
codCurrency: pick(raw, 'codCurrency', 'CodCurrency') ?? '',
codSum: pick(raw, 'codSum', 'CodSum') ?? null,
additionalServices: pick(raw, 'additionalServices', 'AdditionalServices') ?? raw?.additionalServices,
hasInsurance,
hasAllocationTrace: boolish(pick(raw, 'hasAllocationTrace', 'HasAllocationTrace')),
allocationScheme: pick(raw, 'allocationScheme', 'AllocationScheme') ?? '',
forecastReceivableAmount:
pick(raw, 'forecastReceivableAmount', 'ForecastReceivableAmount') ?? null,
forecastPayableAmount: pick(raw, 'forecastPayableAmount', 'ForecastPayableAmount') ?? null,
orderStatus: orderStatusText,
predeclareFailReason,
allocationPendingReason,
status: statusCode,
handleStatus,
handleStatusName,
issueType: pick(raw, 'issueType', 'IssueType') ?? '',
issueTypeName: pick(raw, 'issueTypeName', 'IssueTypeName') ?? '',
issueTitle: pick(raw, 'issueTitle', 'IssueTitle') ?? '',
issueContent: pick(raw, 'issueContent', 'IssueContent') ?? '',
issueTemplateId: pick(raw, 'issueTemplateId', 'IssueTemplateId') ?? null,
issueRemark: pick(raw, 'issueRemark', 'IssueRemark') ?? '',
issueTime: pick(raw, 'issueTime', 'IssueTime') ?? null,
claimType: pick(raw, 'claimType', 'ClaimType') ?? '',
claimTypeName: pick(raw, 'claimTypeName', 'ClaimTypeName') ?? '',
claimAmount: pick(raw, 'claimAmount', 'ClaimAmount') ?? null,
claimCurrency: pick(raw, 'claimCurrency', 'ClaimCurrency') ?? '',
claimReason: pick(raw, 'claimReason', 'ClaimReason') ?? '',
claimTime: pick(raw, 'claimTime', 'ClaimTime') ?? null,
previousOrderStatus: pick(raw, 'previousOrderStatus', 'PreviousOrderStatus') ?? '',
cargoAttribute,
createTime: pick(raw, 'createTime', 'CreateTime') ?? raw?.createTime,
createBy: pick(raw, 'createBy', 'CreateBy') ?? '',
updateTime: pick(raw, 'updateTime', 'UpdateTime') ?? null,
updateBy: pick(raw, 'updateBy', 'UpdateBy') ?? '',
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,
warehouseId: pick(raw, 'warehouseId', 'WarehouseId') ?? null,
warehouse: pick(raw, 'warehouse', 'Warehouse') ?? raw?.warehouse,
customerId,
customerName:
pick(raw, 'customerName', 'CustomerName') ||
String(customerLabel).replace(/\s*\([^)]*\)\s*$/, '') ||
'',
customerCode:
pick(raw, 'customerCode', 'CustomerCode') ||
(String(customerLabel).match(/\(([^)]+)\)\s*$/)?.[1] ?? '') ||
'',
orderSource: pick(raw, 'orderSource', 'OrderSource') ?? '',
pickupTime: pick(raw, 'pickupTime', 'PickupTime') ?? null,
pickupOrderId: pick(raw, 'pickupOrderId', 'PickupOrderId') ?? null,
carrierWaybillNo: pick(raw, 'carrierWaybillNo', 'CarrierWaybillNo') ?? '',
pickupNo: pick(raw, 'pickupNo', 'PickupNo') ?? '',
inboundMethod: pick(raw, 'inboundMethod', 'InboundMethod') ?? '',
fulfillmentType,
fulfillmentSource: pick(raw, 'fulfillmentSource', 'FulfillmentSource') ?? '',
stockBindStatus,
stockBindLineCount: pick(raw, 'stockBindLineCount', 'StockBindLineCount') ?? 0,
stockBindQtyTotal: pick(raw, 'stockBindQtyTotal', 'StockBindQtyTotal') ?? 0,
domesticTrackingNos,
fulfillProgress,
primaryBinCode,
primarySkuCodes: pick(raw, 'primarySkuCodes', 'PrimarySkuCodes') ?? '',
customerSenderAddressId: pick(raw, 'customerSenderAddressId', 'CustomerSenderAddressId') ?? null,
requestId: pick(raw, 'requestId', 'RequestId') ?? '',
exportCustomsMode: pick(raw, 'exportCustomsMode', 'ExportCustomsMode') ?? '',
exportQualificationId: pick(raw, 'exportQualificationId', 'ExportQualificationId') ?? null,
// 预计算 HTML,减轻 Handsontable 每格重算
_orderStatusHtml: orderStatusHtml,
_predeclareFailReasonHtml: predeclareFailReasonHtml,
_allocationPendingReasonHtml: allocationPendingReasonHtml,
_isPaidHtml: buildBooleanTagHtml(
isPaid ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
isPaid,
),
_isCodHtml: buildBooleanTagHtml(
isCod ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
isCod,
),
_isInterceptHtml: buildBooleanTagHtml(
isIntercept ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
isIntercept,
),
_isRemoteHtml: buildBooleanTagHtml(
isRemote ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
isRemote,
),
_waybillNoHtml: `<span data-row-status="${escapeHtml(String(statusCode || ''))}">${escapeHtml(String(waybillNo || '--'))}</span>`,
_consigneeCountryText:
consigneeCountry && consigneeCountryName
? `${consigneeCountry} ${consigneeCountryName}`
: consigneeCountryName || consigneeCountry || null,
};
}
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 }, buildCustomerLabelMap());
} catch {
/* 保留列表行数据 */
}
}
currentOrder.value = nextRow;
const drawer = await ensureDetailDrawer();
drawer?.openDrawer?.({ tab: opts?.tab || 'basic' });
if (opts?.editBasic) {
await nextTick();
drawer?.startBasicEdit?.();
}
}
function handleOrderDetailUpdated(row: any) {
const customerLabelMap = buildCustomerLabelMap();
currentOrder.value = normalizeOrderRow(row, customerLabelMap);
const idx = orderList.value.findIndex((it) => Number(it.id) === Number(row?.id));
if (idx >= 0) {
// 替换数组引用,配合 deepWatchData=false 触发 Handsontable 刷新
const next = orderList.value.slice();
next[idx] = normalizeOrderRow({ ...orderList.value[idx], ...row }, customerLabelMap);
orderList.value = next;
}
}
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',
'pendingPayment',
'pendingShip',
'pendingOnline',
),
);
const showPrintAction = computed(() =>
isActiveTab(
'all',
'predeclared',
'pendingPayment',
'pendingShip',
'pendingOnline',
'shipping',
'deliverySuccess',
'deliveryFailed',
),
);
const showModifyOrderAction = computed(() =>
isActiveTab('all', 'draft', 'predeclared', 'pendingAllocation', 'pendingPayment', 'pendingShip'),
);
const showInterceptActions = computed(() =>
isActiveTab('all', 'predeclared', 'pendingAllocation', 'pendingPayment', 'pendingShip'),
);
const showDeleteAction = computed(() => isActiveTab('all', 'draft', 'predeclared'));
const showCreateTrackAction = computed(() =>
isActiveTab('all', 'pendingOnline', 'shipping', 'deliverySuccess', 'deliveryFailed'),
);
const showReallocateAction = computed(() =>
isActiveTab('all', 'predeclared', 'pendingAllocation', 'pendingPayment', 'pendingShip'),
);
const showStockBindAction = computed(
() => hasPermission('Order.WaybillManage.StockBind'),
);
const showChangeStatusAction = computed(
() =>
hasPermission('Order.WaybillManage.ChangeStatus') &&
isActiveTab('all', 'pendingOnline', 'shipping', 'deliverySuccess', 'deliveryFailed'),
);
const stockBindModalVisible = ref(false);
const stockBindOrderIds = ref<number[]>([]);
/** 本次打开绑定前是否含直发单(用于成功后提示已转代发) */
const stockBindDidConvert = ref(false);
const changeStatusModalVisible = ref(false);
const changeStatusOrderId = ref<number | null>(null);
const changeStatusCurrent = ref('');
const changeStatusWaybillLabel = ref('');
const problematicModalVisible = ref(false);
const claimedModalVisible = ref(false);
const reallocateModalVisible = ref(false);
const exceptionOrderIds = ref<number[]>([]);
const claimContext = ref<{
hasInsurance: boolean | null;
declaredSum: number | null;
declaredCurrency: string | null;
}>({
hasInsurance: null,
declaredSum: null,
declaredCurrency: null,
});
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,
})),
);
const slidingTabItems = computed(() =>
tabs.value.map((tab) => ({
key: tab.key,
label: t(tab.labelKey),
badge: tab.showCount && tab.count != null ? tab.count : undefined,
})),
);
async function loadTabCounts() {
try {
const { createTimeStart, createTimeEnd } = buildListFilterParams();
const { total, counts, handleCounts: handleCountsRaw } = await getOrderStatusCounts({
createTimeStart: createTimeStart || undefined,
createTimeEnd: createTimeEnd || undefined,
});
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(50);
const totalCount = ref(0);
/** 列表筛选:单号关键词 + 创建时间(默认近 90 天) */
const listKeyword = ref('');
const createTimeRange = ref<(Date | string)[] | undefined>([
...buildDefaultCreateTimeRange(),
]);
/** 用户明确放宽时间窗(清空)后,不再自动填回默认 */
const timeWindowRelaxed = ref(false);
const headerSearchParams = reactive<Record<string, string>>({});
const listSortField = ref<string | null>(null);
const listSortOrder = ref<'asc' | 'desc' | null>(null);
const ORDER_SERVER_FILTER_KEYS = [
'waybillNo',
'customerOrderNo',
'trackingNo',
'downstreamServiceNo',
'createTime',
'serviceNo',
'clientNo',
'trackNo',
'orderStatus',
'fulfillmentType',
'shippingMethod',
'serviceChannel',
'isPaid',
'receivableAmount',
'predeclareFailReason',
'allocationPendingReason',
'payTime',
'shipTime',
'receiveTime',
'signTime',
] as const;
const ORDER_SORTABLE_KEYS = [
'createTime',
'payTime',
'shipTime',
'receiveTime',
'signTime',
'waybillNo',
'customerOrderNo',
'trackingNo',
'receivableAmount',
] as const;
function mapSortFieldToApi(field: string | null | undefined): string | undefined {
if (!field) return undefined;
const map: Record<string, string> = {
waybillNo: 'serviceNo',
customerOrderNo: 'clientNo',
trackingNo: 'trackNo',
receivableAmount: 'payableAmount',
};
return map[field] || field;
}
function buildListFilterParams() {
const adapted = adaptOrderListHeaderFilters(headerSearchParams);
let createTimeStart = adapted.createTimeStart || '';
let createTimeEnd = adapted.createTimeEnd || '';
if (!createTimeStart && !createTimeEnd) {
const fromToolbar = formatCreateTimeRangeParam(createTimeRange.value);
if (fromToolbar) {
createTimeStart = fromToolbar.createTimeStart;
createTimeEnd = fromToolbar.createTimeEnd;
} else if (
!timeWindowRelaxed.value &&
!adapted.filters.some((f) =>
['serviceNo', 'clientNo', 'trackNo', 'downstreamServiceNo'].includes(f.field),
) &&
!(adapted.keyword || listKeyword.value.trim())
) {
const [s, e] = buildDefaultCreateTimeRange();
createTimeRange.value = [s, e];
createTimeStart = s.format('YYYY-MM-DD HH:mm:ss');
createTimeEnd = e.format('YYYY-MM-DD HH:mm:ss');
}
}
const toolbarBatch = normalizeBatchInputText(listKeyword.value);
let filters = adapted.filters.length ? [...adapted.filters] : [];
let keyword = '';
// 工具栏批量(多个单号)→ 运单号 IN;单值仍走 keyword 模糊(运单号/客户单号/跟踪号)
if (toolbarBatch.keptCount > 1) {
filters = filters.filter((f) => f.field !== 'serviceNo');
const values = toolbarBatch.value.split(',').map((v) => v.trim()).filter(Boolean);
filters.push({
field: 'serviceNo',
op: 'in',
value: values.join(','),
values,
});
} else {
keyword = (adapted.keyword || toolbarBatch.value || listKeyword.value.trim() || '').trim();
}
return {
keyword,
createTimeStart,
createTimeEnd,
filters: filters.length ? filters : undefined,
sortField: mapSortFieldToApi(listSortField.value),
sortOrder: listSortOrder.value || undefined,
};
}
const usingDefaultTimeWindow = computed(
() => !timeWindowRelaxed.value && isDefaultCreateTimeRange(createTimeRange.value),
);
const timeWindowHelp = computed(() => {
if (timeWindowRelaxed.value && !createTimeRange.value?.length) {
return `已取消时间限制,全量查询可能较慢。建议按单号精确查询,或恢复近 ${ORDER_LIST_DEFAULT_DAYS} 天。`;
}
return `默认仅展示近 ${ORDER_LIST_DEFAULT_DAYS} 天订单;按单号精确查询时可清空时间。`;
});
const dateFilterExpanded = ref(false);
function restoreDefaultTimeWindow() {
createTimeRange.value = [...buildDefaultCreateTimeRange()];
timeWindowRelaxed.value = false;
}
async function onCreateTimeRangeChange(val: any) {
if (val?.length === 2 && val[0] && val[1]) {
timeWindowRelaxed.value = false;
createTimeRange.value = val;
handleSearch();
return;
}
const adapted = adaptOrderListHeaderFilters(headerSearchParams);
const hasNo =
!!(adapted.keyword || listKeyword.value.trim()) ||
adapted.filters.some((f) =>
['serviceNo', 'clientNo', 'trackNo', 'downstreamServiceNo'].includes(f.field),
);
if (hasNo) {
timeWindowRelaxed.value = true;
createTimeRange.value = undefined;
handleSearch();
return;
}
Modal.confirm({
title: '清空时间范围?',
content: `未指定单号时查询全部历史可能较慢。建议保留近 ${ORDER_LIST_DEFAULT_DAYS} 天,或先输入单号再清空时间。`,
okText: '仍要清空',
cancelText: '保留近90天',
onOk: () => {
timeWindowRelaxed.value = true;
createTimeRange.value = undefined;
handleSearch();
},
onCancel: () => {
restoreDefaultTimeWindow();
},
});
}
function handleHeaderSearch() {
currentPage.value = 1;
selectedOrderRows.value = [];
selectedRowCount.value = 0;
loadOrderList();
loadTabCounts();
}
function handleSortChange(payload: { field: string | null; order: 'asc' | 'desc' | null }) {
listSortField.value = payload.field;
listSortOrder.value = payload.order;
currentPage.value = 1;
loadOrderList();
}
const orderList = ref<any[]>([]);
const listLoading = ref(false);
let listRequestSeq = 0;
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 singlePrintPreparing = ref(false);
watch(singlePrintVisible, (visible) => {
if (!visible) singlePrintPreparing.value = false;
});
const batchPrintVisible = ref(false);
const batchPrintPreparing = ref(false);
const singlePrintItems = ref<Array<{ id: number | string; label: string; url: string }>>([]);
const batchPrintItems = ref<Array<{ id: number | string; label: string; url: string }>>([]);
watch(batchPrintVisible, (visible) => {
if (!visible) batchPrintPreparing.value = false;
});
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);
};
const getDownstreamFileType = ref<DownstreamGetFileType>(21);
const getDownstreamFileModalVisible = ref(false);
const getDownstreamFileResultText = ref('');
function tx(key: string, fallback: string) {
return te(key) ? t(key) : fallback;
}
function getDownstreamFileTypeFallback(type: DownstreamGetFileType) {
if (type === 31) return '获取末端面单';
if (type === 180) return '获取POD';
return '获取下游面单';
}
const getDownstreamFileTypeLabel = computed(() => {
const hit = DOWNSTREAM_GET_FILE_TYPES.find((x) => x.value === getDownstreamFileType.value);
if (!hit) {
return tx(
'order.myOrders.actions.getDownstreamFileType.downstreamLabel',
'获取下游面单',
);
}
return tx(hit.labelKey, getDownstreamFileTypeFallback(hit.value));
});
function formatGetDownstreamFileResult(
waybillNo: string,
result: OrderDownstreamGetFileResult,
type: DownstreamGetFileType,
): string {
const lines = [
`【${waybillNo || '-'}】`,
result.success ? '成功' : '失败',
`standardCode:getFile`,
`type:${result.type ?? type}`,
];
if (result.supplierCode) {
lines.push(`supplierCode:${result.supplierCode}`);
}
if (result.downstreamServiceNo) {
lines.push(`服务商单号:${result.downstreamServiceNo}`);
}
if (result.trackNo) {
lines.push(`跟踪号:${result.trackNo}`);
}
if (result.storedFileUrl) {
lines.push(`已存文件:${result.storedFileUrl}`);
}
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');
}
const handleGetDownstreamFile = async (type: DownstreamGetFileType) => {
const rows = requireSelectedRows();
if (!rows.length) return;
getDownstreamFileType.value = type;
await withOrderActionLoading('getDownstreamFile', async () => {
const resultBlocks: string[] = [];
for (const row of rows) {
const id = getOrderRowId(row)!;
const waybillNo = row.waybillNo || row.serviceNo || String(id);
try {
const result = await postOrderDownstreamGetFile({ id, type });
resultBlocks.push(formatGetDownstreamFileResult(waybillNo, result, type));
} catch (err: any) {
resultBlocks.push(`【${waybillNo}】失败:${err?.message || '请求异常'}`);
}
}
getDownstreamFileResultText.value = resultBlocks.join('\n\n');
getDownstreamFileModalVisible.value = true;
await refreshListAndTabCounts();
}).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, filters, sortField, sortOrder } =
buildListFilterParams();
listLoading.value = true;
try {
const { items, total } = await postOrderList(
withCustomerId({
page: currentPage.value,
pageSize: pageSize.value,
status,
handleStatus,
keyword: keyword || undefined,
createTimeStart: createTimeStart || undefined,
createTimeEnd: createTimeEnd || undefined,
filters,
sortField,
sortOrder,
}),
);
if (requestSeq !== listRequestSeq) {
return;
}
const customerLabelMap = buildCustomerLabelMap();
const mapped = (items || []).map((row) => normalizeOrderRow(row, customerLabelMap));
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();
loadTabCounts();
};
const keywordBatchVisible = ref(false);
const keywordBatchInput = ref('');
const keywordBatchCount = computed(() =>
keywordBatchInput.value
.split(/[\n\r]+/)
.flatMap((line) => line.split(/[,,\t;;]/))
.map((v) => v.trim())
.filter(Boolean).length,
);
function openKeywordBatchModal() {
const current = listKeyword.value.trim();
keywordBatchInput.value = current
? current
.split(/[,,\n\r\t;;]+/)
.map((v) => v.trim())
.filter(Boolean)
.join('\n')
: '';
keywordBatchVisible.value = true;
}
function saveKeywordBatch() {
const { value, truncated, originalCount, keptCount } = normalizeBatchInputText(
keywordBatchInput.value,
);
listKeyword.value = value;
keywordBatchVisible.value = false;
handleSearch();
if (truncated) {
Message.warning(
`最多 ${MAX_HEADER_BATCH_VALUES} 个,已保留前 ${keptCount} 个(原 ${originalCount} 个)`,
);
} else if (keptCount > 0) {
Message.success(
keptCount > 1
? `已批量输入 ${keptCount} 个运单号`
: `已输入 ${keptCount} 个值`,
);
}
}
function handleKeywordBatchPaste(event: ClipboardEvent) {
const pastedText = event.clipboardData?.getData('text') || '';
if (!pastedText.includes(',') && !pastedText.includes(',')) return;
event.preventDefault();
const lines = pastedText
.split(/[,,\n\r]+/)
.map((line) => line.trim())
.filter(Boolean);
const textarea = event.target as HTMLTextAreaElement;
const start = textarea.selectionStart || 0;
const end = textarea.selectionEnd || 0;
const text = keywordBatchInput.value;
keywordBatchInput.value =
text.substring(0, start) + lines.join('\n') + text.substring(end);
}
// 当前页数据(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[]) {
if (rows.some((row) => resolveOrderStatusCode(row) !== 'pendingPayment')) {
Message.warning(t('order.myOrders.modal.payPendingOnly'));
return;
}
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 || '操作失败');
}
}
};
function requireNotShippedRows(rows: any[]): any[] | null {
const blocked = rows.filter((row) => !isOrderNotShipped(row));
if (!blocked.length) return rows;
Message.warning(t('order.myOrders.intercept.notShippedOnly'));
return null;
}
async function executeInterceptBatch(rows: any[]) {
try {
await withOrderActionLoading('intercept', async () => {
await runSelectedOrderBatch({
action: 'intercept',
rows,
loadingKey: 'intercept',
successMessage: t('order.myOrders.intercept.submitSuccess'),
syncRunner: async () => {
for (const row of rows) {
await postOrderIntercept({ id: getOrderRowId(row)! });
}
Message.success(t('order.myOrders.intercept.submitSuccess'));
await refreshListAndTabCounts();
},
});
});
} catch (e: any) {
if (e?.message !== 'busy') {
Message.error(e?.message || t('order.myOrders.intercept.submitFailed'));
}
}
}
async function executeCancelInterceptBatch(rows: any[]) {
try {
await withOrderActionLoading('cancelIntercept', async () => {
await runSelectedOrderBatch({
action: 'cancel-intercept',
rows,
loadingKey: 'cancelIntercept',
successMessage: t('order.myOrders.intercept.cancelSuccess'),
syncRunner: async () => {
for (const row of rows) {
await postOrderCancelIntercept({ id: getOrderRowId(row)! });
}
Message.success(t('order.myOrders.intercept.cancelSuccess'));
await refreshListAndTabCounts();
},
});
});
} catch (e: any) {
if (e?.message !== 'busy') {
Message.error(e?.message || t('order.myOrders.intercept.cancelFailed'));
}
}
}
const handleIntercept = () => {
const rows = requireSelectedRows();
if (!rows.length) return;
const eligible = requireNotShippedRows(rows);
if (!eligible) return;
Modal.confirm({
title: t('order.myOrders.intercept.confirmTitle'),
content: t('order.myOrders.intercept.confirmContent'),
okText: t('order.myOrders.intercept.confirmOk'),
cancelText: t('common.cancel'),
onOk: () => executeInterceptBatch(eligible),
});
};
const handleCancelIntercept = () => {
const rows = requireSelectedRows();
if (!rows.length) return;
const eligible = requireNotShippedRows(rows);
if (!eligible) return;
Modal.confirm({
title: t('order.myOrders.intercept.cancelConfirmTitle'),
content: t('order.myOrders.intercept.cancelConfirmContent'),
okText: t('order.myOrders.intercept.cancelConfirmOk'),
cancelText: t('common.cancel'),
onOk: () => executeCancelInterceptBatch(eligible),
});
};
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 handleRowPrint = async (row: any) => {
if (!row || !getOrderRowId(row)) {
Message.warning('请选择要打印的订单');
return;
}
const orderId = getOrderRowId(row)!;
const cachedUrl = getCachedPrintUrl(orderId);
if (isUsablePrintUrl(cachedUrl)) {
singlePrintVisible.value = true;
singlePrintPreparing.value = false;
singlePrintItems.value = [buildPrintItem(row, cachedUrl!)];
return;
}
const printUrlTask = fetchPrintUrlForOrder(orderId);
singlePrintVisible.value = true;
singlePrintItems.value = [buildPrintItem(row, '')];
singlePrintPreparing.value = true;
try {
const printUrl = await printUrlTask;
singlePrintPreparing.value = false;
if (!isUsablePrintUrl(printUrl)) {
Message.info('未获取到打印文件');
return;
}
singlePrintItems.value = [buildPrintItem(row, printUrl)];
} catch (e: any) {
singlePrintPreparing.value = false;
singlePrintVisible.value = false;
Message.error(e?.message || '获取面单失败');
}
};
const handleBatchPrint = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
if (batchPrintPreparing.value || isActionLoading('batchPrint')) return;
// 先打开弹窗:有缓存的立刻可看,其余并行拉取,避免等批量任务/串行接口
batchPrintItems.value = rows.map((row) => {
const orderId = getOrderRowId(row)!;
const cached = getCachedPrintUrl(orderId);
return buildPrintItem(row, isUsablePrintUrl(cached) ? cached! : '');
});
batchPrintVisible.value = true;
batchPrintPreparing.value = true;
try {
await withOrderActionLoading('batchPrint', async () => {
const urlMap = await fetchPrintUrlsForOrders(rows.map((row) => getOrderRowId(row)!));
const printItems = rows
.map((row) => {
const orderId = String(getOrderRowId(row)!);
const printUrl = urlMap.get(orderId);
return printUrl ? buildPrintItem(row, printUrl) : null;
})
.filter(Boolean) as Array<{ id: number | string; label: string; url: string }>;
batchPrintItems.value = printItems.length
? printItems
: rows.map((row) => buildPrintItem(row, ''));
if (!printItems.length) {
Message.info('未获取到可打印文件');
return;
}
if (printItems.length < rows.length) {
Message.warning(`已获取 ${printItems.length}/${rows.length} 张面单,其余失败可重试`);
}
});
} catch (e: any) {
if (e?.message !== 'busy') {
Message.error(e?.message || '批量打印失败');
}
} finally {
batchPrintPreparing.value = false;
}
};
const handleExport = async () => {
try {
await withOrderActionLoading('export', async () => {
const status = activeTab.value === ORDER_STATUS_ALL ? ORDER_STATUS_ALL : activeTab.value;
const { keyword, createTimeStart, createTimeEnd, filters, sortField, sortOrder } =
buildListFilterParams();
const { downloadUrl } = await postOrderExport(
withCustomerId({
status,
keyword,
createTimeStart,
createTimeEnd,
filters,
sortField,
sortOrder,
}),
);
if (downloadUrl) window.open(downloadUrl, '_blank');
Message.success(downloadUrl ? '导出链接已打开' : '导出已提交');
});
} catch (e: any) {
if (e?.message !== 'busy') {
Message.error(
e?.message || `导出失败(单次最多 ${ORDER_EXPORT_MAX_ROWS.toLocaleString()} 条)`,
);
}
}
};
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 openChangeStatusModal = () => {
const rows = requireSelectedRows();
if (!rows.length) return;
if (rows.length > 1) {
Message.warning(t('order.waybillManage.changeStatus.singleOnly'));
return;
}
const row = rows[0];
const id = getOrderRowId(row);
if (id == null) return;
const status = resolveOrderStatusCode(row);
if (!getPostShipStatusChangeTargets(status).length) {
Message.warning(t('order.waybillManage.changeStatus.notAllowed'));
return;
}
const waybillNo = String(row?.waybillNo ?? row?.WaybillNo ?? '').trim();
const serviceNo = String(row?.serviceNo ?? row?.ServiceNo ?? '').trim();
changeStatusOrderId.value = Number(id);
changeStatusCurrent.value = status;
changeStatusWaybillLabel.value = waybillNo || serviceNo || `#${id}`;
changeStatusModalVisible.value = true;
};
const openProblematicModal = () => {
const rows = requireSelectedRows();
if (!rows.length) return;
exceptionOrderIds.value = rows.map((row) => Number(getOrderRowId(row)!));
problematicModalVisible.value = true;
};
const openReallocateModal = () => {
const rows = requireSelectedRows();
if (!rows.length) return;
exceptionOrderIds.value = rows.map((row) => Number(getOrderRowId(row)!));
reallocateModalVisible.value = true;
};
async function openDownstreamWeightImport() {
downstreamWeightImportMounted.value = true;
const modal = await waitForRef(
() => downstreamWeightImportModalRef.value,
(v) => typeof v.open === 'function',
);
modal?.open?.();
}
function resolveClaimContextFromRow(row: any) {
let hasInsurance: boolean | null = null;
const hi = row?.hasInsurance ?? row?.HasInsurance;
if (hi === true || hi === 1 || hi === '1') hasInsurance = true;
else if (hi === false || hi === 0 || hi === '0') hasInsurance = false;
else {
const services = row?.additionalServices ?? row?.AdditionalServices;
if (Array.isArray(services)) {
hasInsurance = services.some((x: any) => String(x).toLowerCase() === 'insurance');
} else if (typeof services === 'string' && services.trim()) {
hasInsurance = services.toLowerCase().includes('insurance');
}
}
const sumRaw = row?.declaredSum ?? row?.DeclaredSum;
const sum = sumRaw == null || sumRaw === '' ? null : Number(sumRaw);
return {
hasInsurance,
declaredSum: Number.isFinite(sum as number) ? (sum as number) : null,
declaredCurrency: String(row?.declaredCurrency ?? row?.DeclaredCurrency ?? '').trim() || null,
};
}
const openClaimedModal = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
exceptionOrderIds.value = rows.map((row) => Number(getOrderRowId(row)!));
let ctxRow = rows[0];
const id = getOrderRowId(ctxRow);
if (rows.length === 1 && id != null) {
try {
const detail = await getOrderDetail(id);
ctxRow = normalizeOrderRow({ ...ctxRow, ...detail });
} catch {
/* keep list row */
}
}
claimContext.value = resolveClaimContextFromRow(ctxRow);
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'));
}
}
};
function isDirectFulfillmentRow(row: any) {
const ft = String(pick(row, 'fulfillmentType', 'FulfillmentType') || 'direct').trim();
return ft !== 'warehouse';
}
const openStockBindModal = () => {
const rows = requireSelectedRows();
if (!rows.length) return;
const needsConvert = rows.some(isDirectFulfillmentRow);
const open = () => {
stockBindDidConvert.value = needsConvert;
stockBindOrderIds.value = rows.map((row) => Number(getOrderRowId(row)!));
stockBindModalVisible.value = true;
};
if (needsConvert) {
Modal.confirm({
title: t('order.myOrders.stockBind.convertTitle'),
content: t('order.myOrders.stockBind.convertContent'),
onOk: () =>
new Promise<void>((resolve) => {
window.setTimeout(() => {
open();
resolve();
}, 0);
}),
});
return;
}
open();
};
const handleStockBindSuccess = async () => {
await refreshListAndTabCounts();
if (stockBindDidConvert.value) {
Message.info(t('order.myOrders.stockBind.convertedHint'));
}
};
const onMoreActionClick = (key: string) => {
if (key === 'createProblematic') return openProblematicModal();
if (key === 'createClaimed') return openClaimedModal();
if (key === 'reallocate') return openReallocateModal();
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 === 'importDownstreamWeight') return openDownstreamWeightImport();
if (key === 'modifyOrder') return handleModifyOrder();
if (key === 'changeStatus') return openChangeStatusModal();
if (key === 'stockBind') return openStockBindModal();
};
// 分页处理函数
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 activeColumnKeys = computed(() => getActiveScheme(columnConfigState.value).keys);
const hotColumns = computed(() => {
const fulfillmentTypeOptions = [
{ label: t('order.waybillManage.filter.fulfillmentDirect'), value: 'direct' },
{ label: t('order.waybillManage.filter.fulfillmentWarehouse'), value: 'warehouse' },
];
const numberKeys = new Set([
'waybillNo',
'customerOrderNo',
'trackingNo',
'downstreamServiceNo',
'serviceNo',
'clientNo',
'trackNo',
]);
return withSortableColumns(
withServerFilterColumns(
buildHotColumns(columnCatalog.value, activeColumnKeys.value, t, te),
ORDER_SERVER_FILTER_KEYS,
(col) => {
if (numberKeys.has(col.key)) {
return {
defaultTextOp: 'equals' as const,
multiValue: true,
headerMode: 'inline' as const,
};
}
if (
col.key === 'createTime' ||
col.key === 'payTime' ||
col.key === 'shipTime' ||
col.key === 'receiveTime' ||
col.key === 'signTime'
) {
return { filterType: 'date' as const, headerMode: 'icon' as const };
}
if (col.key === 'receivableAmount') {
return { filterType: 'number' as const, headerMode: 'icon' as const };
}
if (col.key === 'fulfillmentType') {
return {
filterType: 'enum' as const,
headerMode: 'icon' as const,
filterOptions: fulfillmentTypeOptions,
};
}
return {};
},
),
ORDER_SORTABLE_KEYS,
);
});
const tableColumnsKey = computed(() => activeColumnKeys.value.join('|'));
function syncColumnCatalogFromRows(rows: Record<string, any>[]) {
const next = mergeDiscoveredColumns(getWaybillBaseCatalog(), rows);
const prev = columnCatalog.value;
if (
prev.length === next.length &&
prev.every((col, idx) => col.key === next[idx]?.key)
) {
return;
}
columnCatalog.value = next;
}
async function openColumnConfig() {
columnConfigMounted.value = true;
const modal = await waitForRef(
() => columnConfigModalRef.value,
(v) => typeof v.open === 'function',
);
modal?.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 actionRenderer = (_rowData: any, rowIndex: number) => {
const parts = [
renderWarehouseActionBtn('view', t('common.viewDetail'), 'view'),
renderWarehouseActionBtn('track', t('order.myOrders.actions.viewTrack'), 'track'),
renderWarehouseActionBtn('advisor', t('nanaAssistant.actionAdvisor'), 'advisor'),
];
if (showPrintAction.value) {
parts.push(renderWarehouseActionBtn('print', t('order.myOrders.actions.print'), 'print'));
}
if (showRowPayAction.value) {
parts.push(renderWarehouseActionBtn('pay', t('order.myOrders.actions.pay'), 'pay'));
}
return wrapWarehouseActions(parts, rowIndex);
};
/** 行底色用 class,避免 :has() 在虚拟滚动复用行时闪一下 */
function resolveWaybillRowClassName(rowData: any) {
const code = resolveOrderStatusCode(rowData);
if (code === 'problematic' || code === 'pendingPayment' || code === 'pendingAllocation') {
return `row-status-${code}`;
}
return null;
}
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') return rowData?._orderStatusHtml || null;
if (colKey === 'predeclareFailReason') return rowData?._predeclareFailReasonHtml || null;
if (colKey === 'allocationPendingReason') return rowData?._allocationPendingReasonHtml || null;
if (colKey === 'isPaid') return rowData?._isPaidHtml || null;
if (colKey === 'isCod') return rowData?._isCodHtml || null;
if (colKey === 'isIntercept') return rowData?._isInterceptHtml || null;
if (colKey === 'isRemote') return rowData?._isRemoteHtml || null;
if (colKey === 'waybillNo') return rowData?._waybillNoHtml || null;
if (colKey === 'consigneeCountry') return rowData?._consigneeCountryText || null;
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];
const text = labelKey ? t(labelKey) : String(rowData?.cargoAttribute || value);
const tone = raw === 'sensitive' ? 'wait' : raw === 'general' ? 'mute' : 'neutral';
return buildStatusTagHtml(text, tone);
}
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>`;
}
if (colKey === 'fulfillmentType') {
const code = String(rowData?.fulfillmentType || value || 'direct').trim() || 'direct';
if (code === 'warehouse') {
return buildStatusTagHtml(t('order.fulfillOrders.badge'), 'wait');
}
return buildStatusTagHtml(t('order.waybillManage.fulfillmentType.direct'), 'mute');
}
if (colKey === 'stockBindStatus') {
const code = String(rowData?.stockBindStatus || value || 'none').trim() || 'none';
const key = `order.myOrders.stockBind.status.${code}`;
const text = t(key);
const label = text === key ? code : text;
const tone =
code === 'deducted' ? 'alert' : code === 'bound' ? 'ok' : code === 'released' ? 'wait' : 'mute';
return buildStatusTagHtml(label, tone);
}
if (colKey === 'fulfillProgress') {
const code = String(rowData?.fulfillProgress || value || '').trim();
if (!code) return '--';
const key = `order.fulfillOrders.progress.${code}`;
const text = t(key);
const label = text === key ? code : text;
const tone =
code === 'outboundDone'
? 'ok'
: code === 'arrivedPendingPick'
? 'wait'
: code === 'domesticInTransit'
? 'neutral'
: 'mute';
return buildStatusTagHtml(label, tone);
}
if (colKey === 'domesticTrackingNos') {
const nos = String(rowData?.domesticTrackingNos || value || '').trim();
if (!nos) return '--';
const escaped = escapeHtml(nos);
return `<span title="${escaped}" style="font-family:ui-monospace,Consolas,monospace;font-size:12px;">${escaped}</span>`;
}
if (colKey === 'primaryBinCode') {
const code = String(rowData?.primaryBinCode || value || '').trim();
return code || '--';
}
return null;
};
const handleActionClick = (row: any, _rowIndex: number, action?: string) => {
if (action === 'pay') {
handleRowPay(row);
return;
}
if (action === 'print') {
void handleRowPrint(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 Promise.all([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">
<!-- 单一工作面:Tabs/筛选 + 子状态 + 工具栏 + 表格 -->
<div class="table-section">
<div class="tabs-actions-section">
<div class="tabs-slot">
<SlidingTabs
:model-value="activeTab"
:items="slidingTabItems"
@change="handleTabChange"
/>
</div>
<div class="toolbar-left">
<div class="list-filters">
<a-input
v-model="listKeyword"
allow-clear
:placeholder="t('order.myOrders.search.keywordPlaceholder')"
title="双击支持批量输入"
style="width: 240px"
@dblclick="openKeywordBatchModal"
@press-enter="handleSearch"
/>
<span class="list-date-filter">
<a-tooltip v-if="!dateFilterExpanded" content="日期筛选">
<a-button class="list-date-toggle" @click="dateFilterExpanded = true">
<template #icon>
<IconCalendar />
</template>
</a-button>
</a-tooltip>
<a-range-picker
v-else
:model-value="createTimeRange"
style="width: 260px"
allow-clear
:placeholder="[
t('order.myOrders.search.dateStart'),
t('order.myOrders.search.dateEnd'),
]"
@change="onCreateTimeRangeChange"
/>
<a-tooltip :content="timeWindowHelp" position="top">
<IconQuestionCircleFill
class="list-date-help"
:class="{ 'is-warn': timeWindowRelaxed && !createTimeRange?.length }"
/>
</a-tooltip>
<a-button
v-if="timeWindowRelaxed || !usingDefaultTimeWindow"
type="text"
size="mini"
class="list-date-restore"
@click="restoreDefaultTimeWindow(); handleSearch()"
>
恢复近{{ ORDER_LIST_DEFAULT_DAYS }}天
</a-button>
<a-button
v-if="dateFilterExpanded"
type="text"
size="mini"
class="list-date-restore"
@click="dateFilterExpanded = false"
>
收起
</a-button>
</span>
<a-button class="app-btn-search" @click="handleSearch">
{{ t('order.myOrders.search.actions.search') }}
</a-button>
</div>
</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-toolbar">
<div class="actions-right">
<a-button
v-if="showMarkProcessedAction"
class="app-btn-toolbar"
:loading="isActionLoading('markProcessed')"
:disabled="selectedRowCount === 0 || isActionBusy('markProcessed')"
@click="handleMarkProcessed"
>{{ t('order.myOrders.actions.markProcessed') }}</a-button>
<a-button
v-if="showPayAction"
class="app-btn-toolbar"
:loading="isActionLoading('pay')"
:disabled="selectedRowCount === 0 || isActionBusy('pay')"
@click="handlePay"
>{{ t('order.myOrders.actions.pay') }}</a-button>
<a-button
v-if="showPredeclareAction"
class="app-btn-toolbar"
:loading="isActionLoading('predeclare')"
:disabled="selectedRowCount === 0 || isActionBusy('predeclare')"
@click="handlePreDeclare"
>{{ t('order.myOrders.actions.predeclare') }}</a-button>
<a-button
v-if="showDownstreamAction"
class="app-btn-toolbar"
:loading="isActionLoading('downstream')"
:disabled="selectedRowCount === 0 || isActionBusy('downstream')"
@click="handleForwardDownstreamCreate"
>{{ t('order.myOrders.actions.forwardDownstreamCreate') }}</a-button>
<a-button
v-if="showPrintAction"
class="app-btn-toolbar"
:loading="isActionLoading('batchPrint')"
:disabled="selectedRowCount === 0 || isActionBusy('batchPrint')"
@click="handleBatchPrint"
>{{ t('order.myOrders.actions.batchPrint') }}</a-button>
<a-button
v-if="showStockBindAction"
class="app-btn-toolbar"
:disabled="selectedRowCount === 0 || !!actionLoadingKey"
@click="openStockBindModal"
>{{ t('order.myOrders.actions.stockBindConvert') }}</a-button>
<a-dropdown
trigger="click"
position="bl"
:disabled="selectedRowCount === 0 || !!actionLoadingKey"
>
<a-button
class="app-btn-toolbar btn-more"
:loading="isActionLoading('getDownstreamFile')"
:disabled="selectedRowCount === 0 || isActionBusy('getDownstreamFile')"
>
{{ getDownstreamFileTypeLabel }}
<icon-down class="btn-more-arrow" />
</a-button>
<template #content>
<a-doption
v-for="opt in DOWNSTREAM_GET_FILE_TYPES"
:key="opt.value"
:disabled="selectedRowCount === 0 || isActionBusy('getDownstreamFile')"
@click="handleGetDownstreamFile(opt.value)"
>{{ tx(opt.labelKey, getDownstreamFileTypeFallback(opt.value)) }}</a-doption>
</template>
</a-dropdown>
<a-dropdown trigger="click" position="bl" :disabled="!!actionLoadingKey">
<a-button
class="app-btn-toolbar 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="selectedRowCount === 0 || isActionBusy('markProblematic')"
@click="onMoreActionClick('createProblematic')"
>{{ t('order.myOrders.actions.createProblematic') }}</a-doption>
<a-doption
v-if="showExceptionCreateActions"
:disabled="selectedRowCount === 0 || isActionBusy('markClaimed')"
@click="onMoreActionClick('createClaimed')"
>{{ t('order.myOrders.actions.createClaimed') }}</a-doption>
<a-doption
v-if="showReallocateAction"
:disabled="selectedRowCount === 0 || isActionBusy('reallocate')"
@click="onMoreActionClick('reallocate')"
>{{ t('order.myOrders.actions.reallocate') }}</a-doption>
<a-doption
v-if="showCreateTrackAction"
:disabled="selectedRowCount === 0 || isActionBusy('track')"
@click="onMoreActionClick('createTrackOrder')"
>{{ t('order.myOrders.actions.createTrackOrder') }}</a-doption>
<a-doption
v-if="showInterceptActions"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('intercept')"
>{{ t('order.myOrders.actions.intercept') }}</a-doption>
<a-doption
v-if="showInterceptActions"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('cancelIntercept')"
>{{ t('order.myOrders.actions.cancelIntercept') }}</a-doption>
<a-doption
v-if="showDeleteAction"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('delete')"
>{{ t('order.myOrders.actions.delete') }}</a-doption>
<a-doption
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('export')"
>{{ t('order.myOrders.actions.export') }}</a-doption>
<a-doption @click="onMoreActionClick('importDownstreamWeight')">
{{ t('order.waybillManage.importDownstream.action') }}
</a-doption>
<a-doption
v-if="showModifyOrderAction"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('modifyOrder')"
>{{ t('order.myOrders.actions.modifyOrder') }}</a-doption>
<a-doption
v-if="showChangeStatusAction"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('changeStatus')"
>{{ t('order.myOrders.actions.changeStatus') }}</a-doption>
</template>
</a-dropdown>
</div>
<div class="toolbar-right">
<span class="selected-count">
<i18n-t keypath="order.myOrders.selectedCount" tag="span" scope="global">
<template #count>
<span class="count-number">{{ selectedRowCount }}</span>
</template>
</i18n-t>
</span>
<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"
/>
<a-tooltip content="表头配置">
<a-button
class="app-btn-toolbar column-config-icon-btn"
@click="openColumnConfig"
>
<template #icon>
<IconSettings />
</template>
</a-button>
</a-tooltip>
</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="tableColumnsKey"
:columns="hotColumns"
:data="processedTableData"
:total="totalCount"
:current-page="currentPage"
:page-size="pageSize"
:show-action-column="true"
:action-renderer="actionRenderer"
:custom-cell-renderer="customCellRenderer"
:resolve-row-class-name="resolveWaybillRowClassName"
:stretch-columns="false"
:deep-watch-data="false"
:height="tableHeight"
:search-params="headerSearchParams"
sort-mode="remote"
:sort-field="listSortField"
:sort-order="listSortOrder"
@search="handleHeaderSearch"
@sort-change="handleSortChange"
@action-click="handleActionClick"
@selection-change="handleSelectionChange"
/>
</a-spin>
</div>
</div>
<!-- 订单详情抽屉(左侧):首次打开再挂载 -->
<OrderDetailDrawer
v-if="detailDrawerMounted"
ref="detailDrawerRef"
:order="currentOrder"
@updated="handleOrderDetailUpdated"
/>
<ColumnConfigModal
v-if="columnConfigMounted"
ref="columnConfigModalRef"
:catalog="columnCatalog"
:schemes="columnConfigState.schemes"
:active-id="columnConfigState.activeId"
:default-keys="WAYBILL_MANAGE_DEFAULT_KEYS"
@apply="handleColumnConfigApply"
/>
<OrderPrintModal
v-if="singlePrintVisible"
v-model:visible="singlePrintVisible"
mode="single"
:items="singlePrintItems"
:preparing="singlePrintPreparing"
/>
<OrderPrintModal
v-if="batchPrintVisible"
v-model:visible="batchPrintVisible"
mode="batch"
:items="batchPrintItems"
:preparing="batchPrintPreparing"
/>
<a-modal
v-model:visible="keywordBatchVisible"
title="批量输入单号"
:width="600"
unmount-on-close
@ok="saveKeywordBatch"
>
<div class="keyword-batch-content">
<p class="keyword-batch-tips">
每行一个;支持粘贴逗号分隔。多个单号按运单号精确匹配(最多
{{ MAX_HEADER_BATCH_VALUES }} 个);单个仍支持运单号 / 客户单号 / 跟踪号模糊搜索。
</p>
<a-textarea
v-model="keywordBatchInput"
placeholder="请输入,每行一个,或粘贴逗号分隔的数据"
:auto-size="{ minRows: 10, maxRows: 20 }"
@paste="handleKeywordBatchPaste"
/>
<div v-if="keywordBatchCount" class="keyword-batch-count">
已输入 {{ keywordBatchCount }} 个
<span v-if="keywordBatchCount > MAX_HEADER_BATCH_VALUES" class="is-warn">
(超过上限将截断)
</span>
</div>
</div>
</a-modal>
<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>
<a-modal
v-model:visible="getDownstreamFileModalVisible"
:title="tx('order.myOrders.modal.getDownstreamFileTitle', '获取下游文件结果')"
:width="800"
:footer="false"
>
<pre class="track-upsert-result">{{ getDownstreamFileResultText }}</pre>
</a-modal>
<MarkProblematicModal
v-if="problematicModalVisible"
v-model:visible="problematicModalVisible"
:order-ids="exceptionOrderIds"
@success="handleExceptionActionSuccess"
/>
<MarkClaimedModal
v-if="claimedModalVisible"
v-model:visible="claimedModalVisible"
:order-ids="exceptionOrderIds"
:has-insurance="claimContext.hasInsurance"
:declared-sum="claimContext.declaredSum"
:declared-currency="claimContext.declaredCurrency"
@success="handleExceptionActionSuccess"
/>
<ReallocateModal
v-if="reallocateModalVisible"
v-model:visible="reallocateModalVisible"
:order-ids="exceptionOrderIds"
@success="handleExceptionActionSuccess"
/>
<DownstreamWeightImportModal
v-if="downstreamWeightImportMounted"
ref="downstreamWeightImportModalRef"
@success="handleExceptionActionSuccess"
/>
<OrderPayConfirmModal
v-if="payConfirmVisible"
v-model:visible="payConfirmVisible"
:orders="payConfirmOrders"
@success="handlePayConfirmSuccess"
/>
<StockBindModal
v-model:visible="stockBindModalVisible"
:order-ids="stockBindOrderIds"
@success="handleStockBindSuccess"
/>
<ChangePostShipStatusModal
v-if="changeStatusModalVisible"
v-model:visible="changeStatusModalVisible"
:order-id="changeStatusOrderId"
:current-status="changeStatusCurrent"
:waybill-label="changeStatusWaybillLabel"
@success="handleExceptionActionSuccess"
/>
</div>
</template>
<style lang="less" scoped>
.waybill-manage-container {
width: 100%;
box-sizing: border-box;
padding: 16px 16px 24px;
background-color: var(--app-page-bg);
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: var(--color-text-1);
}
}
.tabs-actions-section {
background: transparent;
border-radius: 0;
padding: 10px 16px;
margin-bottom: 0;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
gap: 12px;
flex-shrink: 0;
min-height: 52px;
box-sizing: border-box;
.tabs-slot {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
overflow: hidden;
}
:deep(.t-tabs) {
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
width: max-content;
}
}
.handle-status-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 0;
padding: 12px 16px;
background: var(--app-card-bg);
border-radius: 0;
border: none;
border-bottom: 1px solid var(--color-border-2);
flex-shrink: 0;
&--problematic {
border-left: 3px solid #8a5c58;
background: linear-gradient(
90deg,
color-mix(in srgb, #8a5c58 10%, var(--app-card-bg)) 0%,
var(--app-card-bg) 28%
);
}
&--claimed {
border-left: 3px solid #5c656d;
background: linear-gradient(
90deg,
color-mix(in srgb, #5c656d 10%, var(--app-card-bg)) 0%,
var(--app-card-bg) 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: #8a5c58;
}
&--claimed &__dot {
background: #5c656d;
}
&__title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-1);
white-space: nowrap;
}
}
.handle-status-segments {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px;
background: var(--color-fill-2);
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: var(--color-text-2);
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
color: var(--color-text-1);
background: var(--color-fill-1);
}
&.active {
color: var(--app-primary-color);
background: var(--app-card-bg);
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: var(--color-fill-3);
font-size: 12px;
line-height: 1;
color: inherit;
}
&.active &__count {
background: var(--app-primary-color-light);
color: var(--app-primary-color);
}
}
.actions-right {
display: flex;
flex-wrap: wrap;
gap: var(--app-button-gap, 8px);
align-items: center;
flex: 1;
min-width: 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-more) {
display: inline-flex;
align-items: center;
gap: 2px;
.btn-more-arrow {
font-size: 12px;
margin-left: 2px;
}
}
}
.toolbar-left {
flex: 0 0 auto;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0;
max-width: 100%;
height: 32px;
.list-filters {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
height: 32px;
:deep(.arco-input-wrapper),
:deep(.arco-btn) {
height: 32px;
}
:deep(.arco-input-wrapper) {
display: inline-flex;
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
}
.list-date-toggle {
width: 32px;
padding: 0;
}
.list-date-filter {
display: inline-flex;
align-items: center;
gap: 4px;
height: 32px;
}
.list-date-help {
font-size: 14px;
color: var(--color-text-3);
cursor: help;
flex-shrink: 0;
&:hover {
color: var(--app-primary-color);
}
&.is-warn {
color: rgb(var(--warning-6));
}
}
.list-date-restore {
padding: 0 4px;
height: auto;
color: var(--app-primary-color);
}
}
.table-section {
background: var(--app-card-bg);
border-radius: 8px;
border: 1px solid var(--app-card-border);
padding: 0;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-toolbar {
padding: 12px 16px 0;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
.selected-count {
font-size: 13px;
color: var(--color-text-3);
white-space: nowrap;
line-height: 32px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
.column-config-icon-btn {
width: 32px;
padding: 0;
}
}
}
}
.list-empty-hint {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
margin: 0 16px 12px;
padding: 10px 12px;
border-radius: 8px;
background: var(--color-fill-2);
color: var(--color-text-2);
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 var(--color-border-2) !important;
border-bottom: 0.5px solid var(--color-border-2) !important;
border-top: none !important;
border-left: none !important;
padding: 7px 16px;
font-size: 13px;
line-height: 1.4;
color: var(--color-text-1);
transition: all 0.2s ease;
background: var(--app-card-bg);
text-align: center !important;
vertical-align: middle !important;
&:hover {
background-color: var(--color-fill-2);
}
&:first-child {
padding-left: 8px;
padding-right: 8px;
}
}
tr {
transition: all 0.2s ease;
&:hover {
td {
// hover时,如果单元格已经有背景色(状态背景),则稍微加深
// 否则使用默认hover颜色
&:not([style*="background-color"]) {
background-color: var(--color-fill-2);
}
}
}
}
th {
background: var(--color-fill-2) !important;
border-right: 0.5px solid var(--color-border-2) !important;
border-bottom: 0.5px solid var(--color-border-2) !important;
border-top: none !important;
border-left: none !important;
}
// 根据订单状态设置行背景(class 由 resolveRowClassName 写入)
tbody tr.row-status-problematic td {
background-color: #fff1f0 !important;
}
tbody tr.row-status-pendingPayment td {
background-color: #ffece8 !important;
}
tbody tr.row-status-pendingAllocation td {
background-color: #fff7e6 !important;
}
// 隐藏筛选图标的边框
.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) {
.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: var(--color-fill-2);
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
.keyword-batch-content {
.keyword-batch-tips {
margin: 0 0 12px;
color: var(--color-text-3);
font-size: 13px;
line-height: 1.5;
}
.keyword-batch-count {
margin-top: 8px;
font-size: 12px;
color: var(--color-text-3);
.is-warn {
color: rgb(var(--warning-6));
}
}
}
</style>