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 @@ @@ -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 }}网络连接失败,请刷新核对任务状态后重试。