93 lines
9.4 KiB
Vue
93 lines
9.4 KiB
Vue
<script setup lang="ts">
|
||
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { useAuthStore } from '@/core/auth/auth'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import OwnedBusinessOperationsDialog from './OwnedBusinessOperationsDialog.vue'
|
||
import { newOwnedBusinessPending, persistOwnedBusinessPending, restoreOwnedBusinessPending, recoverOwnedBusinessCommand, type OwnedBusinessPending } from './OwnedBusinessCommandRecovery'
|
||
import { ownedBusinessDecimal, ownedBusinessVersion, type OwnedBusinessDetail, type OwnedBusinessMoney, type OwnedBusinessSave } from '../ownedBusinessTypes'
|
||
import { overseasMasterDataPermissions as permission } from '../permissions'
|
||
import { getOwnedBusiness } from '../api/ownedBusiness'
|
||
|
||
const open = defineModel<boolean>({ default: false })
|
||
const props = defineProps<{ detail: OwnedBusinessDetail | null }>()
|
||
const emit = defineEmits<{ saved: [value: OwnedBusinessDetail]; busy: [value: boolean] }>()
|
||
const auth = useAuthStore(), access = useAccessPreviewStore(), busy = ref(false), pending = ref<OwnedBusinessPending | null>(null), error = ref<ApiErrorDetails | null>(null), corrupted = ref(false)
|
||
const form = reactive({ currency: '', amount: '', actualDistanceMiles: '', fuelConsumptionLitresPer100Km: '', reason: '', arrivals: {} as Record<string,string> })
|
||
const active = computed(() => open.value || Boolean(pending.value))
|
||
const registration = useUnsavedWorkRegistration({ key: `owned-business-operations:${getCurrentInstance()?.uid}`, label: computed(() => '自有业务运营登记'), isOpen: active, model: reactive({ form, pending }), requiredPermission: computed(() => permission.ownedBusinessEdit) })
|
||
let actor = '', number = '', baseline = ''
|
||
const writable = computed(() => !corrupted.value && access.can(permission.ownedBusinessEdit) && props.detail?.row.actions.edit.allowed === true)
|
||
const locked = computed(() => busy.value || Boolean(pending.value))
|
||
function persist() { persistOwnedBusinessPending(actor, number, 'operations', pending.value) }
|
||
function ensureScope() { if (auth.user?.id !== actor || props.detail?.row.dispatchNo !== number) throw new Error('操作身份或排车单已变化,请重新打开') }
|
||
watch(busy, value => emit('busy', value))
|
||
watch(open, async value => {
|
||
if (!value || !props.detail) return
|
||
actor = auth.user?.id || ''; number = props.detail.row.dispatchNo; error.value = null; corrupted.value = false
|
||
try {
|
||
pending.value = restoreOwnedBusinessPending(actor, number, 'operations')
|
||
const operations = pending.value?.action === 'SAVE_OPERATIONS' ? (pending.value.body as OwnedBusinessSave).operations : props.detail.row.operations
|
||
Object.assign(form, { currency: operations.money?.currency || '', amount: operations.money?.amount ?? '', actualDistanceMiles: operations.actualDistanceMiles ?? '', fuelConsumptionLitresPer100Km: operations.fuelConsumptionLitresPer100Km ?? '', reason: pending.value ? (pending.value.body as OwnedBusinessSave).reason || '' : '', arrivals: Object.fromEntries(props.detail.row.destinations.map(destination => [destination.destinationKey, operations.arrivals.find(arrival => arrival.destinationKey === destination.destinationKey)?.actualArrivedAt || ''])) })
|
||
baseline = JSON.stringify(form); await nextTick(); if (!pending.value) registration.captureBaseline()
|
||
} catch (cause) { corrupted.value = true; error.value = getApiErrorDetails(cause, '运营资料恢复失败') }
|
||
}, { immediate: true })
|
||
function payload(): OwnedBusinessSave {
|
||
const amount = ownedBusinessDecimal(form.amount, 2, 18)
|
||
let money: OwnedBusinessMoney | null = null
|
||
if (amount != null) { if (!['CNY','USD'].includes(form.currency)) throw new Error('请选择金额币种'); money = { currency: form.currency as OwnedBusinessMoney['currency'], amount } }
|
||
else if (form.currency) throw new Error('金额和币种请成对填写,清空时同时清空两项')
|
||
const arrivals = props.detail!.row.destinations.map(destination => {
|
||
const value = form.arrivals[destination.destinationKey] || null
|
||
if (value && (!/(?:Z|[+-]\d{2}:\d{2})$/.test(value) || !Number.isFinite(Date.parse(value)))) throw new Error('实际到达时间必须包含有效时区')
|
||
return { destinationKey: destination.destinationKey, actualArrivedAt: value }
|
||
})
|
||
return { recordVersion: ownedBusinessVersion(props.detail!.row.recordVersion), idempotencyKey: crypto.randomUUID(), operations: { money, actualDistanceMiles: ownedBusinessDecimal(form.actualDistanceMiles, 3, 12), fuelConsumptionLitresPer100Km: ownedBusinessDecimal(form.fuelConsumptionLitresPer100Km, 3, 12), arrivals }, reason: form.reason.trim() || null }
|
||
}
|
||
async function save() {
|
||
if (busy.value || !writable.value || !props.detail) return
|
||
busy.value = true; error.value = null
|
||
try {
|
||
ensureScope()
|
||
if (!pending.value) { if (JSON.stringify(form) === baseline) { open.value = false; return }; pending.value = newOwnedBusinessPending(actor, number, 'SAVE_OPERATIONS', payload()); persist() }
|
||
const result = await recoverOwnedBusinessCommand(pending.value, persist, ensureScope)
|
||
if (!result.detail) throw new Error('运营登记结果未确认')
|
||
pending.value = null; persist(); registration.captureBaseline(); emit('saved', result.detail); open.value = false; ElMessage.success('运营信息已保存')
|
||
} catch (cause) { error.value = getApiErrorDetails(cause, cause instanceof Error ? cause.message : '运营登记结果尚未确认') }
|
||
finally { busy.value = false }
|
||
}
|
||
async function beforeClose(done: () => void) {
|
||
if (busy.value) return
|
||
if (pending.value || JSON.stringify(form) !== baseline) {
|
||
try { await ElMessageBox.confirm(pending.value ? '原请求将保留,重新打开后可核对原结果。是否关闭?' : '放弃尚未保存的运营信息?', '关闭运营登记', { customClass: 'admin-density-confirm', confirmButtonText: pending.value ? '保留请求并关闭' : '放弃修改', cancelButtonText: '继续编辑' }) } catch { return }
|
||
}
|
||
done()
|
||
}
|
||
async function discardRejected() {
|
||
if (!pending.value?.rejected || busy.value) return
|
||
busy.value = true
|
||
try { const current = await getOwnedBusiness(number); ensureScope(); pending.value = null; persist(); emit('saved', current); error.value = null }
|
||
catch (cause) { error.value = getApiErrorDetails(cause, '当前运营版本核对失败') }
|
||
finally { busy.value = false }
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<OwnedBusinessOperationsDialog v-model="open" :dispatch-no="detail?.row.dispatchNo" :busy="busy" :pending="Boolean(pending)" :save-disabled="!writable" :before-close="beforeClose" @save="save">
|
||
<template #state><LoadErrorState v-if="error" title="运营登记未完成" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState><p v-if="pending">原请求已保留,字段锁定;核对结果前不会建立新请求。</p><el-button v-if="pending?.rejected" :disabled="busy" @click="discardRejected">按明确拒绝结果重新编辑</el-button><p v-if="!writable">{{ detail?.row.actions.edit.blockReason || '没有运营登记权限' }}</p></template>
|
||
<template #amount><el-form label-position="top" class="owned-business-form"><el-form-item label="币种"><el-select v-model="form.currency" clearable :disabled="locked"><el-option label="CNY" value="CNY" /><el-option label="USD" value="USD" /></el-select></el-form-item><el-form-item label="整车总金额"><el-input v-model="form.amount" :disabled="locked" placeholder="未登记;允许明确填写0" /></el-form-item><el-form-item label="更正说明"><el-input v-model="form.reason" :disabled="locked" maxlength="1000" /></el-form-item></el-form></template>
|
||
<template #arrivals><div v-for="destination in detail?.row.destinations || []" :key="destination.destinationKey" class="owned-business-arrival"><span>{{ destination.warehouseCode || destination.warehouseName || destination.address || '目的地资料缺失' }}<small v-for="appointment in destination.appointments" :key="appointment.appointmentNo">{{ appointment.appointmentNo }}</small></span><el-date-picker v-model="form.arrivals[destination.destinationKey]" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" :disabled="locked" placeholder="实际到达时间,未登记留空" /></div><p>时间按浏览器本地时区输入,提交时保留时区。</p></template>
|
||
<template #measurements><el-form label-position="top" class="owned-business-form"><el-form-item label="实际里程(mile)"><el-input v-model="form.actualDistanceMiles" :disabled="locked" placeholder="未登记" /></el-form-item><el-form-item label="油耗(L/100km)"><el-input v-model="form.fuelConsumptionLitresPer100Km" :disabled="locked" placeholder="手填耗油率,未登记留空" /></el-form-item></el-form></template>
|
||
</OwnedBusinessOperationsDialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.owned-business-form { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 0 var(--admin-space-4); }
|
||
.owned-business-arrival { display: grid; grid-template-columns: minmax(0,1fr) 280px; align-items: center; gap: var(--admin-space-4); margin-bottom: var(--admin-space-3); }
|
||
.owned-business-arrival small { display: block; }
|
||
</style>
|