Files
pda-overseas-uniapp/src/components/DispatchLoadingDocuments.vue
T
2026-10-21 16:58:44 +08:00

102 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="loading-documents">
<view class="heading"><text>装车资料</text><button :disabled="busy" @tap="load">刷新</button></view>
<text v-if="snapshot?.stage === 'AFTER_LOADING'">上传并保存 5 张装车照片后,本车自动进入派送中。</text>
<view v-if="error" class="error" role="alert"><text>{{ error.message }}</text><text v-if="error.requestId">requestId:{{ error.requestId }}</text><text v-if="error.traceId">traceId:{{ error.traceId }}</text></view>
<view v-if="pending"><text>有装车照片尚未确认保存。</text><button :disabled="busy || !editable" @tap="recover">{{ pending.conflict ? '刷新并重试保存' : '核对原请求并继续保存' }}</button></view>
<view v-for="(slot, index) in slots" :key="slot" class="photo-card">
<text>装车照片 {{ index + 1 }}</text>
<template v-for="photo in snapshot?.photos.filter(item => item.slot === slot) || []" :key="photo.fileId">
<image v-if="images[photo.fileId]" :src="images[photo.fileId]" mode="aspectFit" @tap="preview(images[photo.fileId])" />
<button v-else :disabled="busy" @tap="load">图片读取失败,重试</button>
</template>
<text v-if="!snapshot?.photos.some(item => item.slot === slot)" class="empty">未上传</text>
<button v-if="editable" :disabled="busy || Boolean(pending)" @tap="choose(slot)">上传/替换图片</button>
</view>
</view>
</template>
<script>
import { session } from '@/stores/session.js'
import { toDispatchError } from '@/utils/dispatch-error.js'
import { loadingPhotoSlots, getLoadingDocuments, uploadLoadingPhoto, recoverLoadingPhoto, bindLoadingPhoto, readLoadingPhoto } from '@/api/dispatch-loading-documents.js'
export default {
props: { dispatchNo: { type: String, required: true } },
emits: ['saved', 'busy-change'],
data() { return { slots: loadingPhotoSlots, snapshot: null, pending: null, busy: false, error: null, images: {}, active: true, corrupted: false,
scope: `${session.currentUser?.id || ''}:${session.currentUser?.warehouseId || ''}`, number: this.dispatchNo } },
computed: {
storageKey() { return `dispatch-loading-documents:${this.scope}:${this.number}` },
editable() { return !this.corrupted && this.snapshot?.editable === true }
},
mounted() {
try {
const raw = uni.getStorageSync(this.storageKey)
if (raw) { const value = typeof raw === 'string' ? JSON.parse(raw) : raw; if (!this.slots.includes(value.slot) || !value.key) throw new Error('原装车照片请求无法读取'); this.pending = value }
} catch (cause) { this.corrupted = true; this.error = toDispatchError(cause, '原请求无法读取') }
if (!this.corrupted) this.load()
},
beforeUnmount() { this.active = false },
methods: {
ensureScope() { if (!this.active || this.number !== this.dispatchNo || this.scope !== `${session.currentUser?.id || ''}:${session.currentUser?.warehouseId || ''}`) throw new Error('作业身份已变化,请重新进入任务') },
persist() { if (this.pending) uni.setStorageSync(this.storageKey, JSON.stringify(this.pending)); else uni.removeStorageSync(this.storageKey) },
key() { return `loading-${Date.now()}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}` },
async previews() {
await Promise.allSettled((this.snapshot?.photos || []).map(async photo => {
if (this.images[photo.fileId]) return
const image = await readLoadingPhoto(this.number, photo); this.ensureScope(); this.images[photo.fileId] = image
}))
},
async load() { await this.run(async () => { const value = await getLoadingDocuments(this.number); this.ensureScope(); this.snapshot = value; await this.previews() }) },
async bind() {
const item = this.pending
if (!item?.photo || !this.snapshot) throw new Error('原上传尚未确认')
if (item.photo.slot !== item.slot || !item.photo.fileId) throw new Error('上传结果与装车照片框不一致')
if (!item.binding) { item.binding = { recordVersion: this.snapshot.recordVersion, idempotencyKey: this.key(), fileId: item.photo.fileId }; this.persist() }
try {
const value = await bindLoadingPhoto(this.number, item.slot, item.binding); this.ensureScope()
this.snapshot = value; this.pending = null; this.persist(); await this.previews()
uni.showToast({ title: value.stage === 'DELIVERING' && value.complete ? '资料齐全,已进入派送中' : '装车照片已保存', icon: 'none' })
} catch (cause) { if (this.pending && /^(?:A0|HTTP_)?409$/.test(String(cause.code))) { this.pending.conflict = true; this.persist() }; throw cause }
},
async choose(slot) {
if (!this.editable || this.pending || this.busy) return
await this.run(async () => {
const chosen = await uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['camera', 'album'] }); this.ensureScope()
const filePath = chosen.tempFilePaths?.[0]; if (!filePath) return
if (chosen.tempFiles?.[0]?.size > 10 * 1024 * 1024) throw new Error('请选择不超过10MiB的图片')
this.pending = { slot, key: this.key() }; this.persist()
const photo = await uploadLoadingPhoto(this.number, slot, filePath, this.pending.key); this.ensureScope()
this.pending.photo = photo; this.persist(); await this.bind(); return true
}, true)
},
async recover() {
if (!this.pending || !this.editable) return
await this.run(async () => {
if (this.pending.conflict) { const value = await getLoadingDocuments(this.number); this.ensureScope(); this.snapshot = value; this.pending.binding = null; this.pending.conflict = false; this.persist() }
if (!this.pending.photo) { const photo = await recoverLoadingPhoto(this.number, this.pending.key); this.ensureScope(); this.pending.photo = photo; this.persist() }
await this.bind(); return true
}, true)
},
preview(url) { uni.previewImage({ current: url, urls: [url] }) },
async run(work, notify = false) {
if (this.busy) return
this.busy = true; this.$emit('busy-change', true); this.error = null; let saved = false
try { this.ensureScope(); saved = await work() === true }
catch (cause) { if (this.active && !String(cause.errMsg || '').includes('cancel')) this.error = toDispatchError(cause, '装车资料操作失败') }
finally { if (this.active) { this.busy = false; this.$emit('busy-change', false) } }
if (saved && notify && this.active) this.$emit('saved')
}
}
}
</script>
<style scoped>
.loading-documents { padding: 24rpx; margin-top: 20rpx; background: #fff; border-radius: 16rpx; color: #334155; font-size: 28rpx; }
.heading { display: flex; align-items: center; justify-content: space-between; font-weight: 600; }
.heading button { margin: 0; font-size: 26rpx; }
.photo-card { padding: 20rpx; margin-top: 20rpx; border: 1rpx solid #dbe5f0; border-radius: 12rpx; }
.photo-card image { width: 100%; height: 260rpx; display: block; margin: 16rpx 0; }
.photo-card .empty { display: block; text-align: center; padding: 50rpx 0; color: #64748b; }
.error text { display: block; margin-top: 12rpx; color: #b91c1c; }
</style>