228 lines
26 KiB
Vue
228 lines
26 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminDensity.css'
|
||
import { inquiryReturnTarget } from '../inquiryNavigation'
|
||
import TripDispatchSelector from '../components/TripDispatchSelector.vue'
|
||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||
import FormSection from '@/shared/components/FormSection.vue'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.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 { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import TripPlanDialog from '../components/TripPlanDialog.vue'
|
||
import TripAssignmentPanel from '../components/TripAssignmentPanel.vue'
|
||
import TripLoadingPanel from '../components/TripLoadingPanel.vue'
|
||
import TripBookingPanel from '../components/TripBookingPanel.vue'
|
||
import CapacityOfferSettingsDialog from '../components/CapacityOfferSettingsDialog.vue'
|
||
import TripCostPanel from '../components/TripCostPanel.vue'
|
||
import TripHandoverPanel from '../components/TripHandoverPanel.vue'
|
||
import { getTrip, bindTripDispatch, unbindTripDispatch, cancelTrip, getTripLoading, updateTripPlan,
|
||
getTripCompletion, completeTrip, releaseTripResource, listTripHistory, reviewTripDispatch, reviewTripLoading } from '../api/trips'
|
||
import { overseasMasterDataPermissions as permission } from '../permissions'
|
||
import { driverQualificationLabels } from '../driverTypes'
|
||
import { tripStatusLabels, tripReviewLabels, type TripDetail, type TripLoading, type TripHistory, type TripCompletionInspection,
|
||
type TripCapacityPlanning, type TripComplete, type TripResourceRelease, type ReleasedVehicleStatus } from '../tripTypes'
|
||
|
||
const route = useRoute(), router = useRouter(), access = useAccessPreviewStore()
|
||
const detail = ref<TripDetail | null>(null), loading = ref(false), error = ref<ApiErrorDetails | null>(null), operationError = ref<ApiErrorDetails | null>(null)
|
||
const activeTab = ref('overview'), editorOpen = ref(false), busy = ref(''), dispatchNo = ref('')
|
||
const offerSettingsOpen = ref(false)
|
||
const inspection = ref<TripCompletionInspection | null>(null), inspectionLoading = ref(false)
|
||
const history = ref<TripHistory[]>([]), historyPage = ref(1), historyTotal = ref(0), historyLoading = ref(false)
|
||
const resourceKind = ref<'VEHICLE' | 'DRIVER'>('VEHICLE'), actualEndAt = ref(new Date().toISOString()), endLocation = ref('')
|
||
const vehicleStatus = ref<ReleasedVehicleStatus>('IN_SERVICE')
|
||
const completionRequest = ref<TripComplete | null>(null), releaseRequest = ref<TripResourceRelease | null>(null)
|
||
let generation = 0, inspectionSequence = 0, historySequence = 0
|
||
const planned = computed(() => detail.value?.trip.status === 'PLANNED')
|
||
const editable = computed(() => planned.value && access.can(permission.tripEdit))
|
||
const time = (value: string | null | undefined) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
|
||
const bindingsColumns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 320, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 210 }, { key: 'packageCount', prop: 'packageCount', label: '计划箱数', width: 120 },
|
||
{ key: 'review', label: '调度审核', slotName: 'review', minWidth: 210 },
|
||
{ key: 'boundAt', label: '关联时间', slotName: 'boundAt', minWidth: 180 }]
|
||
const outcomeColumns: DataTableColumn[] = [{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 220 },
|
||
{ key: 'destination', prop: 'destination', label: '目的地', minWidth: 180 }, { key: 'status', label: '状态', slotName: 'status', width: 115 },
|
||
{ key: 'packages', prop: 'packages', label: '原实发箱数', width: 110 },
|
||
{ key: 'signedPackages', prop: 'signedPackages', label: '实签箱数', width: 100 },
|
||
{ key: 'returnedPackages', prop: 'returnedPackages', label: '本趟实退箱数', width: 120 },
|
||
{ key: 'remainingPackages', prop: 'remainingPackages', label: '未结箱数', width: 100 },
|
||
{ key: 'terminalAt', label: '完成时间', slotName: 'terminalAt', minWidth: 180 }]
|
||
const outcomeLabels: Record<string, string> = { DELIVERED: '已签收', RETURNED: '已退回', RETURN_PENDING: '待退回', DELIVERING: '派送中', WAIT_DELIVERY: '待派送', CANCELLED: '已取消' }
|
||
const historyColumns: DataTableColumn[] = [{ key: 'occurredAt', label: '操作时间', slotName: 'time', minWidth: 175 },
|
||
{ key: 'action', label: '操作', slotName: 'action', minWidth: 145 }, { key: 'reason', prop: 'reason', label: '说明', minWidth: 230 },
|
||
{ key: 'vehicle', prop: 'snapshot.trip.vehiclePlate', label: '当时车辆', minWidth: 145 }, { key: 'driver', prop: 'snapshot.trip.driverName', label: '当时司机', minWidth: 125 }]
|
||
const actionLabels: Record<string, string> = { CREATE: '新建计划', PLAN: '调整计划', ASSIGN: '分配车辆司机', BIND_DISPATCH: '关联排车', UNBIND_DISPATCH: '解除关联',
|
||
BOOKING_CREATE: '登记外接询价', BOOKING_EDIT: '更新外接询价', BOOKING_CONFIRM: '确认外接承运', BOOKING_CANCEL: '取消外接预订', BOOKING_INCOMING: '登记外接来货', BOOKING_RECEIPT: '确认外接实收', PLAN_CAPACITY: '核对计划容量', REMARK: '更新备注', DISPATCH_REVIEW: '调度审核', LOADING_REVIEW: '理货审核',
|
||
CONFIRM_LOAD: '确认装载', DEPART: '整车发车', HANDOVER: '在途交接', COMPLETE: '货物结案', CANCEL: '取消计划', RELEASE_VEHICLE: '结束车辆任务', RELEASE_DRIVER: '结束司机任务' }
|
||
function accept(value: TripDetail) {
|
||
if (value.trip.status === 'COMPLETED' && detail.value?.trip.status !== 'COMPLETED') actualEndAt.value = new Date().toISOString()
|
||
detail.value = value; operationError.value = null; endLocation.value ||= value.trip.stops.at(-1)?.name || ''
|
||
if (value.trip.vehicleRelease && resourceKind.value === 'VEHICLE') resourceKind.value = 'DRIVER'
|
||
}
|
||
async function load() {
|
||
const current = ++generation; inspectionSequence++; historySequence++
|
||
loading.value = true; error.value = null; detail.value = null; operationError.value = null; activeTab.value = route.query.tab === 'loading' ? 'loading' : 'overview'
|
||
editorOpen.value = false; busy.value = ''; dispatchNo.value = ''; inspection.value = null; history.value = []; historyPage.value = 1
|
||
completionRequest.value = null; releaseRequest.value = null; endLocation.value = ''
|
||
try { const result = await getTrip(String(route.params.tripNo)); if (current === generation) accept(result) }
|
||
catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '车次详情加载失败') }
|
||
finally { if (current === generation) loading.value = false }
|
||
}
|
||
async function run(action: string, command: () => Promise<TripDetail>): Promise<boolean> {
|
||
if (busy.value) return false
|
||
const current = generation; busy.value = action; operationError.value = null
|
||
try { const result = await command(); if (current !== generation) return false; accept(result); return true }
|
||
catch (cause) {
|
||
if (current === generation) {
|
||
operationError.value = getApiErrorDetails(cause, '操作未完成')
|
||
if (cause instanceof ApiResponseError && /^A04/.test(cause.code)) { completionRequest.value = null; releaseRequest.value = null }
|
||
}
|
||
return false
|
||
} finally { if (current === generation) busy.value = '' }
|
||
}
|
||
async function refreshLinked() {
|
||
const number = detail.value?.trip.tripNo
|
||
if (number) await run('linked', () => getTrip(number))
|
||
}
|
||
async function bind() {
|
||
const trip = detail.value?.trip, number = dispatchNo.value.trim()
|
||
if (!trip || !number) { ElMessage.warning('请输入排车单号'); return }
|
||
if (await run('bind', () => bindTripDispatch(trip.tripNo, { recordVersion: trip.recordVersion, dispatchNo: number, reason: '关联本车发运货物' }))) dispatchNo.value = ''
|
||
}
|
||
async function unbind(number: string) {
|
||
const trip = detail.value?.trip, current = generation
|
||
if (!trip) return
|
||
try { await ElMessageBox.confirm(`解除排车单 ${number} 与当前车次的关联?已开始拣货的单据需要先处理现场作业。`, '解除关联', { customClass: 'admin-density-confirm' }) }
|
||
catch { return }
|
||
if (current === generation) await run('unbind', () => unbindTripDispatch(trip.tripNo, { recordVersion: trip.recordVersion, dispatchNo: number, reason: '解除当前车次关联' }))
|
||
}
|
||
async function matchStops() {
|
||
const trip = detail.value?.trip, current = generation
|
||
if (!trip) return
|
||
await run('stops', async () => {
|
||
const loading = await getTripLoading(trip.tripNo)
|
||
if (current !== generation) throw new Error('已离开当前车次')
|
||
const destinations = new Map(loading.cargo.filter(row => row.destinationKey).map(row => [row.destinationKey!, { name: row.destinationLabel || '卸货点', destinationKey: row.destinationKey }]))
|
||
if (!destinations.size) throw new Error('请先关联有明确目的地的排车单')
|
||
return updateTripPlan(trip.tripNo, { recordVersion: loading.recordVersion, plannedDepartureAt: trip.plannedDepartureAt,
|
||
expectedDriverEndAt: trip.expectedDriverEndAt, remark: trip.remark, stops: [trip.stops[0]!, ...destinations.values()], reason: '按关联货物匹配卸货站' })
|
||
})
|
||
}
|
||
async function cancel() {
|
||
const trip = detail.value?.trip, current = generation
|
||
if (!trip) return
|
||
try { await ElMessageBox.confirm(`取消车次 ${trip.tripNo} 并释放车辆、司机?`, '取消车次', { customClass: 'admin-density-confirm' }) }
|
||
catch { return }
|
||
if (current === generation) await run('cancel', () => cancelTrip(trip.tripNo, { recordVersion: trip.recordVersion, reason: '取消未执行的车次计划' }))
|
||
}
|
||
function capacityPlanned(value: TripCapacityPlanning) {
|
||
if (detail.value?.trip.tripNo === value.tripNo) detail.value.trip.recordVersion = value.recordVersion
|
||
}
|
||
function loaded(value: TripLoading) {
|
||
if (detail.value?.trip.tripNo !== value.tripNo) return
|
||
detail.value.trip.recordVersion = value.recordVersion
|
||
detail.value.loadingReview = { status: 'PENDING', reason: null, reviewerId: null, reviewedAt: null, recordVersion: null }
|
||
}
|
||
async function review(decision: 'APPROVED' | 'REJECTED', number?: string) {
|
||
const trip = detail.value?.trip, current = generation
|
||
if (!trip || busy.value) return
|
||
let reason: string | null = null
|
||
if (decision === 'REJECTED') {
|
||
try {
|
||
const result = await ElMessageBox.prompt('请简要说明需要调整的内容', number ? `打回调度审核 · ${number}` : '打回理货审核', {
|
||
customClass: 'admin-density-confirm', inputPlaceholder: '填写打回原因', inputValidator: value => !!value?.trim() || '请填写打回原因',
|
||
})
|
||
reason = result.value.trim()
|
||
} catch { return }
|
||
}
|
||
if (current !== generation) return
|
||
const body = { recordVersion: trip.recordVersion, decision, reason }
|
||
await run('review', () => number ? reviewTripDispatch(trip.tripNo, number, body) : reviewTripLoading(trip.tripNo, body))
|
||
}
|
||
async function inspect() {
|
||
const trip = detail.value?.trip
|
||
if (!trip || trip.status !== 'IN_TRANSIT') return
|
||
const current = ++inspectionSequence; inspectionLoading.value = true; operationError.value = null
|
||
try { const result = await getTripCompletion(trip.tripNo); if (current === inspectionSequence) inspection.value = result }
|
||
catch (cause) { if (current === inspectionSequence) operationError.value = getApiErrorDetails(cause, '结案检查失败') }
|
||
finally { if (current === inspectionSequence) inspectionLoading.value = false }
|
||
}
|
||
async function complete() {
|
||
const trip = detail.value?.trip, current = generation
|
||
if (!trip || !inspection.value?.canComplete) return
|
||
try { await ElMessageBox.confirm('确认本车货物均已签收或完成异常处置?货物结案后,还需分别结束车辆和司机任务。', '货物结案', { customClass: 'admin-density-confirm' }) }
|
||
catch { return }
|
||
if (current !== generation) return
|
||
completionRequest.value ||= { recordVersion: inspection.value.recordVersion, idempotencyKey: crypto.randomUUID(), reason: '核对全部货物终态后结案' }
|
||
if (await run('complete', () => completeTrip(trip.tripNo, completionRequest.value!))) { completionRequest.value = null; inspection.value = null }
|
||
}
|
||
async function release() {
|
||
const trip = detail.value?.trip, current = generation
|
||
if (!trip) return
|
||
if (!actualEndAt.value || !endLocation.value.trim()) { ElMessage.warning('请填写实际任务结束时间和地点'); return }
|
||
try { await ElMessageBox.confirm(`确认${resourceKind.value === 'VEHICLE' ? '车辆' : '司机'}已结束本次任务,可以安排下一车次?`, '结束资源任务', { customClass: 'admin-density-confirm' }) }
|
||
catch { return }
|
||
if (current !== generation) return
|
||
releaseRequest.value ||= { recordVersion: trip.recordVersion, resourceKind: resourceKind.value, actualEndAt: new Date(actualEndAt.value).toISOString(),
|
||
endLocation: endLocation.value.trim(), vehicleStatus: resourceKind.value === 'VEHICLE' ? vehicleStatus.value : null, idempotencyKey: crypto.randomUUID(), reason: '确认资源实际任务结束' }
|
||
if (await run('release', () => releaseTripResource(trip.tripNo, releaseRequest.value!))) { releaseRequest.value = null; resourceKind.value = detail.value?.trip.vehicleRelease ? 'DRIVER' : 'VEHICLE' }
|
||
}
|
||
async function loadHistory() {
|
||
if (!detail.value) return
|
||
const current = ++historySequence; historyLoading.value = true; operationError.value = null
|
||
try { const result = await listTripHistory(detail.value.trip.tripNo, historyPage.value, 20); if (current === historySequence) { history.value = result.records; historyTotal.value = result.total } }
|
||
catch (cause) { if (current === historySequence) operationError.value = getApiErrorDetails(cause, '历史读取失败') }
|
||
finally { if (current === historySequence) historyLoading.value = false }
|
||
}
|
||
watch(() => route.params.tripNo, load, { immediate: true })
|
||
watch(() => route.query.tab, tab => { activeTab.value = tab === 'loading' ? 'loading' : 'overview' })
|
||
watch(activeTab, tab => { if (tab === 'completion') void inspect(); if (tab === 'history') void loadHistory() })
|
||
watch(historyPage, () => { if (activeTab.value === 'history') void loadHistory() })
|
||
onBeforeUnmount(() => { generation++; inspectionSequence++; historySequence++ })
|
||
</script>
|
||
<template>
|
||
<AdminPage class="admin-density"><DetailPageLayout class="density-detail-layout surface-panel" aria-label="车次详情">
|
||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="router.push(inquiryReturnTarget(route.query) || { name: 'overseas-master-data-trips' })">{{ inquiryReturnTarget(route.query) ? '返回原拼车登记' : '返回车次列表' }}</el-button><h1 class="density-detail-title">{{ detail?.trip.tripNo || '车次详情' }}</h1><span v-if="detail">{{ tripStatusLabels[detail.trip.status] }}</span></div><div class="density-actions"><el-button :disabled="!!busy" @click="load">刷新车次</el-button><el-button v-if="access.can('platform:fleet:capacity-offer:manage')" @click="offerSettingsOpen = true">发布拼车余位</el-button><el-button v-if="editable" :disabled="!!busy" @click="editorOpen = true">编辑计划</el-button><el-button v-if="planned && access.can(permission.tripCancel)" type="danger" plain :disabled="!!busy || !!detail?.dispatches.length" @click="cancel">取消车次</el-button></div></div></template>
|
||
<LoadErrorState v-if="error" title="车次详情加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||
<LoadErrorState v-if="operationError" title="操作未完成" :message="operationError.message" @retry="load"><template #details><ApiErrorMeta :details="operationError" /></template></LoadErrorState>
|
||
<div v-loading="loading"><template v-if="detail">
|
||
<FormSection class="density-form-section" title="车次概况"><dl class="density-fields"><div v-for="[label, value] in [['起运仓',detail.trip.warehouseName],['车辆',detail.trip.vehiclePlate],['司机',detail.trip.driverName],['联系电话',detail.trip.driverPhone],['计划发车',time(detail.trip.plannedDepartureAt)],['司机预计结束',time(detail.trip.expectedDriverEndAt)],['实际发车',time(detail.trip.departedAt)],['货物结案',time(detail.trip.completedAt)]]" :key="label || ''"><dt>{{ label }}</dt><dd>{{ value || '—' }}</dd></div></dl>
|
||
<p>{{ detail.trip.stops.map(stop => stop.name).join(' → ') }}</p><p v-if="detail.driverQualification">{{ driverQualificationLabels[detail.driverQualification.reason] }}<span v-if="detail.driverQualification.expiringSoon"> · 证照将在 30 天内到期,请留意任务延期</span></p>
|
||
</FormSection>
|
||
<el-tabs v-model="activeTab" class="density-element-tabs"><el-tab-pane name="overview"><template #label><span>🚚 分配与货物</span></template></el-tab-pane><el-tab-pane name="bookings"><template #label><span>📦 外接预订</span></template></el-tab-pane><el-tab-pane name="capacity"><template #label><span>📋 计划容量</span></template></el-tab-pane><el-tab-pane name="loading"><template #label><span>📦 实际装载</span></template></el-tab-pane><el-tab-pane name="completion"><template #label><span>✅ 结案与任务结束</span></template></el-tab-pane><el-tab-pane name="handovers"><template #label><span>在途交接</span></template></el-tab-pane><el-tab-pane name="costs"><template #label><span>直接成本</span></template></el-tab-pane><el-tab-pane name="history"><template #label><span>🕘 操作历史</span></template></el-tab-pane></el-tabs>
|
||
<template v-if="activeTab === 'overview'">
|
||
<FormSection v-if="planned && access.can(permission.tripAssign)" class="density-form-section" title="车辆与司机"><TripAssignmentPanel :key="detail.trip.tripNo" :detail="detail" @saved="accept" /></FormSection>
|
||
<FormSection class="density-form-section" title="本车排车单"><div v-if="editable" class="trip-inline"><TripDispatchSelector v-model="dispatchNo" :trip-no="detail.trip.tripNo" :disabled="!!busy" /><el-button type="primary" :loading="busy === 'bind'" :disabled="!!busy || !detail.trip.vehicleId" @click="bind">关联排车单</el-button><el-button :loading="busy === 'stops'" :disabled="!!busy || !detail.dispatches.length" @click="matchStops">按货物匹配卸货站</el-button></div>
|
||
<p>先分配车辆和司机,选择待运排车单后自动匹配卸货站。每单调度审核通过后拣货,确认实际装载并通过理货审核后整车发车。</p>
|
||
<DataTable :data="detail.dispatches" :columns="bindingsColumns" row-key="dispatchNo" empty-text="尚未关联排车单"><template #actions="{ row }"><div class="density-actions"><el-button v-if="access.can('platform:overseas-warehouse:dispatch:list')" class="density-action" @click="router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } })">详情</el-button><el-button v-if="editable" class="density-action" data-tone="danger" :disabled="!!busy" @click="unbind(row.dispatchNo)">解除关联</el-button><el-button v-if="planned && access.can(permission.tripDispatchReview)" class="density-action" :disabled="!!busy || row.review.status === 'APPROVED'" @click="review('APPROVED', row.dispatchNo)">审核通过</el-button><el-button v-if="planned && access.can(permission.tripDispatchReview)" class="density-action" data-tone="danger" :disabled="!!busy" @click="review('REJECTED', row.dispatchNo)">打回</el-button></div></template><template #review="{ row }"><span>{{ tripReviewLabels[row.review.status as keyof typeof tripReviewLabels] }}</span><p v-if="row.review.reason">{{ row.review.reason }}</p><span v-if="row.review.reviewedAt">{{ time(row.review.reviewedAt) }}</span></template><template #boundAt="{ row }">{{ time(row.boundAt) }}</template></DataTable>
|
||
</FormSection>
|
||
</template>
|
||
<FormSection v-if="activeTab === 'bookings'" class="density-form-section" title="本车外接预订"><TripBookingPanel :key="detail.trip.tripNo" :trip="detail.trip" @changed="detail.trip.recordVersion = $event" @linked="refreshLinked" /></FormSection>
|
||
<FormSection v-if="activeTab === 'handovers'" class="density-form-section" title="在途车辆与司机交接"><TripHandoverPanel :key="detail.trip.tripNo" :detail="detail" @saved="refreshLinked" /></FormSection>
|
||
<FormSection v-if="activeTab === 'costs'" class="density-form-section" title="本趟直接成本"><TripCostPanel :key="detail.trip.tripNo" :trip-no="detail.trip.tripNo" /></FormSection>
|
||
<FormSection v-if="activeTab === 'capacity'" class="density-form-section" title="核对本车计划载荷"><TripLoadingPanel :key="`${detail.trip.tripNo}-capacity`" :trip="detail.trip" :editable="editable" planning @planned="capacityPlanned" /></FormSection>
|
||
<FormSection v-if="activeTab === 'loading'" class="density-form-section" title="按实物托盘确认装载"><TripLoadingPanel :key="detail.trip.tripNo" :trip="detail.trip" :editable="editable" @saved="loaded" /><div class="trip-review"><p>理货审核:{{ tripReviewLabels[detail.loadingReview.status] }}<span v-if="detail.loadingReview.reason"> · {{ detail.loadingReview.reason }}</span><span v-if="detail.loadingReview.reviewedAt"> · {{ time(detail.loadingReview.reviewedAt) }}</span></p><div v-if="planned && access.can(permission.tripLoadingReview)" class="density-actions"><el-button type="primary" :disabled="!!busy || detail.loadingReview.status === 'APPROVED'" @click="review('APPROVED')">理货审核通过</el-button><el-button type="danger" plain :disabled="!!busy" @click="review('REJECTED')">理货打回</el-button></div></div></FormSection>
|
||
<template v-if="activeTab === 'completion'">
|
||
<FormSection v-if="detail.trip.status === 'IN_TRANSIT'" v-loading="inspectionLoading" class="density-form-section" title="货物结案检查"><div class="trip-inline"><el-button @click="inspect">重新检查</el-button><el-button v-if="access.can(permission.tripComplete)" type="primary" :disabled="!inspection?.canComplete || !!busy" :loading="busy === 'complete'" @click="complete">确认货物结案</el-button></div><p>{{ inspection?.blockReason || (inspection?.canComplete ? '全部货物已具备结案依据' : '正在读取派送结果') }}</p>
|
||
<DataTable :data="inspection?.outcomes || []" :columns="outcomeColumns" row-key="deliveryNo"><template #status="{ row }">{{ row.status === 'DELIVERED' && row.returnedPackages > 0 ? '余货已签收' : outcomeLabels[row.status] || '待处理' }}</template><template #terminalAt="{ row }">{{ time(row.terminalAt) }}</template></DataTable>
|
||
</FormSection>
|
||
<FormSection class="density-form-section" title="车辆和司机分别结束任务"><p>货物结案与资源释放分别记录;仍有返程或转场任务时,等实际任务结束再释放。</p><dl class="density-fields"><div><dt>车辆任务</dt><dd>{{ detail.trip.vehicleRelease ? `${time(detail.trip.vehicleRelease.actualEndAt)} · ${detail.trip.vehicleRelease.endLocation}` : '尚未结束' }}</dd></div><div><dt>司机任务</dt><dd>{{ detail.trip.driverRelease ? `${time(detail.trip.driverRelease.actualEndAt)} · ${detail.trip.driverRelease.endLocation}` : '尚未结束' }}</dd></div></dl>
|
||
<el-form v-if="detail.trip.status === 'COMPLETED' && access.can(permission.tripRelease) && (!detail.trip.vehicleRelease || !detail.trip.driverRelease)" label-position="top" class="density-form release-form" :disabled="!!busy || !!releaseRequest" @submit.prevent="release">
|
||
<el-form-item label="结束任务的资源"><el-select v-model="resourceKind"><el-option label="车辆" value="VEHICLE" :disabled="!!detail.trip.vehicleRelease" /><el-option label="司机" value="DRIVER" :disabled="!!detail.trip.driverRelease" /></el-select></el-form-item>
|
||
<el-form-item label="实际任务结束时间"><el-date-picker v-model="actualEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" /></el-form-item><el-form-item label="实际结束地点"><el-input v-model="endLocation" placeholder="请输入实际任务结束地点" /></el-form-item>
|
||
<el-form-item v-if="resourceKind === 'VEHICLE'" label="车辆结束状态"><el-select v-model="vehicleStatus"><el-option label="在役可调度" value="IN_SERVICE" /><el-option label="维修" value="MAINTENANCE" /><el-option label="停用" value="INACTIVE" /><el-option label="已归还" value="RETURNED" /></el-select></el-form-item>
|
||
</el-form><el-button v-if="detail.trip.status === 'COMPLETED' && access.can(permission.tripRelease) && (!detail.trip.vehicleRelease || !detail.trip.driverRelease)" type="primary" :loading="busy === 'release'" :disabled="!!busy" @click="release">{{ releaseRequest ? '重试原结束请求' : '确认结束资源任务' }}</el-button>
|
||
</FormSection>
|
||
</template>
|
||
<FormSection v-if="activeTab === 'history'" class="density-form-section" title="操作历史"><DataTable v-model:page="historyPage" :page-size="20" :data="history" :total="historyTotal" :loading="historyLoading" :columns="historyColumns" row-key="recordVersion"><template #time="{ row }">{{ time(row.occurredAt) }}</template><template #action="{ row }">{{ actionLabels[row.action] || '更新记录' }}</template></DataTable></FormSection>
|
||
<TripPlanDialog v-model="editorOpen" :trip="detail.trip" @saved="accept" />
|
||
<CapacityOfferSettingsDialog v-model="offerSettingsOpen" :trip="detail.trip" @trip-updated="detail.trip = $event" @saved="detail.trip.recordVersion = $event" />
|
||
</template></div>
|
||
</DetailPageLayout></AdminPage>
|
||
</template>
|
||
<style scoped>.trip-inline{display:flex;gap:var(--admin-space-4);align-items:center}.trip-inline :deep(.el-input){max-width:340px}.release-form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--admin-space-4) var(--admin-space-6);margin-top:var(--admin-space-6)}.release-form :deep(.el-select),.release-form :deep(.el-date-editor){width:100%}</style>
|