Files
admin-web/src/modules/overseas-master-data/components/TripPlanDialog.vue
T
2026-10-20 23:34:11 +08:00

86 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import '@/styles/adminWorkbench.css'
import { onBeforeUnmount, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { listDriverWarehouses } from '../api/drivers'
import { createTrip, updateTripPlan } from '../api/trips'
import type { DriverWarehouseOption } from '../driverTypes'
import type { FleetTrip, TripDetail, TripDraft, TripStop } from '../tripTypes'
const open = defineModel<boolean>({ default: false })
const props = defineProps<{ trip: FleetTrip | null }>()
const emit = defineEmits<{ saved: [detail: TripDetail] }>()
const form = reactive({ warehouseId: '', plannedDepartureAt: '', expectedDriverEndAt: '', remark: '', reason: '' })
const stops = ref<TripStop[]>([]), warehouses = ref<DriverWarehouseOption[]>([])
const saving = ref(false), error = ref<ApiErrorDetails | null>(null), warehouseError = ref<ApiErrorDetails | null>(null)
const pending = ref<TripDraft | null>(null)
let generation = 0
async function loadWarehouses() {
const current = generation
warehouseError.value = null
try { const result = await listDriverWarehouses('OVERSEAS'); if (current === generation) warehouses.value = result }
catch (cause) { if (current === generation) warehouseError.value = getApiErrorDetails(cause, '起运仓加载失败') }
}
watch([open, () => props.trip?.tripNo], ([visible]) => {
generation++; saving.value = false
if (!visible) return
if (pending.value && !props.trip) return
const trip = props.trip
pending.value = null; error.value = null
Object.assign(form, { warehouseId: trip?.warehouseId || '', plannedDepartureAt: trip?.plannedDepartureAt || new Date(Date.now() + 3600000).toISOString(),
expectedDriverEndAt: trip?.expectedDriverEndAt || new Date(Date.now() + 86400000).toISOString(), remark: trip?.remark || '', reason: '' })
stops.value = trip ? trip.stops.map(stop => ({ ...stop })) : [{ name: '起运仓' }, { name: '待确定卸货点' }]
if (!trip) void loadWarehouses()
})
async function save() {
if (saving.value) return
if (!form.warehouseId || !form.plannedDepartureAt || !form.expectedDriverEndAt || stops.value.some(stop => !stop.name.trim())) {
ElMessage.warning('请填写起运仓、计划时间和站点名称'); return
}
const current = generation, trip = props.trip
saving.value = true; error.value = null
try {
const plan = { plannedDepartureAt: new Date(form.plannedDepartureAt).toISOString(), expectedDriverEndAt: new Date(form.expectedDriverEndAt).toISOString(),
stops: stops.value.map(stop => ({ ...stop, name: stop.name.trim() })), remark: form.remark.trim() || null }
if (!trip) pending.value ||= { ...plan, warehouseId: form.warehouseId, idempotencyKey: crypto.randomUUID() }
const result = trip ? await updateTripPlan(trip.tripNo, { ...plan, recordVersion: trip.recordVersion, reason: form.reason.trim() || '更新车次计划' })
: await createTrip(pending.value!)
if (current !== generation || !open.value) return
pending.value = null; emit('saved', result); open.value = false; ElMessage.success('车次计划已保存')
} catch (cause) {
if (current !== generation) return
if (cause instanceof ApiResponseError && /^A04/.test(cause.code)) pending.value = null
error.value = getApiErrorDetails(cause, '计划保存失败')
} finally { if (current === generation) saving.value = false }
}
onBeforeUnmount(() => { generation++ })
</script>
<template>
<AppDialog v-model="open" class="admin-density-dialog admin-workbench is-dialog" :title="trip ? '编辑车次计划' : '新建车次'" width="1080px" confirm-text="保存计划" :confirm-loading="saving" @confirm="save">
<LoadErrorState v-if="error" title="计划保存失败" :message="error.message" @retry="save"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<LoadErrorState v-if="warehouseError && !trip" title="起运仓加载失败" :message="warehouseError.message" @retry="loadWarehouses" />
<p v-if="pending">上次保存结果尚未确定,再次保存会核对原请求。</p>
<el-form label-position="top" class="density-form trip-plan-form" :disabled="saving || !!pending" @submit.prevent="save">
<el-form-item label="起运仓" required><el-input v-if="trip" :model-value="trip.warehouseName" readonly /><el-select v-else v-model="form.warehouseId" filterable placeholder="选择海外起运仓"><el-option v-for="warehouse in warehouses" :key="warehouse.id" :value="warehouse.id" :label="`${warehouse.name}(${warehouse.code})`" /></el-select></el-form-item>
<el-form-item label="计划发车时间" required><el-date-picker v-model="form.plannedDepartureAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" /></el-form-item>
<el-form-item label="司机预计任务结束" required><el-date-picker v-model="form.expectedDriverEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="包括返程或转场" /></el-form-item>
<div class="full-width"><h3>路线站点</h3>
<div v-for="(stop, index) in stops" :key="index" class="trip-stop-row"><span>{{ index === 0 ? '起点' : `第 ${index} 站` }}</span><el-input v-model="stop.name" placeholder="站点名称" /><el-input v-model="stop.address" placeholder="地址(选填)" /><el-button :disabled="index === 0 || stops.length <= 2" @click="stops.splice(index, 1)">移除</el-button></div>
<el-button @click="stops.push({ name: '' })">添加站点</el-button>
</div>
<el-form-item label="备注" class="full-width"><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="补充本次运输安排(选填)" /></el-form-item>
<el-form-item v-if="trip" label="调整说明(选填)" class="full-width"><el-input v-model="form.reason" placeholder="如改期或调整路线(选填)" /></el-form-item>
</el-form>
</AppDialog>
</template>
<style scoped>
.trip-plan-form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--admin-space-4) var(--admin-space-6)}
.full-width{grid-column:1/-1}.trip-stop-row{display:grid;grid-template-columns:70px 1fr 2fr auto;gap:var(--admin-space-4);align-items:center;margin-bottom:var(--admin-space-4)}
.trip-plan-form :deep(.el-select),.trip-plan-form :deep(.el-date-editor){width:100%}
</style>