feat: 新增 9610 申报页与批量清单/报关单操作

同步 SPA 回退、权限目录、拣货绑定组件及相关系统页调整。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-08-30 17:46:04 +08:00
parent b5edbb8447
commit cf4d2c4a3f
38 changed files with 4066 additions and 478 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -10,7 +10,7 @@
const PHONE = '18123959245';
const EMAIL = 'bd01@1st56.com';
const WECHAT = 'ydgjzzh';
const ICP = '粤ICP备19018720号';
const ICP = '粤ICP备2025463970号-2';
const ADDRESS_ZH = '深圳宝安区福永怀德翠岗5区36栋';
const ADDRESS_EN = 'Bldg 36, Cuigang 5, Fuyong, Bao’an, Shenzhen';

View File

@ -115,8 +115,8 @@ document.addEventListener('DOMContentLoaded', () => {
const languageStorageKey = 'ide-language';
const pageTitleMap = {
'IDE|Faster & Simpler': 'IDE|更快 · 更简单',
'IDE|Immediate Delivery Express — Cross-Border Logistics': 'IDE|Immediate Delivery Express — 跨境物流',
'IDE | Global Digital One-Stop Cross-Border Logistics': 'IDE | 全球数字化一站式跨境物流服务商',
'IDE|Immediate Delivery Express — Cross-Border Logistics': 'IDE | 全球数字化一站式跨境物流服务商',
'Faster & Simpler': '更快 · 更简单',
'|Faster & Simpler': '更快 · 更简单',
'IDE - About': 'IDE - 关于我们',

View File

@ -5,12 +5,12 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>IDE|Immediate Delivery Express — 跨境物流</title>
<title>IDE | 全球数字化一站式跨境物流服务商</title>
<meta content="IDE 面向中小卖家的跨境物流。全球数字化一站式跨境物流服务商。注册登记后即可询价,一口价不砍,专属顾问跟到签收。" name="description">
<meta content="IDE, 跨境物流, 中小卖家, 9610, 疑难单, 出口退税, 欧美专线, 东南亚专线" name="keywords">
<!-- Favicons -->
<link href="assets/img/favicon.png" rel="icon">
<!-- Favicons(logo1 已入库;favicon.png 由 postbuild 同步) -->
<link href="assets/img/logo1.png" rel="icon" type="image/png">
<link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
<!-- Vendor CSS Files -->

View File

@ -370,6 +370,17 @@ const menuData = [
hideInMenu: false,
parentRoute: '/order',
},
{
level: 2,
name: '9610订单',
permissionCode: 'Order.Customs9610',
route: '/order/customs9610Order',
component: 'views/order/customs9610Order/index',
icon: null,
order: 5,
hideInMenu: false,
parentRoute: '/order',
},
// 一级菜单 - 渠道
{

View File

@ -73,6 +73,20 @@ server {
access_log off;
}
location = /favicon.ico {
try_files /favicon.png =404;
expires 7d;
add_header Cache-Control "public";
access_log off;
}
location = /favicon.png {
try_files /favicon.png =404;
expires 7d;
add_header Cache-Control "public";
access_log off;
}
# ============ 客户 / 管理端登录入口 ============
location = /login {
try_files /index.html =200;
@ -166,6 +180,9 @@ server {
location ^~ /order/pickupOrder {
try_files /index.html =200;
}
location ^~ /order/customs9610Order {
try_files /index.html =200;
}
location ^~ /order/batchJobs {
try_files /index.html =200;
}

View File

@ -39,6 +39,20 @@ server {
access_log off;
}
location = /favicon.ico {
try_files /favicon.png =404;
expires 7d;
add_header Cache-Control "public";
access_log off;
}
location = /favicon.png {
try_files /favicon.png =404;
expires 7d;
add_header Cache-Control "public";
access_log off;
}
location = /login {
try_files /index.html =200;
}
@ -85,6 +99,7 @@ server {
location ^~ /order/fulfillOrders { try_files /index.html =200; }
location ^~ /order/waybillManage { try_files /index.html =200; }
location ^~ /order/pickupOrder { try_files /index.html =200; }
location ^~ /order/customs9610Order { try_files /index.html =200; }
location ^~ /order/batchJobs { try_files /index.html =200; }
location ^~ /order/templateMapping { try_files /index.html =200; }
location ^~ /order/platformStore { try_files /index.html =200; }

View File

@ -43,6 +43,7 @@
"@codemirror/state": "^6.5.4",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.17",
"@fontsource/zcool-qingke-huangyou": "^5.3.0",
"@handsontable/vue3": "^16.2.0",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "^9.3.0",

8
pnpm-lock.yaml generated
View File

@ -32,6 +32,9 @@ importers:
'@codemirror/view':
specifier: ^6.39.17
version: 6.39.17
'@fontsource/zcool-qingke-huangyou':
specifier: ^5.3.0
version: 5.3.0
'@handsontable/vue3':
specifier: ^16.2.0
version: 16.2.0(handsontable@16.2.0)(vue@3.5.18(typescript@4.9.5))
@ -922,6 +925,9 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@fontsource/zcool-qingke-huangyou@5.3.0':
resolution: {integrity: sha512-6/ld8x/knq1NjDQxLSyg9pDnBPOmtWDb30ZTFk+n8OxfkBoUvPAxh3aV/6+UFGmA5CekJfcgpUwd2/x/weLvSQ==}
'@handsontable/pikaday@1.0.0':
resolution: {integrity: sha512-1VN6N38t5/DcjJ7y7XUYrDx1LuzvvzlrFdBdMG90Qo1xc8+LXHqbWbsTEm5Ec5gXTEbDEO53vUT35R+2COmOyg==}
@ -7893,6 +7899,8 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@fontsource/zcool-qingke-huangyou@5.3.0': {}
'@handsontable/pikaday@1.0.0': {}
'@handsontable/vue3@16.2.0(handsontable@16.2.0)(vue@3.5.18(typescript@4.9.5))':

View File

@ -74,6 +74,12 @@ function copyOfficialSite() {
fs.copyFileSync(src9610, path.join(dist, '9610.html'));
}
const faviconSrc = path.join(siteDir, 'assets', 'img', 'logo1.png');
if (fs.existsSync(faviconSrc)) {
fs.copyFileSync(faviconSrc, path.join(siteDir, 'assets', 'img', 'favicon.png'));
fs.copyFileSync(faviconSrc, path.join(dist, 'favicon.png'));
}
console.log('[postbuild] copied official site to dist/site (assets under /site/assets)');
}

View File

@ -0,0 +1,37 @@
import { axiosInstance } from '@/api/rbac'
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
function postOk(path: string, body: Record<string, unknown>) {
return axiosInstance.post(omsUrl(path), body).then((res) => {
assertBusinessOk(res.data)
return unwrapData(res.data)
})
}
function getData(path: string, params?: Record<string, unknown>) {
return axiosInstance.get(omsUrl(path), { params }).then((res) => unwrapData(res.data))
}
export const getCustoms9610List = (body: Record<string, unknown>) =>
axiosInstance.post(omsUrl('/order/customs-9610/list'), body).then((res) => parseListPayload(res.data))
export const getCustoms9610StatusCounts = (params?: { customerId?: number }) =>
getData('/order/customs-9610/status-counts', params as Record<string, unknown> | undefined)
export const getCustoms9610Detail = (id: number | string) =>
getData('/order/customs-9610/detail', { id })
export const updateCustoms9610 = (body: Record<string, unknown>) =>
postOk('/order/customs-9610/update', body)
export const declareCustoms9610 = (id: number | string) =>
postOk('/order/customs-9610/declare', { id })
export const declareCustoms9610Batch = (ids: Array<number | string>) =>
postOk('/order/customs-9610/declare-batch', { ids })
export const fetchCustoms9610ManifestBatch = (ids: Array<number | string>) =>
postOk('/order/customs-9610/fetch-manifest-batch', { ids })
export const fetchCustoms9610DeclarationFormBatch = (ids: Array<number | string>) =>
postOk('/order/customs-9610/fetch-declaration-form-batch', { ids })

View File

@ -30,6 +30,9 @@ export const createPickupOrderV2 = (body: PickupOrderCreateBody) =>
export const getPickupOrderList = (body: Record<string, unknown>) =>
axiosInstance.post(omsUrl('/order/pickup-order/list'), body).then((res) => parseListPayload(res.data))
export const getPickupOrderStatusCounts = (params?: { customerId?: number }) =>
getData('/order/pickup-order/status-counts', params as Record<string, unknown> | undefined)
export const getPickupOrderDetail = (id: number | string) =>
getData('/order/pickup-order/detail', { id })

View File

@ -12,33 +12,38 @@
<template v-else>
<div class="shell-header">
<div class="header-left">
<div v-if="$slots.icon" class="header-icon">
<slot name="icon" />
</div>
<div class="header-text">
<div class="header-title-row">
<h2 class="header-title">{{ title }}</h2>
<a-tag
v-for="tag in tags"
:key="tag.label"
:color="tag.color"
size="small"
>
{{ tag.label }}
</a-tag>
<div class="header-main">
<div class="header-left">
<div v-if="$slots.icon" class="header-icon">
<slot name="icon" />
</div>
<p v-if="subtitle" class="header-subtitle">{{ subtitle }}</p>
<div class="header-text">
<div class="header-title-row">
<h2 class="header-title">{{ title }}</h2>
<a-tag
v-for="tag in tags"
:key="tag.label"
:color="tag.color"
size="small"
>
{{ tag.label }}
</a-tag>
</div>
<p v-if="subtitle" class="header-subtitle">{{ subtitle }}</p>
</div>
</div>
<div v-if="stats.length" class="header-stats">
<div v-for="stat in stats" :key="stat.label" class="stat-item">
<span class="stat-value">{{ stat.value }}</span>
<span class="stat-label">{{ stat.label }}</span>
</div>
</div>
<div v-if="$slots.actions" class="header-actions">
<slot name="actions" />
</div>
</div>
<div v-if="stats.length" class="header-stats">
<div v-for="stat in stats" :key="stat.label" class="stat-item">
<span class="stat-value">{{ stat.value }}</span>
<span class="stat-label">{{ stat.label }}</span>
</div>
</div>
<div v-if="$slots.actions" class="header-actions">
<slot name="actions" />
<div v-if="$slots['header-extra']" class="header-extra">
<slot name="header-extra" />
</div>
</div>
@ -51,12 +56,10 @@
@click="onTabClick(tab.key)"
>
<span>{{ tab.label }}</span>
<a-badge
<span
v-if="tab.badge != null && tab.badge !== ''"
:count="tab.badge"
:max-count="999"
class="tab-badge"
/>
class="tab-count"
>{{ tab.badge }}</span>
</div>
</div>
<div v-if="$slots['tabs-extra']" class="tabs-extra">
@ -181,17 +184,23 @@ function onTabClick(key: string) {
.shell-header {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px 24px 16px;
flex-direction: column;
gap: 14px;
padding: 16px 24px;
border-bottom: 1px solid var(--color-border-2);
flex-shrink: 0;
}
.header-main {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.header-left {
display: flex;
align-items: flex-start;
align-items: center;
gap: 14px;
flex: 1;
min-width: 200px;
@ -217,11 +226,15 @@ function onTabClick(key: string) {
}
.header-subtitle {
margin: 6px 0 0;
margin: 4px 0 0;
font-size: 13px;
color: var(--color-text-3);
}
.header-extra {
width: 100%;
}
.header-stats {
display: flex;
gap: 12px;
@ -296,26 +309,38 @@ function onTabClick(key: string) {
}
}
.tab-badge :deep(.arco-badge-number) {
.tab-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: var(--color-text-2);
box-shadow: none;
font-variant-numeric: tabular-nums;
}
.tab-item.active .tab-badge :deep(.arco-badge-number) {
.tab-item.active .tab-count {
background: rgba(var(--primary-6), 0.12);
color: var(--app-primary-color, #2d8c8c);
}
.tabs-extra {
padding: 8px 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
.shell-body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 20px 24px 24px;
padding: 16px 24px 20px;
}
.shell-body--loading {

View File

@ -273,12 +273,16 @@ export const PAGE_BUTTON_PARENT_ALIASES: Record<string, string> = {
'System.DataPerm.Audit': 'Rbac.DataPerm.Audit',
'System.DataPerm.Templates': 'Rbac.DataPerm.Template',
'System.DataPerm.Users': 'Rbac.DataPerm',
'Rbac.email': 'System.BatchEmail',
'System.BatchEmail.Use': 'System.BatchEmail',
'System.BatchEmail': 'System.BatchEmail',
'Api.ApiTest': 'Api.ApiTest',
'Api.Test.Query': 'Api.ApiTest',
'Channel.LabelDesign': 'Channel.LabelDesign',
'Order.BatchJobs.Query': 'Order.BatchJobs',
'Order.WaybillManage.Query': 'Order.WaybillManage',
'Order.Pickup.Query': 'Order.Pickup',
'Order.Customs9610.Query': 'Order.Customs9610',
'Order.SenderAddress.Query': 'Order.SenderAddress',
'Order.PlatformStore.Query': 'Order.PlatformStore',
'Order.PlatformStoreOrder.Query': 'Order.PlatformStoreOrder',
@ -591,6 +595,15 @@ const CORE_GROUPS: PageButtonGroup[] = [
{ code: 'Search', name: '搜索' },
{ code: 'Create', name: '创建揽收订单' },
]),
B('Order.Customs9610', [
{ code: 'Query', name: '查询' },
{ code: 'Search', name: '搜索' },
{ code: 'Update', name: '编辑' },
{ code: 'Declare', name: '申报' },
{ code: 'DeclareBatch', name: '批量申报' },
{ code: 'FetchManifestBatch', name: '批量获取清单数据' },
{ code: 'FetchDeclarationFormBatch', name: '批量获取报关单' },
]),
B('Order.SenderAddress', [
{ code: 'Query', name: '查询' },
{ code: 'Search', name: '搜索' },

View File

@ -119,6 +119,7 @@ export default {
'menu.order.fulfillOrders': 'Fulfillment Orders',
'menu.order.waybillManage': 'Waybill Management',
'menu.order.pickupOrder': 'Pickup sheets',
'menu.order.customs9610Order': '9610 Orders',
'menu.order.batchJobs': 'Batch Jobs',
'menu.order.platformStore': 'Platform Stores',
'menu.order.platformStoreOrder': 'Store Orders',
@ -464,7 +465,7 @@ export default {
'user.senderAddress.table.addressName': 'Alias',
'user.senderAddress.table.senderName': 'Contact',
'user.senderAddress.table.phone': 'Phone',
'user.senderAddress.table.country': 'Country',
'user.senderAddress.table.destCountry': 'Country',
'user.senderAddress.table.province': 'Province',
'user.senderAddress.table.city': 'City',
'user.senderAddress.table.address': 'Address',
@ -475,7 +476,7 @@ export default {
'user.senderAddress.form.senderName': 'Contact',
'user.senderAddress.form.phone': 'Phone',
'user.senderAddress.form.company': 'Company',
'user.senderAddress.form.countryCode': 'Country Code',
'user.senderAddress.form.destCountryCode': 'Country Code',
'user.senderAddress.form.province': 'Province',
'user.senderAddress.form.city': 'City',
'user.senderAddress.form.zipCode': 'Zip Code',
@ -491,6 +492,79 @@ export default {
'user.senderAddress.message.typeRequired': 'Address type is required',
'user.senderAddress.message.senderNameRequired': 'Contact is required',
'user.senderAddress.message.addressRequired': 'Street address is required',
'order.customs9610.status.pending': 'Pending declare',
'order.customs9610.status.declared': 'Declared',
'order.customs9610.search.keyword': 'Service No. / Client No. / Customs order No.',
'order.customs9610.empty.hint': 'No 9610 declarations yet. Orders created with ExportCustomsMode=9610Summary will appear here.',
'order.customs9610.table.serviceNo': 'Service No.',
'order.customs9610.table.clientNo': 'Client No.',
'order.customs9610.table.destCountry': 'Dest country',
'order.customs9610.table.grossWeight': 'Gross weight',
'order.customs9610.table.currency': 'Currency',
'order.customs9610.table.businessType': 'Business type',
'order.customs9610.table.status': 'Status',
'order.customs9610.table.customsOrderNo': 'Customs order No.',
'order.customs9610.table.declareTime': 'Declare time',
'order.customs9610.table.createTime': 'Created',
'order.customs9610.actions.declare': 'Declare',
'order.customs9610.actions.batchDeclare': 'Batch declare',
'order.customs9610.actions.batchFetchManifest': 'Batch fetch manifest',
'order.customs9610.actions.batchFetchDeclarationForm': 'Batch fetch declaration form',
'order.customs9610.modal.edit': 'Edit 9610 declaration',
'order.customs9610.modal.detail': '9610 declaration detail',
'order.customs9610.field.platformCode': 'Platform',
'order.customs9610.field.businessType': 'Business type',
'order.customs9610.field.preRecordFlag': 'Pre-record flag',
'order.customs9610.field.clientNo': 'Client No.',
'order.customs9610.field.serviceNo': 'Service No.',
'order.customs9610.field.destCountry': 'Dest country',
'order.customs9610.field.currency': 'Currency',
'order.customs9610.field.grossWeight': 'Gross weight (KG)',
'order.customs9610.field.freight': 'Freight',
'order.customs9610.field.ecomCorpCode': 'E-commerce corp code',
'order.customs9610.field.ecomCorpName': 'E-commerce corp name',
'order.customs9610.field.producerCode': 'Producer code',
'order.customs9610.field.producerName': 'Producer name',
'order.customs9610.field.region': 'Region',
'order.customs9610.field.agentDeclare': 'Agent declare',
'order.customs9610.field.actualSellerCode': 'Actual seller code',
'order.customs9610.field.shipmentNo': 'Shipment No.',
'order.customs9610.field.customsOrderNo': 'Customs order No.',
'order.customs9610.field.isSummarySimple': 'Summary simple',
'order.customs9610.items.title': 'Order items',
'order.customs9610.items.add': 'Add row',
'order.customs9610.items.sku': 'SKU',
'order.customs9610.items.cnName': 'CN name',
'order.customs9610.items.exHsCode': 'Export HS code',
'order.customs9610.items.declareElement': 'Declare element',
'order.customs9610.items.unitPrice': 'Unit price',
'order.customs9610.items.quantity': 'Quantity',
'order.customs9610.confirm.declareTitle': 'Confirm declare',
'order.customs9610.confirm.declareContent': 'This will call the API-center upstream interface. Continue?',
'order.customs9610.confirm.batchDeclareTitle': 'Batch declare',
'order.customs9610.confirm.batchDeclareContent': 'Declare {count} selected pending records?',
'order.customs9610.confirm.batchManifestTitle': 'Batch fetch manifest',
'order.customs9610.confirm.batchManifestContent':
'Fetch manifest data for {count} selected declared records?',
'order.customs9610.confirm.batchDeclarationFormTitle': 'Batch fetch declaration form',
'order.customs9610.confirm.batchDeclarationFormContent':
'Fetch declaration forms for {count} selected declared records?',
'order.customs9610.message.loadFailed': 'Load failed',
'order.customs9610.message.saveSuccess': 'Saved',
'order.customs9610.message.saveFailed': 'Save failed',
'order.customs9610.message.declareSuccess': 'Declared',
'order.customs9610.message.declareFailed': 'Declare failed',
'order.customs9610.message.batchResult': 'Success {success}, failed {fail}',
'order.customs9610.message.fetchManifestFailed': 'Failed to fetch manifest',
'order.customs9610.message.fetchDeclarationFormFailed': 'Failed to fetch declaration form',
'order.customs9610.message.selectFirst': 'Select records first',
'order.customs9610.message.platformCodeRequired': 'Platform is required',
'order.customs9610.message.clientServiceRequired': 'Client No. and Service No. are required',
'order.customs9610.message.weightRequired': 'Gross weight must be > 0',
'order.customs9610.message.currencyDestCountryRequired': 'Currency and dest country are required',
'order.customs9610.message.preRecordRequired': 'Pre-record flag is required for form type B',
'order.customs9610.message.aFieldsRequired': 'Region and agentDeclare are required for form type A',
'order.customs9610.message.itemsRequired': 'SKU and CN name are required',
'order.pickupOrder.search.keyword': 'System No. / Carrier Waybill / Contact',
'order.pickupOrder.search.customer': 'Customer',
'order.pickupOrder.search.status': 'Status',
@ -510,7 +584,23 @@ export default {
'order.pickupOrder.actions.create': 'Create Pickup Order',
'order.pickupOrder.actions.submit': 'Submit',
'order.pickupOrder.modal.create': 'Create Pickup Order',
'order.pickupOrder.hint.createFromMyOrders': 'To merge multiple parcels, select them in My Orders and click Create Pickup',
'order.pickupOrder.hint.createFromMyOrders': 'Bind pre-declared waybills or create a new one; multi-select must share a warehouse',
'order.pickupOrder.create.lead': 'Fill pickup and cargo details. A waybill and pickup sheet will be created.',
'order.pickupOrder.create.leadBind': 'Select unbound pre-declared waybills (same warehouse), then fill pickup details.',
'order.pickupOrder.create.modeBind': 'Bind existing waybills',
'order.pickupOrder.create.modeNew': 'Create new waybill pickup',
'order.pickupOrder.create.sectionPickup': 'Pickup details',
'order.pickupOrder.create.sectionCargo': 'Cargo details',
'order.pickupOrder.create.sectionOrders': 'Select waybills',
'order.pickupOrder.create.orderKeywordPh': 'Service No. / Client No.',
'order.pickupOrder.create.selectedOrders': '{count} selected · Warehouse: {warehouse}',
'order.pickupOrder.create.warehouseFromOrders': 'Select waybills first (warehouse auto-filled)',
'order.pickupOrder.create.needSelectOrders': 'Select at least one waybill',
'order.pickupOrder.form.cargoNamePlaceholder': 'Enter cargo name (Chinese)',
'order.pickupOrder.form.clientNoPlaceholder': 'Optional; auto-generated if empty',
'order.pickupOrder.message.noPickupProduct': 'No domestic pickup sales product available. Configure it under Channel first.',
'order.pickupOrder.message.contactRequired': 'Please complete phone and email before creating a pickup',
'order.pickupOrder.message.loadOrdersFailed': 'Failed to load bindable waybills',
'order.pickupOrder.table.pickupNo': 'System No.',
'order.pickupOrder.table.carrierWaybillNo': 'Carrier Waybill No.',
'order.pickupOrder.table.customer': 'Customer',
@ -532,6 +622,8 @@ export default {
'order.pickupOrder.table.createTime': 'Created',
'order.pickupOrder.detail.title': 'Pickup Detail',
'order.pickupOrder.detail.orders': 'Linked Waybills',
'order.pickupOrder.detail.sectionInfo': 'Pickup info',
'order.pickupOrder.detail.close': 'Close',
'order.pickupOrder.form.freight': 'Pickup Freight',
'order.pickupOrder.form.otherFee': 'Other Fee',
'order.pickupOrder.form.carrierWaybillNoPh': 'Domestic carrier waybill number',
@ -549,6 +641,7 @@ export default {
'order.pickupOrder.rebind.remove': 'Remove linked waybills',
'order.pickupOrder.rebind.add': 'Add waybills',
'order.pickupOrder.rebind.addPh': 'Enter service / client numbers, separated by comma or newline',
'order.pickupOrder.rebind.addHint': 'Select unbound pre-declared waybills from the same warehouse',
'order.pickupOrder.rebind.emptyBound': 'No linked waybills',
'order.pickupOrder.status.draft': 'Draft',
'order.pickupOrder.status.scheduled': 'Scheduled',
@ -586,11 +679,49 @@ export default {
'order.pickupOrder.message.cancelOk': 'Cancelled',
'order.pickupOrder.message.cancelConfirm': 'Cancel this pickup sheet? Linked waybills will be unbound.',
'order.pickupOrder.message.rebindOk': 'Rebind succeeded',
'order.pickupOrder.message.rebindEmpty': 'Select waybills to remove or enter numbers to add',
'order.pickupOrder.message.rebindEmpty': 'Select waybills to remove or pick waybills to add',
'order.pickupOrder.message.rebindNotFound': 'Waybill not found: {nos}',
'order.pickupOrder.message.pickedOk': 'Marked as picked',
'order.pickupOrder.message.inboundOk': 'Inbound confirmed',
'order.pickupOrder.message.actionFailed': 'Action failed',
'order.pickupOrder.search.warehouse': 'Warehouse',
'order.pickupOrder.search.expectStart': 'Expect pickup from',
'order.pickupOrder.search.expectEnd': 'Expect pickup to',
'order.pickupOrder.table.settlement': 'Settlement',
'order.pickupOrder.settlementStatus.none': 'None',
'order.pickupOrder.settlementStatus.pending': 'Pending',
'order.pickupOrder.settlementStatus.settled': 'Settled',
'order.pickupOrder.actions.reschedule': 'Reschedule',
'order.pickupOrder.actions.batchConfirmFreight': 'Batch Confirm Freight',
'order.pickupOrder.actions.batchSchedule': 'Batch Draft Forecast',
'order.pickupOrder.actions.copyServiceNos': 'Copy all service Nos.',
'order.pickupOrder.create.emptyOrders': 'No bindable pre-declared waybills',
'order.pickupOrder.create.goPredeclared': 'Go to pre-declared',
'order.pickupOrder.createNext.title': 'Created',
'order.pickupOrder.createNext.desc': 'Pickup sheet {no} created. Next steps:',
'order.pickupOrder.createNext.goSchedule': 'Draft forecast',
'order.pickupOrder.createNext.goPay': 'Pay now',
'order.pickupOrder.createNext.viewDetail': 'View detail',
'order.pickupOrder.payCheck.amount': 'Amount due: ¥{amount}',
'order.pickupOrder.payCheck.balance': 'Balance: ¥{balance}',
'order.pickupOrder.payCheck.balanceUnknown': 'Balance: unavailable',
'order.pickupOrder.payCheck.shortfall': 'Shortfall: ¥{shortfall}',
'order.pickupOrder.payCheck.payAnyway': 'Pay anyway',
'order.pickupOrder.detail.timeline': 'Activity',
'order.pickupOrder.timeline.createTime': 'Created',
'order.pickupOrder.timeline.payTime': 'Paid',
'order.pickupOrder.timeline.actualPickupTime': 'Actual pickup',
'order.pickupOrder.timeline.updateTime': 'Updated',
'order.pickupOrder.message.rescheduleOk': 'Rescheduled',
'order.pickupOrder.message.noServiceNos': 'No service numbers to copy',
'order.pickupOrder.message.copyServiceNosOk': 'Service numbers copied',
'order.pickupOrder.message.copyFailed': 'Copy failed',
'order.pickupOrder.message.batchFreightNoCandidates': 'No selected sheets eligible for freight confirm',
'order.pickupOrder.message.batchFreightHint': 'Confirm freight for {count} unpaid pickup sheet(s)',
'order.pickupOrder.message.batchFreightResult': 'Batch freight: {success} succeeded, {fail} failed',
'order.pickupOrder.message.batchScheduleNoCandidates': 'No draft/scheduled sheets selected for forecast',
'order.pickupOrder.message.batchScheduleHint': 'Draft-forecast {count} pickup sheet(s)',
'order.pickupOrder.message.batchScheduleResult': 'Batch schedule: {success} succeeded, {fail} failed',
'order.platformStoreOrder.status.all': 'All',
'order.platformStoreOrder.status.pending': 'Pending',
'order.platformStoreOrder.status.toShip': 'To Ship',
@ -599,19 +730,19 @@ export default {
'order.platformStoreOrder.status.canceled': 'Canceled',
'order.platformStoreOrder.status.completed': 'Completed',
'order.platformStoreOrder.table.platform': 'Platform',
'order.platformStoreOrder.table.country': 'Country',
'order.platformStoreOrder.table.destCountry': 'Country',
'order.platformStoreOrder.table.itemCount': 'Items',
'order.platformStoreOrder.table.shippingLabel': 'Label No',
'order.platformStoreOrder.table.warehouse': 'Warehouse',
'order.platformStoreOrder.table.lastSync': 'Last Sync',
'order.platformStoreOrder.table.orderNo': 'Order No',
'order.platformStoreOrder.table.customsOrderNo': 'Order No',
'order.platformStoreOrder.table.store': 'Store',
'order.platformStoreOrder.table.status': 'Status',
'order.platformStoreOrder.table.amount': 'Amount',
'order.platformStoreOrder.table.orderTime': 'Order Time',
'order.platformStoreOrder.table.linkedServiceNo': 'Linked Service No',
'order.platformStoreOrder.form.product': 'Sales Product',
'order.platformStoreOrder.search.orderNo': 'Order No',
'order.platformStoreOrder.search.customsOrderNo': 'Order No',
'order.platformStoreOrder.summary.pending': 'Pending',
'order.platformStoreOrder.summary.toShip': 'To Ship',
'order.platformStoreOrder.drawer.detail': 'Order Detail',
@ -628,7 +759,7 @@ export default {
'order.platformStoreOrder.recipient.phone': 'Phone',
'order.platformStoreOrder.recipient.telePhone': 'Tel',
'order.platformStoreOrder.recipient.email': 'Email',
'order.platformStoreOrder.recipient.country': 'Country',
'order.platformStoreOrder.recipient.destCountry': 'Country',
'order.platformStoreOrder.recipient.province': 'State/Province',
'order.platformStoreOrder.recipient.city': 'City',
'order.platformStoreOrder.recipient.zipCode': 'Zip',
@ -849,7 +980,7 @@ export default {
'system.mcpTools.selectHint': 'Select a tool on the left',
'system.mcpTools.testTitle': 'Test tool',
'system.mcpTools.testHint': 'Call the OMS wrapper with JSON args (no LLM).',
'system.mcpTools.testArgsPlaceholder': '{ "destinationCountry": "US", "cargoWeight": 0.5 }',
'system.mcpTools.testArgsPlaceholder': `{'{ "destinationCountry": "US", "cargoWeight": 0.5 }'}`,
'system.mcpTools.runTest': 'Run',
'system.mcpTools.testFail': 'Test failed',
'system.mcpTools.argsInvalid': 'Arguments must be valid JSON',
@ -1099,9 +1230,9 @@ export default {
'nanaAssistant.mcp.optional': 'Optional',
'nanaAssistant.mcp.missingAsk': 'Please also provide: {fields}. Then I can look it up.',
'nanaAssistant.mcp.priceTitle': 'Price check',
'nanaAssistant.mcp.priceDesc': 'Estimate freight by country & weight',
'nanaAssistant.mcp.priceDesc': 'Estimate freight by destCountry & weight',
'nanaAssistant.mcp.priceCoach':
'To quote I need:\n1) Destination country (e.g. US)\n2) Actual weight in kg\n\nOptional: postal code, L×W×H (cm), cargo type.\nExample: US 0.5kg zip 10001',
'To quote I need:\n1) Destination destCountry (e.g. US)\n2) Actual weight in kg\n\nOptional: postal code, L×W×H (cm), cargo type.\nExample: US 0.5kg zip 10001',
'nanaAssistant.mcp.priceExample': 'e.g. US 0.5kg zip 10001',
'nanaAssistant.mcp.priceDraft':
'Country:\nWeight (kg):\nPostal code (optional):\nDims LWH cm (optional):',
@ -1117,14 +1248,14 @@ export default {
'Please provide a waybill / tracking / customer order number.\nIf a waybill is already loaded, tap “Track”.',
'nanaAssistant.mcp.trackExample': 'e.g. Track waybill NFXXXX',
'nanaAssistant.mcp.trackDraft': 'Waybill No.:',
'nanaAssistant.mcp.field.country': 'Destination country',
'nanaAssistant.mcp.field.destCountry': 'Destination destCountry',
'nanaAssistant.mcp.field.weight': 'Weight (kg)',
'nanaAssistant.mcp.field.postal': 'Postal code',
'nanaAssistant.mcp.field.dims': 'Dimensions (cm)',
'nanaAssistant.mcp.field.cargoType': 'Cargo type',
'nanaAssistant.mcp.field.product': 'Sales product code/name',
'nanaAssistant.mcp.field.cargoName': 'Cargo name',
'nanaAssistant.mcp.field.orderNo': 'Waybill No.',
'nanaAssistant.mcp.field.customsOrderNo': 'Waybill No.',
'nanaAssistant.human': 'Talk to a human advisor →',
'nanaAssistant.you': 'You',
'nanaAssistant.backToChat': 'Back to chat',
@ -1136,13 +1267,13 @@ export default {
'nanaAssistant.quickAfterSales': 'Something went wrong?',
'nanaAssistant.quickHuman': 'Talk to human advisor',
'nanaAssistant.replySmall': 'Yes. IDE focuses on SMB sellers and shipments others decline — special routes, complex cargo, and low-volume orders are in scope. Tell me origin, destination, and cargo type to get started.',
'nanaAssistant.replyPrice': 'For a quote, destination country + actual weight (kg) is enough — e.g. US 0.5kg. Postal code/dims help; origin city is not required.',
'nanaAssistant.replyQuoteSpeed': 'With country + weight we can return an indicative quote right away; final billing follows inbound weight and product rules.',
'nanaAssistant.replyPrice': 'For a quote, destination destCountry + actual weight (kg) is enough — e.g. US 0.5kg. Postal code/dims help; origin city is not required.',
'nanaAssistant.replyQuoteSpeed': 'With destCountry + weight we can return an indicative quote right away; final billing follows inbound weight and product rules.',
'nanaAssistant.trackHint': 'Just send the waybill number to track, e.g. IDE2607281746002926',
'nanaAssistant.replyVisibility': 'You get milestone updates from pickup to delivery — pickup, export, line-haul, customs, and final delivery. We proactively flag delays before they escalate.',
'nanaAssistant.replyAfterSales': "Your dedicated advisor handles exceptions, claims, and after-sales follow-up — you won't be left chasing tickets alone. Describe the issue and tracking number if you have one.",
'nanaAssistant.replyHuman': 'Reach us via the contact page — phone 18123959245 or email bd01@1st56.com. Mention your route and cargo so we assign the right advisor.',
'nanaAssistant.fallback': 'Tell me destination country + weight to quote, or paste a waybill number to track — or talk to a human advisor.',
'nanaAssistant.fallback': 'Tell me destination destCountry + weight to quote, or paste a waybill number to track — or talk to a human advisor.',
'nanaAssistant.emptyReply': 'No useful reply came back. Please try again with destination + weight, or a waybill number.',
'nanaAssistant.aiUnavailable': 'AI advisor is not configured yet. Please contact a human advisor or try later.',
'nanaAssistant.providerLoading': 'Connecting to AI advisor… please send again in a moment.',
@ -1356,7 +1487,7 @@ export default {
'order.waybillManage.table.senderPhone': 'Sender mobile',
'order.waybillManage.table.senderTelePhone': 'Sender phone',
'order.waybillManage.table.senderEmail': 'Sender email',
'order.waybillManage.table.senderCountryCode': 'Sender country code',
'order.waybillManage.table.senderCountryCode': 'Sender destCountry code',
'order.waybillManage.table.senderProvince': 'Sender state',
'order.waybillManage.table.senderCity': 'Sender city',
'order.waybillManage.table.senderZipCode': 'Sender zip',
@ -1369,7 +1500,7 @@ export default {
'order.waybillManage.table.receiverPhone': 'Consignee mobile (raw)',
'order.waybillManage.table.receiverTelePhone': 'Consignee phone',
'order.waybillManage.table.receiverEmail': 'Consignee email',
'order.waybillManage.table.receiverCountryCode': 'Consignee country code',
'order.waybillManage.table.receiverCountryCode': 'Consignee destCountry code',
'order.waybillManage.table.receiverProvince': 'Consignee state (raw)',
'order.waybillManage.table.receiverCity': 'Consignee city (raw)',
'order.waybillManage.table.receiverStreetNumber': 'Consignee street no.',
@ -1688,7 +1819,7 @@ export default {
'order.myOrders.drawer.declaration': 'Declaration',
'order.myOrders.drawer.senderName': 'Sender Name',
'order.myOrders.drawer.company': 'Company',
'order.myOrders.drawer.country': 'Country',
'order.myOrders.drawer.destCountry': 'Country',
'order.myOrders.drawer.province': 'Province',
'order.myOrders.drawer.city': 'City',
'order.myOrders.drawer.zipCode': 'Zip Code',
@ -1823,9 +1954,9 @@ export default {
'order.myOrders.drawer.fee.priceEffectiveTimePlaceholder': 'Select effective time',
// Order - Price Check
'order.priceCheck.form.destinationCountry': 'Destination country',
'order.priceCheck.form.destinationCountry': 'Destination destCountry',
'order.priceCheck.form.title': 'Quick quote',
'order.priceCheck.form.subtitle': 'Pick country and weight to compare; add postal code or dimensions for more accurate quotes',
'order.priceCheck.form.subtitle': 'Pick destCountry and weight to compare; add postal code or dimensions for more accurate quotes',
'order.priceCheck.form.destinationCountry.placeholder': 'Please select',
'order.priceCheck.form.shippingMethod': 'Shipping method',
'order.priceCheck.form.shippingMethod.placeholder': 'Please select',
@ -1851,7 +1982,7 @@ export default {
'order.priceCheck.form.moreFilters': 'More filters',
'order.priceCheck.actions.query': 'Get quotes',
'order.priceCheck.actions.reset': 'Reset',
'order.priceCheck.validate.requiredBasic': 'Please enter destination country and cargo weight',
'order.priceCheck.validate.requiredBasic': 'Please enter destination destCountry and cargo weight',
'order.priceCheck.validate.partialDimensions': 'Enter length, width and height together, or leave all blank',
'order.priceCheck.message.querySuccess': 'Loaded {count} quotes',
'order.priceCheck.message.queryFailed': 'Quote search failed',
@ -1859,7 +1990,7 @@ export default {
'order.priceCheck.sort.price': 'Price',
'order.priceCheck.sort.time': 'Transit',
'order.priceCheck.resultCount': '{count} results',
'order.priceCheck.shortcuts.countryAria': 'Common destination countries',
'order.priceCheck.shortcuts.destCountryAria': 'Common destination countries',
'order.priceCheck.shortcut.US': 'United States',
'order.priceCheck.shortcut.CA': 'Canada',
@ -1875,10 +2006,10 @@ export default {
'order.priceCheck.shortcut.ES': 'Spain',
'order.priceCheck.shortcut.NL': 'Netherlands',
'order.priceCheck.country.US': 'United States',
'order.priceCheck.country.UK': 'United Kingdom',
'order.priceCheck.country.DE': 'Germany',
'order.priceCheck.country.FR': 'France',
'order.priceCheck.destCountry.US': 'United States',
'order.priceCheck.destCountry.UK': 'United Kingdom',
'order.priceCheck.destCountry.DE': 'Germany',
'order.priceCheck.destCountry.FR': 'France',
'order.priceCheck.shippingMethod.air': 'Air',
'order.priceCheck.shippingMethod.sea': 'Sea',
@ -1918,7 +2049,7 @@ export default {
'order.priceCheck.table.viewDetail': 'Details',
'order.priceCheck.table.viewGuide': 'Channel guide',
'order.priceCheck.table.viewGuideHint': 'Click to view route guide',
'order.priceCheck.table.orderNow': 'Place order',
'order.priceCheck.table.customsOrderNow': 'Place order',
'order.priceCheck.table.askAi': 'Ask AI',
'order.priceCheck.table.askAiCanShip': 'Can this ship?',
'order.priceCheck.table.askAiCheaper': 'Anything cheaper?',
@ -1936,7 +2067,7 @@ export default {
'order.priceCheck.hint.bannerLead': 'Some details were not provided. Prices below are for reference only and may change after order or inbound weighing.',
'order.priceCheck.hint.missing.dimensions': 'No package dimensions: chargeable weight uses actual weight only; price may change after inbound measurement.',
'order.priceCheck.hint.missing.postalCode': 'No postal code: blacklist, remote and zone matching are not exact; quote is for reference only.',
'order.priceCheck.hint.missing.province': 'No state/province: zone matched at country level; exact address may change the price.',
'order.priceCheck.hint.missing.province': 'No state/province: zone matched at destCountry level; exact address may change the price.',
'order.priceCheck.hint.missing.cargoType': 'No cargo attribute selected: quotes shown for general, electronics, sensitive, etc.; final price depends on your selection at order time.',
'order.priceCheck.hint.missing.warehouse': 'No warehouse selected: quotes from all warehouses; confirm warehouse when placing order.',
@ -1948,7 +2079,7 @@ export default {
'order.priceCheck.guide.close': 'Close guide',
'order.priceCheck.guide.idle': 'Select a quote card to view customer guide and limits',
'order.priceCheck.empty.title': 'Start a quote',
'order.priceCheck.empty.desc': 'Pick a country and weight to compare. Postal code improves accuracy.',
'order.priceCheck.empty.desc': 'Pick a destCountry and weight to compare. Postal code improves accuracy.',
'order.priceCheck.empty.step1': 'Choose destination',
'order.priceCheck.empty.step2': 'Enter weight (postal recommended)',
'order.priceCheck.empty.step3': 'Search, then AI-pick by cargo',
@ -2035,7 +2166,7 @@ export default {
'order.placeOrder.fromPriceCheck.item.warehouse': 'Warehouse',
'order.placeOrder.fromPriceCheck.item.weight': 'Weight',
'order.placeOrder.fromPriceCheck.item.dimensions': 'Dimensions',
'order.placeOrder.fromPriceCheck.item.country': 'Destination country',
'order.placeOrder.fromPriceCheck.item.destCountry': 'Destination destCountry',
'order.placeOrder.fromPriceCheck.item.zip': 'Postal code',
'order.placeOrder.fromPriceCheck.item.clientNo': 'Customer order No.',
'order.placeOrder.fromPriceCheck.item.receiverName': 'Recipient name',
@ -2726,11 +2857,14 @@ export default {
// System - Role Management (Permission Assignment)
'system.role.permission.expandAll': 'Expand All',
'system.role.permission.adminAllHint':
'Admin role has all menu and button permissions by default; no assignment needed',
'system.role.permission.collapseAll': 'Collapse All',
'system.role.permission.checkAll': 'Check All',
'system.role.permission.uncheckAll': 'Uncheck All',
'system.role.permission.loading': 'Loading permission data...',
'system.role.permission.assignSuccess': 'Permission assigned successfully',
'system.role.permission.refreshHint': 'Permissions saved. Please sign in again for them to take effect.',
'system.role.permission.search': 'Search permission name or code',
'system.role.permission.selected': 'Selected',
'system.role.permission.total': 'Total',
@ -3085,14 +3219,14 @@ export default {
'operation.common.keepScanFocus': 'Keep focus on scan field',
'operation.common.voiceBroadcast': 'Voice broadcast',
'operation.common.feedback.idle': 'Scan a number — result will appear here',
'operation.common.message.orderNotFound': 'Order not found',
'operation.common.message.customsOrderNotFound': 'Order not found',
'operation.common.message.lookupSuccess': 'Order matched',
// Operation Module - Inbound Scan
'operation.inbound.pageTitle': 'Waybill Information',
'operation.inbound.broadcastSettings': 'Broadcast Settings',
'operation.inbound.scan.title': 'Scan inbound',
'operation.inbound.tip': 'Scan to verify product and country, then enter received weight and dimensions before inbound.',
'operation.inbound.tip': 'Scan to verify product and destCountry, then enter received weight and dimensions before inbound.',
'operation.inbound.preview.title': 'Verify',
'operation.inbound.preview.empty': 'Scan a number to show order info and enter weight / dimensions',
'operation.inbound.form.scanNumber': 'Scan Number:',
@ -3120,7 +3254,7 @@ export default {
'operation.inbound.table.weight': 'Weight',
'operation.inbound.table.dimensions': 'Dimensions',
'operation.inbound.table.product': 'Product',
'operation.inbound.table.country': 'Destination Country',
'operation.inbound.table.destCountry': 'Destination Country',
'operation.inbound.table.warehouse': 'Warehouse',
'operation.inbound.table.operateTime': 'Operate Time',
'operation.inbound.table.operator': 'Operator',
@ -3142,14 +3276,14 @@ export default {
'operation.outbound.pageTitle': 'Waybill Information',
'operation.outbound.broadcastSettings': 'Broadcast Settings',
'operation.outbound.scan.title': 'Scan outbound',
'operation.outbound.tip': 'Scan waybill to verify product, country and channel. Weight and dimensions are optional.',
'operation.outbound.tip': 'Scan waybill to verify product, destCountry and channel. Weight and dimensions are optional.',
'operation.outbound.preview.title': 'Verify',
'operation.outbound.preview.empty': 'Scan a number to show order info; weight / dimensions are optional',
'operation.outbound.form.scanNumber': 'Scan Number:',
'operation.outbound.form.shipWeight': 'Shipped weight',
'operation.outbound.form.dimensions': 'L × W × H',
'operation.outbound.form.salesProduct': 'Sales product',
'operation.outbound.form.destinationCountry': 'Destination country',
'operation.outbound.form.destinationCountry': 'Destination destCountry',
'operation.outbound.form.serviceChannel': 'Service channel',
'operation.outbound.form.placeholder.scanOrInput': 'Scan or enter number',
'operation.outbound.form.placeholder.input': 'Enter',
@ -3170,7 +3304,7 @@ export default {
'operation.outbound.table.weight': 'Weight',
'operation.outbound.table.dimensions': 'Dimensions',
'operation.outbound.table.product': 'Product',
'operation.outbound.table.country': 'Destination',
'operation.outbound.table.destCountry': 'Destination',
'operation.outbound.table.serviceChannel': 'Service channel',
'operation.outbound.table.warehouse': 'Warehouse',
'operation.outbound.table.operateTime': 'Operate Time',
@ -3223,7 +3357,7 @@ export default {
'operation.fulfillPack.message.batchPrintPartial': 'Print done: {success} ok, {fail} failed',
'operation.fulfillPack.message.skuMultiMatch': 'SKU matched {count} orders; queue filtered and selected',
'operation.fulfillPack.message.notInQueue': 'Matched order(s) not in the current pick queue (not arrived or not bound)',
'operation.fulfillPack.message.orderNotInQueue': 'Order not in pick queue; check bind and arrival status',
'operation.fulfillPack.message.customsOrderNotInQueue': 'Order not in pick queue; check bind and arrival status',
'operation.fulfillPack.queue.emptyFiltered': 'No pending orders match the current filter',
'order.myOrders.table.sku': 'SKU',
'operation.printLabel.form.scanNumber': 'Scan number',
@ -3323,7 +3457,7 @@ export default {
'system.batchEmail.preview': 'Template edit & preview',
'system.batchEmail.saveTpl': 'Save template',
'system.batchEmail.deleteTpl': 'Delete template',
'system.batchEmail.placeholderHint': 'Placeholders: {客户单号}, {最新轨迹}, {换单号}',
'system.batchEmail.placeholderHint': `{'Placeholders: {客户单号}, {最新轨迹}, {换单号}'}`,
'system.batchEmail.emailConfig': 'Mailbox',
'system.batchEmail.account': 'Account',
'system.batchEmail.password': 'App password',
@ -3340,7 +3474,7 @@ export default {
'system.batchEmail.saveConfig': 'Save',
'system.batchEmail.testConnection': 'Test connection',
'system.batchEmail.subject': 'Subject',
'system.batchEmail.subjectPlaceholder': 'Placeholders supported, e.g. Order {客户单号}',
'system.batchEmail.subjectPlaceholder': `{'Placeholders supported, e.g. Order {客户单号}'}`,
'system.batchEmail.contentPlaceholder': 'Body',
'system.batchEmail.saveTplModalTitle': 'Save template',
'system.batchEmail.saveTplNamePlaceholder': 'Template name',
@ -3741,7 +3875,7 @@ export default {
'finance.receivablePayable.table.isPaid': 'Payment status',
'finance.receivablePayable.table.productCode': 'Product code',
'finance.receivablePayable.table.channelCode': 'Channel code',
'finance.receivablePayable.table.consigneeCountryName': 'Destination country',
'finance.receivablePayable.table.consigneeCountryName': 'Destination destCountry',
'finance.receivablePayable.table.downstreamProvider': 'Downstream provider',
'finance.receivablePayable.table.priceNo': 'Price no.',
'finance.receivablePayable.table.priceEffectiveTime': 'Price effective time',
@ -4321,7 +4455,7 @@ export default {
'channel.salesProducts.form.additionalServices': 'Additional services',
'channel.salesProducts.billingSimulate.open': 'Billing simulate',
'channel.salesProducts.billingSimulate.title': 'Billing simulate',
'channel.salesProducts.billingSimulate.country': 'Country',
'channel.salesProducts.billingSimulate.destCountry': 'Country',
'channel.salesProducts.billingSimulate.province': 'Province/State',
'channel.salesProducts.billingSimulate.city': 'City',
'channel.salesProducts.billingSimulate.zipCode': 'Zip code',
@ -4346,7 +4480,7 @@ export default {
'channel.salesProducts.billingSimulate.warnings': 'Warnings',
'channel.salesProducts.billingSimulate.emptyResult': 'No simulation result yet',
'channel.salesProducts.billingSimulate.needProduct': 'Select a sales product first',
'channel.salesProducts.billingSimulate.needCountry': 'Please select a country',
'channel.salesProducts.billingSimulate.needCountry': 'Please select a destCountry',
'channel.salesProducts.billingSimulate.costItem': 'Cost item',
'channel.salesProducts.billingSimulate.amount': 'Amount',
'channel.salesProducts.billingSimulate.currency': 'Currency',
@ -4550,7 +4684,7 @@ export default {
'channel.area.pagination.page': 'page',
'channel.area.form.keyword': 'Keyword',
'channel.area.form.areaName': 'Area name',
'channel.area.form.country': 'Country/Region',
'channel.area.form.destCountry': 'Country/Region',
'channel.area.form.province': 'State/Province',
'channel.area.form.city': 'City',
'channel.area.form.leftPostCode': 'Start postcode',
@ -4559,7 +4693,7 @@ export default {
'channel.area.form.placeholder.areaName': 'Enter area name',
'channel.area.form.placeholder.input': 'Please enter',
'channel.area.form.rules.areaNameRequired': 'Please enter area name',
'channel.area.form.rules.countryRequired': 'Please select country/region',
'channel.area.form.rules.destCountryRequired': 'Please select destCountry/region',
'channel.area.form.rules.maxLength128': 'Cannot exceed 128 characters',
'channel.area.form.rules.maxLength30': 'Cannot exceed 30 characters',
'channel.area.form.rules.noChinese': 'Cannot contain Chinese characters',
@ -4593,8 +4727,8 @@ export default {
'channel.config.detailCount': '{count} detail record(s)',
'channel.area.tip.addArea': 'Areas group detail records. Use a clear name such as "US West Coast" or "EU Remote".',
'channel.area.tip.editArea': 'Renaming an area does not change existing details. Avoid duplicate names.',
'channel.area.tip.addDetail': 'Country/region is required; state/province and city are optional. When using postcodes, start must not exceed end. Country only matches all postcodes in that country.',
'channel.area.tip.editDetail': 'Changes apply immediately to channel matching. Verify country and postcode range.',
'channel.area.tip.addDetail': 'Country/region is required; state/province and city are optional. When using postcodes, start must not exceed end. Country only matches all postcodes in that destCountry.',
'channel.area.tip.editDetail': 'Changes apply immediately to channel matching. Verify destCountry and postcode range.',
'channel.area.form.placeholder.provinceOptional': 'Optional; leave blank for any state/province',
'channel.area.form.placeholder.cityOptional': 'Optional; leave blank for any city',
'channel.area.form.placeholder.leftPostCode': 'Optional; pair with end postcode',
@ -4602,8 +4736,8 @@ export default {
'channel.area.form.rules.maxLength50': 'Cannot exceed 50 characters',
'channel.area.form.rules.postcodeRangeInvalid': 'Start postcode cannot be greater than end postcode',
'channel.area.modal.deleteConfirmWithName': 'Delete area "{name}"? All details under it will be removed.',
'channel.area.message.countryLoadFailed': 'Failed to load countries. Please refresh and try again.',
'channel.area.message.countryQuickTagMiss': 'Country not matched. Please select from the dropdown.',
'channel.area.message.destCountryLoadFailed': 'Failed to load countries. Please refresh and try again.',
'channel.area.message.destCountryQuickTagMiss': 'Country not matched. Please select from the dropdown.',
'channel.area.message.editMissingRow': 'Record not found. Please refresh and try again.',
'channel.area.message.deleteFailed': 'Delete failed. Please try again later.',
@ -4846,7 +4980,7 @@ export default {
'channel.issueTemplate.message.saveFailed': 'Failed to save',
// Track Center - Track Order
'track.order.search.placeholder.orderNo': 'Enter order number',
'track.order.search.placeholder.customsOrderNo': 'Enter order number',
'track.order.search.placeholder.isExternal': 'Please select if external',
'track.order.search.label.showTrajectory': 'Show track',
'track.order.search.label.hideTrajectory': 'Hide track',
@ -4864,7 +4998,7 @@ export default {
'track.order.drawer.tabs.small': 'Small package',
'track.order.drawer.tabs.main': 'Main order',
'track.order.table.orderType': 'Order type',
'track.order.table.orderNo': 'Customer order number',
'track.order.table.customsOrderNo': 'Customer order number',
'track.order.table.customerShortName': 'Customer short name',
'track.order.table.thirdPartyOrderNo': 'Last mile order number',
'track.order.table.estimatedDeliveryTime': 'Estimated delivery time',
@ -4883,7 +5017,7 @@ export default {
'track.order.table.product': 'Sales product',
'track.order.table.channel': 'Service channel',
'track.order.table.apiCode': 'API service provider',
'track.order.table.countryCode': 'Country',
'track.order.table.destCountryCode': 'Country',
'track.order.table.province': 'Province/State',
'track.order.table.city': 'City',
'track.order.table.postcode': 'Postcode',
@ -4901,7 +5035,7 @@ export default {
'track.order.detail.title.serviceProvider': 'Service provider',
'track.order.detail.title.trackPlaceCity': 'Track location (city)',
'track.order.detail.title.trackPlaceProvince': 'Track location (province/state)',
'track.order.detail.title.trackPlaceNation': 'Track location (country/region)',
'track.order.detail.title.trackPlaceNation': 'Track location (destCountry/region)',
'track.order.detail.title.trackPlacePostcode': 'Track location postcode',
'track.order.detail.title.trackPlace': 'Track location',
'track.order.detail.title.isVisibility': 'Is visible',
@ -4934,7 +5068,7 @@ export default {
'track.order.modal.viewDetail': 'View detail',
'track.order.modal.deleteSingleTrajectory': 'Are you sure to delete track {content}?',
'track.order.modal.deleteMultipleTrajectory': 'Are you sure to delete the following {count} tracks?',
'track.order.modal.deleteOrder': 'Are you sure to delete order {orderNo}?',
'track.order.modal.deleteOrder': 'Are you sure to delete order {customsOrderNo}?',
'track.order.modal.deleteMultipleOrders': 'Are you sure to delete the following {count} orders?',
'track.order.message.selectSameOrderType': 'Please select the same order type and subsystem',
'track.order.message.selectSameOrderTypeOnly': 'Please select the same order type',
@ -4965,7 +5099,7 @@ export default {
'track.order.common.no': 'No',
'track.order.pullConfig.appKey': 'App Key',
'track.order.pullConfig.orderType': 'Order Type',
'track.order.pullConfig.orderNos': 'Order Numbers',
'track.order.pullConfig.customsOrderNos': 'Order Numbers',
'track.order.pullConfig.items': 'Config Items',
'track.order.pullConfig.noData': 'No data',
'track.order.pullConfig.table.index': 'No.',
@ -5026,7 +5160,7 @@ export default {
'track.warningOrder.pagination.goTo': 'Go to',
'track.warningOrder.pagination.page': 'page',
'track.warningOrder.table.serialNo': 'No.',
'track.warningOrder.table.orderNo': 'Order number',
'track.warningOrder.table.customsOrderNo': 'Order number',
'track.warningOrder.table.status': 'Status',
'track.warningOrder.table.createTime': 'Create time',
'track.warningOrder.message.loadFailed': 'Failed to load data',

View File

@ -120,6 +120,7 @@ export default {
'menu.order.fulfillOrders': '代发订单',
'menu.order.waybillManage': '运单管理',
'menu.order.pickupOrder': '揽收单',
'menu.order.customs9610Order': '9610订单',
'menu.order.batchJobs': '批量任务',
'menu.order.platformStore': '店铺授权',
'menu.order.platformStoreOrder': '店铺订单',
@ -465,7 +466,7 @@ export default {
'user.senderAddress.table.addressName': '别名',
'user.senderAddress.table.senderName': '联系人',
'user.senderAddress.table.phone': '手机',
'user.senderAddress.table.country': '国家',
'user.senderAddress.table.destCountry': '国家',
'user.senderAddress.table.province': '省/州',
'user.senderAddress.table.city': '城市',
'user.senderAddress.table.address': '地址',
@ -476,7 +477,7 @@ export default {
'user.senderAddress.form.senderName': '联系人',
'user.senderAddress.form.phone': '手机',
'user.senderAddress.form.company': '公司',
'user.senderAddress.form.countryCode': '国家二字码',
'user.senderAddress.form.destCountryCode': '国家二字码',
'user.senderAddress.form.province': '省/州',
'user.senderAddress.form.city': '城市',
'user.senderAddress.form.zipCode': '邮编',
@ -492,6 +493,77 @@ export default {
'user.senderAddress.message.typeRequired': '请选择地址类型',
'user.senderAddress.message.senderNameRequired': '请填写联系人',
'user.senderAddress.message.addressRequired': '请填写详细地址',
'order.customs9610.status.pending': '待申报',
'order.customs9610.status.declared': '已申报',
'order.customs9610.search.keyword': '运单号/客户单号/海关订单号',
'order.customs9610.empty.hint': '暂无 9610 申报单,出口报关方式为「9610汇总」的运单创建后会自动出现在此',
'order.customs9610.table.serviceNo': '运单号',
'order.customs9610.table.clientNo': '客户单号',
'order.customs9610.table.destCountry': '目的国',
'order.customs9610.table.grossWeight': '毛重',
'order.customs9610.table.currency': '币制',
'order.customs9610.table.businessType': '业务类型',
'order.customs9610.table.status': '状态',
'order.customs9610.table.customsOrderNo': '海关订单号',
'order.customs9610.table.declareTime': '申报时间',
'order.customs9610.table.createTime': '创建时间',
'order.customs9610.actions.declare': '申报',
'order.customs9610.actions.batchDeclare': '批量申报',
'order.customs9610.actions.batchFetchManifest': '批量获取清单数据',
'order.customs9610.actions.batchFetchDeclarationForm': '批量获取报关单',
'order.customs9610.modal.edit': '编辑 9610 申报',
'order.customs9610.modal.detail': '9610 申报详情',
'order.customs9610.field.platformCode': '电商平台',
'order.customs9610.field.businessType': '业务类型',
'order.customs9610.field.preRecordFlag': '汇总预录',
'order.customs9610.field.clientNo': '客户单号',
'order.customs9610.field.serviceNo': '运单号',
'order.customs9610.field.destCountry': '目的国',
'order.customs9610.field.currency': '币制',
'order.customs9610.field.grossWeight': '毛重(KG)',
'order.customs9610.field.freight': '运费',
'order.customs9610.field.ecomCorpCode': '电商企业代码',
'order.customs9610.field.ecomCorpName': '电商企业名称',
'order.customs9610.field.producerCode': '生产销售单位代码',
'order.customs9610.field.producerName': '生产销售单位',
'order.customs9610.field.region': '区域',
'order.customs9610.field.agentDeclare': '是否代理申报',
'order.customs9610.field.actualSellerCode': '实际卖家信用代码',
'order.customs9610.field.shipmentNo': '出货单号',
'order.customs9610.field.customsOrderNo': '海关订单号',
'order.customs9610.field.isSummarySimple': '汇总简化',
'order.customs9610.items.title': '商品明细',
'order.customs9610.items.add': '添加行',
'order.customs9610.items.sku': 'SKU',
'order.customs9610.items.cnName': '中文品名',
'order.customs9610.items.exHsCode': '出口海关编码',
'order.customs9610.items.declareElement': '申报要素',
'order.customs9610.items.unitPrice': '单价',
'order.customs9610.items.quantity': '数量',
'order.customs9610.confirm.declareTitle': '确认申报',
'order.customs9610.confirm.declareContent': '将调用 API 中心上游接口申报,是否继续?',
'order.customs9610.confirm.batchDeclareTitle': '批量申报',
'order.customs9610.confirm.batchDeclareContent': '将对选中的 {count} 条待申报单据发起申报,是否继续?',
'order.customs9610.confirm.batchManifestTitle': '批量获取清单数据',
'order.customs9610.confirm.batchManifestContent': '将对选中的 {count} 条已申报单据获取清单数据,是否继续?',
'order.customs9610.confirm.batchDeclarationFormTitle': '批量获取报关单',
'order.customs9610.confirm.batchDeclarationFormContent': '将对选中的 {count} 条已申报单据获取报关单,是否继续?',
'order.customs9610.message.loadFailed': '加载失败',
'order.customs9610.message.saveSuccess': '保存成功',
'order.customs9610.message.saveFailed': '保存失败',
'order.customs9610.message.declareSuccess': '申报成功',
'order.customs9610.message.declareFailed': '申报失败',
'order.customs9610.message.batchResult': '成功 {success},失败 {fail}',
'order.customs9610.message.fetchManifestFailed': '获取清单数据失败',
'order.customs9610.message.fetchDeclarationFormFailed': '获取报关单失败',
'order.customs9610.message.selectFirst': '请先勾选单据',
'order.customs9610.message.platformCodeRequired': '请填写电商平台',
'order.customs9610.message.clientServiceRequired': '请填写客户单号与运单号',
'order.customs9610.message.weightRequired': '毛重必须大于 0',
'order.customs9610.message.currencyDestCountryRequired': '请填写币制与目的国',
'order.customs9610.message.preRecordRequired': 'B 汇总申报必须填写预录标记',
'order.customs9610.message.aFieldsRequired': 'A 简化申报必须填写区域与是否代理',
'order.customs9610.message.itemsRequired': '请完善 SKU 与中文品名',
'order.pickupOrder.search.keyword': '系统单号/揽收单号/联系人',
'order.pickupOrder.search.customer': '客户',
'order.pickupOrder.search.status': '状态',
@ -508,7 +580,23 @@ export default {
'order.pickupOrder.actions.markPicked': '标记已取件',
'order.pickupOrder.actions.markInbound': '确认到仓',
'order.pickupOrder.actions.cancel': '取消订单',
'order.pickupOrder.hint.createFromMyOrders': '多票合并请到「我的订单」勾选后点「创建揽收」',
'order.pickupOrder.hint.createFromMyOrders': '可绑定已预报运单,或直接新建运单揽收;多票须同仓',
'order.pickupOrder.create.lead': '填写取件与货物信息后提交,系统将生成运单并创建揽收单。',
'order.pickupOrder.create.leadBind': '勾选已预报且未绑定揽收的运单(须同仓),填写取件信息后提交。',
'order.pickupOrder.create.modeBind': '绑定已有运单',
'order.pickupOrder.create.modeNew': '新建运单揽收',
'order.pickupOrder.create.sectionPickup': '取件信息',
'order.pickupOrder.create.sectionCargo': '货物信息',
'order.pickupOrder.create.sectionOrders': '选择运单',
'order.pickupOrder.create.orderKeywordPh': '运单号 / 客户单号',
'order.pickupOrder.create.selectedOrders': '已选 {count} 票 · 仓库:{warehouse}',
'order.pickupOrder.create.warehouseFromOrders': '请先勾选运单(自动带出仓库)',
'order.pickupOrder.create.needSelectOrders': '请至少勾选一票运单',
'order.pickupOrder.form.cargoNamePlaceholder': '请填写货物中文名称',
'order.pickupOrder.form.clientNoPlaceholder': '选填,不填则系统自动生成',
'order.pickupOrder.message.noPickupProduct': '暂无可用的国内揽收销售产品,请先在渠道维护',
'order.pickupOrder.message.contactRequired': '请先完善手机号和邮箱后再创建揽收',
'order.pickupOrder.message.loadOrdersFailed': '加载可绑定运单失败',
'order.pickupOrder.table.pickupNo': '系统单号',
'order.pickupOrder.table.carrierWaybillNo': '揽收单号',
'order.pickupOrder.table.customer': '客户',
@ -526,12 +614,15 @@ export default {
'order.pickupOrder.table.clientNo': '客户单号',
'order.pickupOrder.detail.title': '揽收单详情',
'order.pickupOrder.detail.orders': '关联运单',
'order.pickupOrder.detail.sectionInfo': '揽收信息',
'order.pickupOrder.detail.close': '关闭',
'order.pickupOrder.form.freight': '揽收运费',
'order.pickupOrder.form.otherFee': '其他费用',
'order.pickupOrder.form.carrierWaybillNoPh': '国内承运商回执单号',
'order.pickupOrder.rebind.remove': '移除已绑运单',
'order.pickupOrder.rebind.add': '追加运单',
'order.pickupOrder.rebind.addPh': '输入运单号/客户单号,多个用逗号或换行分隔',
'order.pickupOrder.rebind.addHint': '勾选同仓、已预报且未绑定揽收的运单进行追加',
'order.pickupOrder.rebind.emptyBound': '当前无关联运单',
'order.pickupOrder.status.draft': '草稿',
'order.pickupOrder.status.scheduled': '已预约',
@ -558,7 +649,7 @@ export default {
'order.pickupOrder.message.cancelOk': '已取消',
'order.pickupOrder.message.cancelConfirm': '确认取消该揽收单?关联运单将解绑。',
'order.pickupOrder.message.rebindOk': '改绑成功',
'order.pickupOrder.message.rebindEmpty': '请勾选要移除的运单,或填写要追加的运单号',
'order.pickupOrder.message.rebindEmpty': '请勾选要移除的运单,或从列表选择要追加的运单',
'order.pickupOrder.message.rebindNotFound': '未找到运单:{nos}',
'order.pickupOrder.message.pickedOk': '已标记取件',
'order.pickupOrder.message.inboundOk': '已确认到仓',
@ -592,6 +683,44 @@ export default {
'order.pickupOrder.message.createSuccess': '揽收订单创建成功',
'order.pickupOrder.message.createSuccessWithNo': '创建成功,运单号:{no}',
'order.pickupOrder.message.createFailed': '创建揽收订单失败',
'order.pickupOrder.search.warehouse': '仓库',
'order.pickupOrder.search.expectStart': '期望揽收开始',
'order.pickupOrder.search.expectEnd': '期望揽收结束',
'order.pickupOrder.table.settlement': '结算',
'order.pickupOrder.settlementStatus.none': '无',
'order.pickupOrder.settlementStatus.pending': '待结算',
'order.pickupOrder.settlementStatus.settled': '已结算',
'order.pickupOrder.actions.reschedule': '改约',
'order.pickupOrder.actions.batchConfirmFreight': '批量确认运费',
'order.pickupOrder.actions.batchSchedule': '批量草稿预报',
'order.pickupOrder.actions.copyServiceNos': '复制全部运单号',
'order.pickupOrder.create.emptyOrders': '暂无可绑定的已预报运单',
'order.pickupOrder.create.goPredeclared': '去已预报运单',
'order.pickupOrder.createNext.title': '创建成功',
'order.pickupOrder.createNext.desc': '揽收单 {no} 已创建,可继续以下操作:',
'order.pickupOrder.createNext.goSchedule': '去草稿预报',
'order.pickupOrder.createNext.goPay': '去支付',
'order.pickupOrder.createNext.viewDetail': '查看详情',
'order.pickupOrder.payCheck.amount': '应付金额:¥{amount}',
'order.pickupOrder.payCheck.balance': '账户余额:¥{balance}',
'order.pickupOrder.payCheck.balanceUnknown': '账户余额:暂无法获取',
'order.pickupOrder.payCheck.shortfall': '余额不足,差额:¥{shortfall}',
'order.pickupOrder.payCheck.payAnyway': '仍要支付',
'order.pickupOrder.detail.timeline': '操作痕迹',
'order.pickupOrder.timeline.createTime': '创建时间',
'order.pickupOrder.timeline.payTime': '支付时间',
'order.pickupOrder.timeline.actualPickupTime': '实际取件时间',
'order.pickupOrder.timeline.updateTime': '更新时间',
'order.pickupOrder.message.rescheduleOk': '改约成功',
'order.pickupOrder.message.noServiceNos': '暂无可复制的运单号',
'order.pickupOrder.message.copyServiceNosOk': '运单号已复制',
'order.pickupOrder.message.copyFailed': '复制失败',
'order.pickupOrder.message.batchFreightNoCandidates': '所选揽收单无可确认运费项',
'order.pickupOrder.message.batchFreightHint': '将对已选 {count} 张未支付揽收单统一确认运费',
'order.pickupOrder.message.batchFreightResult': '批量确认运费:成功 {success},失败 {fail}',
'order.pickupOrder.message.batchScheduleNoCandidates': '所选揽收单无草稿/已预约可预报项',
'order.pickupOrder.message.batchScheduleHint': '将对已选 {count} 张揽收单统一草稿预报',
'order.pickupOrder.message.batchScheduleResult': '批量预报:成功 {success},失败 {fail}',
'order.platformStoreOrder.status.all': '全部',
'order.platformStoreOrder.status.pending': '待处理',
'order.platformStoreOrder.status.toShip': '待发货',
@ -600,19 +729,19 @@ export default {
'order.platformStoreOrder.status.canceled': '已取消',
'order.platformStoreOrder.status.completed': '已完成',
'order.platformStoreOrder.table.platform': '平台',
'order.platformStoreOrder.table.country': '国家',
'order.platformStoreOrder.table.destCountry': '国家',
'order.platformStoreOrder.table.itemCount': '商品数',
'order.platformStoreOrder.table.shippingLabel': '面单号',
'order.platformStoreOrder.table.warehouse': '发货仓库',
'order.platformStoreOrder.table.lastSync': '同步时间',
'order.platformStoreOrder.table.orderNo': '平台订单号',
'order.platformStoreOrder.table.customsOrderNo': '平台订单号',
'order.platformStoreOrder.table.store': '店铺',
'order.platformStoreOrder.table.status': '状态',
'order.platformStoreOrder.table.amount': '金额',
'order.platformStoreOrder.table.orderTime': '下单时间',
'order.platformStoreOrder.table.linkedServiceNo': '关联服务单号',
'order.platformStoreOrder.form.product': '销售产品',
'order.platformStoreOrder.search.orderNo': '平台订单号',
'order.platformStoreOrder.search.customsOrderNo': '平台订单号',
'order.platformStoreOrder.summary.pending': '待处理',
'order.platformStoreOrder.summary.toShip': '待发货',
'order.platformStoreOrder.drawer.detail': '订单详情',
@ -629,7 +758,7 @@ export default {
'order.platformStoreOrder.recipient.phone': '手机',
'order.platformStoreOrder.recipient.telePhone': '电话',
'order.platformStoreOrder.recipient.email': '邮箱',
'order.platformStoreOrder.recipient.country': '国家',
'order.platformStoreOrder.recipient.destCountry': '国家',
'order.platformStoreOrder.recipient.province': '省/州',
'order.platformStoreOrder.recipient.city': '城市',
'order.platformStoreOrder.recipient.zipCode': '邮编',
@ -855,7 +984,7 @@ export default {
'system.mcpTools.selectHint': '请选择左侧工具',
'system.mcpTools.testTitle': '试跑工具',
'system.mcpTools.testHint': '填写 JSON 参数后直接调用 OMS 封装,不经过大模型。',
'system.mcpTools.testArgsPlaceholder': '{ "destinationCountry": "US", "cargoWeight": 0.5 }',
'system.mcpTools.testArgsPlaceholder': `{'{ "destinationCountry": "US", "cargoWeight": 0.5 }'}`,
'system.mcpTools.runTest': '试跑',
'system.mcpTools.testFail': '试跑失败',
'system.mcpTools.argsInvalid': '参数必须是合法 JSON',
@ -934,14 +1063,14 @@ export default {
'查轨迹请提供运单号 / 跟踪号 / 客户单号(任选其一)。\n若已从订单带入运单,可直接点「查轨迹」。',
'nanaAssistant.mcp.trackExample': '例:请查运单 NFXXXX 的轨迹',
'nanaAssistant.mcp.trackDraft': '运单号:',
'nanaAssistant.mcp.field.country': '目的国家',
'nanaAssistant.mcp.field.destCountry': '目的国家',
'nanaAssistant.mcp.field.weight': '实重(kg)',
'nanaAssistant.mcp.field.postal': '邮编',
'nanaAssistant.mcp.field.dims': '长宽高(cm)',
'nanaAssistant.mcp.field.cargoType': '货物属性',
'nanaAssistant.mcp.field.product': '销售产品代码/名称',
'nanaAssistant.mcp.field.cargoName': '客户品名',
'nanaAssistant.mcp.field.orderNo': '运单号',
'nanaAssistant.mcp.field.customsOrderNo': '运单号',
'nanaAssistant.human': '联系人工顾问 →',
'nanaAssistant.you': '你',
'nanaAssistant.backToChat': '返回对话',
@ -1609,7 +1738,7 @@ export default {
'order.myOrders.drawer.declaration': '申报信息',
'order.myOrders.drawer.senderName': '发件人姓名',
'order.myOrders.drawer.company': '公司',
'order.myOrders.drawer.country': '国家',
'order.myOrders.drawer.destCountry': '国家',
'order.myOrders.drawer.province': '州省',
'order.myOrders.drawer.city': '城市',
'order.myOrders.drawer.zipCode': '邮编',
@ -1780,7 +1909,7 @@ export default {
'order.priceCheck.sort.price': '价格',
'order.priceCheck.sort.time': '时效',
'order.priceCheck.resultCount': '共 {count} 条',
'order.priceCheck.shortcuts.countryAria': '常用目的国家',
'order.priceCheck.shortcuts.destCountryAria': '常用目的国家',
'order.priceCheck.shortcut.US': '美国',
'order.priceCheck.shortcut.CA': '加拿大',
@ -1796,10 +1925,10 @@ export default {
'order.priceCheck.shortcut.ES': '西班牙',
'order.priceCheck.shortcut.NL': '荷兰',
'order.priceCheck.country.US': '美国',
'order.priceCheck.country.UK': '英国',
'order.priceCheck.country.DE': '德国',
'order.priceCheck.country.FR': '法国',
'order.priceCheck.destCountry.US': '美国',
'order.priceCheck.destCountry.UK': '英国',
'order.priceCheck.destCountry.DE': '德国',
'order.priceCheck.destCountry.FR': '法国',
'order.priceCheck.shippingMethod.air': '空运',
'order.priceCheck.shippingMethod.sea': '海运',
@ -1839,7 +1968,7 @@ export default {
'order.priceCheck.table.viewDetail': '详情',
'order.priceCheck.table.viewGuide': '渠道说明',
'order.priceCheck.table.viewGuideHint': '点击查看线路说明',
'order.priceCheck.table.orderNow': '立刻下单',
'order.priceCheck.table.customsOrderNow': '立刻下单',
'order.priceCheck.table.askAi': '问 AI',
'order.priceCheck.table.askAiCanShip': '这个能不能发?',
'order.priceCheck.table.askAiCheaper': '有没有更便宜?',
@ -1956,7 +2085,7 @@ export default {
'order.placeOrder.fromPriceCheck.item.warehouse': '仓库',
'order.placeOrder.fromPriceCheck.item.weight': '重量',
'order.placeOrder.fromPriceCheck.item.dimensions': '尺寸',
'order.placeOrder.fromPriceCheck.item.country': '目的国家',
'order.placeOrder.fromPriceCheck.item.destCountry': '目的国家',
'order.placeOrder.fromPriceCheck.item.zip': '邮编',
'order.placeOrder.fromPriceCheck.item.clientNo': '客户单号',
'order.placeOrder.fromPriceCheck.item.receiverName': '收件人名称',
@ -2651,12 +2780,14 @@ export default {
'system.menu.iconPicker.empty': '未找到匹配图标',
// 系统 - 角色管理(权限分配相关)
'system.role.permission.adminAllHint': 'Admin 角色默认拥有全部菜单与按钮权限,无需分配',
'system.role.permission.expandAll': '全部展开',
'system.role.permission.collapseAll': '全部收起',
'system.role.permission.checkAll': '全选',
'system.role.permission.uncheckAll': '取消全选',
'system.role.permission.loading': '加载权限数据中...',
'system.role.permission.assignSuccess': '权限分配成功',
'system.role.permission.refreshHint': '权限已保存,请重新登录后生效',
'system.role.permission.search': '搜索权限名称或编码',
'system.role.permission.selected': '已选',
'system.role.permission.total': '总计',
@ -3143,7 +3274,7 @@ export default {
'operation.common.keepScanFocus': '焦点维持',
'operation.common.voiceBroadcast': '语音播报',
'operation.common.feedback.idle': '请扫描单号,结果将在此显示',
'operation.common.message.orderNotFound': '未找到对应订单',
'operation.common.message.customsOrderNotFound': '未找到对应订单',
'operation.common.message.lookupSuccess': '订单匹配成功',
// 操作模块 - 入库扫描
@ -3178,7 +3309,7 @@ export default {
'operation.inbound.table.weight': '重量',
'operation.inbound.table.dimensions': '尺寸',
'operation.inbound.table.product': '产品',
'operation.inbound.table.country': '目的国家',
'operation.inbound.table.destCountry': '目的国家',
'operation.inbound.table.warehouse': '仓库',
'operation.inbound.table.operateTime': '操作时间',
'operation.inbound.table.operator': '操作人',
@ -3228,7 +3359,7 @@ export default {
'operation.outbound.table.weight': '重量',
'operation.outbound.table.dimensions': '尺寸',
'operation.outbound.table.product': '产品',
'operation.outbound.table.country': '目的国家',
'operation.outbound.table.destCountry': '目的国家',
'operation.outbound.table.serviceChannel': '服务渠道',
'operation.outbound.table.warehouse': '仓库',
'operation.outbound.table.operateTime': '操作时间',
@ -3280,7 +3411,7 @@ export default {
'operation.fulfillPack.message.batchPrintPartial': '打印完成:成功 {success} 单,失败 {fail} 单',
'operation.fulfillPack.message.skuMultiMatch': 'SKU 命中 {count} 单,已筛选队列并勾选',
'operation.fulfillPack.message.notInQueue': '命中订单不在当前待拣队列(可能未到仓或未绑定)',
'operation.fulfillPack.message.orderNotInQueue': '订单不在当前待拣队列,请核对绑定与到仓状态',
'operation.fulfillPack.message.customsOrderNotInQueue': '订单不在当前待拣队列,请核对绑定与到仓状态',
'operation.fulfillPack.queue.emptyFiltered': '当前筛选无待拣订单',
'operation.printLabel.form.scanNumber': '扫描号码',
'operation.printLabel.form.placeholder.scanOrInput': '运单号 / 客户单号 / 跟踪号 / 包裹单号 / SKU',
@ -3379,7 +3510,7 @@ export default {
'system.batchEmail.preview': '模板编辑与预览',
'system.batchEmail.saveTpl': '保存模板',
'system.batchEmail.deleteTpl': '删除模板',
'system.batchEmail.placeholderHint': '提示:使用{客户单号}、{最新轨迹}、{换单号}作为占位符',
'system.batchEmail.placeholderHint': `{'提示:使用{客户单号}、{最新轨迹}、{换单号}作为占位符'}`,
'system.batchEmail.emailConfig': '邮箱配置',
'system.batchEmail.account': '账号',
'system.batchEmail.password': '安全密码',
@ -3396,7 +3527,7 @@ export default {
'system.batchEmail.saveConfig': '保存配置',
'system.batchEmail.testConnection': '测试连接',
'system.batchEmail.subject': '邮件主题',
'system.batchEmail.subjectPlaceholder': '支持占位符,如:订单通知 - {客户单号}',
'system.batchEmail.subjectPlaceholder': `{'支持占位符,如:订单通知 - {客户单号}'}`,
'system.batchEmail.contentPlaceholder': '邮件正文',
'system.batchEmail.saveTplModalTitle': '保存模板',
'system.batchEmail.saveTplNamePlaceholder': '请输入模板名称',
@ -4377,7 +4508,7 @@ export default {
'channel.salesProducts.form.additionalServices': '附加服务',
'channel.salesProducts.billingSimulate.open': '计费模拟',
'channel.salesProducts.billingSimulate.title': '计费模拟',
'channel.salesProducts.billingSimulate.country': '国家',
'channel.salesProducts.billingSimulate.destCountry': '国家',
'channel.salesProducts.billingSimulate.province': '省/州',
'channel.salesProducts.billingSimulate.city': '城市',
'channel.salesProducts.billingSimulate.zipCode': '邮编',
@ -4609,7 +4740,7 @@ export default {
'channel.area.pagination.page': '页',
'channel.area.form.keyword': '关键字',
'channel.area.form.areaName': '分区名称',
'channel.area.form.country': '国家/地区',
'channel.area.form.destCountry': '国家/地区',
'channel.area.form.province': '州/省',
'channel.area.form.city': '城市',
'channel.area.form.leftPostCode': '起始邮编',
@ -4618,7 +4749,7 @@ export default {
'channel.area.form.placeholder.areaName': '请输入分区名称',
'channel.area.form.placeholder.input': '请输入',
'channel.area.form.rules.areaNameRequired': '请输入分区名称',
'channel.area.form.rules.countryRequired': '请选择国家/地区',
'channel.area.form.rules.destCountryRequired': '请选择国家/地区',
'channel.area.form.rules.maxLength128': '限制不可超过128个字符',
'channel.area.form.rules.maxLength30': '限制不可超过30个字符',
'channel.area.form.rules.noChinese': '不能存在中文字符',
@ -4676,8 +4807,8 @@ export default {
'channel.area.form.rules.maxLength50': '限制不可超过50个字符',
'channel.area.form.rules.postcodeRangeInvalid': '起始邮编不能大于截止邮编',
'channel.area.modal.deleteConfirmWithName': '确定删除分区「{name}」吗?其下明细将一并删除。',
'channel.area.message.countryLoadFailed': '国家列表加载失败,请刷新后重试',
'channel.area.message.countryQuickTagMiss': '未匹配到该国家,请从下拉选择',
'channel.area.message.destCountryLoadFailed': '国家列表加载失败,请刷新后重试',
'channel.area.message.destCountryQuickTagMiss': '未匹配到该国家,请从下拉选择',
'channel.area.message.editMissingRow': '未找到要编辑的数据,请刷新后重试',
'channel.area.message.deleteFailed': '删除失败,请稍后重试',
@ -4919,7 +5050,7 @@ export default {
'channel.issueTemplate.message.saveFailed': '保存失败',
// 轨迹中心 - 轨迹订单
'track.order.search.placeholder.orderNo': '请输入单号',
'track.order.search.placeholder.customsOrderNo': '请输入单号',
'track.order.search.placeholder.isExternal': '请选择是否对外',
'track.order.search.label.showTrajectory': '显示轨迹',
'track.order.search.label.hideTrajectory': '隐藏轨迹',
@ -4938,7 +5069,7 @@ export default {
'track.order.drawer.tabs.package': '大包',
'track.order.drawer.tabs.main': '主单',
'track.order.table.orderType': '订单类型',
'track.order.table.orderNo': '客户单号',
'track.order.table.customsOrderNo': '客户单号',
'track.order.table.customerShortName': '客户简称',
'track.order.table.thirdPartyOrderNo': '尾程单号',
'track.order.table.estimatedDeliveryTime': '预计签收时间',
@ -4957,7 +5088,7 @@ export default {
'track.order.table.product': '销售产品',
'track.order.table.channel': '服务渠道',
'track.order.table.apiCode': 'API服务商',
'track.order.table.countryCode': '国家',
'track.order.table.destCountryCode': '国家',
'track.order.table.province': '州省',
'track.order.table.city': '城市',
'track.order.table.postcode': '邮编',
@ -5003,7 +5134,7 @@ export default {
'track.order.modal.viewDetail': '查看明细',
'track.order.modal.deleteSingleTrajectory': '是否要删除轨迹 {content}?',
'track.order.modal.deleteMultipleTrajectory': '是否要删除以下{count}条轨迹?',
'track.order.modal.deleteOrder': '是否要删除订单 {orderNo}?',
'track.order.modal.deleteOrder': '是否要删除订单 {customsOrderNo}?',
'track.order.modal.deleteMultipleOrders': '是否要删除以下 {count} 个订单?',
'track.order.message.selectSameOrderType': '请选择同一订单类型和子系统',
'track.order.message.selectSameOrderTypeOnly': '请选择同一订单类型',
@ -5034,7 +5165,7 @@ export default {
'track.order.common.no': '否',
'track.order.pullConfig.appKey': '应用Key',
'track.order.pullConfig.orderType': '订单类型',
'track.order.pullConfig.orderNos': '订单号',
'track.order.pullConfig.customsOrderNos': '订单号',
'track.order.pullConfig.items': '配置明细',
'track.order.pullConfig.noData': '暂无数据',
'track.order.pullConfig.table.index': '序号',
@ -5095,7 +5226,7 @@ export default {
'track.warningOrder.pagination.goTo': '前往',
'track.warningOrder.pagination.page': '页',
'track.warningOrder.table.serialNo': '序号',
'track.warningOrder.table.orderNo': '订单号',
'track.warningOrder.table.customsOrderNo': '订单号',
'track.warningOrder.table.status': '状态',
'track.warningOrder.table.createTime': '创建时间',
'track.warningOrder.message.loadFailed': '加载数据失败',

View File

@ -88,6 +88,17 @@ const ORDER: AppRouteRecordRaw = {
permission: 'Order.Pickup',
},
},
{
path: 'customs9610Order',
name: 'OrderCustoms9610',
component: () => import('@/views/order/customs9610Order/index.vue'),
meta: {
locale: 'menu.order.customs9610Order',
requiresAuth: true,
order: 5,
permission: 'Order.Customs9610',
},
},
{
path: 'templateMapping',
name: 'OrderTemplateMapping',

View File

@ -104,8 +104,8 @@ const SYSTEM_MANAGEMENT: AppRouteRecordRaw = {
meta: {
locale: 'menu.system.batchEmail',
requiresAuth: true,
/** RBAC 菜单/接口权限编码(与后端 Permission 配置一致即可) */
permission: 'System.BatchEmail.Use',
/** 与菜单/权限树编码一致(历史码 Rbac.email) */
permission: 'Rbac.email',
},
},
{

View File

@ -472,6 +472,8 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
// 检查权限(与 @/utils/rbac-permission 规则一致:资源码 ↔ Query 互通)
function hasPermission(permission: string): boolean {
if (!permission?.trim()) return true
// Admin 角色默认全权限
if (hasRole('Admin')) return true
const code = permission.trim()
const granted = permissions.value
if (granted.includes(code)) return true

View File

@ -46,7 +46,15 @@
.wh-action-btn--approve:hover,
.wh-action-btn--receive:hover,
.wh-action-btn--read:hover {
.wh-action-btn--read:hover,
.wh-action-btn--pay:hover {
color: rgb(var(--green-6));
background: rgb(var(--green-1));
}
.wh-action-btn--publish:hover,
.wh-action-btn--rebind:hover,
.wh-action-btn--ship:hover {
color: var(--app-primary-color);
background: var(--app-primary-color-light, #e6f7f7);
}

View File

@ -25,6 +25,7 @@ export const MENU_NAME_TO_LOCALE_KEY: Record<string, string> = {
我的订单: 'menu.order.myOrders',
运单管理: 'menu.order.waybillManage',
揽收订单: 'menu.order.pickupOrder',
'9610订单': 'menu.order.customs9610Order',
批量任务: 'menu.order.batchJobs',
店铺授权: 'menu.order.platformStore',
店铺订单: 'menu.order.platformStoreOrder',

View File

@ -29,6 +29,7 @@ export const SUB_MENU_DEFAULT_ICONS: Record<string, string> = {
'menu.order.placeOrder': 'IconPlus',
'menu.order.myOrders': 'IconOrderedList',
'menu.order.waybillManage': 'IconStorage',
'menu.order.customs9610Order': 'IconFile',
'menu.order.batchJobs': 'IconSchedule',
'menu.channel.channelGuide': 'IconBook',
'menu.channel.salesProducts': 'IconTag',

View File

@ -1,5 +1,62 @@
import { useRbacUserStore } from '@/store/modules/rbac-user'
import { normalizeResourcePermissionCode } from '@/constants/pageButtonCatalog'
import {
normalizeResourcePermissionCode,
PAGE_BUTTON_PARENT_ALIASES,
} from '@/constants/pageButtonCatalog'
/** 页面级动作后缀:有资源码或动作码之一即可进页 */
const PAGE_ACTION_SUFFIX = /\.(Query|Use|View)$/i
/**
* 展开权限码候选(含资源级、动作级、历史别名),用于路由/按钮校验互认。
*
* 例:
* - `System.BatchEmail.Use` ↔ `System.BatchEmail` ↔ `Rbac.email`
* - `Rbac.Roles.Query` ↔ `Rbac.Roles`
*/
function expandPermissionCandidates(permission: string): string[] {
const code = permission.trim()
if (!code) return []
const set = new Set<string>([code])
const addAliasFamily = (raw: string) => {
if (!raw) return
set.add(raw)
const mapped = PAGE_BUTTON_PARENT_ALIASES[raw]
if (mapped) set.add(mapped)
if (PAGE_ACTION_SUFFIX.test(raw)) {
const resource = raw.replace(PAGE_ACTION_SUFFIX, '')
set.add(resource)
const mappedResource = PAGE_BUTTON_PARENT_ALIASES[resource]
if (mappedResource) set.add(mappedResource)
} else {
set.add(`${raw}.Query`)
set.add(`${raw}.Use`)
set.add(`${raw}.View`)
}
}
addAliasFamily(code)
// 反向:别名表里指向同一资源的历史菜单码
const resources = Array.from(set)
for (const [from, to] of Object.entries(PAGE_BUTTON_PARENT_ALIASES)) {
if (resources.includes(to) || resources.includes(from)) {
addAliasFamily(from)
addAliasFamily(to)
}
}
// normalizeResourcePermissionCode 仅剥 .Query,这里再补一层
for (const item of Array.from(set)) {
const normalized = normalizeResourcePermissionCode(item)
if (normalized) set.add(normalized)
}
return Array.from(set)
}
/**
* 检查是否有权限(三级按钮 / 路由 meta.permission)
@ -7,23 +64,16 @@ import { normalizeResourcePermissionCode } from '@/constants/pageButtonCatalog'
* 兼容菜单同步后的编码差异:
* - 二级菜单可能是资源码 `Operation.PrintLabel`
* - 路由 meta 可能是 `Operation.PrintLabel.Query`
* 二者互相视为可访问页面。
* - 历史菜单码如 `Rbac.email` 与目录码 `System.BatchEmail.Use`
* 以上互相视为可访问。
*/
export function hasPermission(permission: string): boolean {
const userStore = useRbacUserStore()
if (!permission?.trim()) return true
const code = permission.trim()
// Admin 角色默认全权限,无需在角色管理中逐项分配
if (hasRole('Admin')) return true
const granted = userStore.permissions
if (granted.includes(code)) return true
if (/\.Query$/i.test(code)) {
const resource = normalizeResourcePermissionCode(code)
if (resource && granted.includes(resource)) return true
} else if (granted.includes(`${code}.Query`)) {
return true
}
return false
return expandPermissionCandidates(permission).some((code) => granted.includes(code))
}
/**

View File

@ -221,7 +221,10 @@ export function buildPermissionCascadeContext(
}
}
/** 勾选子级时联动勾选上级;取消一级时联动取消其下全部权限 */
/**
* 勾选:选中自身 + 祖先 + 子孙(一级/二级勾选即带齐其下按钮权限)
* 取消:一级/二级取消时清空其下全部;按钮仅取消自身
*/
export function togglePermissionWithCascade(
selected: readonly string[],
context: PermissionCascadeContext,
@ -237,6 +240,12 @@ export function togglePermissionWithCascade(
for (const ancestor of context.ancestors.get(code) ?? []) {
next.add(ancestor)
}
for (const descendant of context.descendantsByLevel1.get(code) ?? []) {
next.add(descendant)
}
for (const descendant of context.descendantsByLevel2.get(code) ?? []) {
next.add(descendant)
}
return Array.from(next)
}

View File

@ -112,7 +112,8 @@ function isActive(path: string) {
z-index: 100;
display: flex;
justify-content: center;
padding: 0 14px calc(6px + env(safe-area-inset-bottom, 0px));
/* 下沉贴近底部圆角 / Home Indicator,略侵入 safe-area */
padding: 0 14px var(--h5-dock-bottom-inset, max(4px, calc(env(safe-area-inset-bottom, 0px) - 16px)));
pointer-events: none;
}

View File

@ -180,9 +180,6 @@
>
<!-- 顶栏:渠道 + 单号(左上) -->
<div class="h5-order-top">
<span v-if="isFulfillOrder(row)" class="h5-order-fulfill-badge">
{{ t('order.fulfillOrders.badge') }}
</span>
<button
type="button"
class="h5-order-no"
@ -2158,16 +2155,6 @@ void bootstrap();
margin-bottom: 18px;
}
.h5-order-fulfill-badge {
flex-shrink: 0;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
color: var(--app-primary-color, #054f74);
background: rgba(29, 114, 129, 0.1);
}
.h5-order-no {
min-width: 0;
flex: 1;

View File

@ -23,7 +23,9 @@
--h5-primary-soft: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
--h5-primary-softer: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
--h5-safe-bottom: env(safe-area-inset-bottom, 0px);
--h5-dock-offset: calc(62px + var(--h5-safe-bottom));
/* 底部 dock 下沉贴近圆角;有 Home Indicator 时略侵入 safe-area */
--h5-dock-bottom-inset: max(4px, calc(var(--h5-safe-bottom) - 16px));
--h5-dock-offset: calc(56px + var(--h5-dock-bottom-inset));
background: var(--h5-bg);
color: var(--h5-text);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,315 @@
<template>
<div class="bindable-order-picker">
<div class="bindable-order-picker__toolbar">
<a-input
v-model="keyword"
allow-clear
:placeholder="t('order.pickupOrder.create.orderKeywordPh')"
style="width: 220px"
@press-enter="reload"
/>
<a-button class="app-btn-search" :loading="loading" @click="reload">
{{ t('order.pickupOrder.actions.search') }}
</a-button>
</div>
<a-table
row-key="id"
size="small"
:loading="loading"
:data="rows"
:pagination="false"
:scroll="{ y: scrollY }"
:row-selection="rowSelection"
:columns="columns"
v-model:selected-keys="selectedKeys"
@selection-change="onSelectionChange"
>
<template #empty>
<div class="bindable-order-picker__empty">
<p>{{ t('order.pickupOrder.create.emptyOrders') }}</p>
<a-button type="outline" size="small" @click="goPredeclared">
{{ t('order.pickupOrder.create.goPredeclared') }}
</a-button>
</div>
</template>
</a-table>
<div class="bindable-order-picker__footer">
{{
t('order.pickupOrder.create.selectedOrders', {
count: selectedRows.length,
warehouse: selectedWarehouseLabel || warehouseHint || '-',
})
}}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue'
import { postOrderList } from '@/api/order'
import { $baseMessage } from '@/utils'
import { normalizeListRow, pickField } from '@/utils/warehouseListMapper'
import { getOrderStatusMeta, resolveOrderStatusCode } from '@/constants/orderStatus'
import { useRbacUserStore } from '@/store/modules/rbac-user'
export type BindableOrderRow = {
id: number
serviceNo: string
clientNo: string
warehouseId: number
warehouseName: string
customerId?: number
status: string
statusName: string
}
const BINDABLE_STATUSES = new Set([
'draft',
'predeclared',
'pendingAllocation',
'pendingPayment',
'pendingShip',
'pendingOnline',
])
const props = withDefaults(
defineProps<{
customerId?: number
/** 限定同仓(改绑时传入揽收单仓库) */
warehouseId?: number
/** 排除已在揽收单中的订单 */
excludeOrderIds?: number[]
scrollY?: number
/** 打开时是否自动加载 */
autoLoad?: boolean
}>(),
{
scrollY: 240,
autoLoad: true,
excludeOrderIds: () => [],
},
)
const emit = defineEmits<{
change: [rows: BindableOrderRow[]]
}>()
const { t } = useI18n()
const router = useRouter()
const rbacUserStore = useRbacUserStore()
const loading = ref(false)
const keyword = ref('')
const rows = ref<BindableOrderRow[]>([])
const selectedKeys = ref<Array<number | string>>([])
const selectedRows = ref<BindableOrderRow[]>([])
const columns = computed<TableColumnData[]>(() => [
{
title: t('order.pickupOrder.table.serviceNo'),
dataIndex: 'serviceNo',
width: 160,
ellipsis: true,
tooltip: true,
},
{
title: t('order.pickupOrder.table.clientNo'),
dataIndex: 'clientNo',
width: 120,
ellipsis: true,
tooltip: true,
},
{
title: t('order.pickupOrder.table.warehouse'),
dataIndex: 'warehouseName',
width: 120,
ellipsis: true,
tooltip: true,
},
{ title: t('order.pickupOrder.table.status'), dataIndex: 'statusName', width: 100 },
])
const rowSelection = computed<TableRowSelection>(() => ({
type: 'checkbox',
showCheckedAll: true,
onlyCurrent: true,
}))
const warehouseHint = computed(() => {
if (!props.warehouseId) return ''
const hit = rows.value.find((x) => x.warehouseId === props.warehouseId)
return hit?.warehouseName || String(props.warehouseId)
})
const selectedWarehouseLabel = computed(() => {
const ids = [...new Set(selectedRows.value.map((x) => x.warehouseId).filter((x) => x > 0))]
if (ids.length !== 1) return ''
const fromRow = selectedRows.value.find((x) => x.warehouseId === ids[0])?.warehouseName
return fromRow && fromRow !== '-' ? fromRow : String(ids[0])
})
const excludeSet = computed(() => new Set((props.excludeOrderIds || []).map(Number).filter((x) => x > 0)))
function statusName(code: string) {
const meta = getOrderStatusMeta(code)
return meta ? t(meta.labelKey) : code
}
function mapOrderRow(raw: any): BindableOrderRow | null {
const row = normalizeListRow(raw)
const id = Number(pickField(row, 'id', 'Id') || 0)
if (!id || excludeSet.value.has(id)) return null
const status =
resolveOrderStatusCode(row) ||
String(pickField(row, 'orderStatus', 'OrderStatus', 'status', 'Status') || '')
const pickupOrderId = Number(pickField(row, 'pickupOrderId', 'PickupOrderId') || 0)
if (pickupOrderId > 0) return null
if (!BINDABLE_STATUSES.has(status)) return null
const warehouseId = Number(pickField(row, 'warehouseId', 'WarehouseId') || 0)
if (props.warehouseId && props.warehouseId > 0 && warehouseId !== props.warehouseId) return null
return {
id,
serviceNo: String(pickField(row, 'serviceNo', 'ServiceNo') || '-'),
clientNo: String(pickField(row, 'clientNo', 'ClientNo') || '-'),
warehouseId,
warehouseName: String(
pickField(row, 'warehouse', 'Warehouse', 'warehouseName', 'WarehouseName') || '-',
),
customerId: Number(pickField(row, 'customerId', 'CustomerId') || 0) || undefined,
status,
statusName: statusName(status),
}
}
function onSelectionChange(keys: Array<string | number>) {
const nextKeys = (keys || []).map((k) => Number(k)).filter((id) => id > 0)
const fromCurrent = rows.value.filter((r) => nextKeys.includes(r.id))
const kept = selectedRows.value.filter(
(r) => nextKeys.includes(r.id) && !fromCurrent.some((n) => n.id === r.id),
)
let next = [...kept, ...fromCurrent]
if (!props.warehouseId) {
const whIds = [...new Set(next.map((x) => x.warehouseId).filter((x) => x > 0))]
if (whIds.length > 1) {
const keepWh = next[0]?.warehouseId
next = next.filter((x) => x.warehouseId === keepWh)
$baseMessage(t('order.myOrders.createPickup.sameWarehouse'), 'warning')
}
}
selectedRows.value = next
selectedKeys.value = next.map((x) => x.id)
emit('change', next)
}
function goPredeclared() {
const path = rbacUserStore.isCustomerPortal()
? '/order/myOrders'
: '/order/waybillManage'
router.push({ path, query: { status: 'predeclared' } })
}
async function reload() {
if (props.customerId != null && !(props.customerId > 0) && props.customerId !== undefined) {
// noop — allow undefined for portal
}
loading.value = true
try {
const body: Record<string, unknown> = {
page: 1,
pageSize: 100,
status: 'predeclared',
keyword: keyword.value.trim() || undefined,
}
if (props.customerId && props.customerId > 0) {
body.customerId = props.customerId
}
if (props.warehouseId && props.warehouseId > 0) {
body.warehouseId = props.warehouseId
}
const res = await postOrderList(body as any)
rows.value = (res.items || [])
.map((raw: any) => mapOrderRow(raw))
.filter((x): x is BindableOrderRow => !!x)
const idSet = new Set(rows.value.map((x) => x.id))
// 有关键词时只保留仍可见的勾选;清空搜索则保留已选
if (keyword.value.trim()) {
selectedRows.value = selectedRows.value.filter((x) => idSet.has(x.id))
selectedKeys.value = selectedRows.value.map((x) => x.id)
emit('change', selectedRows.value)
}
} catch (e: any) {
rows.value = []
$baseMessage(e?.message || t('order.pickupOrder.message.loadOrdersFailed'), 'error')
} finally {
loading.value = false
}
}
function clear() {
keyword.value = ''
rows.value = []
selectedKeys.value = []
selectedRows.value = []
emit('change', [])
}
function getSelected() {
return [...selectedRows.value]
}
watch(
() => [props.customerId, props.warehouseId, (props.excludeOrderIds || []).join(',')] as const,
() => {
if (props.autoLoad) void reload()
},
)
defineExpose({ reload, clear, getSelected })
</script>
<style scoped lang="less">
.bindable-order-picker__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.bindable-order-picker__footer {
margin-top: 8px;
padding: 8px 12px;
border-radius: 4px;
background: var(--color-fill-2);
font-size: 13px;
color: var(--color-text-2);
}
.bindable-order-picker__empty {
padding: 24px 12px;
text-align: center;
p {
margin: 0 0 12px;
font-size: 13px;
color: var(--color-text-3);
line-height: 1.5;
}
:deep(.arco-btn) {
height: 32px;
}
}
:deep(.arco-table-th) {
background: var(--color-fill-1);
}
</style>

View File

@ -0,0 +1,738 @@
<template>
<ModalPro
:title="t('order.pickupOrder.modal.create')"
:width="820"
:mask-closable="false"
@register="registerModal"
@cancel="closeModal"
>
<div class="wh-form-modal pk-create-modal">
<a-radio-group v-model="createMode" type="button" class="pk-create-mode" @change="onModeChange">
<a-radio value="bind">{{ t('order.pickupOrder.create.modeBind') }}</a-radio>
<a-radio value="new">{{ t('order.pickupOrder.create.modeNew') }}</a-radio>
</a-radio-group>
<a-alert type="info" show-icon class="pk-create-alert">
{{
createMode === 'bind'
? t('order.pickupOrder.create.leadBind')
: t('order.pickupOrder.create.lead')
}}
</a-alert>
<!-- 绑定已有运单 -->
<template v-if="createMode === 'bind'">
<div class="wh-form-section__head">
<div class="wh-form-section__title">{{ t('order.pickupOrder.create.sectionOrders') }}</div>
<a-space :size="8">
<a-input
v-model="orderKeyword"
allow-clear
:placeholder="t('order.pickupOrder.create.orderKeywordPh')"
style="width: 220px"
@press-enter="loadBindableOrders"
/>
<a-button class="app-btn-search" :loading="orderLoading" @click="loadBindableOrders">
{{ t('order.pickupOrder.actions.search') }}
</a-button>
</a-space>
</div>
<a-table
row-key="id"
size="small"
:loading="orderLoading"
:data="orderRows"
:pagination="false"
:scroll="{ y: 240 }"
:row-selection="orderRowSelection"
:columns="orderColumns"
v-model:selected-keys="selectedOrderKeys"
@selection-change="onOrderSelectionChange"
>
<template #empty>
<div class="pk-create-empty">
<p>{{ t('order.pickupOrder.create.emptyOrders') }}</p>
<a-button type="outline" size="small" @click="goPredeclared">
{{ t('order.pickupOrder.create.goPredeclared') }}
</a-button>
</div>
</template>
</a-table>
<div class="pk-create-selected">
{{
t('order.pickupOrder.create.selectedOrders', {
count: selectedOrders.length,
warehouse: selectedWarehouseLabel || '-',
})
}}
</div>
<a-divider class="wh-form-divider" />
<div class="wh-form-section__title">{{ t('order.pickupOrder.create.sectionPickup') }}</div>
<a-form
:model="bindForm"
layout="horizontal"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
>
<a-form-item :label="t('order.pickupOrder.form.senderAddress')" required>
<a-select
v-model="bindForm.senderAddressId"
:options="senderOptions"
allow-search
allow-clear
:loading="refLoading"
:placeholder="t('order.pickupOrder.form.senderAddressPlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.warehouse')" required>
<a-input :model-value="selectedWarehouseLabel || t('order.pickupOrder.create.warehouseFromOrders')" disabled />
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.pickupTime')" required>
<a-date-picker
v-model="bindForm.expectPickupTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</a-form-item>
<a-form-item :label="t('order.myOrders.createPickup.carrierService')">
<a-select
v-model="bindForm.carrierService"
:options="carrierServiceOptions"
allow-search
allow-clear
:loading="refLoading"
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
/>
</a-form-item>
<a-form-item :label="t('order.myOrders.createPickup.remark')">
<a-textarea v-model="bindForm.remark" :max-length="512" allow-clear :auto-size="{ minRows: 2, maxRows: 4 }" />
</a-form-item>
</a-form>
</template>
<!-- 新建运单揽收 -->
<template v-else>
<div class="wh-form-section__title">{{ t('order.pickupOrder.create.sectionPickup') }}</div>
<a-form
:model="newForm"
layout="horizontal"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
>
<a-form-item :label="t('order.pickupOrder.form.senderAddress')" required>
<a-select
v-model="newForm.senderAddressId"
:options="senderOptions"
allow-search
allow-clear
:loading="refLoading"
:placeholder="t('order.pickupOrder.form.senderAddressPlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.warehouse')" required>
<a-select
v-model="newForm.warehouseId"
:options="warehouseOptions"
allow-search
allow-clear
:loading="refLoading"
:placeholder="t('order.pickupOrder.form.warehousePlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.pickupTime')" required>
<a-date-picker
v-model="newForm.pickupTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</a-form-item>
</a-form>
<a-divider class="wh-form-divider" />
<div class="wh-form-section__title">{{ t('order.pickupOrder.create.sectionCargo') }}</div>
<a-form
:model="newForm"
layout="horizontal"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
>
<a-form-item :label="t('order.pickupOrder.form.product')" required>
<a-select
v-model="newForm.productCode"
:options="productOptions"
allow-search
allow-clear
:loading="refLoading"
:placeholder="t('order.pickupOrder.form.productPlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.cargoName')" required>
<a-input
v-model="newForm.cargoName"
allow-clear
:max-length="200"
:placeholder="t('order.pickupOrder.form.cargoNamePlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.actualWeight')" required>
<a-input-number
v-model="newForm.actualWeight"
:min="0.001"
:precision="3"
style="width: 100%"
:placeholder="t('order.pickupOrder.form.weightPlaceholder')"
>
<template #suffix>kg</template>
</a-input-number>
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.clientNo')">
<a-input
v-model="newForm.clientNo"
allow-clear
:max-length="64"
:placeholder="t('order.pickupOrder.form.clientNoPlaceholder')"
/>
</a-form-item>
</a-form>
</template>
</div>
<template #footer>
<a-space :size="8">
<a-button @click="closeModal">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :loading="creating" @click="handleCreate">
{{ t('order.pickupOrder.actions.submit') }}
</a-button>
</a-space>
</template>
</ModalPro>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import dayjs from 'dayjs'
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import { postOrderList } from '@/api/order'
import { createPickupOrderV2 } from '@/api/order/pickupOrder'
import { createPickupOrder, getSenderAddressList } from '@/api/order/senderAddress'
import { getWarehouseList } from '@/api/system/warehouse'
import { $baseMessage } from '@/utils'
import { normalizeListRow, pickField } from '@/utils/warehouseListMapper'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
import { loadDomesticPickupProductOptions } from '@/utils/domesticPickupProducts'
import {
checkCustomerContactRequired,
isCustomerContactRequiredError,
} from '@/utils/customerContactGate'
import type { CustomerProfileCompleteness } from '@/api/customer'
import { resolveOrderStatusCode, getOrderStatusMeta } from '@/constants/orderStatus'
import { useRbacUserStore } from '@/store/modules/rbac-user'
defineOptions({ name: 'CreatePickupSheetModal' })
type CreateMode = 'bind' | 'new'
type BindableOrderRow = {
id: number
serviceNo: string
clientNo: string
warehouseId: number
warehouseName: string
customerId?: number
status: string
statusName: string
}
const BINDABLE_STATUSES = new Set([
'draft',
'predeclared',
'pendingAllocation',
'pendingPayment',
'pendingShip',
'pendingOnline',
])
export type CreatePickupSuccessPayload = {
serviceNo?: string
pickupNo?: string
id?: number
pickupOrderId?: number
status?: string
payStatus?: string
receivableAmount?: number
}
const emit = defineEmits<{
success: [payload: CreatePickupSuccessPayload]
needContact: [profile: CustomerProfileCompleteness | null]
}>()
const { t } = useI18n()
const router = useRouter()
const rbacUserStore = useRbacUserStore()
const { isAdmin, withCustomerId, ensureMutationCustomerId } = useWarehouseAdminCustomer()
const [registerModal, { openModal, closeModal }] = useModal() as any
const createMode = ref<CreateMode>('bind')
const creating = ref(false)
const refLoading = ref(false)
const orderLoading = ref(false)
const orderKeyword = ref('')
const orderRows = ref<BindableOrderRow[]>([])
const selectedOrderKeys = ref<Array<number | string>>([])
const selectedOrders = ref<BindableOrderRow[]>([])
const senderOptions = ref<{ value: number; label: string }[]>([])
const warehouseOptions = ref<{ value: number; label: string }[]>([])
const productOptions = ref<{ value: string; label: string }[]>([])
const carrierServiceOptions = ref<{ value: string; label: string }[]>([])
const preferredCustomerId = ref<number | undefined>()
const bindForm = reactive({
senderAddressId: undefined as number | undefined,
expectPickupTime: '' as string,
carrierService: '' as string,
remark: '',
})
const newForm = reactive({
senderAddressId: undefined as number | undefined,
warehouseId: undefined as number | undefined,
productCode: '',
pickupTime: undefined as string | undefined,
cargoName: '',
actualWeight: undefined as number | undefined,
clientNo: '',
})
const orderColumns = computed<TableColumnData[]>(() => [
{ title: t('order.pickupOrder.table.serviceNo'), dataIndex: 'serviceNo', width: 160, ellipsis: true, tooltip: true },
{ title: t('order.pickupOrder.table.clientNo'), dataIndex: 'clientNo', width: 120, ellipsis: true, tooltip: true },
{ title: t('order.pickupOrder.table.warehouse'), dataIndex: 'warehouseName', width: 120, ellipsis: true, tooltip: true },
{ title: t('order.pickupOrder.table.status'), dataIndex: 'statusName', width: 100 },
])
const selectedWarehouseId = computed(() => {
const ids = [...new Set(selectedOrders.value.map((x) => x.warehouseId).filter((x) => x > 0))]
return ids.length === 1 ? ids[0] : undefined
})
const selectedWarehouseLabel = computed(() => {
if (!selectedWarehouseId.value) return ''
const fromRow = selectedOrders.value.find((x) => x.warehouseId === selectedWarehouseId.value)?.warehouseName
if (fromRow && fromRow !== '-') return fromRow
return warehouseOptions.value.find((x) => x.value === selectedWarehouseId.value)?.label || String(selectedWarehouseId.value)
})
const orderRowSelection = computed<TableRowSelection>(() => ({
type: 'checkbox',
showCheckedAll: true,
onlyCurrent: true,
}))
function onOrderSelectionChange(keys: Array<string | number>) {
const nextKeys = (keys || []).map((k) => Number(k)).filter((id) => id > 0)
const fromCurrent = orderRows.value.filter((r) => nextKeys.includes(r.id))
const kept = selectedOrders.value.filter(
(r) => nextKeys.includes(r.id) && !fromCurrent.some((n) => n.id === r.id),
)
let next = [...kept, ...fromCurrent]
const whIds = [...new Set(next.map((x) => x.warehouseId).filter((x) => x > 0))]
if (whIds.length > 1) {
const keepWh = next[0]?.warehouseId
next = next.filter((x) => x.warehouseId === keepWh)
$baseMessage(t('order.myOrders.createPickup.sameWarehouse'), 'warning')
}
selectedOrders.value = next
selectedOrderKeys.value = next.map((x) => x.id)
}
function resolveMutationCustomerId(): number | undefined {
if (!isAdmin.value) return undefined
const preferred = preferredCustomerId.value
if (preferred != null && preferred > 0) return preferred
return ensureMutationCustomerId()
}
function defaultPickupTime() {
return dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
}
function statusName(code: string) {
const meta = getOrderStatusMeta(code)
return meta ? t(meta.labelKey) : code
}
function mapOrderRow(raw: any): BindableOrderRow | null {
const row = normalizeListRow(raw)
const id = Number(pickField(row, 'id', 'Id') || 0)
if (!id) return null
const status = resolveOrderStatusCode(row) || String(pickField(row, 'orderStatus', 'OrderStatus', 'status', 'Status') || '')
const pickupOrderId = Number(pickField(row, 'pickupOrderId', 'PickupOrderId') || 0)
if (pickupOrderId > 0) return null
if (!BINDABLE_STATUSES.has(status)) return null
return {
id,
serviceNo: String(pickField(row, 'serviceNo', 'ServiceNo') || '-'),
clientNo: String(pickField(row, 'clientNo', 'ClientNo') || '-'),
warehouseId: Number(pickField(row, 'warehouseId', 'WarehouseId') || 0),
warehouseName: String(pickField(row, 'warehouse', 'Warehouse', 'warehouseName', 'WarehouseName') || '-'),
customerId: Number(pickField(row, 'customerId', 'CustomerId') || 0) || undefined,
status,
statusName: statusName(status),
}
}
async function loadReferenceOptions() {
const mutationCustomerId = resolveMutationCustomerId()
if (isAdmin.value && mutationCustomerId == null) {
senderOptions.value = []
return false
}
refLoading.value = true
try {
const senderBody = withCustomerId({
page: 1,
pageSize: 200,
addressType: 'sender',
})
if (mutationCustomerId) senderBody.customerId = mutationCustomerId
const [senderRes, warehouseRes, pickupProducts] = await Promise.all([
getSenderAddressList(senderBody),
getWarehouseList({ page: 1, pageSize: 500 }),
loadDomesticPickupProductOptions(),
])
senderOptions.value = (senderRes.items || [])
.map((item: any) => {
const row = normalizeListRow(item)
const id = Number(pickField(row, 'id', 'Id') || 0)
const name = String(
pickField(row, 'addressName', 'AddressName') ||
pickField(row, 'senderName', 'SenderName') ||
id,
)
const phone = String(pickField(row, 'phone', 'Phone') || '')
return { value: id, label: phone ? `${name} · ${phone}` : name }
})
.filter((x) => x.value > 0)
warehouseOptions.value = (warehouseRes.items || [])
.map((item: any) => {
const id = Number(item?.id ?? item?.Id ?? 0)
const name = String(item?.warehouseName ?? item?.WarehouseName ?? id)
return { value: id, label: name }
})
.filter((x) => x.value > 0)
const productOpts = pickupProducts.map((p) => ({ value: p.value, label: p.label }))
productOptions.value = productOpts
carrierServiceOptions.value = productOpts
return true
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.loadRefFailed'), 'error')
return false
} finally {
refLoading.value = false
}
}
async function loadBindableOrders() {
const mutationCustomerId = resolveMutationCustomerId()
if (isAdmin.value && mutationCustomerId == null) return
orderLoading.value = true
try {
const body: Record<string, unknown> = {
page: 1,
pageSize: 100,
status: 'predeclared',
keyword: orderKeyword.value.trim() || undefined,
}
if (mutationCustomerId) body.customerId = mutationCustomerId
const res = await postOrderList(body as any)
const mapped = (res.items || [])
.map((raw: any) => mapOrderRow(raw))
.filter((x): x is BindableOrderRow => !!x)
orderRows.value = mapped
// 清理不在最新结果中的勾选(关键词搜索后)
const idSet = new Set(mapped.map((x) => x.id))
selectedOrders.value = selectedOrders.value.filter((x) => idSet.has(x.id) || !orderKeyword.value.trim())
selectedOrderKeys.value = selectedOrders.value.map((x) => x.id)
} catch (e: any) {
orderRows.value = []
$baseMessage(e?.message || t('order.pickupOrder.message.loadOrdersFailed'), 'error')
} finally {
orderLoading.value = false
}
}
function resetBindForm() {
bindForm.senderAddressId = senderOptions.value[0]?.value
bindForm.expectPickupTime = defaultPickupTime()
bindForm.carrierService = ''
bindForm.remark = ''
orderKeyword.value = ''
orderRows.value = []
selectedOrderKeys.value = []
selectedOrders.value = []
}
function resetNewForm() {
Object.assign(newForm, {
senderAddressId: senderOptions.value[0]?.value,
warehouseId: warehouseOptions.value[0]?.value,
productCode: productOptions.value[0]?.value || '',
pickupTime: defaultPickupTime(),
cargoName: '',
actualWeight: undefined,
clientNo: '',
})
}
async function onModeChange() {
if (createMode.value === 'bind' && !orderRows.value.length) {
await loadBindableOrders()
}
}
function goPredeclared() {
const path = rbacUserStore.isCustomerPortal()
? '/order/myOrders'
: '/order/waybillManage'
router.push({ path, query: { status: 'predeclared' } })
}
function buildSuccessPayload(res: any, extra?: Partial<CreatePickupSuccessPayload>): CreatePickupSuccessPayload {
const id = Number(res?.id ?? res?.Id ?? extra?.id ?? 0) || undefined
const pickupOrderId =
Number(res?.pickupOrderId ?? res?.PickupOrderId ?? extra?.pickupOrderId ?? id ?? 0) || undefined
const receivableRaw = res?.receivableAmount ?? res?.ReceivableAmount
const receivableAmount =
receivableRaw != null && Number.isFinite(Number(receivableRaw))
? Number(receivableRaw)
: undefined
return {
serviceNo: extra?.serviceNo || res?.serviceNo || res?.ServiceNo || undefined,
pickupNo: extra?.pickupNo || res?.pickupNo || res?.PickupNo || undefined,
id,
pickupOrderId,
status: String(extra?.status ?? res?.status ?? res?.Status ?? '') || undefined,
payStatus: String(extra?.payStatus ?? res?.payStatus ?? res?.PayStatus ?? '') || undefined,
receivableAmount,
}
}
async function open(customerId?: number) {
preferredCustomerId.value =
customerId != null && Number(customerId) > 0 ? Number(customerId) : undefined
if (isAdmin.value && resolveMutationCustomerId() == null) return
if (!isAdmin.value) {
const { needContact, profile } = await checkCustomerContactRequired()
if (needContact) {
emit('needContact', profile)
$baseMessage(t('order.pickupOrder.message.contactRequired'), 'warning')
return
}
}
const ok = await loadReferenceOptions()
if (!ok) return
if (!senderOptions.value.length) {
$baseMessage(t('order.pickupOrder.message.noSenderAddress'), 'warning')
return
}
createMode.value = 'bind'
resetBindForm()
resetNewForm()
openModal(true)
await loadBindableOrders()
}
function validateBind() {
if (!selectedOrders.value.length) {
$baseMessage(t('order.pickupOrder.create.needSelectOrders'), 'warning')
return false
}
const whIds = [...new Set(selectedOrders.value.map((x) => x.warehouseId).filter((x) => x > 0))]
if (whIds.length !== 1) {
$baseMessage(t('order.myOrders.createPickup.sameWarehouse'), 'warning')
return false
}
if (!bindForm.senderAddressId) {
$baseMessage(t('order.pickupOrder.message.senderRequired'), 'warning')
return false
}
if (!bindForm.expectPickupTime) {
$baseMessage(t('order.pickupOrder.message.pickupTimeRequired'), 'warning')
return false
}
return true
}
function validateNew() {
if (!newForm.senderAddressId) {
$baseMessage(t('order.pickupOrder.message.senderRequired'), 'warning')
return false
}
if (!newForm.warehouseId) {
$baseMessage(t('order.pickupOrder.message.warehouseRequired'), 'warning')
return false
}
if (!newForm.productCode) {
$baseMessage(t('order.pickupOrder.message.productRequired'), 'warning')
return false
}
if (!newForm.pickupTime) {
$baseMessage(t('order.pickupOrder.message.pickupTimeRequired'), 'warning')
return false
}
if (!newForm.cargoName.trim()) {
$baseMessage(t('order.pickupOrder.message.cargoNameRequired'), 'warning')
return false
}
if (!newForm.actualWeight || Number(newForm.actualWeight) <= 0) {
$baseMessage(t('order.pickupOrder.message.weightRequired'), 'warning')
return false
}
return true
}
async function handleCreate() {
const customerId = resolveMutationCustomerId()
if (isAdmin.value && customerId == null) return
if (createMode.value === 'bind') {
if (!validateBind()) return
creating.value = true
try {
const res: any = await createPickupOrderV2({
customerId: customerId || undefined,
senderAddressId: bindForm.senderAddressId!,
warehouseId: selectedWarehouseId.value!,
carrierService: bindForm.carrierService || undefined,
expectPickupTime: bindForm.expectPickupTime,
orderIds: selectedOrders.value.map((x) => x.id),
remark: bindForm.remark || undefined,
})
const pickupNo = res?.pickupNo || res?.PickupNo || ''
$baseMessage(
t('order.myOrders.createPickup.success', {
no: pickupNo || '',
count: selectedOrders.value.length,
}),
'success',
)
closeModal()
emit('success', buildSuccessPayload(res, { pickupNo }))
} catch (e: any) {
if (isCustomerContactRequiredError(e)) emit('needContact', null)
$baseMessage(e?.message || t('order.pickupOrder.message.createFailed'), 'error')
} finally {
creating.value = false
}
return
}
if (!productOptions.value.length) {
$baseMessage(t('order.pickupOrder.message.noPickupProduct'), 'warning')
return
}
if (!validateNew()) return
creating.value = true
try {
const result: any = await createPickupOrder({
senderAddressId: newForm.senderAddressId,
warehouseId: newForm.warehouseId,
productCode: newForm.productCode,
pickupTime: newForm.pickupTime,
cargoName: newForm.cargoName.trim(),
actualWeight: Number(newForm.actualWeight),
clientNo: newForm.clientNo.trim() || undefined,
customerId,
})
const serviceNo = result?.serviceNo || result?.ServiceNo || ''
$baseMessage(
serviceNo
? t('order.pickupOrder.message.createSuccessWithNo', { no: serviceNo })
: t('order.pickupOrder.message.createSuccess'),
'success',
)
closeModal()
emit('success', buildSuccessPayload(result, { serviceNo }))
} catch (e: any) {
if (isCustomerContactRequiredError(e)) emit('needContact', null)
$baseMessage(e?.message || t('order.pickupOrder.message.createFailed'), 'error')
} finally {
creating.value = false
}
}
defineExpose({ open })
</script>
<style scoped lang="less">
@import '@/styles/warehouse-form-modal.less';
.pk-create-mode {
margin-bottom: 12px;
}
.pk-create-alert {
margin-bottom: 16px;
}
.pk-create-selected {
margin-top: 8px;
padding: 8px 12px;
border-radius: 4px;
background: var(--color-fill-2);
font-size: 13px;
color: var(--color-text-2);
}
.pk-create-empty {
padding: 24px 12px;
text-align: center;
p {
margin: 0 0 12px;
font-size: 13px;
color: var(--color-text-3);
line-height: 1.5;
}
:deep(.arco-btn) {
height: 32px;
}
}
.pk-create-modal {
:deep(.arco-form-item) {
margin-bottom: 16px;
}
:deep(.arco-table-th) {
background: var(--color-fill-1);
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -79,10 +79,12 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import PaginationPro from '@/components/PaginationPro/index.vue'
import { getAccessLogList, getChangeLogList, type AccessLogItem, type ChangeLogItem } from '@/api/rbac/data-perm'
import { handleApiError } from '@/utils/rbac-error-handler'
const { t } = useI18n()
const activeTab = ref('change')
const changeLoading = ref(false)

View File

@ -1,5 +1,5 @@
<template>
<div class="template-item-form">
<a-form :model="model" layout="vertical" class="template-item-form">
<p class="form-tip">{{ dimensionTip }}</p>
<!-- 组织 -->
@ -139,7 +139,7 @@
{{ t('system.dataPerm.templates.form.filterExample') }}
</a-alert>
</template>
</div>
</a-form>
</template>
<script setup lang="ts">
@ -150,7 +150,6 @@ import { DEFAULT_BIZ_TYPES, FILTER_OPERATORS } from '../constants'
import type { TemplateItemFormModel } from '../templates/templateItemContent'
const props = defineProps<{
modelValue: TemplateItemFormModel
orgTree: { key: string; title: string; value: string; children?: unknown[] }[]
orgScopeOptions: { value: number; label: string }[]
dataScopeOptions: { value: number; label: string }[]

View File

@ -88,13 +88,13 @@
{{ t('system.dataPerm.templates.bindApplyHint') }}
</a-alert>
<a-form layout="vertical" class="template-form">
<a-form :model="bindForm" layout="vertical" class="template-form">
<a-form-item
:label="t('system.dataPerm.templates.field.bindRole')"
:extra="t('system.dataPerm.templates.field.bindRoleExtra')"
>
<a-select
v-model="boundRoleId"
v-model="bindForm.boundRoleId"
:options="roleOptions"
:placeholder="t('system.dataPerm.templates.field.bindRolePlaceholder')"
allow-clear
@ -114,7 +114,7 @@
<span class="apply-row-desc">{{ t('system.dataPerm.templates.applyToRoleDesc') }}</span>
</div>
<a-button
:disabled="!boundRoleId"
:disabled="!bindForm.boundRoleId"
:loading="roleApplyLoading"
@click="applyToBoundRole"
>
@ -319,7 +319,9 @@ const pagination = reactive({ current: 1, pageSize: 100, total: 0 })
const rolesLoading = ref(false)
const roleOptions = ref<{ value: string; label: string; code: string }[]>([])
const boundRoleId = ref<string>()
const bindForm = reactive({
boundRoleId: undefined as string | undefined,
})
const applyLoading = ref(false)
const roleApplyLoading = ref(false)
const usersLoading = ref(false)
@ -487,7 +489,7 @@ function resetForm() {
isBuiltIn: false,
items: [newItemRow(createDefaultItemForm('data_scope', 1))],
})
boundRoleId.value = undefined
bindForm.boundRoleId = undefined
Object.assign(applyForm, {
targetUserIds: [],
mode: 'replace',
@ -521,10 +523,10 @@ async function loadRoles() {
function syncBoundRoleFromCode() {
if (!form.roleCode) {
boundRoleId.value = undefined
bindForm.boundRoleId = undefined
return
}
boundRoleId.value = roleOptions.value.find((r) => r.code === form.roleCode)?.value
bindForm.boundRoleId = roleOptions.value.find((r) => r.code === form.roleCode)?.value
}
function onBoundRoleChange(roleId: string | undefined) {
@ -591,13 +593,13 @@ async function runApply(
}
async function applyToBoundRole() {
if (!boundRoleId.value) {
if (!bindForm.boundRoleId) {
Message.warning(t('system.dataPerm.templates.bindRoleFirst'))
return
}
roleApplyLoading.value = true
try {
await runApply('role', [boundRoleId.value], 'replace')
await runApply('role', [bindForm.boundRoleId], 'replace')
} catch (e) {
handleApiError(e)
} finally {

View File

@ -345,6 +345,19 @@ function actionRenderer(_row: DictDto, _rowIndex: number): string {
])
}
/** 固定列序:操作 + 序号 → 数据从 col 2 起 */
function customCellRenderer(_row: number, col: number, _value: unknown, rowData: DictDto): string | null {
const dataColIndex = col - 2
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
const key = hotColumns.value[dataColIndex].key
if (key === 'createTime') {
if (!rowData.createTime) return '--'
const d = dayjs(rowData.createTime)
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : String(rowData.createTime)
}
return null
}
function handleActionClick(row: DictDto, _rowIndex: number, action?: string) {
if (action === 'edit') {
handleEdit(row)

View File

@ -35,7 +35,7 @@
<div class="tool-detail liquid-card">
<template v-if="selected">
<div class="detail-title">{{ selected.displayName }}</div>
<a-form layout="vertical" class="detail-form">
<a-form :model="editForm" layout="vertical" class="detail-form">
<a-form-item :label="t('system.mcpTools.displayName')">
<a-input v-model="editForm.displayName" />
</a-form-item>

View File

@ -32,10 +32,9 @@
:empty="!selectedRole"
:empty-title="t('system.role.emptySelect')"
:empty-desc="t('system.role.emptyDesc')"
:title="selectedRole?.name || ''"
:subtitle="selectedRole?.code || ''"
:title="headerTitle"
:subtitle="headerSubtitle"
:tags="roleTags"
:stats="roleStats"
:tabs="roleTabs"
v-model:active-tab="activeTab"
:loading="permissionLoading && activeTab === 'permissions'"
@ -46,44 +45,37 @@
</div>
</template>
<template #actions>
<a-space>
<a-button
v-if="hasPermission('Rbac.Roles.Update') && activeTab === 'info'"
type="primary"
:loading="modalLoading"
@click="handleSaveRole"
>
{{ t('system.role.actions.save') }}
</a-button>
<a-button
v-if="hasPermission('Rbac.Roles.AssignPermissions') && activeTab === 'permissions'"
type="primary"
:loading="permissionSaving"
@click="handleSavePermissions"
>
{{ t('system.role.permission.save') }}
</a-button>
</a-space>
<a-button
v-if="hasPermission('Rbac.Roles.Update') || isCreating"
type="primary"
:loading="modalLoading || permissionSaving"
@click="handleSaveAll"
>
{{ t('system.role.actions.save') }}
</a-button>
</template>
<div v-show="activeTab === 'info'" class="section-card">
<h3 class="section-card-title">{{ t('system.role.tab.info') }}</h3>
<template #header-extra>
<a-form
ref="formRef"
:model="formState"
:rules="formRules"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
class="role-form"
layout="inline"
class="role-header-form"
>
<a-form-item :label="t('system.role.form.name')" field="name">
<a-input v-model="formState.name" :placeholder="t('system.role.form.namePlaceholder')" />
<a-input
v-model="formState.name"
:placeholder="t('system.role.form.namePlaceholder')"
style="width: 160px"
/>
</a-form-item>
<a-form-item :label="t('system.role.form.code')" field="code">
<a-input
v-model="formState.code"
:placeholder="t('system.role.form.codePlaceholder')"
:disabled="!isCreating"
style="width: 160px"
/>
</a-form-item>
<a-form-item :label="t('system.role.form.boundWarehouse')" field="boundWarehouseId">
@ -92,33 +84,43 @@
:placeholder="t('system.role.form.boundWarehousePlaceholder')"
allow-clear
:options="warehouseOptions"
style="width: 180px"
/>
</a-form-item>
<a-form-item v-if="!isCreating" :label="t('system.role.table.createTime')">
<span>{{ formatTime(selectedRole?.createdAt) }}</span>
<span class="role-create-time">{{ formatTime(selectedRole?.createdAt) }}</span>
</a-form-item>
</a-form>
</div>
</template>
<div v-show="activeTab === 'permissions'" class="permission-section">
<div class="sticky-toolbar">
<a-space wrap>
<a-input
v-model="permissionKeyword"
:placeholder="t('system.role.permission.search')"
allow-clear
style="width: 220px"
/>
<a-button size="small" @click="checkAllPermissions">{{ t('system.role.permission.checkAll') }}</a-button>
<a-button size="small" @click="uncheckAllPermissions">{{ t('system.role.permission.uncheckAll') }}</a-button>
</a-space>
<span class="perm-summary">
{{ t('system.role.permission.selectedCount', { selected: selectedPermissionCount, total: totalPermissionCount }) }}
</span>
</div>
<div class="section-card permission-list-card">
<template v-if="activeTab === 'permissions' && !isSelectedAdminRole" #tabs-extra>
<a-space wrap :size="8">
<a-input
v-model="permissionKeyword"
:placeholder="t('system.role.permission.search')"
allow-clear
style="width: 220px"
/>
<a-button class="app-btn-toolbar" @click="checkAllPermissions">
{{ t('system.role.permission.checkAll') }}
</a-button>
<a-button class="app-btn-toolbar" @click="uncheckAllPermissions">
{{ t('system.role.permission.uncheckAll') }}
</a-button>
</a-space>
</template>
<div v-show="activeTab === 'permissions'" class="permission-body">
<a-alert
v-if="isSelectedAdminRole"
type="info"
show-icon
:title="t('system.role.permission.adminAllHint')"
/>
<template v-else>
<a-table
v-if="filteredPermissionRows.length > 0"
class="permission-table"
:data="filteredPermissionRows"
:pagination="false"
:bordered="{ cell: true }"
@ -158,10 +160,7 @@
</a-table-column>
<a-table-column :title="t('system.role.permission.col.level3')">
<template #cell="{ record }">
<div
v-if="record.level2HasChildren"
class="permission-button-group"
>
<div v-if="record.level2HasChildren" class="permission-button-group">
<a-checkbox
v-for="btn in record.buttons"
:key="btn.code"
@ -177,9 +176,13 @@
</template>
</a-table>
<div v-else class="permission-empty">
{{ permissionKeyword ? t('system.role.permission.noMatch') : t('system.role.permission.loading') }}
{{
permissionKeyword
? t('system.role.permission.noMatch')
: t('system.role.permission.loading')
}}
</div>
</div>
</template>
</div>
</RbacDetailShell>
</div>
@ -204,6 +207,7 @@ import type {
import { useWarehouseList } from '@/composables/useWarehouseList'
import { hasPermission } from '@/utils/rbac-permission'
import { handleApiError } from '@/utils/rbac-error-handler'
import { useRbacUserStore } from '@/store/modules/rbac-user'
import { buildFlatLeftMenuTree, findLeftMenuItemByKey } from '@/utils/rbacLeftMenuTree'
import {
buildPermissionAssignRows,
@ -225,7 +229,7 @@ const searchKeyword = ref('')
const selectedRole = ref<RoleDto | null>(null)
const isCreating = ref(false)
const expandedKeys = ref(['category-roles'])
const activeTab = ref('info')
const activeTab = ref('permissions')
const formRef = ref()
const permissionKeyword = ref('')
@ -250,7 +254,7 @@ const permissionLoading = ref(false)
const permissionSaving = ref(false)
const permissionTree = ref<PermissionGroupDto[]>([])
const selectedPermissions = ref<string[]>([])
const permissionTableScrollY = 'calc(100vh - 340px)'
const permissionTableScrollY = 'calc(100vh - 300px)'
const roleTreeData = computed(() =>
buildFlatLeftMenuTree('category-roles', t('system.role.pageTitle'), dataSource.value, {
@ -275,24 +279,48 @@ const filteredPermissionRows = computed(() =>
const level1RowSpans = computed(() => computeLevel1RowSpans(filteredPermissionRows.value))
const totalPermissionCount = computed(() =>
collectPermissionCodesFromRows(permissionRows.value).length
)
const selectedPermissionCount = computed(() =>
countSelectedAssignablePermissions(selectedPermissions.value, permissionRows.value)
)
const totalPermissionCount = computed(() =>
collectPermissionCodesFromRows(permissionRows.value).length
)
const normalizedSelectedPermissions = computed(() =>
normalizeSelectedPermissions(selectedPermissions.value, permissionRows.value)
)
function isCustomerRole(role?: RoleDto | null) {
const code = role?.code || ''
return code === 'Customer' || code.startsWith('CustomerSub_')
}
function isAdminRole(role?: RoleDto | null) {
return (role?.code || '').toLowerCase() === 'admin'
}
const isSelectedAdminRole = computed(() => isAdminRole(selectedRole.value))
const headerTitle = computed(() => {
if (isCreating.value) return t('system.role.actions.add')
return formState.name || selectedRole.value?.name || ''
})
const headerSubtitle = computed(() => {
if (isCreating.value) return formState.code || ''
return formState.code || selectedRole.value?.code || ''
})
const roleTags = computed((): RbacDetailTag[] => {
if (!selectedRole.value) return []
const tags: RbacDetailTag[] = []
if (isCreating.value) {
tags.push({ label: t('system.role.tag.creating'), color: 'orangered' })
}
if (isAdminRole(selectedRole.value)) {
tags.push({ label: 'Admin', color: 'purple' })
}
if (isCustomerRole(selectedRole.value)) {
tags.push({ label: t('system.role.category.system'), color: 'arcoblue' })
}
@ -302,21 +330,20 @@ const roleTags = computed((): RbacDetailTag[] => {
return tags
})
const roleStats = computed(() => [
{ label: t('system.role.permission.selected'), value: selectedPermissionCount.value },
{ label: t('system.role.permission.total'), value: totalPermissionCount.value },
])
const roleTabs = computed(() => {
const tabs = [{ key: 'info', label: t('system.role.tab.info') }]
if (hasPermission('Rbac.Roles.AssignPermissions')) {
tabs.push({
if (!hasPermission('Rbac.Roles.AssignPermissions')) return []
if (isSelectedAdminRole.value) {
return [{ key: 'permissions', label: t('system.role.modal.assignPermissions') }]
}
const selected = selectedPermissionCount.value
const total = totalPermissionCount.value
return [
{
key: 'permissions',
label: t('system.role.modal.assignPermissions'),
badge: selectedPermissionCount.value || undefined,
})
}
return tabs
badge: `${selected}/${total}`,
},
]
})
function permissionSpanMethod({
@ -352,12 +379,10 @@ function togglePermission(code: string, checked: boolean | (string | number | bo
}
function formatTime(value?: string | null) {
return value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'
}
function isCustomerRole(role?: RoleDto | null) {
const code = role?.code || ''
return code === 'Customer' || code.startsWith('CustomerSub_')
if (!value) return '—'
const d = dayjs(value)
if (!d.isValid() || d.year() < 1970) return '—'
return d.format('YYYY-MM-DD HH:mm:ss')
}
function selectRoleFromKey(key: string) {
@ -446,7 +471,7 @@ function handleAdd() {
isCreating.value = true
selectedRole.value = { name: '', code: '' }
Object.assign(formState, { name: '', code: '', boundWarehouseId: undefined })
activeTab.value = 'info'
activeTab.value = 'permissions'
selectedPermissions.value = []
}
@ -496,45 +521,80 @@ async function handleSaveRole() {
if (newRoleId) {
selectedRole.value = dataSource.value.find((r) => r.id === newRoleId) || null
}
} else if (selectedRole.value?.id) {
return newRoleId || null
}
if (selectedRole.value?.id) {
const updateData: RoleUpdateDto = { name: formState.name }
await RoleService.putRbacRoleUpdate(selectedRole.value.id, updateData)
if (formState.boundWarehouseId !== undefined) {
roleWarehouseMap.value[selectedRole.value.id] = formState.boundWarehouseId || ''
}
Message.success(t('system.role.message.updateSuccess'))
await loadData()
return selectedRole.value.id
}
return null
} catch (error: any) {
if (error?.fields) return
if (error?.fields) return null
handleApiError(error)
return null
} finally {
modalLoading.value = false
}
}
async function handleSavePermissions() {
if (!selectedRole.value?.id || isCreating.value) return
async function handleSavePermissions(roleId?: string) {
const id = roleId || selectedRole.value?.id
if (!id || isCreating.value) return false
if (isAdminRole(selectedRole.value)) return true
if (!hasPermission('Rbac.Roles.AssignPermissions')) return true
permissionSaving.value = true
try {
await RoleService.postRbacRoleAssignPermissions(
selectedRole.value.id,
normalizedSelectedPermissions.value
)
Message.success(t('system.role.permission.assignSuccess'))
await RoleService.postRbacRoleAssignPermissions(id, normalizedSelectedPermissions.value)
const rbacUserStore = useRbacUserStore()
const currentHasRole = rbacUserStore.userInfo?.roles?.some((r) => r.id === id)
if (currentHasRole) {
try {
await rbacUserStore.refreshUserInfo()
} catch {
Message.warning(t('system.role.permission.refreshHint'))
}
}
return true
} catch (error) {
handleApiError(error)
return false
} finally {
permissionSaving.value = false
}
}
async function handleSaveAll() {
const wasCreating = isCreating.value
const roleId = await handleSaveRole()
if (!roleId) return
// 新建已提示成功;Admin 无需写权限
if (wasCreating || isAdminRole(selectedRole.value)) {
if (!wasCreating) {
Message.success(t('system.role.message.updateSuccess'))
}
return
}
const ok = await handleSavePermissions(roleId)
if (ok) {
Message.success(t('system.role.message.updateSuccess'))
}
}
watch(
() => selectedRole.value?.id,
async (roleId) => {
if (!roleId || isCreating.value) return
syncFormFromRole(selectedRole.value!)
activeTab.value = 'info'
activeTab.value = 'permissions'
permissionKeyword.value = ''
await loadRolePermissions(roleId)
}
@ -549,18 +609,57 @@ onMounted(async () => {
<style scoped lang="less">
@import '@/components/RbacSplit/rbac-split-layout.less';
.role-form {
max-width: 640px;
.role-header-form {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
width: 100%;
:deep(.arco-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
:deep(.arco-form-item-label-col > label) {
color: var(--color-text-3);
font-size: 13px;
}
}
.permission-section {
.role-create-time {
display: inline-block;
min-width: 150px;
font-size: 13px;
color: var(--color-text-2);
line-height: 32px;
}
.permission-body {
display: flex;
flex-direction: column;
gap: 0;
min-height: 0;
height: 100%;
}
.permission-list-card {
min-height: 320px;
.permission-table {
width: 100%;
:deep(.arco-table-th) {
background: var(--color-fill-1);
}
:deep(.arco-table-td) {
vertical-align: middle;
}
:deep(.arco-table-td .arco-checkbox) {
display: inline-flex;
align-items: center;
}
:deep(.arco-table-td .arco-checkbox .arco-checkbox-label) {
line-height: 1.4;
}
}
.permission-button-group {
@ -574,26 +673,8 @@ onMounted(async () => {
color: var(--color-text-4);
}
.permission-list-card :deep(.arco-checkbox) {
align-items: flex-start;
}
.permission-list-card :deep(.arco-table-th) {
background: var(--color-fill-1);
}
.permission-list-card :deep(.arco-table-td) {
vertical-align: top;
}
.perm-summary {
font-size: 13px;
color: var(--color-text-3);
white-space: nowrap;
}
.permission-empty {
padding: 48px 24px;
padding: 64px 24px;
text-align: center;
color: var(--color-text-3);
}

View File

@ -64,6 +64,7 @@ export default defineConfig(({ mode }) => {
'/order/fulfillOrders',
'/order/waybillManage',
'/order/pickupOrder',
'/order/customs9610Order',
'/order/batchJobs',
'/order/templateMapping',
'/order/platformStore',
@ -448,7 +449,9 @@ export default defineConfig(({ mode }) => {
'/order/fulfillOrders',
'/order/waybillManage',
'/order/pickupOrder',
'/order/customs9610Order',
'/order/batchJobs',
'/order/templateMapping',
'/order/platformStore',
'/order/platformStoreOrder',
// Xhs 相关前端路由(勿用 /xhs 前缀,否则 API 会被误判为 SPA)