feat(fleet): add internal sales capacity pool workflow
This commit is contained in:
@@ -1,5 +1,18 @@
|
||||
# 海外业务原型核对矩阵
|
||||
|
||||
## 池来源本土订单承运安排
|
||||
|
||||
依据:[内部拼车需求池首版开发契约](../../../../../docs/海外仓业务/内部拼车需求池首版开发契约.md) 第7节及本轮用户授权。
|
||||
|
||||
| 页面 | 操作 | 确认承载与实现 |
|
||||
| --- | --- | --- |
|
||||
| 本土卡车 / 本土散板列表 | 池来源承运安排 | 1080px居中只读摘要,使用列表已有fleetArrangement,打开不增加请求;不显示旧编辑动作 |
|
||||
| 本土卡车 / 本土散板旧编辑深链 | 池来源只读 | 保持独立路由,读取原订单详情后仅展示真实安排与只读原因,不提交旧手工流程 |
|
||||
| 承运安排 | 关联单据 | 有目标权限才开放车次/排车跳转,否则仅显示业务号;未知签收数量显示“—” |
|
||||
| Client本土订单 | 列表摘要 / 原独立详情 | 同一客户订单响应展示承运安排,客户不访问运力池,不提供状态写入 |
|
||||
|
||||
独立来源订单保持原承载。本轮视觉按1920×1080内置浏览器验收,自动检查与实际页面证据分别报告。
|
||||
|
||||
原型来源:`docs/悦/货代SAAS平台后台/prototype-src/modules/07-overseas-business.html`;按钮函数来源:同原型目录 `prototype-src/app.js`。
|
||||
|
||||
| 页面 | 操作 | 原型证据 | 原型承载方式 | 冻结结论 |
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { poolOrderStatusLabels, type FleetArrangement } from '../fleetArrangement'
|
||||
defineProps<{ arrangement: FleetArrangement }>()
|
||||
const access = useAccessPreviewStore(), router = useRouter()
|
||||
const time = (value: string | null) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
|
||||
</script>
|
||||
<template>
|
||||
<section aria-label="承运安排">
|
||||
<p>{{ arrangement.readOnlyReason }}</p>
|
||||
<dl class="density-fields">
|
||||
<div><dt>订单号</dt><dd>{{ arrangement.businessOrderNo }}</dd></div>
|
||||
<div><dt>外接预订号</dt><dd>{{ arrangement.bookingNo }}</dd></div>
|
||||
<div><dt>车次号</dt><dd><el-button v-if="access.can('platform:fleet:trip:list')" link type="primary" @click="router.push({ name: 'overseas-master-data-trip-detail', params: { tripNo: arrangement.tripNo } })">{{ arrangement.tripNo }}</el-button><span v-else>{{ arrangement.tripNo }}</span></dd></div>
|
||||
<div><dt>计划发车</dt><dd>{{ time(arrangement.plannedDepartureAt) }}</dd></div>
|
||||
<div><dt>起运仓</dt><dd>{{ arrangement.originWarehouseName || '—' }}</dd></div>
|
||||
<div><dt>目的地</dt><dd>{{ arrangement.destinationLabel || '—' }}</dd></div>
|
||||
<div><dt>交货方式</dt><dd>{{ arrangement.deliveryMode === 'SELF_DELIVERY' ? '客户自送我仓' : '线下提货回我仓' }}</dd></div>
|
||||
<div><dt>履约阶段</dt><dd>{{ poolOrderStatusLabels[arrangement.fulfillmentStage] || '待核对履约进度' }}</dd></div>
|
||||
<div><dt>阶段时间</dt><dd>{{ time(arrangement.stageOccurredAt) }}</dd></div>
|
||||
<div><dt>约定运费(非实收)</dt><dd>{{ arrangement.agreedFreightAmount == null ? '—' : `${Number(arrangement.agreedFreightAmount).toFixed(2)} ${arrangement.currency || ''}` }}</dd></div>
|
||||
<div><dt>来货单号</dt><dd>{{ arrangement.incomingNo || '—' }}</dd></div>
|
||||
<div><dt>排车单号</dt><dd><el-button v-if="arrangement.dispatchNo && access.can('platform:overseas-warehouse:dispatch:list')" link type="primary" @click="router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: arrangement.dispatchNo } })">{{ arrangement.dispatchNo }}</el-button><span v-else>{{ arrangement.dispatchNo || '—' }}</span></dd></div>
|
||||
<div><dt>派送单号</dt><dd>{{ arrangement.deliveryNo || '—' }}</dd></div>
|
||||
<div><dt>已签 / 未签 / 退回箱数</dt><dd>{{ arrangement.signedPackages ?? '—' }} / {{ arrangement.unsignedPackages ?? '—' }} / {{ arrangement.returnedPackages ?? '—' }}</dd></div>
|
||||
</dl>
|
||||
<p v-if="arrangement.deliveryInstructions">交货说明:{{ arrangement.deliveryInstructions }}</p>
|
||||
<p v-if="arrangement.exceptionSummary" role="status">履约说明:{{ arrangement.exceptionSummary }}</p>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
export interface FleetArrangement {
|
||||
sourceType: 'FLEET_CAPACITY_OFFER'
|
||||
businessType: 'LOCAL_LOOSEBOARD' | 'US_TRUCK'
|
||||
businessOrderNo: string
|
||||
bookingNo: string
|
||||
tripNo: string
|
||||
plannedDepartureAt: string | null
|
||||
originWarehouseName: string
|
||||
destinationLabel: string
|
||||
deliveryMode: 'SELF_DELIVERY' | 'MANUAL_PICKUP_TO_WAREHOUSE'
|
||||
deliveryInstructions: string | null
|
||||
fulfillmentStage: string
|
||||
stageOccurredAt: string | null
|
||||
agreedFreightAmount: number | null
|
||||
currency: string | null
|
||||
incomingNo: string | null
|
||||
dispatchNo: string | null
|
||||
deliveryNo: string | null
|
||||
signedPackages: number | null
|
||||
unsignedPackages: number | null
|
||||
returnedPackages: number | null
|
||||
exceptionSummary: string | null
|
||||
readOnlyReason: string
|
||||
}
|
||||
|
||||
export const poolOrderStatusLabels: Record<string, string> = {
|
||||
WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托',
|
||||
PENDING_DISPATCH: '待派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消',
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { listPlatformTenants } from '@/modules/iam'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import type { FleetArrangement } from '../fleetArrangement'
|
||||
import type { LocalLooseboardDetail, LocalLooseboardOrder, LocalTransportDetail, LocalTransportOrder, OverseasPurchaseDetail } from '../types'
|
||||
import {
|
||||
createOverseasPurchaseFee, uploadOverseasPurchaseFile, createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getOverseasPurchase, getUsTruckOrder,
|
||||
@@ -44,10 +45,11 @@ const purchase: OverseasPurchaseDetail = {
|
||||
overseasCarrier: 'UPS', overseasTrackingNo: 'UPS-1', estimatedArrivalAt: '2026-08-25T12:00:00', remark: '采购备注', createdBy: '7', reviewedBy: '8', reviewRemark: null, reviewedAt: '2026-08-02T09:00:00',
|
||||
histories: [{ operationType: 'UPDATE', fromStatus: 'APPROVED', toStatus: 'APPROVED', operatorId: 'INTERNAL-OPERATOR-7', changeDetail: '{"remark":"采购备注"}', remark: null, createdAt: '2026-08-02T10:00:00' }], files: [], fees: [],
|
||||
}
|
||||
const truck: LocalTransportOrder = { id: '9007199254740995', customerId: '10002', orderNo: 'TK-200', transportType: 'PORT_PICKUP', origin: 'Long Beach', destination: 'Ontario', containerType: '40HQ', palletCount: 2, packageCount: 20, receivedPalletCount: 0, allocatedPalletCount: 0, shippedPalletCount: 0, totalWeightKg: '1200.000', totalVolumeCbm: '16.500', budgetAmount: '850.00', currency: 'USD', contactName: 'Bob', contactInfo: '10010', requestedDate: '2026-08-28', status: 'PENDING_REVIEW', version: 1, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-02T10:00:00' }
|
||||
const truck: LocalTransportOrder = { fleetArrangement: null, id: '9007199254740995', customerId: '10002', orderNo: 'TK-200', transportType: 'PORT_PICKUP', origin: 'Long Beach', destination: 'Ontario', containerType: '40HQ', palletCount: 2, packageCount: 20, receivedPalletCount: 0, allocatedPalletCount: 0, shippedPalletCount: 0, totalWeightKg: '1200.000', totalVolumeCbm: '16.500', budgetAmount: '850.00', currency: 'USD', contactName: 'Bob', contactInfo: '10010', requestedDate: '2026-08-28', status: 'PENDING_REVIEW', version: 1, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-02T10:00:00' }
|
||||
const truckDetail: LocalTransportDetail = { ...truck, originAddress: 'Pier A', destinationAddress: 'Warehouse B', remark: null, createdBy: '7', pallets: [], histories: [] }
|
||||
const looseboard: LocalLooseboardOrder = { id: '9007199254740997', customerId: '10003', orderNo: 'LB-300', transportMode: 'SHARED_TRUCK', palletRequired: 'YES', transportType: 'WAREHOUSE_TO_WAREHOUSE', origin: 'LA', destination: 'ONT', containerType: '40HQ', packageCount: 8, totalWeightKg: '80.5', totalVolumeCbm: '3.2', budgetAmount: '50.00', currency: 'USD', contactName: null, contactInfo: null, requestedDate: null, status: 'PENDING_ACCEPTANCE', version: 0, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-01T10:00:00' }
|
||||
const looseboard: LocalLooseboardOrder = { fleetArrangement: null, id: '9007199254740997', customerId: '10003', orderNo: 'LB-300', transportMode: 'SHARED_TRUCK', palletRequired: 'YES', transportType: 'WAREHOUSE_TO_WAREHOUSE', origin: 'LA', destination: 'ONT', containerType: '40HQ', packageCount: 8, totalWeightKg: '80.5', totalVolumeCbm: '3.2', budgetAmount: '50.00', currency: 'USD', contactName: null, contactInfo: null, requestedDate: null, status: 'PENDING_ACCEPTANCE', version: 0, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-01T10:00:00' }
|
||||
const looseboardDetail: LocalLooseboardDetail = { ...looseboard, originAddress: null, destinationAddress: null, remark: null, createdBy: '7', histories: [] }
|
||||
const arrangement: FleetArrangement = { sourceType: 'FLEET_CAPACITY_OFFER', businessType: 'US_TRUCK', businessOrderNo: 'TR-200', bookingNo: 'FB-TEST', tripNo: 'TRIP-TEST', plannedDepartureAt: null, originWarehouseName: '我仓', destinationLabel: 'ONT8', deliveryMode: 'SELF_DELIVERY', deliveryInstructions: null, fulfillmentStage: 'WAITING_INBOUND', stageOccurredAt: null, agreedFreightAmount: null, currency: 'USD', incomingNo: null, dispatchNo: null, deliveryNo: null, signedPackages: null, unsignedPackages: null, returnedPackages: null, exceptionSummary: null, readOnlyReason: '池来源订单按实际运输履约推进' }
|
||||
|
||||
function activatePermissions() { const pinia = createPinia(); setActivePinia(pinia); const auth = useAuthStore(); auth.token = 'test-token'; auth.permissions = ['platform:overseas-business:purchase:list', 'platform:overseas-business:purchase:edit', 'platform:overseas-business:purchase:review', 'platform:overseas-business:truck:list', 'platform:overseas-business:truck:create', 'platform:overseas-business:truck:edit', 'platform:overseas-business:looseboard:list', 'platform:overseas-business:looseboard:create', 'platform:overseas-business:looseboard:edit']; useAccessPreviewStore().initializeAfterLogin(); return pinia }
|
||||
function mountPage(component: Parameters<typeof mount>[0], props?: Record<string, unknown>) { const pinia = activatePermissions(); return mount(component, { attachTo: document.body, props, global: { plugins: [pinia, ElementPlus] } }) }
|
||||
@@ -65,6 +67,23 @@ describe('overseas business mounted pages', () => {
|
||||
})
|
||||
|
||||
|
||||
it('opens pool arrangements from the list without another request or exposing edit actions', async () => {
|
||||
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [{ ...truck, fleetArrangement: arrangement, status: 'WAITING_INBOUND' }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
||||
expect(wrapper.findAll('.el-table__body-wrapper button').some(b => b.text() === '编辑')).toBe(false)
|
||||
await wrapper.findAll('.el-table__body-wrapper button').find(b => b.text() === '承运安排')!.trigger('click'); await flushPromises()
|
||||
expect(document.body.textContent).toContain('池来源订单按实际运输履约推进')
|
||||
expect(document.body.textContent).toContain('FB-TEST'); expect(document.body.textContent).toContain('— / — / —')
|
||||
expect(listUsTruckOrders).toHaveBeenCalledOnce(); expect(getUsTruckOrder).not.toHaveBeenCalled()
|
||||
})
|
||||
it('keeps a pool order read-only even when an old edit deep link is used', async () => {
|
||||
routing.route.params = { orderId: 'TR-200' }; routing.route.query = { customerId: truck.customerId }
|
||||
vi.mocked(getUsTruckOrder).mockResolvedValue({ ...truckDetail, fleetArrangement: arrangement, status: 'WAITING_INBOUND' })
|
||||
const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises()
|
||||
expect(wrapper.text()).toContain('池来源订单按实际运输履约推进')
|
||||
expect(wrapper.find('form').exists()).toBe(false); expect(wrapper.findAll('button').some(b => b.text() === '保存修改')).toBe(false)
|
||||
expect(updateUsTruckOrder).not.toHaveBeenCalled(); expect(getUsTruckOrder).toHaveBeenCalledOnce()
|
||||
})
|
||||
it('keeps all procurement facts in dense groups and business references in the heading', async () => {
|
||||
routing.route.params = { purchaseId: 'internal-purchase-route' }; routing.route.query = { customerId: '10001' }
|
||||
const wrapper = mountPage(PurchaseOrderDetailView)
|
||||
|
||||
@@ -17,6 +17,8 @@ import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE } from '@/shared/domai
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getUsTruckOrder, updateLocalLooseboardOrder, updateUsTruckOrder } from '../api/platformOverseasBusiness'
|
||||
import { overseasBusinessPermissions } from '../permissions'
|
||||
import FleetArrangementSummary from '../components/FleetArrangementSummary.vue'
|
||||
import type { FleetArrangement } from '../fleetArrangement'
|
||||
import type { CreateLocalLooseboardOrderRequest, CreateLocalTransportOrderRequest, LocalLooseboardStatus, LocalTransportStatus, LocalTransportType, LooseboardTransportMode, PalletRequired, UpdateLocalLooseboardOrderRequest, UpdateLocalTransportOrderRequest } from '../types'
|
||||
|
||||
const props = defineProps<{ domain: 'truck' | 'looseboard' }>()
|
||||
@@ -37,6 +39,7 @@ interface TransportFormModel {
|
||||
function newIdempotencyKey() { return globalThis.crypto?.randomUUID?.() || `admin-${Date.now()}-${Math.random().toString(36).slice(2)}` }
|
||||
const initial = (): TransportFormModel => ({ customerId: '', orderNo: '', transportMode: '', palletRequired: '', transportType: '', origin: '', destination: '', originAddress: null, destinationAddress: null, contactName: null, contactInfo: null, containerType: DEFAULT_CONTAINER_TYPE, palletCount: 1, packageCount: 0, totalWeightKg: null, totalVolumeCbm: null, budgetAmount: null, currency: 'USD', requestedDate: null, remark: null, status: isLooseboard.value ? 'PENDING_ACCEPTANCE' : 'PENDING_REVIEW', version: 0, idempotencyKey: newIdempotencyKey() })
|
||||
const form = reactive<TransportFormModel>(initial()); const loading = ref(false); const saving = ref(false)
|
||||
const fleetArrangement = ref<FleetArrangement | null>(null)
|
||||
const loadError = ref<ApiErrorDetails | null>(null); const submitError = ref<ApiErrorDetails | null>(null)
|
||||
const customers = ref<PlatformTenantResource[]>([]); const customersLoading = ref(false); const customersError = ref('')
|
||||
const requiredDecimal = (label: string) => ({ validator: (_rule: unknown, value: string | null, callback: (error?: Error) => void) => value !== null && value !== '' && Number.isFinite(Number(value)) && Number(value) >= 0 ? callback() : callback(new Error(`请输入有效的${label}`)), trigger: 'blur' })
|
||||
@@ -54,9 +57,9 @@ const rules: FormRules<TransportFormModel> = {
|
||||
const transportTypes = [{ value: 'PORT_PICKUP', label: '⚓ 港口提柜' }, { value: 'WAREHOUSE_TO_WAREHOUSE', label: '🏭 仓到仓' }, { value: 'WAREHOUSE_TO_DOOR', label: '🚛 仓到门' }]
|
||||
const transportModes = [{ value: 'SHARED_TRUCK', label: '🚐 拼车' }, { value: 'PRIORITY_CONSOLIDATION', label: '📦 优先拼载' }]
|
||||
const truckStatusLabels: Record<LocalTransportStatus, string> = { PENDING_REVIEW: '待审核', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PALLET_OVERAGE_REVIEW: '超托待审', PARTIALLY_ALLOCATED: '部分分配', ALLOCATED: '已分配', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', REJECTED: '已拒绝', CANCELLED: '已取消' }
|
||||
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' }
|
||||
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' }
|
||||
const statusLabel = computed(() => isLooseboard.value ? looseboardStatusLabels[form.status as LocalLooseboardStatus] : truckStatusLabels[form.status as LocalTransportStatus])
|
||||
const editableStatus = computed(() => !editing.value || (isLooseboard.value ? form.status === 'PENDING_ACCEPTANCE' : form.status === 'PENDING_REVIEW'))
|
||||
const editableStatus = computed(() => !fleetArrangement.value && (!editing.value || (isLooseboard.value ? form.status === 'PENDING_ACCEPTANCE' : form.status === 'PENDING_REVIEW')))
|
||||
const { captureBaseline } = useUnsavedWorkRegistration({ key: `overseas-${props.domain}-editor`, label: computed(() => `${editing.value ? '编辑' : '新增'}${domainLabel.value}订单`), isOpen: computed(() => true), model: form, requiredPermission })
|
||||
function optional(value: string | null) { return value?.trim() || null }
|
||||
function amount(value: string | null) { return value === null || value === '' ? null : Number(value) }
|
||||
@@ -68,16 +71,18 @@ async function loadCustomers() {
|
||||
finally { customersLoading.value = false }
|
||||
}
|
||||
async function load() {
|
||||
Object.assign(form, initial()); loadError.value = null; submitError.value = null
|
||||
Object.assign(form, initial()); loadError.value = null; submitError.value = null; fleetArrangement.value = null
|
||||
if (!editing.value) { captureBaseline(); return }
|
||||
if (!routeCustomerId.value) { loadError.value = { message: `缺少客户标识,请从${domainLabel.value}列表重新进入` }; captureBaseline(); return }
|
||||
loading.value = true
|
||||
try {
|
||||
if (isLooseboard.value) {
|
||||
const detail = await getLocalLooseboardOrder(orderId.value, routeCustomerId.value)
|
||||
fleetArrangement.value = detail.fleetArrangement
|
||||
Object.assign(form, { customerId: detail.customerId, orderNo: detail.orderNo, transportMode: detail.transportMode, palletRequired: detail.palletRequired, transportType: detail.transportType, origin: detail.origin, destination: detail.destination, originAddress: detail.originAddress, destinationAddress: detail.destinationAddress, contactName: detail.contactName, contactInfo: detail.contactInfo, containerType: detail.containerType, packageCount: detail.packageCount, totalWeightKg: detail.totalWeightKg, totalVolumeCbm: detail.totalVolumeCbm, budgetAmount: detail.budgetAmount, currency: detail.currency, requestedDate: detail.requestedDate, remark: detail.remark, status: detail.status, version: detail.version })
|
||||
} else {
|
||||
const detail = await getUsTruckOrder(orderId.value, routeCustomerId.value)
|
||||
fleetArrangement.value = detail.fleetArrangement
|
||||
Object.assign(form, { customerId: detail.customerId, orderNo: detail.orderNo, transportType: detail.transportType || '', origin: detail.origin, destination: detail.destination, originAddress: detail.originAddress, destinationAddress: detail.destinationAddress, contactName: detail.contactName, contactInfo: detail.contactInfo, containerType: detail.containerType, palletCount: detail.palletCount, packageCount: detail.packageCount, totalWeightKg: detail.totalWeightKg, totalVolumeCbm: detail.totalVolumeCbm, budgetAmount: detail.budgetAmount, currency: detail.currency, requestedDate: detail.requestedDate, remark: detail.remark, status: detail.status, version: detail.version })
|
||||
}
|
||||
captureBaseline()
|
||||
@@ -110,6 +115,7 @@ watch([orderId, routeCustomerId, isLooseboard], () => { void load(); void loadCu
|
||||
<AdminPage class="admin-density overseas-business-editor"><OperationErrorToast :details="submitError" /><DetailPageLayout class="overseas-business-form-layout" :aria-label="`${editing ? '编辑' : '新增'}${domainLabel}订单`">
|
||||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="back"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ editing ? '编辑' : '新增' }}{{ domainLabel }}订单</h1><span v-if="editing" class="form-reference">{{ form.orderNo || '—' }}</span></div></div></template>
|
||||
<LoadErrorState v-if="loadError" :title="`${domainLabel}订单暂时无法加载`" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<FleetArrangementSummary v-else-if="fleetArrangement" :arrangement="fleetArrangement" />
|
||||
<section v-else v-loading="loading" class="form-panel"><el-form class="density-form" ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="saving || !editableStatus">
|
||||
<FormSection class="density-form-section" v-if="!editing" title="客户信息"><el-form-item label="客户"><el-select v-model="form.customerId" filterable :loading="customersLoading" :disabled="Boolean(customersError)" placeholder="请选择客户" style="width:100%"><el-option v-for="item in customers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select><small v-if="customersError" class="field-error">{{ customersError }}</small></el-form-item></FormSection>
|
||||
<el-alert v-if="editing && !editableStatus" type="warning" :closable="false" show-icon title="当前状态不可普通编辑,请返回列表查看" />
|
||||
|
||||
@@ -9,6 +9,9 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import FleetArrangementSummary from '../components/FleetArrangementSummary.vue'
|
||||
import { poolOrderStatusLabels, type FleetArrangement } from '../fleetArrangement'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listLocalLooseboardOrders, listUsTruckOrders } from '../api/platformOverseasBusiness'
|
||||
@@ -23,9 +26,11 @@ const createPermission = computed(() => isLooseboard.value ? overseasBusinessPer
|
||||
const editPermission = computed(() => isLooseboard.value ? overseasBusinessPermissions.looseboardEdit : overseasBusinessPermissions.truckEdit)
|
||||
const rows = ref<Array<LocalTransportOrder | LocalLooseboardOrder>>([]); const loading = ref(false); const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const page = ref(1); const pageSize = ref(20); const total = ref(0)
|
||||
const arrangementOpen = ref(false), arrangement = ref<FleetArrangement | null>(null)
|
||||
function showArrangement(row: LocalTransportOrder | LocalLooseboardOrder) { arrangement.value = row.fleetArrangement; arrangementOpen.value = true }
|
||||
const filters = ref<QueryModel>({ status: '', routeKeyword: '' }); const applied = ref({ ...filters.value })
|
||||
const truckStatusLabels: Record<LocalTransportStatus, string> = { PENDING_REVIEW: '待审核', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PALLET_OVERAGE_REVIEW: '超托待审', PARTIALLY_ALLOCATED: '部分分配', ALLOCATED: '已分配', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', REJECTED: '已拒绝', CANCELLED: '已取消' }
|
||||
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' }
|
||||
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels }
|
||||
const transportTypeLabels: Record<LocalTransportType, string> = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' }
|
||||
const transportModeLabels: Record<LooseboardTransportMode, string> = { SHARED_TRUCK: '拼车', PRIORITY_CONSOLIDATION: '优先拼载' }
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
@@ -69,7 +74,7 @@ async function load() {
|
||||
}
|
||||
function search(model: QueryModel) { applied.value = { ...model }; if (page.value === 1) void load(); else page.value = 1 }
|
||||
function openCreate() { void router.push({ name: isLooseboard.value ? 'overseas-business-looseboard-new' : 'overseas-business-truck-new' }) }
|
||||
function canEdit(row: LocalTransportOrder | LocalLooseboardOrder) { return isLooseboard.value ? row.status === 'PENDING_ACCEPTANCE' : row.status === 'PENDING_REVIEW' }
|
||||
function canEdit(row: LocalTransportOrder | LocalLooseboardOrder) { return !row.fleetArrangement && (isLooseboard.value ? row.status === 'PENDING_ACCEPTANCE' : row.status === 'PENDING_REVIEW') }
|
||||
function openEdit(row: LocalTransportOrder | LocalLooseboardOrder) { if (!canEdit(row)) return; void router.push({ name: isLooseboard.value ? 'overseas-business-looseboard-edit' : 'overseas-business-truck-edit', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) }
|
||||
watch([page, pageSize], () => { void load() }); watch(isLooseboard, () => { page.value = 1; applied.value = { status: '', routeKeyword: '' }; filters.value = { ...applied.value }; void load() }); onMounted(() => { void load() })
|
||||
</script>
|
||||
@@ -85,10 +90,10 @@ watch([page, pageSize], () => { void load() }); watch(isLooseboard, () => { page
|
||||
|
||||
|
||||
<template #toolbar-actions><el-button v-if="access.can(createPermission)" type="primary" @click="openCreate"><Plus :size="16" />新增订单</el-button></template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-tooltip v-if="access.can(editPermission)" :disabled="canEdit(row as LocalTransportOrder | LocalLooseboardOrder)" content="当前状态不可编辑"><span><el-button class="density-action" data-tone="neutral" size="small" :disabled="!canEdit(row as LocalTransportOrder | LocalLooseboardOrder)" @click="openEdit(row as LocalTransportOrder | LocalLooseboardOrder)">编辑</el-button></span></el-tooltip></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button v-if="row.fleetArrangement" class="density-action" @click="showArrangement(row as LocalTransportOrder | LocalLooseboardOrder)">承运安排</el-button><el-tooltip v-if="!row.fleetArrangement && access.can(editPermission)" :disabled="canEdit(row as LocalTransportOrder | LocalLooseboardOrder)" content="当前状态不可编辑"><span><el-button class="density-action" data-tone="neutral" size="small" :disabled="!canEdit(row as LocalTransportOrder | LocalLooseboardOrder)" @click="openEdit(row as LocalTransportOrder | LocalLooseboardOrder)">编辑</el-button></span></el-tooltip></div></template>
|
||||
<template #pagination-left>共 {{ total }} 条{{ domainLabel }}订单</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout></AdminPage>
|
||||
</ListPageLayout><AppDialog v-model="arrangementOpen" class="admin-density-dialog" title="承运安排" width="1080px" :show-confirm="false" cancel-text="关闭"><FleetArrangementSummary v-if="arrangement" :arrangement="arrangement" /></AppDialog></AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>.table-panel { overflow:hidden; }</style>
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface LocalTransportQuery {
|
||||
transportType?: LocalTransportType | ''; customerId?: string; requestedFrom?: string; requestedTo?: string
|
||||
}
|
||||
export interface LocalTransportOrder {
|
||||
fleetArrangement: import('./fleetArrangement').FleetArrangement | null
|
||||
id: string; customerId: string; orderNo: string; transportType: LocalTransportType | null
|
||||
origin: string; destination: string; containerType: string | null; palletCount: number; packageCount: number
|
||||
receivedPalletCount: number; allocatedPalletCount: number; shippedPalletCount: number
|
||||
@@ -83,12 +84,13 @@ export interface UpdateLocalTransportOrderRequest {
|
||||
|
||||
export type LooseboardTransportMode = 'SHARED_TRUCK' | 'PRIORITY_CONSOLIDATION'
|
||||
export type PalletRequired = 'YES' | 'NO'
|
||||
export type LocalLooseboardStatus = 'PENDING_ACCEPTANCE' | 'DISPATCHED' | 'IN_TRANSIT' | 'COMPLETED' | 'CANCELLED'
|
||||
export type LocalLooseboardStatus = 'PENDING_ACCEPTANCE' | 'WAITING_INBOUND' | 'RECEIVING' | 'WAITING_CONSOLIDATION' | 'PENDING_DISPATCH' | 'DISPATCHED' | 'IN_TRANSIT' | 'COMPLETED' | 'CANCELLED'
|
||||
export interface LocalLooseboardQuery {
|
||||
page?: number; size?: number; customerId?: string; orderNo?: string; routeKeyword?: string
|
||||
status?: LocalLooseboardStatus | ''; transportMode?: LooseboardTransportMode | ''; requestedFrom?: string; requestedTo?: string
|
||||
}
|
||||
export interface LocalLooseboardOrder {
|
||||
fleetArrangement: import('./fleetArrangement').FleetArrangement | null
|
||||
id: string; customerId: string; orderNo: string; transportMode: LooseboardTransportMode; palletRequired: PalletRequired
|
||||
transportType: LocalTransportType; origin: string; destination: string; containerType: string | null; packageCount: number
|
||||
totalWeightKg: string; totalVolumeCbm: string; budgetAmount: string | null; currency: string
|
||||
|
||||
Reference in New Issue
Block a user