This commit is contained in:
2026-08-05 15:54:59 +08:00
parent eede351038
commit 4b31eb7786
43 changed files with 2117 additions and 851 deletions
@@ -84,17 +84,8 @@ async function mountView(permissions = [
return wrapper
}
async function setFilterContext(wrapper: VueWrapper, customerId = '10001', warehouseId = '') {
async function setWarehouseFilter(wrapper: VueWrapper, warehouseId = '') {
wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', {
customerId,
warehouseId: '',
palletCode: '',
palletType: '',
})
await flushPromises()
if (!warehouseId) return
wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', {
customerId,
warehouseId,
palletCode: '',
palletType: '',
@@ -103,9 +94,8 @@ async function setFilterContext(wrapper: VueWrapper, customerId = '10001', wareh
}
async function loadRows(wrapper: VueWrapper, warehouseId = '') {
await setFilterContext(wrapper, '10001', warehouseId)
await setWarehouseFilter(wrapper, warehouseId)
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
customerId: '10001',
warehouseId,
palletCode: '',
palletType: '',
@@ -129,33 +119,33 @@ describe('PalletsView', () => {
vi.mocked(createPlatformPallet).mockReset().mockResolvedValue('20002')
})
it('starts with all customers and uses compact semantic filter widths', async () => {
it('starts with all customers and all warehouses using compact filters', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
const fields = toolbar.props('fields')
expect(toolbar.props('modelValue')).toMatchObject({ customerId: '', warehouseId: '' })
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
expect(fields.map((field) => [field.key, field.width])).toEqual([
['customerId', 260],
['warehouseId', 240],
['warehouseId', 300],
['palletCode', 240],
['palletType', 180],
])
expect(wrapper.find('.pallet-filter-toolbar').exists()).toBe(true)
expect(fields.find((field) => field.key === 'customerId')?.placeholder).toBe('全部客户')
expect(listPlatformWarehouses).not.toHaveBeenCalled()
expect(fields.find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
})
it('loads warehouses after customer selection and omits an empty warehouse id when searching', async () => {
it('loads all warehouses independently and omits an empty warehouse id when searching', async () => {
const wrapper = await mountView()
await loadRows(wrapper)
expect(listPlatformWarehouses).toHaveBeenCalledWith(10001, 1, 200)
expect(listPlatformWarehouses).toHaveBeenCalledTimes(1)
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20, tenantId: 10001,
page: 1, size: 20,
})
expect(wrapper.text()).toContain('TP-001')
expect(wrapper.text()).toContain('木托盘')
@@ -165,7 +155,7 @@ describe('PalletsView', () => {
expect(wrapper.text()).not.toContain('启停托盘')
expect(wrapper.text()).not.toContain('全部状态')
expect(wrapper.text()).toContain('全部托盘类型')
expect(wrapper.find('input[aria-label="所属企业"]').exists()).toBe(true)
expect(wrapper.find('input[aria-label="客户"]').exists()).toBe(false)
expect(wrapper.find('input[aria-label="所属仓库"]').exists()).toBe(true)
expect(wrapper.find('.platform-tenant-selector').exists()).toBe(false)
expect(wrapper.text()).not.toContain('租户')
@@ -179,7 +169,7 @@ describe('PalletsView', () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.$emit('search', { customerId: '', warehouseId: '', palletCode: '', palletType: '' })
toolbar.vm.$emit('search', { warehouseId: '', palletCode: '', palletType: '' })
await flushPromises()
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20,
@@ -187,56 +177,49 @@ describe('PalletsView', () => {
})
it('resets customer and dependent warehouse filters without sending an empty id', async () => {
it('resets the warehouse filter without sending an empty id', async () => {
const wrapper = await mountView()
await setFilterContext(wrapper, '10001', '19001')
await setWarehouseFilter(wrapper, '19001')
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.reset()
await flushPromises()
expect(toolbar.props('modelValue')).toMatchObject({ customerId: '', warehouseId: '' })
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
expect(listPlatformPallets).toHaveBeenCalledTimes(2)
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20,
})
expect(toolbar.props('fields').find((field) => field.key === 'customerId')?.placeholder).toBe('全部客户')
expect(toolbar.props('fields').find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
})
it('preserves a snowflake customer id when switching the enterprise filter', async () => {
it('does not expose customer ownership in warehouse choices', async () => {
const wrapper = await mountView()
const snowflakeId = '9223372036854775806'
const warehouseField = wrapper.findComponent(QueryToolbar).props('fields').find((field) => field.key === 'warehouseId')
wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', {
customerId: snowflakeId,
warehouseId: '',
palletCode: '',
palletType: '',
})
await flushPromises()
expect(listPlatformWarehouses).toHaveBeenLastCalledWith(snowflakeId, 1, 200)
expect(listPlatformWarehouses).toHaveBeenCalledTimes(1)
expect(warehouseField?.options).toEqual([{ label: '义乌收货仓(CN-YW-01)', value: '19001' }])
})
it('applies pallet filters only after search', async () => {
const wrapper = await mountView()
await setFilterContext(wrapper, '10001', '19001')
await setWarehouseFilter(wrapper, '19001')
await wrapper.find('input[placeholder="托盘编码(模糊查询)"]').setValue('TP-00')
expect(listPlatformPallets).toHaveBeenCalledTimes(1)
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
customerId: '10001', warehouseId: '19001', palletCode: 'TP-00', palletType: '木托盘',
warehouseId: '19001', palletCode: 'TP-00', palletType: '木托盘',
})
await flushPromises()
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20, tenantId: 10001, warehouseId: 19001, palletCode: 'TP-00', palletType: '木托盘',
page: 1, size: 20, warehouseId: 19001, palletCode: 'TP-00', palletType: '木托盘',
})
})
it('creates a pallet in the selected warehouse with the real request fields', async () => {
const wrapper = await mountView()
await setFilterContext(wrapper, '10001', '19001')
await setWarehouseFilter(wrapper, '19001')
const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘')
await createButton?.trigger('click')
await flushPromises()
@@ -276,13 +259,13 @@ describe('PalletsView', () => {
expect(wrapper.text()).not.toContain('TP-001')
})
it('stops dependent requests when the enterprise filter cannot load', async () => {
it('keeps platform warehouse and pallet queries available when customer names cannot load', async () => {
vi.mocked(listPlatformCustomers).mockRejectedValueOnce(new Error('Gateway unavailable'))
const wrapper = await mountView()
expect(wrapper.text()).toContain('所属企业列表暂时无法加载')
expect(wrapper.text()).toContain('客户列表暂时无法加载')
expect(wrapper.text()).toContain('重新加载')
expect(listPlatformWarehouses).not.toHaveBeenCalled()
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
@@ -24,8 +24,6 @@ const access = useAccessPreviewStore()
const customers = ref<PlatformCustomerResource[]>([])
const customerLoading = ref(false)
const customerError = ref<ApiErrorDetails | null>(null)
const selectedCustomerId = ref('')
const selectedWarehouseId = ref('')
const warehouses = ref<PlatformWarehouseResource[]>([])
const warehouseLoading = ref(false)
const warehouseError = ref<ApiErrorDetails | null>(null)
@@ -45,36 +43,24 @@ const formRef = ref<FormInstance>()
let warehouseSequence = 0
let palletSequence = 0
type PalletFilters = QueryModel & { customerId: string; warehouseId: string; palletCode: string; palletType: string }
type AppliedPalletFilters = Pick<PalletFilters, 'palletCode' | 'palletType'>
const filters = ref<PalletFilters>({ customerId: '', warehouseId: '', palletCode: '', palletType: '' })
const appliedFilters = ref<AppliedPalletFilters>({ palletCode: '', palletType: '' })
type PalletFilters = QueryModel & { warehouseId: string; palletCode: string; palletType: string }
type AppliedPalletFilters = Pick<PalletFilters, 'warehouseId' | 'palletCode' | 'palletType'>
const filters = ref<PalletFilters>({ warehouseId: '', palletCode: '', palletType: '' })
const appliedFilters = ref<AppliedPalletFilters>({ warehouseId: '', palletCode: '', palletType: '' })
const palletTypeOptions = ['木托盘', '塑料托盘', '金属托盘', '纸托盘', '免熏蒸托盘']
const queryFields = computed<QueryField[]>(() => [
{
key: 'customerId',
type: 'select',
width: 260,
placeholder: customerLoading.value ? '正在加载客户' : customerError.value ? '客户列表加载失败' : '全部客户',
options: customers.value.map((customer) => ({
label: `${customer.tenantName}(${customer.tenantCode || customer.id})`,
value: String(customer.id),
})),
props: { filterable: true, disabled: contextLocked.value || customerLoading.value || Boolean(customerError.value), 'aria-label': '所属企业' },
},
{
key: 'warehouseId',
type: 'select',
width: 240,
width: 300,
placeholder: warehouseLoading.value ? '正在加载所属仓库' : warehouseError.value ? '所属仓库加载失败' : '全部仓库',
options: warehouses.value.map((warehouse) => ({
label: `${warehouse.warehouseName}(${warehouse.warehouseCode})`,
value: String(warehouse.id),
disabled: warehouse.status !== 'ACTIVE',
})),
props: {
filterable: true,
disabled: contextLocked.value || !selectedCustomerId.value || warehouseLoading.value || Boolean(warehouseError.value),
disabled: contextLocked.value || warehouseLoading.value || Boolean(warehouseError.value),
'aria-label': '所属仓库',
},
},
@@ -92,6 +78,7 @@ const queryFields = computed<QueryField[]>(() => [
const emptyForm = () => ({ id: '', customerId: '', warehouseId: '', palletCode: '', palletType: '', capacityCbm: null as number | null, remark: '' })
const form = reactive(emptyForm())
const rules: FormRules<typeof form> = {
warehouseId: [{ required: true, message: '请选择所属仓库', trigger: 'change' }],
palletCode: [
{ required: true, message: '请输入托盘编码', trigger: 'blur' },
{ max: 50, message: '托盘编码不能超过 50 个字符', trigger: 'blur' },
@@ -110,10 +97,7 @@ const rules: FormRules<typeof form> = {
remark: [{ max: 500, message: '备注不能超过 500 个字符', trigger: 'blur' }],
}
const currentWarehouse = computed(() => warehouses.value.find((item) => String(item.id) === selectedWarehouseId.value) || null)
const editorWarehouse = computed(() => warehouses.value.find((item) => String(item.id) === form.warehouseId) || null)
const currentCustomer = computed(() => customers.value.find((item) => String(item.id) === selectedCustomerId.value) || null)
const contextReady = computed(() => Boolean(selectedCustomerId.value && selectedWarehouseId.value && currentWarehouse.value))
const editorTitle = computed(() => form.id ? '编辑托盘' : '新增托盘')
const editorPermission = computed(() => form.id ? 'platform:pallet:edit' : 'platform:pallet:create')
const contextLocked = computed(() => editorOpen.value || detailOpen.value || saving.value || deletingIds.value.size > 0)
@@ -187,18 +171,13 @@ async function loadCustomers() {
}
}
async function loadWarehouses(customerId = selectedCustomerId.value) {
async function loadWarehouses() {
const sequence = ++warehouseSequence
selectedWarehouseId.value = ''
filters.value = { ...filters.value, warehouseId: '' }
warehouses.value = []
rows.value = []
total.value = 0
warehouseError.value = null
if (!customerId) return
warehouseLoading.value = true
try {
const result = await listPlatformWarehouses(apiId(customerId), 1, 200)
const result = await listPlatformWarehouses(undefined, 1, 200)
if (sequence !== warehouseSequence) return
warehouses.value = result.records
} catch (error) {
@@ -210,8 +189,6 @@ async function loadWarehouses(customerId = selectedCustomerId.value) {
}
async function loadPallets() {
const customerId = selectedCustomerId.value
const warehouseId = selectedWarehouseId.value
const sequence = ++palletSequence
rows.value = []
total.value = 0
@@ -221,8 +198,7 @@ async function loadPallets() {
const result = await listPlatformPallets({
page: page.value,
size: pageSize.value,
...(customerId ? { tenantId: apiId(customerId) } : {}),
...(warehouseId ? { warehouseId: apiId(warehouseId) } : {}),
...(appliedFilters.value.warehouseId ? { warehouseId: apiId(appliedFilters.value.warehouseId) } : {}),
...(appliedFilters.value.palletCode ? { palletCode: appliedFilters.value.palletCode } : {}),
...(appliedFilters.value.palletType ? { palletType: appliedFilters.value.palletType } : {}),
})
@@ -238,29 +214,25 @@ async function loadPallets() {
}
function applyFilters(model: QueryModel) {
selectedCustomerId.value = String(model.customerId || '')
selectedWarehouseId.value = String(model.warehouseId || '')
appliedFilters.value = { palletCode: String(model.palletCode || ''), palletType: String(model.palletType || '') }
appliedFilters.value = {
warehouseId: String(model.warehouseId || ''),
palletCode: String(model.palletCode || ''),
palletType: String(model.palletType || ''),
}
if (page.value === 1) void loadPallets()
else page.value = 1
}
function resetFilters() {
selectedCustomerId.value = ''
selectedWarehouseId.value = ''
appliedFilters.value = { palletCode: '', palletType: '' }
appliedFilters.value = { warehouseId: '', palletCode: '', palletType: '' }
if (page.value === 1) void loadPallets()
else page.value = 1
}
function openCreate() {
if (!contextReady.value) {
ElMessage.warning('请先选择所属企业和仓库')
return
}
Object.assign(form, emptyForm())
form.customerId = selectedCustomerId.value
form.warehouseId = selectedWarehouseId.value
form.warehouseId = filters.value.warehouseId
form.customerId = editorWarehouse.value ? String(editorWarehouse.value.tenantId) : ''
editorOpen.value = true
captureBaseline()
void formRef.value?.clearValidate()
@@ -308,7 +280,13 @@ async function openEdit(row: PlatformPalletResource) {
}
async function savePallet() {
if ((!form.id && !contextReady.value) || !form.customerId || !form.warehouseId || !await formRef.value?.validate().catch(() => false) || form.capacityCbm === null) return
if (!await formRef.value?.validate().catch(() => false) || form.capacityCbm === null) return
const warehouse = editorWarehouse.value
if (!warehouse) {
ElMessage.error('所选仓库已不存在,请重新选择')
return
}
form.customerId = String(warehouse.tenantId)
saving.value = true
const payload: SavePlatformPalletRequest = {
warehouseId: apiId(form.warehouseId),
@@ -357,18 +335,6 @@ async function removePallet(row: PlatformPalletResource) {
}
}
watch(() => filters.value.customerId, (customerId) => {
selectedCustomerId.value = String(customerId || '')
page.value = 1
void loadWarehouses(selectedCustomerId.value)
})
watch(() => filters.value.warehouseId, (warehouseId) => {
selectedWarehouseId.value = String(warehouseId || '')
page.value = 1
rows.value = []
total.value = 0
loadError.value = null
})
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
if (nextPage === previousPage && nextSize === previousSize) return
void loadPallets()
@@ -378,6 +344,7 @@ watch(() => access.can(editorPermission.value), (canWrite) => {
}, { flush: 'sync' })
onMounted(() => {
void loadCustomers()
void loadWarehouses()
void loadPallets()
})
</script>
@@ -386,7 +353,7 @@ onMounted(() => {
<section class="pallet-page">
<LoadErrorState
v-if="customerError"
title="所属企业列表暂时无法加载"
title="客户列表暂时无法加载"
:message="customerError.message"
:loading="customerLoading"
@retry="loadCustomers"
@@ -407,7 +374,7 @@ onMounted(() => {
title="仓库列表暂时无法加载"
:message="warehouseError.message"
:loading="warehouseLoading"
@retry="loadWarehouses()"
@retry="loadWarehouses"
/>
<LoadErrorState
@@ -420,8 +387,8 @@ onMounted(() => {
<section class="table-panel surface-panel" aria-label="托盘列表区域">
<div class="panel-heading">
<div><h2>托盘列表</h2><p>{{ currentCustomer?.tenantName || '全部客户' }} · {{ currentWarehouse?.warehouseName || '全部仓库' }} · 共 {{ total }} 个托盘</p></div>
<el-button v-if="access.can('platform:pallet:create')" type="primary" :disabled="!contextReady" @click="openCreate"><Plus :size="16" />新增托盘</el-button>
<div><h2>托盘列表</h2><p>共 {{ total }} 个托盘</p></div>
<el-button v-if="access.can('platform:pallet:create')" type="primary" @click="openCreate"><Plus :size="16" />新增托盘</el-button>
</div>
<DataTable
v-model:page="page"
@@ -431,7 +398,7 @@ onMounted(() => {
:loading="loading || warehouseLoading"
:total="total"
row-key="id"
:empty-text="filtered ? '暂无符合筛选条件的托盘' : selectedCustomerId ? '当前客户暂无托盘' : '暂无托盘数据'"
:empty-text="filtered ? '暂无符合筛选条件的托盘' : '暂无托盘数据'"
aria-label="托盘列表"
>
<template #actions="{ row }">
@@ -484,11 +451,21 @@ onMounted(() => {
</el-drawer>
<el-drawer v-model="editorOpen" :title="editorTitle" size="520px" class="business-drawer" :close-on-click-modal="false">
<div class="editor-context">
<div v-if="form.id" class="editor-context">
<WarehouseIcon :size="18" />
<div><strong>{{ editorWarehouse?.warehouseName || '当前仓库' }}({{ editorWarehouse?.warehouseCode || form.warehouseId }})</strong><span>所属企业 {{ customerLabel(form.customerId) }} · 托盘保存后不可跨仓移动</span></div>
<div><strong>{{ editorWarehouse?.warehouseName || '当前仓库' }}({{ editorWarehouse?.warehouseCode || form.warehouseId }})</strong><span>客户 {{ customerLabel(form.customerId) }} · 托盘保存后不可跨仓移动</span></div>
</div>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="editor-form">
<el-form-item v-if="!form.id" label="所属仓库" prop="warehouseId" required>
<el-select v-model="form.warehouseId" filterable placeholder="请选择所属仓库" :loading="warehouseLoading">
<el-option
v-for="warehouse in warehouses"
:key="String(warehouse.id)"
:label="`${warehouse.warehouseName}(${warehouse.warehouseCode})`"
:value="String(warehouse.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="托盘编码" prop="palletCode" required>
<el-input v-model="form.palletCode" maxlength="50" show-word-limit placeholder="例如 TP-001" />
</el-form-item>