diff --git a/AGENTS.md b/AGENTS.md
index 68cc29b..6bb5b66 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,7 +6,7 @@
2. 登录/Token/统一请求先读下表登录API/会话、`src/config/runtime.js`、`../../docs/统一认证与服务通信契约.md`。
3. 沿实际 API 读直接服务指引:入仓/库存 `../../baiyue-firstleg/lastleg-service/AGENTS.md`,资源 `../../baiyue-firstleg/warehouse-overseas-service/AGENTS.md`;网关路径/鉴权问题才读 `../../baiyue-firstleg/gateway/AGENTS.md`。跨模块或入口不明才读 `../../docs/海外仓业务/README.md`。
-仅读适用指引/目标页面/API;纯页面不读后端,跨服务状态/鉴权不明才扩读。遵根预算,先规则后命中段,不全文读海外仓 README。
+按根预算先规则后命中,不通读海外仓 README;纯页面不读后端,跨服务状态/鉴权不明才扩读。样式读[规范](../../docs/海外PDA界面样式规范.md)。
## 项目定位
diff --git a/src/api/workbench.js b/src/api/workbench.js
new file mode 100644
index 0000000..43afa2e
--- /dev/null
+++ b/src/api/workbench.js
@@ -0,0 +1,70 @@
+import { listReceivingCandidates } from './inbound.js'
+import { assertReceivingWarehouse } from './receiving-adapter.js'
+import { listWarehouseTransferCandidates } from './warehouse-transfers.js'
+import { listReturnCountTasks } from './return-counting.js'
+import { listPackingPlans } from './return-packing.js'
+import { listDispatchTasks, listOutboundTasks, listCargoDiscardTasks } from './fulfillment.js'
+import { listRelabelCandidates } from './relabel.js'
+
+export const WORKBENCH_COUNT_KEYS = ['receiving', 'warehouseTransferList', 'warehouseTransferWork', 'returnCounting', 'returnPacking', 'dispatchPicking', 'outboundPicking', 'relabelReceive', 'relabelExecute', 'relabelReview']
+
+const ready = (count, unit = '单') => ({ status: 'ready', count, unit })
+const distinctCount = (rows, key) => {
+ if (!Array.isArray(rows)) throw new Error('待办列表返回结构无效')
+ const codes = rows.map((row) => String(row?.[key] || '').trim())
+ if (codes.some((code) => !code)) throw new Error('待办列表缺少业务编号')
+ return new Set(codes).size
+}
+
+export async function loadWorkbenchCounts({ profile, activeTransferNo = '' }) {
+ const sources = [
+ { keys: ['receiving'], load: async () => {
+ const rows = await listReceivingCandidates()
+ rows.forEach((row) => assertReceivingWarehouse(row, profile, '待收货柜'))
+ if (rows.some((row) => !row.sourceContainerBookingId || !row.containerNo)) throw new Error('待收货柜缺少业务定位信息')
+ const keys = rows.map((row) => ({ key: `${row.sourceContainerBookingId}:${row.containerNo}` }))
+ return { receiving: ready(distinctCount(keys, 'key'), '柜') }
+ } },
+ { keys: ['warehouseTransferList', 'warehouseTransferWork'], load: async () => {
+ const rows = (await listWarehouseTransferCandidates()).filter((row) => ['WAIT_PDA_RECEIVING', 'PARTIALLY_RECEIVED'].includes(row.status))
+ const active = rows.filter((row) => String(row.transferNo).trim() === String(activeTransferNo).trim())
+ return { warehouseTransferList: ready(distinctCount(rows, 'transferNo')), warehouseTransferWork: ready(distinctCount(active, 'transferNo'), '单进行中') }
+ } },
+ { keys: ['returnCounting'], load: async () => {
+ const value = await listReturnCountTasks({ page: 1, size: 1 })
+ const total = value?.total
+ if ((typeof total !== 'number' && !(typeof total === 'string' && /^\d+$/.test(total))) || !Number.isSafeInteger(Number(total)) || Number(total) < 0) throw new Error('退货清点待办总数无效')
+ return { returnCounting: ready(Number(total), '托') }
+ } },
+ { keys: ['returnPacking'], load: async () => ({ returnPacking: ready(distinctCount((await listPackingPlans()).filter((row) => row.status === 'READY'), 'planNo'), '单计划') }) },
+ { keys: ['dispatchPicking'], load: async () => {
+ const rows = (await listDispatchTasks()).filter((row) => row.status !== 'CANCELLED' && ['BEFORE_LOADING', 'LOADING', 'AFTER_LOADING'].includes(row.stage))
+ return { dispatchPicking: ready(distinctCount(rows, 'dispatchNo'), '单未发车') }
+ } },
+ { keys: ['outboundPicking'], load: async () => {
+ const [outbounds, discards] = await Promise.all([listOutboundTasks('PENDING'), listCargoDiscardTasks()])
+ return { outboundPicking: { ...ready(distinctCount(outbounds, 'taskNo')), discardCount: distinctCount(discards.filter((row) => row.status !== 'COMPLETED'), 'discardNo') } }
+ } },
+ { keys: ['relabelReceive', 'relabelExecute', 'relabelReview'], load: async () => {
+ const rows = await listRelabelCandidates()
+ return {
+ relabelReceive: ready(distinctCount(rows.filter((row) => !row.taskStatus || row.taskStatus === 'AVAILABLE'), 'relabelNo'), '单待接'),
+ relabelExecute: ready(distinctCount(rows.filter((row) => row.taskStatus === 'PROCESSING' && row.claimedByCurrentOperator === true), 'relabelNo'), '单进行中'),
+ relabelReview: ready(distinctCount(rows.filter((row) => row.taskStatus === 'WAIT_REVIEW'), 'relabelNo'), '单待复核')
+ }
+ } }
+ ]
+ const results = await Promise.allSettled(sources.map((source) => source.load()))
+ return Object.assign({}, ...results.map((result, index) => result.status === 'fulfilled' ? result.value : Object.fromEntries(sources[index].keys.map((key) => [key, { status: 'error', error: result.reason }]))))
+}
+
+export function workbenchBadge(key, value) {
+ if (!WORKBENCH_COUNT_KEYS.includes(key)) return { label: key.endsWith('Query') ? '扫码查询' : key === 'returnReceipt' ? '扫码签收' : '扫码作业', tone: 'is-static' }
+ if (!value) return { label: '加载中…', tone: 'is-loading' }
+ if (value.status === 'error') return { label: '获取失败', tone: 'is-error' }
+ const hasWork = value.count > 0 || value.discardCount > 0
+ let label = `${value.count} ${value.unit}`
+ if (key === 'warehouseTransferWork' && value.count === 0) label = '未开始'
+ if (key === 'outboundPicking' && value.discardCount > 0) label = `${value.count} 出库 · ${value.discardCount} 废弃`
+ return { label, tone: hasWork ? 'has-work' : 'is-empty' }
+}
diff --git a/src/components/ReceivingSessionSummary.vue b/src/components/ReceivingSessionSummary.vue
index 8fb4e80..23f5d88 100644
--- a/src/components/ReceivingSessionSummary.vue
+++ b/src/components/ReceivingSessionSummary.vue
@@ -1,7 +1,7 @@
- 当前柜号{{ session?.containerNo || '—' }}
+ 当前柜号{{ session?.containerNo || '—' }}
{{ statusLabel }}
@@ -45,10 +45,11 @@
+
+
diff --git a/src/pages/home/home.vue b/src/pages/home/home.vue
index e82f296..b13382f 100644
--- a/src/pages/home/home.vue
+++ b/src/pages/home/home.vue
@@ -8,15 +8,15 @@
{{ greeting }},{{ displayName }}
{{ warehouseLabel }}
- {{ availableCount }}项可处理
+ 作业待办
- {{ group.name }}{{ group.operations.length }}
+ {{ group.name }}
@@ -28,8 +28,10 @@
+
+
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index a9b0f42..847bda6 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -1,5 +1,5 @@
-
+
@@ -8,34 +8,47 @@
正在恢复当前柜作业…
- 共 {{ candidates.length }} 柜
- {{ candidateError.message || '待卸货集装箱加载失败' }}code={{ candidateError.code }}requestId={{ candidateError.requestId }}traceId={{ candidateError.traceId }}
+
+ 共 {{ candidates.length }} 柜
+
+
+
+ {{ candidateError.message || '待卸货集装箱加载失败' }}
+ 错误码:{{ candidateError.code }}请求号:{{ candidateError.requestId }}跟踪号:{{ candidateError.traceId }}
+
+
正在刷新待卸货集装箱…
暂无待卸货集装箱
- {{ item.containerNo }}计划到仓:{{ formatPlannedArrival(item.plannedArrivalAt) }}{{ item.operable ? statusLabel(item.status) : '待齐全' }}{{ item.operable ? (item.currentAccount ? '继续' : '详情') : '只读' }} ›
+
+ {{ item.containerNo }}计划到仓:{{ formatPlannedArrival(item.plannedArrivalAt) }}
+ {{ item.operable ? statusLabel(item.status) : '待齐全' }}
+
{{ item.totalOrderCount }}总订单{{ item.inboundCodeCount }}入仓码{{ item.planCount }}已生成计划{{ item.pendingPlanCount }}未操作
- 已生成 {{ item.planCount }} 个入库计划,{{ item.pendingPlanCount }} 个未操作,其中 {{ item.missingInboundCodeCount }} 个缺入仓码
- {{ item.pendingPlanCount }} 单待处理
- 海外接柜仓:{{ receivingWarehouseLabel(item) }}
- 业务目的仓:{{ item.destinationCodes.join(' / ') }}
+
+ {{ item.missingInboundCodeCount }} 单缺入仓码
+ {{ item.pendingPlanCount }} 单待处理
+ 海外接柜点:{{ receivingWarehouseLabel(item) }}
+ 业务目的仓:{{ item.destinationCodes.join(' / ') }}
+
+ {{ item.operable ? (item.currentAccount ? '继续' : '详情') : '只读' }}
-
+
- {{ selected?.containerNo }}计划到仓:{{ formatPlannedArrival(selected?.plannedArrivalAt) }}暂不可操作
+ {{ selected?.containerNo }}计划到仓:{{ formatPlannedArrival(selected?.plannedArrivalAt) }}暂不可操作
{{ selected?.totalOrderCount }}总订单{{ selected?.inboundCodeCount }}入仓码{{ selected?.planCount }}已生成计划{{ selected?.pendingPlanCount }}未操作
已生成 {{ selected?.planCount }} 个入库计划,{{ selected?.pendingPlanCount }} 个未操作,其中 {{ selected.missingInboundCodeCount }} 个缺入仓码
{{ selected?.blockReason || '当前柜入库计划尚未齐全' }}
海外接柜仓:{{ receivingWarehouseLabel(selected) }}
-
+
@@ -68,7 +81,7 @@
-
+
@@ -109,7 +122,7 @@
本次已收 {{ lastReceipt.acceptedPackages }} 箱,整柜剩余 {{ current.pendingPackages }} 箱
-
+
@@ -117,8 +130,8 @@
- {{ error.message }}
- code={{ error.code }}requestId={{ error.requestId }}traceId={{ error.traceId }}
+ {{ error.message }}
+ 错误码:{{ error.code }}请求号:{{ error.requestId }}跟踪号:{{ error.traceId }}
@@ -147,7 +160,7 @@
export default {
components: { PortalHeader, ReceivingSessionSummary, ScanTargetInput },
- data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, candidateLoading: false, busy: false, bootstrapped: false, detailOpen: false, domesticPhotosExpanded: false, cartonInput: '', palletInput: '', packageCount: '', candidateError: null, error: null, scanQueue: [], toast: { visible: false, message: '', type: '' } } },
+ data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, candidateLoading: false, busy: false, bootstrapped: false, detailOpen: false, domesticPhotosExpanded: false, cartonInput: '', palletInput: '', packageCount: '', candidateError: null, error: null, candidateErrorDetailsOpen: false, errorDetailsOpen: false, scanQueue: [], toast: { visible: false, message: '', type: '' } } },
computed: {
inboundMode() { return this.moduleKey === 'inbound' },
photoSteps() { return RECEIVING_PHOTO_ROLES.map((role) => ({ role, label: ({ LICENSE_PLATE: '车牌照片', CONTAINER_SEAL: '集装箱封签', DOOR_OPEN: '开柜现场' })[role] })) },
@@ -162,6 +175,7 @@
canFinishOnsite() { return Boolean(this.current && ['SIGNED', 'RECEIVING'].includes(this.current.status)) },
pageTitle() { return { candidates: '待卸货集装箱', detail: '柜号详情', sign: '拆柜拍照签收', receive: '当前柜收货', completed: '入库完成' }[this.phase] }
},
+ watch: { error() { this.errorDetailsOpen = false }, candidateError() { this.candidateErrorDetailsOpen = false } },
onLoad(options) { this.moduleKey = options.module || 'query'; const key = options.op || (this.moduleKey === 'inbound' ? 'receiving' : 'inventoryQuery'); this.operationName = ({ receiving: '拆柜收货', stocktake: '库存盘点', move: '商品移库', pallet: '托盘管理', relabelReceive: '换标列表', relabelExecute: '换标执行', relabelReview: '换标复核', inventoryQuery: '库存查询', locationQuery: '库位查询', palletQuery: '托盘查询' })[key] || '库存查询' },
onShow() { if (!requireSession()) return; this.user = session.currentUser; this._pageActive = true; this.startScanner(); if (this.inboundMode && (!this.bootstrapped || this.phase === 'candidates' || this._resumeReceiving)) { if (!this.busy) { this._resumeReceiving = false; this.bootstrap() } } },
onHide() { this.suspendReceiving() }, onUnload() { this.suspendReceiving() }, beforeUnmount() { this.suspendReceiving() },
@@ -250,10 +264,11 @@
diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue
index 024495e..c26a904 100644
--- a/src/pages/outbound/outbound.vue
+++ b/src/pages/outbound/outbound.vue
@@ -2,7 +2,7 @@
-
+
出库任务
当前任务出库作业详情
@@ -14,9 +14,13 @@
{{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}网络连接失败,请刷新核对任务状态后重试。
- 待处理出库任务{{ tasks.length }} 个任务
-
-
+ 待处理出库任务 · {{ tasks.length }} 项
+
+
暂无出库任务可刷新列表,或扫描任务号恢复未完成作业。
废弃订单{{ discardOrderCount }} 个订单
@@ -39,7 +43,7 @@
本次拣货:{{ lineBusinessCode(selectedPosition) }}本来源计划出库 {{ selectedItem.returnSkuQuantity }} 件,请清点这些件数后确认;余数保留原箱。拣货箱数最多 {{ selectedRemaining }} 箱
- 完成出库任务尚有 {{ remainingPackages }} 箱未拣,不能完成出库。出库任务已完成,不可重复提交。全部计划货物已扫描核平,服务端将再次校验库存门禁。
+ 完成出库任务尚有 {{ remainingPackages }} 箱未拣,不能完成出库。出库任务已完成,不可重复提交。
@@ -354,3 +358,7 @@
.discard-remark .card-title, .discard-remark .task-meta { color: #dc2626; }
.discard-scanned-count { display: block; margin-bottom: 8px; color: #dc2626; font-size: 15px; font-weight: 700; }
+
+
diff --git a/src/pages/relabel/relabel.vue b/src/pages/relabel/relabel.vue
index 28e11d8..dd7980e 100644
--- a/src/pages/relabel/relabel.vue
+++ b/src/pages/relabel/relabel.vue
@@ -1,18 +1,19 @@
-
+
- {{ candidateHeading }}
+ {{ candidateHeading }} · {{ visibleCandidates.length }} 单
正在加载换标任务…
{{ emptyText }}
-
-
- {{ item.relabelNo }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}
- {{ relabelTypeName(item.operationType) }}退回重做
- 已换标 {{ item.relabelledPackages }} / {{ item.expectedPackages }}跳过/异常 {{ item.skippedPackages + item.exceptionPackages }}进入 ›
+
+
+ {{ item.relabelNo }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}
+ {{ relabelTypeName(item.operationType) }}退回重做
+ {{ item.expectedPackages }}应处理{{ item.relabelledPackages }}已换标{{ item.skippedPackages + item.exceptionPackages }}跳过/异常
+
@@ -391,3 +392,7 @@
.summary-metrics{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:4px;margin-left:auto;font-size:9px;line-height:18px}.summary-metric{display:inline-block;padding:1px 4px;border-radius:4px;background:#dcfce7;color:#16a34a;font-weight:700;white-space:nowrap}
.candidate-toolbar{align-items:center;margin-bottom:12px}.candidate-toolbar .card-title{margin-bottom:0;font-size:16px;line-height:24px}.candidate-toolbar .link-button{display:flex;align-items:center;justify-content:center;box-sizing:border-box;min-width:88px;height:44px;min-height:44px;margin:0;padding:0 16px;border:1px solid #9dc0f4;border-radius:8px;color:#135ee8;font-size:16px;font-weight:700;line-height:normal;background:#eaf2ff}.candidate-toolbar .link-button::after{border:0}.candidate-toolbar .link-button[disabled]{color:#718095;border-color:#dce5f0;background:#e8edf3}
.relabel-candidate .business-code{min-width:0;font-size:18px;line-height:24px;overflow-wrap:anywhere}.relabel-candidate .status-pill{font-size:12px;line-height:18px}.candidate-kind{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px;color:#52647c;font-size:14px;line-height:22px}.candidate-footer{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;margin-top:10px}.candidate-progress{color:#52647c;font-size:14px;line-height:24px}.candidate-progress-value{color:#135ee8;font-size:18px;font-weight:700}.candidate-warning{color:#b54708;font-size:12px;line-height:20px}.candidate-enter{margin-left:auto;color:#135ee8;font-size:14px;font-weight:600;line-height:24px}
+
+
diff --git a/src/pages/warehouse-transfer/warehouse-transfer.vue b/src/pages/warehouse-transfer/warehouse-transfer.vue
index e2a534f..e107bbd 100644
--- a/src/pages/warehouse-transfer/warehouse-transfer.vue
+++ b/src/pages/warehouse-transfer/warehouse-transfer.vue
@@ -1,28 +1,28 @@
-
-
+
+
- 仓储转运测量列表
-
+ 待测转运单 · {{ candidates.length }} 单
+
- 待测转运单{{ candidates.length }} 单
+ 正在加载待测转运单…
暂无待测量任务新任务进入后可点击右上角刷新
-
- 暂无进行中的转运测量作业请先从“仓储转运测量列表”选择待测转运单。
+ 暂无进行中的转运测量作业请先从“仓储转运测量”选择待测转运单。
正在读取转运明细请稍候,不要重复选择
@@ -365,6 +365,7 @@ export default {
},
openPalletOperations() { uni.navigateTo({ url: '/pages/pallet/pallet' }) },
destinationLabel(value, fallback = {}) { const code = String(value?.fbaWarehouseCode || fallback?.fbaWarehouseCode || '').trim(); if (code) return code; const type = value?.destinationType || fallback?.destinationType; return type === 'PRIVATE' ? '私人仓' : '目的仓待定' },
+ listInboundCode(item) { const code = String(item?.inboundCode || '').trim(); return code !== String(item?.transferNo || '').trim() ? code : '' },
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handlePalletScan(value), onError: (error) => { this.error = error } }) },
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
handlePalletScan(value) { const pallet = this.pallets.find((item) => item.key === this.activePalletKey && item.generated && !item.bound); if (!pallet) return; pallet.bindCode = String(value || '').trim(); this.bindPallet(pallet) },
@@ -547,7 +548,7 @@ export default {
this.countDiscrepancyReason = reason
this.saveCartonDraft()
}
- if (!this.pendingKey) this.pendingKey = `pda-transfer-${this.selected.transferNo}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
+ this.pendingKey = `pda-transfer-${this.selected.transferNo}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
const payload = { idempotencyKey: this.pendingKey, photoFileIds: [...this.photoFileIds], discrepancyReason: this.hasSkuReceiptDifference ? this.countDiscrepancyReason : null }
const transferNo = this.selected.transferNo; const result = await palletizeWarehouseTransfer(transferNo, payload); this.completedTransferNo = transferNo; this.completedDestination = result?.fbaWarehouseCode || this.selected.fbaWarehouseCode || ''; this.completedDestinationType = result?.destinationType || this.selected.destinationType || ''; this.completedPallets = (result?.pallets || []).filter((pallet) => pallet?.palletNo); this.success = true; resetWarehouseDomain('warehouseTransfer'); this.selected = null; this.pendingKey = ''; this.pallets = [newPallet()]; this.photoFileIds = []; this.photoUrls = {}; this.photoNames = []; await this.loadCandidates()
} catch (e) { this.error = e } finally { this.submitting = false }
@@ -592,7 +593,32 @@ export default {
.carton-count-card{padding:14px 10px;margin:10px 0;border:1px solid #d8e3f1;border-radius:12px}
-.transfer-page{height:100vh;overflow:hidden;background:#f2f5f9}.transfer-scroll{height:calc(100vh - 48px - var(--status-bar-height,0px))}.transfer-content{padding-bottom:52px}.transfer-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin:4px 2px 17px}.transfer-title,.transfer-subtitle,.transfer-no,.transfer-meta,.summary-eyebrow,.summary-no,.pallet-heading-copy text,.measure-title,.measure-hint,.sku-copy text,.validation-title,.transfer-validation>text,.feedback-title,.transfer-error>text,.transfer-success>text,.empty-title,.transfer-empty>text{display:block}.transfer-title{color:#14263d;font-size:22px;font-weight:850;letter-spacing:.2px}.transfer-subtitle{margin-top:4px;color:#718095;font-size:11px;line-height:17px}.refresh-button{display:flex;align-items:center;justify-content:center;gap:4px;width:auto;height:34px;margin:0;padding:0 11px;border:1px solid #b8d1f5;border-radius:17px;color:#135ee8;font-size:10px;line-height:34px;background:#f8fbff}.refresh-button[disabled]{opacity:.55}.section-heading{display:flex;align-items:center;justify-content:space-between;margin:0 2px 8px;color:#243951;font-size:12px;font-weight:800}.section-heading text:last-child{color:#8290a2;font-size:10px;font-weight:600}.candidate-list{display:flex;flex-direction:column;gap:9px;margin-bottom:16px}.transfer-card{box-sizing:border-box;width:100%;margin:0;padding:13px;border:1px solid #dce5f0;border-radius:13px;text-align:left;background:#fff;box-shadow:0 4px 15px rgba(32,67,111,.055)}.transfer-card.selected{border-color:#6da3f1;background:#f7faff;box-shadow:0 5px 18px rgba(19,94,232,.11)}.transfer-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.transfer-no{overflow:hidden;color:#18324f;font-size:14px;font-weight:850;text-overflow:ellipsis;white-space:nowrap}.status-pill{flex-shrink:0;padding:4px 7px;border-radius:6px;color:#135ee8;font-size:9px;font-weight:750;background:#eaf2ff}.task-metrics{display:grid;grid-template-columns:.75fr 1.25fr .75fr;gap:6px;margin-top:11px}.task-metrics view{min-width:0;padding:7px 8px;border-radius:8px;background:#f4f7fa}.task-metrics text{display:block;color:#8492a4;font-size:8px}.task-metrics strong{display:block;overflow:hidden;margin-top:2px;color:#2c4058;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.transfer-meta{margin-top:8px;color:#7b899b;font-size:9px}.transfer-empty{margin-bottom:12px;padding:26px 14px;border:1px dashed #ccd7e4;border-radius:13px;color:#8996a6;font-size:10px;line-height:18px;text-align:center;background:rgba(255,255,255,.65)}.empty-title{color:#56677b;font-size:12px;font-weight:800}.order-summary{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 15px;border-radius:14px 14px 0 0;color:#fff;background:linear-gradient(135deg,#123d73,#135ee8)}.summary-eyebrow{font-size:9px;opacity:.72}.summary-no{margin-top:3px;font-size:15px;font-weight:850}.summary-count{display:flex;align-items:baseline;gap:3px;flex-shrink:0}.summary-count strong{font-size:24px}.summary-count text{font-size:10px;opacity:.8}.summary-metrics{display:grid;grid-template-columns:repeat(3,1fr);margin-bottom:12px;padding:11px 8px;border-radius:0 0 14px 14px;background:#fff;box-shadow:0 5px 17px rgba(32,67,111,.07)}.summary-metrics view{position:relative;text-align:center}.summary-metrics view+view:before{position:absolute;top:3px;bottom:3px;left:0;width:1px;background:#e7edf4;content:''}.summary-metrics strong{display:block;color:#17314f;font-size:14px}.summary-metrics text{display:block;margin-top:2px;color:#8390a1;font-size:8px}.pallet-card{box-sizing:border-box;margin-bottom:12px;padding:14px;border:1px solid #d9e3ee;border-radius:15px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.065)}.pallet-head{display:flex;align-items:center;gap:9px;margin:-2px -2px 14px;padding-bottom:11px;border-bottom:1px solid #edf1f5}.pallet-heading-copy{min-width:0;flex:1}.pallet-heading-copy text:first-child{color:#17314f;font-size:13px;font-weight:850}.pallet-heading-copy>text:last-child{margin-top:2px;color:#8995a5;font-size:8px}.remove-button{width:auto;height:28px;margin:0;padding:0 9px;border-radius:7px;color:#b33030;font-size:9px;line-height:28px;background:#fff0f0}.field-label{display:block;margin-bottom:5px;color:#65768b;font-size:9px;font-weight:700}.field-label small{margin-left:3px;color:#9aa5b3;font-size:8px;font-weight:500}.measure-input,.input-with-unit{box-sizing:border-box;width:100%;height:42px;border:1px solid #cad7e5;border-radius:9px;color:#203750;background:#fafcfe}.measure-input{padding:0 11px;font-size:12px}.pallet-code-field{display:block;margin-bottom:14px}.measure-section-head,.sku-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.measure-title{color:#243a53;font-size:11px;font-weight:850}.measure-hint{margin-top:2px;color:#8a96a5;font-size:8px}.volume-chip{flex-shrink:0;padding:5px 8px;border-radius:8px;text-align:right;background:#edf5ff}.volume-chip text{display:block;color:#7189a7;font-size:7px}.volume-chip strong{display:block;margin-top:1px;color:#135ee8;font-size:10px}.dimension-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.measure-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin-top:10px}.input-with-unit{display:flex;align-items:center;overflow:hidden}.input-with-unit input{box-sizing:border-box;min-width:0;height:40px;padding:0 4px 0 10px;flex:1;color:#203750;font-size:16px;font-weight:800}.input-with-unit>text{padding-right:8px;color:#8491a1;font-size:8px}.input-with-unit.important{border-color:#9ebff0;background:#f8fbff}.sku-section{margin:15px -2px -2px;padding:12px;border-radius:11px;background:#f5f8fb}.sku-total{flex-shrink:0;padding:5px 7px;border-radius:7px;color:#9a5c13;font-size:9px;font-weight:800;background:#fff1dd}.sku-total.matched{color:#08705f;background:#dff5ed}.sku-row{display:flex;align-items:center;gap:9px;min-height:48px;border-top:1px solid #e3eaf2}.sku-copy{min-width:0;flex:1}.sku-code{overflow:hidden;color:#233b56;font-size:11px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.sku-copy text:last-child{margin-top:2px;color:#8693a3;font-size:8px}.sku-input{display:flex;align-items:center;gap:4px;color:#718095;font-size:9px}.sku-input input{box-sizing:border-box;width:64px;height:34px;padding:0 7px;border:1px solid #abc4e8;border-radius:8px;color:#17314f;font-size:14px;font-weight:800;text-align:center;background:#fff}.add-pallet-button{display:flex;align-items:center;justify-content:center;gap:5px;box-sizing:border-box;width:100%;height:44px;margin:2px 0 11px;border:1px dashed #78a8ed;border-radius:11px;color:#135ee8;font-size:11px;font-weight:800;background:#f7faff}.transfer-validation,.transfer-error,.transfer-success{box-sizing:border-box;margin-bottom:11px;padding:11px 12px;border-radius:10px;font-size:9px;line-height:16px}.transfer-validation,.transfer-error{color:#9d3030;background:#fff0f0}.validation-title,.feedback-title{font-size:11px;font-weight:850}.transfer-success{color:#176b42;background:#eaf8f1}.submit-panel{box-sizing:border-box;padding:12px;border:1px solid #dce5f0;border-radius:13px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.07)}.submit-panel>view{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px;color:#778598;font-size:9px}.submit-panel strong{color:#243951;font-size:11px}.transfer-primary{box-sizing:border-box;width:100%;height:44px;margin:0;border-radius:9px;color:#fff;font-size:12px;font-weight:850;line-height:44px;background:#135ee8}.transfer-primary[disabled]{color:#9aa7b5;background:#e7ecf2;opacity:1}
+.transfer-page{height:100vh;overflow:hidden;background:#f2f5f9}.transfer-scroll{height:calc(100vh - 48px - var(--status-bar-height,0px))}.transfer-content{padding-bottom:52px}.transfer-list-page{background:#f1f5fd}
+.transfer-list-page .transfer-content{padding-top:14px;padding-bottom:calc(24px + env(safe-area-inset-bottom,0px))}
+.transfer-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 11px}
+.transfer-list-count{min-width:0;color:#526174;font-size:14px;font-weight:700;line-height:20px}
+.transfer-no,.transfer-meta,.summary-eyebrow,.summary-no,.pallet-heading-copy text,.measure-title,.measure-hint,.sku-copy text,.validation-title,.transfer-validation>text,.feedback-title,.transfer-error>text,.transfer-success>text,.empty-title,.transfer-empty>text{display:block}
+.refresh-button{display:flex;align-items:center;justify-content:center;gap:4px;width:auto;height:34px;margin:0;padding:0 11px;border:1px solid #b8d1f5;border-radius:17px;color:#135ee8;font-size:10px;line-height:34px;background:#f8fbff}
+.transfer-heading .refresh-button{box-sizing:border-box;flex-shrink:0;gap:5px;min-width:72px;height:36px;padding:0 14px;border-radius:20px;font-size:14px;font-weight:700;line-height:normal;background:#fff}
+.refresh-button[disabled]{opacity:.55}
+.candidate-list{display:flex;flex-direction:column;gap:11px;margin-bottom:16px}
+.transfer-card{display:block;box-sizing:border-box;width:100%;margin:0;padding:14px;border:1px solid #dce5f0;border-radius:13px;color:#1f2937;line-height:normal;text-align:left;white-space:normal;background:#fff;box-shadow:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}
+.transfer-card-pressed{border-color:#93baf0;background:#f4f8ff}
+.transfer-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
+.transfer-no{flex:1;min-width:0;color:#1f2937;font-size:18px;font-weight:750;line-height:26px;overflow-wrap:anywhere;word-break:break-all}
+.status-pill{flex-shrink:0;padding:6px 10px;border-radius:9px;color:#135ee8;font-size:13px;font-weight:700;line-height:16px;white-space:nowrap;background:#e5efff}
+.status-measuring{color:#08705f;background:#e2f6f0}
+.task-metrics{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.3fr) minmax(0,1fr);margin-top:12px;padding:7px 0;border-top:1px solid #e1e7f0;border-bottom:1px solid #e1e7f0}
+.task-metrics view{min-width:0;padding:0 6px;border-right:1px solid #e1e7f0;text-align:center}
+.task-metrics view:last-child{border-right:0}
+.task-metrics strong{display:block;color:#1f2937;font-size:22px;font-weight:750;line-height:26px;overflow-wrap:anywhere}
+.task-metrics text{display:block;margin-top:2px;color:#6f8194;font-size:12px;line-height:18px}
+.transfer-card-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;min-height:28px;margin-top:10px}
+.transfer-meta{flex:1;min-width:0;color:#718095;font-size:12px;line-height:20px;overflow-wrap:anywhere}
+.transfer-card-action{display:flex;flex-shrink:0;align-items:center;gap:2px;color:#135ee8;font-size:13px;font-weight:700;line-height:22px;white-space:nowrap}
+.transfer-list-page .transfer-empty{font-size:12px;line-height:20px}
+.transfer-list-page .empty-title{font-size:14px}
+.transfer-empty{margin-bottom:12px;padding:26px 14px;border:1px dashed #ccd7e4;border-radius:13px;color:#8996a6;font-size:10px;line-height:18px;text-align:center;background:rgba(255,255,255,.65)}.empty-title{color:#56677b;font-size:12px;font-weight:800}.order-summary{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 15px;border-radius:14px 14px 0 0;color:#fff;background:linear-gradient(135deg,#123d73,#135ee8)}.summary-eyebrow{font-size:9px;opacity:.72}.summary-no{margin-top:3px;font-size:15px;font-weight:850}.summary-count{display:flex;align-items:baseline;gap:3px;flex-shrink:0}.summary-count strong{font-size:24px}.summary-count text{font-size:10px;opacity:.8}.summary-metrics{display:grid;grid-template-columns:repeat(3,1fr);margin-bottom:12px;padding:11px 8px;border-radius:0 0 14px 14px;background:#fff;box-shadow:0 5px 17px rgba(32,67,111,.07)}.summary-metrics view{position:relative;text-align:center}.summary-metrics view+view:before{position:absolute;top:3px;bottom:3px;left:0;width:1px;background:#e7edf4;content:''}.summary-metrics strong{display:block;color:#17314f;font-size:14px}.summary-metrics text{display:block;margin-top:2px;color:#8390a1;font-size:8px}.pallet-card{box-sizing:border-box;margin-bottom:12px;padding:14px;border:1px solid #d9e3ee;border-radius:15px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.065)}.pallet-head{display:flex;align-items:center;gap:9px;margin:-2px -2px 14px;padding-bottom:11px;border-bottom:1px solid #edf1f5}.pallet-heading-copy{min-width:0;flex:1}.pallet-heading-copy text:first-child{color:#17314f;font-size:13px;font-weight:850}.pallet-heading-copy>text:last-child{margin-top:2px;color:#8995a5;font-size:8px}.remove-button{width:auto;height:28px;margin:0;padding:0 9px;border-radius:7px;color:#b33030;font-size:9px;line-height:28px;background:#fff0f0}.field-label{display:block;margin-bottom:5px;color:#65768b;font-size:9px;font-weight:700}.field-label small{margin-left:3px;color:#9aa5b3;font-size:8px;font-weight:500}.measure-input,.input-with-unit{box-sizing:border-box;width:100%;height:42px;border:1px solid #cad7e5;border-radius:9px;color:#203750;background:#fafcfe}.measure-input{padding:0 11px;font-size:12px}.pallet-code-field{display:block;margin-bottom:14px}.measure-section-head,.sku-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.measure-title{color:#243a53;font-size:11px;font-weight:850}.measure-hint{margin-top:2px;color:#8a96a5;font-size:8px}.volume-chip{flex-shrink:0;padding:5px 8px;border-radius:8px;text-align:right;background:#edf5ff}.volume-chip text{display:block;color:#7189a7;font-size:7px}.volume-chip strong{display:block;margin-top:1px;color:#135ee8;font-size:10px}.dimension-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.measure-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin-top:10px}.input-with-unit{display:flex;align-items:center;overflow:hidden}.input-with-unit input{box-sizing:border-box;min-width:0;height:40px;padding:0 4px 0 10px;flex:1;color:#203750;font-size:16px;font-weight:800}.input-with-unit>text{padding-right:8px;color:#8491a1;font-size:8px}.input-with-unit.important{border-color:#9ebff0;background:#f8fbff}.sku-section{margin:15px -2px -2px;padding:12px;border-radius:11px;background:#f5f8fb}.sku-total{flex-shrink:0;padding:5px 7px;border-radius:7px;color:#9a5c13;font-size:9px;font-weight:800;background:#fff1dd}.sku-total.matched{color:#08705f;background:#dff5ed}.sku-row{display:flex;align-items:center;gap:9px;min-height:48px;border-top:1px solid #e3eaf2}.sku-copy{min-width:0;flex:1}.sku-code{overflow:hidden;color:#233b56;font-size:11px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.sku-copy text:last-child{margin-top:2px;color:#8693a3;font-size:8px}.sku-input{display:flex;align-items:center;gap:4px;color:#718095;font-size:9px}.sku-input input{box-sizing:border-box;width:64px;height:34px;padding:0 7px;border:1px solid #abc4e8;border-radius:8px;color:#17314f;font-size:14px;font-weight:800;text-align:center;background:#fff}.add-pallet-button{display:flex;align-items:center;justify-content:center;gap:5px;box-sizing:border-box;width:100%;height:44px;margin:2px 0 11px;border:1px dashed #78a8ed;border-radius:11px;color:#135ee8;font-size:11px;font-weight:800;background:#f7faff}.transfer-validation,.transfer-error,.transfer-success{box-sizing:border-box;margin-bottom:11px;padding:11px 12px;border-radius:10px;font-size:9px;line-height:16px}.transfer-validation,.transfer-error{color:#9d3030;background:#fff0f0}.validation-title,.feedback-title{font-size:11px;font-weight:850}.transfer-success{color:#176b42;background:#eaf8f1}.submit-panel{box-sizing:border-box;padding:12px;border:1px solid #dce5f0;border-radius:13px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.07)}.submit-panel>view{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px;color:#778598;font-size:9px}.submit-panel strong{color:#243951;font-size:11px}.transfer-primary{box-sizing:border-box;width:100%;height:44px;margin:0;border-radius:9px;color:#fff;font-size:12px;font-weight:850;line-height:44px;background:#135ee8}.transfer-primary[disabled]{color:#9aa7b5;background:#e7ecf2;opacity:1}
.photo-card{box-sizing:border-box;margin-bottom:11px;padding:14px;border:1px solid #d9e3ee;border-radius:13px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.06)}
.photo-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}
.photo-count{flex-shrink:0;padding:5px 8px;border-radius:7px;color:#135ee8;font-size:9px;font-weight:800;background:#eaf2ff}
diff --git a/src/styles/task-list.scss b/src/styles/task-list.scss
new file mode 100644
index 0000000..924efec
--- /dev/null
+++ b/src/styles/task-list.scss
@@ -0,0 +1,89 @@
+.pda-task-list {
+ .pda-list-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ min-height: 36px;
+ margin: 12px 0 11px;
+ color: #526174;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 20px;
+ }
+
+ .pda-list-toolbar > text { min-width: 0; margin: 0; color: inherit; font-size: inherit; font-weight: inherit; line-height: inherit; }
+
+ .pda-list-toolbar button {
+ display: flex;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ gap: 5px;
+ width: auto;
+ min-width: 72px;
+ height: 36px;
+ min-height: 36px;
+ margin: 0;
+ padding: 0 14px;
+ border: 1px solid #b8d1f5;
+ border-radius: 20px;
+ color: #135ee8;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: normal;
+ background: #fff;
+ }
+
+ .pda-list-toolbar button[disabled] { opacity: .55; }
+ .pda-list-cards { display: flex; flex-direction: column; gap: 11px; }
+
+ .pda-task-card {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ margin: 0;
+ padding: 14px;
+ border: 1px solid #dce5f0;
+ border-radius: 13px;
+ color: #1f2937;
+ line-height: normal;
+ text-align: left;
+ white-space: normal;
+ background: #fff;
+ box-shadow: none;
+ }
+
+ .pda-task-card-pressed { border-color: #93baf0; background: #f4f8ff; }
+ .pda-task-card.current { border-color: #8eb8f5; background: #f7faff; }
+ .pda-task-card.rework-card { border-color: #f29b38; background: #fffaf2; box-shadow: none; }
+ .pda-task-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
+
+ .pda-task-no {
+ display: block;
+ flex: 1;
+ min-width: 0;
+ color: #1f2937;
+ font-size: 18px;
+ font-weight: 750;
+ line-height: 26px;
+ overflow-wrap: anywhere;
+ word-break: break-all;
+ }
+
+ .pda-task-status { flex-shrink: 0; max-width: 45%; padding: 6px 10px; border-radius: 9px; font-size: 13px; line-height: 16px; white-space: normal; overflow-wrap: anywhere; }
+ .pda-task-meta { display: block; margin-top: 8px; color: #718095; font-size: 12px; line-height: 20px; overflow-wrap: anywhere; }
+ .pda-task-flags { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 8px; color: #526174; font-size: 12px; line-height: 20px; }
+
+ .pda-task-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 12px; padding: 9px 0; border-top: 1px solid #e1e7f0; border-bottom: 1px solid #e1e7f0; }
+ .pda-task-metrics.has-three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .pda-task-metrics > view { min-width: 0; padding: 0 6px; border-right: 1px solid #e1e7f0; text-align: center; }
+ .pda-task-metrics > view:last-child { border-right: 0; }
+ .pda-task-metrics strong { display: block; color: #1f2937; font-size: 22px; font-weight: 750; line-height: 26px; overflow-wrap: anywhere; }
+ .pda-task-metrics text { display: block; margin-top: 3px; color: #6f8194; font-size: 12px; line-height: 18px; }
+ .pda-task-metric-warning strong { color: #a35b0c; }
+ .pda-task-footer { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; min-height: 28px; margin-top: 10px; }
+ .pda-task-action { display: flex; align-items: center; gap: 2px; margin-left: auto; color: #135ee8; font-size: 13px; font-weight: 700; line-height: 22px; white-space: nowrap; }
+ .pda-task-footer .recent-badge { font-size: 11px; line-height: 16px; }
+}
diff --git a/tests/receiving-photo-ui-contract.test.mjs b/tests/receiving-photo-ui-contract.test.mjs
index affe2cb..68f9393 100644
--- a/tests/receiving-photo-ui-contract.test.mjs
+++ b/tests/receiving-photo-ui-contract.test.mjs
@@ -91,7 +91,7 @@ test('domestic cabinet photos are read-only, previewable and non-blocking in sig
})
test('fixed receiving action leaves enough scroll-safe space above the PDA bottom bar', () => {
- assert.match(page, /v-if="canFinishOnsite" class="sticky-action one"/)
+ assert.match(page, /v-if="canFinishOnsite" class="sticky-action-dock"> operation.available)
assert.deepEqual(enabled.map(({ module, name }) => ({ module, name })), [
{ module: 'inbound', name: '拆柜收货' },
- { module: 'inbound', name: '仓储转运测量列表' }, { module: 'inbound', name: '转运测量' },
+ { module: 'inbound', name: '仓储转运测量' }, { module: 'inbound', name: '转运测量' },
{ module: 'inbound', name: '退货入库' },
{ module: 'inbound', name: '合箱' },
{ module: 'inbound', name: '退货清点' },
@@ -28,7 +28,7 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
assert.equal(operations.find((item) => item.name === '退货入库')?.route, '/pages/return-receipt/return-receipt')
assert.equal(operations.find((item) => item.name === '合箱')?.route, '/pages/return-packing/return-packing')
assert.equal(operations.find((item) => item.name === '退货清点')?.route, '/pages/return-counting/return-counting')
- assert.equal(operations.find((item) => item.name === '仓储转运测量列表')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=list')
+ assert.equal(operations.find((item) => item.name === '仓储转运测量')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=list')
assert.equal(operations.find((item) => item.name === '转运测量')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=work')
assert.equal(operations.some((item) => item.name === '库存盘点'), false)
diff --git a/tests/workbench-counts.test.mjs b/tests/workbench-counts.test.mjs
new file mode 100644
index 0000000..8b559cc
--- /dev/null
+++ b/tests/workbench-counts.test.mjs
@@ -0,0 +1,122 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { parse, compileScript, compileTemplate, compileStyle } from 'vue/compiler-sfc'
+import { assertReceivingWarehouse } from '../src/api/receiving-adapter.js'
+import { MODULES } from '../src/data/overseas-workbench.js'
+
+const apiSource = readFileSync(new URL('../src/api/workbench.js', import.meta.url), 'utf8')
+const homeSource = readFileSync(new URL('../src/pages/home/home.vue', import.meta.url), 'utf8')
+const profile = { id: 'OP', warehouseId: 'WH', warehouseCode: 'WH-CODE', warehouseName: '测试仓' }
+const emptySources = () => ({
+ assertReceivingWarehouse,
+ listReceivingCandidates: async () => [], listWarehouseTransferCandidates: async () => [],
+ listReturnCountTasks: async () => ({ records: [], total: 0 }), listPackingPlans: async () => [],
+ listDispatchTasks: async () => [], listOutboundTasks: async () => [], listCargoDiscardTasks: async () => [],
+ listRelabelCandidates: async () => []
+})
+function api(overrides = {}) {
+ const dependencies = { ...emptySources(), ...overrides }
+ return new Function(...Object.keys(dependencies), apiSource.replace(/^import .*$/gm, '').replace(/^export /gm, '') + '\nreturn { loadWorkbenchCounts, workbenchBadge, WORKBENCH_COUNT_KEYS }')(...Object.values(dependencies))
+}
+function home(loadWorkbenchCounts) {
+ const session = { currentUser: { ...profile }, apiBase: 'LOCAL' }
+ const scopes = []
+ const dependencies = { PortalHeader: {}, MODULES, session, requireSession: () => true, applyWarehouseProfile: value => value, getCurrentWarehouseProfile: async () => session.currentUser, loadWorkbenchCounts, workbenchBadge: api().workbenchBadge, restoreWarehouseWork: scope => scopes.push(scope), warehouseWork: { warehouseTransfer: { currentNo: 'T2' } } }
+ const page = new Function(...Object.keys(dependencies), homeSource.match(/