86 lines
6.5 KiB
Vue
86 lines
6.5 KiB
Vue
<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>
|