86 lines
7.9 KiB
Vue
86 lines
7.9 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminWorkbench.css'
|
||
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 { ownedBusinessDecimal, ownedBusinessVersion, type OwnedBusinessDetail, type OwnedBusinessMoney, type OwnedBusinessSave } from '../ownedBusinessTypes'
|
||
import { overseasMasterDataPermissions as permission } from '../permissions'
|
||
import { saveOwnedBusinessOperations } 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), error = ref<ApiErrorDetails | null>(null)
|
||
const form = reactive({ currency: '', amount: '', actualDistanceMiles: '', fuelConsumptionLitresPer100Km: '', reason: '', arrivals: {} as Record<string,string> })
|
||
const active = computed(() => open.value)
|
||
const registration = useUnsavedWorkRegistration({ key: `owned-business-operations:${getCurrentInstance()?.uid}`, label: computed(() => '自有业务运营登记'), isOpen: active, model: reactive({ form }), requiredPermission: computed(() => permission.ownedBusinessEdit) })
|
||
let actor = '', number = '', baseline = ''
|
||
const writable = computed(() => access.can(permission.ownedBusinessEdit) && props.detail?.row.actions.edit.allowed === true)
|
||
const locked = computed(() => busy.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
|
||
const operations = props.detail.row.operations
|
||
Object.assign(form, { currency: operations.money?.currency || '', amount: operations.money?.amount ?? '', actualDistanceMiles: operations.actualDistanceMiles ?? '', fuelConsumptionLitresPer100Km: operations.fuelConsumptionLitresPer100Km ?? '', 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(); registration.captureBaseline()
|
||
}, { 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 (JSON.stringify(form) === baseline) { open.value = false; return }
|
||
const request = payload()
|
||
const result = await saveOwnedBusinessOperations(number, request); ensureScope()
|
||
registration.captureBaseline(); emit('saved', result); open.value = false; ElMessage.success('运营信息已保存')
|
||
} catch (cause) { error.value = getApiErrorDetails(cause, '运营登记失败,请重新操作') }
|
||
finally { busy.value = false }
|
||
}
|
||
async function beforeClose(done: () => void) {
|
||
if (busy.value) return
|
||
if (JSON.stringify(form) !== baseline) {
|
||
try { await ElMessageBox.confirm('放弃尚未保存的运营信息?', '关闭运营登记', { customClass: 'admin-density-confirm admin-workbench is-dialog', confirmButtonText: '放弃修改', cancelButtonText: '继续编辑' }) } catch { return }
|
||
}
|
||
done()
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<OwnedBusinessOperationsDialog v-model="open" :dispatch-no="detail?.row.dispatchNo" :busy="busy" :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="!writable">{{ detail?.row.actions.edit.blockReason || '没有运营登记权限' }}</p></template>
|
||
<template #amount><el-form label-position="top" class="density-form 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="density-form 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) minmax(240px,320px); align-items: center; gap: var(--admin-space-4); margin-bottom: var(--admin-space-3); }
|
||
.owned-business-arrival small { display: block; }
|
||
|
||
.owned-business-form :deep(.el-form-item) { min-width: 0; }
|
||
.owned-business-form :deep(.el-select), .owned-business-arrival :deep(.el-date-editor) { width: 100%; min-width: 0; }
|
||
.owned-business-arrival > span { min-width: 0; overflow-wrap: anywhere; }
|
||
</style>
|