@@ -2,38 +2,59 @@
< view v-if = "user" class="page-shell portal-page" >
< PortalHeader :title = "operationName" :user = "user" :fallback-url = "`/pages/module/module?key=${moduleKey}`" / >
< view class = "portal-content operation-page" >
< view class = "operation-page-title-row" > < view > < text class = "operation-page-title" > { { operationName } } < / text > < text class = "operation-page-subtitle" > { { prototype . subtitle } } < / text > < / view > < text class = "prototype-badge" > 静态原型 < / text > < / view >
< view class = "operation-scan-card" > < text class = "operation-scan-label" > { { prototype . scanLabel } } < / text > < view class = "operation-scan-row" > < input v-model = "scanValue" class="operation-scan-input" :aria-label="prototype.scanLabel" :placeholder="prototype.scanPlaceholder" confirm-type="search" @confirm="prototypeScan" /><button class="operation-scan-button" @tap="prototypeScan">确认</button></view><text class="operation-scan-tip" > 支持 Android PDA 广播扫码 , 也可手工输入单号 < / text > < / view >
< view class = "operation-metrics" > < view v-for = "metric in prototype. met rics " :key="metric.label" class="operation-metric"><text class="operation-metric-value" :class="metric.tone">{{ metric.value }}</text><text class="operation-metric-label" > {{ metric.label }} < / text > < / view > < / view >
< view class = "task-toolbar" > < text class = "group-title operation-group-title" > 任务列表 < / text > < button class = "filter-button" @tap ="showToast('筛选面板为静态原型', 'warning')" > 筛选 < / button > < / view >
< view class = "prototype-tabs " > < button v-for = "item in tabs" :key="item" class="prototype-tab " : class="{ active: tab === item }" @tap="tab = item" > {{ item }} < / button > < / view >
< view v-if = "filteredTasks.length" class="prototype-task-list"><view v-for="task in filteredTasks" :key="task.code" class="prototype-task-card"><view class="prototype-task-head"><text class="prototype-task-code">{{ task.code }}</text><text class="prototype-task-status" :class="task.tone">{{ task.status }}</text></view><text class="prototype-task-title">{{ task.title }}</text><view class="prototype-task-meta"><text>{{ task.meta }}</text><text>{{ task.location }}</text></view><view class="prototype-task-foot"><text class="prototype-task-qty">{{ task.quantity }}</text><button class="prototype-task-action" @tap="showToast(`${task.action}: ${task.code}`, 'success')" > {{ task.action }} < / button > < / view > < / view > < / view >
< view v-else class = "operation-empty " > < text class = "operation-empty-icon" > ✓ < / text > < text class = "history-empty-title " > 暂无 { { tab } } 任务 < / text > < text class = "history-empty-desc " > 切换其他状态查看演示数据 < / text > < / view >
< / view >
< view v-if = "toast.visible" class="app-toast" :class="toast.type" role="status" > {{ toast.message }} < / view >
< view class = "operation-page-title-row" > < view > < text class = "operation-page-title" > { { operationName } } < / text > < text class = "operation-page-subtitle" > { { inboundMode ? '海外仓真实入仓作业' : prototype . subtitle } } < / text > < / view > < text class = "prototype-badge" > { { inboundMode ? '真实接口' : '静态原型' } } < / text > < / view >
< template v-if = "inboundMode" >
< view class = "work-card" > < text class = "card-title" > 任务范围 < / text > < input v-model = "form.customerId" class="work-input" placeholder="客户ID(平台作业必填)" inputmode="nu meric" /><view class="operation-scan-row"><input v-model="form.planNo" class="operation-scan-input" placeholder="扫描或输入入仓计划号" confirm-type="search" @confirm="loadTask" /><button class="operation-scan-button" :disabled="busy" @tap="loadTask">查询</button></view><view class="button-row"><button class="secondary-button" :disabled="busy" @tap="pushTask">推送/到仓登记</button><button class="secondary-button" :disabled="busy || !task" @tap="loadTask" > 刷新任务 < / button > < / view > < / view >
< view v-if = "task" class="work-card task-summary"><text class="card-title">{{ task.taskNo }}</text><text>计划:{{ task.planNo }}</text><text>状态:{{ task.status }}</text><text>卸柜照片:{{ task.unloadPhotoFileIds?.length || 0 }} 张</text><text v-if="task.estimatedEmptyReturnAt">预计报空:{{ task.estimatedEmptyReturnAt }}</text><button v-for="item in task.pallets || []" :key="item.planPalletId" class="instruction-row" @tap="selectPallet(item)">托盘 {{ item.palletNo }} · {{ item.assignmentStatus }} · 实收 {{ item.actualPackageCount }} 箱</button><button v-for="item in task.allocations || []" :key="item.allocationId" class="instruction-row" @tap="selectAllocation(item)" > 入仓 {{ item.inboundCode }} · 计划 {{ item.plannedPackageCount }} · 已收 {{ item.pickedPackageCount }} < / button > < / view >
< view class = "work-card " > < text class = "card-title" > 托盘资源分配 < / text > < input v-model = "form.planPalletId" class="work-input" placeholder="计划托盘ID" inputmode="numeric" /><input v-model="form.warehouseId" class="work-input" placeholder="海外仓ID" inputmode="numeric" /><input v-model="form.palletId " class="work-input" placeholder="实物托盘ID" inputmode="numeric" /><input v-model="form.locationId" class="work-input" placeholder="目标库位ID" inputmode="numeric" /><button class="primary-action" :disabled="busy" @tap="assignPallet" > 确认分配 < / button > < / view >
< view class = "work-card" > < text class = "card-title" > 卸柜实收 < / text > < input v-model = "form.allocationId" class="work-input" placeholder="分配明细ID" inputmode="numeric" /><input v-model.number="form.packageCount" class="work-input" placeholder="本次箱数" inputmode="numeric" type="number" /><view class="button-row"><button class="secondary-button" :disabled="busy" @tap="countPackages">确认点数</button><button class="secondary-button" :disabled="busy" @tap="pickPackages">确认实收</button></view><input v-model="form.cartonMark" class="work-input" placeholder="扫描国内冻结箱唛" @confirm="scanCarton" /><view class="button-row"><button class="secondary-button" :disabled="busy" @tap="scanCarton">绑定托盘</button><button class="danger-button" :disabled="busy" @tap="removeCarton" > 移除箱唛 < / button > < / view > < / view >
< view class = "work-card " > < text class = "card-title" > 卸柜照片与完成 < / text > < button class = "secondary-button full-button" :disabled = "busy" @tap ="chooseUnloadPhotos "> 选择并上传卸柜照片 < / button > < text v-if = "photoFileIds.length" class="hint-text">已上传文件ID: {{ photoFileIds.join('、') }}</text><button class="secondary-button full-button" :disabled="busy || !photoFileIds.length" @tap="saveUnloadPhotos">保存照片凭证</button><button class="primary-action complete-button" :disabled="busy || !task" @tap="completeTask " > 完成入仓并入库存 < / button > < / view >
< view v-if = "lastResult" class="result-card"><text class="card-title">最近结果</text><text>{{ lastResult }}</text></view><view v-if="error" class="error-card"><text>{{ error.message }}</text><text v-if="error.code">code={{ error.code }}</text><text v-if="error.requestId">requestId={{ error.requestId }}</text><text v-if="error.traceId" > traceId = { { error . traceId } } < / text > < / view >
< / template >
< template v-else > < view class = "operation-scan-card" > < text class = "operation-scan-label" > { { prototype . scanLabel } } < / text > < view class = "operation-scan-row" > < input v-model = "scanValue" class="operation-scan-input" :placeholder="prototype.scanPlaceholder" @confirm="prototypeScan" /><button class="operation-scan-button" @tap="prototypeScan">确认</button></view><text class="operation-scan-tip" > 该库内模块仍待后续真实接口接入 < / text > < / view > < / template >
< / view > < view v-if = "toast.visible" class="app-toast" :class="toast.type" role="status" > {{ toast.message }} < / view >
< / view >
< / template >
< script >
import PortalHeader from '@/components/PortalHeader.vue'
import { MODULES , getOperationPrototype } from '@/data/overseas-workbench.js'
import { getOperationPrototype } from '@/data/overseas-workbench.js'
import { session , requireSession } from '@/stores/session.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { playSuccessSound } from '@/utils/audio-feedback.js'
import { assignInboundPallet , bindUnloadPhotos , completeInboundTask , countInboundPackages , getInboundTask , pickInboundPackages , pushInboundTask , removeInboundCarton , scanInboundCarton , uploadUnloadPhotoFile } from '@/api/inbound.js'
const positive = ( value ) => / ^ \ d + $ / . test ( String ( value || '' ) ) && Number ( value ) > 0
export default {
components : { PortalHeader } ,
data ( ) { return { user : session . currentUser , moduleKey : 'query' , operationName : '库存查询' , scanValue : '' , tabs : [ '待处理' , '进行中' , '已完成' ] , tab : '待处理' , toast : { visible : false , message : '' , type : '' } } } ,
computed : { currentModule ( ) { return MODULES . find ( ( module ) => module . key === this . moduleKey ) || MODULES [ 0 ] } , prototype ( ) { return getOperationPrototype ( this . operationName ) } , filteredTasks ( ) { return this . prototype . tasks . filter ( ( task ) => task . status === this . tab ) } } ,
onLoad ( options ) { this . moduleKey = options . module || 'query' ; this . operationName = decodeURIComponent ( options . name || '库存查询' ) } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; this . startScanner ( ) } ,
onHide ( ) { this . stopScanner ( ) } ,
onUnload ( ) { this . stopScanner ( ) } ,
beforeUnmount ( ) { clearTimeout ( this . _toastTimer ) ; this . stopScanner ( ) } ,
data ( ) { return { user : session . currentUser , moduleKey : 'query' , operationName : '库存查询' , scanValue : '' , busy : false , task : null , error : null , lastResult : '' , photoFileIds : [ ] , keys : { } , form : { customerId : '' , planNo : '' , planPalletId : '' , warehouseId : '' , palletId : '' , locationId : '' , allocationId : '' , packageCount : 1 , cartonMark : '' } , toast : { visible : false , message : '' , type : '' } } } ,
computed : { prototype ( ) { return getOperationPrototype ( this . operationName ) } , inboundMode ( ) { return this . moduleKey === 'inbound' } } ,
onLoad ( options ) { this . moduleKey = options . module || 'query' ; this . operationName = decodeURIComponent ( options . name || '库存查询' ) ; if ( session . tenantId && session . tenantId !== '0' ) this . form . customerId = session . tenantId } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; this . startScanner ( ) } , onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } , beforeUnmount ( ) { clearTimeout ( this . _toastTimer ) ; this . stopScanner ( ) } ,
methods : {
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => { this . scanValue = value ; this . prototypeScan ( ) } } ) } ,
stopScanner ( ) { if ( typeof this . _stopScanner === 'function' ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
prototypeScan ( ) { const value = String ( this . scanValue || '' ) . trim ( ) ; if ( ! value ) return this . showToast ( '请先扫描或输入编码' , 'warning' ) ; playSuccessSound ( ) ; this . showToast ( ` 已读取 ${ value } ;业务接口待接入 ` , 'success' ) } ,
showToast ( message , type ) { clearTimeout ( this . _toastTimer ) ; this . toast = { visible : true , message , type } ; this . _toastTimer = setTimeout ( ( ) => { this . toast . visible = false } , 2200 ) }
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => { if ( this . inboundMode ) { if ( ! this . form . planNo ) this . form . planNo = value ; else this . form . cartonMark = value } else this . scanValue = value ; playSuccessSound ( ) } } ) } , stopScanner ( ) { if ( typeof this . _stopScanner === 'function' ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
validateScope ( ) { if ( ! this . form . customerId . trim ( ) ) throw new Error ( '请输入目标客户ID' ) ; if ( ! this . form . planNo . trim ( ) ) throw new Error ( '请扫描或输入入仓计划号' ) } ,
key ( action , payload ) { const signature = ` ${ action } : ${ JSON . stringify ( payload ) } ` ; if ( ! this . keys [ signature ] ) this . keys [ signature ] = ` pda- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ; return { signature , value : this . keys [ signature ] } } ,
async execute ( action , work ) { this . busy = true ; this . error = null ; try { const value = await work ( ) ; this . lastResult = action ; this . showToast ( ` ${ action } 成功 ` , 'success' ) ; return value } catch ( error ) { this . error = error ; this . showToast ( error . message || ` ${ action } 失败 ` , 'warning' ) ; throw error } finally { this . busy = false } } ,
operator ( ) { return { operatorId : String ( this . user ? . id || '' ) , operatorName : this . user ? . name || 'PDA作业员' } } ,
selectPallet ( item ) { this . form . planPalletId = String ( item . planPalletId ) ; if ( item . warehousePalletId ) this . form . palletId = String ( item . warehousePalletId ) ; if ( item . warehouseLocationId ) this . form . locationId = String ( item . warehouseLocationId ) } ,
selectAllocation ( item ) { this . form . allocationId = String ( item . allocationId ) ; this . form . planPalletId = String ( item . planPalletId ) ; this . form . packageCount = Math . max ( 1 , Number ( item . plannedPackageCount || 0 ) - Number ( item . pickedPackageCount || 0 ) ) } ,
async loadTask ( ) { try { this . validateScope ( ) ; this . task = await this . execute ( '查询入仓任务' , ( ) => getInboundTask ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) ) ) } catch ( _ ) { } } ,
async pushTask ( ) { try { this . validateScope ( ) ; const payload = this . operator ( ) ; const key = this . key ( 'push' , payload ) ; this . task = await this . execute ( '推送入仓任务' , ( ) => pushInboundTask ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , { ... payload , idempotencyKey : key . value } ) ) ; delete this . keys [ key . signature ] } catch ( _ ) { } } ,
async assignPallet ( ) { try { this . validateScope ( ) ; for ( const field of [ 'planPalletId' , 'warehouseId' , 'palletId' , 'locationId' ] ) if ( ! positive ( this . form [ field ] ) ) throw new Error ( '计划托盘、仓库、实物托盘和库位ID均必须填写' ) ; const payload = { warehouseId : this . form . warehouseId , palletId : this . form . palletId , locationId : this . form . locationId } ; const key = this . key ( 'assign' , payload ) ; await this . execute ( '分配托盘资源' , ( ) => assignInboundPallet ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , this . form . planPalletId , { ... payload , idempotencyKey : key . value } ) ) ; delete this . keys [ key . signature ] } catch ( error ) { if ( ! this . error ) this . error = error } } ,
packagePayload ( ) { if ( ! positive ( this . form . allocationId ) || ! positive ( this . form . packageCount ) ) throw new Error ( '分配明细ID和箱数必须大于0' ) ; return { packageCount : Number ( this . form . packageCount ) , ... this . operator ( ) } } ,
async countPackages ( ) { try { this . validateScope ( ) ; const payload = this . packagePayload ( ) ; const key = this . key ( 'count' , payload ) ; await this . execute ( '确认点数' , ( ) => countInboundPackages ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , this . form . allocationId , { ... payload , idempotencyKey : key . value } ) ) ; delete this . keys [ key . signature ] } catch ( error ) { if ( ! this . error ) this . error = error } } ,
async pickPackages ( ) { try { this . validateScope ( ) ; const payload = this . packagePayload ( ) ; const key = this . key ( 'pick' , payload ) ; await this . execute ( '确认实收' , ( ) => pickInboundPackages ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , this . form . allocationId , { ... payload , idempotencyKey : key . value } ) ) ; delete this . keys [ key . signature ] } catch ( error ) { if ( ! this . error ) this . error = error } } ,
async scanCarton ( ) { try { this . validateScope ( ) ; if ( ! this . form . cartonMark . trim ( ) || ! positive ( this . form . planPalletId ) ) throw new Error ( '请扫描箱唛并填写计划托盘ID' ) ; const payload = { cartonMark : this . form . cartonMark . trim ( ) , planPalletId : this . form . planPalletId , ... this . operator ( ) } ; const key = this . key ( 'scan' , payload ) ; await this . execute ( '箱唛绑定托盘' , ( ) => scanInboundCarton ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , { ... payload , idempotencyKey : key . value } ) ) ; delete this . keys [ key . signature ] ; this . form . cartonMark = '' } catch ( error ) { if ( ! this . error ) this . error = error } } ,
async removeCarton ( ) { try { this . validateScope ( ) ; if ( ! this . form . cartonMark . trim ( ) ) throw new Error ( '请扫描需要移除的箱唛' ) ; const payload = { cartonMark : this . form . cartonMark . trim ( ) , ... this . operator ( ) } ; const key = this . key ( 'remove' , payload ) ; await this . execute ( '移除箱唛' , ( ) => removeInboundCarton ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , { ... payload , idempotencyKey : key . value } ) ) ; delete this . keys [ key . signature ] } catch ( error ) { if ( ! this . error ) this . error = error } } ,
chooseUnloadPhotos ( ) { try { this . validateScope ( ) ; uni . chooseImage ( { count : 9 , sourceType : [ 'camera' , 'album' ] , success : async ( { tempFilePaths } ) => { try { for ( const path of tempFilePaths ) { const file = await this . execute ( '上传卸柜照片' , ( ) => uploadUnloadPhotoFile ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , path ) ) ; if ( file ? . fileId && ! this . photoFileIds . includes ( String ( file . fileId ) ) ) this . photoFileIds . push ( String ( file . fileId ) ) } } catch ( _ ) { } } } ) } catch ( error ) { this . error = error } } ,
async saveUnloadPhotos ( ) { try { this . validateScope ( ) ; if ( ! this . photoFileIds . length ) throw new Error ( '请先上传卸柜照片' ) ; const payload = { fileIds : this . photoFileIds , ... this . operator ( ) } ; const key = this . key ( 'photos' , payload ) ; this . task = await this . execute ( '保存卸柜照片' , ( ) => bindUnloadPhotos ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) , { ... payload , idempotencyKey : key . value } ) ) ; delete this . keys [ key . signature ] } catch ( error ) { if ( ! this . error ) this . error = error } } ,
async completeTask ( ) { try { this . validateScope ( ) ; this . task = await this . execute ( '完成入仓' , ( ) => completeInboundTask ( this . form . planNo . trim ( ) , this . form . customerId . trim ( ) ) ) } catch ( _ ) { } } ,
prototypeScan ( ) { const value = String ( this . scanValue || '' ) . trim ( ) ; if ( ! value ) return this . showToast ( '请先扫描或输入编码' , 'warning' ) ; this . showToast ( '该模块尚未接入真实业务接口' , 'warning' ) } ,
showToast ( message , type ) { clearTimeout ( this . _toastTimer ) ; this . toast = { visible : true , message , type } ; this . _toastTimer = setTimeout ( ( ) => { this . toast . visible = false } , 2600 ) }
}
}
< / script >
< style scoped >
. work - card , . result - card , . error - card { margin - bottom : 12 px ; padding : 14 px ; border - radius : 12 px ; background : # fff ; box - shadow : 0 4 px 15 px rgba ( 32 , 67 , 111 , .06 ) } . card - title , . task - summary text , . hint - text , . error - card text { display : block ; margin - bottom : 8 px } . card - title { font - weight : 700 } . instruction - row { width : 100 % ; min - height : 38 px ; margin - top : 6 px ; padding : 7 px 9 px ; color : # 23456 f ; font - size : 11 px ; text - align : left ; border : 1 px solid # d7e4f5 ; background : # f5f9ff } . work - input { box - sizing : border - box ; width : 100 % ; height : 44 px ; margin - bottom : 9 px ; padding : 0 11 px ; border : 1 px solid # cfd8e3 ; border - radius : 8 px ; background : # fbfdff } . button - row { display : flex ; gap : 8 px } . secondary - button , . danger - button , . primary - action { min - height : 42 px ; flex : 1 ; font - size : 13 px } . secondary - button { color : # 135 ee8 ; border : 1 px solid # 8 cb5f5 ; background : # edf4ff } . danger - button { color : # b42318 ; border : 1 px solid # f2a29b ; background : # fff1f0 } . primary - action { width : 100 % ; color : # fff ; background : # 135 ee8 } . full - button , . complete - button { width : 100 % ; margin - top : 8 px } . hint - text { margin - top : 8 px ; color : # 52657 c ; font - size : 11 px ; word - break : break - all } . error - card { color : # b42318 ; background : # fff1f0 } . result - card { color : # 176 b3a ; background : # eefbf3 }
< / style >