diff --git a/AGENTS.md b/AGENTS.md index 598fd78c..ba27cd7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ | 国内整柜业务 | `modules/domestic-containers/` | Backend `warehouse/domesticcontainer/`;OpenAPI `warehouse/src/main/resources/openapi/domestic-container-v1.yaml`;核对入库、出库、库存详情和幂等 | | 客户运营指标与操作日志 | 客户详情指标在 `modules/customers/`;日志组件与 API 在 `modules/audit/` | 统计联查 Backend `firstleg/customerstatistics/`;审计查询联查 `audit-service/`,列表与详情使用 `platform:audit:list` | | 平台用户与 RBAC | `modules/iam/`、`core/access/` | `auth/`、`iam/` | -| 平台消息管理、模板与我的消息 | `modules/notifications/` | Backend `iam/src/main/java/com/baiyue/iam/notification/`;接口契约 `iam/src/main/resources/openapi/notification-v1.yaml`,触发消息的业务事件仍联查对应业务服务 | +| 平台消息管理与模板 | `modules/notifications/` | 仅通过顶部“消息中心”进入,不进入左侧菜单;“我的消息”仅属于 Client。Backend `iam/src/main/java/com/baiyue/iam/notification/`;接口契约 `iam/src/main/resources/openapi/notification-v1.yaml`,触发消息的业务事件仍联查对应业务服务 | | 导航与原型菜单图标 | `core/navigation/adminNavigation.ts`、`core/navigation/prototypeMenuIcons.ts` | 菜单结构、权限与路由同步核对;已有原型图标优先使用映射资源 | | 平台仓库与仓储资源 | `modules/warehouses/`、`modules/warehouse-resources/` | `warehouse/` | | 订单列表、详情与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;费用入口 `modules/unified-orders/components/InboundFeeDetailManager.vue`、`modules/unified-orders/api/inboundFeeDetails.ts` | `order/`、`firstleg/`;费用来源审核发布联查 `firstleg/AGENTS.md` | diff --git a/src/app/layouts/AppLayout.test.ts b/src/app/layouts/AppLayout.test.ts index af7cfc20..3d9fa373 100644 --- a/src/app/layouts/AppLayout.test.ts +++ b/src/app/layouts/AppLayout.test.ts @@ -55,6 +55,8 @@ describe('AppLayout', () => { expect(wrapper.text()).toContain('消息管理') expect(wrapper.text()).toContain('模板管理') expect(wrapper.text()).not.toContain('我的消息') + const notificationTrigger = wrapper.get('.notification-trigger') + expect(notificationTrigger.find('.notification-count').exists()).toBe(false) }) it('hides the shell heading when the route opts out', async () => { diff --git a/src/app/layouts/AppLayout.vue b/src/app/layouts/AppLayout.vue index 3b65b1ee..b5c36aeb 100644 --- a/src/app/layouts/AppLayout.vue +++ b/src/app/layouts/AppLayout.vue @@ -168,7 +168,9 @@ watch(() => auth.user?.id || auth.user?.account || '', (userId, previousUserId) diff --git a/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue b/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue index cf5727dc..1038af26 100644 --- a/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue +++ b/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue @@ -9,14 +9,106 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue' import StatusBadge from '@/shared/components/StatusBadge.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { getDomesticContainerInventory } from '../api/domesticContainers' -import type { InventoryDetail, TransactionRecord } from '../types' +import type { InventoryDetail, InventoryStatus, TransactionRecord } from '../types' + +const route = useRoute() +const router = useRouter() +const loading = ref(false) +const error = ref(null) +const detail = ref(null) + +const transactionColumns: DataTableColumn[] = [ + { prop: 'businessType', label: '业务类型', width: 110, slotName: 'businessType' }, + { prop: 'orderNo', label: '单号', minWidth: 210 }, + { prop: 'quantity', label: '数量', width: 110, align: 'right', formatter: (row: TransactionRecord) => `${row.quantity} 箱` }, + { prop: 'operationTime', label: '操作时间', width: 170, formatter: (row: TransactionRecord) => displayTime(row.operationTime) }, + { prop: 'remark', label: '备注', minWidth: 180, formatter: (row: TransactionRecord) => row.remark || '—' }, +] + +function inventoryStatus(status: InventoryStatus) { + return ({ IN_STOCK: '在库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUND: '已出库' } as const)[status] +} + +function displayTime(value: string) { + return value ? value.replace('T', ' ').slice(0, 16) : '—' +} + +function back() { + void router.push({ name: 'domestic-containers' }) +} + +async function load() { + loading.value = true + error.value = null + try { + detail.value = await getDomesticContainerInventory(String(route.params.inventoryId)) + } catch (cause) { + error.value = getApiErrorDetails(cause, '整柜库存详情加载失败') + } finally { + loading.value = false + } +} -const route = useRoute(); const router = useRouter(); const loading = ref(false); const error = ref(null); const detail = ref(null) -const transactionColumns: DataTableColumn[] = [{ prop: 'businessType', label: '类型', width: 100, slotName: 'businessType' }, { prop: 'orderNo', label: '单号', minWidth: 160 }, { prop: 'quantity', label: '数量', width: 100, align: 'right', formatter: (row: TransactionRecord) => `${row.quantity} 箱` }, { prop: 'operationTime', label: '操作时间', minWidth: 170, formatter: (row: TransactionRecord) => row.operationTime.replace('T', ' ').slice(0, 16) }, { prop: 'remark', label: '备注', minWidth: 180, formatter: (row: TransactionRecord) => row.remark || '—' }] -function inventoryStatus(status: string) { return ({ IN_STOCK: '在库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUND: '已出库' } as Record)[status] || status } -async function load() { loading.value = true; error.value = null; try { detail.value = await getDomesticContainerInventory(String(route.params.inventoryId)) } catch (cause) { error.value = getApiErrorDetails(cause, '整柜库存详情加载失败') } finally { loading.value = false } } -function back() { void router.push({ name: 'domestic-containers' }) } onMounted(load) - - + + + + diff --git a/src/modules/domestic-containers/pages/DomesticContainersView.vue b/src/modules/domestic-containers/pages/DomesticContainersView.vue index 87508815..1ac6dc7c 100644 --- a/src/modules/domestic-containers/pages/DomesticContainersView.vue +++ b/src/modules/domestic-containers/pages/DomesticContainersView.vue @@ -68,4 +68,4 @@ watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWa
入库箱数{{ summary.inboundBoxes }}
已出库箱数{{ summary.outboundBoxes }}
当前库存{{ summary.availableBoxes }}
累计入库体积{{ summary.inboundVolumeCbm.toFixed(2) }} m³
- + diff --git a/src/modules/notifications/PROTOTYPE_MATRIX.md b/src/modules/notifications/PROTOTYPE_MATRIX.md index d51500cd..6277389e 100644 --- a/src/modules/notifications/PROTOTYPE_MATRIX.md +++ b/src/modules/notifications/PROTOTYPE_MATRIX.md @@ -14,9 +14,7 @@ | 模板管理 | 新增模板 | `modal-msg-template-create`,约 760px 居中弹框 | 760px `AppDialog`;CODE、名称、分类、标题/正文、变量白名单、预览;保存为 DRAFT | 仅支持 `IN_APP`,不展示真实渠道选择 | | 模板管理 | 编辑模板 | 原型仅有编辑按钮,未定义容器 | 复用同尺寸模板弹框,仅 DRAFT 可编辑,CODE 不可变并携带 version | 冻结契约明确只允许编辑草稿,复用表单避免重复交互 | | 模板管理 | 状态流转 | 原型未定义 | DRAFT 可激活,ACTIVE 可禁用,DISABLED 无操作 | 严格执行 `DRAFT -> ACTIVE -> DISABLED`;禁用后不可恢复 | -| 我的消息 | 列表/查询 | `page-msg-my` 页面内表格 | `/notifications/mine` 查询当前平台员工收件箱,支持状态、分类筛选;操作列固定第一列 | 使用 `platform` app,服务端从登录上下文隔离,不传企业 ID | -| 我的消息 | 查看/标为已读 | 原型仅有查看按钮;支持单条和全部已读 | 680px 居中详情弹框;打开未读详情自动标记;支持单条和全部标为已读 | 对接 detail/read/read-all;变更后刷新顶部未读数 | -| 我的消息 | 清空已读 | 原型列表按钮 | 不展示 | 首期没有删除或清空已读 API,不能展示可点击后报错的入口 | -| 顶部入口 | 未读入口/三页跳转 | `topMessageMenu` 顶部下拉 | 有收件箱权限时加载真实未读数;按任务/模板权限展示管理入口,始终提供“我的消息” | 对接 `/platform/v1/notifications/unread-count`;加载失败显示异常标记并保留可访问入口 | +| 我的消息 | 页面归属 | 原型保留 `page-msg-my` 历史页面 | Admin 不实现、不注册路由 | 产品确认“我的消息”仅属于企业客户端 | +| 顶部入口 | 管理入口 | `topMessageMenu` 顶部下拉,左侧无消息菜单 | 按任务/模板权限展示“消息管理”“模板管理” | Admin 仅保留后台管理能力,不展示个人收件箱与未读数 | 接口依赖:所有响应经 `{code,data}` 解包;分页统一读取 `records/page/size/total/pages`;响应中的 `int64` ID 在前端保持字符串。写请求中的企业、用户和成员 ID 也在类型适配边界保持数字字符串,避免 JavaScript 大整数精度损失,由服务端 Long DTO 解析。 diff --git a/src/modules/notifications/api/notificationCenter.test.ts b/src/modules/notifications/api/notificationCenter.test.ts index 4b4a5de3..f9418636 100644 --- a/src/modules/notifications/api/notificationCenter.test.ts +++ b/src/modules/notifications/api/notificationCenter.test.ts @@ -4,13 +4,8 @@ import { cancelTask, changeTemplateStatus, createNotification, - getInboxDetail, - getUnreadCount, - listInbox, listTasks, listTemplates, - markAllInboxRead, - markInboxRead, retryTask, updateTemplate, } from './notificationCenter' @@ -21,38 +16,12 @@ vi.mock('@/shared/api/http', () => ({ })) const envelope = (data: T) => ({ code: '200', data }) -const inbox = { id: '9007199254740993', category: 'ORDER', title: '已发货', status: 'UNREAD' as const, sentAt: '2026-08-12T10:00:00Z', readAt: null } const template = { id: 12, templateCode: 'ORDER_SENT', templateName: '订单已发货', category: 'ORDER', channel: 'IN_APP' as const, titleTemplate: '已发货', bodyTemplate: '正文', variableWhitelist: [], status: 'DRAFT' as const, version: 1 } const task = { id: 13, taskNo: 'NT-13', category: 'ORDER', title: '已发货', status: 'CREATED' as const, recipientCount: 2, scheduledAt: null, sentAt: null, duplicated: false } describe('notificationCenter API', () => { beforeEach(() => vi.clearAllMocks()) - it('uses the platform inbox paths and normalizes envelope pagination and ids', async () => { - vi.mocked(http.get).mockResolvedValueOnce(envelope({ records: [inbox], page: '1', size: '20', total: '1', pages: '1' })) - const result = await listInbox({ page: 1, size: 20, status: 'UNREAD' }) - expect(http.get).toHaveBeenCalledWith('/platform/v1/notifications', { params: { page: 1, size: 20, status: 'UNREAD' } }) - expect(result).toMatchObject({ page: 1, size: 20, total: 1, pages: 1 }) - expect(result.records[0]?.id).toBe(String(inbox.id)) - - vi.mocked(http.get).mockResolvedValueOnce(envelope({ unreadCount: '7' })) - expect(await getUnreadCount()).toBe(7) - expect(http.get).toHaveBeenLastCalledWith('/platform/v1/notifications/unread-count') - }) - - it('gets and marks inbox messages without a request body', async () => { - vi.mocked(http.get).mockResolvedValueOnce(envelope({ ...inbox, body: '正文', sourceType: null, sourceId: null })) - vi.mocked(http.put) - .mockResolvedValueOnce(envelope({ ...inbox, body: '正文', sourceType: null, sourceId: null })) - .mockResolvedValueOnce(envelope({ updatedCount: '3' })) - await getInboxDetail('10/20') - await markInboxRead('10/20') - expect(await markAllInboxRead()).toBe(3) - expect(http.get).toHaveBeenCalledWith('/platform/v1/notifications/10%2F20') - expect(http.put).toHaveBeenNthCalledWith(1, '/platform/v1/notifications/10%2F20/read') - expect(http.put).toHaveBeenNthCalledWith(2, '/platform/v1/notifications/read-all') - }) - it('posts the frozen create-notification DTO unchanged', async () => { vi.mocked(http.post).mockResolvedValueOnce(envelope(task)) const payload = { tenantId: '0', templateCode: 'ORDER_SENT', recipientType: 'PLATFORM_ROLE' as const, roleCode: 'PLATFORM_ADMIN', variables: {}, scheduledAt: null, idempotencyKey: 'notification-1' } diff --git a/src/modules/notifications/api/notificationCenter.ts b/src/modules/notifications/api/notificationCenter.ts index 4b71a4b8..3c03258b 100644 --- a/src/modules/notifications/api/notificationCenter.ts +++ b/src/modules/notifications/api/notificationCenter.ts @@ -1,34 +1,20 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' -import type { CreateNotificationRequest, CreateTemplateRequest, InboxDetail, InboxItem, InboxStatus, NotificationTask, NotificationTaskStatus, NotificationTemplate, PageResult, TemplateStatus, UpdateTemplateRequest } from '../types' +import type { CreateNotificationRequest, CreateTemplateRequest, NotificationTask, NotificationTaskStatus, NotificationTemplate, PageResult, TemplateStatus, UpdateTemplateRequest } from '../types' type RawId = string | number type RawPage = Omit, 'page' | 'size' | 'total' | 'pages'> & { page: RawId; size: RawId; total: RawId; pages: RawId } -type RawInbox = Omit & { id: RawId } -type RawInboxDetail = Omit & { id: RawId } type RawTemplate = Omit & { id: RawId } type RawTask = Omit & { id: RawId } -const inboxPath = '/platform/v1/notifications' +const notificationPath = '/platform/v1/notifications' const templatePath = '/platform/v1/notification-templates' const taskPath = '/platform/v1/notification-tasks' const id = (value: RawId) => String(value) const page = (value: RawPage): PageResult => ({ ...value, page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) }) -const inbox = (value: RawInbox): InboxItem => ({ ...value, id: id(value.id) }) -const inboxDetail = (value: RawInboxDetail): InboxDetail => ({ ...value, id: id(value.id) }) const template = (value: RawTemplate): NotificationTemplate => ({ ...value, id: id(value.id) }) const task = (value: RawTask): NotificationTask => ({ ...value, id: id(value.id) }) -export async function listInbox(query: { page: number; size: number; status?: InboxStatus; category?: string }) { - const response = await http.get>, ApiEnvelope>>(inboxPath, { params: query }) - const data = unwrapApiResponse(response) - return page({ ...data, records: data.records.map(inbox) }) -} -export async function getUnreadCount() { const response = await http.get, ApiEnvelope<{ unreadCount: RawId }>>(`${inboxPath}/unread-count`); return Number(unwrapApiResponse(response).unreadCount) } -export async function getInboxDetail(messageId: string) { const response = await http.get, ApiEnvelope>(`${inboxPath}/${encodeURIComponent(messageId)}`); return inboxDetail(unwrapApiResponse(response)) } -export async function markInboxRead(messageId: string) { const response = await http.put, ApiEnvelope>(`${inboxPath}/${encodeURIComponent(messageId)}/read`); return inboxDetail(unwrapApiResponse(response)) } -export async function markAllInboxRead() { const response = await http.put, ApiEnvelope<{ updatedCount: RawId }>>(`${inboxPath}/read-all`); return Number(unwrapApiResponse(response).updatedCount) } - -export async function createNotification(payload: CreateNotificationRequest) { const response = await http.post, ApiEnvelope>(inboxPath, payload); return task(unwrapApiResponse(response)) } +export async function createNotification(payload: CreateNotificationRequest) { const response = await http.post, ApiEnvelope>(notificationPath, payload); return task(unwrapApiResponse(response)) } export async function listTemplates(query: { page: number; size: number; templateCode?: string; category?: string; title?: string; status?: TemplateStatus }) { const response = await http.get>, ApiEnvelope>>(templatePath, { params: query }); const data = unwrapApiResponse(response); return page({ ...data, records: data.records.map(template) }) } export async function createTemplate(payload: CreateTemplateRequest) { const response = await http.post, ApiEnvelope>(templatePath, payload); return template(unwrapApiResponse(response)) } export async function updateTemplate(templateId: string, payload: UpdateTemplateRequest) { const response = await http.put, ApiEnvelope>(`${templatePath}/${encodeURIComponent(templateId)}`, payload); return template(unwrapApiResponse(response)) } diff --git a/src/modules/notifications/notifications.test.ts b/src/modules/notifications/notifications.test.ts index fcb5b021..bbfc838f 100644 --- a/src/modules/notifications/notifications.test.ts +++ b/src/modules/notifications/notifications.test.ts @@ -27,14 +27,12 @@ describe('notification center contract UI', () => { it.each([ './pages/MessageManagementView.vue', './pages/MessageTemplatesView.vue', - './pages/MyMessagesView.vue', ])('keeps actions as the first table column in %s', (path) => { const content = source(path) expect(content).toMatch(/const columns:[^=]+\s*=\s*\[\{key:'actions',label:'操作'/) }) it('does not expose unsupported clear-read or disabled-template restore actions', () => { - expect(source('./pages/MyMessagesView.vue')).not.toContain('清空已读') const templates = source('./pages/MessageTemplatesView.vue') expect(templates).toContain("status!=='DISABLED'") expect(templates).toContain("status==='DRAFT'?'激活':'禁用'") diff --git a/src/modules/notifications/pages/MyMessagesView.test.ts b/src/modules/notifications/pages/MyMessagesView.test.ts deleted file mode 100644 index 10696372..00000000 --- a/src/modules/notifications/pages/MyMessagesView.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { readFileSync } from 'node:fs' -import { resolve } from 'node:path' -import { describe, expect, it } from 'vitest' - -const source = readFileSync(resolve(process.cwd(), 'src/modules/notifications/pages/MyMessagesView.vue'), 'utf8') - -describe('MyMessagesView permissions', () => { - it('keeps inbox viewing separate from read operations', () => { - expect(source).toContain("access.can('platform:notification:read')") - expect(source).toContain("access.can('platform:notification:read-all')") - expect(source).toContain("detail.value.status==='UNREAD'&&canMarkRead.value") - expect(source).toContain('v-if="canMarkAllRead"') - expect(source).toContain("v-if=\"canMarkRead&&(row as InboxItem).status==='UNREAD'\"") - }) -}) diff --git a/src/modules/notifications/pages/MyMessagesView.vue b/src/modules/notifications/pages/MyMessagesView.vue deleted file mode 100644 index fa918943..00000000 --- a/src/modules/notifications/pages/MyMessagesView.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/src/modules/notifications/types.ts b/src/modules/notifications/types.ts index 48c36f8c..3b2720f1 100644 --- a/src/modules/notifications/types.ts +++ b/src/modules/notifications/types.ts @@ -1,11 +1,8 @@ -export type InboxStatus = 'UNREAD' | 'READ' export type TemplateStatus = 'DRAFT' | 'ACTIVE' | 'DISABLED' export type NotificationTaskStatus = 'CREATED' | 'SCHEDULED' | 'DISPATCHING' | 'RETRYING' | 'SENT' | 'DEAD' | 'CANCELLED' export type RecipientType = 'USER' | 'MEMBERSHIP' | 'TENANT_ROLE' | 'TENANT_ALL' | 'PLATFORM_ROLE' export interface PageResult { records: T[]; page: number; size: number; total: number; pages: number } -export interface InboxItem { id: string; category: string; title: string; status: InboxStatus; sentAt: string; readAt: string | null } -export interface InboxDetail extends InboxItem { body: string; sourceType: string | null; sourceId: string | null } export interface NotificationTemplate { id: string; templateCode: string; templateName: string; category: string; channel: 'IN_APP'; titleTemplate: string; bodyTemplate: string; variableWhitelist: string[]; status: TemplateStatus; version: number } export interface NotificationTask { id: string; taskNo: string; category: string; title: string; status: NotificationTaskStatus; recipientCount: number; scheduledAt: string | null; sentAt: string | null; duplicated: boolean } diff --git a/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts b/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts index 65aff93a..d31f7406 100644 --- a/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts +++ b/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts @@ -117,13 +117,13 @@ describe('PalletsView', () => { expect(palletsSource).toContain('stripe') expect(palletsSource).toContain('size="small" plain type="primary"') expect(palletsSource).toMatch(/size="small"\s+plain\s+type="danger"/) - expect(palletsSource).toContain('') + expect(palletsSource).not.toContain('') expect(palletsSource).toContain('') expect(palletsSource).toContain('') expect(palletsSource).toContain('@media (max-width: 720px)') }) - it('uses the prototype-sized centered editor while retaining the detail drawer', () => { - expect(palletsSource).toContain(' { + expect(palletsSource).not.toContain(' { const wrapper = await mountView() const bodyRows = wrapper.findAll('.el-table__body-wrapper tbody tr') - expect(bodyRows[0]?.text()).toContain('查看') expect(bodyRows[0]?.text()).not.toContain('编辑') expect(bodyRows[1]?.text()).toContain('编辑') }) @@ -277,7 +276,7 @@ describe('PalletsView', () => { inputNumbers[0]?.vm.$emit('update:modelValue', 1.5) inputNumbers[1]?.vm.$emit('update:modelValue', 10) await wrapper.find('input[placeholder="例如 黄色"]').setValue('黑色') - await wrapper.find('textarea[placeholder="可填写其他说明"]').setValue('标准托盘') + await wrapper.find('input[placeholder="选填"]').setValue('标准托盘') const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === '保存托盘') await saveButton?.trigger('click') await flushPromises() @@ -287,7 +286,7 @@ describe('PalletsView', () => { }) }) - it('groups the pallet editor into clear ownership and specification sections', async () => { + it('uses a compact three-row two-column editor', async () => { const wrapper = await mountView() const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘') await createButton?.trigger('click') @@ -296,9 +295,9 @@ describe('PalletsView', () => { const editorDialog = document.body.querySelector('.pallet-editor-dialog') expect(editorDialog?.textContent).toContain('新增托盘') expect(editorDialog?.querySelector('.el-dialog__title')?.textContent).toBe('新增托盘') - expect(document.body.querySelectorAll('.editor-section')).toHaveLength(2) - expect(document.body.querySelector('#pallet-ownership-heading')?.textContent).toBe('归属仓库') - expect(document.body.querySelector('#pallet-spec-heading')?.textContent).toBe('托盘规格') + expect(document.body.querySelectorAll('.editor-field-grid')).toHaveLength(3) + expect(document.body.querySelector('.editor-section')).toBeNull() + expect(document.body.querySelector('.editor-context')).toBeNull() expect(document.body.querySelector('.pallet-editor-footer')?.textContent).toContain('为必填项') }) @@ -309,7 +308,7 @@ describe('PalletsView', () => { const actions = wrapper.find('.pallet-row-actions').text() expect(actions).not.toContain('编辑') expect(actions).not.toContain('删除') - expect(actions).toContain('查看') + expect(actions).not.toContain('查看') }) it('shows a recoverable list failure instead of demo data', async () => { @@ -333,59 +332,22 @@ describe('PalletsView', () => { }) }) - it('shows only the pallet fields exposed by the backend contract', async () => { + it('keeps the list limited to fields exposed by the backend contract', async () => { 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([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(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('托盘容积') - expect(document.body.textContent).toContain('归属信息') - expect(document.body.textContent).toContain('托盘规格') - expect(document.body.textContent).toContain('记录信息') - expect(document.body.textContent).not.toContain('企业 ID') - expect(document.body.textContent).not.toContain('租户') - expect(document.body.textContent).not.toContain('托盘状态') - expect(document.body.textContent).not.toContain('最大承重') - }) - - it('keeps the latest detail when an older pallet request finishes last', async () => { - const secondPallet = { ...pallet, id: '20002', palletCode: 'TP-002' } - let resolveFirst!: (value: PlatformPalletResource) => void - let resolveSecond!: (value: PlatformPalletResource) => void - vi.mocked(getPlatformPallet).mockImplementation((_warehouseId, palletId) => new Promise((resolve) => { - if (String(palletId) === String(pallet.id)) resolveFirst = resolve - else resolveSecond = resolve - })) - const wrapper = await mountView() - const vm = wrapper.vm as unknown as { - detailOpen: boolean - detail: PlatformPalletResource | null - openDetail(row: PlatformPalletResource): Promise - } - - void vm.openDetail(pallet) - vm.detailOpen = false - await wrapper.vm.$nextTick() - void vm.openDetail(secondPallet) - resolveSecond(secondPallet) - await flushPromises() - resolveFirst(pallet) - await flushPromises() - - expect(vm.detail?.palletCode).toBe('TP-002') + const tableText = wrapper.find('.data-table').text() + expect(tableText).toContain('托盘编码') + expect(tableText).toContain('托盘类型') + expect(tableText).toContain('托盘容积') + expect(tableText).not.toContain('所属企业') + expect(tableText).not.toContain('企业 ID') + expect(tableText).not.toContain('租户') + expect(tableText).not.toContain('托盘状态') + expect(tableText).not.toContain('最大承重') }) it('keeps the latest pallet selected for editing', async () => { diff --git a/src/modules/warehouse-resources/pallets/pages/PalletsView.vue b/src/modules/warehouse-resources/pallets/pages/PalletsView.vue index fd7b17f8..fe0c35d4 100644 --- a/src/modules/warehouse-resources/pallets/pages/PalletsView.vue +++ b/src/modules/warehouse-resources/pallets/pages/PalletsView.vue @@ -1,16 +1,14 @@