diff --git a/src/modules/overseas-warehouse/pages/InboundOrdersView.appointment.test.ts b/src/modules/overseas-warehouse/pages/InboundOrdersView.appointment.test.ts
index 11ca46e8..7ad70e0b 100644
--- a/src/modules/overseas-warehouse/pages/InboundOrdersView.appointment.test.ts
+++ b/src/modules/overseas-warehouse/pages/InboundOrdersView.appointment.test.ts
@@ -1,24 +1,37 @@
import { flushPromises, mount } from '@vue/test-utils'
-import { expect, it, vi } from 'vitest'
+import { beforeEach, expect, it, vi } from 'vitest'
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import InboundOrdersView from './InboundOrdersView.vue'
import { getFbaAppointment, updateFbaAppointment } from '../api/inboundOrders'
vi.mock('vue-router', () => ({ useRouter: () => ({push: vi.fn()}), onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() }))
-vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({can: () => true}) }))
+const access=vi.hoisted(()=>({canWrite:true}))
+vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({can: (permission:string) => !permission.endsWith(':fba-appointment') || access.canWrite}) }))
vi.mock('../api/inboundOrders', () => ({
listInboundOrders: vi.fn().mockResolvedValue({records:[{lineId:'1',planNo:'IP-1',customerId:'A',appointmentNo:'AP-1',fbaWarehouseCode:'ONT8'}],total:1}),
getFbaAppointment: vi.fn(), updateFbaAppointment: vi.fn(), createFbaAppointment: vi.fn(), updateInboundPlan: vi.fn(),
}))
vi.mock('element-plus', () => ({ ElMessage: {warning:vi.fn(),success:vi.fn()} }))
-it('keeps the inbound appointment retry across close, reopen and route attempts', async () => {
- vi.mocked(getFbaAppointment).mockResolvedValue({id:'1',appointmentNo:'AP-1',version:2,originWarehouseId:'100',fbaWarehouseCode:'ONT8',appointmentStatus:'PENDING',selectedCount:1,items:[]})
- vi.mocked(updateFbaAppointment).mockRejectedValueOnce(new Error('connection lost')).mockResolvedValueOnce({} as never)
- const wrapper=mount(InboundOrdersView,{global:{directives:{loading:{}},stubs:{
+const render=()=>mount(InboundOrdersView,{global:{directives:{loading:{}},stubs:{
AdminPage:{template:'
'},ListPageLayout:{template:'
'},
DataTable:{props:['data'],template:'
'},
- AppDialog:{name:'AppDialog',props:['modelValue','beforeClose'],template:'
'},
+ AppDialog:{name:'AppDialog',props:['modelValue','beforeClose','showConfirm'],template:'
'},
ElButton:{template:'
'}, FbaAppointmentFields:true,SmartDispatchDialog:true,QueryToolbar:true,OperationErrorToast:true,
}}})
+beforeEach(()=>{vi.clearAllMocks();access.canWrite=true;vi.mocked(getFbaAppointment).mockResolvedValue({id:'1',appointmentNo:'AP-1',version:2,originWarehouseId:'100',fbaWarehouseCode:'ONT8',appointmentStatus:'PENDING',selectedCount:1,items:[]})})
+it('opens linked appointment details for list-only users and blocks saving',async()=>{
+ access.canWrite=false
+ const wrapper=render();await flushPromises()
+ expect(wrapper.findAll('button').some(b=>b.text()==='预约')).toBe(false)
+ await wrapper.findAll('button').find(b=>b.text()==='查看预约')!.trigger('click');await flushPromises()
+ expect(getFbaAppointment).toHaveBeenCalledWith('AP-1')
+ expect(wrapper.findComponent({name:'FbaAppointmentFields'}).props('disabled')).toBe(true)
+ expect(wrapper.find('[data-save]').exists()).toBe(false)
+ wrapper.findComponent({name:'AppDialog'}).vm.$emit('confirm');await flushPromises()
+ expect(updateFbaAppointment).not.toHaveBeenCalled();wrapper.unmount()
+})
+it('keeps the inbound appointment retry across close, reopen and route attempts', async () => {
+ vi.mocked(updateFbaAppointment).mockRejectedValueOnce(new Error('connection lost')).mockResolvedValueOnce({} as never)
+ const wrapper=render()
await flushPromises()
const open=()=>wrapper.findAll('button').find(b=>b.text()==='预约')!.trigger('click')
await open();await flushPromises();await wrapper.get('[data-save]').trigger('click');await flushPromises()
diff --git a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue
index 44a3b7d7..48505b71 100644
--- a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue
+++ b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue
@@ -58,7 +58,9 @@ const appointmentVersion = ref(0)
const appointmentForm = reactive(createAppointmentForm())
const appointmentTimezone = ref
(null)
const pendingAppointment = ref<{appointmentNo: string | null; version: number; originWarehouseId: string; fbaWarehouseCode: string; lineIds: string[]; details: ReturnType; idempotencyKey: string} | null>(null)
-const appointmentTitle = computed(() => appointmentMode.value === 'edit' ? '编辑预约' : '预约')
+const appointmentViewOnly = ref(false)
+const appointmentReadOnly = computed(() => appointmentViewOnly.value || !access.can(permission.inboundOrderAppointment))
+const appointmentTitle = computed(() => appointmentReadOnly.value ? '查看预约' : appointmentMode.value === 'edit' ? '编辑预约' : '预约')
const planEditOpen = ref(false); const planEditLoading = ref(false); const planEditSaving = ref(false); const planEditTarget = ref(null); const planEditForm = reactive({ warehouseId: '', warehouseCode: '', warehouseName: '', version: 0 }); const warehouses = ref([])
function selectableRow(source: object) {
@@ -127,7 +129,8 @@ function selectionError() {
}
function openAppointment() {
- if (!canLeaveAppointment()) return
+ if (!access.can(permission.inboundOrderAppointment) || !canLeaveAppointment()) return
+ appointmentViewOnly.value = false
const error = selectionError()
if (error) { ElMessage.warning(error); return }
appointmentMode.value = 'create'; editingAppointmentNo.value = ''
@@ -142,10 +145,11 @@ function openSmartDispatch() {
smartDispatchOpen.value = true
}
-async function openAppointmentEdit(source: object) {
+async function openAppointmentEdit(source: object, readOnly = false) {
if (!canLeaveAppointment()) return
const row = source as InboundOrderListItem
if (!row.appointmentNo || row.appointmentNo === '—') return
+ appointmentViewOnly.value = readOnly
appointmentMode.value = 'edit'; editingAppointmentNo.value = row.appointmentNo
appointmentLoading.value = true; mutationError.value = null; appointmentOpen.value = true
try {
@@ -169,7 +173,7 @@ function beforeCloseAppointment(done: () => void) { if (canLeaveAppointment()) d
onBeforeRouteLeave(canLeaveAppointment)
onBeforeRouteUpdate(canLeaveAppointment)
async function submitAppointment() {
- if (saving.value || appointmentLoading.value) return
+ if (appointmentReadOnly.value || saving.value || appointmentLoading.value) return
if (appointmentMode.value === 'create') { const error = selectionError(); if (error) { ElMessage.warning(error); return } }
const validation = appointmentFormError(appointmentForm)
if (!pendingAppointment.value && validation) { ElMessage.warning(validation); return }
@@ -220,14 +224,14 @@ onMounted(() => { void load() })
预约智能排车
- 查看编辑预约
+ 查看编辑查看预约预约
共 {{ total }} 条入库单 · 已选 {{ selectedRows.length }} 条
-
+
已选择 {{ appointmentSelectedCount }} 条,FBA仓库:{{ appointmentFbaWarehouse }}
-
+
diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts
index f65d9ab3..8eca76a7 100644
--- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts
+++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts
@@ -369,7 +369,8 @@ describe('overseas warehouse first-batch prototype contract', () => {
expect(detail).toContain('单据信息')
expect(detail).toContain('关联订单明细')
expect(detail).toContain(':data="relatedOrders"')
- expect(detail).toContain("router.push({ name: 'overseas-dispatches' })")
+ expect(detail).toContain("router.push(inquiryReturnTarget(route.query) || { name: 'overseas-dispatches' })")
+ expect(detail).toContain("inquiryReturnTarget(route.query) ? '返回原拼车登记' : '返回排车列表'")
expect(detail).not.toContain('ElMessage.success')
})