Feat: 前端改版 Web 端界面与业务作业流程

按设计稿改版登录后的全部页面,登录页与 Logo 资源未改动。

- 全局外壳:浅色侧边栏与分组菜单、左下角用户卡片、Ctrl/⌘K 搜索面板、
  测试模式提示条、深色模式配色、窄屏适配
- 共用组件:页头 PageHeader、托盘流程条 FlowStepper、扫码反馈 ScanFeedback、
  缩短单号 DocNo、托盘档案链接 PalletCodeLink、图标 SfIcon、上级资料选择
  MasterRefSelect,以及编号段解析等工具
- 工作台重做;新增托盘档案页;托盘追溯增加单托盘入口,报表托盘号可跳转档案
- 基础资料维护:各页加指标条、名称代替 ID、详情面板;三步式批量导入预览;
  分配用户对话框标注增减;7 个新增/编辑对话框统一布局
- 业务管理:发货出库、仓库入库、工厂装填、客户现场使用、退回仓库改为
  列表 + 详情面板 + 全屏作业区;写入流程、幂等键与防串单逻辑保持不变
- 修复入库页扫码反馈条因同名状态变量遮蔽组件而不显示的问题,并新增契约检查
- 新增入库粘贴解析、退回、装填的行为测试与现场使用页新流程测试,
  同步更新业务与工作台契约脚本

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 05:03:22 -07:00
co-authored by Claude Opus 5.5
parent 3050ccdc4e
commit 167df4adb8
71 changed files with 18396 additions and 2428 deletions
@@ -91,13 +91,15 @@ const expectedRequests = {
{ url: "'/master/site'", method: 'post' },
{ url: "'/master/site'", method: 'put' },
{ url: "'/master/site/' + siteId + '/users'", method: 'get' },
{ url: "'/master/site/' + siteId + '/users'", method: 'put' }
{ url: "'/master/site/' + siteId + '/users'", method: 'put' },
{ url: "'/master/site/customer-options'", method: 'get' }
],
'src/api/master/line.js': [
{ url: "'/master/line/list'", method: 'get' },
{ url: "'/master/line/' + lineId", method: 'get' },
{ url: "'/master/line'", method: 'post' },
{ url: "'/master/line'", method: 'put' },
{ url: "'/master/line/site-options'", method: 'get' }
],
'src/api/master/warehouse.js': [
{ url: "'/master/warehouse/list'", method: 'get' },
@@ -119,9 +121,11 @@ const expectedRequests = {
{ url: "'/master/tunnel/' + tunnelId", method: 'get' },
{ url: "'/master/tunnel'", method: 'post' },
{ url: "'/master/tunnel'", method: 'put' },
{ url: "'/master/tunnel/site-options'", method: 'get' }
],
'src/api/business/inbound.js': [
{ url: "'/business/inbound/list'", method: 'get' },
{ url: "'/business/inbound/summary'", method: 'get' },
{ url: "'/business/inbound/warehouse-options'", method: 'get' },
{ url: "'/business/inbound/scan/' + encodeURIComponent(qrToken)", method: 'get' },
{ url: "'/business/inbound/' + inboundId", method: 'get' },
@@ -131,6 +135,9 @@ const expectedRequests = {
],
'src/api/business/filling.js': [
{ url: "'/business/filling/list'", method: 'get' },
{ url: "'/business/filling/summary'", method: 'get' },
{ url: "'/business/filling/batches'", method: 'get' },
{ url: "'/business/filling/batches/detail'", method: 'get' },
{ url: "'/business/filling/strain-options'", method: 'get' },
{ url: "'/business/filling/scan/' + encodeURIComponent(qrToken)", method: 'get' },
{ url: "'/business/filling/' + fillingId", method: 'get' },
@@ -151,6 +158,9 @@ const expectedRequests = {
{ url: "'/business/usage/list'", method: 'get' },
{ url: "'/business/usage/line-options'", method: 'get' },
{ url: "'/business/usage/tunnel-options'", method: 'get' },
{ url: "'/business/usage/site-options'", method: 'get' },
{ url: "'/business/usage/site-board'", method: 'get' },
{ url: "'/business/usage-fifo/queue'", method: 'get' },
{ url: "'/business/usage/enter-tunnel'", method: 'post' },
{ url: "'/business/usage/scan/' + encodeURIComponent(qrToken)", method: 'get' },
{ url: "'/business/usage-fifo/recommendation'", method: 'get' },
@@ -159,6 +169,8 @@ const expectedRequests = {
],
'src/api/business/return.js': [
{ url: "'/business/return/list'", method: 'get' },
{ url: "'/business/return/summary'", method: 'get' },
{ url: "'/business/return/site-options'", method: 'get' },
{ url: "'/business/return/warehouse-options'", method: 'get' },
{ url: "'/business/return/scan/' + encodeURIComponent(qrToken)", method: 'get' },
{ url: "'/business/return/request'", method: 'post' },
@@ -211,14 +223,14 @@ const masterPageContracts = {
},
site: {
relativePath: 'src/views/master/site/index.vue',
apiNames: ['addSite', 'getSite', 'listSite', 'updateSite'],
apiNames: ['addSite', 'getSite', 'getSiteCustomerOptions', 'listSite', 'updateSite'],
permissions: ['add', 'assign', 'edit', 'list', 'query'],
addPayloadKeys: ['address', 'customerId', 'remark', 'siteCode', 'siteName', 'status'],
editPayloadKeys: ['address', 'customerId', 'remark', 'siteCode', 'siteId', 'siteName', 'status']
},
line: {
relativePath: 'src/views/master/line/index.vue',
apiNames: ['addLine', 'getLine', 'listLine', 'updateLine'],
apiNames: ['addLine', 'getLine', 'getLineSiteOptions', 'listLine', 'updateLine'],
permissions: ['add', 'edit', 'list', 'query'],
addPayloadKeys: ['lineCode', 'lineName', 'remark', 'siteId', 'status'],
editPayloadKeys: ['lineCode', 'lineId', 'lineName', 'remark', 'siteId', 'status']
@@ -246,6 +258,7 @@ const businessPageContracts = {
'addInbound',
'confirmInbound',
'getInbound',
'getInboundSummary',
'getInboundWarehouseOptions',
'listInbound',
'scanInboundPallet',
@@ -260,7 +273,15 @@ const businessPageContracts = {
},
filling: {
relativePath: 'src/views/business/filling/index.vue',
apiNames: ['addFilling', 'getFillingStrainOptions', 'listFilling', 'scanFillingPallet'],
apiNames: [
'addFilling',
'getFillingBatchDetail',
'getFillingStrainOptions',
'getFillingSummary',
'listFilling',
'listFillingBatches',
'scanFillingPallet'
],
permissions: ['add', 'list', 'query'],
payloads: {
addFilling: [
@@ -298,7 +319,18 @@ const businessPageContracts = {
},
usage: {
relativePath: 'src/views/business/usage/index.vue',
apiNames: ['addUsage', 'enterUsageTunnel', 'getUsageFifoRecommendation', 'getUsageLineOptions', 'getUsageTunnelOptions', 'listUsage', 'scanUsagePallet'],
apiNames: [
'addUsage',
'enterUsageTunnel',
'getUsageFifoQueue',
'getUsageFifoRecommendation',
'getUsageLineOptions',
'getUsageSiteBoard',
'getUsageSiteOptions',
'getUsageTunnelOptions',
'listUsage',
'scanUsagePallet'
],
permissions: ['add', 'list', 'query'],
// 使用请求需原样持久化以安全重试,载荷边界由 usage-behavior 的真实请求断言验证。
payloads: {}
@@ -308,6 +340,8 @@ const businessPageContracts = {
apiNames: [
'confirmReturn',
'getReturn',
'getReturnSiteOptions',
'getReturnSummary',
'getReturnWarehouseOptions',
'listReturn',
'requestReturn',
@@ -1538,7 +1572,7 @@ test('首次入库确认失败后取消重试不丢弃原幂等键', async () =>
test('18 个 API 文件逐一匹配硬编码请求契约', async (t) => {
assert.equal(Object.keys(expectedRequests).length, 18)
assert.equal(Object.values(expectedRequests).flat().length, 74)
assert.equal(Object.values(expectedRequests).flat().length, 86)
for (const [relativePath, expected] of Object.entries(expectedRequests)) {
await t.test(relativePath, async () => {
@@ -2923,3 +2957,20 @@ test('三个业务菜单公共样式统一使用 Element Plus 主题变量', asy
assert.doesNotMatch(styles, /background:\s*#f7f9fc\s*!important/)
assert.doesNotMatch(styles, /background:\s*#fbfcfe\s*;/)
})
test('业务页面导入的组件不被同名响应式状态遮蔽', async (t) => {
// 非 .vue 路径导入的组件按 maybe-ref 绑定,模板标签会优先解析为同名 camelCase 状态(如 scanFeedback 遮蔽 ScanFeedback)
const { compileScript, parse } = await import('@vue/compiler-sfc')
for (const [resource, contract] of Object.entries(businessPageContracts)) {
await t.test(resource, async () => {
const source = await readFile(path.join(frontendRoot, contract.relativePath), 'utf8')
const { descriptor } = parse(source)
const compiled = compileScript(descriptor, { id: `contract-${resource}`, inlineTemplate: true }).content
const components = [...source.matchAll(/^import\s+([A-Z]\w*)\s+from\s+['"]@\/components\/[^'"]+['"]/gm)].map((match) => match[1])
for (const component of components) {
const shadow = component[0].toLowerCase() + component.slice(1)
assert.doesNotMatch(compiled, new RegExp(`_create(?:VNode|Block)\\(_unref\\(${shadow}\\)`), `${component} 被同名状态 ${shadow} 遮蔽`)
}
})
}
})