278 lines
29 KiB
Vue
278 lines
29 KiB
Vue
<template>
|
||
<view v-if="user" class="page-shell palletization-page">
|
||
<PortalHeader title="一件代发入库" :user="user" :fallback-url="'/pages/dropship-tasks/dropship-tasks'" />
|
||
<view class="content">
|
||
<view v-if="error" class="message error" role="alert">{{ error }}</view>
|
||
<view v-if="notice" class="message success" role="status">{{ notice }}</view>
|
||
<view v-if="loading && !task" class="state">正在加载入库任务…</view>
|
||
<template v-if="task">
|
||
<view class="task-summary-card">
|
||
<view class="summary-head"><text class="inbound-code">{{ task.inboundCode || '—' }}</text><text class="status">{{ statusLabel }}</text></view>
|
||
<text class="warehouse-summary">{{ task.warehouseName || '仓库未关联' }}</text>
|
||
<view class="compact-metrics">
|
||
<text><strong>{{ task.totalCartons || 0 }}</strong>总箱</text>
|
||
<text><strong>{{ task.countedCartons || 0 }}</strong>清点</text>
|
||
<text><strong>{{ task.boundCartons || 0 }}</strong>归托</text>
|
||
<text><strong>{{ task.unboundCartons || 0 }}</strong>待归</text>
|
||
<text class="pallet-readiness" :class="{ invalid: !palletSetupReady }"><strong>{{ task.generatedPalletCount || 0 }}/{{ task.expectedPalletCount || 0 }}</strong>托盘</text>
|
||
</view>
|
||
<view v-if="!palletSetupReady" class="message error pallet-setup-error" role="alert">{{ palletSetupMessage }}</view>
|
||
<button v-if="task.taskStatus === 'PENDING'" class="primary-button" :disabled="submitting" @tap="claimTask">领取并开始入库</button>
|
||
</view>
|
||
|
||
<view v-if="task.taskStatus === 'CLAIMED' || currentPallet" class="section-card">
|
||
<text class="section-title">1. 扫箱清点</text>
|
||
<template v-if="palletSetupReady">
|
||
<ScanTargetInput v-model="boxCode" name="boxCode" label="箱码" placeholder="扫描或输入箱唛箱码" :active="activeScan === 'boxCode'" @activate="activeScan = 'boxCode'" @submit="resolveCarton" />
|
||
<view v-if="!carton" class="count-entry">
|
||
<text>请扫描箱码</text>
|
||
<button class="compact-action" :disabled="!canResolveCarton" @tap="resolveCarton()">读取 SKU</button>
|
||
</view>
|
||
<view v-if="carton" class="carton-card">
|
||
<text class="carton-owner">{{ scanTask?.customerName || task.customerName }} · {{ carton.inboundCode }}</text>
|
||
<text v-if="carton.blockReason" class="pallet-hint blocked">{{ carton.blockReason }}</text>
|
||
<view v-for="(line, index) in skuCounts" :key="line.inboundDetailId" class="sku-count-row">
|
||
<view class="sku-copy"><text class="sku-label">SKU {{ index + 1 }}</text><text class="sku-code">{{ line.skuCode || '—' }}</text></view>
|
||
<view v-if="!isResolvedPendingCarton" class="quantity-stepper">
|
||
<button :aria-label="`减少 ${line.skuCode} 清点数量`" @tap="adjustQuantity(index, -1)">−</button>
|
||
<input v-model="line.actualQuantity" type="number" :aria-label="`${line.skuCode} 清点数量`" />
|
||
<button :aria-label="`增加 ${line.skuCode} 清点数量`" @tap="adjustQuantity(index, 1)">+</button>
|
||
</view>
|
||
<text v-else class="counted-quantity">已清点 {{ line.actualQuantity }} 件</text>
|
||
<view v-if="!isResolvedPendingCarton && lineDiffers(line)" class="field stacked difference-field"><text>差异说明</text><textarea v-model="line.discrepancyRemark" maxlength="500" placeholder="数量不一致时必填" /></view>
|
||
</view>
|
||
<text v-if="assignableCurrentPallet" class="pallet-hint">归入 {{ assignableCurrentPallet.palletNo }}</text>
|
||
<text v-else-if="currentPallet?.status === 'SEALED'" class="pallet-hint blocked">当前托盘已封托,请先解除封托或扫描其他托盘</text>
|
||
<text v-else-if="isResolvedPendingCarton" class="pallet-hint">本箱已清点,请先扫描目标托盘</text>
|
||
<button class="primary-button" :disabled="!canSubmitCarton || submitting" @tap="confirmCarton">{{ cartonActionLabel }}</button>
|
||
</view>
|
||
<view class="pallet-scan-block">
|
||
<text class="field-label">扫托盘 <text>可选</text></text>
|
||
<ScanTargetInput v-model="palletCode" name="palletCode" label="托盘码" placeholder="扫描或输入已生成的 DP 托盘码" :active="activeScan === 'palletCode'" @activate="activeScan = 'palletCode'" @submit="selectPallet" />
|
||
</view>
|
||
</template>
|
||
<view v-else class="count-unavailable">托盘码未准备完成,当前不能开始箱码清点。</view>
|
||
</view>
|
||
<view v-if="task.taskStatus === 'CLAIMED' && task.totalCartons > 0 && task.boundCartons === task.totalCartons" class="continue-strip">
|
||
<text>本票已全部归托。托盘未装满时先继续装其他票,实物装满后再封托。</text>
|
||
<button class="secondary-button" :disabled="submitting || loading" @tap="continueOtherTask">继续装其他入仓单</button>
|
||
</view>
|
||
|
||
<view class="section-card">
|
||
<view class="section-heading"><text class="section-title">2. 托盘操作</text><button class="refresh-button" :disabled="loading" @tap="loadTask">{{ loading ? '刷新中…' : '刷新' }}</button></view>
|
||
<view v-if="currentPallet" class="pallet-card">
|
||
<view class="pallet-head"><text class="pallet-code">{{ currentPallet.palletNo }}</text><text class="pallet-status" :class="currentPallet.status === 'SEALED' ? 'sealed' : ''">{{ currentPallet.status === 'SEALED' ? '已封托' : '当前托盘' }}</text></view>
|
||
<text class="pallet-meta">{{ currentPallet.cartonCount || 0 }} 箱 · {{ currentPallet.skuCount || 0 }} SKU · {{ currentPallet.weightKg || 0 }} kg · {{ currentPallet.volumeCbm || 0 }} m³</text>
|
||
<text class="pallet-meta">整托 {{ palletCustomerCount }} 个客户 · {{ palletMembers.length }} 票</text>
|
||
<view v-for="member in palletMembers" :key="member.taskId" class="member-row"><text>{{ member.customerName || '客户未关联' }} · {{ member.inboundCode }}</text><strong>{{ member.cartonCount }} 箱</strong></view>
|
||
<view v-for="item in (currentPalletDetails?.cartons || [])" :key="item.cartonId" class="carton-row"><view><text>{{ item.boxCode }}</text><text class="carton-owner">{{ memberForCarton(item)?.customerName }} · {{ item.inboundCode }}</text></view><button v-if="currentPallet.status === 'OPEN'" class="text-button" :disabled="submitting || loading" @tap="moveCarton(item, null)">移回待绑托</button></view>
|
||
<button v-if="currentPallet.palletId && currentPallet.status === 'OPEN'" class="primary-button" :disabled="!currentPallet.cartonCount || submitting" @tap="sealPallet(currentPallet)">封托</button>
|
||
<button v-if="currentPallet.palletId && currentPallet.status === 'SEALED'" class="secondary-button unseal-button" :disabled="submitting" @tap="openUnseal(currentPallet)">解除封托</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
<uni-popup ref="unsealPopup" type="center" :is-mask-click="true" @change="handleUnsealPopupChange">
|
||
<view class="unseal-dialog" role="dialog" aria-modal="true" aria-label="确认解除封托">
|
||
<PopupHeader title="确认解除封托?" tone="danger" @close="closeUnseal" />
|
||
<text class="unseal-copy">托盘 {{ unsealTarget?.palletNo || '—' }} 将恢复为开放状态,可继续移箱、归托和重新封托。</text>
|
||
<view class="unseal-actions"><button class="secondary-button" @tap="closeUnseal">取消</button><button class="danger-button" :disabled="submitting" @tap="confirmUnseal">确认解除</button></view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import PortalHeader from '@/components/PortalHeader.vue'
|
||
import PopupHeader from '@/components/PopupHeader.vue'
|
||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||
import { bindDropshipPendingCartons, claimDropshipPalletizationTask, countDropshipCarton, getDropshipPalletizationTask, moveDropshipCarton, resolveDropshipWorkspaceCarton, resolveDropshipWorkspacePallet, sealDropshipPallet, unsealDropshipPallet } from '@/api/pda-dropship.js'
|
||
import { session, requireSession } from '@/stores/session.js'
|
||
import { sanitizeUserMessage } from '@/utils/user-facing.js'
|
||
import { stableOperationId } from '@/utils/idempotency.js'
|
||
import { isOnline } from '@/utils/network.js'
|
||
import { clearDropshipPalletizingDraft, restoreDropshipPalletizingDraft, saveDropshipPalletizingDraft } from '@/stores/dropship-palletizing.js'
|
||
import { createDropshipScanState, planDropshipScan, DROPSHIP_SCAN_INTENT } from '@/utils/dropship-palletizing-state.js'
|
||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||
|
||
export default {
|
||
components: { PopupHeader, PortalHeader, ScanTargetInput },
|
||
data() { return { user: session.currentUser, taskId: '', warehouseId: '', task: null, scanTask: null, workspace: null, initialPalletNo: '', palletCode: '', selectedPalletId: '', selectedPalletNo: '', boxCode: '', carton: null, skuCounts: [], activeScan: 'boxCode', loading: false, submitting: false, error: '', notice: '', unsealOpen: false, unsealTarget: null } },
|
||
computed: {
|
||
statusLabel() { return this.task?.taskStatus === 'COMPLETED' ? '已完成' : (this.task?.taskStatus === 'CLAIMED' ? '作业中' : '待领取') },
|
||
palletSetupReady() { return this.task?.palletSetupReady === true },
|
||
palletSetupMessage() { return this.task?.palletSetupMessage || `预计 ${this.task?.expectedPalletCount || 0} 托,当前仅生成 ${this.task?.generatedPalletCount || 0} 个托盘码,请联系后台处理后刷新` },
|
||
canResolveCarton() { return Boolean(this.palletSetupReady && String(this.boxCode || '').trim() && !this.loading && !this.submitting) },
|
||
canCount() { return Boolean(this.carton?.canCount && this.skuCounts.length && this.skuCounts.every((line) => Number.isInteger(Number(line.actualQuantity)) && Number(line.actualQuantity) >= 0 && (!this.lineDiffers(line) || String(line.discrepancyRemark || '').trim()))) },
|
||
isResolvedPendingCarton() { return this.carton?.countStatus === 'COUNTED_UNBOUND' },
|
||
canRebindResolvedCarton() { return Boolean(this.isResolvedPendingCarton && this.carton?.cartonId && this.assignableCurrentPallet?.palletNo) },
|
||
canSubmitCarton() { return this.currentPallet?.status === 'SEALED' || ((this.selectedPalletNo || this.palletCode) && !this.currentPallet) ? false : (this.isResolvedPendingCarton ? this.canRebindResolvedCarton : this.canCount) },
|
||
cartonActionLabel() { return this.isResolvedPendingCarton ? '确认归入当前托盘' : '确认清点' },
|
||
currentPalletDetails() { return this.workspace?.pallet?.palletNo === this.selectedPalletNo ? this.workspace.pallet : null },
|
||
currentPallet() { return this.currentPalletDetails },
|
||
palletMembers() { return this.workspace?.members || [] },
|
||
palletCustomerCount() { return new Set(this.palletMembers.map((member) => String(member.customerId))).size },
|
||
assignableCurrentPallet() { return this.currentPallet?.status === 'OPEN' ? this.currentPallet : null }
|
||
},
|
||
onLoad(options) { this.taskId = String(options.taskId || ''); this.warehouseId = String(options.warehouseId || ''); this.initialPalletNo = String(options.palletNo || '') },
|
||
onShow() { if (!requireSession()) return; this.user = session.currentUser; const draft = restoreDropshipPalletizingDraft(this.draftIdentity()); this.selectedPalletNo = this.initialPalletNo || draft?.activePalletNo || ''; this.initialPalletNo = ''; this.startScanner(); this.loadTask({ restorePallet: true }) },
|
||
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
|
||
onBackPress() { if (!this.unsealOpen) return false; this.closeUnseal(); return true },
|
||
methods: {
|
||
formatOperationError(error, fallback) {
|
||
console.error('[PDA dropship inbound]', { code: error?.code, requestId: error?.requestId, traceId: error?.traceId })
|
||
return sanitizeUserMessage(error?.message, fallback)
|
||
},
|
||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleScan(value), onError: () => { this.error = '扫码头连接失败,可使用输入框继续作业' } }) },
|
||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||
handleScan(value) { if (this.loading || this.submitting) return; if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return } const plan = planDropshipScan(createDropshipScanState({ online: isOnline() }), value, { unboundCartons: this.task?.unboundCartons }); if ([DROPSHIP_SCAN_INTENT.BLOCKED, DROPSHIP_SCAN_INTENT.INVALID].includes(plan.intent)) { this.error = plan.message; return } const code = plan.palletNo || plan.boxCode; if (plan.palletNo) { this.palletCode = code; this.selectPallet(code) } else { this.boxCode = code; this.resolveCarton() } },
|
||
resetMessages() { this.error = ''; this.notice = '' },
|
||
async loadTask({ restorePallet = false } = {}) {
|
||
if (!this.taskId || !this.warehouseId) { this.error = '缺少合托任务或仓库信息,请返回任务列表'; return }
|
||
this.loading = true; this.resetMessages()
|
||
try { this.task = await getDropshipPalletizationTask(this.taskId, this.warehouseId); await this.refreshPallet({ restorePallet }); if (this.task.taskStatus === 'COMPLETED') clearDropshipPalletizingDraft(this.draftIdentity()) }
|
||
catch (error) { this.error = this.formatOperationError(error, '一件代发入库任务加载失败') }
|
||
finally { this.loading = false }
|
||
},
|
||
async claimTask() { await this.runMutation(() => claimDropshipPalletizationTask(this.taskId, this.warehouseId), '任务领取成功') },
|
||
async resolveCarton(value) {
|
||
if (this.loading || this.submitting) return
|
||
if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return }
|
||
const code = String(value || this.boxCode || '').trim().toUpperCase(); if (!code || this.loading) return
|
||
this.boxCode = code; this.loading = true; this.resetMessages(); this.carton = null
|
||
try { const resolved = await resolveDropshipWorkspaceCarton(this.warehouseId, code); this.scanTask = resolved.task; this.carton = resolved.carton; this.skuCounts = (this.carton.skuLines || []).map((line) => ({ ...line, actualQuantity: String(line.actualQuantity ?? line.expectedQuantity ?? ''), discrepancyRemark: line.discrepancyRemark || '' })) }
|
||
catch (error) { this.error = this.formatOperationError(error, '箱码解析失败') }
|
||
finally { this.loading = false }
|
||
},
|
||
async countCarton() {
|
||
const skuLines = this.skuCounts.map((line) => ({ inboundDetailId: line.inboundDetailId, skuCode: line.skuCode, actualQuantity: Number(line.actualQuantity), discrepancyAction: this.lineDiffers(line) ? 'ACCEPT_VARIANCE' : 'MATCH', discrepancyRemark: this.lineDiffers(line) ? String(line.discrepancyRemark || '').trim() : null }))
|
||
const result = await this.runMutation(async () => { await this.activateScanTask(); return countDropshipCarton(this.taskId, { warehouseId: this.warehouseId, boxCode: this.carton.boxCode, palletNo: this.assignableCurrentPallet?.palletNo || null, skuLines }) }, this.assignableCurrentPallet ? '本箱已清点并归托' : '本箱已完成入库清点')
|
||
if (result) { this.boxCode = ''; this.carton = null; this.skuCounts = [] }
|
||
},
|
||
async confirmCarton() {
|
||
if (!this.canSubmitCarton || this.loading || this.submitting) return
|
||
if (this.isResolvedPendingCarton) { await this.rebindResolvedCarton(); return }
|
||
await this.countCarton()
|
||
},
|
||
async rebindResolvedCarton() {
|
||
if (!this.canRebindResolvedCarton) return
|
||
const targetPalletNo = this.assignableCurrentPallet.palletNo
|
||
const payload = { warehouseId: this.warehouseId, targetPalletNo }
|
||
payload.operationId = stableOperationId('dropship-move', String(this.user?.id) + '-' + this.taskId, { cartonId: this.carton.cartonId, taskVersion: this.task.version, ...payload })
|
||
const result = await this.runMutation(async () => { await this.activateScanTask(); return moveDropshipCarton(this.taskId, this.carton.cartonId, payload) }, `本箱已归入 ${targetPalletNo}`)
|
||
if (result) { this.boxCode = ''; this.carton = null; this.skuCounts = [] }
|
||
},
|
||
lineDiffers(line) { return Number(line.actualQuantity) !== Number(line.expectedQuantity) },
|
||
adjustQuantity(index, step) { const line = this.skuCounts[index]; if (line) line.actualQuantity = String(Math.max(0, Number(line.actualQuantity || 0) + step)) },
|
||
async selectPallet(value) {
|
||
if (this.loading || this.submitting) return
|
||
if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return }
|
||
const code = String(value || this.palletCode || '').trim().toUpperCase()
|
||
this.palletCode = code
|
||
this.loading = true; this.resetMessages(); this.workspace = null
|
||
try { this.workspace = await resolveDropshipWorkspacePallet(this.warehouseId, code); this.selectedPalletNo = code; this.palletCode = code; this.selectedPalletId = String(this.workspace.pallet.palletId); this.activeScan = 'boxCode'; saveDropshipPalletizingDraft(this.draftIdentity(), { activePalletNo: code }) }
|
||
catch (error) { this.selectedPalletNo = ''; this.error = this.formatOperationError(error, '托盘校验失败'); return }
|
||
finally { this.loading = false }
|
||
const pallet = this.currentPallet
|
||
if (pallet?.status === 'SEALED') { this.notice = `托盘 ${code} 已封托,可在托盘操作区解除封托`; return }
|
||
if (Number(this.task?.unboundCartons || 0) > 0) await this.runMutation(() => bindDropshipPendingCartons(this.taskId, this.warehouseId, code, this.task.version), `待归托箱已绑定至 ${code}`)
|
||
else this.notice = `已选择仓库开放托盘 ${code},可连续扫描不同客户的箱码`
|
||
},
|
||
// 页面恢复只延续开放托盘;主动扫码和刚封托后的回查仍保留真实封托结果。
|
||
async refreshPallet({ restorePallet = false } = {}) {
|
||
this.workspace = null
|
||
if (!this.selectedPalletNo) return
|
||
const workspace = await resolveDropshipWorkspacePallet(this.warehouseId, this.selectedPalletNo)
|
||
if (restorePallet && workspace.pallet.status !== 'OPEN') {
|
||
this.selectedPalletNo = ''; this.selectedPalletId = ''; this.palletCode = ''
|
||
clearDropshipPalletizingDraft(this.draftIdentity())
|
||
return
|
||
}
|
||
this.workspace = workspace
|
||
},
|
||
memberForCarton(carton) { return this.palletMembers.find((member) => String(member.inboundId) === String(carton.inboundId)) },
|
||
async activateScanTask() {
|
||
const targetId = String(this.scanTask?.taskId || this.taskId)
|
||
let target = await getDropshipPalletizationTask(targetId, this.warehouseId)
|
||
if (target.taskStatus === 'PENDING') target = await claimDropshipPalletizationTask(targetId, this.warehouseId)
|
||
this.taskId = targetId; this.task = target
|
||
saveDropshipPalletizingDraft(this.draftIdentity(), { activePalletNo: this.selectedPalletNo })
|
||
},
|
||
async activatePalletTask() {
|
||
const member = this.palletMembers.find((item) => String(item.taskId) === this.taskId) || this.palletMembers[0]
|
||
if (!member) throw new Error('托盘没有已归托箱,请先清点归托')
|
||
const target = await getDropshipPalletizationTask(member.taskId, this.warehouseId)
|
||
this.taskId = String(target.taskId); this.task = target
|
||
},
|
||
draftIdentity() { return { operatorId: this.user?.id, taskId: this.taskId, warehouseId: this.warehouseId } },
|
||
async moveCarton(carton, targetPalletNo) {
|
||
if (this.submitting || !isOnline() || !this.palletSetupReady) return
|
||
const payload = { warehouseId: this.warehouseId, targetPalletNo: targetPalletNo || null }
|
||
payload.operationId = stableOperationId('dropship-move', String(this.user?.id) + '-' + this.taskId, { cartonId: carton.cartonId, taskVersion: this.task.version, ...payload })
|
||
const member = this.memberForCarton(carton)
|
||
if (!member) { this.error = '未找到箱码所属任务,请刷新托盘'; return }
|
||
await this.runMutation(async () => { const target = await getDropshipPalletizationTask(member.taskId, this.warehouseId); this.taskId = String(target.taskId); this.task = target; return moveDropshipCarton(this.taskId, carton.cartonId, payload) }, targetPalletNo ? '箱码已换托' : '箱码已移回待绑托区')
|
||
},
|
||
continueOtherTask() { uni.redirectTo({ url: `/pages/dropship-tasks/dropship-tasks?warehouseId=${encodeURIComponent(this.warehouseId)}&palletNo=${encodeURIComponent(this.assignableCurrentPallet?.palletNo || '')}` }) },
|
||
async sealPallet(pallet) {
|
||
if (this.loading || this.submitting) return
|
||
const content = this.palletMembers.map((member) => `${member.customerName || '客户未关联'} / ${member.inboundCode}:${member.cartonCount}箱`).join('\n')
|
||
uni.showModal({ title: `确认封托 · ${pallet.cartonCount}箱`, content: `${content}\n请确认实物已装满,封托后按整托装柜。`, success: async (answer) => { if (answer.confirm) await this.runMutation(async () => { await this.activatePalletTask(); return sealDropshipPallet(this.taskId, this.warehouseId, pallet) }, `${pallet.palletNo} 已封托`) } })
|
||
},
|
||
openUnseal(pallet) { this.unsealTarget = pallet; this.unsealOpen = true; this.$refs.unsealPopup.open() },
|
||
closeUnseal() { this.unsealOpen = false; this.$refs.unsealPopup?.close(); this.unsealTarget = null },
|
||
handleUnsealPopupChange(event) { this.unsealOpen = Boolean(event.show); if (!event.show) this.unsealTarget = null },
|
||
async confirmUnseal() {
|
||
const pallet = this.unsealTarget; if (!pallet || this.submitting) return
|
||
this.$refs.unsealPopup.close(); this.unsealOpen = false
|
||
const result = await this.runMutation(async () => { await this.activatePalletTask(); return unsealDropshipPallet(this.taskId, this.warehouseId, pallet) }, `${pallet.palletNo} 已解除封托`)
|
||
this.unsealTarget = null
|
||
if (result) { this.selectedPalletNo = pallet.palletNo; saveDropshipPalletizingDraft(this.draftIdentity(), { activePalletNo: pallet.palletNo }) }
|
||
},
|
||
async runMutation(action, message) {
|
||
if (this.submitting) return null
|
||
if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return null }
|
||
this.submitting = true; this.resetMessages()
|
||
try {
|
||
const result = await action()
|
||
this.task = result?.taskId && Array.isArray(result?.pallets)
|
||
? result
|
||
: await getDropshipPalletizationTask(this.taskId, this.warehouseId)
|
||
this.notice = message
|
||
try { await this.refreshPallet() } catch (error) { this.error = this.formatOperationError(error, '操作已成功,托盘汇总刷新失败,请刷新后继续') }
|
||
return result
|
||
}
|
||
catch (error) { this.error = this.formatOperationError(error, '操作失败'); return null }
|
||
finally { this.submitting = false }
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.member-row { display: flex; justify-content: space-between; gap: 8px; padding: 8px 0; border-bottom: 1px solid #edf1f5; font-size: 12px; }
|
||
.carton-owner { display: block; color: #667085; font-size: 12px; line-height: 20px; overflow-wrap: anywhere; }
|
||
.content { padding: 0 0 32px; background: #fff; }
|
||
.state, .message { margin-bottom: 10px; padding: 12px; border-radius: 9px; color: #667085; font-size: 12px; line-height: 18px; text-align: center; background: #fff; }
|
||
.message.error { color: #b42318; background: #fff1f0; }.message.success { color: #067647; background: #ecfdf3; }
|
||
.task-summary-card, .section-card { margin: 0; padding: 11px 12px; border: 0; border-bottom: 6px solid #eef2f7; border-radius: 0; background: #fff; box-shadow: none; }
|
||
.continue-strip { padding: 10px 12px 12px; border-bottom: 6px solid #eef2f7; color: #475467; font-size: 13px; line-height: 19px; background: #f8fbff; }.continue-strip .secondary-button { margin-top: 7px; }
|
||
.task-summary-card { display: block; }
|
||
.summary-head, .section-heading, .pallet-head, .carton-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
|
||
.inbound-code { min-width: 0; color: #12233f; font-size: 17px; font-weight: 700; overflow-wrap: anywhere; }
|
||
.status, .pallet-status { flex-shrink: 0; padding: 4px 8px; border-radius: 99px; color: #155eef; font-size: 12px; background: #eff4ff; }.pallet-status.sealed { color: #067647; background: #ecfdf3; }
|
||
.warehouse-summary { display: block; margin-top: 3px; color: #667085; font-size: 12px; }
|
||
.compact-metrics { display: grid; grid-template-columns: repeat(5,1fr); margin-top: 8px; padding-top: 7px; border-top: 1px solid #edf1f5; }.compact-metrics > text { display: flex; align-items: center; justify-content: center; gap: 3px; color: #667085; font-size: 11px; white-space: nowrap; }.compact-metrics strong { color: #12233f; font-size: 15px; }.compact-metrics .pallet-readiness { color: #067647; }.compact-metrics .pallet-readiness.invalid,.compact-metrics .pallet-readiness.invalid strong { color: #b42318; }.pallet-setup-error { margin-top: 8px; margin-bottom: 0; text-align: left; }
|
||
.section-title { display: block; color: #12233f; font-size: 18px; font-weight: 700; }.section-heading { align-items: center; }.refresh-button { display: flex; align-items: center; justify-content: center; flex-shrink: 0; box-sizing: border-box; min-width: 88px; height: 44px; margin: 0; padding: 0 16px; border: 1px solid #9dc0f4; border-radius: 8px; color: #155eef; font-size: 16px; font-weight: 700; line-height: normal; background: #eaf2ff; }.refresh-button::after { border: 0; }.refresh-button[disabled] { color: #718095; border-color: #dce5f0; background: #e8edf3; }
|
||
.primary-button, .secondary-button { box-sizing: border-box; width: 100%; height: 46px; margin-top: 10px; border-radius: 9px; font-size: 14px; font-weight: 700; line-height: 44px; }.primary-button { border: 0; color: #fff; background: #1a73e8; }.secondary-button { border: 1px solid #8ab8ef; color: #155eef; background: #f6f9ff; }.primary-button[disabled],.secondary-button[disabled] { opacity: .45; }
|
||
.unseal-button { border-color: #fda29b; color: #b42318; background: #fff5f4; }.pallet-hint.blocked { color: #b42318; }
|
||
.unseal-dialog { box-sizing: border-box; width: min(350px, calc(100vw - 32px)); overflow: hidden; border-radius: 12px; background: #fff; }.unseal-copy { display: block; padding: 18px 16px; color: #475467; font-size: 14px; line-height: 21px; }.unseal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 0 16px 16px; }.unseal-actions .secondary-button { margin-top: 0; }.danger-button { height: 46px; margin: 0; border: 0; border-radius: 9px; color: #fff; font-size: 14px; font-weight: 700; background: #d92d20; }.danger-button[disabled] { opacity: .45; }
|
||
.count-entry { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; padding: 5px 7px; border: 0; border-radius: 0; color: #667085; font-size: 13px; background: #f6f9ff; }.compact-action { width: 86px; height: 34px; margin: 0; border: 0; border-radius: 6px; color: #fff; font-size: 13px; line-height: 34px; background: #1a73e8; }.compact-action[disabled] { opacity: .35; }.count-unavailable { padding: 12px 8px; color: #b42318; font-size: 13px; line-height: 18px; text-align: center; background: #fff1f0; }.pallet-scan-block { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e6edf5; }.field-label { display: block; margin-bottom: 5px; color: #344054; font-size: 15px; font-weight: 700; }.field-label text { margin-left: 3px; color: #98a2b3; font-size: 12px; font-weight: 400; }
|
||
.carton-card, .pallet-card { margin-top: 8px; padding: 8px 0 0; border: 0; border-top: 1px solid #e3eaf2; border-radius: 0; background: transparent; }.sku-count-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #edf1f5; }.sku-copy { min-width: 0; }.sku-label { display: block; color: #667085; font-size: 11px; }.sku-code { display: block; margin-top: 2px; color: #12233f; font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }.pallet-meta { display: block; margin-top: 5px; color: #667085; font-size: 14px; line-height: 20px; }
|
||
.quantity-stepper { display: grid; grid-template-columns: 38px 56px 38px; overflow: hidden; border: 1px solid #b8c6d8; border-radius: 8px; background: #fff; }.quantity-stepper button,.quantity-stepper input { box-sizing: border-box; height: 40px; margin: 0; border: 0; border-radius: 0; text-align: center; background: #fff; }.quantity-stepper button { color: #155eef; font-size: 20px; line-height: 40px; }.quantity-stepper input { width: 56px; border-right: 1px solid #dce4ed; border-left: 1px solid #dce4ed; color: #12233f; font-size: 15px; }.difference-field { grid-column: 1 / -1; margin-top: 0; }.pallet-hint { display: block; margin-top: 10px; color: #155eef; font-size: 11px; text-align: center; }
|
||
.counted-quantity { flex: 0 0 auto; color: #067647; font-size: 14px; font-weight: 700; }
|
||
.field { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; color: #344054; font-size: 12px; }.field input { box-sizing: border-box; width: 120px; height: 42px; padding: 0 10px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; }.field.stacked { align-items: stretch; flex-direction: column; gap: 6px; }.field textarea { box-sizing: border-box; width: 100%; height: 70px; padding: 9px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; }
|
||
.pallet-code { color: #12233f; font-size: 19px; font-weight: 700; overflow-wrap: anywhere; }.text-button { min-width: 58px; height: 40px; border: 0; color: #155eef; font-size: 14px; line-height: 40px; background: transparent; }.empty { padding: 18px 0 8px; color: #98a2b3; font-size: 14px; text-align: center; }
|
||
.palletization-page :deep(.scan-target-value) { font-size: 17px; }
|
||
</style>
|