This commit is contained in:
2026-08-06 13:56:04 +08:00
parent 6736893128
commit a813426588
35 changed files with 2822 additions and 489 deletions
@@ -9,13 +9,11 @@ import {
listPlatformLocations,
type PlatformLocationResource,
} from '../api/platformLocations'
import { listPlatformTenants, type PlatformTenantResource } from '../../../iam/api/platformIam'
import { listPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import DataTable from '@/shared/components/DataTable.vue'
import LocationsView from './LocationsView.vue'
vi.mock('../../../iam/api/platformIam', () => ({ listPlatformTenants: vi.fn() }))
vi.mock('../../../warehouses/api/platformWarehouses', () => ({ listPlatformWarehouses: vi.fn() }))
vi.mock('../api/platformLocations', () => ({
listPlatformLocations: vi.fn(),
@@ -44,32 +42,7 @@ function createMemoryStorage(): Storage {
}
}
const tenant = (id: number | string): PlatformTenantResource => ({
id,
tenantCode: String(id),
tenantName: '雪花企业',
status: 'ACTIVE',
legalName: '雪花企业',
legalRepresentative: null,
unifiedSocialCreditCode: null,
contactName: null,
contactPhone: null,
contactEmail: null,
servicePackage: null,
contactWechat: null,
salesOwnerName: null,
paymentTermDays: null,
enterpriseType: null,
establishedDate: null,
registeredCapital: null,
registeredCapitalCurrency: null,
registeredAddress: null,
internalRemark: null,
createdAt: null,
approvedAt: null,
})
const tenantId = '9223372036854775806'
const tenantId = '0'
const warehouseId = '9223372036854775805'
const warehouse: PlatformWarehouseResource = {
id: warehouseId,
@@ -151,9 +124,6 @@ describe('LocationsView', () => {
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
document.body.innerHTML = ''
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({
records: [tenant(tenantId)], page: 1, size: 200, total: 1, pages: 1,
})
vi.mocked(listPlatformWarehouses).mockReset().mockResolvedValue({
records: [warehouse], page: 1, size: 200, total: 1, pages: 1,
})
@@ -170,14 +140,13 @@ describe('LocationsView', () => {
const toolbar = wrapper.findComponent(QueryToolbar)
const fields = toolbar.props('fields') as QueryField[]
expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200 })
expect(wrapper.find('[aria-label="所属企业"]').exists()).toBe(false)
expect(fields[0]?.placeholder).toBe('全部仓库')
expect(fields.map(({ key, width }) => [key, width])).toEqual([
['warehouseId', 300], ['zoneCode', 210],
['locationCode', 220], ['locationType', 170], ['status', 160],
])
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200)
expect(listPlatformLocations).toHaveBeenCalledWith({
page: 1,
size: 20,
@@ -206,7 +175,7 @@ describe('LocationsView', () => {
expect(panelHeading.text()).toContain('共 2 个库位')
expect(panelHeading.text()).toContain('新增库位')
expect(wrapper.findComponent(QueryToolbar).text()).not.toContain('新增库位')
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200)
expect(listPlatformLocations).toHaveBeenLastCalledWith({
page: 1,
size: 20,
@@ -259,7 +228,8 @@ describe('LocationsView', () => {
expect(getPlatformLocation).toHaveBeenCalledWith(tenantId, warehouseId, availableLocation.id)
expect(document.body.textContent).toContain('库位详情')
expect(document.body.textContent).toContain('义乌收货仓(CN-YW-01)')
expect(document.body.textContent).toContain(`所属企业雪花企业(${tenantId})`)
expect(document.body.textContent).toContain('资源作用域平台公共资源')
expect(document.body.textContent).not.toContain('所属企业')
expect(document.body.textContent).not.toContain('企业 ID')
expect(document.body.textContent).toContain('库位名称A1 区 01 位')
expect(document.body.textContent).toContain('v3')
@@ -2,7 +2,6 @@
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Eye, MapPin, MoreHorizontal, Pencil, Plus, Power, Trash2 } from '@lucide/vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
@@ -43,9 +42,6 @@ type AppliedLocationFilters = {
}
const access = useAccessPreviewStore()
const customers = ref<PlatformTenantResource[]>([])
const customerLoading = ref(false)
const customerError = ref<ApiErrorDetails | null>(null)
const warehouses = ref<PlatformWarehouseResource[]>([])
const warehouseLoading = ref(false)
const warehouseError = ref<ApiErrorDetails | null>(null)
@@ -82,7 +78,7 @@ const queryFields = computed<QueryField[]>(() => [
},
options: warehouses.value.map((warehouse) => ({
value: String(warehouse.id),
label: `${warehouse.warehouseName}(${warehouse.warehouseCode}) · ${customerLabel(warehouse.tenantId)}`,
label: `${warehouse.warehouseName}(${warehouse.warehouseCode})`,
})),
},
{ key: 'zoneCode', type: 'input', width: 210, placeholder: '库区编码', props: { maxlength: 50, 'aria-label': '库区编码' } },
@@ -232,32 +228,13 @@ function recordScope(row: PlatformLocationResource) {
return { customerId: apiId(String(row.tenantId)), warehouseId: apiId(String(row.warehouseId)) }
}
function customerLabel(customerId: number | string) {
const customer = customers.value.find((item) => String(item.id) === String(customerId))
return customer ? `${customer.tenantName}(${customer.tenantCode || customer.id})` : String(customerId)
}
async function loadCustomers() {
customerLoading.value = true
customerError.value = null
try {
const result = await listPlatformTenants({ page: 1, size: 200 })
customers.value = result.records
} catch (error) {
customers.value = []
customerError.value = getApiErrorDetails(error, '所属企业列表加载失败,请稍后重试')
} finally {
customerLoading.value = false
}
}
async function loadWarehouses() {
const sequence = ++warehouseLoadSequence
warehouseLoading.value = true
warehouseError.value = null
warehouses.value = []
try {
const result = await listPlatformWarehouses(undefined, 1, 200)
const result = await listPlatformWarehouses(1, 200)
if (sequence !== warehouseLoadSequence) return
warehouses.value = result.records
} catch (error) {
@@ -482,7 +459,6 @@ watch(() => form.warehouseId, (warehouseId) => {
})
onMounted(() => {
void loadCustomers()
void loadWarehouses()
void loadLocations()
})
@@ -497,12 +473,6 @@ onMounted(() => {
@search="applyFilters"
/>
<el-alert v-if="customerError" type="warning" :closable="false" show-icon class="state-alert">
<template #title>所属企业列表加载失败</template>
<p>{{ customerError.message }}</p>
<ApiErrorMeta :details="customerError" />
<el-button size="small" @click="loadCustomers">重新加载所属企业</el-button>
</el-alert>
<el-alert v-if="warehouseError" type="error" :closable="false" show-icon class="state-alert">
<template #title>仓库列表加载失败</template>
<p>{{ warehouseError.message }}</p>
@@ -637,7 +607,7 @@ onMounted(() => {
<el-tag :type="statusMeta[detail.location.status].type">{{ statusMeta[detail.location.status].label }}</el-tag>
</section>
<el-descriptions :column="2" border class="detail-descriptions">
<el-descriptions-item label="所属企业">{{ customerLabel(detail.location.tenantId) }}</el-descriptions-item>
<el-descriptions-item label="资源作用域">平台公共资源</el-descriptions-item>
<el-descriptions-item label="所属仓库">{{ detail.warehouseName }}({{ detail.warehouseCode }})</el-descriptions-item>
<el-descriptions-item label="库位编码">{{ detail.location.locationCode }}</el-descriptions-item>
<el-descriptions-item label="库位名称">{{ detail.location.locationName || '—' }}</el-descriptions-item>
@@ -677,21 +647,21 @@ onMounted(() => {
>
<div v-if="drawerMode === 'edit'" class="editor-scope">
<MapPin :size="18" />
<div><strong>{{ detail?.warehouseName || selectedWarehouse?.warehouseName }}({{ detail?.warehouseCode || selectedWarehouse?.warehouseCode }})</strong><span>所属企业:{{ customerLabel(form.customerId) }} · 保存后不可跨仓移动</span></div>
<div><strong>{{ detail?.warehouseName || selectedWarehouse?.warehouseName }}({{ detail?.warehouseCode || selectedWarehouse?.warehouseCode }})</strong><span>平台公共资源 · 保存后不可跨仓移动</span></div>
</div>
<el-form-item v-if="drawerMode === 'create'" 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}) · ${customerLabel(warehouse.tenantId)}`"
:label="`${warehouse.warehouseName}(${warehouse.warehouseCode})`"
:value="String(warehouse.id)"
/>
</el-select>
</el-form-item>
<section class="form-section">
<h3>库位标识</h3>
<p class="section-description">库位编码在当前企业和仓库内唯一;编辑后不能移动到其他仓库。</p>
<p class="section-description">库位编码在当前仓库内唯一;编辑后不能移动到其他仓库。</p>
<div class="form-grid two-cols">
<el-form-item label="库位编码" prop="locationCode" required>
<el-input v-model.trim="form.locationCode" maxlength="50" placeholder="例如 A1-01" />
@@ -4,7 +4,6 @@ import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/t
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import { listPlatformTenants as listPlatformCustomers, type PlatformTenantResource as PlatformCustomerResource } from '@/modules/iam'
import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
import DataTable from '@/shared/components/DataTable.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
@@ -16,11 +15,6 @@ import {
} from '../api/platformPallets'
import PalletsView from './PalletsView.vue'
vi.mock('@/modules/iam', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/modules/iam')>()
const listPlatformTenants = vi.fn<typeof actual.listPlatformTenants>()
return { ...actual, listPlatformTenants }
})
vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: vi.fn() }))
vi.mock('../api/platformPallets', () => ({
listPlatformPallets: vi.fn(),
@@ -48,23 +42,14 @@ function createMemoryStorage(): Storage {
}
}
const customer = (id: number | string, tenantName: string): PlatformCustomerResource => ({
id, tenantCode: String(id), tenantName, status: 'ACTIVE', legalName: tenantName,
legalRepresentative: null, unifiedSocialCreditCode: null, contactName: null,
contactPhone: null, contactEmail: null, servicePackage: null, contactWechat: null,
salesOwnerName: null, paymentTermDays: null, enterpriseType: null, establishedDate: null,
registeredCapital: null, registeredCapitalCurrency: null, registeredAddress: null,
internalRemark: null, createdAt: null, approvedAt: null,
})
const warehouse: PlatformWarehouseResource = {
id: '19001', tenantId: 10001, warehouseCode: 'CN-YW-01', warehouseName: '义乌收货仓',
id: '19001', tenantId: 0, warehouseCode: 'CN-YW-01', warehouseName: '义乌收货仓',
warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null,
contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null,
}
const pallet: PlatformPalletResource = {
id: '20001', tenantId: 10001, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘',
id: '20001', tenantId: 0, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘',
capacityCbm: '1.200000', remark: '标准川字托盘', createdAt: '2026-08-04T09:00:00', updatedAt: '2026-08-04T10:00:00',
}
@@ -110,9 +95,6 @@ describe('PalletsView', () => {
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
document.body.innerHTML = ''
vi.mocked(listPlatformCustomers).mockReset().mockResolvedValue({
records: [customer(10001, '默认企业'), customer('9223372036854775806', '雪花企业')], page: 1, size: 200, total: 2, pages: 1,
})
vi.mocked(listPlatformWarehouses).mockReset().mockResolvedValue({ records: [warehouse], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listPlatformPallets).mockReset().mockResolvedValue({ records: [pallet], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformPallet).mockReset().mockResolvedValue(pallet)
@@ -132,7 +114,7 @@ describe('PalletsView', () => {
])
expect(wrapper.find('.pallet-filter-toolbar').exists()).toBe(true)
expect(fields.find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200)
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
@@ -143,7 +125,7 @@ describe('PalletsView', () => {
await loadRows(wrapper)
expect(listPlatformWarehouses).toHaveBeenCalledTimes(1)
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200)
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
@@ -234,7 +216,7 @@ describe('PalletsView', () => {
await saveButton?.trigger('click')
await flushPromises()
expect(createPlatformPallet).toHaveBeenCalledWith(10001, {
expect(createPlatformPallet).toHaveBeenCalledWith(0, {
warehouseId: 19001, palletCode: 'TP-002', palletType: '塑料托盘', capacityCbm: 1.5, remark: '蓝色标准托盘',
})
})
@@ -277,33 +259,33 @@ describe('PalletsView', () => {
expect(wrapper.text()).not.toContain('TP-001')
})
it('keeps platform warehouse and pallet queries available when customer names cannot load', async () => {
vi.mocked(listPlatformCustomers).mockRejectedValueOnce(new Error('Gateway unavailable'))
it('keeps platform warehouse and pallet queries independent from customer names', async () => {
const wrapper = await mountView()
expect(wrapper.text()).toContain('客户列表暂时无法加载')
expect(wrapper.text()).toContain('重新加载')
expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200)
expect(wrapper.text()).not.toContain('客户列表暂时无法加载')
expect(wrapper.text()).not.toContain('所属企业')
expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200)
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
})
it('shows only the pallet fields exposed by the backend contract', async () => {
const crossCustomerPallet = { ...pallet, tenantId: '9223372036854775806' }
vi.mocked(listPlatformPallets).mockResolvedValue({ records: [crossCustomerPallet], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformPallet).mockResolvedValue(crossCustomerPallet)
const publicPallet = { ...pallet, tenantId: '0' }
vi.mocked(listPlatformPallets).mockResolvedValue({ records: [publicPallet], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformPallet).mockResolvedValue(publicPallet)
const wrapper = await mountView()
await loadRows(wrapper)
expect(wrapper.findComponent(DataTable).props('data')).toEqual([crossCustomerPallet])
expect(wrapper.findComponent(DataTable).props('data')).toEqual([publicPallet])
const tableBody = wrapper.find('.el-table__body-wrapper')
const viewButton = tableBody.findAll('.pallet-row-actions button').find((button) => button.text().trim() === '查看')
await viewButton?.trigger('click')
await flushPromises()
expect(getPlatformPallet).toHaveBeenCalledWith('9223372036854775806', 19001, '20001')
expect(document.body.textContent).toContain('所属企业')
expect(document.body.textContent).toContain('雪花企业(9223372036854775806)')
expect(getPlatformPallet).toHaveBeenCalledWith(0, 19001, '20001')
expect(document.body.textContent).toContain('资源作用域')
expect(document.body.textContent).toContain('平台公共资源')
expect(document.body.textContent).not.toContain('所属企业')
expect(document.body.textContent).toContain('托盘编码')
expect(document.body.textContent).toContain('托盘类型')
expect(document.body.textContent).toContain('托盘容积')
@@ -4,7 +4,6 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
import { Boxes, Eye, Pencil, Plus, Trash2, Warehouse as WarehouseIcon } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { listPlatformTenants as listPlatformCustomers, type PlatformTenantResource as PlatformCustomerResource } from '@/modules/iam'
import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
@@ -21,9 +20,6 @@ import {
} from '../api/platformPallets'
const access = useAccessPreviewStore()
const customers = ref<PlatformCustomerResource[]>([])
const customerLoading = ref(false)
const customerError = ref<ApiErrorDetails | null>(null)
const warehouses = ref<PlatformWarehouseResource[]>([])
const warehouseLoading = ref(false)
const warehouseError = ref<ApiErrorDetails | null>(null)
@@ -140,44 +136,17 @@ function warehouseFor(warehouseId: number | string) {
return warehouses.value.find((item) => String(item.id) === String(warehouseId)) || null
}
function customerLabel(customerId: number | string) {
const customer = customers.value.find((item) => String(item.id) === String(customerId))
return customer ? `${customer.tenantName}(${customer.tenantCode || customer.id})` : '—'
}
function errorMessage(details: ApiErrorDetails) {
return details.requestId ? `${details.message}(requestId: ${details.requestId})` : details.message
}
async function loadCustomers() {
customerLoading.value = true
customerError.value = null
try {
const records: PlatformCustomerResource[] = []
let currentPage = 1
let pages = 1
do {
const result = await listPlatformCustomers({ page: currentPage, size: 200 })
records.push(...result.records)
pages = Math.max(1, result.pages)
currentPage += 1
} while (currentPage <= pages)
customers.value = records
} catch (error) {
customers.value = []
customerError.value = getApiErrorDetails(error, '所属企业列表加载失败,请稍后重试')
} finally {
customerLoading.value = false
}
}
async function loadWarehouses() {
const sequence = ++warehouseSequence
warehouses.value = []
warehouseError.value = null
warehouseLoading.value = true
try {
const result = await listPlatformWarehouses(undefined, 1, 200)
const result = await listPlatformWarehouses(1, 200)
if (sequence !== warehouseSequence) return
warehouses.value = result.records
} catch (error) {
@@ -343,7 +312,6 @@ watch(() => access.can(editorPermission.value), (canWrite) => {
if (!canWrite && editorOpen.value) editorOpen.value = false
}, { flush: 'sync' })
onMounted(() => {
void loadCustomers()
void loadWarehouses()
void loadPallets()
})
@@ -351,14 +319,6 @@ onMounted(() => {
<template>
<section class="pallet-page">
<LoadErrorState
v-if="customerError"
title="客户列表暂时无法加载"
:message="customerError.message"
:loading="customerLoading"
@retry="loadCustomers"
/>
<QueryToolbar
v-model="filters"
class="surface-panel pallet-filter-toolbar"
@@ -430,7 +390,7 @@ onMounted(() => {
<div class="detail-hero"><span><Boxes :size="22" /></span><div><h2>{{ detail.palletCode }}</h2><p>{{ detail.palletType }}</p></div></div>
<h3 class="detail-section-title">归属信息</h3>
<el-descriptions :column="1" border>
<el-descriptions-item label="所属企业">{{ customerLabel(detail.tenantId) }}</el-descriptions-item>
<el-descriptions-item label="资源作用域">平台公共资源</el-descriptions-item>
<el-descriptions-item label="所属仓库">{{ warehouseFor(detail.warehouseId)?.warehouseName || '—' }}({{ warehouseFor(detail.warehouseId)?.warehouseCode || '—' }})</el-descriptions-item>
</el-descriptions>
<h3 class="detail-section-title">托盘规格</h3>
@@ -467,7 +427,7 @@ onMounted(() => {
<div><h3 id="pallet-ownership-heading">归属仓库</h3><p>托盘创建后固定归属于所选仓库,不支持跨仓移动。</p></div>
</div>
<div v-if="form.id" class="editor-context">
<div><strong>{{ editorWarehouse?.warehouseName || '当前仓库' }}</strong><span>{{ editorWarehouse?.warehouseCode || form.warehouseId }} · 客户 {{ customerLabel(form.customerId) }}</span></div>
<div><strong>{{ editorWarehouse?.warehouseName || '当前仓库' }}</strong><span>{{ editorWarehouse?.warehouseCode || form.warehouseId }} · 平台公共资源</span></div>
</div>
<el-form-item v-else label="所属仓库" prop="warehouseId" required>
<el-select v-model="form.warehouseId" filterable placeholder="请选择所属仓库" :loading="warehouseLoading">