75 lines
6.5 KiB
Vue
75 lines
6.5 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import { getCargoVerification, saveCargoVerification, type CargoVerificationContext, type CargoVerificationSource, type SaveCargoVerification } from './cargoVerificationApi'
|
||
import { rejectedCommand, useWorkDraft } from './useWorkDraft'
|
||
const props = defineProps<{ source: CargoVerificationSource }>()
|
||
const emit = defineEmits<{ saved: [] }>()
|
||
const access = useAccessPreviewStore()
|
||
const context = ref<CargoVerificationContext | null>(null), loading = ref(false), saving = ref(false)
|
||
const error = ref<ApiErrorDetails | null>(null), saveError = ref<ApiErrorDetails | null>(null), pending = ref<SaveCargoVerification | null>(null)
|
||
const form = reactive({ packages: '', references: '', remark: '' }), baseline = ref(JSON.stringify(form))
|
||
const dirty = computed(() => JSON.stringify(form) !== baseline.value)
|
||
useWorkDraft(dirty, computed(() => saving.value || !!pending.value))
|
||
const canWrite = computed(() => access.can('platform:overseas-warehouse:inbound-order:fba-appointment') && context.value?.allowedActions.includes('VERIFY_CARGO'))
|
||
async function load(preserve = false) {
|
||
loading.value = true; error.value = null
|
||
if (!access.can('platform:appointment:list') || !access.can('platform:overseas-warehouse:last-mile-order:list')) { error.value = { message: '当前账号没有该货源的读取权限,请联系预约负责人处理。' }; loading.value = false; return }
|
||
try {
|
||
context.value = await getCargoVerification(props.source)
|
||
if (!preserve) {
|
||
const record = context.value.verification
|
||
Object.assign(form, { packages: record?.packageCount == null ? '' : String(record.packageCount), references: record?.shipmentReferences.join('\n') || '', remark: record?.remark || '' })
|
||
baseline.value = JSON.stringify(form)
|
||
}
|
||
} catch (cause) { error.value = getApiErrorDetails(cause, '核实资料读取失败') }
|
||
finally { loading.value = false }
|
||
}
|
||
async function save() {
|
||
if (!context.value || !canWrite.value || loading.value || error.value || saving.value) return
|
||
if (!pending.value) {
|
||
const count = String(form.packages).trim() ? Number(form.packages) : null
|
||
if (count !== null && (!Number.isSafeInteger(count) || count < 1)) { ElMessage.warning('核实箱数须为正整数,暂未核实可留空'); return }
|
||
if (count !== null && (count < context.value.allocatedPackages || (context.value.source.packages != null && count > context.value.source.packages))) { ElMessage.warning('核实箱数不能低于已分配量,也不能超过已知原箱数'); return }
|
||
const references = [...new Set(form.references.split(/\r?\n/).map(value => value.trim()).filter(Boolean))]
|
||
if (references.length > 100 || references.some(value => value.length > 128)) { ElMessage.warning('最多填写100条货件号,每条不超过128字'); return }
|
||
pending.value = { destinationKey: context.value.source.destinationKey, expectedSourceVersion: context.value.source.sourceVersion, expectedVersion: Number(context.value.verification?.version || 0), packageCount: count, shipmentReferences: references, remark: form.remark.trim() || null, idempotencyKey: crypto.randomUUID() }
|
||
}
|
||
saving.value = true; saveError.value = null
|
||
try {
|
||
await saveCargoVerification(props.source, pending.value)
|
||
pending.value = null; baseline.value = JSON.stringify(form)
|
||
ElMessage.success('核实资料已保存,原订单与预约状态保持不变'); emit('saved'); await load()
|
||
} catch (cause) {
|
||
saveError.value = getApiErrorDetails(cause, '保存结果待确认,请重试本次保存')
|
||
if (rejectedCommand(cause)) { pending.value = null; await load(true) }
|
||
} finally { saving.value = false }
|
||
}
|
||
onMounted(() => load())
|
||
</script>
|
||
<template>
|
||
<section aria-label="货物核实资料">
|
||
<h3>核实货物资料</h3><p>未约成也可保存;只用于预约核实,保留原订单资料。</p>
|
||
<LoadErrorState v-if="error" title="核实资料读取失败" :message="error.message" :loading="loading" @retry="load(true)"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||
<p v-if="loading">正在读取货物资料…</p>
|
||
<template v-if="context">
|
||
<dl class="cargo-facts"><dt>原票</dt><dd>{{ context.source.inboundCode || context.source.bookingNo || '—' }}</dd><dt>目的仓</dt><dd>{{ context.source.destinationWarehouseCode || '—' }}</dd><dt>原箱数</dt><dd>{{ context.source.packages ?? '待核实' }}</dd><dt>已分配</dt><dd>{{ context.allocatedPackages }} 箱<span v-if="context.unknownAllocationCount">;另有 {{ context.unknownAllocationCount }} 项数量未知</span></dd><dt>可分配</dt><dd>{{ context.availablePackages ?? '待核实' }}</dd></dl>
|
||
<p v-if="context.sourceChanged" role="alert">原货源已变化,请核对当前原票和已分配量后重新保存。</p><p v-for="reason in context.blockReasons" :key="reason" role="status">{{ reason }}</p>
|
||
<form class="density-form work-form" @submit.prevent="save"><fieldset :disabled="!canWrite || loading || !!error || saving || !!pending">
|
||
<label>核实箱数(可暂不填)<input v-model="form.packages" type="number" min="1" step="1" aria-label="核实箱数"></label>
|
||
<label>本票货件号 / 追踪号(每行一个)<textarea v-model="form.references" rows="4" aria-label="本票货件号" /></label>
|
||
<label>核实说明<textarea v-model="form.remark" maxlength="1000" rows="2" /></label>
|
||
</fieldset><p v-if="!canWrite">当前仅可查看核实资料;需核实权限及有效货源资格才能保存。</p>
|
||
<LoadErrorState v-if="saveError" title="核实资料未保存" :message="saveError.message" :show-retry="false"><template #details><ApiErrorMeta :details="saveError" /></template></LoadErrorState>
|
||
<el-button type="primary" native-type="submit" :loading="saving" :disabled="!canWrite || loading || !!error">{{ pending ? '重试本次保存' : '保存核实资料' }}</el-button></form>
|
||
</template>
|
||
</section>
|
||
</template>
|
||
<style scoped>
|
||
.cargo-facts { display: grid; grid-template-columns: 80px 1fr; gap: var(--admin-space-3); }.cargo-facts dd { margin: 0; overflow-wrap: anywhere; }.cargo-facts dt { color: var(--el-text-color-secondary); }
|
||
</style>
|