From c0f3a0d7948cb83c8fe995434d49c26d45e58327 Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Thu, 2 Apr 2026 15:36:11 +0800 Subject: [PATCH] update order page --- .../src/pages/orders/__tests__/index.spec.ts | 149 ++++++++++++++++++ frontend/src/pages/orders/index.vue | 71 +++++++-- 2 files changed, 203 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/orders/__tests__/index.spec.ts b/frontend/src/pages/orders/__tests__/index.spec.ts index caaf714..81e8bf5 100644 --- a/frontend/src/pages/orders/__tests__/index.spec.ts +++ b/frontend/src/pages/orders/__tests__/index.spec.ts @@ -516,4 +516,153 @@ describe('OrdersPage', () => { const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || '' expect(drawerHtml).toContain('暂无数据') }) + + // ─── P17.1 双栏布局测试 ───────────────────────────────── + + async function openDrawerWithData(options?: { rawFail?: boolean }) { + const { api } = await mountPage() + if (options?.rawFail) { + vi.mocked(api.get) + .mockResolvedValueOnce(mockOrderDetail) + .mockRejectedValueOnce(new Error('Not found')) + } else { + vi.mocked(api.get) + .mockResolvedValueOnce(mockOrderDetail) + .mockResolvedValueOnce(mockRawDetail) + } + + const buttons = Array.from(document.body.querySelectorAll('.ant-btn')) + const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看') + viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + await flushPromises() + await nextTick() + return { api } + } + + it('renders two-column layout in drawer', async () => { + await openDrawerWithData() + + const drawer = document.body.querySelector('.ant-drawer') + const row = drawer?.querySelector('.ant-row') + expect(row).toBeTruthy() + const cols = row?.querySelectorAll(':scope > .ant-col') + expect(cols?.length).toBe(2) + }) + + it('left column contains all business sections', async () => { + await openDrawerWithData() + + const drawer = document.body.querySelector('.ant-drawer') + const cols = drawer?.querySelectorAll('.ant-row > .ant-col') + const leftCol = cols?.[0] + expect(leftCol).toBeTruthy() + + const leftHtml = leftCol?.innerHTML || '' + expect(leftHtml).toContain('基本信息') + expect(leftHtml).toContain('金额信息') + expect(leftHtml).toContain('时间与地址') + expect(leftHtml).toContain('订单子项') + expect(leftHtml).toContain('扩展数据') + }) + + it('right column renders raw JSON', async () => { + await openDrawerWithData() + + const drawer = document.body.querySelector('.ant-drawer') + const cols = drawer?.querySelectorAll('.ant-row > .ant-col') + const rightCol = cols?.[1] + expect(rightCol).toBeTruthy() + + const rightHtml = rightCol?.innerHTML || '' + expect(rightHtml).toContain('原始数据') + expect(rightHtml).toContain('tmall_001') + expect(rightHtml).toContain('TRADE_FINISHED') + }) + + it('right column shows empty state when raw is null', async () => { + await openDrawerWithData({ rawFail: true }) + + const drawer = document.body.querySelector('.ant-drawer') + const cols = drawer?.querySelectorAll('.ant-row > .ant-col') + const rightCol = cols?.[1] + expect(rightCol).toBeTruthy() + + const rightHtml = rightCol?.innerHTML || '' + expect(rightHtml).toContain('暂无原始数据') + }) + + it('right column copy button calls clipboard API', async () => { + const writeTextMock = vi.fn().mockResolvedValue(undefined) + Object.assign(navigator, { + clipboard: { writeText: writeTextMock }, + }) + + await openDrawerWithData() + + const drawer = document.body.querySelector('.ant-drawer') + const cols = drawer?.querySelectorAll('.ant-row > .ant-col') + const rightCol = cols?.[1] + const copyBtn = rightCol?.querySelector('.ant-btn') + expect(copyBtn).toBeTruthy() + + copyBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + await flushPromises() + await nextTick() + + expect(writeTextMock).toHaveBeenCalledWith( + JSON.stringify(mockRawDetail.raw, null, 2), + ) + }) + + it('left column does not contain raw data section', async () => { + await openDrawerWithData() + + const drawer = document.body.querySelector('.ant-drawer') + const cols = drawer?.querySelectorAll('.ant-row > .ant-col') + const leftCol = cols?.[0] + const leftHtml = leftCol?.innerHTML || '' + + expect(leftHtml).not.toContain('tmall_001') + expect(leftHtml).not.toContain('TRADE_FINISHED') + }) + + it('table copy button is before platform_order_id text', async () => { + await mountPage() + + const table = document.body.querySelector('.ant-table') + const cells = table?.querySelectorAll('td') || [] + let targetCell: Element | null = null + cells.forEach((cell) => { + if (cell.textContent?.includes('ORD-20260101-001')) { + targetCell = cell + } + }) + expect(targetCell).toBeTruthy() + + const span = targetCell!.querySelector('.inline-flex') + expect(span).toBeTruthy() + const children = span?.children + // First child is copy icon, second is text + expect(children?.[0]?.classList.contains('copy-icon-stub')).toBe(true) + expect(children?.[1]?.textContent).toContain('ORD-20260101-001') + }) + + it('table copy button calls clipboard API', async () => { + const writeTextMock = vi.fn().mockResolvedValue(undefined) + Object.assign(navigator, { + clipboard: { writeText: writeTextMock }, + }) + + await mountPage() + + const table = document.body.querySelector('.ant-table') + const copyIcon = table?.querySelector('.copy-icon-stub') + expect(copyIcon).toBeTruthy() + + copyIcon?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + await flushPromises() + await nextTick() + + expect(writeTextMock).toHaveBeenCalledWith('ORD-20260101-001') + }) }) diff --git a/frontend/src/pages/orders/index.vue b/frontend/src/pages/orders/index.vue index 56d5172..5e95f1f 100644 --- a/frontend/src/pages/orders/index.vue +++ b/frontend/src/pages/orders/index.vue @@ -131,6 +131,16 @@ async function handleCopyOrderId(platformOrderId: string) { } } +async function handleCopyRaw() { + if (!rawDetail.value?.raw) return + try { + await navigator.clipboard.writeText(JSON.stringify(rawDetail.value.raw, null, 2)) + message.success('已复制到剪贴板') + } catch { + message.error('复制失败') + } +} + async function handleViewDetail(record: { id: number }) { const currentRequestId = ++detailRequestId drawerVisible.value = true @@ -244,7 +254,13 @@ async function handleViewDetail(record: { id: number }) { {{ store.storeMap.get(record.store_id) || record.store_id }}