Files
admin-web/src/modules/overseas-master-data/components/TripOperationsRecordDialog.vue
T

55 lines
6.8 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { onBeforeUnmount, reactive, ref, watch } from 'vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { isFleetCommandRejected } from '../api/fleetQuotes'
import { getTripOperationsRecord, saveTripOperationsRecord, listTripOperationsRecordHistory, type TripOperationsRow, type TripOperationsRecord, type TripOperationsRecordHistory, type TripOperationsRecordSave, type TripOperationsMeasured } from '../api/tripOperations'
const open = defineModel<boolean>({ default: false })
const props = defineProps<{ row: TripOperationsRow | null; readonly?: boolean }>(), emit = defineEmits<{ saved: [] }>()
const form = reactive<TripOperationsMeasured>({ actualDistanceMiles: null, actualFuelLitres: null, remark: null }), reason = ref('')
const record = ref<TripOperationsRecord | null>(null), loading = ref(false), saving = ref(false), error = ref<ApiErrorDetails | null>(null), tab = ref('measured')
const pending = new Map<string, TripOperationsRecordSave>(), uncertain = ref(false)
const history = ref<TripOperationsRecordHistory[]>([]), total = ref(0), page = ref(1), size = ref(20)
let sequence = 0
const columns: DataTableColumn[] = [{ key: 'recordVersion', label: '版本', width: 90 }, { key: 'occurredAt', label: '更正时间', minWidth: 190 }, { key: 'distance', label: '里程(mile)', width: 130, slotName: 'distance' }, { key: 'fuel', label: '油耗(L)', width: 120, slotName: 'fuel' }, { key: 'reason', label: '更正说明', minWidth: 180 }, { key: 'remark', label: '备注', minWidth: 200, slotName: 'remark' }]
async function load() {
if (!open.value || !props.row) return
const no = props.row.tripNo, current = ++sequence; loading.value = true; error.value = null; record.value = null
try { const result = await getTripOperationsRecord(no); if (current !== sequence) return; record.value = result; const retry = pending.get(no); uncertain.value = !!retry; Object.assign(form, retry?.details || result.details); reason.value = retry?.reason || '' }
catch (e) { if (current === sequence) error.value = getApiErrorDetails(e, '实测记录读取失败') } finally { if (current === sequence) loading.value = false }
}
async function loadHistory() {
if (!open.value || !props.row || tab.value !== 'history') return
const current = ++sequence; loading.value = true; error.value = null
try { const result = await listTripOperationsRecordHistory(props.row.tripNo, page.value, size.value); if (current === sequence) { history.value = result.records; total.value = result.total } } catch (e) { if (current === sequence) error.value = getApiErrorDetails(e, '更正历史读取失败') } finally { if (current === sequence) loading.value = false }
}
async function save() {
if (!record.value || !props.row || props.readonly || saving.value) return
const no = props.row.tripNo; saving.value = true; error.value = null
const request = pending.get(no) || { recordVersion: record.value.recordVersion, idempotencyKey: crypto.randomUUID(), details: { actualDistanceMiles: form.actualDistanceMiles ?? null, actualFuelLitres: form.actualFuelLitres ?? null, remark: form.remark || null }, reason: reason.value || null }
pending.set(no, request)
try { await saveTripOperationsRecord(no, request); pending.delete(no); uncertain.value = false; open.value = false; emit('saved') }
catch (e) { error.value = getApiErrorDetails(e, '实测记录保存失败'); if (isFleetCommandRejected(e)) pending.delete(no); uncertain.value = pending.has(no) }
finally { saving.value = false }
}
watch([open, () => props.row?.tripNo], () => { sequence++; tab.value = 'measured'; history.value = []; page.value = 1; total.value = 0; if (open.value) void load() })
watch([tab, page, size], () => { if (tab.value === 'history') void loadHistory() }); onBeforeUnmount(() => { sequence++ })
</script>
<template><AppDialog v-model="open" title="出车实测记录" width="820px" class="admin-density-dialog" :close-on-click-modal="false">
<el-alert v-if="error" :title="error.message" type="error" :closable="false"><ApiErrorMeta :details="error" /><el-button @click="tab === 'history' ? loadHistory() : load()">重新读取</el-button></el-alert>
<p v-if="row">{{ row.tripNo }} · {{ row.vehiclePlate || '—' }} · 实际发车 {{ row.departedAt }} · 货物结案 {{ row.completedAt || '—' }}</p>
<el-tabs v-model="tab" class="density-element-tabs"><el-tab-pane label="实测记录" name="measured">
<p v-if="uncertain">上次保存结果未确认,将按原请求重试。</p>
<el-form label-position="top" class="density-form record-form" :disabled="readonly || !record || loading || saving || uncertain">
<el-form-item label="本趟实际里程(mile)"><el-input-number :model-value="form.actualDistanceMiles" @update:model-value="form.actualDistanceMiles = $event ?? null" :min="0" :precision="1" placeholder="选填,本趟实际英里数" /></el-form-item>
<el-form-item label="本趟实际油耗(L)"><el-input-number :model-value="form.actualFuelLitres" @update:model-value="form.actualFuelLitres = $event ?? null" :min="0" :precision="1" placeholder="选填,本趟实际用油升数" /></el-form-item>
<el-form-item label="备注" class="full"><el-input :model-value="form.remark || ''" @update:model-value="form.remark = $event || null" type="textarea" maxlength="1000" placeholder="选填,补充实测情况" /></el-form-item>
<el-form-item v-if="!readonly" label="更正说明(选填)" class="full"><el-input v-model="reason" maxlength="1000" placeholder="选填,留空使用标准更正说明" /></el-form-item>
</el-form><p>未填写与明确零值分别保存。实测记录不改车辆累计里程、货物履约或财务费用。</p>
</el-tab-pane><el-tab-pane label="更正历史" name="history"><DataTable v-model:page="page" v-model:page-size="size" :data="history" :columns="columns" :total="total" :loading="loading" row-key="recordVersion" empty-text="暂无实测更正历史"><template #distance="{ row: item }">{{ item.details.actualDistanceMiles ?? '—' }}</template><template #fuel="{ row: item }">{{ item.details.actualFuelLitres ?? '—' }}</template><template #remark="{ row: item }">{{ item.details.remark || '—' }}</template></DataTable></el-tab-pane></el-tabs>
<template #footer><el-button :disabled="saving" @click="open = false">关闭</el-button><el-button v-if="!readonly && tab === 'measured'" type="primary" :loading="saving" :disabled="!record || loading" @click="save">{{ uncertain ? '按原请求重试' : '保存记录' }}</el-button></template>
</AppDialog></template>
<style scoped>.record-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 var(--admin-space-4); }.full { grid-column:1/-1; }</style>