All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m59s
侧栏改为 Cursor 式账户面板(仓库眼标预览、语言切换、假邮箱过滤),运单「更多」支持发货后四态变更且妥投成功仅可进入。 Co-authored-by: Cursor <cursoragent@cursor.com>
3098 lines
105 KiB
Vue
3098 lines
105 KiB
Vue
<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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
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> |