feat(fleet): integrate trip management and fee coverage workflows
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, 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 { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasMasterDataPermissions as permission } from '../permissions'
|
||||
import { createDriver, listDriverWarehouses, updateDriver, uploadDriverLicense } from '../api/drivers'
|
||||
import type { DriverDetail, DriverDraft, DriverProfile, DriverWarehouseOption } from '../driverTypes'
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const props = defineProps<{ driver: DriverDetail | null }>()
|
||||
const emit = defineEmits<{ saved: [driver: DriverDetail] }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const saving = ref(false), error = ref<ApiErrorDetails | null>(null), warehouseError = ref<ApiErrorDetails | null>(null)
|
||||
const warehouseLoading = ref(false), warehouseOptions = ref<DriverWarehouseOption[]>([])
|
||||
const current = ref<DriverDetail | null>(null), selectedFile = ref<File | null>(null), fileInput = ref<HTMLInputElement | null>(null)
|
||||
const emptyProfile = (): DriverProfile => ({ warehouseId: null, vehicleModels: null, languagesAndArea: null, medicalExpiresOn: null, trainingExpiresOn: null, insuranceExpiresOn: null })
|
||||
const form = reactive<DriverDraft>({ name: '', phone: '', ownership: 'OWNED', region: 'OVERSEAS', licenseNumber: null, licenseType: null, licenseIssuedOn: null, licenseExpiresOn: null, licenseZone: '', remark: null, profile: emptyProfile() })
|
||||
const profile = computed(() => form.profile!)
|
||||
const zones = ['America/Los_Angeles', 'America/New_York', 'America/Chicago', 'America/Denver', 'Asia/Shanghai', 'Europe/London', 'UTC']
|
||||
let warehouseRequest = 0, editSession = 0, lastSaved = ''
|
||||
async function loadWarehouses() {
|
||||
const request = ++warehouseRequest
|
||||
warehouseLoading.value = true; warehouseError.value = null
|
||||
try { const rows = await listDriverWarehouses(form.region); if (request === warehouseRequest) warehouseOptions.value = rows }
|
||||
catch (cause) { if (request === warehouseRequest) { warehouseOptions.value = []; warehouseError.value = getApiErrorDetails(cause, '仓库候选加载失败') } }
|
||||
finally { if (request === warehouseRequest) warehouseLoading.value = false }
|
||||
}
|
||||
function changeRegion() { profile.value.warehouseId = null; void loadWarehouses() }
|
||||
watch([open, () => props.driver?.id], ([value]) => {
|
||||
editSession++; saving.value = false
|
||||
if (!value) { warehouseRequest++; return }
|
||||
current.value = props.driver; selectedFile.value = null; error.value = null; lastSaved = ''
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
Object.assign(form, props.driver ? { ...props.driver.details, profile: { ...emptyProfile(), ...props.driver.details.profile } }
|
||||
: { name: '', phone: '', ownership: 'OWNED', region: 'OVERSEAS', licenseNumber: null, licenseType: null, licenseIssuedOn: null, licenseExpiresOn: null, licenseZone: '', remark: null, profile: emptyProfile() })
|
||||
void loadWarehouses()
|
||||
})
|
||||
function selectFile(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0] || null
|
||||
if (file && (!['application/pdf', 'image/jpeg', 'image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024)) {
|
||||
ElMessage.warning('请选择非空 PDF、JPG 或 PNG,最大 10MiB'); input.value = ''; selectedFile.value = null; return
|
||||
}
|
||||
selectedFile.value = file
|
||||
}
|
||||
async function save() {
|
||||
if (saving.value) return
|
||||
if (!form.name.trim() || !form.phone.trim() || !form.licenseZone.trim()) { ElMessage.warning('请填写姓名、电话和证照时区'); return }
|
||||
saving.value = true; error.value = null
|
||||
const session = editSession, sourceDriverId = props.driver?.id
|
||||
const isCurrent = () => session === editSession && open.value && props.driver?.id === sourceDriverId
|
||||
let persisted = false
|
||||
try {
|
||||
const details: DriverDraft = { ...form, name: form.name.trim(), phone: form.phone.trim(), profile: { ...profile.value } }
|
||||
const fingerprint = JSON.stringify(details)
|
||||
if (!current.value || fingerprint !== lastSaved) {
|
||||
const savedDriver = current.value ? await updateDriver(current.value.id, current.value.recordVersion, details) : await createDriver(details)
|
||||
if (!isCurrent()) return
|
||||
current.value = savedDriver
|
||||
lastSaved = fingerprint
|
||||
}
|
||||
persisted = true
|
||||
if (selectedFile.value && current.value) {
|
||||
const uploadedDriver = await uploadDriverLicense(current.value.id, current.value.recordVersion, selectedFile.value)
|
||||
if (!isCurrent()) return
|
||||
current.value = uploadedDriver
|
||||
selectedFile.value = null
|
||||
}
|
||||
if (current.value) emit('saved', current.value)
|
||||
ElMessage.success('司机档案已保存'); open.value = false
|
||||
} catch (cause) {
|
||||
if (!isCurrent()) return
|
||||
if (persisted && current.value) emit('saved', current.value)
|
||||
const details = getApiErrorDetails(cause, '司机保存失败')
|
||||
error.value = { ...details, message: persisted ? `档案已保存,驾驶证上传失败:${details.message}。可直接重试。` : details.message }
|
||||
} finally { if (isCurrent()) saving.value = false }
|
||||
}
|
||||
onBeforeUnmount(() => { warehouseRequest++; editSession++ })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog v-model="open" class="admin-density-dialog" :title="current ? '编辑司机档案' : '新增司机档案'" 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>
|
||||
<el-form label-position="top" class="density-form driver-form" :disabled="saving" @submit.prevent="save">
|
||||
<h3 class="full-width">基础信息</h3>
|
||||
<el-form-item label="司机姓名" required><el-input v-model="form.name" maxlength="128" placeholder="请输入司机姓名" /></el-form-item>
|
||||
<el-form-item label="联系电话" required><el-input v-model="form.phone" maxlength="64" placeholder="请输入联系电话" /></el-form-item>
|
||||
<el-form-item label="司机归属"><el-select v-model="form.ownership"><el-option label="自有司机" value="OWNED" /><el-option label="外派司机" value="DISPATCHED" /></el-select></el-form-item>
|
||||
<el-form-item label="区域"><el-select v-model="form.region" @change="changeRegion"><el-option label="国内" value="DOMESTIC" /><el-option label="海外" value="OVERSEAS" /></el-select></el-form-item>
|
||||
<el-form-item label="所属仓库"><el-select v-model="profile.warehouseId" clearable filterable :loading="warehouseLoading" placeholder="选择所属仓库"><el-option v-for="warehouse in warehouseOptions" :key="warehouse.id" :label="`${warehouse.name}(${warehouse.code})`" :value="warehouse.id" /></el-select></el-form-item>
|
||||
<el-form-item label="可驾驶车型"><el-input v-model="profile.vehicleModels" maxlength="128" placeholder="如 53 尺拖车" /></el-form-item>
|
||||
<LoadErrorState v-if="warehouseError" class="full-width" title="仓库候选加载失败" :message="warehouseError.message" @retry="loadWarehouses" ><template #details><ApiErrorMeta :details="warehouseError" /></template></LoadErrorState>
|
||||
<el-form-item label="语言 / 服务区域" class="full-width"><el-input v-model="profile.languagesAndArea" maxlength="256" placeholder="如 中文、英语 / 洛杉矶区域" /></el-form-item>
|
||||
<h3 class="full-width">驾驶资质</h3>
|
||||
<el-form-item label="驾照类型"><el-select v-model="form.licenseType" filterable allow-create default-first-option clearable><el-option v-for="kind in ['Class A', 'Class B', 'Class C', '普通驾照']" :key="kind" :label="kind" :value="kind" /></el-select></el-form-item>
|
||||
<el-form-item label="驾照编号"><el-input v-model="form.licenseNumber" maxlength="128" placeholder="请输入驾驶证编号" /></el-form-item>
|
||||
<el-form-item label="证照有效时区" required><el-select v-model="form.licenseZone" filterable allow-create default-first-option placeholder="请选择或输入 IANA 时区"><el-option v-for="zone in zones" :key="zone" :label="zone" :value="zone" /></el-select></el-form-item>
|
||||
<el-form-item label="驾照签发日"><el-date-picker v-model="form.licenseIssuedOn" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="驾照到期日"><el-date-picker v-model="form.licenseExpiresOn" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="体检到期日"><el-date-picker v-model="profile.medicalExpiresOn" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="培训 / 安全到期日"><el-date-picker v-model="profile.trainingExpiresOn" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="保险到期日"><el-date-picker v-model="profile.insuranceExpiresOn" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<p class="full-width form-hint">驾驶证在 30 天内到期但仍有效时允许派车并提醒;预计任务结束前到期则禁止。其他证照日期用于档案记录。</p>
|
||||
<h3 class="full-width">证照附件</h3>
|
||||
<div class="full-width">
|
||||
<label v-if="access.can(permission.driverUpload)">驾驶证(PDF / JPG / PNG,最大 10MiB)<input ref="fileInput" type="file" accept="application/pdf,image/jpeg,image/png" :disabled="saving" @change="selectFile" /></label>
|
||||
<p class="form-hint">{{ selectedFile?.name || (current?.licenseFileId ? '已有驾驶证附件;上传新文件后需要重新审核。' : '可先保存档案,补齐驾驶证后才能通过审核。') }}</p>
|
||||
</div>
|
||||
<el-form-item label="备注" class="full-width"><el-input v-model="form.remark" type="textarea" :rows="2" maxlength="1000" placeholder="补充司机档案说明(选填)" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.driver-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; }
|
||||
.driver-form h3 { margin: 0; }
|
||||
.driver-form :deep(.el-select), .driver-form :deep(.el-date-editor) { width: 100%; }
|
||||
.form-hint { margin: var(--admin-space-3) 0; color: var(--cc-muted); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user