326 lines
47 KiB
Vue
326 lines
47 KiB
Vue
<template>
|
||
<view v-if="user" class="page-shell portal-page work-page">
|
||
<PortalHeader :title="title" :user="user" fallback-url="/pages/module/module?key=relabel" />
|
||
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
|
||
<view class="work-heading"><text class="work-title">{{ title }}</text><text class="work-subtitle">{{ stageSubtitle }}</text></view>
|
||
<view v-if="stage === 'execute' && showCurrent" class="detail-toolbar"><view><text class="detail-kicker">当前任务</text><text class="detail-title">换标执行详情</text></view><button :disabled="busy" @tap="closeCurrent">‹ 返回列表</button></view>
|
||
|
||
<template v-if="stage !== 'execute' || !showCurrent">
|
||
<view class="row-head"><text class="card-title">{{ candidateHeading }}</text><button v-if="stage !== 'review'" class="link-button" :disabled="loading" @tap="bootstrap">{{ loading ? '刷新中…' : '刷新' }}</button></view>
|
||
<view v-if="loading && !visibleCandidates.length" class="empty-box"><uni-load-more status="loading" /><text class="empty-copy">正在加载换标任务…</text></view>
|
||
<view v-else-if="!visibleCandidates.length" class="empty-box"><uni-icons type="info" color="#8090a3" size="32" /><text class="empty-copy">{{ emptyText }}</text></view>
|
||
<view v-else-if="stage !== 'review'" class="task-list"><view v-for="item in visibleCandidates" :key="item.relabelNo" class="task-card" :class="{ current: current?.relabelNo === item.relabelNo }" @tap="openExecution(item)"><view class="row-head"><view><text class="business-code">{{ item.relabelNo }}</text><text class="meta-line">客户订单:{{ item.customerOrderNo || '—' }}</text><text class="meta-line">换标类型:{{ relabelTypeName(item.operationType) }}</text></view><text class="status-pill">{{ statusLabel(item.taskStatus || item.applicationStatus) }}</text></view><view class="metric-row"><view class="metric"><strong>{{ item.expectedPackages }}</strong><text>应处理</text></view><view class="metric"><strong>{{ item.relabelledPackages }}</strong><text>已换标</text></view><view class="metric"><strong>{{ item.skippedPackages + item.exceptionPackages }}</strong><text>跳过/异常</text></view><text class="link-button">查看并执行 ›</text></view></view></view>
|
||
<view v-else class="task-list"><view v-for="item in visibleCandidates" :key="item.relabelNo" class="task-card"><view class="row-head"><view><text class="business-code">{{ item.relabelNo }}</text><text class="meta-line">客户订单:{{ item.customerOrderNo || '—' }}</text><text class="meta-line">换标类型:{{ relabelTypeName(item.operationType) }}</text></view><text class="status-pill">{{ statusLabel(item.taskStatus || item.applicationStatus) }}</text></view><view class="metric-row"><view class="metric"><strong>{{ item.expectedPackages }}</strong><text>应处理</text></view><view class="metric"><strong>{{ item.relabelledPackages }}</strong><text>已换标</text></view><view class="metric"><strong>{{ item.skippedPackages + item.exceptionPackages }}</strong><text>跳过/异常</text></view></view></view></view>
|
||
|
||
</template>
|
||
<view v-if="showCurrent" class="work-card">
|
||
<text class="status-pill">换标类型:{{ relabelTypeName(current.operationType) }}</text>
|
||
<view class="row-head"><view><text class="business-code">{{ current.relabelNo }}</text><text class="meta-line">客户订单:{{ current.customerOrderNo || '—' }}</text><text class="meta-line">标签截止:{{ formatTime(current.labelDeadline) }}</text></view><text class="status-pill">{{ statusLabel(current.taskStatus || current.applicationStatus) }}</text></view>
|
||
<view class="metric-row"><view class="metric"><strong>{{ current.expectedPackages }}</strong><text>应处理</text></view><view class="metric"><strong>{{ current.relabelledPackages }}</strong><text>已换标</text></view><view class="metric"><strong>{{ current.skippedPackages }}</strong><text>跳过</text></view><view class="metric"><strong>{{ current.exceptionPackages }}</strong><text>异常</text></view></view>
|
||
<view class="section-gap"><view class="row-head"><text class="card-title">标签文件</text><button class="link-button" :disabled="busy" @tap="loadDocuments">刷新文件</button></view><text v-if="!documents.length" class="meta-line">暂无标签文件</text><view v-for="file in documents" :key="`${file.scope}:${file.fileId}`" class="row-head"><text class="meta-line">{{ file.scope === 'CARTON_TOTAL' ? '总箱唛' : file.scope === 'SKU_TOTAL' ? '总 SKU 标签' : '明细标签' }}:{{ file.fileName }}</text><button class="secondary" :disabled="busy" @tap="printDocument(file)">查看 / 打印</button></view></view>
|
||
<template v-if="stage === 'execute'">
|
||
<view v-if="current.reviewDecision === 'REWORK' && current.reviewRemark" class="error-box section-gap"><text class="error-title">退回重新贴标</text><text>{{ current.reviewRemark }}</text></view>
|
||
<view v-if="current.taskStatus !== 'PROCESSING'" class="empty-box section-gap"><text class="empty-copy">当前任务不是处理中状态,请返回换标列表选择可执行任务,或等待复核退回。</text></view>
|
||
<template v-else>
|
||
<view v-if="availableTabs.length" class="tab-row" role="tablist"><button v-for="tab in availableTabs" :key="tab.key" class="tab-button" role="tab" :aria-selected="taskTab === tab.key" :class="{ active: taskTab === tab.key }" :disabled="busy" @tap="changeTab(tab.key)">{{ tab.name }}</button></view>
|
||
<text class="card-title section-gap">{{ isReturnBulk ? '退货贴标 SKU 明细' : taskTab === 'sku' ? 'SKU 换标明细' : '箱唛换标明细' }}</text>
|
||
<view v-if="isReturnBulk" class="section-gap"><text class="meta-line">先扫描实际贴标的 SKU,核对数量后登记;无需扫描箱唛。</text><ScanTargetInput name="returnSku" v-model="returnSkuCode" label="扫描退货 SKU" placeholder="扫描或输入待贴标 SKU" :active="activeTarget === 'return-sku'" @activate="activeTarget = 'return-sku'" @submit="verifyReturnSku" /><button class="secondary" :disabled="busy || !returnSkuCode.trim()" @tap="verifyReturnSku(returnSkuCode)">识别 SKU</button><text v-if="verifiedReturnSkuKey" class="success-title">SKU 已识别,请确认实际贴标数量后登记</text></view>
|
||
<view v-if="isReturnBulk" class="item-list section-gap"><view v-for="group in returnBulkSkuGroups" :key="group.key" class="item-card" :class="{ done: group.remaining === 0 }">
|
||
<view class="row-head"><view><text class="business-code">{{ group.skuCode }}</text><text class="meta-line">FBA 目的仓:{{ group.destinationFbaWarehouseCode || '—' }}</text></view><text class="status-pill">剩余 {{ group.remaining }} 件</text></view>
|
||
<view class="metric-row"><view class="metric"><strong>{{ group.expectedPackages }}</strong><text>贴标预报</text></view><view class="metric"><strong>{{ group.relabelledPackages }}</strong><text>已贴</text></view></view>
|
||
<view class="source-allocation"><text class="source-title">来源托盘拆箱数量</text><view v-for="source in group.sources" :key="source.itemId" class="source-row"><text>{{ source.palletCode || '未记录托盘' }}</text><strong>拆箱 {{ source.expectedPackages }} 件</strong></view><view class="source-total"><text>合计拆箱</text><strong>{{ group.expectedPackages }} 件</strong></view></view>
|
||
<view v-if="group.remaining > 0"><input v-model="bulkQuantities[group.key]" class="bulk-quantity" type="number" :placeholder="`本次贴标数量(默认剩余 ${group.remaining} 件)`" /><button class="primary bulk-total-button" :disabled="busy || verifiedReturnSkuKey !== group.key || returnSkuCode.trim().toUpperCase() !== group.skuCode.toUpperCase()" @tap="completeBulkSku(group)">{{ busyAction === 'return-bulk-sku' ? '提交中…' : `登记本次贴标(默认 ${group.remaining} 件)` }}</button></view>
|
||
</view></view>
|
||
<view v-if="taskTab === 'carton' && !isReturnBulk" class="section-gap"><ScanTargetInput name="quickCarton" v-model="quickCarton" label="仓库唛头" placeholder="扫描或输入箱唛" :active="activeTarget === 'quick-carton'" @activate="activeTarget = 'quick-carton'" @submit="submitCarton" /><button class="primary" :disabled="busy || !quickCarton.trim()" @tap="submitCarton(quickCarton)">确认本箱</button></view>
|
||
<view v-if="taskTab === 'sku'" class="section-gap">
|
||
<ScanTargetInput name="quickSku" v-model="quickSku" label="旧 SKU 标签" placeholder="扫描或输入旧 SKU 标签" :active="activeTarget === 'quick-sku'" @activate="activeTarget = 'quick-sku'" @submit="submitSku" />
|
||
<button class="primary" :disabled="busy || !quickSku.trim()" @tap="submitSku(quickSku)">确认 SKU</button>
|
||
<text v-if="skuChoices.length" class="hint">同一旧 SKU 有多条明细,请核对实际箱选择本次作业:</text>
|
||
<button v-for="row in skuChoices" :key="`${row.item.itemId}:${row.index}`" :disabled="busy" @tap="commitSku(row)">{{ row.item.cartonMark }}:{{ row.sku.skuCode }} 换标 {{ row.sku.newSkuCode }} · {{ row.sku.quantity }} 件</button>
|
||
<view v-for="row in pendingSkuRows" :key="`${row.item.itemId}:${row.index}`" class="item-card"><text class="business-code">{{ row.sku.skuCode }} 换标 {{ row.sku.newSkuCode }}</text><text class="meta-line">{{ row.item.cartonMark }} · {{ row.sku.quantity }} 件</text><button class="link-button" @tap="selectItem(row.item); changeAction('EXCEPTION')">异常 / 跳过</button></view>
|
||
</view>
|
||
<view v-if="taskTab === 'carton'" class="item-list"><view v-for="item in visibleItems" :key="item.itemId" class="item-card" :class="{ done: remaining(item) === 0, current: selectedItem?.itemId === item.itemId }" @tap="selectItem(item)">
|
||
<view class="row-head"><text class="business-code">{{ item.cartonMark || '—' }} → {{ item.fbaCartonMark || '—' }}</text></view>
|
||
<text class="meta-line">FBA 目的仓:{{ item.destinationFbaWarehouseCode || '待维护' }}</text>
|
||
</view></view>
|
||
<template v-if="taskTab === 'sku' && selectedItem && action !== 'RELABEL' && remaining(selectedItem) > 0">
|
||
<view class="mode-row section-gap"><button v-for="mode in actionModes" :key="mode.key" class="mode-button" :class="{ active: action === mode.key }" @tap="changeAction(mode.key)">{{ mode.name }}</button></view>
|
||
<ScanTargetInput name="oldLabel" v-model="oldLabel" label="仓库唛头" placeholder="扫描或输入仓库唛头" :active="activeTarget === 'old'" @activate="activeTarget = 'old'" @submit="handleOldLabel" />
|
||
<template v-if="action !== 'RELABEL'"><textarea v-model="reason" class="reason-input section-gap" maxlength="1000" placeholder="必填:跳过或异常原因" /><view v-if="action === 'EXCEPTION'" class="mode-row section-gap"><button v-for="item in exceptionCodes" :key="item.key" class="mode-button" :class="{ active: exceptionCode === item.key }" @tap="exceptionCode = item.key">{{ item.name }}</button></view></template>
|
||
<button class="primary" :disabled="busy || !canSubmitUnit" @tap="submitUnit">{{ busyAction === 'unit' ? '提交中…' : unitActionText }}</button>
|
||
</template>
|
||
<view class="section-gap">
|
||
<!-- Return SKU relabel still requires moving the received carton from the
|
||
source pallet to a new pallet carrying the FBA destination. -->
|
||
<RelabelPalletWork v-if="taskTab === 'carton' || isReturnBulk" ref="palletWork" :key="current.relabelNo" :task="current" @activate="activeTarget = 'pallet-work'" @busy="busy = $event" @saved="reloadCurrent" />
|
||
<view class="row-head"><text class="card-title">完成照片</text><text class="status-pill">{{ photoFileIds.length }} / 20 张</text></view>
|
||
<button class="secondary" :disabled="busy || photoFileIds.length >= 20" @tap="choosePhoto">拍照或从相册选择</button>
|
||
<view class="photo-grid"><view v-for="(fileId,index) in photoFileIds" :key="fileId" class="photo-tile">
|
||
<image v-if="photoUrls[fileId]" class="photo-thumbnail" :src="photoUrls[fileId]" mode="aspectFill" @tap="previewPhoto(fileId)" />
|
||
<button v-else class="photo-thumbnail photo-placeholder" @tap="loadPhoto(fileId)">{{ photoErrors[fileId] ? '加载失败,点击重试' : '照片加载中' }}</button>
|
||
<text class="photo-name">{{ photoNames[index] || '换标现场照片' }}</text>
|
||
<view class="photo-actions"><button class="link-button" :disabled="busy" @tap="retakePhoto(index)">重拍</button><button class="link-button" :disabled="busy" @tap="removePhoto(index)">移除</button></view>
|
||
</view></view>
|
||
<textarea v-model="remark" class="reason-input section-gap" maxlength="900" :placeholder="allProcessed ? '完成备注(选填)' : '必填:未全部扫描完成的仓库异常作业备注'" />
|
||
<view v-if="completionBlockReason" class="error-box section-gap"><text class="error-title">还不能完成换标</text><text>{{ completionBlockReason }}</text></view>
|
||
<button class="primary" :disabled="busy || Boolean(completionBlockReason)" @tap="completeTask">{{ busyAction === 'complete' ? '提交中…' : allProcessed ? '完成现场换标并等待复核' : '手动完成并登记异常' }}</button>
|
||
</view>
|
||
</template>
|
||
</template>
|
||
|
||
</view>
|
||
|
||
<view v-if="error" class="error-box" role="alert"><text class="error-title">{{ error.message }}</text><text v-if="error.code">错误码:{{ error.code }}</text><text v-if="error.requestId">requestId:{{ error.requestId }}</text><text v-if="error.traceId">traceId:{{ error.traceId }}</text><button class="secondary" @tap="retry">重新读取任务</button></view>
|
||
<view v-if="successMessage" class="success-box" role="status"><text class="success-title">{{ successMessage }}</text></view>
|
||
</view></scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import PortalHeader from '@/components/PortalHeader.vue'
|
||
import RelabelPalletWork from '@/components/RelabelPalletWork.vue'
|
||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||
import { claimRelabelTask, completeRelabelTask, getRelabelTask, listRelabelCandidates, relabelUnit, reportRelabelException, reviewRelabelTask, skipRelabelUnit, uploadRelabelPhoto } from '@/api/relabel.js'
|
||
import { session, requireSession } from '@/stores/session.js'
|
||
import { releaseWarehouseKey, restoreWarehouseWork, updateWarehouseWork, warehouseIdempotencyKey, warehouseWork } from '@/stores/warehouse-work.js'
|
||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||
import { scanRelabelSku, scanRelabelCarton, completeReturnRelabelSkuTotal, listRelabelDocuments, downloadRelabelDocument, downloadRelabelPhoto } from '@/api/relabel.js'
|
||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||
|
||
const STAGES = { relabelReceive: 'receive', relabelExecute: 'execute', relabelReview: 'review' }
|
||
const isConflict = (error) => ['409', 'A0409', 'B0409'].includes(String(error?.code || ''))
|
||
export default {
|
||
components: { PortalHeader, ScanTargetInput, RelabelPalletWork },
|
||
data() { return { user: session.currentUser, stage: 'receive', entryTaskNo: '', unitDrafts: {}, taskTab: 'sku', candidates: [], current: null, selectedItem: null, returnSkuCode: '', verifiedReturnSkuKey: '', bulkQuantities: {}, activeTarget: 'task', loading: false, busy: false, busyAction: '', error: null, successMessage: '', exceptionCode: 'OTHER', actionModes: [{ key: 'RELABEL', name: '正常换标' }, { key: 'SKIP', name: '跳过' }, { key: 'EXCEPTION', name: '异常' }], exceptionCodes: [{ key: 'DAMAGED', name: '破损' }, { key: 'MISSING_LABEL', name: '缺标签' }, { key: 'LABEL_MISMATCH', name: '标签不符' }, { key: 'UNREADABLE', name: '无法识别' }, { key: 'OTHER', name: '其他' }] } },
|
||
computed: {
|
||
isReturnBulk() { return this.current?.operationType === 'RETURN_BULK_SKU_LABEL' },
|
||
returnBulkSkuGroups() {
|
||
const groups = new Map()
|
||
for (const item of (this.current?.items || []).filter(row => row.operationType === 'RETURN_BULK_SKU_LABEL')) {
|
||
const skuCode = String(item.skuCode || '').trim()
|
||
const destination = String(item.destinationFbaWarehouseCode || '').trim()
|
||
const key = `${skuCode.toUpperCase()}::${destination.toUpperCase()}`
|
||
if (!groups.has(key)) groups.set(key, { key, skuCode, destinationFbaWarehouseCode: destination, expectedPackages: 0, relabelledPackages: 0, skippedPackages: 0, exceptionPackages: 0, sources: [] })
|
||
const group = groups.get(key)
|
||
group.expectedPackages += Number(item.expectedPackages || 0)
|
||
group.relabelledPackages += Number(item.relabelledPackages || 0)
|
||
group.skippedPackages += Number(item.skippedPackages || 0)
|
||
group.exceptionPackages += Number(item.exceptionPackages || 0)
|
||
group.sources.push({ itemId: item.itemId, palletCode: item.cartonMark, expectedPackages: Number(item.expectedPackages || 0) })
|
||
}
|
||
return [...groups.values()].map(group => ({ ...group, remaining: Math.max(0, group.expectedPackages - group.relabelledPackages - group.skippedPackages - group.exceptionPackages) }))
|
||
},
|
||
showCurrent() { if (!this.current || this.stage === 'review') return false; const owned = this.candidates.some((item) => item.relabelNo === this.current.relabelNo && item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator === true); if (this.stage === 'execute') return this.current.taskStatus === 'PROCESSING' && owned; return !this.current.taskStatus || this.current.taskStatus === 'AVAILABLE' || (this.current.taskStatus === 'PROCESSING' && owned) },
|
||
stageSubtitle() { return this.stage === 'review' ? '仅展示当前仓库已完成现场换标并等待复核的单据' : this.stage === 'execute' ? '仅展示当前账号已认领且正在处理的换标任务' : '点击任意换标单直接进入执行;同一账号必须先完成当前任务才能处理其他换标单' },
|
||
availableTabs() { const items = this.current?.items || []; return [{ key: 'sku', name: 'SKU 换标', types: ['SKU_LABEL', 'CARTON_AND_SKU'] }, { key: 'carton', name: '箱唛换标', types: ['CARTON_LABEL', 'CARTON_AND_SKU'] }].filter((tab) => items.some((item) => tab.types.includes(item.operationType))) },
|
||
unitDraft() { return this.unitDrafts?.[this.selectedItem?.itemId] || { cartonMark: '', fbaCartonMark: '', skuVerifications: [] } },
|
||
skuVerified() { if (!this.selectedItem || this.selectedItem.operationType === 'CARTON_LABEL') return true; const expected = this.skuDetails(this.selectedItem); return expected.length > 0 && expected.every((sku, index) => { const actual = this.unitDraft.skuVerifications[index]; return actual && actual.skuCode.trim() === sku.skuCode && actual.newSkuCode.trim() === sku.newSkuCode && Number(actual.quantity) > 0 && Number(actual.quantity) === Number(sku.quantity) }) },
|
||
cartonVerified() { return Boolean(this.selectedItem && this.oldLabel.trim() === this.selectedItem.cartonMark && (this.taskTab === 'sku' || this.newLabel.trim() === this.selectedItem.fbaCartonMark && this.newLabel.trim())) },
|
||
title() { return ({ receive: '换标列表', execute: '换标执行', review: '换标复核' })[this.stage] },
|
||
candidateHeading() { return this.stage === 'review' ? '待复核任务' : this.stage === 'execute' ? '处理中任务' : '换标任务' },
|
||
emptyText() { return `暂无${this.candidateHeading}` },
|
||
visibleCandidates() { const candidates = this.candidates; if (this.stage === 'review') return candidates.filter((item) => item.taskStatus === 'WAIT_REVIEW'); if (this.stage === 'execute') return candidates.filter((item) => item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator); return candidates.filter((item) => !item.taskStatus || item.taskStatus === 'AVAILABLE' || (item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator)) },
|
||
visibleItems() { return (this.current?.items || []).filter((item) => (this.taskTab === 'sku' ? ['SKU_LABEL', 'CARTON_AND_SKU'].includes(item.operationType) : ['CARTON_LABEL', 'CARTON_AND_SKU'].includes(item.operationType)) && this.remaining(item) > 0) },
|
||
pendingSkuRows() { return this.visibleItems.flatMap(item => this.skuDetails(item).map((sku, index) => ({ item, sku, index })).filter(row => !this.skuDone(row.item.itemId, row.index))) },
|
||
requiresNewCartonLabel() { return this.action === 'RELABEL' && this.selectedItem && this.selectedItem.operationType !== 'SKU_LABEL' },
|
||
taskCode: { get() { return warehouseWork.relabel.currentNo }, set(value) { updateWarehouseWork('relabel', { currentNo: value }) } },
|
||
oldLabel: { get() { return this.unitDraft.cartonMark }, set(value) { if (this.selectedItem) this.unitDraft.cartonMark = value } },
|
||
newLabel: { get() { return this.unitDraft.fbaCartonMark }, set(value) { if (this.selectedItem) this.unitDraft.fbaCartonMark = value } },
|
||
action: { get() { return warehouseWork.relabel.action }, set(value) { updateWarehouseWork('relabel', { action: value }) } },
|
||
reason: { get() { return warehouseWork.relabel.reason }, set(value) { updateWarehouseWork('relabel', { reason: value }) } },
|
||
remark: { get() { return warehouseWork.relabel.remark }, set(value) { updateWarehouseWork('relabel', { remark: value }) } },
|
||
photoFileIds() { return warehouseWork.relabel.photoFileIds || [] }, photoNames() { return warehouseWork.relabel.photoNames || [] },
|
||
allProcessed() { return Boolean(this.current?.expectedPackages) && this.current.relabelledPackages + this.current.skippedPackages + this.current.exceptionPackages === this.current.expectedPackages },
|
||
completionBlockReason() {
|
||
if (!this.current) return '请先选择换标任务'
|
||
if (!this.allProcessed && !this.remark.trim()) return '仍有未扫描明细,请继续处理;需要异常完工时必须填写异常作业备注'
|
||
const requiresPalletBinding = (this.current.items || []).some((item) => ['CARTON_LABEL', 'CARTON_AND_SKU', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType))
|
||
if (requiresPalletBinding && Number(this.current.relabelledPackages || 0) > 0) {
|
||
const round = this.current.reviewedAt || 'INITIAL'
|
||
const marks = new Set((this.current.history || []).filter((entry) => entry.action === 'PALLET_BIND').flatMap((entry) => {
|
||
try { const detail = JSON.parse(entry.detail); return detail.round === round ? detail.cartonMarks || [] : [] } catch { return [] }
|
||
}))
|
||
const requiredMarks = new Set((this.current.items || []).filter((item) =>
|
||
['CARTON_LABEL', 'CARTON_AND_SKU', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType)
|
||
&& Number(item.relabelledPackages || 0) > 0).map((item) => item.cartonMark).filter(Boolean))
|
||
if ([...requiredMarks].some((mark) => !marks.has(mark))) return '请先在“换标后组托”中完成本轮已换标箱的拆托合托绑定'
|
||
}
|
||
if (!this.photoFileIds.length) return '请至少上传 1 张换标完成照片'
|
||
return ''
|
||
},
|
||
canSubmitUnit() { if (!this.selectedItem || !this.visibleItems.some((item) => item.itemId === this.selectedItem.itemId) || !this.oldLabel.trim()) return false; if (this.action === 'RELABEL') return this.cartonVerified && this.skuVerified; return Boolean(this.reason.trim()) },
|
||
unitActionText() { return ({ RELABEL: '确认本箱换标完成', SKIP: '确认跳过本箱', EXCEPTION: '登记本箱异常' })[this.action] }
|
||
},
|
||
onLoad(options) { this.stage = STAGES[options.op] || 'receive'; this.entryTaskNo = String(options.task || '').trim() },
|
||
mixins: [{ data() { return { documents: [], quickCarton: '', lastCarton: null, quickSku: '', lastSku: '', skuChoices: [], photoUrls: {}, photoErrors: {} } } }],
|
||
watch: {
|
||
'current.reviewedAt'(value) { if (this.current?.reviewDecision === 'REWORK' && warehouseWork.relabel.reworkRound !== `${this.current.relabelNo}:${value}`) { this.unitDrafts = {}; this.selectedItem = null; this.lastCarton = null; this.quickCarton = ''; updateWarehouseWork('relabel', { reworkRound: `${this.current.relabelNo}:${value}`, photoFileIds: [], photoNames: [], remark: '' }) } },
|
||
photoFileIds: { deep: true, handler(ids) { for (const id of ids) if (!this.photoUrls[id]) this.loadPhoto(id) } },
|
||
'current.relabelNo'() { this.photoUrls = {}; this.photoErrors = {}; for (const id of this.photoFileIds) this.loadPhoto(id) }
|
||
},
|
||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); if (this.stage === 'execute') this.startScanner(); this.bootstrap() },
|
||
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
|
||
methods: {
|
||
skuDone(itemId, index) { return (this.current?.history || []).some(entry => { if (entry.action !== 'SKU_SCAN') return false; try { const data = JSON.parse(entry.detail); return String(data.itemId) === String(itemId) && data.skuIndex === index && data.round === (this.current.reviewedAt || 'INITIAL') } catch { return false } }) },
|
||
async submitSku(raw) {
|
||
const oldSku = String(raw?.detail?.value ?? raw ?? this.quickSku).trim()
|
||
if (!oldSku || this.busy || !this.current) return
|
||
this.quickSku = oldSku
|
||
const matches = this.pendingSkuRows.filter(row => row.sku.skuCode === oldSku)
|
||
this.skuChoices = []
|
||
if (!matches.length) return this.setError(new Error('该旧 SKU 不在待处理明细中,或已处理完成'))
|
||
if (matches.length > 1) { this.skuChoices = matches; return }
|
||
await this.commitSku(matches[0])
|
||
},
|
||
async commitSku(row) {
|
||
if (this.busy || this.quickSku.trim() !== row.sku.skuCode) return
|
||
const payload = { itemId: row.item.itemId, skuIndex: row.index, oldSku: this.quickSku.trim(), expectedVersion: this.current.version }
|
||
const key = warehouseIdempotencyKey('relabel', 'sku-scan', { relabelNo: this.current.relabelNo, ...payload })
|
||
await this.run('sku-scan', async () => {
|
||
const value = await scanRelabelSku(this.current.relabelNo, { ...payload, idempotencyKey: key.value })
|
||
this.applyCurrent(value); releaseWarehouseKey('relabel', key.signature)
|
||
const confirmed = value.items.find(item => item.itemId === row.item.itemId)?.skuItems?.[row.index]
|
||
this.lastSku = confirmed ? `${confirmed.skuCode} 换标 ${confirmed.newSkuCode} · ${confirmed.quantity} 件` : '已保存,请重新读取标签绑定信息'
|
||
this.quickSku = ''; this.skuChoices = []; this.activeTarget = 'quick-sku'
|
||
await new Promise(resolve => uni.showModal({ title: '请粘贴对应 SKU 标签', content: `${row.item.cartonMark}\n${this.lastSku}`, showCancel: false, confirmText: '知道了', complete: resolve }))
|
||
}, true, key)
|
||
},
|
||
relabelTypeName(type) { return ({ CARTON_LABEL: '只换箱唛', SKU_LABEL: '只换 SKU', CARTON_AND_SKU: '混合换标(SKU + 箱唛)', RETURN_BULK_SKU_LABEL: '退货贴标(按 SKU 数量)' })[type] || '换标类型待确认' },
|
||
remainingReturnQuantity(item) { return Math.max(0, Number(item.expectedPackages || 0) - Number(item.relabelledPackages || 0) - Number(item.skippedPackages || 0) - Number(item.exceptionPackages || 0)) },
|
||
verifyReturnSku(raw) {
|
||
const code = String(raw?.detail?.value ?? raw ?? this.returnSkuCode).trim()
|
||
this.returnSkuCode = code; this.verifiedReturnSkuKey = ''
|
||
const matches = this.returnBulkSkuGroups.filter(group => group.remaining > 0 && group.skuCode.toUpperCase() === code.toUpperCase())
|
||
if (matches.length !== 1) { this.setError(new Error(matches.length ? '相同 SKU 对应多个目的仓,请核对任务' : '扫描的 SKU 不属于本单待贴标明细,或已全部登记')); return }
|
||
this.error = null; this.verifiedReturnSkuKey = matches[0].key; this.activeTarget = 'return-sku'
|
||
},
|
||
async completeBulkSku(group) {
|
||
if (this.busy || this.verifiedReturnSkuKey !== group.key || this.returnSkuCode.trim().toUpperCase() !== group.skuCode.toUpperCase()) return
|
||
const entered = String(this.bulkQuantities[group.key] ?? '').trim()
|
||
const quantity = entered ? Number(entered) : Number(group.remaining || 0)
|
||
if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > group.remaining) { this.setError(new Error(`本次贴标数量须为 1 至 ${group.remaining} 件`)); return }
|
||
const payload = { skuCode: group.skuCode, quantity, expectedVersion: this.current.version }
|
||
const key = warehouseIdempotencyKey('relabel', 'return-bulk-sku', { relabelNo: this.current.relabelNo, ...payload })
|
||
await this.run('return-bulk-sku', async () => { this.applyCurrent(await completeReturnRelabelSkuTotal(this.current.relabelNo, { ...payload, idempotencyKey: key.value })); releaseWarehouseKey('relabel', key.signature); this.returnSkuCode = ''; this.verifiedReturnSkuKey = ''; delete this.bulkQuantities[group.key]; this.activeTarget = 'return-sku'; this.successMessage = `${group.skuCode} 已登记贴标 ${quantity} 件` }, true, key)
|
||
},
|
||
async loadPhoto(fileId) {
|
||
const no = this.current?.relabelNo
|
||
if (!no || this.photoUrls[fileId] || !this.photoFileIds.includes(fileId)) return
|
||
this.photoErrors[fileId] = false
|
||
try { const path = await downloadRelabelPhoto(no, fileId); if (this.current?.relabelNo === no && this.photoFileIds.includes(fileId)) this.photoUrls[fileId] = path }
|
||
catch (_) { if (this.current?.relabelNo === no) this.photoErrors[fileId] = true }
|
||
},
|
||
previewPhoto(fileId) { const urls = this.photoFileIds.map((id) => this.photoUrls[id]).filter(Boolean); if (this.photoUrls[fileId]) uni.previewImage({ current: this.photoUrls[fileId], urls }) },
|
||
retakePhoto(index) {
|
||
if (this.busy || !this.current) return
|
||
const no = this.current.relabelNo; const oldId = this.photoFileIds[index]
|
||
uni.chooseImage({ count: 1, sourceType: ['camera'], success: ({ tempFilePaths, tempFiles }) => {
|
||
if (this.current?.relabelNo !== no || !this.photoFileIds.includes(oldId) || !tempFilePaths?.[0]) return
|
||
this.run('photo', async () => {
|
||
if (Number(tempFiles?.[0]?.size || 0) > 10 * 1024 * 1024) throw new Error('照片不能超过 10MB')
|
||
const file = await uploadRelabelPhoto(no, tempFilePaths[0])
|
||
const ids = [...this.photoFileIds]; const names = [...this.photoNames]; const position = ids.indexOf(oldId)
|
||
if (position < 0 || this.current?.relabelNo !== no) return
|
||
ids[position] = String(file.fileId); names[position] = file.fileName || '换标现场照片'
|
||
this.photoUrls[String(file.fileId)] = tempFilePaths[0]
|
||
updateWarehouseWork('relabel', { photoFileIds: ids, photoNames: names })
|
||
this.successMessage = '照片已重新拍摄并替换'
|
||
}, false)
|
||
} })
|
||
},
|
||
async loadDocuments() { if (!this.current) return; const no = this.current.relabelNo; try { const files = await listRelabelDocuments(no); if (this.current?.relabelNo === no) this.documents = files || [] } catch (error) { this.setError(error, '标签文件加载失败') } },
|
||
async printDocument(file) { await this.run('document', async () => { const path = await downloadRelabelDocument(this.current.relabelNo, file);
|
||
// #ifdef H5
|
||
const frame = document.createElement('iframe'); frame.style.cssText = 'position:fixed;width:1px;height:1px;border:0'; frame.src = path; frame.onload = () => { try { frame.contentWindow.focus(); frame.contentWindow.print() } catch (_) { window.open(path, '_blank', 'noopener') } }; document.body.appendChild(frame); setTimeout(() => frame.remove(), 120000);
|
||
// #endif
|
||
// #ifndef H5
|
||
await new Promise((resolve, reject) => uni.openDocument({ filePath: path, fileType: 'pdf', showMenu: true, success: resolve, fail: reject }));
|
||
// #endif
|
||
}, false) },
|
||
async submitCarton(raw) {
|
||
const mark = String(raw?.detail?.value ?? raw ?? this.quickCarton).trim()
|
||
if (!mark || this.busy || !this.current) return
|
||
const item = this.current.items.find((row) => row.cartonMark === mark && row.operationType !== 'SKU_LABEL')
|
||
if (!item) return this.setError(new Error('该仓库唛头不属于本单箱唛换标明细'))
|
||
const payload = { relabelNo: this.current.relabelNo, cartonMark: mark, expectedVersion: this.current.version }
|
||
const key = warehouseIdempotencyKey('relabel', 'carton-scan', payload)
|
||
await this.run('carton-scan', async () => {
|
||
const result = await scanRelabelCarton(payload.relabelNo, { cartonMark: mark, expectedVersion: payload.expectedVersion, idempotencyKey: key.value })
|
||
this.applyCurrent(result)
|
||
releaseWarehouseKey('relabel', key.signature)
|
||
const confirmedItem = result.items.find((row) => row.itemId === item.itemId)
|
||
this.lastCarton = { cartonMark: mark, fbaCartonMark: confirmedItem?.fbaCartonMark || item.fbaCartonMark, destinationFbaWarehouseCode: confirmedItem?.destinationFbaWarehouseCode || item.destinationFbaWarehouseCode }
|
||
this.quickCarton = ''
|
||
this.activeTarget = 'quick-carton'
|
||
this.successMessage = ''
|
||
await new Promise((resolve) => uni.showModal({ title: '请粘贴对应箱唛', content: `仓库唛头:${mark}\n请贴 FBA 箱唛:${this.lastCarton.fbaCartonMark}\nFBA 目的仓:${this.lastCarton.destinationFbaWarehouseCode || '待维护'}`, showCancel: false, confirmText: '知道了', complete: resolve }))
|
||
}, true, key)
|
||
},
|
||
removePhoto(index) { updateWarehouseWork('relabel', { photoFileIds: this.photoFileIds.filter((_, i) => i !== index), photoNames: this.photoNames.filter((_, i) => i !== index) }) },
|
||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => { if (this.busy) return; if (this.activeTarget === 'pallet-work') { this.$refs.palletWork?.handleScan(value); return } if (this.activeTarget === 'quick-carton') { this.quickCarton = value; this.submitCarton(value) } else this.handleHardwareScan(value) }, onError: (error) => this.setError(error, '扫码广播注册失败') }) },
|
||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||
handleHardwareScan(value) { if (this.activeTarget === 'return-sku') { this.verifyReturnSku(value); return } if (this.activeTarget === 'quick-sku') { this.quickSku = value; this.submitSku(value); return } if (this.activeTarget === 'old') { this.oldLabel = value; return } this.taskCode = value; this.openTask(value) },
|
||
handleOldLabel(value) { this.oldLabel = String(value?.detail?.value ?? value ?? '').trim(); if (this.action === 'RELABEL') this.activeTarget = this.taskTab === 'sku' ? 'sku-new-0' : 'new' },
|
||
handleNewLabel(value) { this.newLabel = String(value?.detail?.value ?? value ?? '').trim() },
|
||
async bootstrap() { await this.loadCandidates(); if (this.error || this.stage !== 'execute' || !this.entryTaskNo) return; const target = this.candidates.find((item) => item.relabelNo === this.entryTaskNo && item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator === true); this.entryTaskNo = ''; if (target) await this.openTask(target.relabelNo, false) },
|
||
async loadCandidates() { if (this.loading) return; this.loading = true; this.error = null; try { this.candidates = await listRelabelCandidates() } catch (error) { this.setError(error, '换标任务加载失败') } finally { this.loading = false } },
|
||
async openTask(raw, sound = true) { const no = String(raw?.detail?.value ?? raw ?? this.taskCode).trim(); if (!no || this.busy) return; this.taskCode = no; await this.run('detail', async () => { this.applyCurrent(await getRelabelTask(no)); if (sound) playSuccessSound() }, false) },
|
||
applyCurrent(value) { const changed = this.current?.relabelNo !== value.relabelNo; this.current = value; this.taskCode = value.relabelNo; if (changed) { this.unitDrafts = {}; this.documents = []; this.quickCarton = ''; this.lastCarton = null; this.returnSkuCode = ''; this.verifiedReturnSkuKey = ''; this.bulkQuantities = {}; if (warehouseWork.relabel.photoTaskNo !== value.relabelNo) updateWarehouseWork('relabel', { photoTaskNo: value.relabelNo, photoFileIds: [], photoNames: [], remark: '' }); this.changeTab(value.items.some((item) => item.operationType === 'SKU_LABEL') ? 'sku' : 'carton'); this.loadDocuments() } if (this.selectedItem) this.selectedItem = this.visibleItems.find((item) => item.itemId === this.selectedItem.itemId) || null },
|
||
changeTab(value) { this.taskTab = value; this.selectedItem = null; this.reason = ''; this.quickSku = ''; this.skuChoices = []; this.activeTarget = this.isReturnBulk ? 'return-sku' : value === 'carton' ? 'quick-carton' : 'quick-sku' },
|
||
skuDetails(item) { return item.skuItems?.length ? item.skuItems : [{ skuCode: item.skuCode, newSkuCode: item.newSkuCode, quantity: null }] },
|
||
selectItem(item) { if (this.taskTab === 'carton') { this.activeTarget = 'quick-carton'; return } if (this.remaining(item) <= 0) return; if (!this.unitDrafts[item.itemId]) this.unitDrafts[item.itemId] = { cartonMark: '', fbaCartonMark: '', skuVerifications: this.skuDetails(item).map((sku) => ({ skuCode: sku.skuCode || '', newSkuCode: '', quantity: sku.quantity })) }; this.selectedItem = item; this.reason = ''; this.activeTarget = 'old' },
|
||
focusNextSku(index) { const next = index + 1; this.activeTarget = next < this.unitDraft.skuVerifications.length ? `sku-new-${next}` : 'old' },
|
||
remaining(item) { const processed = this.taskTab === 'sku' ? item.skuProcessedPackages : item.cartonProcessedPackages; const completed = item.operationType === 'CARTON_AND_SKU' ? Number(processed || 0) : Number(processed ?? item.relabelledPackages); return Math.max(0, item.expectedPackages - completed - item.skippedPackages - item.exceptionPackages) },
|
||
changeAction(value) { this.action = value; this.newLabel = ''; this.reason = ''; this.activeTarget = 'old' },
|
||
async claimTask() { if (!this.current) return; const payload = { relabelNo: this.current.relabelNo }; const key = warehouseIdempotencyKey('relabel', 'claim', payload); await this.run('claim', async () => { this.applyCurrent(await claimRelabelTask(this.current.relabelNo, { idempotencyKey: key.value })); releaseWarehouseKey('relabel', key.signature); this.successMessage = '换标任务已认领,可进入换标执行'; await this.loadCandidates() }, true, key) },
|
||
async promptActiveTask(active) {
|
||
if (this.busy) return
|
||
this.busy = true
|
||
this.error = null
|
||
try {
|
||
const confirmed = await new Promise((resolve) => uni.showModal({
|
||
title: '已有正在执行的换标任务',
|
||
content: `当前账号正在执行 ${active.relabelNo},请先完成该任务。是否前往继续作业?`,
|
||
confirmText: '前往任务', cancelText: '取消',
|
||
success: (result) => resolve(result.confirm), fail: () => resolve(false)
|
||
}))
|
||
if (!confirmed) return
|
||
this.taskCode = active.relabelNo
|
||
uni.redirectTo({ url: `/pages/relabel/relabel?op=relabelExecute&task=${encodeURIComponent(active.relabelNo)}` })
|
||
} finally { this.busy = false }
|
||
},
|
||
async openExecution(item) {
|
||
if (this.busy || this.stage === 'review') return
|
||
if (this.stage === 'execute') {
|
||
if (item.taskStatus !== 'PROCESSING' || item.claimedByCurrentOperator !== true) return
|
||
return this.openTask(item.relabelNo)
|
||
}
|
||
const active = this.candidates.find((candidate) => candidate.taskStatus === 'PROCESSING' && candidate.claimedByCurrentOperator === true)
|
||
if (active && active.relabelNo !== item.relabelNo) return this.promptActiveTask(active)
|
||
this.taskCode = item.relabelNo
|
||
if (item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator) return uni.redirectTo({ url: `/pages/relabel/relabel?op=relabelExecute&task=${encodeURIComponent(item.relabelNo)}` })
|
||
const payload = { relabelNo: item.relabelNo }; const key = warehouseIdempotencyKey('relabel', 'claim', payload); let claimed = false
|
||
await this.run('claim', async () => { this.applyCurrent(await claimRelabelTask(item.relabelNo, { idempotencyKey: key.value })); releaseWarehouseKey('relabel', key.signature); claimed = true }, true, key)
|
||
if (claimed) uni.redirectTo({ url: `/pages/relabel/relabel?op=relabelExecute&task=${encodeURIComponent(item.relabelNo)}` })
|
||
},
|
||
async submitUnit() { if (!this.canSubmitUnit || this.busy) return; const base = { itemId: this.selectedItem.itemId, oldLabel: this.oldLabel.trim(), expectedVersion: this.current.version }; const payload = { relabelNo: this.current.relabelNo, action: this.action, ...base, newLabel: this.newLabel.trim(), skuVerifications: this.unitDraft.skuVerifications.map((sku) => ({ ...sku, skuCode: sku.skuCode.trim(), newSkuCode: sku.newSkuCode.trim() })), reason: this.reason.trim(), exceptionCode: this.exceptionCode }; const key = warehouseIdempotencyKey('relabel', 'unit', payload); await this.run('unit', async () => { let value; if (this.action === 'SKIP') value = await skipRelabelUnit(this.current.relabelNo, { ...base, reason: this.reason.trim(), idempotencyKey: key.value }); else if (this.action === 'EXCEPTION') value = await reportRelabelException(this.current.relabelNo, { ...base, exceptionCode: this.exceptionCode, reason: this.reason.trim(), idempotencyKey: key.value }); else value = await relabelUnit(this.current.relabelNo, { ...base, newLabel: this.newLabel.trim(), skuVerifications: payload.skuVerifications, idempotencyKey: key.value }); releaseWarehouseKey('relabel', key.signature); this.applyCurrent(value); this.oldLabel = ''; this.newLabel = ''; this.reason = ''; this.activeTarget = 'old'; this.successMessage = '本箱处理结果已保存' }, true, key) },
|
||
choosePhoto() { if (this.busy || !this.current || this.photoFileIds.length >= 20) return; const no = this.current.relabelNo; uni.chooseImage({ count: Math.min(9, 20 - this.photoFileIds.length), sourceType: ['camera', 'album'], success: async ({ tempFilePaths, tempFiles }) => { if (this.current?.relabelNo !== no) return; await this.run('photo', async () => { for (const [index, path] of (tempFilePaths || []).entries()) { const selected = tempFiles?.[index]?.file || tempFiles?.[index]; if (Number(tempFiles?.[index]?.size || 0) > 10 * 1024 * 1024) throw new Error('照片不能超过 10MB'); const uploadSource = typeof Blob !== 'undefined' && selected instanceof Blob ? selected : path; const file = await uploadRelabelPhoto(no, uploadSource); this.photoUrls[String(file.fileId)] = path; updateWarehouseWork('relabel', { photoTaskNo: no, photoFileIds: [...this.photoFileIds, String(file.fileId)], photoNames: [...this.photoNames, file.fileName || '换标现场照片'] }) } this.successMessage = '现场照片已上传' }, false) } }) },
|
||
async uploadPhoto(path) { await this.run('photo', async () => { const file = await uploadRelabelPhoto(this.current.relabelNo, path); updateWarehouseWork('relabel', { photoFileIds: [...this.photoFileIds, String(file.fileId)], photoNames: [...this.photoNames, file.fileName || '换标现场照片'] }); this.successMessage = '现场照片已上传' }, false) },
|
||
async completeTask() { if (this.busy || !this.photoFileIds.length || (!this.allProcessed && !this.remark.trim())) return; const payload = { photoFileIds: [...this.photoFileIds], expectedVersion: this.current.version, remark: this.remark.trim(), manualCompletion: !this.allProcessed }; const no = this.current.relabelNo; if (payload.manualCompletion) { const confirmed = await new Promise((resolve) => uni.showModal({ title: '手动完成换标', content: '未处理箱将登记异常,关联尾程订单将标为异常。确认提交?', success: (result) => resolve(result.confirm), fail: () => resolve(false) })); if (!confirmed || this.current?.relabelNo !== no || this.current.version !== payload.expectedVersion) return } const key = warehouseIdempotencyKey('relabel', 'complete', { relabelNo: no, ...payload }); let completed = false; await this.run('complete', async () => { await completeRelabelTask(no, { ...payload, idempotencyKey: key.value }); releaseWarehouseKey('relabel', key.signature); updateWarehouseWork('relabel', { currentNo: '', photoFileIds: [], photoNames: [], remark: '' }); completed = true }, true, key); if (completed) this.goToClaimList() },
|
||
goToClaimList() { this.taskCode = ''; uni.redirectTo({ url: '/pages/relabel/relabel?op=relabelReceive' }) },
|
||
closeCurrent() { this.current = null; this.selectedItem = null; this.error = null; this.successMessage = ''; this.activeTarget = 'task'; this.taskCode = '' },
|
||
async review(decision) { if (!this.current || this.current.taskStatus !== 'WAIT_REVIEW') return; const payload = { relabelNo: this.current.relabelNo, decision, expectedVersion: this.current.version, remark: this.remark.trim() }; const key = warehouseIdempotencyKey('relabel', 'review', payload); await this.run('review', async () => { this.applyCurrent(await reviewRelabelTask(this.current.relabelNo, { decision, expectedVersion: this.current.version, idempotencyKey: key.value, remark: this.remark.trim() || undefined })); releaseWarehouseKey('relabel', key.signature); this.remark = ''; this.successMessage = decision === 'APPROVE' ? '换标复核已通过' : '换标任务已退回重做'; await this.loadCandidates() }, true, key) },
|
||
async retry() { if (this.current?.relabelNo) await this.openTask(this.current.relabelNo, false); else await this.loadCandidates() },
|
||
async reloadCurrent() { if (this.current?.relabelNo) this.applyCurrent(await getRelabelTask(this.current.relabelNo)) },
|
||
async run(action, work, refreshOnConflict = false, key = null) { if (this.busy) return; this.busy = true; this.busyAction = action; this.error = null; this.successMessage = ''; try { await work(); if (!['detail'].includes(action)) playSuccessSound() } catch (error) { this.setError(error, `${this.title}失败`); if (refreshOnConflict && isConflict(error)) { if (key) releaseWarehouseKey('relabel', key.signature); try { await this.reloadCurrent(); this.error.message += ',已刷新任务版本,请重新确认' } catch (_) {} } } finally { this.busy = false; this.busyAction = '' } },
|
||
setError(error, fallback = '换标作业失败') { this.error = { message: error?.message || fallback, code: error?.code, requestId: error?.requestId, traceId: error?.traceId }; playErrorSound() },
|
||
statusLabel(value) { return ({ PENDING_PROCESSING: '待认领', PROCESSING: '处理中', WAIT_REVIEW: '待复核', COMPLETED: '已完成' })[value] || value || '待认领' },
|
||
formatTime(value) { return value ? String(value).replace('T', ' ').slice(0, 16) : '—' }
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">@import '../warehouse/operations.scss';.action-row>.danger,.action-row>.primary{width:50%}.item-card.current{border-color:#6ea3ee;background:#f3f8ff}.detail-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:4px 0 10px;padding:10px 12px;border:1px solid #bed4f6;border-radius:11px;background:#eef5ff}.detail-kicker,.detail-title{display:block}.detail-kicker{color:#6580a4;font-size:9px}.detail-title{margin-top:2px;color:#153d74;font-size:14px;font-weight:800}.detail-toolbar button{flex-shrink:0;min-height:34px;margin:0;padding:0 11px;border-radius:8px;color:#135ee8;font-size:10px;line-height:34px;background:#fff}.tab-row{display:flex;gap:10px;margin:14px 0 10px}.tab-button{flex:1;border:1px solid #d7e1ef;border-radius:10px;background:#fff;color:#52647c;font-size:15px;padding:10px 8px}.tab-button.active{border-color:#2167e8;background:#2167e8;color:#fff;font-weight:600}
|
||
.photo-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:12px}.photo-tile{min-width:0;overflow:hidden;border:1px solid #d7e1ef;border-radius:8px}.photo-thumbnail{display:block;width:100%;height:140px;background:#f1f4f8}.photo-placeholder{margin:0;border:0;border-radius:0;font-size:13px;white-space:normal;line-height:1.5;display:flex;align-items:center;justify-content:center}.photo-name{display:block;padding:6px 8px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px;color:#52647c}.photo-actions{display:flex;justify-content:space-around;padding:4px}.photo-actions .link-button{margin:0}.bulk-total-button{margin-top:14px}.bulk-quantity{box-sizing:border-box;width:100%;padding:12px;border:1px solid #d7e1ef;border-radius:8px;background:#fff}.source-allocation{margin-top:14px;padding:12px;border-radius:10px;background:#f6f8fb}.source-title{display:block;font-weight:700;color:#24364b;margin-bottom:8px}.source-row,.source-total{display:flex;justify-content:space-between;gap:12px;padding:8px 0;color:#52647c}.source-row+ .source-row{border-top:1px solid #e4eaf2}.source-row text{min-width:0;overflow-wrap:anywhere}.source-row strong,.source-total strong{white-space:nowrap;color:#1f5fca}.source-total{border-top:1px solid #d7e1ef;font-weight:700}
|
||
</style>
|