118 lines
8.3 KiB
Vue
118 lines
8.3 KiB
Vue
<script setup lang="ts">
|
||
import { computed, reactive, ref, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import { executeUsTruckCommand } from '../api/platformOverseasBusiness'
|
||
import { overseasBusinessPermissions } from '../permissions'
|
||
import { availableTruckCommands, truckCommandHints, truckCommandLabels, type TruckCommand, type TruckOperation } from '../truckWorkflow'
|
||
import type { LocalTransportDetail } from '../types'
|
||
|
||
const props = defineProps<{ detail: LocalTransportDetail }>()
|
||
const emit = defineEmits<{ updated: [detail: LocalTransportDetail] }>()
|
||
const access = useAccessPreviewStore()
|
||
const commands = computed(() => access.can(overseasBusinessPermissions.truckStatus) ? availableTruckCommands(props.detail) : [])
|
||
const open = ref(false), saving = ref(false)
|
||
const active = ref<TruckCommand | null>(null)
|
||
const error = ref<ApiErrorDetails | null>(null), validationError = ref('')
|
||
// 响应丢失时保留原命令、版本和幂等键,重试不得变更请求内容。
|
||
const pending = ref<TruckOperation | null>(null)
|
||
const form = reactive({ decision: 'APPROVE' as 'APPROVE' | 'REJECT', actualPalletCount: 1, packageCount: undefined as number | undefined, palletIds: [] as string[], remark: '' })
|
||
const selectablePallets = computed(() => props.detail.pallets.filter(pallet => pallet.status === 'RECEIVED'))
|
||
const review = computed(() => active.value === 'review' || active.value === 'review-pallet-overage')
|
||
const { captureBaseline } = useUnsavedWorkRegistration({ key: 'truck-order-command', label: computed(() => active.value ? truckCommandLabels[active.value] : '卡车单据操作'), isOpen: open, model: form, requiredPermission: computed(() => overseasBusinessPermissions.truckStatus) })
|
||
|
||
watch(() => [props.detail.customerId, props.detail.orderNo, props.detail.version], () => {
|
||
pending.value = null; open.value = false; active.value = null; error.value = null
|
||
})
|
||
|
||
function start(command: TruckCommand) {
|
||
if (saving.value || !commands.value.includes(command)) return
|
||
if (pending.value) {
|
||
active.value = pending.value.command; open.value = true
|
||
return
|
||
}
|
||
active.value = command; error.value = null; validationError.value = ''
|
||
Object.assign(form, { decision: 'APPROVE', actualPalletCount: Math.max(1, props.detail.receivedPalletCount), packageCount: undefined, palletIds: [], remark: '' })
|
||
open.value = true; captureBaseline()
|
||
}
|
||
|
||
function buildOperation(): TruckOperation | null {
|
||
if (!active.value) return null
|
||
const base = { version: props.detail.version, idempotencyKey: crypto.randomUUID(), remark: form.remark.trim() || undefined }
|
||
if ((active.value === 'cancel' || (review.value && form.decision === 'REJECT')) && !base.remark) {
|
||
validationError.value = '请填写拒绝或取消原因'; return null
|
||
}
|
||
switch (active.value) {
|
||
case 'review': case 'review-pallet-overage': return { command: active.value, payload: { ...base, decision: form.decision } }
|
||
case 'receive-pallets':
|
||
if (!Number.isSafeInteger(form.actualPalletCount) || form.actualPalletCount < Math.max(1, props.detail.receivedPalletCount)) {
|
||
validationError.value = '累计实收托数须为正整数,且不能少于已实收托数'; return null
|
||
}
|
||
if (form.packageCount != null && (!Number.isSafeInteger(form.packageCount) || form.packageCount < 0)) {
|
||
validationError.value = '实收件数须为非负整数'; return null
|
||
}
|
||
return { command: active.value, payload: { ...base, actualPalletCount: form.actualPalletCount, ...(form.packageCount == null ? {} : { packageCount: form.packageCount }) } }
|
||
case 'allocate-pallets':
|
||
if (!form.palletIds.length || form.palletIds.some(id => !selectablePallets.value.some(pallet => pallet.id === id))) {
|
||
validationError.value = '请至少选择一个已实收且未分配的托盘'; return null
|
||
}
|
||
return { command: active.value, payload: { ...base, palletIds: [...form.palletIds] } }
|
||
case 'cancel': return { command: active.value, payload: { ...base, status: 'CANCELLED' } }
|
||
default: return { command: active.value, payload: base }
|
||
}
|
||
}
|
||
|
||
async function submit() {
|
||
if (saving.value || !active.value || !commands.value.includes(active.value)) return
|
||
validationError.value = ''; error.value = null
|
||
const operation = pending.value ?? buildOperation()
|
||
if (!operation) return
|
||
pending.value = operation; saving.value = true
|
||
const customerId = props.detail.customerId, orderNo = props.detail.orderNo
|
||
try {
|
||
const updated = await executeUsTruckCommand(orderNo, customerId, operation)
|
||
if (props.detail.customerId !== customerId || props.detail.orderNo !== orderNo) return
|
||
pending.value = null; open.value = false; captureBaseline()
|
||
ElMessage.success(`${truckCommandLabels[operation.command]}成功`)
|
||
emit('updated', updated)
|
||
} catch (cause) {
|
||
if (props.detail.customerId === customerId && props.detail.orderNo === orderNo) error.value = getApiErrorDetails(cause, '操作未确认成功,请重试或刷新订单核对最新记录')
|
||
} finally { saving.value = false }
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="commands.length" class="truck-order-actions">
|
||
<el-button v-for="command in commands" :key="command" :type="command === 'cancel' ? 'danger' : 'primary'" :plain="command === 'cancel'" :disabled="saving" @click="start(command)">{{ truckCommandLabels[command] }}</el-button>
|
||
<p v-if="pending && !open" role="status">上次操作尚未确认结果,请重新打开重试,或刷新订单核对最新记录。</p>
|
||
</div>
|
||
<AppDialog v-model="open" class="admin-density-dialog" :title="active ? truckCommandLabels[active] : '单据操作'" width="680px" :confirm-text="pending ? '重试原操作' : '确认提交'" :confirm-type="active === 'cancel' || (review && form.decision === 'REJECT') ? 'danger' : 'primary'" :confirm-loading="saving" :confirm-disabled="!active || !commands.includes(active)" @confirm="submit">
|
||
<template v-if="active">
|
||
<p>{{ detail.orderNo }} · {{ truckCommandHints[active] }}</p>
|
||
<el-alert v-if="validationError" :title="validationError" type="warning" :closable="false" role="alert" />
|
||
<div v-if="error" role="alert"><el-alert :title="error.message" type="error" :closable="false" /><ApiErrorMeta :details="error" /><p>可重试原操作;如提示版本冲突,请关闭弹框并刷新订单,核对最新状态及操作记录。</p></div>
|
||
<el-form :model="form" label-width="130px" :disabled="saving || !!pending" @submit.prevent="submit">
|
||
<el-form-item v-if="review" label="审核结果"><el-radio-group v-model="form.decision"><el-radio value="APPROVE">通过</el-radio><el-radio value="REJECT">拒绝</el-radio></el-radio-group></el-form-item>
|
||
<template v-if="active === 'receive-pallets'">
|
||
<el-form-item label="累计实收托数" required><el-input-number v-model="form.actualPalletCount" :min="Math.max(1, detail.receivedPalletCount)" :precision="0" /></el-form-item>
|
||
<el-form-item label="实收件数"><el-input-number v-model="form.packageCount" :min="0" :precision="0" placeholder="选填" /></el-form-item>
|
||
</template>
|
||
<el-form-item v-if="active === 'allocate-pallets'" label="分配托盘" required>
|
||
<el-checkbox-group v-model="form.palletIds"><el-checkbox v-for="pallet in selectablePallets" :key="pallet.id" :value="pallet.id">{{ pallet.palletNo }}({{ pallet.packageCount ?? '未登记' }} 件)</el-checkbox></el-checkbox-group>
|
||
<p v-if="!selectablePallets.length">暂无可分配托盘,请刷新核对实收记录。</p>
|
||
</el-form-item>
|
||
<el-form-item :label="review ? '审核意见' : '操作说明'" :required="active === 'cancel' || (review && form.decision === 'REJECT')"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item>
|
||
</el-form>
|
||
</template>
|
||
</AppDialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.truck-order-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||
.truck-order-actions p { flex-basis: 100%; margin: 0; color: var(--cc-muted); }
|
||
</style>
|