@@ -43,6 +43,16 @@
< template v-else-if = "phase === 'sign'" >
< ReceivingSessionSummary :session = "current" / >
< view class = "photo-card domestic-photo-card" >
< view class = "card-head" > < view > < text class = "card-title" > 国内装柜照片 ( 只读 ) < / text > < text class = "container-meta" > 现场对照国内装柜过程与封柜状态 < / text > < / view > < text class = "status-tag status-good" > { { domesticPhotoCount } } / 5 < / text > < / view >
< view class = "domestic-photo-grid" >
< view v-for = "photo in domesticLoadingPhotos" :key="photo.evidenceType" class="domestic-photo-item" @tap="previewDomesticPhoto(photo)" >
< image v-if = "photo.localPath" class="domestic-photo-thumb" :src="photo.localPath" mode="aspectFill" / >
< view v-else class = "domestic-photo-placeholder" > < uni-load-more v-if = "photo.status === 'loading'" status="loading" :show-text="false" /><uni-icons v-else type="image" color="#8090a3" size="22" / > < text > { { photo . status === 'error' ? '加载失败,点按重试' : '正在准备照片' } } < / text > < / view >
< text > { { photo . label } } < / text >
< / view >
< / view >
< / view >
< view class = "photo-card" >
< view class = "card-head" > < view > < text class = "card-title" > 卸货签收照片 < / text > < text class = "container-meta" > 三个固定槽位各拍摄 1 张现场照片 < / text > < / view > < text class = "status-tag" : class = "photosReady ? 'status-good' : 'status-wait'" > { { uploadedPhotoCount } } / 3 < / text > < / view >
< view class = "photo-grid" >
@@ -65,6 +75,16 @@
< template v-else-if = "phase === 'receive'" >
< ReceivingSessionSummary :session = "current" / >
< view class = "photo-card domestic-photo-card domestic-photo-collapsible" >
< view class = "card-head" @tap ="domesticPhotosExpanded = !domesticPhotosExpanded" > < view > < text class = "card-title" > 国内装柜照片 < / text > < text class = "container-meta" > 已同步 { { domesticPhotoCount } } / 5 , 点按 { { domesticPhotosExpanded ? '收起' : '展开' } } 对照 < / text > < / view > < text class = "container-detail-link" > { { domesticPhotosExpanded ? '收起' : '查看' } } › < / text > < / view >
< view v-if = "domesticPhotosExpanded" class="domestic-photo-grid" >
< view v-for = "photo in domesticLoadingPhotos" :key="photo.evidenceType" class="domestic-photo-item" @tap.stop="previewDomesticPhoto(photo)" >
< image v-if = "photo.localPath" class="domestic-photo-thumb" :src="photo.localPath" mode="aspectFill" / >
< view v-else class = "domestic-photo-placeholder" > < uni-load-more v-if = "photo.status === 'loading'" status="loading" :show-text="false" /><uni-icons v-else type="image" color="#8090a3" size="22" / > < text > { { photo . status === 'error' ? '加载失败,点按重试' : '正在准备照片' } } < / text > < / view >
< text > { { photo . label } } < / text >
< / view >
< / view >
< / view >
< view class = "scan-card" >
< text class = "section-label" > 1. 扫描 DP 、 箱码或 FBA 任一箱 < / text >
< ScanTargetInput name = "carton" v-model = "cartonInput" label="货物码" placeholder="扫描 DP 或箱码" :active="activeTarget === 'carton'" @activate="activeTarget = 'carton'" @submit="resolveCarton" / >
@@ -122,16 +142,17 @@
import { idempotencyKey , receiving , receivingPhase , receivingProgress , releaseIdempotencyKey , resetReceivingDraft , setCurrentReceiving } from '@/stores/receiving.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { playErrorSound , playSuccessSound } from '@/utils/audio-feedback.js'
import { completeCurrentReceiving , downloadCurrentUnloadPhoto , getCurrentReceiving , getCurrentWarehouseProfile , getReceivingContainer , listReceivingCandidates , prepareReceivingContainer , receiveCurrentBulk , receiveCurrentCarton , receiveCurrentWholePallet , resolveCurrentCarton , resolveCurrentPallet , signCurrentReceiving , uploadCurrentUnloadPhoto } from '@/api/inbound.js'
import { completeCurrentReceiving , downloadCurrentDomesticLoadingPhoto , downloadCurrentUnloadPhoto , getCurrentReceiving , getCurrentWarehouseProfile , getReceivingContainer , listReceivingCandidates , prepareReceivingContainer , receiveCurrentBulk , receiveCurrentCarton , receiveCurrentWholePallet , resolveCurrentCarton , resolveCurrentPallet , signCurrentReceiving , uploadCurrentUnloadPhoto } from '@/api/inbound.js'
const MAX _UNLOAD _PHOTO _BYTES = 10 * 1024 * 1024
export default {
components : { PortalHeader , ReceivingSessionSummary , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , moduleKey : 'query' , operationName : '库存查询' , loading : false , candidateLoading : false , busy : false , bootstrapped : false , detailOpen : 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 , 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 ] } ) ) } ,
candidates ( ) { return receiving . candidates } , selected ( ) { return receiving . selected } , current ( ) { return receiving . current } , photoSlots ( ) { return receiving . photoSlots } ,
domesticLoadingPhotos ( ) { return this . current ? . domesticLoadingPhotos || [ ] } , domesticPhotoCount ( ) { return this . domesticLoadingPhotos . length } ,
resolvedCarton ( ) { return receiving . resolvedCarton } , resolvedPallet ( ) { return receiving . resolvedPallet } , lastReceipt ( ) { return receiving . lastReceipt } ,
activeTarget : { get ( ) { return receiving . activeScanTarget } , set ( value ) { receiving . activeScanTarget = value } } ,
phase ( ) { return this . detailOpen && ! this . current ? 'detail' : receivingPhase ( this . current ) } , progress ( ) { return receivingProgress ( this . current ) } , activeInbound ( ) { return this . resolvedCarton } ,
@@ -162,7 +183,10 @@
async run ( action , work ) { this . busy = true ; this . error = null ; try { const result = await work ( ) ; this . showToast ( ` ${ action } 成功 ` , 'success' ) ; return result } catch ( error ) { const mappedError = toReceivingError ( error , ` ${ action } 失败 ` ) ; this . error = mappedError ; playErrorSound ( ) ; this . showToast ( mappedError . message , 'error' ) ; throw mappedError } finally { this . busy = false ; setTimeout ( ( ) => this . drainScanQueue ( ) , 0 ) } } ,
setScanTargetForCurrent ( value ) { receiving . activeScanTarget = value ? . status === 'WAIT_SIGN' ? 'container' : 'carton' } ,
async restorePhotoPreviews ( ) { await Promise . all ( receiving . photoSlots . filter ( ( photo ) => photo ? . downloadPath && ! photo . localPath ) . map ( async ( photo ) => { photo . status = 'restoring' ; try { photo . localPath = await downloadCurrentUnloadPhoto ( photo . downloadPath ) ; photo . status = 'success' } catch ( error ) { photo . status = 'error' ; photo . errorMessage = error . message || '照片加载失败' ; this . error = error } } ) ) } ,
async applyCurrentReceiving ( value ) { this . validateReceivingWarehouse ( value , '当前接柜会话' ) ; setCurrentReceiving ( value ) ; await this . restorePhotoPreviews ( ) ; this . setScanTargetForCurrent ( value ) } ,
async restoreDomesticPhotoPreviews ( ) { await Promise . all ( this . domesticLoadingPhotos . filter ( ( photo ) => photo . downloadPath && ! photo . localPath ) . map ( ( photo ) => this . loadDomesticPhoto ( photo ) ) ) } ,
async loadDomesticPhoto ( photo ) { if ( ! photo ? . downloadPath || photo . status === 'loading' ) return ; photo . status = 'loading' ; try { photo . localPath = await downloadCurrentDomesticLoadingPhoto ( photo . downloadPath ) ; photo . status = 'success' } catch ( _ ) { photo . status = 'error' } } ,
async previewDomesticPhoto ( photo ) { if ( ! photo ? . localPath ) await this . loadDomesticPhoto ( photo ) ; if ( photo ? . localPath ) { const urls = this . domesticLoadingPhotos . map ( ( item ) => item . localPath ) . filter ( Boolean ) ; uni . previewImage ( { urls , current : photo . localPath } ) } } ,
async applyCurrentReceiving ( value ) { this . validateReceivingWarehouse ( value , '当前接柜会话' ) ; setCurrentReceiving ( value ) ; await Promise . all ( [ this . restorePhotoPreviews ( ) , this . restoreDomesticPhotoPreviews ( ) ] ) ; this . setScanTargetForCurrent ( value ) } ,
async bootstrap ( ) { if ( this . loading ) return ; this . loading = true ; this . error = null ; this . detailOpen = false ; try { await this . refreshBoundWarehouse ( ) ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . candidateError = null } catch ( error ) { if ( ! isNoCurrentReceiving ( error ) ) throw error ; resetReceivingDraft ( ) ; await this . loadCandidates ( false , false ) } this . bootstrapped = true } catch ( error ) { this . candidateError = error } finally { this . loading = false } } ,
async recoverCurrentAfterConflict ( error , preserveEvidence = false ) { if ( ! isReceivingConflict ( error ) ) return false ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . detailOpen = false ; this . error = preserveEvidence ? error : null ; this . showToast ( ` 账号已有未完成柜 ${ current . containerNo } ,已返回当前柜 ` , 'warning' ) ; return true } catch ( recoveryError ) { if ( isNoCurrentReceiving ( recoveryError ) ) { this . detailOpen = false ; receiving . selected = null ; await this . loadCandidates ( false ) ; this . candidateError = preserveEvidence ? error : null ; this . error = preserveEvidence ? error : null ; this . showToast ( '柜状态已变化,列表已刷新' , 'warning' ) ; return true } this . error = error ; return false } } ,
async loadCandidates ( feedback = true , refreshWarehouse = true ) { if ( this . candidateLoading ) return ; this . candidateLoading = true ; this . candidateError = null ; this . error = null ; try { if ( refreshWarehouse ) await this . refreshBoundWarehouse ( ) ; const values = await listReceivingCandidates ( ) ; receiving . candidates = values . map ( ( item ) => this . validateReceivingWarehouse ( item , ` 柜号 ${ item . containerNo || '—' } ` ) ) ; if ( feedback ) this . showToast ( '刷新待卸柜成功' , 'success' ) } catch ( error ) { receiving . candidates = [ ] ; this . candidateError = error ; if ( feedback ) { playErrorSound ( ) ; this . showToast ( error . message || '刷新待卸柜失败' , 'error' ) } } finally { this . candidateLoading = false } } ,
@@ -203,6 +227,7 @@
. photo - error { margin - top : 3 px ; overflow : hidden ; color : # c9362b ; font - size : 9 px ; line - height : 13 px ; text - overflow : ellipsis ; white - space : nowrap }
. photo - actions { display : grid ; grid - template - columns : 1 fr }
. photo - actions button { box - sizing : border - box ; min - height : 44 px ; padding : 0 5 px ; border : 1 px solid # 8 fb8f5 ; border - radius : 8 px ; color : # 1668 dc ; font - size : 11 px ; font - weight : 700 ; line - height : 44 px ; background : # fff }
. domestic - photo - card { border - color : # b9d3f4 ; background : # f9fbff } . domestic - photo - collapsible . card - head { cursor : pointer } . domestic - photo - grid { display : grid ; grid - template - columns : repeat ( 5 , minmax ( 0 , 1 fr ) ) ; gap : 7 px ; margin - top : 11 px } . domestic - photo - item { display : flex ; min - width : 0 ; align - items : center ; flex - direction : column ; gap : 5 px ; color : # 315777 ; font - size : 10 px ; font - weight : 700 } . domestic - photo - thumb , . domestic - photo - placeholder { box - sizing : border - box ; width : 100 % ; height : 66 px ; border : 1 px solid # cddbeb ; border - radius : 8 px ; background : # edf4fb } . domestic - photo - placeholder { display : flex ; align - items : center ; flex - direction : column ; justify - content : center ; gap : 2 px ; padding : 3 px ; color : # 75869 a ; font - size : 8 px ; line - height : 11 px ; text - align : center } . domestic - photo - thumb { display : block }
. matched - inbound - card { margin : 14 px 0 ; padding : 12 px ; border : 1 px solid # cfe0f2 ; border - radius : 10 px ; background : # f8fbff }
. matched - inbound - card . mode - banner { margin - bottom : 10 px }
. matched - inbound - code { display : block ; color : # 17324 d ; font - size : 15 px ; font - weight : 800 }
@@ -210,7 +235,7 @@
. matched - inbound - progress { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 6 px ; margin - top : 8 px ; color : # 5 f7185 ; font - size : 10 px }
. matched - inbound - progress text { padding : 6 px 4 px ; border - radius : 6 px ; text - align : center ; background : # edf3f9 }
. error - card text { overflow - wrap : anywhere ; word - break : break - word }
@ media ( max - width : 360 px ) { . photo - role { grid - template - columns : 68 px minmax ( 0 , 1 fr ) 64 px ; gap : 7 px ; padding : 8 px } . photo - preview { width : 68 px ; height : 64 px } . photo - actions button { min - height : 34 px ; padding : 0 2 px ; line - height : 34 px } . photo - state { font - size : 9 px ; line - height : 13 px } }
@ media ( max - width : 360 px ) { . photo - role { grid - template - columns : 68 px minmax ( 0 , 1 fr ) 64 px ; gap : 7 px ; padding : 8 px } . photo - preview { width : 68 px ; height : 64 px } . photo - actions button { min - height : 34 px ; padding : 0 2 px ; line - height : 34 px } . photo - state { font - size : 9 px ; line - height : 13 px } . domestic - photo - grid { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) } }
. receiving - page { padding - bottom : env ( safe - area - inset - bottom , 0 px ) }
. receiving - content { box - sizing : border - box ; padding - bottom : calc ( 24 px + env ( safe - area - inset - bottom , 0 px ) ) }
. receiving - content - with - action { padding - bottom : calc ( 152 px + env ( safe - area - inset - bottom , 0 px ) ) }