Files
admin-web/src/modules/appointment-calendar/CargoVerificationForm.vue
T

81 lines
7.5 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import './appointmentNativeForms.css'
import '@/styles/adminWorkbench.css'
2026-10-20 15:48:23 +08:00
import InlineHelp from '@/shared/components/InlineHelp.vue'
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'
2026-10-21 14:30:33 +08:00
const props = defineProps<{ source: CargoVerificationSource; confirmation?: boolean; disabled?: boolean }>()
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)
2026-10-21 14:30:33 +08:00
const canLeave = useWorkDraft(dirty, computed(() => saving.value || !!pending.value))
defineExpose({ canLeave })
const canWrite = computed(() => !props.disabled && 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
2026-10-21 14:30:33 +08:00
Object.assign(form, { packages: context.value.effectivePackages == null ? '' : String(context.value.effectivePackages), 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))]
2026-10-21 14:30:33 +08:00
if (props.confirmation && (count === null || !references.length)) { ElMessage.warning('请填写本票箱数,以及至少一个 FBA 货件号或追踪号;外部约号不能代替货件号'); return }
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="货物核实资料">
2026-10-21 14:30:33 +08:00
<h3>{{ confirmation ? '补齐本票送仓资料' : '核实货物资料' }}<InlineHelp title="核实资料说明"><p>未约成也可保存;只用于预约核实,保留原订单资料。</p></InlineHelp></h3>
<p v-if="confirmation">请向客户或货物负责人取得本票完整的 FBA 货件号或追踪号,每行一个;不要填写预约外部约号。下方保存只补充货物资料,完成后还需保存预约安排。</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">
2026-10-21 14:30:33 +08:00
<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><template v-if="!confirmation"><dt>已分配</dt><dd>{{ context.allocatedPackages }} 箱<span v-if="context.unknownAllocationCount">;另有 {{ context.unknownAllocationCount }} 项数量未知</span></dd><dt>可分配</dt><dd>{{ context.availablePackages ?? '待核实' }}</dd></template></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 appointment-native-form" @submit.prevent="save"><fieldset :disabled="!canWrite || loading || !!error || saving || !!pending">
2026-10-21 14:30:33 +08:00
<label>{{ confirmation ? '本票核实总箱数(必填)' : '核实箱数(可暂不填)' }}<input v-model="form.packages" type="number" min="1" step="1" aria-label="核实箱数" class="workbench-input"></label>
<label>本票货件号 / 追踪号(每行一个)<textarea v-model="form.references" rows="4" aria-label="本票货件号" class="workbench-input" /></label>
<label>核实说明<textarea v-model="form.remark" maxlength="1000" rows="2" class="workbench-input" /></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>
<div class="density-actions density-actions--footer appointment-form-actions"><el-button type="primary" native-type="submit" :loading="saving" :disabled="!canWrite || loading || !!error">{{ pending ? '重试本次保存' : '保存核实资料' }}</el-button></div></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>