- 刷新预订{{ pendingCreate ? '继续原登记' : '登记外接询价' }}
+ 刷新预订{{ pendingCreate ? '继续原登记' : '登记外接询价' }}询价不占位,确认承运后预留容量。实收时核对运费,转派送后按确认报价登记应收,付款状态以财务为准。
详情编辑确认承运取消预订来货详情登记来货核对实收关联排车
{{ bookingStatusLabels[row.status as keyof typeof bookingStatusLabels] }}
{{ row.receipt ? '实收' : '预计' }} · {{ row.cargo.packageCount ?? '—' }} 箱 · {{ row.expectedPalletCount ?? '—' }} 实托 · {{ row.cargo.palletSpaces ?? '—' }} 托位 · {{ row.cargo.weightKg ?? '—' }} kg · {{ row.cargo.volumeCbm ?? '—' }} m³
@@ -177,9 +178,9 @@ onBeforeUnmount(() => { generation++; searchSequence++; quoteSequence++; clearTi
来源:{{ quoteSource.quoteNo }} · 版本 {{ quoteSource.recordVersion }} · {{ quoteSource.originCity }} → {{ quoteSource.destinationCode }} · 适用日期 {{ quoteSource.effectiveOn }}
来源单价:{{ quoteSource.quotedUnitPrice.toFixed(2) }} {{ quoteSource.currency }} / {{ quoteSource.priceBasis === 'PALLET' ? '托位' : '单' }} · 有效期 {{ quoteSource.validFrom }} 至 {{ quoteSource.validTo }}
{{ quoteRef ? `已选报价 ${quoteRef.quoteNo} · 版本 ${quoteRef.recordVersion}` : '手工询价,尚未选择报价库来源。' }}
- 查询报价库
+ 查询报价库来源报价与实际约定价格分别保存;确认承运时重新核对报价版本与适用日期。可选择手工询价。
改为手工询价
- 选择客户、FBA卸货站及CNY或USD后可查询;其他场景可继续填写手工价格。
+ 查询报价需客户、FBA卸货站和 CNY/USD。
@@ -187,7 +188,7 @@ onBeforeUnmount(() => { generation++; searchSequence++; quoteSequence++; clearTi
上一页 {{ quotePage }} / {{ quotePages }} 下一页
{{ quoteError.message }}
- 来源报价与实际约定价格分别保存;确认承运时会重新核对报价版本与适用日期。
+
约定运费:{{ quotedAmount == null ? '待报价' : `${quotedAmount.toFixed(2)} ${form.currency || ''}` }}。保存询价不会占位,确认承运时核对本车剩余容量。
diff --git a/src/modules/overseas-master-data/components/TripCostPanel.vue b/src/modules/overseas-master-data/components/TripCostPanel.vue
index d60586de..5219fc96 100644
--- a/src/modules/overseas-master-data/components/TripCostPanel.vue
+++ b/src/modules/overseas-master-data/components/TripCostPanel.vue
@@ -1,4 +1,5 @@
+
+
+
+ 登记运营信息{{ access.can(permission.ownedBusinessUpload) && detail.row.actions.upload.allowed ? '上传照片' : '查看照片' }}
+ {{ issue }}
+ - {{ field[0] }}
- {{ field[1] }}
+ {{ destination.warehouseCode || destination.warehouseName || destination.address || '目的地资料缺失' }}
{{ destination.address }}
预约号 {{ appointment.appointmentNo }} · 确认号 {{ text(appointment.confirmationNo) }} · 预约到达 {{ time(appointment.scheduledAt) }}
实际到达 {{ time(destination.actualArrivedAt) }}
{{ destinationLabel(row.destinationKey) }}原排车关联单据
出库单:{{ detail.dispatch.documents.outbounds.map(item => item.outboundNo).join('、') || '—' }}
派送单:{{ detail.dispatch.documents.deliveries.map(item => item.deliveryNo).join('、') || '—' }}
查看分类照片
+ {{ row.eventSource === 'DISPATCH' ? '原排车' : '运营登记' }}{{ ownedBusinessActionLabels[row.action] || '其他操作' }}{{ time(row.occurredAt) }}整车金额 {{ row.operations.money ? row.operations.money.currency + ' ' + row.operations.money.amount : '未登记' }}
里程 {{ row.operations.actualDistanceMiles ?? '未登记' }} mile · 油耗 {{ row.operations.fuelConsumptionLitresPer100Km ?? '未登记' }} L/100km
{{ destinationLabel(arrival.destinationKey) }} · {{ time(arrival.actualArrivedAt) }}
—{{ photoLabel(photo) }}
+
+
+
+
+
+
diff --git a/src/modules/overseas-master-data/pages/OwnedBusinessView.vue b/src/modules/overseas-master-data/pages/OwnedBusinessView.vue
new file mode 100644
index 00000000..9db24f68
--- /dev/null
+++ b/src/modules/overseas-master-data/pages/OwnedBusinessView.vue
@@ -0,0 +1,115 @@
+
+
+
+
+ {{ item[0] }}{{ item[1] ?? '—' }}
+
+ 自有业务
+ 日期和时间按浏览器本地时区显示;统计覆盖同一完整筛选范围。
+ {{ warehouseError.message }} 重试目录
没有公共仓库目录读取权限,仓库筛选暂不可选。
+ 查看明细登记运营信息{{ access.can(permission.ownedBusinessUpload) && row.actions.upload.allowed ? '上传照片' : '查看照片' }}
+ {{ text(row.driver.name) }}
{{ text(row.driver.phone) }}
+ {{ destination.warehouseCode || destination.warehouseName || '—' }}{{ appointment.appointmentNo }}确认号 {{ appointment.confirmationNo }}
—
+ {{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}预约 {{ time(appointment.scheduledAt) }}
实到 {{ time(destination.actualArrivedAt) }}
+ {{ row.operations.fuelConsumptionLitresPer100Km ?? '未登记' }}
+ {{ row.operations.money.currency }} {{ row.operations.money.amount }}
单价 {{ row.unitPrice ?? '—' }}未登记
+ {{ time(row.plannedDepartureAt) }}{{ time(row.loadingCompletedAt) }}{{ ownedBusinessStages[row.stage as OwnedBusinessStage] }}
+
+
+
+
+
+
+
diff --git a/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts b/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts
new file mode 100644
index 00000000..432cd58c
--- /dev/null
+++ b/src/modules/overseas-master-data/pages/OwnedBusinessViews.test.ts
@@ -0,0 +1,85 @@
+import { defineComponent, h } from 'vue'
+import { flushPromises, mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import dayjs from 'dayjs'
+import OwnedBusinessView from './OwnedBusinessView.vue'
+import OwnedBusinessDetailView from './OwnedBusinessDetailView.vue'
+import { ownedBusinessFixture } from '../components/OwnedBusinessFixtures'
+import { overseasMasterDataPermissions as permission } from '../permissions'
+
+const calls = vi.hoisted(() => ({ list: vi.fn(), detail: vi.fn(), history: vi.fn(), content: vi.fn(), warehouses: vi.fn(), push: vi.fn(), allowed: new Set() }))
+vi.mock('../api/ownedBusiness', () => ({ listOwnedBusiness: calls.list, getOwnedBusiness: calls.detail, listOwnedBusinessHistory: calls.history, getOwnedBusinessPhotoContent: calls.content }))
+vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: calls.warehouses }))
+vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (code: string) => calls.allowed.has(code) }) }))
+vi.mock('vue-router', () => ({ useRouter: () => ({ push: calls.push }), useRoute: () => ({ params: { dispatchNo: 'DS-OWN-1' } }) }))
+const Surface = defineComponent({ template: '' })
+const Button = defineComponent({ props: ['disabled'], template: '' })
+const Tabs = defineComponent({ props: ['modelValue'], emits: ['update:modelValue'], template: '
' })
+const Pane = defineComponent({ props: ['name','label'], template: '' })
+const Query = defineComponent({ props: ['modelValue','fields'], emits: ['update:modelValue','search'], template: '' })
+const Table = defineComponent({ props: ['data','columns','total','page','pageSize'], emits: ['update:page','update:pageSize'], setup(props,{slots}) { return () => h('div', [slots['toolbar-start']?.(), slots.filters?.(), slots.notices?.(), ...(props.data || []).map((row: object) => h('div', Object.entries(slots).filter(([name]) => !['toolbar-start','filters','notices'].includes(name)).map(([,slot]) => slot?.({row}))))]) } })
+const Editor = defineComponent({ props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '' })
+const global = { stubs: { AdminPage: Surface, DetailPageLayout: Surface, DataTable: Table, QueryToolbar: Query, ElTabs: Tabs, ElTabPane: Pane, ElButton: Button, OwnedBusinessOperationsEditor: Editor, OwnedBusinessPhotosEditor: Editor, LoadErrorState: { props:['message'], template:'{{message}}
' }, ApiErrorMeta: true }, directives: { loading: () => {} } }
+
+beforeEach(() => {
+ vi.clearAllMocks(); calls.allowed = new Set(Object.values(permission)); calls.allowed.add('platform:warehouse:list')
+ const detail = ownedBusinessFixture()
+ calls.list.mockResolvedValue({ records:[detail.row],page:1,size:20,total:41,summary:{dispatchCount:41,afterLoadingCount:30,deliveringCount:10,deliveredCount:1} })
+ calls.detail.mockResolvedValue(detail); calls.history.mockResolvedValue({records:[],page:1,size:20,total:0}); calls.warehouses.mockResolvedValue([])
+})
+
+describe('owned business views', () => {
+ it('uses one list request for all statistics and never hydrates each row', async () => {
+ const wrapper = mount(OwnedBusinessView,{global}); await flushPromises()
+ expect(calls.list).toHaveBeenCalledTimes(1); expect(calls.detail).not.toHaveBeenCalled(); expect(calls.content).not.toHaveBeenCalled()
+ expect(wrapper.find('[aria-label="完整筛选范围自有业务统计"]').text()).toContain('排车数41')
+ expect(wrapper.text()).not.toContain('今日排车')
+ expect(wrapper.findComponent(Table).props('columns')[0].key).toBe('actions')
+ expect(wrapper.findComponent(Table).props('total')).toBe(41)
+ wrapper.unmount()
+ })
+ it('applies planned date half-open boundaries only on query and keeps all stage counts from the response', async () => {
+ const wrapper=mount(OwnedBusinessView,{global});await flushPromises();const query=wrapper.findComponent(Query)
+ query.vm.$emit('update:modelValue',{plannedRange:['2026-10-20','2026-10-21'],keyword:' FBA-1 ',stage:'AFTER_LOADING',warehouseId:'1'});await wrapper.vm.$nextTick()
+ expect(calls.list).toHaveBeenCalledTimes(1)
+ query.vm.$emit('search');await flushPromises()
+ expect(calls.list).toHaveBeenLastCalledWith({page:1,size:20,keyword:'FBA-1',stage:'AFTER_LOADING',warehouseId:'1',plannedDepartureFrom:dayjs('2026-10-20').startOf('day').toISOString(),plannedDepartureTo:dayjs('2026-10-21').add(1,'day').startOf('day').toISOString()})
+ expect(wrapper.find('[aria-label="完整筛选范围自有业务统计"]').text()).toContain('装车完成30')
+ wrapper.unmount()
+ })
+ it('opens the operations modal on demand with auxiliary zero version instead of the dispatch version', async () => {
+ const wrapper=mount(OwnedBusinessView,{global});await flushPromises()
+ await wrapper.findAll('button').find(button=>button.text()==='登记运营信息')!.trigger('click');await flushPromises()
+ expect(calls.detail).toHaveBeenCalledExactlyOnceWith('DS-OWN-1')
+ const editor=wrapper.findAllComponents(Editor).find(child=>child.props('modelValue'))!
+ expect(editor.props('detail').row.recordVersion).toBe('0');expect(editor.props('detail').dispatch.recordVersion).toBe('8')
+ wrapper.unmount()
+ })
+ it('keeps a list-only user read-only while retaining detail and authorized photo reads', async () => {
+ calls.allowed=new Set([permission.ownedBusinessList])
+ const wrapper=mount(OwnedBusinessView,{global});await flushPromises()
+ expect(wrapper.text()).not.toContain('登记运营信息');expect(wrapper.text()).toContain('查看照片');expect(calls.warehouses).not.toHaveBeenCalled()
+ wrapper.unmount()
+ })
+ it('loads aggregated history only on its tab, preserving null operations on dispatch events', async () => {
+ calls.history.mockResolvedValue({records:[{eventSource:'DISPATCH',recordVersion:null,dispatchRecordVersion:'8',action:'LOADING_COMPLETE',operatorName:null,occurredAt:'2026-10-20T09:00:00Z',reason:null,operations:null,photos:[]},{eventSource:'OPERATIONS',recordVersion:'1',dispatchRecordVersion:null,action:'SAVE_OPERATIONS',operatorName:'登记员',occurredAt:'2026-10-20T10:00:00Z',reason:null,operations:ownedBusinessFixture().row.operations,photos:[]}],page:1,size:20,total:2})
+ const wrapper=mount(OwnedBusinessDetailView,{global});await flushPromises()
+ expect(calls.detail).toHaveBeenCalledTimes(1);expect(calls.history).not.toHaveBeenCalled()
+ wrapper.findComponent(Tabs).vm.$emit('update:modelValue','history');await flushPromises()
+ expect(calls.history).toHaveBeenCalledExactlyOnceWith('DS-OWN-1',1,20)
+ expect(wrapper.text()).toContain('原排车');expect(wrapper.text()).toContain('运营登记')
+ const table=wrapper.findComponent(Table), records=table.props('data')
+ expect(records[0].eventKey).not.toBe(records[1].eventKey);expect(records[0].operations).toBeNull()
+ table.vm.$emit('update:page',2);await flushPromises();expect(calls.history).toHaveBeenLastCalledWith('DS-OWN-1',2,20)
+ expect(calls.detail).toHaveBeenCalledTimes(1);expect(calls.content).not.toHaveBeenCalled()
+ wrapper.unmount()
+ })
+ it('shows authoritative large money text and unit price without rounding or recomputing from two cargo rows', async () => {
+ const detail=ownedBusinessFixture();detail.row.operations.money={currency:'USD',amount:'9999999999999999.99'};detail.row.unitPrice='9999999999999999.99'
+ calls.detail.mockResolvedValue(detail)
+ const wrapper=mount(OwnedBusinessDetailView,{global});await flushPromises()
+ expect(wrapper.text()).toContain('USD 9999999999999999.99');expect(wrapper.text()).not.toContain('10000000000000000')
+ expect(wrapper.text()).not.toContain('4999999999999999')
+ wrapper.unmount()
+ })
+})
diff --git a/src/modules/overseas-master-data/routes.test.ts b/src/modules/overseas-master-data/routes.test.ts
index 82389457..d279afaa 100644
--- a/src/modules/overseas-master-data/routes.test.ts
+++ b/src/modules/overseas-master-data/routes.test.ts
@@ -11,6 +11,8 @@ describe('overseas master data routes', () => {
['overseas-master-data/capacity-offers/:offerNo', overseasMasterDataPermissions.capacityOfferList],
['overseas-master-data/fleet-quotes', overseasMasterDataPermissions.fleetQuoteList],
['overseas-master-data/trip-operations', overseasMasterDataPermissions.operationsList],
+ ['overseas-master-data/owned-business', overseasMasterDataPermissions.ownedBusinessList],
+ ['overseas-master-data/owned-business/:dispatchNo', overseasMasterDataPermissions.ownedBusinessList],
['overseas-master-data/trips', overseasMasterDataPermissions.tripList],
['overseas-master-data/trips/:tripNo', overseasMasterDataPermissions.tripList],
['overseas-master-data/drivers', overseasMasterDataPermissions.driverList],
@@ -31,6 +33,18 @@ describe('overseas master data routes', () => {
}
})
+ it('uses one list permission for owned-business list and independent detail without granting writes', () => {
+ const owned = overseasMasterDataRoutes.filter(route => route.path.includes('/owned-business'))
+ expect(owned).toHaveLength(2)
+ expect(owned.map(route => route.path)).toContain('overseas-master-data/owned-business/:dispatchNo')
+ for (const route of owned) {
+ expect(route.meta?.permission).toBe(overseasMasterDataPermissions.ownedBusinessList)
+ expect(route.meta?.permissionAnyOf).toBeUndefined()
+ expect(route.meta?.permission).not.toBe(overseasMasterDataPermissions.ownedBusinessEdit)
+ expect(route.meta?.permission).not.toBe(overseasMasterDataPermissions.ownedBusinessUpload)
+ }
+ })
+
it('keeps vehicle detail as a route page and does not add a duplicate partner warehouse route', () => {
expect(overseasMasterDataRoutes.at(-1)?.meta?.showPageHeading).toBe(false)
expect(overseasMasterDataRoutes.some((route) => route.path.includes('partner-warehouse'))).toBe(false)
diff --git a/src/modules/overseas-master-data/routes.ts b/src/modules/overseas-master-data/routes.ts
index a7a7d332..029b1e16 100644
--- a/src/modules/overseas-master-data/routes.ts
+++ b/src/modules/overseas-master-data/routes.ts
@@ -26,6 +26,18 @@ export const overseasMasterDataRoutes = [
component: () => import('./pages/TripOperationsView.vue'),
meta: { title: '出车记录', description: '按实际发车记录查询车辆运营统计', permission: permission.operationsList, showPageHeading: false },
},
+ {
+ path: 'overseas-master-data/owned-business',
+ name: 'overseas-master-data-owned-business',
+ component: () => import('./pages/OwnedBusinessView.vue'),
+ meta: { title: '自有业务', description: '查看自有车辆装车后的排车与运营记录', permission: permission.ownedBusinessList, showPageHeading: false },
+ },
+ {
+ path: 'overseas-master-data/owned-business/:dispatchNo',
+ name: 'overseas-master-data-owned-business-detail',
+ component: () => import('./pages/OwnedBusinessDetailView.vue'),
+ meta: { title: '自有业务详情', description: '查看原排车事实与附加运营信息', permission: permission.ownedBusinessList, showPageHeading: false },
+ },
{
path: 'overseas-master-data/trips',
name: 'overseas-master-data-trips',
diff --git a/src/modules/overseas-warehouse/components/DispatchResourceFields.vue b/src/modules/overseas-warehouse/components/DispatchResourceFields.vue
index 1f8f9e00..0ecb1563 100644
--- a/src/modules/overseas-warehouse/components/DispatchResourceFields.vue
+++ b/src/modules/overseas-warehouse/components/DispatchResourceFields.vue
@@ -45,7 +45,7 @@ function selectDriver(id: string) {
| |
| |
-
+
{{ error }}
@@ -61,7 +61,7 @@ function selectDriver(id: string) {