366 lines
46 KiB
Vue
366 lines
46 KiB
Vue
<template>
|
||
<view v-if="user" class="page-shell portal-page fulfillment-page">
|
||
<PortalHeader title="出库作业" :user="user" />
|
||
<scroll-view class="fulfillment-scroll" scroll-y>
|
||
<view class="portal-content fulfillment-content" :class="{ 'pda-task-list': !current && !discardCurrent }">
|
||
<view class="fulfillment-heading"><text class="fulfillment-title">出库任务</text></view>
|
||
<view v-if="current" class="detail-toolbar"><view><text class="detail-kicker">当前任务</text><text class="detail-title">出库作业详情</text></view><button :disabled="busy" @tap="closeTask">‹ 返回列表</button></view>
|
||
<view v-if="!discardCurrent" class="fulfillment-card">
|
||
<ScanTargetInput name="outbound" :model-value="scanCode" :label="scanLabel" :placeholder="scanPlaceholder" :active="true" @update:modelValue="updateScanCode" @submit="handleScan" />
|
||
<button class="secondary-button" :disabled="!scanCode || loading || busy || discardLoading || discardBusy" @tap="handleScan(scanCode)">{{ discardCurrent ? (discardTask?.scanMode === 'CARTON' ? '核对废弃箱唛' : '打开废弃托盘') : current ? '匹配出库货物' : '打开出库任务' }}</button>
|
||
</view>
|
||
|
||
<view v-if="loading" class="fulfillment-state"><text>正在加载出库任务…</text></view>
|
||
<view v-else-if="error" class="fulfillment-error" role="alert"><text class="state-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><text v-if="error.network" class="offline-note">网络连接失败,请刷新核对任务状态后重试。</text><button class="secondary-button" :disabled="loading || busy || discardLoading || discardBusy" @tap="retry">重试</button></view>
|
||
|
||
<template v-if="!loading && !current && !discardCurrent">
|
||
<view class="list-toolbar pda-list-toolbar"><text>待处理出库任务<text v-if="!error"> · {{ tasks.length }} 项</text></text><button :disabled="loading" @tap="loadTasks"><uni-icons type="refresh" color="#135ee8" size="18" /><text>刷新</text></button></view>
|
||
<view v-if="tasks.length" class="task-list pda-list-cards">
|
||
<button v-for="task in tasks" :key="task.taskNo" class="task-card pda-task-card" hover-class="pda-task-card-pressed" @tap="openTask(task.taskNo)">
|
||
<view class="pda-task-head"><text class="pda-task-no">{{ task.outboundNo || '—' }}</text><text class="status-badge pda-task-status">{{ statusLabel(task.status) }}</text></view>
|
||
<view class="pda-task-meta"><text>{{ taskType(task) }}</text><text v-if="String(task.taskNo || '').trim() && String(task.taskNo || '').trim() !== String(task.outboundNo || '').trim()"> · 任务号 {{ task.taskNo }}</text></view>
|
||
<view class="pda-task-footer"><text v-if="task.taskNo === recentTaskNo" class="recent-badge">上次作业</text><view class="pda-task-action"><text>查看并开始出库</text><uni-icons type="right" color="#135ee8" size="14" /></view></view>
|
||
</button>
|
||
</view>
|
||
<view v-else-if="!error && !discardTasks.length" class="fulfillment-state"><text class="state-title">暂无出库任务</text><text>可刷新列表,或扫描任务号恢复未完成作业。</text></view>
|
||
<view v-if="discardTasks.length" class="discard-section"><view class="list-toolbar"><view><text class="list-title">废弃订单</text><text class="list-count">{{ discardOrderCount }} 个订单</text></view></view><view class="task-list"><button v-for="order in discardOrders" :key="order.discardNo" class="task-card" @tap="openDiscardOrder(order.discardNo)"><view class="task-head"><view class="task-code-row"><text class="business-code">{{ order.discardNo || '—' }}</text></view><text class="status-badge">废弃订单</text></view><text v-if="order.outboundNo" class="task-meta">出库单 {{ order.outboundNo }} · 废弃出库</text><text class="task-meta">托盘 {{ order.palletCount }} 个 · 按箱废弃 {{ order.cartonCount }} 箱 · 待作业 {{ order.pendingPalletCount }} 个</text><text class="task-enter">点击进入废弃出库作业 ›</text></button></view></view>
|
||
</template>
|
||
|
||
<template v-if="current">
|
||
<view class="fulfillment-card summary-card"><view class="task-head"><text class="business-code">{{ current.outboundNo || '—' }}</text><text class="status-badge">{{ statusLabel(current.status) }}</text></view><text class="task-meta">{{ taskType(current) }} · 任务号 {{ current.taskNo || '—' }}</text><view class="metrics"><view><text class="metric-value">{{ plannedPackages }}</text><text>计划箱数</text></view><view><text class="metric-value success">{{ pickedPackages }}</text><text>已拣箱数</text></view><view><text class="metric-value warning">{{ remainingPackages }}</text><text>待拣箱数</text></view></view></view>
|
||
|
||
<view v-if="current.items.length" class="item-list">
|
||
<view v-for="item in displayItems" :key="item.displayKey || item.outboundPalletItemId" class="fulfillment-card item-card">
|
||
<view class="task-head"><text class="business-code">{{ item.palletNo || '散件出库' }}</text><text>{{ item.pickedPackages }}/{{ item.plannedPackages }} 箱</text></view>
|
||
<view v-for="position in current.outboundType === 'RETURN_OUTBOUND' && item.palletNo ? [] : item.positions" :key="businessLineKey(position)" class="outbound-line" :class="{ selected: selectedPosition === position }" @tap="selectPosition(item, position)">
|
||
<text class="line-code">{{ lineBusinessCode(position) }}</text><text class="line-meta">{{ position.inboundCode || '无入仓码' }} · 已拣 {{ position.pickedPackages }} 箱 · 可拣 {{ position.availablePackages }} 箱</text>
|
||
</view>
|
||
<text v-if="current.outboundType === 'RETURN_OUTBOUND' && item.palletNo && item.pickedPackages < item.plannedPackages" class="scan-help">整托出库请扫描托盘号 {{ item.palletNo }}</text>
|
||
<text v-if="!item.positions.length && item.pickedPackages < item.plannedPackages" class="missing-scan-contract">当前明细缺少可扫码库存业务码,已禁止拣货;请刷新或联系平台确认任务下发。</text>
|
||
</view>
|
||
</view>
|
||
<view v-else class="fulfillment-state"><text class="state-title">任务详情尚无可拣货明细</text><text>请刷新;若持续为空,请联系平台确认出库货物是否已下发。</text></view>
|
||
|
||
<view v-if="selectedPosition && selectedRemaining > 0" class="fulfillment-card"><text class="card-title">本次拣货:{{ lineBusinessCode(selectedPosition) }}</text><text v-if="selectedItem.returnSkuQuantity != null" class="scan-help">本来源计划出库 {{ selectedItem.returnSkuQuantity }} 件,请清点这些件数后确认;余数保留原箱。</text><view class="count-row"><text>拣货箱数</text><input type="number" :value="packages" :placeholder="`请输入 1-${selectedRemaining} 箱`" @input="updatePackages" /><text>最多 {{ selectedRemaining }} 箱</text></view><button class="primary-button" :disabled="!canPick || busy" @tap="submitPick">{{ busyAction === 'pick' ? '提交中…' : '确认出库拣货' }}</button></view>
|
||
|
||
<view class="fulfillment-card gate-card"><text class="card-title">完成出库任务</text><text v-if="remainingPackages > 0" class="gate-warning">尚有 {{ remainingPackages }} 箱未拣,不能完成出库。</text><text v-else-if="current.status === 'COMPLETED'" class="gate-success">出库任务已完成,不可重复提交。</text><button v-if="current.status !== 'COMPLETED'" class="primary-button" :disabled="!canComplete || busy" @tap="completeTask">{{ busyAction === 'complete' ? '完成中…' : '完成出库任务' }}</button></view>
|
||
</template>
|
||
|
||
<template v-else-if="discardCurrent">
|
||
<view class="detail-toolbar"><view><text class="detail-kicker">废弃订单</text><text class="detail-title">{{ discardCurrent.discardNo }}</text><text v-if="discardCurrent.outboundNo" class="task-meta">出库单 {{ discardCurrent.outboundNo }}</text></view><button :disabled="discardBusy" @tap="closeDiscard">‹ 返回列表</button></view>
|
||
<view v-if="discardOrderRemark" class="fulfillment-card discard-remark"><text class="card-title">废弃备注</text><text class="task-meta">{{ discardOrderRemark }}</text></view>
|
||
<view v-if="discardOrderRemaining != null" class="fulfillment-card"><text>申请废弃 {{ discardCurrentTasks[0]?.requestedPackages ?? discardTask?.requestedPackages }} 箱 · 剩余 {{ discardOrderRemaining }} 箱</text></view>
|
||
<view v-if="successMessage" class="fulfillment-success" role="status"><text>{{ successMessage }}</text></view>
|
||
<view v-if="!discardTask && discardPendingTasks.length" class="fulfillment-card"><text class="card-title">废弃箱关联托盘</text><view v-for="task in discardPendingTasks" :key="task.palletNo" class="outbound-line"><view class="task-head"><text class="business-code">{{ task.palletNo === '散箱' ? '散箱' : task.palletNo }}</text><text>待废弃</text></view></view></view>
|
||
<view v-if="!discardTask && discardPendingTasks.length" class="fulfillment-card"><ScanTargetInput name="cargo-discard" :model-value="discardPalletCode" label="废弃托盘码" placeholder="扫描对应托盘码" :active="discardTask?.scanMode !== 'CARTON'" @update:modelValue="updateDiscardPalletCode" @submit="resolveDiscardPallet" /></view>
|
||
<view v-if="discardTask" class="fulfillment-card"><ScanTargetInput v-if="discardNeedsRefresh" name="cargo-discard-retry" :model-value="discardPalletCode" label="重新扫描废弃托盘" placeholder="重新扫描当前托盘码" :active="true" @update:modelValue="updateDiscardPalletCode" @submit="resolveDiscardPallet" /><view v-else class="task-head"><text class="business-code">{{ discardTask.palletNo }}</text><text>{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view></view>
|
||
<view v-if="discardTask" class="fulfillment-card item-card">
|
||
<text v-if="discardQuantityLines.length" class="discard-scanned-count">已扫描 {{ discardUnboundScans.length }} 箱</text>
|
||
<view v-if="discardQuantityLines.length > 1" class="quantity-row"><button v-for="sku in discardQuantityLines" :key="sku.itemId" class="secondary-button" :disabled="discardBusy" @tap="discardScanItemId = sku.itemId">{{ sku.skuCode || '—' }}{{ discardScanItemId === sku.itemId ? '(当前扫描)' : '' }}</button></view>
|
||
<view v-if="discardQuantityLines.length"><text class="business-code">扫描箱号出库</text><ScanTargetInput name="cargo-discard-count" :model-value="discardCartonCode" label="废弃箱唛" placeholder="扫描现场已有箱唛" :active="true" @update:modelValue="discardCartonCode = $event" @submit="scanDiscardCarton" /><view v-if="discardUnboundScans.length" class="discard-carton-heading" role="button" :aria-expanded="discardScannedExpanded" :aria-label="discardScannedExpanded ? '收起已扫描箱唛' : '展开已扫描箱唛'" @tap="discardScannedExpanded = !discardScannedExpanded"><text class="scan-help">已扫描 {{ discardUnboundScans.length }} 箱</text><view class="discard-carton-chevron" :class="{ expanded: discardScannedExpanded }" aria-hidden="true"></view></view><view v-if="discardScannedExpanded"><view v-for="scan in discardUnboundScans" :key="scan.mark" class="outbound-line"><text>{{ scan.mark }}</text><button class="discard-scan-remove" :disabled="discardBusy || discardPhotoBusy" :aria-label="`移除箱唛 ${scan.mark}`" @tap="removeDiscardScan(scan.mark)">移除</button></view></view></view><view v-if="discardTask.scanMode === 'CARTON'"><view class="discard-carton-heading" role="button" :aria-expanded="discardCartonsExpanded" :aria-label="discardCartonsExpanded ? '收起箱号核对' : '展开箱号核对'" @tap="discardCartonsExpanded = !discardCartonsExpanded"><text class="business-code">箱号核对</text><view class="discard-carton-heading-end"><text class="scan-help">待扫描 {{ discardPendingMarks.length }} 箱</text><view class="discard-carton-chevron" :class="{ expanded: discardCartonsExpanded }" aria-hidden="true"></view></view></view>
|
||
<text class="scan-help">已核对 {{ discardScannedMarks.length }}/{{ discardTask.cartonMarks.length }} 箱</text>
|
||
<view v-if="discardCartonsExpanded">
|
||
<view v-for="mark in discardPendingMarks" :key="mark" class="outbound-line"><text>{{ mark }}</text></view>
|
||
<text v-if="!discardPendingMarks.length" class="scan-help">箱号已全部核对</text>
|
||
</view><view v-if="discardTask.status !== 'COMPLETED'"><text class="business-code">扫描箱号出库</text><ScanTargetInput name="cargo-discard-carton" :model-value="discardCartonCode" label="废弃箱号" placeholder="扫描本次废弃的系统箱号" :active="true" @update:modelValue="discardCartonCode = $event" @submit="scanDiscardCarton" /></view></view><view class="discard-photo-section"><text class="business-code">废弃现场照片(至少 1 张)</text><view class="discard-photo-grid"><view v-for="photo in discardPhotos" :key="photo.fileId" class="discard-photo-tile"><image v-if="discardPhotoUrls[photo.fileId]" :src="discardPhotoUrls[photo.fileId]" mode="aspectFill" @tap="previewDiscardPhoto(photo.fileId)" /><button v-else class="secondary-button" @tap="loadDiscardPhoto(photo.fileId)">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button></view></view><button v-if="discardTask.status !== 'COMPLETED'" class="secondary-button" :disabled="discardLoading || discardBusy || discardPhotoBusy || !discardPhotoReady || discardPhotos.length >= 20" @tap="takeDiscardPhoto">{{ discardPhotoBusy ? '照片上传中…' : '拍摄废弃现场照片' }}({{ discardPhotos.length }}/20)</button><button v-if="!discardPhotoReady" class="secondary-button" :disabled="discardLoading || discardPhotoBusy" @tap="restoreDiscardPhotos">重试读取照片</button></view><button class="primary-button" :disabled="!canConfirmDiscard || discardBusy" @tap="confirmDiscard">{{ discardBusy ? '提交中…' : '废弃出库' }}</button></view>
|
||
</template>
|
||
|
||
<view v-if="successMessage && !discardCurrent" class="fulfillment-success" role="status"><text>{{ successMessage }}</text></view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import PortalHeader from '@/components/PortalHeader.vue'
|
||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||
import { listCargoDiscardPhotos, uploadCargoDiscardPhoto, downloadCargoDiscardPhoto, completeOutboundTask, confirmCargoDiscard, getOutboundTask, listCargoDiscardTasks, listOutboundTasks, normalizeCargoDiscardTask, outboundPickPayload, pickOutboundPallet, pickOutboundTask, resolveCargoDiscardPallet, resolveOutboundTask } from '@/api/fulfillment.js'
|
||
import { fulfillment, fulfillmentIdempotencyKey, restoreFulfillmentDraft, updateFulfillmentDraft } from '@/stores/fulfillment.js'
|
||
import { requireSession, session } from '@/stores/session.js'
|
||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||
|
||
export default {
|
||
components: { PortalHeader, ScanTargetInput },
|
||
data() { return { user: session.currentUser, tasks: [], discardTasks: [], current: null, discardCurrent: null, discardTask: null, discardScannedExpanded: false, discardUnboundScans: [], discardScanItemId: '', discardActuals: {}, discardScannedMarks: [], discardCartonsExpanded: true, discardCartonCode: '', discardPhotos: [], discardPhotoUrls: {}, discardPhotoErrors: {}, discardPhotoBusy: false, discardPhotoReady: false, discardPalletCode: '', discardLoading: false, discardBusy: false, discardNeedsRefresh: false, selectedItem: null, selectedPosition: null, selectedScanCode: '', scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } },
|
||
computed: {
|
||
// 已扫描箱号保留提交凭据,列表只显示尚未核对的箱子。
|
||
discardOrderRemark() { return this.discardTask?.remark || this.discardCurrentTasks.find(task => task.remark)?.remark || '' },
|
||
discardOrderRemaining() { return this.discardTask?.remainingPackages ?? this.discardCurrentTasks[0]?.remainingPackages ?? null },
|
||
discardPendingMarks() { return (this.discardTask?.cartonMarks || []).filter(mark => !this.discardScannedMarks.includes(mark)) },
|
||
discardQuantityLines() { return (this.discardTask?.skus || []).filter(sku => sku.actualQuantityEditable) },
|
||
displayItems() {
|
||
const items = this.current?.items || []
|
||
if (this.current?.outboundType !== 'RETURN_OUTBOUND') return items
|
||
const pallets = new Map()
|
||
const rows = []
|
||
for (const item of items) {
|
||
if (!item.palletNo) { rows.push(item); continue }
|
||
let pallet = pallets.get(item.palletNo)
|
||
if (!pallet) {
|
||
pallet = { ...item, displayKey: `pallet:${item.palletNo}`, plannedPackages: 0, pickedPackages: 0, positions: [] }
|
||
pallets.set(item.palletNo, pallet)
|
||
rows.push(pallet)
|
||
}
|
||
pallet.plannedPackages += Number(item.plannedPackages || 0)
|
||
pallet.pickedPackages += Number(item.pickedPackages || 0)
|
||
pallet.positions.push(...(item.positions || []))
|
||
}
|
||
return rows
|
||
},
|
||
hasReturnPallet() { return this.current?.outboundType === 'RETURN_OUTBOUND' && (this.current.items || []).some(item => item.palletNo) },
|
||
scanLabel() { return this.discardCurrent ? (this.discardTask?.scanMode === 'CARTON' ? '废弃箱唛' : '废弃订单托盘码') : this.hasReturnPallet ? '扫描托盘号(退货整托出库)' : '任务号、出库单号、排车单号、托盘号或箱码' },
|
||
scanPlaceholder() { return this.discardCurrent ? (this.discardTask?.scanMode === 'CARTON' ? '扫描本单所选箱唛' : '扫描废弃订单对应托盘码') : this.hasReturnPallet ? '扫描托盘号;散箱扫描箱唛' : '扫描任务号 / 出库单号 / 排车单号 / 托盘号 / 箱码' },
|
||
recentTaskNo() { return fulfillment.outbound.currentTaskNo || '' },
|
||
packages() { return fulfillment.outbound.packages },
|
||
plannedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.plannedPackages, 0) },
|
||
pickedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.pickedPackages, 0) },
|
||
remainingPackages() { return Math.max(0, this.plannedPackages - this.pickedPackages) },
|
||
selectedRemaining() { return Math.max(0, Math.min(Number(this.selectedItem?.plannedPackages || 0) - Number(this.selectedItem?.pickedPackages || 0), Number(this.selectedPosition?.availablePackages || 0))) },
|
||
taskOperable() { return ['PENDING', 'IN_PROGRESS'].includes(this.current?.status) },
|
||
canPick() { return this.taskOperable && this.selectedItem && this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining },
|
||
canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 && this.taskOperable },
|
||
discardOrders() { const groups = new Map(); for (const task of this.discardTasks) { const current = groups.get(task.discardNo) || { discardNo: task.discardNo, outboundNo: task.outboundNo, palletCount: 0, cartonCount: 0, pendingPalletCount: 0 }; if (task.scanMode === 'CARTON') current.cartonCount += (task.cartonMarks || []).length; else current.palletCount += 1; if (task.status !== 'COMPLETED') current.pendingPalletCount += 1; groups.set(task.discardNo, current) } return [...groups.values()] },
|
||
discardOrderCount() { return this.discardOrders.length },
|
||
discardPendingTasks() { return this.discardCurrentTasks.filter(task => task.status !== 'COMPLETED') },
|
||
discardCurrentTasks() { return this.discardTasks.filter(task => task.discardNo === this.discardCurrent?.discardNo) },
|
||
discardQuantitySummary() {
|
||
const task = this.discardTask
|
||
if (!task) return ''
|
||
const pieces = task.skus.filter(sku => sku.quantityUnit === '件').reduce((total, sku) => total + sku.plannedPackages, 0)
|
||
const boxes = new Set(task.cartonMarks || []).size || task.skus.filter(sku => sku.quantityUnit !== '件').reduce((total, sku) => total + sku.plannedPackages, 0)
|
||
return `${boxes} 箱${pieces ? ` · ${pieces} 件` : ''}`
|
||
},
|
||
canConfirmDiscard() { const task = this.discardTask; return Boolean(this.discardPhotoReady && this.discardPhotos.length > 0 && !this.discardPhotoBusy && !this.discardLoading && !this.discardBusy && task && task.status !== 'COMPLETED' && (task.remainingPackages == null || task.scanMode === 'CARTON' || task.skus.reduce((sum, sku) => sum + Number(this.discardActuals[sku.itemId] || 0), 0) <= task.remainingPackages) && task.skus.length && task.skus.every(sku => this.validDiscardActual(sku)) && (task.scanMode !== 'CARTON' || (task.cartonMarks?.length && task.cartonMarks.every(mark => this.discardScannedMarks.includes(mark))))) }
|
||
},
|
||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.startScanner(); if (this.discardPhotoBusy || this.discardBusy) return; if (this.discardCurrent) { if (this.discardTask) this.restoreDiscardPhotos(); return } if (!this.bootstrapped) { this.bootstrapped = true; this.scanCode = ''; this.loadTasks(); return } if (this.current?.taskNo) this.openTask(this.current.taskNo); else this.loadTasks() },
|
||
onHide() { this.stopScanner() },
|
||
beforeUnmount() { this.stopScanner() },
|
||
methods: {
|
||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleScan(value), onError: (error) => this.setError(error, '扫码广播注册失败') }) },
|
||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||
updateScanCode(value) { this.scanCode = value; updateFulfillmentDraft('outbound', { scanCode: value }) },
|
||
updatePackages(event) { const value = Math.trunc(Number(event.detail.value || 0)); updateFulfillmentDraft('outbound', { packages: Number.isFinite(value) ? Math.max(0, value) : 0 }) },
|
||
async loadTasks() { await this.run('加载出库任务', async () => { const [tasks, discardTasks] = await Promise.all([listOutboundTasks('PENDING'), listCargoDiscardTasks()]); this.tasks = tasks; this.discardTasks = discardTasks; this.current = null; this.discardCurrent = null; this.discardTask = null }, true) },
|
||
async openTask(taskNo) { const code = String(taskNo || '').trim(); if (!code) return; await this.run('恢复出库任务', async () => { const pendingScan = fulfillment.outbound.currentTaskNo === code ? fulfillment.outbound.scanCode : ''; this.current = await getOutboundTask(code); this.selectedItem = null; this.selectedPosition = null; this.scanCode = pendingScan; updateFulfillmentDraft('outbound', { currentTaskNo: this.current.taskNo, scanCode: pendingScan, packages: pendingScan ? fulfillment.outbound.packages : 1 }); if (pendingScan) this.findAndSelectPosition(pendingScan, false) }, true) },
|
||
async openScannedTask(businessNo) {
|
||
await this.run('解析出库作业', async () => {
|
||
const resolution = await resolveOutboundTask(businessNo)
|
||
if (resolution?.workflow === 'DISPATCH' && resolution.dispatchNo) {
|
||
updateFulfillmentDraft('outbound', { currentTaskNo: '', scanCode: '', packages: 1 })
|
||
updateFulfillmentDraft('dispatch', { currentNo: resolution.dispatchNo, palletCode: '', packages: 1 })
|
||
uni.navigateTo({ url: '/pages/dispatch/dispatch?resume=1' })
|
||
return
|
||
}
|
||
if (resolution?.workflow !== 'OUTBOUND' || !resolution.taskNo) throw new Error('服务端未返回可执行的出库任务')
|
||
this.current = await getOutboundTask(resolution.taskNo)
|
||
this.selectedItem = null; this.selectedPosition = null; this.scanCode = ''
|
||
updateFulfillmentDraft('outbound', { currentTaskNo: this.current.taskNo, scanCode: '', packages: 1 })
|
||
}, true)
|
||
},
|
||
closeTask() { this.current = null; this.selectedItem = null; this.selectedPosition = null; this.error = null; this.successMessage = ''; updateFulfillmentDraft('outbound', { currentTaskNo: '', scanCode: '', packages: 1 }); this.loadTasks() },
|
||
openDiscardOrder(discardNo) { if (this.discardPhotoBusy) return; this.resetDiscardPhotos(); const code = String(discardNo || '').trim(); if (!code || this.discardBusy || this.busy || this.loading) return; this.current = null; this.discardLoading = false; this.discardCurrent = { discardNo: code, outboundNo: this.discardOrders.find(order => order.discardNo === code)?.outboundNo || '' }; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; this.discardUnboundScans = []; this.discardScannedExpanded = false; this.discardScanItemId = ''; this.discardScannedMarks = []; this.discardCartonsExpanded = true; this.discardCartonCode = ''; this.discardNeedsRefresh = false; this.error = null; this.successMessage = ''; },
|
||
closeDiscard() { if (this.discardBusy || this.discardPhotoBusy) return; this.resetDiscardPhotos(); this.discardLoading = false; this.discardCurrent = null; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; this.loadTasks() },
|
||
updateDiscardPalletCode(value) { this.discardPalletCode = value },
|
||
// 下方扫描框只核对当前任务的真实箱号,不切换托盘或清空照片。
|
||
async scanDiscardCarton(raw) {
|
||
if (!this.discardTask || this.discardTask.status === 'COMPLETED' || this.discardBusy || this.discardLoading || this.discardPhotoBusy) return
|
||
const code = String(typeof raw === 'string' ? raw : raw?.detail?.value ?? this.discardCartonCode).trim()
|
||
if (!code) return
|
||
if (this.discardNeedsRefresh && !await this.refreshDiscardTask()) return
|
||
if (this.discardTask.status === 'COMPLETED') return
|
||
if (this.discardQuantityLines.length) {
|
||
if ((this.discardCurrentTasks || []).some(task => task.palletNo === code)) { this.setError(new Error('请扫描箱唛,不能把托盘码作为箱唛'), '箱唛核对失败'); return }
|
||
const sku = this.discardQuantityLines.length === 1 ? this.discardQuantityLines[0] : this.discardQuantityLines.find(line => line.itemId === this.discardScanItemId)
|
||
if (!sku) { this.setError(new Error('请先选择当前扫描的货物'), '箱唛核对失败'); return }
|
||
if (!(sku.allowedCartonMarks || []).includes(code)) { this.setError(new Error('箱唛不属于本废弃单的原入仓库存'), '箱唛核对失败'); return }
|
||
if (this.discardUnboundScans.some(scan => scan.mark === code)) { this.discardCartonCode = ''; return }
|
||
if (code.length > 128 || this.discardUnboundScans.filter(scan => scan.itemId === sku.itemId).length >= sku.plannedPackages || this.discardUnboundScans.length >= (this.discardTask.remainingPackages ?? Infinity)) { this.setError(new Error('扫描箱数不能超过本托预留或申请剩余箱数'), '箱唛核对失败'); return }
|
||
this.discardUnboundScans = [...this.discardUnboundScans, { itemId: sku.itemId, mark: code }]
|
||
this.discardActuals = { ...this.discardActuals, [sku.itemId]: this.discardUnboundScans.filter(scan => scan.itemId === sku.itemId).length }
|
||
this.discardCartonCode = ''; this.error = null; playSuccessSound(); return
|
||
}
|
||
if (!this.discardTask.cartonMarks.includes(code)) { this.setError(new Error('箱号不属于当前废弃任务'), '箱号核对失败'); return }
|
||
if (!this.discardScannedMarks.includes(code)) this.discardScannedMarks = [...this.discardScannedMarks, code]
|
||
this.discardCartonCode = ''; this.error = null; playSuccessSound()
|
||
},
|
||
/** 删除误扫的现场箱唛,并同步扣回对应明细实扫数量。 */
|
||
removeDiscardScan(mark) {
|
||
if (this.discardBusy || this.discardPhotoBusy) return
|
||
const removed = this.discardUnboundScans.find(scan => scan.mark === mark)
|
||
if (!removed) return
|
||
this.discardUnboundScans = this.discardUnboundScans.filter(scan => scan.mark !== mark)
|
||
this.discardActuals = { ...this.discardActuals, [removed.itemId]: this.discardUnboundScans.filter(scan => scan.itemId === removed.itemId).length }
|
||
this.error = null; this.successMessage = ''
|
||
},
|
||
async resolveDiscardPallet(raw) {
|
||
const code = String(typeof raw === 'string' ? raw : raw?.detail?.value ?? this.discardPalletCode).trim()
|
||
if (!code || !this.discardCurrent || this.discardBusy || this.discardLoading || this.discardPhotoBusy) return
|
||
if ((this.discardQuantityLines.length && !this.discardCurrentTasks.some(task => task.palletNo === code) && this.discardTask?.palletNo !== code) || (this.discardTask?.scanMode === 'CARTON' && this.discardTask.cartonMarks.includes(code))) { await this.scanDiscardCarton(code); return }
|
||
if (this.discardTask?.palletNo === code) { await this.refreshDiscardTask(); return }
|
||
// 当前托盘可重扫,同一废弃单内也可通过托盘或所选箱唛切换任务。
|
||
if (this.discardTask?.scanMode === 'CARTON' && !this.discardTasks.some(task => task.discardNo === this.discardCurrent.discardNo && (task.palletNo === code || (task.cartonMarks || []).includes(code)))) { this.setError(new Error('托盘或箱唛不属于当前废弃单所选货物'), '箱唛核对失败'); return }
|
||
// 有托盘的任务须先扫描本单托盘,箱号仅用于展开后的逐箱核对。
|
||
const selectedTasks = this.discardTasks.filter(task => task.discardNo === this.discardCurrent.discardNo)
|
||
if (selectedTasks.length && !selectedTasks.some(task => task.palletNo !== '散箱' ? task.palletNo === code : (task.cartonMarks || []).includes(code))) { this.setError(new Error('请先扫描当前废弃单对应的托盘码'), '托盘核对失败'); return }
|
||
const order = this.discardCurrent
|
||
this.resetDiscardPhotos(); this.discardScannedMarks = []; this.discardCartonsExpanded = true; this.discardCartonCode = ''; this.discardPalletCode = code; this.discardLoading = true; this.discardNeedsRefresh = false; this.discardTask = null; this.discardUnboundScans = []; this.discardScannedExpanded = false; this.discardScanItemId = ''; this.discardActuals = {}; this.error = null; this.successMessage = ''
|
||
try {
|
||
const task = await resolveCargoDiscardPallet(code, order.discardNo)
|
||
if (this.discardCurrent !== order) return
|
||
if (task.discardNo !== order.discardNo) throw new Error('扫描的托盘不属于当前废弃订单')
|
||
this.discardTask = normalizeCargoDiscardTask(task)
|
||
if (this.discardTask.scanMode === 'CARTON' && this.discardTask.cartonMarks.includes(code)) this.discardScannedMarks = [code]
|
||
this.discardActuals = Object.fromEntries(this.discardTask.skus.map((sku) => [sku.itemId, sku.actualPackages == null ? (sku.actualQuantityEditable ? 0 : sku.plannedPackages) : sku.actualPackages])); await this.restoreDiscardPhotos()
|
||
} catch (error) { if (this.discardCurrent === order) this.setError(error, '废弃托盘读取失败') }
|
||
finally { if (this.discardCurrent === order) this.discardLoading = false }
|
||
},
|
||
/** 重扫与重试先读取当前权威任务,只保留仍匹配的箱号和实点数;照片仍归当前托盘。 */
|
||
async refreshDiscardTask() {
|
||
if (!this.discardCurrent || !this.discardTask || this.discardLoading || this.discardPhotoBusy) return false
|
||
const order = this.discardCurrent; const previous = this.discardTask
|
||
this.discardLoading = true
|
||
try {
|
||
const latest = await resolveCargoDiscardPallet(previous.palletNo, order.discardNo)
|
||
if (this.discardCurrent !== order || this.discardTask !== previous) return false
|
||
if (latest.discardNo !== order.discardNo || latest.palletNo !== previous.palletNo) throw new Error('刷新结果不属于当前废弃托盘')
|
||
this.discardTask = normalizeCargoDiscardTask(latest)
|
||
this.discardScannedMarks = this.discardScannedMarks.filter(mark => (this.discardTask.cartonMarks || []).includes(mark))
|
||
this.discardUnboundScans = this.discardUnboundScans.filter(scan => this.discardTask.skus.some(sku => sku.actualQuantityEditable && sku.itemId === scan.itemId))
|
||
this.discardActuals = Object.fromEntries(this.discardTask.skus.map(sku => {
|
||
const old = previous.skus.find(line => line.itemId === sku.itemId)
|
||
const unchanged = old && old.plannedPackages === sku.plannedPackages && old.actualQuantityEditable === sku.actualQuantityEditable
|
||
return [sku.itemId, sku.actualPackages ?? (unchanged ? this.discardActuals[sku.itemId] : sku.actualQuantityEditable ? 0 : sku.plannedPackages)]
|
||
}))
|
||
this.discardPalletCode = latest.palletNo; this.discardNeedsRefresh = false; this.error = null
|
||
if (!this.discardPhotoReady) await this.restoreDiscardPhotos()
|
||
if (latest.status === 'COMPLETED') this.successMessage = '当前托盘已完成废弃,请返回列表核对本单剩余任务'
|
||
return true
|
||
} catch (error) { if (this.discardCurrent === order) { this.discardNeedsRefresh = true; this.setError(error, '废弃任务刷新失败,请重新扫描') }; return false }
|
||
finally { if (this.discardCurrent === order) this.discardLoading = false }
|
||
},
|
||
resetDiscardPhotos() { this.discardPhotos = []; this.discardPhotoUrls = {}; this.discardPhotoErrors = {}; this.discardPhotoReady = false },
|
||
async restoreDiscardPhotos() {
|
||
if (!this.discardTask || !this.discardCurrent) return
|
||
const order = this.discardCurrent; const task = this.discardTask
|
||
this.discardPhotoReady = false
|
||
try {
|
||
const photos = await listCargoDiscardPhotos(order.discardNo, task.palletNo)
|
||
if (this.discardCurrent !== order || this.discardTask !== task) return
|
||
this.discardPhotos = photos; this.discardPhotoReady = true
|
||
await Promise.all(photos.map(photo => this.loadDiscardPhoto(photo.fileId)))
|
||
} catch (error) { if (this.discardCurrent === order && this.discardTask === task) this.setError(error, '废弃照片读取失败') }
|
||
},
|
||
async loadDiscardPhoto(fileId) {
|
||
if (!this.discardCurrent || !this.discardTask) return
|
||
const order = this.discardCurrent; const task = this.discardTask
|
||
try {
|
||
const url = await downloadCargoDiscardPhoto(order.discardNo, task.palletNo, fileId)
|
||
if (this.discardCurrent === order && this.discardTask === task) { this.discardPhotoUrls = { ...this.discardPhotoUrls, [fileId]: url }; this.discardPhotoErrors = { ...this.discardPhotoErrors, [fileId]: false } }
|
||
} catch (_) { if (this.discardCurrent === order && this.discardTask === task) this.discardPhotoErrors = { ...this.discardPhotoErrors, [fileId]: true } }
|
||
},
|
||
previewDiscardPhoto(fileId) { const urls = this.discardPhotos.map(photo => this.discardPhotoUrls[photo.fileId]).filter(Boolean); if (this.discardPhotoUrls[fileId]) uni.previewImage({ current: this.discardPhotoUrls[fileId], urls }) },
|
||
takeDiscardPhoto() {
|
||
if (!this.discardCurrent || !this.discardTask || this.discardTask.status === 'COMPLETED' || this.discardPhotoBusy || this.discardBusy || this.discardLoading || !this.discardPhotoReady || this.discardPhotos.length >= 20) return
|
||
const order = this.discardCurrent; const task = this.discardTask; this.discardPhotoBusy = true
|
||
uni.chooseImage({ count: 1, sourceType: ['camera'], success: async ({ tempFilePaths }) => {
|
||
try {
|
||
if (this.discardCurrent !== order || this.discardTask !== task || !tempFilePaths?.[0]) return
|
||
const photo = await uploadCargoDiscardPhoto(order.discardNo, task.palletNo, tempFilePaths[0])
|
||
if (this.discardCurrent !== order || this.discardTask !== task) return
|
||
this.discardPhotos.push(photo); this.discardPhotoUrls = { ...this.discardPhotoUrls, [photo.fileId]: tempFilePaths[0] }; this.error = null
|
||
} catch (error) { this.setError(error, '废弃照片上传失败'); await this.restoreDiscardPhotos() }
|
||
finally { this.discardPhotoBusy = false }
|
||
}, fail: () => { this.discardPhotoBusy = false } })
|
||
},
|
||
// 未填写、负数、小数或超出预留数均阻止提交,保留输入供仓库修正。
|
||
validDiscardActual(sku) { const raw = this.discardActuals[sku.itemId]; if (raw == null || String(raw).trim() === '') return false; if (sku.actualQuantityEditable && Number(raw) !== this.discardUnboundScans.filter(scan => scan.itemId === sku.itemId).length) return false; const actual = Number(raw); return Number.isSafeInteger(actual) && actual >= 0 && (sku.actualQuantityEditable === true ? actual <= sku.plannedPackages : actual === sku.plannedPackages) },
|
||
updateDiscardActual(itemId, event) { this.discardActuals = { ...this.discardActuals, [itemId]: event.detail.value } },
|
||
/** 单托完成后刷新同单待办,仍有托盘时明确提示继续扫码。 */
|
||
async confirmDiscard() {
|
||
if (this.discardBusy || this.discardLoading || !this.canConfirmDiscard || !this.discardTask || !this.discardCurrent) return
|
||
this.error = null; this.successMessage = ''; this.discardBusy = true
|
||
const palletNo = this.discardTask.palletNo; const discardNo = this.discardCurrent.discardNo
|
||
try {
|
||
if (this.discardNeedsRefresh) {
|
||
if (!await this.refreshDiscardTask() || this.discardTask.status === 'COMPLETED' || !this.discardPhotoReady || !this.discardTask.skus.every(sku => this.validDiscardActual(sku)) || (this.discardTask.scanMode === 'CARTON' && !this.discardTask.cartonMarks.every(mark => this.discardScannedMarks.includes(mark)))) return
|
||
}
|
||
const lines = this.discardTask.skus.map(sku => ({ itemId: sku.itemId, actualPackages: Number(this.discardActuals[sku.itemId]), ...(sku.actualQuantityEditable ? { scannedCartonMarks: this.discardUnboundScans.filter(scan => scan.itemId === sku.itemId).map(scan => scan.mark) } : {}) }))
|
||
const photoFileIds = this.discardPhotos.map(photo => String(photo.fileId))
|
||
const key = fulfillmentIdempotencyKey('outbound', 'DISCARD', { discardNo, palletNo, lines, photoFileIds, scannedCartonMarks: this.discardScannedMarks, confirmAllCartons: false })
|
||
await confirmCargoDiscard(discardNo, palletNo, lines, photoFileIds, this.discardScannedMarks, false, { idempotencyKey: key.value })
|
||
this.resetDiscardPhotos(); this.discardPalletCode = ''; this.discardTask = null; this.discardScannedMarks = []; this.discardCartonCode = ''
|
||
// 确认已成功时,刷新失败不能被显示为出库失败或宣告整单完成。
|
||
try { this.discardTasks = await listCargoDiscardTasks() }
|
||
catch (error) { this.discardTasks = this.discardTasks.filter(task => task.discardNo !== discardNo || task.palletNo !== palletNo); this.successMessage = '托盘 ' + palletNo + ' 废弃已完成,其他托盘待办刷新失败,请返回列表重新加载'; this.setError(error, '待办刷新失败'); return }
|
||
const remaining = this.discardPendingTasks.map(task => task.palletNo)
|
||
this.successMessage = remaining.length ? '托盘 ' + palletNo + ' 废弃已完成,本单还有 ' + remaining.length + ' 个待废弃任务:' + remaining.join('、') + ',请继续扫描对应托盘' : '本单全部废弃出库作业及照片已提交,已完成'
|
||
playSuccessSound()
|
||
} catch (error) { this.discardNeedsRefresh = true; this.setError(error, '废弃出库失败') }
|
||
finally { this.discardBusy = false }
|
||
},
|
||
handleScan(raw) {
|
||
const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading || this.discardBusy || this.discardLoading) return
|
||
console.info('[Overseas PDA outbound] scan', { code, task: this.current?.taskNo || '', discard: this.discardCurrent?.discardNo || '' }); this.scanCode = code; updateFulfillmentDraft('outbound', { scanCode: code })
|
||
if (this.discardCurrent) return this.resolveDiscardPallet(code)
|
||
const discardOrder = !this.current && this.discardOrders.find(order => order.discardNo === code || order.outboundNo === code)
|
||
if (discardOrder) return this.openDiscardOrder(discardOrder.discardNo)
|
||
if (!this.current) return this.openScannedTask(code)
|
||
if (code === this.current.taskNo || code === this.current.outboundNo) return this.openTask(this.current.taskNo)
|
||
if (!this.taskOperable) return
|
||
if ((this.current.items || []).some((item) => item.palletNo === code)) return this.submitPalletPick(code)
|
||
if (this.findAndSelectPosition(code)) return
|
||
this.setError(new Error('扫码未匹配当前出库任务的托盘码、箱码、SKU 或入仓码'), '扫码未匹配')
|
||
},
|
||
findAndSelectPosition(code, feedback = true) { for (const item of this.current?.items || []) { if (this.current?.outboundType === 'RETURN_OUTBOUND' && item.palletNo) continue; const position = item.positions.find((value) => value.availablePackages > 0 && [value.palletNo, value.cartonMark, value.skuCode, value.inboundCode].filter(Boolean).includes(code)); if (position) { this.selectPosition(item, position, code); if (feedback) playSuccessSound(); return true } } return false },
|
||
selectPosition(item, position, scannedCode = '') { if (!this.taskOperable || !position || position.availablePackages <= 0 || (this.current?.outboundType === 'RETURN_OUTBOUND' && item.palletNo)) return; this.selectedItem = item; this.selectedPosition = position; this.selectedScanCode = scannedCode || this.lineBusinessCode(position); updateFulfillmentDraft('outbound', { scanCode: this.selectedScanCode, packages: Math.max(1, Math.min(item.plannedPackages - item.pickedPackages, position.availablePackages)) }) },
|
||
async submitPick() {
|
||
if (!this.canPick) return
|
||
const payload = outboundPickPayload(this.selectedItem, this.selectedPosition, this.packages, this.selectedScanCode); const key = fulfillmentIdempotencyKey('outbound', 'pick', { taskNo: this.current.taskNo, ...payload })
|
||
await this.run('提交出库拣货', async () => { await pickOutboundTask(this.current.taskNo, { ...payload, idempotencyKey: key.value }); this.current = await getOutboundTask(this.current.taskNo); this.selectedItem = null; this.selectedPosition = null; this.selectedScanCode = ''; updateFulfillmentDraft('outbound', { scanCode: '', packages: 1 }); this.successMessage = '本次出库拣货已记录'; playSuccessSound() })
|
||
},
|
||
async submitPalletPick(palletNo) {
|
||
const code = String(palletNo || '').trim(); if (!code || !this.taskOperable) return
|
||
const key = fulfillmentIdempotencyKey('outbound', 'pallet-pick', { taskNo: this.current.taskNo, palletNo: code })
|
||
await this.run('整托拣货', async () => { await pickOutboundPallet(this.current.taskNo, code, { idempotencyKey: key.value }); this.current = await getOutboundTask(this.current.taskNo); this.selectedItem = null; this.selectedPosition = null; updateFulfillmentDraft('outbound', { scanCode: '', packages: 1 }); this.successMessage = `${code} 整托拣货已完成`; playSuccessSound() })
|
||
},
|
||
async completeTask() {
|
||
if (!this.canComplete) return
|
||
const key = fulfillmentIdempotencyKey('outbound', 'complete', { taskNo: this.current.taskNo })
|
||
await this.run('完成出库任务', async () => { await completeOutboundTask(this.current.taskNo, { idempotencyKey: key.value }); this.current = await getOutboundTask(this.current.taskNo); this.successMessage = '出库任务已完成'; playSuccessSound() })
|
||
},
|
||
async retry() { if (this.discardCurrent) { if (this.discardTask) await this.refreshDiscardTask(); else if (this.discardPalletCode) await this.resolveDiscardPallet(this.discardPalletCode); return } if (this.current?.taskNo) await this.openTask(this.current.taskNo); else if (fulfillment.outbound.currentTaskNo) await this.openTask(fulfillment.outbound.currentTaskNo); else if (this.scanCode) await this.openScannedTask(this.scanCode); else await this.loadTasks() },
|
||
async run(action, work, loading = false) { if (this.busy || this.loading) return; this.error = null; this.successMessage = ''; if (loading) this.loading = true; else { this.busy = true; this.busyAction = action.includes('完成') ? 'complete' : 'pick' } try { await work() } catch (error) { this.setError(error, `${action}失败`) } finally { this.loading = false; this.busy = false; this.busyAction = '' } },
|
||
setError(error, fallback) { const forbidden = ['403', 'A0403'].includes(String(error?.code || '')); this.error = { message: forbidden ? '当前账号没有海外仓 PDA 作业权限,请联系管理员授权' : error?.message || fallback, code: error?.code, requestId: error?.requestId, traceId: error?.traceId, network: Boolean(error?.network) }; console.error('[Overseas PDA outbound] failed', this.error); playErrorSound() },
|
||
lineBusinessCode(position) { return position?.cartonMark || position?.palletNo || position?.skuCode || position?.inboundCode || '业务码缺失' },
|
||
businessLineKey(position) { return [position?.cartonMark, position?.palletNo, position?.skuCode, position?.inboundCode].filter(Boolean).join(':') || `line-${position?.inventoryPositionId || 'missing'}` },
|
||
statusLabel(status) { return ({ PENDING: '待拣货', IN_PROGRESS: '拣货中', COMPLETED: '已完成', CANCELLED: '已取消' })[status] || status || '状态未知' },
|
||
taskType(task) { return ({ RETURN_OUTBOUND: '退货出库', RELABEL_OUTBOUND: '换标出库', ONE_CLICK_DROPSHIP: '一件代发出库', WAREHOUSE_TRANSFER_OUTBOUND: '仓储转运出库' })[task?.outboundType] || '普通出库' }
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
@import '../dispatch/fulfillment.scss';
|
||
.discard-carton-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 40px; cursor: pointer; }
|
||
.discard-carton-heading > .business-code { flex-shrink: 0; white-space: nowrap; }
|
||
.discard-carton-heading-end { display: flex; align-items: center; gap: 12px; }
|
||
.discard-carton-heading-end .scan-help { margin: 0; }
|
||
.discard-carton-chevron { flex-shrink: 0; width: 8px; height: 8px; margin-right: 3px; border-right: 2px solid #718095; border-bottom: 2px solid #718095; transform: rotate(45deg); }
|
||
.discard-carton-chevron.expanded { transform: rotate(225deg); }
|
||
.discard-scan-remove { margin-left: auto; padding: 4px 10px; border: 0; border-radius: 4px; background: #fef2f2; color: #dc2626; font-size: 12px; }
|
||
.discard-photo-section{margin:14px 0;padding-top:12px;border-top:1px solid #d9e3ee}.discard-photo-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:12px 0}.discard-photo-tile image{display:block;width:100%;height:120px;border-radius:8px}
|
||
.outbound-line { margin-top: 9px; padding: 10px; border: 1px solid #e0e7ef; border-radius: 8px; background: #f8fafc; }
|
||
.outbound-line.selected { border: 2px solid #135ee8; background: #edf5ff; }
|
||
.line-code, .line-meta { display: block; }
|
||
.line-code { color: #263b55; font-size: 12px; font-weight: 800; overflow-wrap: anywhere; }
|
||
.line-meta { margin-top: 4px; color: #718095; font-size: 10px; }
|
||
.missing-scan-contract { display: block; margin-top: 9px; padding: 9px; border-radius: 7px; color: #a56800; font-size: 10px; line-height: 16px; background: #fff6df; }
|
||
.discard-remark .card-title, .discard-remark .task-meta { color: #dc2626; }
|
||
.discard-scanned-count { display: block; margin-bottom: 8px; color: #dc2626; font-size: 15px; font-weight: 700; }
|
||
</style>
|
||
|
||
<style scoped lang="scss">
|
||
@import '../../styles/task-list.scss';
|
||
</style>
|