146 lines
11 KiB
Vue
146 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import '@/styles/adminDensity.css'
|
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { Plus } from '@lucide/vue'
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
|
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
import DriverEditorDialog from '../components/DriverEditorDialog.vue'
|
|
import DriverLicensePreview from '../components/DriverLicensePreview.vue'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
import { changeDriverStatus, getDriver, listDrivers, type DriverQuery } from '../api/drivers'
|
|
import { overseasMasterDataPermissions as permission } from '../permissions'
|
|
import { driverLicenseLabels, driverReviewLabels, type DriverDetail, type DriverLicenseState, type DriverSummary } from '../driverTypes'
|
|
import { masterDataErrorMessage } from '../apiError'
|
|
|
|
const router = useRouter(), access = useAccessPreviewStore()
|
|
const rows = ref<DriverDetail[]>([]), total = ref(0), page = ref(1), pageSize = ref(20), loading = ref(false)
|
|
const error = ref<ApiErrorDetails | null>(null), summary = ref<DriverSummary | null>(null)
|
|
const filters = ref<QueryModel>({ keyword: '', region: '', ownership: '', active: '', reviewStatus: '', licenseState: '' })
|
|
let applied: Omit<DriverQuery, 'page' | 'size'> = {}, request = 0
|
|
const editorOpen = ref(false), editing = ref<DriverDetail | null>(null), actionId = ref('')
|
|
const previewOpen = ref(false), preview = ref<DriverDetail | null>(null)
|
|
const fields: QueryField[] = [
|
|
{ key: 'keyword', type: 'input', label: '司机关键词', placeholder: '姓名 / 手机 / 司机编号', width: 220 },
|
|
{ key: 'region', type: 'select', label: '区域', width: 120, options: [{ label: '国内', value: 'DOMESTIC' }, { label: '海外', value: 'OVERSEAS' }] },
|
|
{ key: 'ownership', type: 'select', label: '归属', width: 140, options: [{ label: '自有司机', value: 'OWNED' }, { label: '外派司机', value: 'DISPATCHED' }] },
|
|
{ key: 'active', type: 'select', label: '状态', width: 120, options: [{ label: '启用', value: 'true' }, { label: '停用', value: 'false' }] },
|
|
{ key: 'reviewStatus', type: 'select', label: '审核状态', width: 140, options: Object.entries(driverReviewLabels).map(([value, label]) => ({ value, label })) },
|
|
{ key: 'licenseState', type: 'select', label: '驾照有效期', width: 150, options: Object.entries(driverLicenseLabels).map(([value, label]) => ({ value, label })) },
|
|
]
|
|
const columns: DataTableColumn[] = [
|
|
{ key: 'actions', label: '操作', width: 205, fixed: 'left', slotName: 'actions' },
|
|
{ key: 'driverNo', prop: 'driverNo', label: '司机编号', minWidth: 205 },
|
|
{ key: 'name', prop: 'details.name', label: '姓名', minWidth: 115 },
|
|
{ key: 'ownership', label: '归属', width: 100, slotName: 'ownership' },
|
|
{ key: 'phone', prop: 'details.phone', label: '联系电话', minWidth: 140 },
|
|
{ key: 'vehicleModels', label: '可驾驶车型', minWidth: 150, slotName: 'vehicleModels' },
|
|
{ key: 'warehouseName', prop: 'warehouseName', label: '所属仓库', minWidth: 160 },
|
|
{ key: 'license', label: '驾照类型 / 编号', minWidth: 180, slotName: 'license' },
|
|
{ key: 'expires', label: '驾照到期', minWidth: 140, slotName: 'expires' },
|
|
{ key: 'medical', label: '体检到期', width: 125, slotName: 'medical' },
|
|
{ key: 'otherDocs', label: '培训 / 保险到期', minWidth: 185, slotName: 'otherDocs' },
|
|
{ key: 'review', label: '审核状态', width: 100, slotName: 'review' },
|
|
{ key: 'file', label: '驾驶证附件', width: 110, slotName: 'file' },
|
|
{ key: 'active', label: '当前状态', width: 100, slotName: 'active' },
|
|
]
|
|
async function load() {
|
|
const current = ++request
|
|
loading.value = true; error.value = null
|
|
try {
|
|
const result = await listDrivers({ ...applied, page: page.value, size: pageSize.value })
|
|
if (current === request) { rows.value = result.records; total.value = result.total; summary.value = result.summary }
|
|
} catch (cause) {
|
|
if (current === request) { rows.value = []; total.value = 0; summary.value = null; error.value = getApiErrorDetails(cause, '司机列表加载失败') }
|
|
} finally { if (current === request) loading.value = false }
|
|
}
|
|
function search() {
|
|
applied = { keyword: String(filters.value.keyword || '').trim() || undefined,
|
|
region: (filters.value.region || undefined) as DriverQuery['region'], ownership: (filters.value.ownership || undefined) as DriverQuery['ownership'],
|
|
active: filters.value.active === 'true' ? true : filters.value.active === 'false' ? false : undefined,
|
|
reviewStatus: (filters.value.reviewStatus || undefined) as DriverQuery['reviewStatus'], licenseState: (filters.value.licenseState || undefined) as DriverQuery['licenseState'] }
|
|
if (page.value !== 1) page.value = 1
|
|
else void load()
|
|
}
|
|
function filterExpiry(state: DriverLicenseState) { filters.value.licenseState = state; search() }
|
|
function actions(row: DriverDetail): RowActionItem[] {
|
|
return [{ key: 'detail', label: '详情', priority: 3 },
|
|
{ key: 'edit', label: '编辑', visible: access.can(permission.driverEdit), disabled: !!actionId.value, priority: 2 },
|
|
{ key: 'status', label: row.active ? '停用' : '启用', visible: access.can(permission.driverStatus), disabled: !!actionId.value, tone: row.active ? 'danger' : 'success' }]
|
|
}
|
|
async function action(key: string, row: DriverDetail) {
|
|
if (key === 'detail') { await router.push({ name: 'overseas-master-data-driver-detail', params: { driverId: row.id } }); return }
|
|
if (actionId.value) return
|
|
actionId.value = row.id
|
|
try {
|
|
if (key === 'edit') { editing.value = await getDriver(row.id); editorOpen.value = true }
|
|
else {
|
|
const result = await ElMessageBox.prompt(`请填写${row.active ? '停用' : '启用'}司机 ${row.details.name} 的原因`, row.active ? '停用司机' : '启用司机', {
|
|
customClass: 'admin-density-confirm', inputValidator: value => !!value?.trim() || '请填写原因', confirmButtonText: '确认', cancelButtonText: '取消', inputType: 'textarea', closeOnClickModal: false,
|
|
})
|
|
await changeDriverStatus(row.id, row.recordVersion, !row.active, result.value.trim())
|
|
ElMessage.success(row.active ? '司机已停用' : '司机已启用'); await load()
|
|
}
|
|
} catch (cause) { if (cause !== 'cancel' && cause !== 'close') ElMessage.error(masterDataErrorMessage(cause, '司机操作失败')) }
|
|
finally { actionId.value = '' }
|
|
}
|
|
function create() { editing.value = null; editorOpen.value = true }
|
|
function showLicense(row: DriverDetail) { preview.value = row; previewOpen.value = true }
|
|
function saved(row: DriverDetail) {
|
|
const index = rows.value.findIndex(item => item.id === row.id)
|
|
if (index >= 0) rows.value[index] = row
|
|
}
|
|
watch(editorOpen, (value, previous) => { if (!value && previous) void load() })
|
|
watch([page, pageSize], () => void load())
|
|
onMounted(load)
|
|
onBeforeUnmount(() => { request++ })
|
|
</script>
|
|
|
|
<template>
|
|
<AdminPage class="admin-density">
|
|
<ListPageLayout>
|
|
<div class="driver-summary" aria-label="当前筛选范围司机统计">
|
|
<section class="surface-panel"><span>司机总数</span><strong>{{ summary?.total ?? '—' }}</strong><small>自有 {{ summary?.owned ?? '—' }} · 外派 {{ summary?.dispatched ?? '—' }}</small></section>
|
|
<button class="surface-panel" type="button" @click="filterExpiry('EXPIRING')"><span>即将到期</span><strong>{{ summary?.expiringSoon ?? '—' }}</strong><small>30 天内到期 · 点击筛选</small></button>
|
|
<button class="surface-panel" type="button" @click="filterExpiry('EXPIRED')"><span>驾照已过期</span><strong>{{ summary?.expired ?? '—' }}</strong><small>不可派车 · 点击筛选</small></button>
|
|
</div>
|
|
<section class="surface-panel"><QueryToolbar v-model="filters" :fields="fields" ariaLabel="司机查询" preference-key="overseas-drivers-query" :loading="loading" @search="search" /></section>
|
|
<LoadErrorState v-if="error" title="司机列表加载失败" :message="error.message" @retry="load" ><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
|
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="id" empty-text="暂无符合条件的司机">
|
|
<template #toolbar-actions><el-button v-if="access.can(permission.driverCreate)" type="primary" @click="create"><Plus :size="16" />新增司机</el-button></template>
|
|
<template #actions="{ row }"><RowActions :actions="actions(row as DriverDetail)" text-actions @action="action($event, row as DriverDetail)" /></template>
|
|
<template #ownership="{ row }">{{ row.details.ownership === 'OWNED' ? '自有司机' : '外派司机' }}</template>
|
|
<template #vehicleModels="{ row }">{{ row.details.profile?.vehicleModels || '—' }}</template>
|
|
<template #license="{ row }">{{ row.details.licenseType || '—' }} / {{ row.details.licenseNumber || '—' }}</template>
|
|
<template #expires="{ row }"><span>{{ row.details.licenseExpiresOn || '—' }}</span><div :class="{ 'expiry-warning': row.licenseState === 'EXPIRING', 'expiry-danger': row.licenseState === 'EXPIRED' }">{{ driverLicenseLabels[row.licenseState as DriverLicenseState] }}</div></template>
|
|
<template #medical="{ row }">{{ row.details.profile?.medicalExpiresOn || '—' }}</template>
|
|
<template #otherDocs="{ row }">{{ row.details.profile?.trainingExpiresOn || '—' }} / {{ row.details.profile?.insuranceExpiresOn || '—' }}</template>
|
|
<template #review="{ row }"><StatusBadge :status="driverReviewLabels[row.reviewStatus as keyof typeof driverReviewLabels]" /></template>
|
|
<template #file="{ row }"><el-button v-if="row.licenseFileId" link type="primary" @click="showLicense(row as DriverDetail)">查看附件</el-button><span v-else>待上传</span></template>
|
|
<template #active="{ row }"><StatusBadge :status="row.active ? '启用' : '停用'" /></template>
|
|
<template #pagination-left>共 {{ total }} 位司机 · 统计随筛选变化</template>
|
|
</DataTable>
|
|
</ListPageLayout>
|
|
<DriverEditorDialog v-model="editorOpen" :driver="editing" @saved="saved" />
|
|
<DriverLicensePreview v-model="previewOpen" :driver-id="preview?.id || ''" :file-id="preview?.licenseFileId || ''" :driver-name="preview?.details.name || ''" />
|
|
</AdminPage>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.driver-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--admin-space-6); }
|
|
.driver-summary > * { display: grid; gap: var(--admin-space-3); padding: var(--admin-space-6); text-align: left; color: inherit; font: inherit; }
|
|
.driver-summary button { cursor: pointer; }
|
|
.driver-summary strong { font-size: 24px; }
|
|
.driver-summary small { color: var(--cc-muted); }
|
|
.expiry-warning { color: var(--el-color-warning); }
|
|
.expiry-danger { color: var(--el-color-danger); }
|
|
</style>
|