优化
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
<template v-else-if="discardCurrent">
|
||||
<view class="detail-toolbar"><view><text class="detail-kicker">废弃订单</text><text class="detail-title">{{ discardCurrent.discardNo }}</text></view><button :disabled="discardBusy" @tap="closeDiscard">‹ 返回列表</button></view>
|
||||
<view class="fulfillment-card"><ScanTargetInput name="cargo-discard" :model-value="discardPalletCode" label="废弃托盘码" placeholder="扫描对应托盘码" :active="true" @update:modelValue="updateDiscardPalletCode" @submit="resolveDiscardPallet" /><button class="secondary-button" :disabled="!discardPalletCode || discardLoading || discardBusy" @tap="resolveDiscardPallet">{{ discardLoading ? '读取中…' : '打开托盘废弃作业' }}</button><text class="scan-help">扫描该废弃订单对应的托盘,再填写每个 SKU 的实际废弃数量。</text></view>
|
||||
<view v-if="discardTask" class="fulfillment-card item-card"><view class="task-head"><text class="business-code">托盘 {{ discardTask.palletNo }}</text><text class="status-badge">{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><view v-for="sku in discardTask.skus" :key="sku.itemId" class="outbound-line"><text class="line-code">{{ sku.skuCode || '—' }}</text><text class="line-meta">{{ sku.productName || '—' }} · 计划 {{ sku.plannedPackages }} 箱</text><view class="count-row"><text>实际废弃数量</text><input type="number" :value="discardActuals[sku.itemId]" :disabled="discardTask.status === 'COMPLETED'" :placeholder="`请输入 0-${sku.plannedPackages} 箱`" @input="updateDiscardActual(sku.itemId, $event)" /><text>最多 {{ sku.plannedPackages }} 箱</text></view></view><button class="primary-button" :disabled="!canConfirmDiscard || discardBusy" @tap="confirmDiscard">{{ discardBusy ? '提交中…' : '废弃出库' }}</button></view>
|
||||
<view v-if="discardTask" class="fulfillment-card item-card"><view class="task-head"><text class="business-code">托盘 {{ discardTask.palletNo }}</text><text class="status-badge">{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><view v-for="sku in discardTask.skus" :key="sku.itemId" class="outbound-line"><text class="line-code">{{ sku.skuCode || '—' }}</text><text class="line-meta">{{ sku.productName || '—' }} · 计划 {{ sku.plannedPackages }} 箱</text><view class="count-row"><text>实际废弃数量</text><input type="number" :value="discardActuals[sku.itemId]" :disabled="discardTask.status === 'COMPLETED'" :placeholder="`请输入 0-${sku.plannedPackages} 箱`" @input="updateDiscardActual(sku.itemId, $event)" /><text>最多 {{ sku.plannedPackages }} 箱</text></view></view><view class="discard-photo-section"><text class="business-code">废弃现场照片(至少 1 张)</text><text class="scan-help">照片关联当前托盘,完成出库后可在废弃单详情查看。</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" class="fulfillment-success" role="status"><text>{{ successMessage }}</text></view>
|
||||
@@ -58,7 +58,7 @@
|
||||
<script>
|
||||
import PortalHeader from '@/components/PortalHeader.vue'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { completeOutboundTask, confirmCargoDiscard, getOutboundTask, listCargoDiscardTasks, listOutboundTasks, normalizeCargoDiscardTask, outboundPickPayload, pickOutboundPallet, pickOutboundTask, resolveCargoDiscardPallet, resolveOutboundTask } from '@/api/fulfillment.js'
|
||||
import { listCargoDiscardPhotos, uploadCargoDiscardPhoto, downloadCargoDiscardPhoto, completeOutboundTask, confirmCargoDiscard, getOutboundTask, listCargoDiscardTasks, listOutboundTasks, normalizeCargoDiscardTask, outboundPickPayload, pickOutboundPallet, pickOutboundTask, resolveCargoDiscardPallet, resolveOutboundTask } from '@/api/fulfillment.js'
|
||||
import { fulfillment, fulfillmentIdempotencyKey, releaseFulfillmentKey, restoreFulfillmentDraft, updateFulfillmentDraft } from '@/stores/fulfillment.js'
|
||||
import { requireSession, session } from '@/stores/session.js'
|
||||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
export default {
|
||||
components: { PortalHeader, ScanTargetInput },
|
||||
data() { return { user: session.currentUser, tasks: [], discardTasks: [], current: null, discardCurrent: null, discardTask: null, discardActuals: {}, discardPalletCode: '', discardLoading: false, discardBusy: false, selectedItem: null, selectedPosition: null, selectedScanCode: '', scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } },
|
||||
data() { return { user: session.currentUser, tasks: [], discardTasks: [], current: null, discardCurrent: null, discardTask: null, discardActuals: {}, discardPhotos: [], discardPhotoUrls: {}, discardPhotoErrors: {}, discardPhotoBusy: false, discardPhotoReady: false, discardPalletCode: '', discardLoading: false, discardBusy: false, selectedItem: null, selectedPosition: null, selectedScanCode: '', scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } },
|
||||
computed: {
|
||||
displayItems() {
|
||||
const items = this.current?.items || []
|
||||
@@ -101,9 +101,9 @@
|
||||
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, palletCount: 0, pendingPalletCount: 0 }; current.palletCount += 1; if (task.status !== 'COMPLETED') current.pendingPalletCount += 1; groups.set(task.discardNo, current) } return [...groups.values()] },
|
||||
discardOrderCount() { return this.discardOrders.length },
|
||||
canConfirmDiscard() { return Boolean(!this.discardLoading && !this.discardBusy && this.discardTask && this.discardTask.status !== 'COMPLETED' && this.discardTask.skus.length && this.discardTask.skus.every((sku) => Number.isInteger(Number(this.discardActuals[sku.itemId])) && Number(this.discardActuals[sku.itemId]) >= 0 && Number(this.discardActuals[sku.itemId]) <= sku.plannedPackages)) }
|
||||
canConfirmDiscard() { return Boolean(this.discardPhotoReady && this.discardPhotos.length > 0 && !this.discardPhotoBusy && !this.discardLoading && !this.discardBusy && this.discardTask && this.discardTask.status !== 'COMPLETED' && this.discardTask.skus.length && this.discardTask.skus.every((sku) => Number.isInteger(Number(this.discardActuals[sku.itemId])) && Number(this.discardActuals[sku.itemId]) >= 0 && Number(this.discardActuals[sku.itemId]) <= sku.plannedPackages)) }
|
||||
},
|
||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.startScanner(); if (!this.bootstrapped) { this.bootstrapped = true; this.scanCode = ''; this.loadTasks(); return } if (this.current?.taskNo) this.openTask(this.current.taskNo); else this.loadTasks() },
|
||||
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: {
|
||||
@@ -129,25 +129,59 @@
|
||||
}, 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) { const code = String(discardNo || '').trim(); if (!code || this.discardBusy || this.busy || this.loading) return; this.current = null; this.discardLoading = false; this.discardCurrent = { discardNo: code }; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; this.error = null },
|
||||
closeDiscard() { if (this.discardBusy) return; this.discardLoading = false; this.discardCurrent = null; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; 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 }; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; this.error = null },
|
||||
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 resolveDiscardPallet(raw) {
|
||||
const code = String(raw?.detail?.value ?? raw ?? this.discardPalletCode).trim()
|
||||
if (!code || !this.discardCurrent || this.discardBusy || this.discardLoading) return
|
||||
if (!code || !this.discardCurrent || this.discardBusy || this.discardLoading || this.discardPhotoBusy) return
|
||||
const order = this.discardCurrent
|
||||
this.discardPalletCode = code; this.discardLoading = true; this.discardTask = null; this.discardActuals = {}; this.error = null
|
||||
this.resetDiscardPhotos(); this.discardPalletCode = code; this.discardLoading = true; this.discardTask = null; this.discardActuals = {}; this.error = null
|
||||
try {
|
||||
const task = await resolveCargoDiscardPallet(code)
|
||||
if (this.discardCurrent !== order) return
|
||||
if (task.discardNo !== order.discardNo) throw new Error('扫描的托盘不属于当前废弃订单')
|
||||
this.discardTask = normalizeCargoDiscardTask(task)
|
||||
this.discardActuals = Object.fromEntries(this.discardTask.skus.map((sku) => [sku.itemId, sku.actualPackages == null ? sku.plannedPackages : sku.actualPackages]))
|
||||
this.discardActuals = Object.fromEntries(this.discardTask.skus.map((sku) => [sku.itemId, sku.actualPackages == null ? sku.plannedPackages : sku.actualPackages])); await this.restoreDiscardPhotos()
|
||||
} catch (error) { if (this.discardCurrent === order) this.setError(error, '废弃托盘读取失败') }
|
||||
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 } })
|
||||
},
|
||||
updateDiscardActual(itemId, event) { const value = Math.max(0, Math.trunc(Number(event.detail.value || 0))); this.discardActuals = { ...this.discardActuals, [itemId]: value } },
|
||||
async confirmDiscard() { if (this.discardBusy || this.discardLoading || !this.canConfirmDiscard || !this.discardTask || !this.discardCurrent) return; this.discardBusy = true; try { const lines = this.discardTask.skus.map((sku) => ({ itemId: sku.itemId, actualPackages: Number(this.discardActuals[sku.itemId]) })); await confirmCargoDiscard(this.discardCurrent.discardNo, this.discardTask.palletNo, lines); this.successMessage = '废弃出库作业已完成,等待仓库确认'; this.discardPalletCode = ''; this.discardTask = null; this.discardTasks = await listCargoDiscardTasks(); playSuccessSound() } catch (error) { this.setError(error, '废弃出库失败') } finally { this.discardBusy = false } },
|
||||
async confirmDiscard() { if (this.discardBusy || this.discardLoading || !this.canConfirmDiscard || !this.discardTask || !this.discardCurrent) return; this.discardBusy = true; try { const lines = this.discardTask.skus.map((sku) => ({ itemId: sku.itemId, actualPackages: Number(this.discardActuals[sku.itemId]) })); await confirmCargoDiscard(this.discardCurrent.discardNo, this.discardTask.palletNo, lines, this.discardPhotos.map(photo => String(photo.fileId))); this.successMessage = '废弃出库作业及照片已提交,等待仓库确认'; this.resetDiscardPhotos(); this.discardPalletCode = ''; this.discardTask = null; this.discardTasks = await listCargoDiscardTasks(); playSuccessSound() } catch (error) { 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 })
|
||||
@@ -189,6 +223,7 @@
|
||||
|
||||
<style scoped>
|
||||
@import '../dispatch/fulfillment.scss';
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user