From 941b7b11f33e581af513bd7edad20f238492c2fa Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Fri, 28 Aug 2026 10:24:22 +0800 Subject: [PATCH] feat(#122): add SYB inner-code admin workflow --- web/src/api/goauto/syb-inner-codes.js | 13 +++ web/src/router/index.js | 13 +++ .../views/goauto/syb-inner-codes/index.vue | 101 ++++++++++++++++++ web/tests/e2e/syb-inner-codes.spec.ts | 50 +++++++++ 4 files changed, 177 insertions(+) create mode 100644 web/src/api/goauto/syb-inner-codes.js create mode 100644 web/src/views/goauto/syb-inner-codes/index.vue create mode 100644 web/tests/e2e/syb-inner-codes.spec.ts diff --git a/web/src/api/goauto/syb-inner-codes.js b/web/src/api/goauto/syb-inner-codes.js new file mode 100644 index 0000000..3302d57 --- /dev/null +++ b/web/src/api/goauto/syb-inner-codes.js @@ -0,0 +1,13 @@ +import request from '@/utils/request' + +const base = '/api/admin/v1/syb-inner-codes' +export function listSybInnerCodes(params) { return request({ url: base, method: 'get', params }) } +export function getSybInnerCode(id) { return request({ url: `${base}/${id}`, method: 'get' }) } +export function importSybInnerCodes(file, requestId) { const data = new FormData(); data.append('file', file); data.append('requestId', requestId); return request({ url: `${base}/import`, method: 'post', data, headers: { 'Content-Type': 'multipart/form-data' }}) } +export function deleteSybInnerCodes(data) { return request({ url: `${base}/batch-delete`, method: 'post', data }) } +export function previewSybInnerCodeApply(ids) { return request({ url: `${base}/apply-preview`, method: 'post', data: { ids }}) } +export function applySybInnerCodes(data) { return request({ url: `${base}/apply`, method: 'post', data }) } +export function getSybInnerCodeMatchJob(id) { return request({ url: `${base}/match-jobs/${id}`, method: 'get' }) } +export function getSybInnerCodeApplyBatch(id) { return request({ url: `${base}/apply-batches/${id}`, method: 'get' }) } +export function recheckSybInnerCode(id) { return request({ url: `${base}/${id}/recheck`, method: 'post', data: {}}) } +export function rematchSybInnerCodes(data) { return request({ url: `${base}/rematch`, method: 'post', data }) } diff --git a/web/src/router/index.js b/web/src/router/index.js index 5a2b3cd..55e0d82 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -158,6 +158,19 @@ export const constantRoutes = [ } ] }, + { + path: '/syb-inner-codes', + component: Layout, + redirect: '/syb-inner-codes/index', + children: [ + { + path: 'index', + component: () => import('@/views/goauto/syb-inner-codes/index'), + name: 'GoAutoSybInnerCodes', + meta: { title: '档口入库码', icon: 'list' } + } + ] + }, { path: '/collection-rules', component: Layout, diff --git a/web/src/views/goauto/syb-inner-codes/index.vue b/web/src/views/goauto/syb-inner-codes/index.vue new file mode 100644 index 0000000..0173e16 --- /dev/null +++ b/web/src/views/goauto/syb-inner-codes/index.vue @@ -0,0 +1,101 @@ + + + + + 档口入库码导入 Excel 后自动匹配 SYB 商品;确认后逐件回写,结果不明确时只读复核。 + + 选择 Excel + 导入 + + + 清除 + 搜索 + + 回写({{ applyEligibleCount }}) + 删除({{ deleteEligibleCount }}) + + 已选择:{{ selectedFile.name }}({{ fileSize(selectedFile.size) }})请选择“标签入库码映射”Excel 文件,最大 10MB。 + + 重新加载 + {{ activeJob.errorMessage }}关闭浏览器不会中断自动匹配。 + 远端写入全局串行,结果不明确不会自动重试。 + + + + + + {{ code }}共 {{ row.items.length }} 个 + {{ row.stall || '—' }} + + {{ row.plan.sybSpec || '已读取,规格为空' }}{{ sybSpecPlaceholder(row.status) }} + {{ statusMeta(row.status).label }}{{ row.resultMessage }} + 详情重新匹配只读复核 + + + + + + + + 业务记录数{{ applyDialog.preview.records || 0 }}入库码总数{{ applyDialog.preview.inboundCodes || 0 }}预计占位明细数{{ applyDialog.preview.placeholderDetails || 0 }}替换旧码数{{ applyDialog.preview.replaceOldCodes || 0 }} + + + 取消回写 {{ applyDialog.preview.records || 0 }} 条 + + + + + 选中数量{{ selected.length }}可删除数量{{ deleteEligibleCount }}阻塞数量{{ deleteBlocked.length }} + + 取消删除 {{ deleteEligibleCount }} 条 + + + + {{ detail.item.businessDate }}{{ detail.item.orderNumber }}{{ detail.item.stall || '—' }}{{ detail.item.shopName || '—' }}{{ detail.item.sourceSkuRaw || '—' }}{{ detail.item.specRaw }}{{ detail.item.plan?.sybSpec || '未形成计划' }}{{ statusMeta(detail.item.status).label }}{{ detail.item.resultMessage || '—' }}全部入库码 + + + + + + + + diff --git a/web/tests/e2e/syb-inner-codes.spec.ts b/web/tests/e2e/syb-inner-codes.spec.ts new file mode 100644 index 0000000..2e1352b --- /dev/null +++ b/web/tests/e2e/syb-inner-codes.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test' + +test('档口入库码列表、回写确认与物理删除门禁符合已确认原型', async({ page, context }) => { + await page.setViewportSize({ width: 1920, height: 1080 }) + await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }]) + const items = [ + { id: 1, businessDate: '2026-08-28', orderNumber: 'ORDER-001', stall: 'A档#101', specRaw: '黑色,L', shopName: '内部店铺', status: 'ready', resultMessage: '唯一匹配,等待确认回写', items: [{ ordinal: 1, sourceRow: 2, code: 'IN-001' }, { ordinal: 2, sourceRow: 3, code: 'IN-002' }], plan: { sybSpec: '黑色,L' }}, + { id: 2, businessDate: '2026-08-28', orderNumber: 'ORDER-002', stall: 'B档#202', specRaw: '白色,M', shopName: '内部店铺', status: 'needs_check', resultMessage: '写入响应不明确', items: [{ ordinal: 1, sourceRow: 4, code: 'IN-003' }], plan: { sybSpec: '白色,M' }}, + ] + + await page.route('**/api/**', async route => { + const url = new URL(route.request().url()) + if (url.pathname.startsWith('/src/api/')) return route.continue() + if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] }}}) + if (url.pathname.endsWith('/api/admin/v1/syb-inner-codes/apply-preview')) return route.fulfill({ json: { code: 200, data: { records: 1, inboundCodes: 2, placeholderDetails: 1, replaceOldCodes: 1, blocked: [] }}}) + if (url.pathname.endsWith('/api/admin/v1/syb-inner-codes')) return route.fulfill({ json: { code: 200, data: { items, total: items.length, page: 1, pageSize: 100 }}}) + return route.fulfill({ json: { code: 200, data: {} }}) + }) + + await page.goto('http://localhost:9527/#/syb-inner-codes/index') + await expect(page.getByRole('heading', { name: '档口入库码' })).toBeVisible() + await expect(page.getByPlaceholder('订单号或入库码')).toBeVisible() + await expect(page.getByRole('columnheader', { name: '订单号' })).toBeVisible() + await expect(page.getByRole('columnheader', { name: '入库码' })).toBeVisible() + await expect(page.getByRole('columnheader', { name: '档口' })).toBeVisible() + await expect(page.getByRole('columnheader', { name: '本地规格' })).toBeVisible() + await expect(page.getByRole('columnheader', { name: 'SYB 规格' })).toBeVisible() + await expect(page.getByRole('columnheader', { name: '店铺' })).toHaveCount(0) + await expect(page.getByRole('columnheader', { name: '单件码' })).toHaveCount(0) + await expect(page.getByText('IN-001', { exact: true })).toBeVisible() + + const rows = page.locator('.el-table__body-wrapper tbody tr') + await rows.nth(0).locator('.el-checkbox').click() + await page.getByRole('button', { name: '回写(1)' }).click() + await expect(page.getByText('业务记录数', { exact: true })).toBeVisible() + await expect(page.getByText('入库码总数', { exact: true })).toBeVisible() + await expect(page.getByText('预计占位明细数', { exact: true })).toBeVisible() + await expect(page.getByText('替换旧码数', { exact: true })).toBeVisible() + await expect(page.getByRole('button', { name: '回写 1 条' })).toBeEnabled() + await page.getByRole('dialog').getByRole('button', { name: '取消' }).click() + + await rows.nth(0).locator('.el-checkbox').click() + await rows.nth(1).locator('.el-checkbox').click() + await expect(rows.nth(1).getByRole('button', { name: '只读复核' })).toBeVisible() + await expect(rows.nth(1).getByRole('button', { name: '重新匹配' })).toHaveCount(0) + await page.getByRole('button', { name: '删除(0)' }).click() + await expect(page.getByText('阻塞数量', { exact: true })).toBeVisible() + await expect(page.getByText(/本次不能部分删除/)).toBeVisible() + await expect(page.getByRole('button', { name: '删除 0 条' })).toBeDisabled() +})
导入 Excel 后自动匹配 SYB 商品;确认后逐件回写,结果不明确时只读复核。