102 lines
7.1 KiB
Vue
102 lines
7.1 KiB
Vue
<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>
|