This commit is contained in:
2026-10-29 14:31:47 +08:00
parent 8970ad52fa
commit 07a160e843
@@ -25,11 +25,11 @@ import CapacityOfferSettingsDialog from '../components/CapacityOfferSettingsDial
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'
getTripCompletion, completeTrip, 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'
type TripCapacityPlanning, type TripComplete } 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)
@@ -40,9 +40,7 @@ const appointmentsOpen = ref(false)
const appointmentsPanel = ref<InstanceType<typeof TripAppointmentPanel> | null>(null)
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)
const completionRequest = ref<TripComplete | null>(null)
let generation = 0, inspectionSequence = 0, historySequence = 0
const isPickup = computed(() => detail.value?.trip.tripPurpose === 'PICKUP')
const planned = computed(() => detail.value?.trip.status === 'PLANNED')
@@ -67,9 +65,7 @@ const actionLabels: Record<string, string> = { PICKUP_PLAN: '安排上门取货'
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'
detail.value = value; operationError.value = null
}
async function load() {
if (pickupLocked.value) { ElMessage.warning('请先重试并确认原取货操作'); return }
@@ -78,7 +74,7 @@ async function load() {
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
appointmentsOpen.value = false
completionRequest.value = null; releaseRequest.value = null; endLocation.value = ''
completionRequest.value = null
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 }
@@ -90,7 +86,7 @@ async function run(action: string, command: () => Promise<TripDetail>): Promise<
catch (cause) {
if (current === generation) {
operationError.value = getApiErrorDetails(cause, '操作未完成')
if (cause instanceof ApiResponseError && /^A04/.test(cause.code)) { completionRequest.value = null; releaseRequest.value = null }
if (cause instanceof ApiResponseError && /^A04/.test(cause.code)) { completionRequest.value = null }
}
return false
} finally { if (current === generation) busy.value = '' }
@@ -165,23 +161,13 @@ async function inspect() {
async function complete() {
const trip = detail.value?.trip, current = generation
if (!trip || !inspection.value?.canComplete) return
try { await ElMessageBox.confirm('确认本车货物均已签收或完成异常处置?货物结案后,还需分别结束车辆和司机任务。', '货物结案', { customClass: 'admin-density-confirm admin-workbench is-dialog' }) }
try { await ElMessageBox.confirm('确认本车货物均已签收或完成异常处置?结案后将自动释放车辆和司机。', '货物结案', { customClass: 'admin-density-confirm admin-workbench is-dialog' }) }
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 admin-workbench is-dialog' }) }
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
@@ -225,12 +211,8 @@ onBeforeUnmount(() => { generation++; inspectionSequence++; historySequence++ })
<FormSection v-if="!isPickup && 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>
<div class="workbench-table"><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></div>
</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 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>
</FormSection>
</template>
<FormSection v-if="activeTab === 'history'" class="density-form-section" title="操作历史"><div class="workbench-table"><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></div></FormSection>