feat(web-admin): 三方车辆/人员粘贴识别弹窗与入口
共享识别预览弹窗,支持冲突三选一,并在三方车辆与三方驾押人员工具栏接入。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
c60ebfc082
commit
6258821273
3 files changed
+460
-2
No files matched your search
@@ -0,0 +1,430 @@
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
:title="dialogTitle"
|
||||
width="820px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="dialog-form-layout pl-paste-recognize-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form label-position="top" class="form-grid-2">
|
||||
<el-form-item label="客户原文" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="rawText"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="粘贴客户提供的车辆与驾押信息后点击识别"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-col-span-2">
|
||||
<el-button type="primary" @click="handleParse">
|
||||
<el-icon><MagicStick /></el-icon>识别
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="parsed">
|
||||
<el-form-item v-if="showVehicleFields" label="承运商" class="form-col-span-2" required>
|
||||
<el-select
|
||||
v-model="carrierId"
|
||||
placeholder="请选择承运商"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in carrierOptions"
|
||||
:key="c.id"
|
||||
:label="c.carrierName"
|
||||
:value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="mode === 'personnel-first' && hasPlate"
|
||||
label="建车选项"
|
||||
class="form-col-span-2"
|
||||
>
|
||||
<el-checkbox v-model="createVehicle">一并创建三方车辆并关联</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="showVehicleFields" label="主车号">
|
||||
<el-input v-model="vehicleForm.licensePlate" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showVehicleFields" label="挂车号">
|
||||
<el-input v-model="vehicleForm.trailerNo" maxlength="20" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showVehicleFields" label="核载(吨)">
|
||||
<el-input-number
|
||||
v-model="vehicleForm.loadCapacity"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="驾驶员">
|
||||
<el-input v-model="driverForm.personnelName" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="驾驶员手机">
|
||||
<el-input v-model="driverForm.phoneNo" maxlength="32" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="驾驶员身份证" class="form-col-span-2">
|
||||
<el-input v-model="driverForm.idCardNo" maxlength="32" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="押运员">
|
||||
<el-input v-model="escortForm.personnelName" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="押运员电话">
|
||||
<el-input v-model="escortForm.phoneNo" maxlength="32" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="押运员身份证" class="form-col-span-2">
|
||||
<el-input v-model="escortForm.idCardNo" maxlength="32" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<div v-if="conflictRows.length" class="form-col-span-2 paste-conflict-block">
|
||||
<h4 class="paste-conflict-title">冲突处理</h4>
|
||||
<div v-for="row in conflictRows" :key="row.key" class="paste-conflict-row">
|
||||
<div class="paste-conflict-label">
|
||||
{{ row.label }}已存在:{{ row.existingLabel }}
|
||||
</div>
|
||||
<el-radio-group v-model="row.action">
|
||||
<el-radio value="reuse">复用</el-radio>
|
||||
<el-radio value="overwrite">覆盖更新</el-radio>
|
||||
<el-radio value="cancel">取消</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="parsed.warnings.length"
|
||||
class="form-col-span-2"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
:title="`有 ${parsed.warnings.length} 行未识别`"
|
||||
:description="parsed.warnings.join(';')"
|
||||
/>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" :disabled="!parsed" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确认生成
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, MagicStick } from '@element-plus/icons-vue'
|
||||
import {
|
||||
carrierApi,
|
||||
pasteRecognizeApi,
|
||||
personnelApi,
|
||||
vehicleApi,
|
||||
type Carrier,
|
||||
type PasteConflictAction,
|
||||
type PasteConflictItem
|
||||
} from '@/api/pl'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import {
|
||||
parsePlCustomerVehicleText,
|
||||
type PlPasteRecognizeResult
|
||||
} from '@/utils/pl-paste-recognize'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
mode: 'vehicle-bundle' | 'personnel-first'
|
||||
carrierOptions?: Carrier[]
|
||||
}>(),
|
||||
{
|
||||
carrierOptions: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [boolean]
|
||||
done: []
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (v: boolean) => emit('update:visible', v)
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
props.mode === 'vehicle-bundle' ? '粘贴识别(整车套装)' : '粘贴识别(以人为主)'
|
||||
)
|
||||
|
||||
const rawText = ref('')
|
||||
const parsed = ref<PlPasteRecognizeResult | null>(null)
|
||||
const carrierId = ref('')
|
||||
const createVehicle = ref(true)
|
||||
const submitLoading = ref(false)
|
||||
const localCarriers = ref<Carrier[]>([])
|
||||
|
||||
const vehicleForm = reactive({
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
loadCapacity: undefined as number | undefined
|
||||
})
|
||||
const driverForm = reactive({
|
||||
personnelName: '',
|
||||
phoneNo: '',
|
||||
idCardNo: ''
|
||||
})
|
||||
const escortForm = reactive({
|
||||
personnelName: '',
|
||||
phoneNo: '',
|
||||
idCardNo: ''
|
||||
})
|
||||
|
||||
type ConflictRow = {
|
||||
key: 'vehicle' | 'driver' | 'escort'
|
||||
label: string
|
||||
existingId: string
|
||||
existingLabel: string
|
||||
action: PasteConflictAction
|
||||
}
|
||||
const conflictRows = ref<ConflictRow[]>([])
|
||||
|
||||
const carrierOptions = computed(() =>
|
||||
props.carrierOptions.length ? props.carrierOptions : localCarriers.value
|
||||
)
|
||||
|
||||
const hasPlate = computed(() => Boolean(vehicleForm.licensePlate.trim()))
|
||||
const showVehicleFields = computed(() => {
|
||||
if (props.mode === 'vehicle-bundle') return true
|
||||
return createVehicle.value && hasPlate.value
|
||||
})
|
||||
|
||||
async function ensureCarriers() {
|
||||
if (props.carrierOptions.length) return
|
||||
if (localCarriers.value.length) return
|
||||
const res = await carrierApi.list({ page: 1, pageSize: 100 })
|
||||
const page = pickBxPageResult<Carrier>(res)
|
||||
localCarriers.value = page.list || []
|
||||
}
|
||||
|
||||
async function handleParse() {
|
||||
const result = parsePlCustomerVehicleText(rawText.value)
|
||||
parsed.value = result
|
||||
vehicleForm.licensePlate = result.vehicle?.licensePlate || ''
|
||||
vehicleForm.trailerNo = result.vehicle?.trailerNo || ''
|
||||
vehicleForm.loadCapacity = result.vehicle?.loadCapacity
|
||||
driverForm.personnelName = result.driver?.personnelName || ''
|
||||
driverForm.phoneNo = result.driver?.phoneNo || ''
|
||||
driverForm.idCardNo = result.driver?.idCardNo || ''
|
||||
escortForm.personnelName = result.escort?.personnelName || ''
|
||||
escortForm.phoneNo = result.escort?.phoneNo || ''
|
||||
escortForm.idCardNo = result.escort?.idCardNo || ''
|
||||
conflictRows.value = []
|
||||
createVehicle.value = Boolean(result.vehicle?.licensePlate)
|
||||
if (!result.vehicle?.licensePlate && !result.driver && !result.escort) {
|
||||
ElMessage.warning('未能识别有效内容,请检查原文')
|
||||
return
|
||||
}
|
||||
await detectConflicts()
|
||||
}
|
||||
|
||||
async function detectConflicts() {
|
||||
const rows: ConflictRow[] = []
|
||||
const needVehicle = showVehicleFields.value
|
||||
if (needVehicle && vehicleForm.licensePlate.trim()) {
|
||||
const res = await vehicleApi.list({
|
||||
q: vehicleForm.licensePlate.trim(),
|
||||
vehicleType: 'carrier',
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
const page = pickBxPageResult<{ id: string; licensePlate: string }>(res)
|
||||
const hit = (page.list || []).find(
|
||||
v => (v.licensePlate || '').trim() === vehicleForm.licensePlate.trim()
|
||||
)
|
||||
if (hit) {
|
||||
rows.push({
|
||||
key: 'vehicle',
|
||||
label: '车牌',
|
||||
existingId: hit.id,
|
||||
existingLabel: hit.licensePlate,
|
||||
action: 'reuse'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function findPersonnelByIdCard(idCardNo: string, key: 'driver' | 'escort', label: string) {
|
||||
const card = idCardNo.trim()
|
||||
if (!card) return
|
||||
const res = await personnelApi.list({
|
||||
q: card,
|
||||
personnelAffiliation: 'carrier',
|
||||
page: 1,
|
||||
pageSize: 50
|
||||
})
|
||||
const page = pickBxPageResult<{ id: string; personnelName: string; idCardNo?: string }>(res)
|
||||
const hit = (page.list || []).find(
|
||||
p => (p.idCardNo || '').trim().toUpperCase() === card.toUpperCase()
|
||||
)
|
||||
if (hit) {
|
||||
rows.push({
|
||||
key,
|
||||
label,
|
||||
existingId: hit.id,
|
||||
existingLabel: `${hit.personnelName}(${hit.idCardNo})`,
|
||||
action: 'reuse'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await findPersonnelByIdCard(driverForm.idCardNo, 'driver', '驾驶员')
|
||||
await findPersonnelByIdCard(escortForm.idCardNo, 'escort', '押运员')
|
||||
conflictRows.value = rows
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!parsed.value) return
|
||||
if (props.mode === 'vehicle-bundle' && !vehicleForm.licensePlate.trim()) {
|
||||
ElMessage.warning('请填写主车车牌')
|
||||
return
|
||||
}
|
||||
if (showVehicleFields.value && !carrierId.value) {
|
||||
ElMessage.warning('请选择承运商')
|
||||
return
|
||||
}
|
||||
if (
|
||||
props.mode === 'personnel-first' &&
|
||||
!driverForm.personnelName.trim() &&
|
||||
!escortForm.personnelName.trim()
|
||||
) {
|
||||
ElMessage.warning('请至少填写一名人员')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await ensureCarriers()
|
||||
if (!conflictRows.value.length) {
|
||||
await detectConflicts()
|
||||
}
|
||||
if (conflictRows.value.some(r => r.action === 'cancel')) {
|
||||
ElMessage.info('已取消本次生成')
|
||||
return
|
||||
}
|
||||
|
||||
const conflicts: PasteConflictItem[] = conflictRows.value.map(r => ({
|
||||
key: r.key,
|
||||
existingId: r.existingId,
|
||||
action: r.action
|
||||
}))
|
||||
|
||||
const carrier = carrierOptions.value.find(c => c.id === carrierId.value)
|
||||
const needVehicle = showVehicleFields.value
|
||||
|
||||
await pasteRecognizeApi.commit({
|
||||
mode: props.mode === 'vehicle-bundle' ? 'vehicle_bundle' : 'personnel_first',
|
||||
createVehicle: needVehicle,
|
||||
vehicle: needVehicle
|
||||
? {
|
||||
licensePlate: vehicleForm.licensePlate.trim(),
|
||||
trailerNo: vehicleForm.trailerNo || undefined,
|
||||
loadCapacity: vehicleForm.loadCapacity ?? undefined,
|
||||
carrierId: carrierId.value,
|
||||
carrierName: carrier?.carrierName
|
||||
}
|
||||
: null,
|
||||
driver: driverForm.personnelName.trim()
|
||||
? {
|
||||
personnelName: driverForm.personnelName.trim(),
|
||||
phoneNo: driverForm.phoneNo || undefined,
|
||||
idCardNo: driverForm.idCardNo || undefined,
|
||||
personnelType: 'driver'
|
||||
}
|
||||
: null,
|
||||
escort: escortForm.personnelName.trim()
|
||||
? {
|
||||
personnelName: escortForm.personnelName.trim(),
|
||||
phoneNo: escortForm.phoneNo || undefined,
|
||||
idCardNo: escortForm.idCardNo || undefined,
|
||||
personnelType: 'escort'
|
||||
}
|
||||
: null,
|
||||
conflicts
|
||||
})
|
||||
|
||||
ElMessage.success('生成成功')
|
||||
emit('done')
|
||||
innerVisible.value = false
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.detail || e?.message || '生成失败'
|
||||
ElMessage.error(typeof msg === 'string' ? msg : '生成失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
rawText.value = ''
|
||||
parsed.value = null
|
||||
carrierId.value = ''
|
||||
createVehicle.value = true
|
||||
conflictRows.value = []
|
||||
vehicleForm.licensePlate = ''
|
||||
vehicleForm.trailerNo = ''
|
||||
vehicleForm.loadCapacity = undefined
|
||||
driverForm.personnelName = ''
|
||||
driverForm.phoneNo = ''
|
||||
driverForm.idCardNo = ''
|
||||
escortForm.personnelName = ''
|
||||
escortForm.phoneNo = ''
|
||||
escortForm.idCardNo = ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async v => {
|
||||
if (v) await ensureCarriers()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.paste-conflict-block {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
background: var(--mint-bg, #f8fafc);
|
||||
}
|
||||
|
||||
.paste-conflict-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.paste-conflict-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.paste-conflict-label {
|
||||
min-width: 220px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -53,6 +53,13 @@
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增人员
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isCarrierPersonnel"
|
||||
class="btn-ghost"
|
||||
@click="pasteVisible = true"
|
||||
>
|
||||
<el-icon><DocumentCopy /></el-icon>粘贴识别
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@@ -337,13 +344,21 @@
|
||||
<PlCertViewList :cert-list="viewDetail?.certList || []" layout="table" />
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
|
||||
<PlPasteRecognizeDialog
|
||||
v-if="isCarrierPersonnel"
|
||||
v-model:visible="pasteVisible"
|
||||
mode="personnel-first"
|
||||
@done="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, User } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Delete, DocumentCopy, Edit, Plus, RefreshLeft, Search, User } from '@element-plus/icons-vue'
|
||||
import { personnelApi, type Personnel, type PersonnelDetail, type CertRow } from '@/api/pl'
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
||||
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
|
||||
import PlPersonnelCertBadges from '@/components/business/pl-personnel-cert-badges/index.vue'
|
||||
import SystemViewDialog, {
|
||||
@@ -407,6 +422,7 @@ const submitLoading = ref(false)
|
||||
const personnelRows = ref<Personnel[]>([])
|
||||
const selectedRows = ref<Personnel[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const pasteVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref()
|
||||
const isEdit = ref(false)
|
||||
|
||||
@@ -64,6 +64,9 @@
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增车辆
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="pasteVisible = true">
|
||||
<el-icon><DocumentCopy /></el-icon>粘贴识别
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@@ -384,6 +387,13 @@
|
||||
/>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
|
||||
<PlPasteRecognizeDialog
|
||||
v-model:visible="pasteVisible"
|
||||
mode="vehicle-bundle"
|
||||
:carrier-options="carrierList"
|
||||
@done="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -391,7 +401,7 @@
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Van } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Delete, DocumentCopy, Edit, Plus, RefreshLeft, Search, Van } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
carrierApi,
|
||||
@@ -406,6 +416,7 @@ import {
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import PlVehicleCertBadges from '@/components/business/pl-vehicle-cert-badges/index.vue'
|
||||
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import SystemViewDialog, {
|
||||
type ViewSection
|
||||
@@ -459,6 +470,7 @@ const {
|
||||
const personnelOptions = ref<Personnel[]>([])
|
||||
const selectedRows = ref<Vehicle[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const pasteVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref()
|
||||
const isEdit = ref(false)
|
||||
|
||||
Reference in new issue
Block a user