优化
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { accessTokenIdentity } from '@/api/session'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { confirmProcurement, rejectProcurement, getProcurementWorkflow, getProcurementFile, type ProcurementSummary, type ProcurementWorkflow } from '../api/procurement'
|
||||
import { procurementRequestKey } from '../procurementSubmission'
|
||||
import { dateTime } from '../procurementPresentation'
|
||||
|
||||
const props = defineProps<{ order: ProcurementSummary; decision: 'confirm' | 'reject' }>()
|
||||
const emit = defineEmits<{ close: []; changed: [] }>()
|
||||
const auth = useAuthStore()
|
||||
const owner = auth.token ? accessTokenIdentity(auth.token) : null
|
||||
const current = () => active && !!owner && owner === (auth.token ? accessTokenIdentity(auth.token) : null)
|
||||
const workflow = shallowRef<ProcurementWorkflow | null>(null)
|
||||
const loading = ref(false), saving = ref(false), error = ref(''), photoError = ref(''), reason = ref('')
|
||||
const photos = ref<Record<string, string>>({})
|
||||
const isReject = computed(() => props.decision === 'reject')
|
||||
const canSubmit = computed(() => !loading.value && workflow.value?.status === 'WAIT_CUSTOMER_CONFIRM' && auth.hasPermission('merchant:cross-border:procurement:create'))
|
||||
let active = true, generation = 0, attempt: { fingerprint: string; key: string } | null = null
|
||||
function clearPhotos() { Object.values(photos.value).forEach(url => URL.revokeObjectURL(url)); photos.value = {} }
|
||||
async function load() {
|
||||
if (loading.value || saving.value || !current()) return
|
||||
const sequence = ++generation
|
||||
loading.value = true; error.value = ''; photoError.value = ''; workflow.value = null; clearPhotos()
|
||||
try {
|
||||
const result = await getProcurementWorkflow(props.order.orderNo)
|
||||
if (!current() || sequence !== generation) return
|
||||
workflow.value = result
|
||||
if (result.status !== 'WAIT_CUSTOMER_CONFIRM') { error.value = '订单状态已更新,当前不能确认或驳回,请返回列表刷新'; return }
|
||||
const ids = [...new Set(result.receipts.flatMap(receipt => receipt.fileIds))]
|
||||
await Promise.all(ids.map(async id => {
|
||||
try {
|
||||
const blob = await getProcurementFile(props.order.orderNo, id)
|
||||
if (!current() || sequence !== generation) return
|
||||
if (!/^image\/(png|jpeg|gif|webp|bmp)$/i.test(blob.type)) throw new Error('附件不是可预览的收货照片')
|
||||
photos.value[id] = URL.createObjectURL(blob)
|
||||
} catch { if (current() && sequence === generation) photoError.value = '部分收货照片加载失败,请重新加载后核对' }
|
||||
}))
|
||||
} catch (cause) { if (current() && sequence === generation) error.value = cause instanceof Error ? cause.message : '采购结果加载失败' }
|
||||
finally { if (current() && sequence === generation) loading.value = false }
|
||||
}
|
||||
function fileName(id: string) { return workflow.value?.files?.find(file => file.fileId === id)?.fileName || '收货照片' }
|
||||
async function submitDecision() {
|
||||
if (saving.value || !current() || !canSubmit.value || !!photoError.value || !workflow.value) return
|
||||
if (isReject.value && (!reason.value.trim() || reason.value.trim().length > 1000)) { error.value = '请填写驳回原因,最多1000字'; return }
|
||||
const version = workflow.value.version, orderNo = props.order.orderNo
|
||||
const fingerprint = JSON.stringify([owner, orderNo, version, props.decision, isReject.value ? reason.value.trim() : ''])
|
||||
if (attempt?.fingerprint !== fingerprint) attempt = { fingerprint, key: procurementRequestKey() }
|
||||
saving.value = true; error.value = ''
|
||||
try {
|
||||
const data = { version, idempotencyKey: attempt.key }
|
||||
if (isReject.value) await rejectProcurement(orderNo, { ...data, reason: reason.value.trim() })
|
||||
else await confirmProcurement(orderNo, data)
|
||||
if (!current()) return
|
||||
ElMessage.success(isReject.value ? '已驳回,等待平台重新上传照片' : '采购结果已确认')
|
||||
emit('changed')
|
||||
} catch (cause) { if (current()) error.value = cause instanceof Error ? cause.message : '操作失败,请重试' }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
onBeforeUnmount(() => { active = false; generation++; clearPhotos() })
|
||||
void load()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog :model-value="true" :title="isReject ? '驳回采购结果' : '确认采购结果'" width="800px" append-to-body :close-on-click-modal="false" :close-on-press-escape="!saving" :show-close="!saving" @close="!saving && emit('close')">
|
||||
<div v-loading="loading" class="decision-content">
|
||||
<dl class="decision-quantities"><div><dt>采购单号</dt><dd>{{ order.orderNo }}</dd></div><div><dt>商品名称</dt><dd>{{ order.productName }}</dd></div><div><dt>采购数量</dt><dd>{{ order.purchaseQuantity }} 件</dd></div><div><dt>累计收货数量</dt><dd>{{ workflow?.receivedQuantity ?? '—' }} 件</dd></div></dl>
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
||||
<el-alert v-if="photoError" :title="photoError" type="error" :closable="false" />
|
||||
<el-button v-if="error || photoError" :disabled="saving || loading" @click="load">重新加载</el-button>
|
||||
<div class="decision-batches">
|
||||
<section v-for="(receipt, index) in workflow?.receipts || []" :key="receipt.id" class="decision-batch">
|
||||
<h3>第 {{ index + 1 }} 批:{{ receipt.quantity }} 件 <small>{{ dateTime(receipt.receivedAt) }}</small></h3>
|
||||
<div class="decision-photos"><el-image v-for="id in receipt.fileIds" :key="id" :src="photos[id]" :alt="fileName(id)" :preview-src-list="Object.values(photos)" :initial-index="Object.keys(photos).indexOf(id)" fit="contain" preview-teleported><template #error><span>{{ fileName(id) }}:{{ loading ? '加载中' : '照片未加载' }}</span></template></el-image></div>
|
||||
</section>
|
||||
</div>
|
||||
<p v-if="workflow && !workflow.receipts.length">暂无收货照片</p>
|
||||
<el-form v-if="isReject" label-position="top" :disabled="saving"><el-form-item label="驳回原因" required><el-input v-model="reason" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请填写采购结果不符合要求的原因" /></el-form-item></el-form>
|
||||
<p v-else>请核对采购数量和收货照片,确认后平台将安排出库。</p>
|
||||
</div>
|
||||
<template #footer><el-button :disabled="saving" @click="emit('close')">取消</el-button><el-button :type="isReject ? 'danger' : 'primary'" :loading="saving" :disabled="!canSubmit || !!photoError" @click="submitDecision">{{ isReject ? '提交驳回' : '确认采购结果' }}</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.decision-content { min-height: 160px; }
|
||||
.decision-quantities { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 0 0 16px; }
|
||||
.decision-quantities > div { display: flex; gap: 12px; min-width: 0; }
|
||||
dt { color: var(--muted); } dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.decision-batches { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin: 16px 0; }
|
||||
.decision-batch { min-width: 0; }
|
||||
h3 { margin: 0 0 8px; color: var(--navy); font-size: 14px; } small { display: block; margin-top: 4px; color: var(--muted); font-weight: 400; }
|
||||
.decision-photos { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; gap: 8px; overflow-x: auto; padding: 2px 0 6px; }
|
||||
.decision-photos :deep(.el-image) { flex: 0 0 112px; width: 112px; height: 84px; border: 1px solid var(--border); border-radius: 4px; }
|
||||
.decision-photos span { padding: 12px; overflow-wrap: anywhere; }
|
||||
</style>
|
||||
@@ -78,6 +78,7 @@ const totals = computed(() => feeTotals(props.detail.fees))
|
||||
<div><dt>收件人</dt><dd>{{ detail.order.contactName }}</dd></div>
|
||||
<div><dt>联系电话</dt><dd>{{ detail.order.contactPhone }}</dd></div>
|
||||
<div class="wide"><dt>收货地址</dt><dd>{{ detail.order.receivingAddress }}</dd></div>
|
||||
<div v-if="detail.order.remark" class="full"><dt>采购备注</dt><dd>{{ detail.order.remark }}</dd></div>
|
||||
<div v-if="detail.order.productUrl" class="full"><dt>商品链接</dt><dd><a v-if="productHref(detail.order.productUrl)" :href="productHref(detail.order.productUrl)" target="_blank" rel="noopener noreferrer">查看商品</a><span v-else>{{ text(detail.order.productUrl) }}</span></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user