feat: complete phase 5 merchant delivery
This commit is contained in:
@@ -1,17 +1,125 @@
|
||||
import { overseasApiRequest } from '@/api/overseasHttp'
|
||||
|
||||
export interface PageResult<T> { records: T[]; page: number; size: number; total: number; pages: number }
|
||||
export interface DeliveryRow { id: string; deliveryNo: string; outboundNo: string; dispatchNo: string; overseasWarehouseName: string; status: string; waybillNo: string; deliveryMethod: string; carrierName: string; vehiclePlate: string; driverName: string; palletCount: number; packageCount: number; totalWeightKg: number; totalVolumeCbm: number; plannedDeliveryAt: string; expectedArrivalAt: string; signedAt: string; createdAt: string }
|
||||
export interface DeliveryQuery { page: number; size: number; documentNo?: string; warehouseId?: string; status?: string; dispatchNo?: string; waybillNo?: string; vehiclePlate?: string; driverName?: string; deliveryMethod?: string; plannedFrom?: string; plannedTo?: string }
|
||||
export interface DeliveryTransferPayload { dispatchNo: string; idempotencyKey: string; plannedDeliveryAt: string; deliveryMethod: string; pickupMethod: string; loadingType: string; waybillNo?: string; carrierName?: string; vehicleType?: string; vehicleSize?: string; driverName?: string; driverPhone?: string; vehiclePlate?: string; appointmentAccount?: string; sellerName?: string; remark?: string; fees: Array<{ feeType: string; currency: string; amount: number; remark?: string }> }
|
||||
export interface PageResult<T> {
|
||||
records: T[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
pages: number
|
||||
}
|
||||
|
||||
export interface DeliveryRow {
|
||||
id: string
|
||||
deliveryNo: string
|
||||
outboundNo: string | null
|
||||
dispatchNo: string
|
||||
overseasWarehouseName: string | null
|
||||
status: string
|
||||
waybillNo: string | null
|
||||
deliveryMethod: string
|
||||
carrierName: string | null
|
||||
vehiclePlate: string | null
|
||||
driverName: string | null
|
||||
palletCount: number
|
||||
packageCount: number
|
||||
totalWeightKg: number | null
|
||||
totalVolumeCbm: number | null
|
||||
plannedDeliveryAt: string
|
||||
expectedArrivalAt: string | null
|
||||
signedAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface DeliveryOrderFee {
|
||||
id?: string
|
||||
feeType: string
|
||||
currency: string
|
||||
amount: number
|
||||
remark?: string | null
|
||||
}
|
||||
|
||||
export interface DeliveryOrderItem {
|
||||
id?: string
|
||||
palletNo?: string | null
|
||||
inboundCode?: string | null
|
||||
skuCode?: string | null
|
||||
productName?: string | null
|
||||
packageCount?: number | null
|
||||
weightKg?: number | null
|
||||
volumeCbm?: number | null
|
||||
}
|
||||
|
||||
export interface DeliveryOrderDetail extends DeliveryRow {
|
||||
pickupMethod: string
|
||||
loadingType: string
|
||||
vehicleType: string | null
|
||||
vehicleSize: string | null
|
||||
driverPhone: string | null
|
||||
appointmentAccount: string | null
|
||||
sellerName: string | null
|
||||
remark: string | null
|
||||
fees: DeliveryOrderFee[]
|
||||
items?: DeliveryOrderItem[] | PageResult<DeliveryOrderItem>
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
export interface DeliveryQuery {
|
||||
page: number
|
||||
size: number
|
||||
documentNo?: string
|
||||
warehouseId?: string
|
||||
status?: string
|
||||
dispatchNo?: string
|
||||
waybillNo?: string
|
||||
vehiclePlate?: string
|
||||
driverName?: string
|
||||
deliveryMethod?: string
|
||||
plannedFrom?: string
|
||||
plannedTo?: string
|
||||
}
|
||||
|
||||
export interface DeliveryTransferPayload {
|
||||
dispatchNo: string
|
||||
idempotencyKey: string
|
||||
plannedDeliveryAt: string
|
||||
deliveryMethod: string
|
||||
pickupMethod: string
|
||||
loadingType: string
|
||||
waybillNo?: string
|
||||
carrierName?: string
|
||||
vehicleType?: string
|
||||
vehicleSize?: string
|
||||
driverName?: string
|
||||
driverPhone?: string
|
||||
vehiclePlate?: string
|
||||
appointmentAccount?: string
|
||||
sellerName?: string
|
||||
remark?: string
|
||||
fees: DeliveryOrderFee[]
|
||||
}
|
||||
|
||||
export function listDeliveryOrders(params: DeliveryQuery) {
|
||||
return overseasApiRequest<PageResult<DeliveryRow>>({ method: 'GET', url: '/merchant/v1/last-leg/delivery-orders', params })
|
||||
return overseasApiRequest<PageResult<DeliveryRow>>({
|
||||
method: 'GET',
|
||||
url: '/merchant/v1/last-leg/delivery-orders',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function getDeliveryOrder(deliveryNo: string) {
|
||||
return overseasApiRequest<Record<string, unknown>>({ method: 'GET', url: `/merchant/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, params: { page: 1, size: 200 } })
|
||||
return overseasApiRequest<DeliveryOrderDetail>({
|
||||
method: 'GET',
|
||||
url: `/merchant/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`,
|
||||
params: { page: 1, size: 200 },
|
||||
})
|
||||
}
|
||||
|
||||
export function createDeliveryOrder(payload: DeliveryTransferPayload) {
|
||||
const { dispatchNo, ...request } = payload
|
||||
return overseasApiRequest<Record<string, unknown>>({ method: 'POST', url: '/merchant/v1/last-leg/delivery-orders', params: { dispatchNo }, data: request })
|
||||
return overseasApiRequest<DeliveryOrderDetail>({
|
||||
method: 'POST',
|
||||
url: '/merchant/v1/last-leg/delivery-orders',
|
||||
params: { dispatchNo },
|
||||
data: request,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
|
||||
const apiSource = readFileSync(new URL('./api/deliveryOrders.ts', import.meta.url), 'utf8')
|
||||
const routesSource = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8')
|
||||
const listSource = readFileSync(new URL('./views/DeliveryOrdersView.vue', import.meta.url), 'utf8')
|
||||
const detailSource = readFileSync(new URL('./views/DeliveryOrderDetailView.vue', import.meta.url), 'utf8')
|
||||
const createSource = readFileSync(new URL('./views/DeliveryOrderCreateView.vue', import.meta.url), 'utf8')
|
||||
const permissionSource = readFileSync(new URL('../tenant-management/contracts.ts', import.meta.url), 'utf8')
|
||||
|
||||
async function importTypeScriptModule(source) {
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText
|
||||
return import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`)
|
||||
}
|
||||
|
||||
test('uses only authenticated merchant delivery endpoints without customer scope or mock fallback', () => {
|
||||
assert.match(apiSource, /method: 'GET'[\s\S]*url: '\/merchant\/v1\/last-leg\/delivery-orders'/)
|
||||
assert.match(apiSource, /delivery-orders\/\$\{encodeURIComponent\(deliveryNo\)\}/)
|
||||
assert.match(apiSource, /method: 'POST'[\s\S]*url: '\/merchant\/v1\/last-leg\/delivery-orders'/)
|
||||
assert.match(apiSource, /const \{ dispatchNo, \.\.\.request \} = payload/)
|
||||
assert.match(apiSource, /params: \{ dispatchNo \}/)
|
||||
assert.match(apiSource, /overseasApiRequest<DeliveryOrderDetail>/)
|
||||
assert.doesNotMatch(apiSource, /customerId|tenantId|Record<string, unknown>|mock|fallback/i)
|
||||
})
|
||||
|
||||
test('keeps list, read-only detail and create permissions independent in active routes', () => {
|
||||
assert.match(routesSource, /path: 'orders\/delivery'[\s\S]*permission: 'merchant:warehouse:delivery:list'[\s\S]*navigation:/)
|
||||
assert.match(routesSource, /path: 'orders\/delivery\/create'[\s\S]*DeliveryOrderCreateView\.vue[\s\S]*permission: 'merchant:warehouse:delivery:create'/)
|
||||
assert.match(routesSource, /path: 'orders\/delivery\/:deliveryNo'[\s\S]*DeliveryOrderDetailView\.vue[\s\S]*props: true[\s\S]*permission: 'merchant:warehouse:delivery:list'/)
|
||||
assert.match(routesSource, /orders-delivery-create[\s\S]*keepAlive: false/)
|
||||
assert.match(routesSource, /orders-delivery-detail[\s\S]*keepAlive: false/)
|
||||
})
|
||||
|
||||
test('renders the operation column first and covers loading, empty, error and pagination states', () => {
|
||||
const operationIndex = listSource.indexOf('label="操作"')
|
||||
const deliveryNumberIndex = listSource.indexOf('label="派送单号"')
|
||||
assert.ok(operationIndex >= 0 && operationIndex < deliveryNumberIndex)
|
||||
assert.match(listSource, /v-loading="loading"/)
|
||||
assert.match(listSource, /empty-text="暂无派送单"/)
|
||||
assert.match(listSource, /delivery-state--error/)
|
||||
assert.match(listSource, /@click="loadOrders">重试/)
|
||||
assert.match(listSource, /listDeliveryOrders\(buildQuery\(\)\)/)
|
||||
assert.match(listSource, /<el-pagination/)
|
||||
assert.doesNotMatch(listSource, /row\.id[^a-zA-Z]|deliveryNo \|\| row\.id|customerId|tenantId/)
|
||||
})
|
||||
|
||||
test('shows the create button only with the independent create permission', () => {
|
||||
assert.match(listSource, /auth\.hasPermission\('merchant:warehouse:delivery:create'\)/)
|
||||
assert.match(listSource, /<el-button v-if="canCreate" type="primary" @click="openCreate"/)
|
||||
assert.doesNotMatch(listSource, /hasPermission\('merchant:warehouse:delivery:list'\)[\s\S]{0,120}创建派送单/)
|
||||
})
|
||||
|
||||
test('loads a typed detail route by delivery number and never exposes an internal id fallback', () => {
|
||||
assert.match(detailSource, /defineProps<\{ deliveryNo: string \}>/)
|
||||
assert.match(detailSource, /getDeliveryOrder\(props\.deliveryNo\)/)
|
||||
assert.match(detailSource, /派送单详情加载失败/)
|
||||
for (const heading of ['订单信息', '运输与预约', '货物明细', '费用明细']) assert.match(detailSource, new RegExp(heading))
|
||||
assert.doesNotMatch(detailSource, /detail\?\.id|deliveryNo \|\| detail\?\.id|customerId|tenantId/)
|
||||
})
|
||||
|
||||
test('submits the complete create contract with stable retry idempotency and recoverable failures', () => {
|
||||
for (const model of ['dispatchNo', 'plannedDeliveryAt', 'deliveryMethod', 'pickupMethod', 'loadingType', 'waybillNo', 'carrierName', 'vehicleType', 'vehicleSize', 'driverName', 'driverPhone', 'vehiclePlate', 'appointmentAccount', 'sellerName', 'remark']) {
|
||||
assert.match(createSource, new RegExp(`v-model="form\\.${model}"`))
|
||||
}
|
||||
assert.match(createSource, /if \(!pendingIdempotencyKey\) pendingIdempotencyKey = crypto\.randomUUID\(\)/)
|
||||
assert.match(createSource, /createDeliveryOrder\(requestPayload\(\)\)/)
|
||||
assert.match(createSource, /fees: form\.fees\.map/)
|
||||
assert.match(createSource, /submitError\.value = error instanceof Error/)
|
||||
assert.match(createSource, /useUnsavedWorkRegistration/)
|
||||
assert.doesNotMatch(createSource, /customerId|tenantId|mock|fallback/i)
|
||||
})
|
||||
|
||||
test('maps both merchant delivery permissions independently', async () => {
|
||||
assert.match(permissionSource, /'merchant:warehouse:delivery:list': \['merchant:warehouse:delivery:list'\]/)
|
||||
assert.match(permissionSource, /'merchant:warehouse:delivery:create': \['merchant:warehouse:delivery:create'\]/)
|
||||
const contracts = await importTypeScriptModule(permissionSource)
|
||||
assert.equal(contracts.hasMappedMerchantPermission('merchant:warehouse:delivery:list', new Set(['merchant:warehouse:delivery:list'])), true)
|
||||
assert.equal(contracts.hasMappedMerchantPermission('merchant:warehouse:delivery:list', new Set(['merchant:warehouse:delivery:create'])), false)
|
||||
assert.equal(contracts.hasMappedMerchantPermission('merchant:warehouse:delivery:create', new Set(['merchant:warehouse:delivery:create'])), true)
|
||||
assert.equal(contracts.hasMappedMerchantPermission('merchant:warehouse:delivery:create', new Set(['merchant:warehouse:delivery:list'])), false)
|
||||
})
|
||||
@@ -29,5 +29,20 @@ export const orderManagementModule = {
|
||||
meta: { title: '派送订单列表', description: '查询海外仓派送订单', permission: 'merchant:warehouse:delivery:list' },
|
||||
navigation: { key: 'orders-delivery', icon: Truck },
|
||||
},
|
||||
{
|
||||
path: 'orders/delivery/create',
|
||||
name: 'orders-delivery-create',
|
||||
component: () => import('./views/DeliveryOrderCreateView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '创建派送单', description: '从排车单创建海外仓派送单', permission: 'merchant:warehouse:delivery:create', keepAlive: false },
|
||||
},
|
||||
{
|
||||
path: 'orders/delivery/:deliveryNo',
|
||||
name: 'orders-delivery-detail',
|
||||
component: () => import('./views/DeliveryOrderDetailView.vue'),
|
||||
props: true,
|
||||
release: 'active',
|
||||
meta: { title: '派送单详情', description: '查看本企业海外仓派送单', permission: 'merchant:warehouse:delivery:list', keepAlive: false },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeft, Plus, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { useUnsavedWorkRegistration } from '@/navigation/workspace-tabs/unsavedWork'
|
||||
import { createDeliveryOrder, type DeliveryOrderFee, type DeliveryTransferPayload } from '../api/deliveryOrders'
|
||||
|
||||
type DeliveryCreateForm = Omit<DeliveryTransferPayload, 'idempotencyKey'>
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const submitError = ref('')
|
||||
const submitted = ref(false)
|
||||
let pendingIdempotencyKey = ''
|
||||
|
||||
const form = reactive<DeliveryCreateForm>({
|
||||
dispatchNo: '',
|
||||
plannedDeliveryAt: '',
|
||||
deliveryMethod: '',
|
||||
pickupMethod: '',
|
||||
loadingType: '',
|
||||
waybillNo: '',
|
||||
carrierName: '',
|
||||
vehicleType: '',
|
||||
vehicleSize: '',
|
||||
driverName: '',
|
||||
driverPhone: '',
|
||||
vehiclePlate: '',
|
||||
appointmentAccount: '',
|
||||
sellerName: '',
|
||||
remark: '',
|
||||
fees: [],
|
||||
})
|
||||
|
||||
const rules: FormRules<DeliveryCreateForm> = {
|
||||
dispatchNo: [{ required: true, message: '请输入排车单号', trigger: 'blur' }],
|
||||
plannedDeliveryAt: [{ required: true, message: '请选择计划派送时间', trigger: 'change' }],
|
||||
deliveryMethod: [{ required: true, message: '请选择派送方式', trigger: 'change' }],
|
||||
pickupMethod: [{ required: true, message: '请选择提货方式', trigger: 'change' }],
|
||||
loadingType: [{ required: true, message: '请选择装载类型', trigger: 'change' }],
|
||||
}
|
||||
|
||||
const dirty = computed(() => !submitted.value && Boolean(
|
||||
form.dispatchNo || form.plannedDeliveryAt || form.deliveryMethod || form.pickupMethod || form.loadingType
|
||||
|| form.waybillNo || form.carrierName || form.vehicleType || form.vehicleSize || form.driverName
|
||||
|| form.driverPhone || form.vehiclePlate || form.appointmentAccount || form.sellerName || form.remark || form.fees.length,
|
||||
))
|
||||
|
||||
useUnsavedWorkRegistration({
|
||||
key: 'delivery-order-create-form',
|
||||
routeKey: 'orders-delivery-create',
|
||||
label: '创建派送单',
|
||||
dirty,
|
||||
})
|
||||
|
||||
function optional(value: string | undefined) {
|
||||
const normalized = value?.trim()
|
||||
return normalized || undefined
|
||||
}
|
||||
|
||||
function addFee() {
|
||||
form.fees.push({ feeType: '', currency: 'USD', amount: 0, remark: '' })
|
||||
}
|
||||
|
||||
function removeFee(index: number) {
|
||||
form.fees.splice(index, 1)
|
||||
}
|
||||
|
||||
function validateFees() {
|
||||
const invalidIndex = form.fees.findIndex((fee) => !fee.feeType.trim() || !fee.currency.trim() || fee.amount <= 0)
|
||||
if (invalidIndex < 0) return true
|
||||
ElMessage.warning(`请完整填写第 ${invalidIndex + 1} 条费用的类型、币种和大于 0 的金额`)
|
||||
return false
|
||||
}
|
||||
|
||||
function requestPayload(): DeliveryTransferPayload {
|
||||
return {
|
||||
dispatchNo: form.dispatchNo.trim(),
|
||||
idempotencyKey: pendingIdempotencyKey,
|
||||
plannedDeliveryAt: form.plannedDeliveryAt,
|
||||
deliveryMethod: form.deliveryMethod,
|
||||
pickupMethod: form.pickupMethod,
|
||||
loadingType: form.loadingType,
|
||||
waybillNo: optional(form.waybillNo),
|
||||
carrierName: optional(form.carrierName),
|
||||
vehicleType: optional(form.vehicleType),
|
||||
vehicleSize: optional(form.vehicleSize),
|
||||
driverName: optional(form.driverName),
|
||||
driverPhone: optional(form.driverPhone),
|
||||
vehiclePlate: optional(form.vehiclePlate),
|
||||
appointmentAccount: optional(form.appointmentAccount),
|
||||
sellerName: optional(form.sellerName),
|
||||
remark: optional(form.remark),
|
||||
fees: form.fees.map((fee): DeliveryOrderFee => ({
|
||||
feeType: fee.feeType.trim(),
|
||||
currency: fee.currency.trim().toUpperCase(),
|
||||
amount: fee.amount,
|
||||
remark: optional(fee.remark ?? ''),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
submitError.value = ''
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid || !validateFees()) return
|
||||
if (!pendingIdempotencyKey) pendingIdempotencyKey = crypto.randomUUID()
|
||||
submitting.value = true
|
||||
try {
|
||||
const created = await createDeliveryOrder(requestPayload())
|
||||
submitted.value = true
|
||||
pendingIdempotencyKey = ''
|
||||
ElMessage.success('派送单创建成功')
|
||||
if (auth.hasPermission('merchant:warehouse:delivery:list')) {
|
||||
await router.replace({ name: 'orders-delivery-detail', params: { deliveryNo: created.deliveryNo } })
|
||||
} else {
|
||||
await router.replace('/')
|
||||
}
|
||||
} catch (error) {
|
||||
submitError.value = error instanceof Error ? error.message : '派送单创建失败,请检查后重试'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function cancel() {
|
||||
if (dirty.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm('当前派送单尚未提交,离开后填写内容将丢失。', '放弃创建?', { type: 'warning', confirmButtonText: '放弃', cancelButtonText: '继续填写' })
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
throw error
|
||||
}
|
||||
}
|
||||
submitted.value = true
|
||||
await router.push({ name: 'orders-delivery' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="delivery-page" aria-labelledby="delivery-create-title">
|
||||
<button type="button" class="delivery-back" @click="cancel"><ArrowLeft :size="16" />返回派送单列表</button>
|
||||
<div class="delivery-heading"><div><h2 id="delivery-create-title">创建派送单</h2><p>从已确认的排车单创建本企业海外仓派送任务。</p></div></div>
|
||||
|
||||
<div v-if="submitError" class="delivery-card delivery-state delivery-state--error" role="alert"><p>{{ submitError }}</p></div>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="delivery-card delivery-form" @submit.prevent="submit">
|
||||
<section>
|
||||
<h3>基础信息</h3>
|
||||
<div class="delivery-form-grid">
|
||||
<el-form-item label="排车单号" prop="dispatchNo" required><el-input v-model="form.dispatchNo" maxlength="64" placeholder="请输入排车单号" /></el-form-item>
|
||||
<el-form-item label="计划派送时间" prop="plannedDeliveryAt" required><el-date-picker v-model="form.plannedDeliveryAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择计划派送时间" /></el-form-item>
|
||||
<el-form-item label="派送方式" prop="deliveryMethod" required><el-select v-model="form.deliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="COURIER" /><el-option label="客户自提" value="SELF_PICKUP" /></el-select></el-form-item>
|
||||
<el-form-item label="提货方式" prop="pickupMethod" required><el-select v-model="form.pickupMethod" placeholder="请选择提货方式"><el-option label="仓库提货" value="WAREHOUSE_PICKUP" /><el-option label="上门提货" value="DOOR_PICKUP" /></el-select></el-form-item>
|
||||
<el-form-item label="装载类型" prop="loadingType" required><el-select v-model="form.loadingType" placeholder="请选择装载类型"><el-option label="托盘" value="PALLET" /><el-option label="散箱" value="LOOSE_CARTON" /><el-option label="混合装载" value="MIXED" /></el-select></el-form-item>
|
||||
<el-form-item label="运单号"><el-input v-model="form.waybillNo" maxlength="64" placeholder="选填" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>承运与车辆</h3>
|
||||
<div class="delivery-form-grid">
|
||||
<el-form-item label="承运商"><el-input v-model="form.carrierName" maxlength="128" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="车型"><el-input v-model="form.vehicleType" maxlength="64" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="车辆尺寸"><el-input v-model="form.vehicleSize" maxlength="64" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="车牌号"><el-input v-model="form.vehiclePlate" maxlength="32" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="司机姓名"><el-input v-model="form.driverName" maxlength="64" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="司机电话"><el-input v-model="form.driverPhone" maxlength="32" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="预约账号"><el-input v-model="form.appointmentAccount" maxlength="128" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="卖家名称"><el-input v-model="form.sellerName" maxlength="128" placeholder="选填" /></el-form-item>
|
||||
<el-form-item label="备注" class="delivery-span-3"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="选填" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="delivery-fee-heading"><h3>费用明细</h3><el-button @click="addFee"><Plus :size="16" />添加费用</el-button></div>
|
||||
<el-table v-if="form.fees.length" :data="form.fees" table-layout="fixed">
|
||||
<el-table-column label="操作" width="88"><template #default="{ $index }"><el-button link type="danger" aria-label="删除费用" @click="removeFee($index)"><Trash2 :size="16" />删除</el-button></template></el-table-column>
|
||||
<el-table-column label="费用类型" min-width="180"><template #default="{ row }"><el-input v-model="row.feeType" maxlength="64" placeholder="请输入费用类型" /></template></el-table-column>
|
||||
<el-table-column label="币种" width="140"><template #default="{ row }"><el-input v-model="row.currency" maxlength="3" placeholder="USD" /></template></el-table-column>
|
||||
<el-table-column label="金额" width="180"><template #default="{ row }"><el-input-number v-model="row.amount" :min="0" :precision="2" :step="1" controls-position="right" /></template></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" placeholder="选填" /></template></el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="delivery-fee-empty">暂无费用明细,可按实际情况添加。</div>
|
||||
</section>
|
||||
|
||||
<div class="delivery-form-actions"><el-button :disabled="submitting" @click="cancel">取消</el-button><el-button type="primary" native-type="submit" :loading="submitting">提交创建</el-button></div>
|
||||
</el-form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped src="./deliveryOrders.css"></style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { getDeliveryOrder, type DeliveryOrderDetail, type DeliveryOrderItem } from '../api/deliveryOrders'
|
||||
|
||||
const props = defineProps<{ deliveryNo: string }>()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detail = ref<DeliveryOrderDetail | null>(null)
|
||||
|
||||
const items = computed<DeliveryOrderItem[]>(() => {
|
||||
const value = detail.value?.items
|
||||
if (!value) return []
|
||||
return Array.isArray(value) ? value : value.records
|
||||
})
|
||||
|
||||
function display(value: string | number | null | undefined) {
|
||||
return value === null || value === undefined || value === '' ? '—' : String(value)
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null | undefined) {
|
||||
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—'
|
||||
}
|
||||
|
||||
function formatQuantity(value: number | null | undefined, unit: string) {
|
||||
return value === null || value === undefined ? '—' : `${value} ${unit}`
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
detail.value = await getDeliveryOrder(props.deliveryNo)
|
||||
} catch (error) {
|
||||
detail.value = null
|
||||
errorMessage.value = error instanceof Error ? error.message : '派送单详情加载失败,请重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function backToList() {
|
||||
void router.push({ name: 'orders-delivery' })
|
||||
}
|
||||
|
||||
onMounted(loadDetail)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="delivery-page" aria-labelledby="delivery-detail-title">
|
||||
<button type="button" class="delivery-back" @click="backToList"><ArrowLeft :size="16" />返回派送单列表</button>
|
||||
<div class="delivery-heading"><div><h2 id="delivery-detail-title">派送单详情</h2><p>{{ display(detail?.deliveryNo || deliveryNo) }}</p></div></div>
|
||||
|
||||
<div v-if="errorMessage" class="delivery-card delivery-state delivery-state--error" role="alert"><p>{{ errorMessage }}</p><el-button @click="loadDetail">重试</el-button></div>
|
||||
<div v-else v-loading="loading" class="delivery-detail-content">
|
||||
<template v-if="detail">
|
||||
<section class="delivery-card delivery-section">
|
||||
<h3>订单信息</h3>
|
||||
<div class="delivery-detail-grid">
|
||||
<div><span>派送单号</span><strong>{{ display(detail.deliveryNo) }}</strong></div>
|
||||
<div><span>排车单号</span><strong>{{ display(detail.dispatchNo) }}</strong></div>
|
||||
<div><span>出库单号</span><strong>{{ display(detail.outboundNo) }}</strong></div>
|
||||
<div><span>状态</span><strong>{{ display(detail.status) }}</strong></div>
|
||||
<div><span>海外仓</span><strong>{{ display(detail.overseasWarehouseName) }}</strong></div>
|
||||
<div><span>派送方式</span><strong>{{ display(detail.deliveryMethod) }}</strong></div>
|
||||
<div><span>提货方式</span><strong>{{ display(detail.pickupMethod) }}</strong></div>
|
||||
<div><span>装载类型</span><strong>{{ display(detail.loadingType) }}</strong></div>
|
||||
<div><span>托盘数</span><strong>{{ formatQuantity(detail.palletCount, '托') }}</strong></div>
|
||||
<div><span>箱数</span><strong>{{ formatQuantity(detail.packageCount, '箱') }}</strong></div>
|
||||
<div><span>总重量</span><strong>{{ formatQuantity(detail.totalWeightKg, 'kg') }}</strong></div>
|
||||
<div><span>总体积</span><strong>{{ formatQuantity(detail.totalVolumeCbm, 'm³') }}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="delivery-card delivery-section">
|
||||
<h3>运输与预约</h3>
|
||||
<div class="delivery-detail-grid">
|
||||
<div><span>运单号</span><strong>{{ display(detail.waybillNo) }}</strong></div>
|
||||
<div><span>承运商</span><strong>{{ display(detail.carrierName) }}</strong></div>
|
||||
<div><span>车型</span><strong>{{ display(detail.vehicleType) }}</strong></div>
|
||||
<div><span>车辆尺寸</span><strong>{{ display(detail.vehicleSize) }}</strong></div>
|
||||
<div><span>车牌号</span><strong>{{ display(detail.vehiclePlate) }}</strong></div>
|
||||
<div><span>司机姓名</span><strong>{{ display(detail.driverName) }}</strong></div>
|
||||
<div><span>司机电话</span><strong>{{ display(detail.driverPhone) }}</strong></div>
|
||||
<div><span>预约账号</span><strong>{{ display(detail.appointmentAccount) }}</strong></div>
|
||||
<div><span>卖家名称</span><strong>{{ display(detail.sellerName) }}</strong></div>
|
||||
<div><span>计划派送</span><strong>{{ formatDateTime(detail.plannedDeliveryAt) }}</strong></div>
|
||||
<div><span>预计到达</span><strong>{{ formatDateTime(detail.expectedArrivalAt) }}</strong></div>
|
||||
<div><span>签收时间</span><strong>{{ formatDateTime(detail.signedAt) }}</strong></div>
|
||||
<div><span>创建时间</span><strong>{{ formatDateTime(detail.createdAt) }}</strong></div>
|
||||
<div><span>更新时间</span><strong>{{ formatDateTime(detail.updatedAt) }}</strong></div>
|
||||
<div class="delivery-span-all"><span>备注</span><strong>{{ display(detail.remark) }}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="delivery-card delivery-section">
|
||||
<h3>货物明细</h3>
|
||||
<el-table :data="items" empty-text="暂无货物明细">
|
||||
<el-table-column prop="palletNo" label="托盘码" min-width="150"><template #default="{ row }">{{ display(row.palletNo) }}</template></el-table-column>
|
||||
<el-table-column prop="inboundCode" label="入仓码" min-width="150"><template #default="{ row }">{{ display(row.inboundCode) }}</template></el-table-column>
|
||||
<el-table-column prop="skuCode" label="SKU" min-width="140"><template #default="{ row }">{{ display(row.skuCode) }}</template></el-table-column>
|
||||
<el-table-column prop="productName" label="品名" min-width="180"><template #default="{ row }">{{ display(row.productName) }}</template></el-table-column>
|
||||
<el-table-column prop="packageCount" label="箱数" width="100"><template #default="{ row }">{{ display(row.packageCount) }}</template></el-table-column>
|
||||
<el-table-column prop="weightKg" label="重量(kg)" width="120"><template #default="{ row }">{{ display(row.weightKg) }}</template></el-table-column>
|
||||
<el-table-column prop="volumeCbm" label="体积(m³)" width="120"><template #default="{ row }">{{ display(row.volumeCbm) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="delivery-card delivery-section">
|
||||
<h3>费用明细</h3>
|
||||
<el-table :data="detail.fees || []" empty-text="暂无费用明细">
|
||||
<el-table-column prop="feeType" label="费用类型" min-width="180"><template #default="{ row }">{{ display(row.feeType) }}</template></el-table-column>
|
||||
<el-table-column prop="currency" label="币种" width="110"><template #default="{ row }">{{ display(row.currency) }}</template></el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="140"><template #default="{ row }">{{ display(row.amount) }}</template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="220"><template #default="{ row }">{{ display(row.remark) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped src="./deliveryOrders.css"></style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Plus, RefreshCw, Search } from '@lucide/vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { listDeliveryOrders, type DeliveryQuery, type DeliveryRow } from '../api/deliveryOrders'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const records = ref<DeliveryRow[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = reactive({ documentNo: '', dispatchNo: '', waybillNo: '', status: '', deliveryMethod: '', plannedRange: [] as string[] })
|
||||
const canCreate = computed(() => auth.hasPermission('merchant:warehouse:delivery:create'))
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'PENDING', label: '待派送' },
|
||||
{ value: 'IN_TRANSIT', label: '派送中' },
|
||||
{ value: 'DELIVERED', label: '已送达' },
|
||||
{ value: 'SIGNED', label: '已签收' },
|
||||
{ value: 'CANCELLED', label: '已取消' },
|
||||
]
|
||||
|
||||
const statusLabels: Record<string, string> = Object.fromEntries(statusOptions.map((item) => [item.value, item.label]))
|
||||
|
||||
function display(value: string | number | null | undefined) {
|
||||
return value === null || value === undefined || value === '' ? '—' : String(value)
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null | undefined) {
|
||||
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—'
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return statusLabels[status] ?? status
|
||||
}
|
||||
|
||||
function statusType(status: string) {
|
||||
if (status === 'DELIVERED' || status === 'SIGNED') return 'success'
|
||||
if (status === 'CANCELLED') return 'info'
|
||||
if (status === 'IN_TRANSIT') return 'warning'
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
function buildQuery(): DeliveryQuery {
|
||||
return {
|
||||
page: page.value,
|
||||
size: pageSize.value,
|
||||
documentNo: filters.documentNo.trim() || undefined,
|
||||
dispatchNo: filters.dispatchNo.trim() || undefined,
|
||||
waybillNo: filters.waybillNo.trim() || undefined,
|
||||
status: filters.status || undefined,
|
||||
deliveryMethod: filters.deliveryMethod || undefined,
|
||||
plannedFrom: filters.plannedRange[0] || undefined,
|
||||
plannedTo: filters.plannedRange[1] || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const result = await listDeliveryOrders(buildQuery())
|
||||
records.value = result.records
|
||||
total.value = result.total
|
||||
} catch (error) {
|
||||
records.value = []
|
||||
total.value = 0
|
||||
errorMessage.value = error instanceof Error ? error.message : '派送单加载失败,请重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
void loadOrders()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(filters, { documentNo: '', dispatchNo: '', waybillNo: '', status: '', deliveryMethod: '', plannedRange: [] })
|
||||
search()
|
||||
}
|
||||
|
||||
function changePage(nextPage: number) {
|
||||
page.value = nextPage
|
||||
void loadOrders()
|
||||
}
|
||||
|
||||
function changePageSize(nextSize: number) {
|
||||
pageSize.value = nextSize
|
||||
page.value = 1
|
||||
void loadOrders()
|
||||
}
|
||||
|
||||
function openDetail(row: DeliveryRow) {
|
||||
if (!row.deliveryNo) return
|
||||
void router.push({ name: 'orders-delivery-detail', params: { deliveryNo: row.deliveryNo } })
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
void router.push({ name: 'orders-delivery-create' })
|
||||
}
|
||||
|
||||
onMounted(loadOrders)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="delivery-page" aria-labelledby="delivery-list-title">
|
||||
<div class="delivery-heading">
|
||||
<div><h2 id="delivery-list-title">派送订单</h2><p>查看本企业海外仓派送进度与签收结果。</p></div>
|
||||
<el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />创建派送单</el-button>
|
||||
</div>
|
||||
|
||||
<form class="delivery-card delivery-filter" @submit.prevent="search">
|
||||
<div class="delivery-filter-grid">
|
||||
<el-input v-model="filters.documentNo" clearable placeholder="派送单号 / 出库单号" aria-label="派送单号或出库单号" />
|
||||
<el-input v-model="filters.dispatchNo" clearable placeholder="排车单号" aria-label="排车单号" />
|
||||
<el-input v-model="filters.waybillNo" clearable placeholder="运单号" aria-label="运单号" />
|
||||
<el-select v-model="filters.status" clearable placeholder="全部状态" aria-label="派送状态"><el-option v-for="option in statusOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select>
|
||||
<el-select v-model="filters.deliveryMethod" clearable placeholder="全部派送方式" aria-label="派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="COURIER" /><el-option label="客户自提" value="SELF_PICKUP" /></el-select>
|
||||
<el-date-picker v-model="filters.plannedRange" type="datetimerange" value-format="YYYY-MM-DDTHH:mm:ss" start-placeholder="计划开始时间" end-placeholder="计划结束时间" range-separator="至" aria-label="计划派送时间" />
|
||||
</div>
|
||||
<div class="delivery-filter-actions"><el-button type="primary" native-type="submit"><Search :size="16" />查询</el-button><el-button @click="reset"><RefreshCw :size="16" />重置</el-button></div>
|
||||
</form>
|
||||
|
||||
<div class="delivery-card delivery-table-card">
|
||||
<div v-if="errorMessage" class="delivery-state delivery-state--error" role="alert"><p>{{ errorMessage }}</p><el-button @click="loadOrders">重试</el-button></div>
|
||||
<el-table v-else v-loading="loading" :data="records" empty-text="暂无派送单" table-layout="fixed">
|
||||
<el-table-column label="操作" width="96" fixed="left"><template #default="{ row }"><el-button link type="primary" :disabled="!row.deliveryNo" @click="openDetail(row as DeliveryRow)">详情</el-button></template></el-table-column>
|
||||
<el-table-column prop="deliveryNo" label="派送单号" min-width="180"><template #default="{ row }">{{ display(row.deliveryNo) }}</template></el-table-column>
|
||||
<el-table-column prop="dispatchNo" label="排车单号" min-width="170"><template #default="{ row }">{{ display(row.dispatchNo) }}</template></el-table-column>
|
||||
<el-table-column prop="outboundNo" label="出库单号" min-width="170"><template #default="{ row }">{{ display(row.outboundNo) }}</template></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="110"><template #default="{ row }"><el-tag :type="statusType(row.status)" effect="light">{{ statusLabel(row.status) }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="overseasWarehouseName" label="海外仓" min-width="150"><template #default="{ row }">{{ display(row.overseasWarehouseName) }}</template></el-table-column>
|
||||
<el-table-column prop="deliveryMethod" label="派送方式" width="120"><template #default="{ row }">{{ display(row.deliveryMethod) }}</template></el-table-column>
|
||||
<el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }">{{ display(row.waybillNo) }}</template></el-table-column>
|
||||
<el-table-column prop="carrierName" label="承运商" min-width="140"><template #default="{ row }">{{ display(row.carrierName) }}</template></el-table-column>
|
||||
<el-table-column prop="plannedDeliveryAt" label="计划派送" min-width="160"><template #default="{ row }">{{ formatDateTime(row.plannedDeliveryAt) }}</template></el-table-column>
|
||||
<el-table-column prop="expectedArrivalAt" label="预计到达" min-width="160"><template #default="{ row }">{{ formatDateTime(row.expectedArrivalAt) }}</template></el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" min-width="160"><template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<div v-if="!errorMessage && (records.length > 0 || total > 0)" class="delivery-pagination"><span>共 {{ total }} 条</span><el-pagination :current-page="page" :page-size="pageSize" :page-sizes="[10, 20, 50, 100]" layout="sizes, prev, pager, next" :total="total" @update:current-page="changePage" @update:page-size="changePageSize" /></div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped src="./deliveryOrders.css"></style>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.delivery-page{color:#25324a}.delivery-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:16px}.delivery-heading h2{margin:0;color:#17233a;font-size:22px}.delivery-heading p{margin:6px 0 0;color:#7a8596;font-size:13px}.delivery-card{margin-bottom:14px;border:1px solid #e5eaf1;border-radius:6px;background:#fff;box-shadow:0 1px 3px rgba(25,42,70,.04)}.delivery-filter{padding:18px 20px}.delivery-filter-grid{display:grid;grid-template-columns:repeat(3,minmax(210px,1fr));gap:14px}.delivery-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.delivery-table-card{overflow:hidden}.delivery-pagination{display:flex;align-items:center;justify-content:flex-end;gap:16px;padding:14px 18px;color:#778398;font-size:12px}.delivery-state{display:flex;min-height:220px;align-items:center;justify-content:center;gap:12px;flex-direction:column;color:#7a8596}.delivery-state p{margin:0}.delivery-state--error{margin:20px;border:1px solid #f5c2bd;border-radius:6px;background:#fff8f7;color:#b42318}.delivery-section{padding:20px}.delivery-section h3{margin:0 0 18px;padding-bottom:12px;border-bottom:1px solid #edf0f4;color:#26364f;font-size:15px}.delivery-detail-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:20px 28px}.delivery-detail-grid div{display:flex;min-width:0;flex-direction:column;gap:7px}.delivery-detail-grid span{color:#8792a2;font-size:12px}.delivery-detail-grid strong{overflow-wrap:anywhere;color:#334157;font-size:13px;font-weight:550}.delivery-span-all{grid-column:1/-1}.delivery-back{display:inline-flex;align-items:center;gap:6px;margin-bottom:14px;padding:0;border:0;background:none;color:#2176d2;cursor:pointer}.delivery-form{padding:22px}.delivery-form-grid{display:grid;grid-template-columns:repeat(3,minmax(210px,1fr));gap:4px 24px}.delivery-form-grid .delivery-span-2{grid-column:span 2}.delivery-form-grid .delivery-span-3{grid-column:1/-1}.delivery-form-actions{display:flex;justify-content:center;gap:10px;padding-top:8px}.delivery-fee-heading{display:flex;align-items:center;justify-content:space-between;margin:10px 0 12px}.delivery-fee-heading h3{margin:0;font-size:15px}.delivery-fee-empty{padding:20px;color:#8792a2;text-align:center}:deep(.el-button span){display:inline-flex;align-items:center;gap:6px}:deep(.el-date-editor){width:100%}@media(max-width:1200px){.delivery-filter-grid,.delivery-form-grid{grid-template-columns:repeat(2,minmax(210px,1fr))}.delivery-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.delivery-form-grid .delivery-span-3{grid-column:1/-1}}
|
||||
@@ -99,6 +99,8 @@ export const FRONTEND_TO_MERCHANT_PERMISSION_CODES: Readonly<Record<string, read
|
||||
'order:first-leg:edit': ['merchant:order:edit'],
|
||||
'order:first-leg:submit': ['merchant:order:submit'],
|
||||
'order:first-leg:cancel': ['merchant:order:cancel'],
|
||||
'merchant:warehouse:delivery:list': ['merchant:warehouse:delivery:list'],
|
||||
'merchant:warehouse:delivery:create': ['merchant:warehouse:delivery:create'],
|
||||
'company:profile:view': ['merchant:profile:view'],
|
||||
'company:profile:edit': ['merchant:profile:edit'],
|
||||
'company:qualification:upload': ['merchant:qualification:upload'],
|
||||
|
||||
@@ -57,6 +57,10 @@ test('keeps only implemented navigation groups visible', () => {
|
||||
title: '海外仓',
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标'],
|
||||
},
|
||||
{
|
||||
title: '订单管理',
|
||||
children: ['派送订单列表'],
|
||||
},
|
||||
{
|
||||
title: '财务结算',
|
||||
children: ['客户账单'],
|
||||
@@ -95,6 +99,7 @@ test('flattens visible leaves with unique paths', () => {
|
||||
'/overseas-warehouse/storage-transfer',
|
||||
'/overseas-warehouse/cargo-disposal',
|
||||
'/overseas-warehouse/return-relabel',
|
||||
'/orders/delivery',
|
||||
'/finance/bills',
|
||||
'/company/profile',
|
||||
'/organization/members',
|
||||
@@ -335,7 +340,7 @@ test('keeps prototype views cataloged while allowing implemented order and overs
|
||||
]
|
||||
|
||||
for (const [path, view] of routes) {
|
||||
if (path === 'first-leg/quick-order' || path.startsWith('overseas-warehouse/')) {
|
||||
if (path === 'first-leg/quick-order' || path.startsWith('overseas-warehouse/') || path === 'orders/delivery') {
|
||||
assert.match(routerSource, new RegExp(`path: '${path}'[\\s\\S]*?\\./views/${view.replace('.', '\\.')}[\\s\\S]*?release: 'active'[\\s\\S]*?permission:`))
|
||||
assert.equal(getNavigationLeaves(clientNavigation).some((item) => item.path === `/${path}`), true)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user