提交
This commit is contained in:
1 parent
1938a9b76a
commit
9da462fcba
36 files changed
+609614
-6945
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -13,7 +13,7 @@
|
||||
@click.stop="handlePreview(item)"
|
||||
>
|
||||
<el-image
|
||||
v-if="item.isImage"
|
||||
v-if="item.isImage && !usePlaceholderThumb"
|
||||
class="pl-cert-list-attachment-cell__image pl-cert-list-attachment-cell__image--passive"
|
||||
fit="cover"
|
||||
lazy
|
||||
@@ -26,9 +26,18 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="pl-cert-list-attachment-cell__placeholder is-clickable">
|
||||
<el-icon :size="20" color="#0284c7"><Document /></el-icon>
|
||||
<span v-if="item.ext" class="pl-cert-list-attachment-cell__ext">{{ item.ext }}</span>
|
||||
<div
|
||||
v-else
|
||||
class="pl-cert-list-attachment-cell__placeholder is-clickable"
|
||||
:class="{ 'is-common-thumb': usePlaceholderThumb }"
|
||||
>
|
||||
<el-icon :size="20" :color="usePlaceholderThumb ? '#10b981' : '#0284c7'">
|
||||
<Picture v-if="usePlaceholderThumb" />
|
||||
<Document v-else />
|
||||
</el-icon>
|
||||
<span v-if="!usePlaceholderThumb && item.ext" class="pl-cert-list-attachment-cell__ext">{{
|
||||
item.ext
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-icon
|
||||
@@ -51,7 +60,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Document, Download } from '@element-plus/icons-vue'
|
||||
import { Document, Download, Picture } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import {
|
||||
@@ -69,11 +78,14 @@
|
||||
showVehiclePart?: boolean
|
||||
/** KeepAlive 离开页时置 false,暂停缩略图请求 */
|
||||
mediaActive?: boolean
|
||||
/** 列表态仅用统一占位缩略图,点击预览/下载时再加载真实文件 */
|
||||
usePlaceholderThumb?: boolean
|
||||
}>(),
|
||||
{
|
||||
certList: () => [],
|
||||
showVehiclePart: false,
|
||||
mediaActive: true
|
||||
mediaActive: true,
|
||||
usePlaceholderThumb: false
|
||||
}
|
||||
)
|
||||
|
||||
@@ -280,6 +292,10 @@
|
||||
&.is-clickable {
|
||||
background: #e0f2fe;
|
||||
}
|
||||
|
||||
&.is-common-thumb {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
}
|
||||
}
|
||||
|
||||
.pl-cert-list-attachment-cell__ext {
|
||||
|
||||
@@ -109,117 +109,77 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { personnelApi, type CertRow, type PersonnelDetail } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { calcAgeFromChineseIdCard } from '@/utils/id-card'
|
||||
import {
|
||||
collectRequiredCertSlotErrors,
|
||||
formatRequiredCertErrors,
|
||||
mergeCertListWithRequiredSlots
|
||||
} from '@/utils/pl-cert-slots'
|
||||
import {
|
||||
getRequiredPersonnelCertNamesByType,
|
||||
PL_PERSONNEL_BOTH_REQUIRED_CERT_NAMES,
|
||||
PL_PERSONNEL_TYPE_OPTIONS
|
||||
} from '@/utils/pl-personnel-cert'
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { personnelApi, type CertRow, type PersonnelDetail } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { calcAgeFromChineseIdCard } from '@/utils/id-card'
|
||||
import {
|
||||
collectRequiredCertSlotErrors,
|
||||
formatRequiredCertErrors,
|
||||
mergeCertListWithRequiredSlots
|
||||
} from '@/utils/pl-cert-slots'
|
||||
import {
|
||||
getRequiredPersonnelCertNamesByType,
|
||||
PL_PERSONNEL_BOTH_REQUIRED_CERT_NAMES,
|
||||
PL_PERSONNEL_TYPE_OPTIONS
|
||||
} from '@/utils/pl-personnel-cert'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
/** carrier=三方,self=山峡;锁定归属,不可切换 */
|
||||
affiliation: 'carrier' | 'self'
|
||||
/** 有值则为编辑,无值则为新增 */
|
||||
personnelId?: string | null
|
||||
}>()
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
/** carrier=三方,self=山峡;锁定归属,不可切换 */
|
||||
affiliation: 'carrier' | 'self'
|
||||
/** 有值则为编辑,无值则为新增 */
|
||||
personnelId?: string | null
|
||||
/** 新增时预填人员类型(如车辆关联行的司机/押运员) */
|
||||
initialPersonnelType?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
saved: [payload: { mode: 'create' | 'edit'; personnel: PersonnelDetail }]
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
saved: [payload: { mode: 'create' | 'edit'; personnel: PersonnelDetail }]
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const isCarrierPersonnel = computed(() => props.affiliation === 'carrier')
|
||||
const isEdit = computed(() => Boolean(props.personnelId))
|
||||
const isCarrierPersonnel = computed(() => props.affiliation === 'carrier')
|
||||
const isEdit = computed(() => Boolean(props.personnelId))
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (isEdit.value) {
|
||||
return isCarrierPersonnel.value ? '编辑人员' : '编辑山峡人员'
|
||||
}
|
||||
return isCarrierPersonnel.value ? '新增人员' : '新增山峡人员'
|
||||
})
|
||||
const dialogTitle = computed(() => {
|
||||
if (isEdit.value) {
|
||||
return isCarrierPersonnel.value ? '编辑人员' : '编辑山峡人员'
|
||||
}
|
||||
return isCarrierPersonnel.value ? '新增人员' : '新增山峡人员'
|
||||
})
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const dialogLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const photoUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const certList = ref<CertListItem[]>([])
|
||||
const certFormListRef = ref<CertFormListRef>()
|
||||
const submitLoading = ref(false)
|
||||
const dialogLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const photoUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const certList = ref<CertListItem[]>([])
|
||||
const certFormListRef = ref<CertFormListRef>()
|
||||
|
||||
const certNameOptions = [
|
||||
'身份证',
|
||||
'驾驶证',
|
||||
'驾驶员从业资格证',
|
||||
'押运员从业资格证',
|
||||
'押运员证',
|
||||
'健康证'
|
||||
]
|
||||
const certNameOptions = [
|
||||
'身份证',
|
||||
'驾驶证',
|
||||
'驾驶员从业资格证',
|
||||
'押运员从业资格证',
|
||||
'押运员证',
|
||||
'健康证'
|
||||
]
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
personnelName: '',
|
||||
phoneNo: '',
|
||||
age: null as number | null,
|
||||
idCardNo: '',
|
||||
personnelType: '',
|
||||
personnelAffiliation: props.affiliation,
|
||||
status: '1',
|
||||
remark: '',
|
||||
photoUrls: ''
|
||||
})
|
||||
|
||||
const requiredPersonnelCertNames = computed(() =>
|
||||
isCarrierPersonnel.value
|
||||
? []
|
||||
: formData.personnelType
|
||||
? getRequiredPersonnelCertNamesByType(formData.personnelType)
|
||||
: []
|
||||
)
|
||||
|
||||
const formRules = {
|
||||
personnelName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
personnelType: [{ required: true, message: '请选择人员类型', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function mergePersonnelCertSlots(list: CertListItem[]) {
|
||||
if (isCarrierPersonnel.value || !formData.personnelType) return list
|
||||
return mergeCertListWithRequiredSlots(
|
||||
list,
|
||||
getRequiredPersonnelCertNamesByType(formData.personnelType),
|
||||
{ stripManagedNames: PL_PERSONNEL_BOTH_REQUIRED_CERT_NAMES }
|
||||
)
|
||||
}
|
||||
|
||||
function resetCertList() {
|
||||
certList.value = []
|
||||
certFormListRef.value?.clearUploadRefs()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(formData, {
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
personnelName: '',
|
||||
phoneNo: '',
|
||||
age: null,
|
||||
age: null as number | null,
|
||||
idCardNo: '',
|
||||
personnelType: '',
|
||||
personnelAffiliation: props.affiliation,
|
||||
@@ -227,172 +187,216 @@ function resetForm() {
|
||||
remark: '',
|
||||
photoUrls: ''
|
||||
})
|
||||
resetCertList()
|
||||
}
|
||||
|
||||
function applyAgeFromIdCard(idCard?: string) {
|
||||
const age = calcAgeFromChineseIdCard(idCard)
|
||||
if (age != null) {
|
||||
formData.age = age
|
||||
}
|
||||
}
|
||||
const requiredPersonnelCertNames = computed(() =>
|
||||
isCarrierPersonnel.value
|
||||
? []
|
||||
: formData.personnelType
|
||||
? getRequiredPersonnelCertNamesByType(formData.personnelType)
|
||||
: []
|
||||
)
|
||||
|
||||
function handleIdCardNoInput(val: string | number) {
|
||||
applyAgeFromIdCard(String(val ?? ''))
|
||||
}
|
||||
const formRules = {
|
||||
personnelName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
personnelType: [{ required: true, message: '请选择人员类型', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function handleIdCardRecognized(certNo: string) {
|
||||
if (certNo) {
|
||||
formData.idCardNo = certNo
|
||||
applyAgeFromIdCard(certNo)
|
||||
}
|
||||
}
|
||||
|
||||
function validateCerts(): boolean {
|
||||
if (!formData.personnelType) {
|
||||
ElMessage.warning('请选择人员类型')
|
||||
return false
|
||||
}
|
||||
if (!isCarrierPersonnel.value) {
|
||||
const errors = collectRequiredCertSlotErrors(certList.value, requiredPersonnelCertNames.value)
|
||||
if (errors.length) {
|
||||
ElMessage.warning(formatRequiredCertErrors(errors))
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const cert of certList.value) {
|
||||
if (!cert._required && !cert.certName?.trim()) {
|
||||
ElMessage.warning('其他证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function loadDetail(personnelId: string) {
|
||||
dialogLoading.value = true
|
||||
try {
|
||||
const res = await personnelApi.get(personnelId)
|
||||
const detail = pickBxData<PersonnelDetail>(res)
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
personnelName: detail.personnelName,
|
||||
phoneNo: detail.phoneNo || '',
|
||||
age: detail.age ?? null,
|
||||
idCardNo: detail.idCardNo || '',
|
||||
personnelType: detail.personnelType || 'both',
|
||||
personnelAffiliation: props.affiliation,
|
||||
status: detail.status,
|
||||
remark: detail.remark || '',
|
||||
photoUrls: detail.photoUrls || ''
|
||||
})
|
||||
certList.value = mergePersonnelCertSlots(
|
||||
(detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
}))
|
||||
function mergePersonnelCertSlots(list: CertListItem[]) {
|
||||
if (isCarrierPersonnel.value || !formData.personnelType) return list
|
||||
return mergeCertListWithRequiredSlots(
|
||||
list,
|
||||
getRequiredPersonnelCertNamesByType(formData.personnelType),
|
||||
{ stripManagedNames: PL_PERSONNEL_BOTH_REQUIRED_CERT_NAMES }
|
||||
)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
} catch {
|
||||
ElMessage.error('加载人员详情失败')
|
||||
visible.value = false
|
||||
} finally {
|
||||
dialogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openDialog() {
|
||||
resetForm()
|
||||
if (props.personnelId) {
|
||||
await loadDetail(props.personnelId)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid || !validateCerts()) return
|
||||
if (certFormListRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('证书附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
if (photoUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
personnelName: formData.personnelName.trim(),
|
||||
phoneNo: formData.phoneNo || undefined,
|
||||
age: formData.age ?? undefined,
|
||||
idCardNo: formData.idCardNo || undefined,
|
||||
personnelType: formData.personnelType,
|
||||
function resetCertList() {
|
||||
certList.value = []
|
||||
certFormListRef.value?.clearUploadRefs()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
personnelName: '',
|
||||
phoneNo: '',
|
||||
age: null,
|
||||
idCardNo: '',
|
||||
personnelType: '',
|
||||
personnelAffiliation: props.affiliation,
|
||||
status: formData.status,
|
||||
remark: formData.remark || undefined,
|
||||
photoUrls: formData.photoUrls ?? '',
|
||||
certList: certList.value.map(c => ({
|
||||
certId: c.certId,
|
||||
certName: c.certName.trim(),
|
||||
certNo: c.certNo || undefined,
|
||||
expireDate: c.expireDate || undefined,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || undefined
|
||||
}))
|
||||
}
|
||||
status: '1',
|
||||
remark: '',
|
||||
photoUrls: ''
|
||||
})
|
||||
resetCertList()
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await personnelApi.update(formData.id, payload)
|
||||
const res = await personnelApi.get(formData.id)
|
||||
const detail = pickBxData<PersonnelDetail>(res)
|
||||
if (!detail?.id) {
|
||||
ElMessage.error('编辑成功但未返回人员信息')
|
||||
return
|
||||
function applyAgeFromIdCard(idCard?: string) {
|
||||
const age = calcAgeFromChineseIdCard(idCard)
|
||||
if (age != null) {
|
||||
formData.age = age
|
||||
}
|
||||
}
|
||||
|
||||
function handleIdCardNoInput(val: string | number) {
|
||||
applyAgeFromIdCard(String(val ?? ''))
|
||||
}
|
||||
|
||||
function handleIdCardRecognized(certNo: string) {
|
||||
if (certNo) {
|
||||
formData.idCardNo = certNo
|
||||
applyAgeFromIdCard(certNo)
|
||||
}
|
||||
}
|
||||
|
||||
function validateCerts(): boolean {
|
||||
if (!formData.personnelType) {
|
||||
ElMessage.warning('请选择人员类型')
|
||||
return false
|
||||
}
|
||||
if (!isCarrierPersonnel.value) {
|
||||
const errors = collectRequiredCertSlotErrors(certList.value, requiredPersonnelCertNames.value)
|
||||
if (errors.length) {
|
||||
ElMessage.warning(formatRequiredCertErrors(errors))
|
||||
return false
|
||||
}
|
||||
ElMessage.success('编辑成功')
|
||||
emit('saved', { mode: 'edit', personnel: detail })
|
||||
} else {
|
||||
const res = await personnelApi.create(payload)
|
||||
const detail = pickBxData<PersonnelDetail>(res)
|
||||
if (!detail?.id) {
|
||||
ElMessage.error('新增成功但未返回人员信息')
|
||||
return
|
||||
}
|
||||
for (const cert of certList.value) {
|
||||
if (!cert._required && !cert.certName?.trim()) {
|
||||
ElMessage.warning('其他证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
ElMessage.success('新增成功')
|
||||
emit('saved', { mode: 'create', personnel: detail })
|
||||
}
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
formRef.value?.resetFields()
|
||||
resetCertList()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.personnelType,
|
||||
() => {
|
||||
if (!formData.personnelType) return
|
||||
certList.value = mergePersonnelCertSlots(certList.value)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async open => {
|
||||
if (open) {
|
||||
await openDialog()
|
||||
async function loadDetail(personnelId: string) {
|
||||
dialogLoading.value = true
|
||||
try {
|
||||
const res = await personnelApi.get(personnelId)
|
||||
const detail = pickBxData<PersonnelDetail>(res)
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
personnelName: detail.personnelName,
|
||||
phoneNo: detail.phoneNo || '',
|
||||
age: detail.age ?? null,
|
||||
idCardNo: detail.idCardNo || '',
|
||||
personnelType: detail.personnelType || 'both',
|
||||
personnelAffiliation: props.affiliation,
|
||||
status: detail.status,
|
||||
remark: detail.remark || '',
|
||||
photoUrls: detail.photoUrls || ''
|
||||
})
|
||||
certList.value = mergePersonnelCertSlots(
|
||||
(detail.certList || []).map((c) => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
}))
|
||||
)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
} catch {
|
||||
ElMessage.error('加载人员详情失败')
|
||||
visible.value = false
|
||||
} finally {
|
||||
dialogLoading.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function openDialog() {
|
||||
resetForm()
|
||||
if (props.personnelId) {
|
||||
await loadDetail(props.personnelId)
|
||||
} else if (props.initialPersonnelType) {
|
||||
formData.personnelType = props.initialPersonnelType
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid || !validateCerts()) return
|
||||
if (certFormListRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('证书附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
if (photoUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
personnelName: formData.personnelName.trim(),
|
||||
phoneNo: formData.phoneNo || undefined,
|
||||
age: formData.age ?? undefined,
|
||||
idCardNo: formData.idCardNo || undefined,
|
||||
personnelType: formData.personnelType,
|
||||
personnelAffiliation: props.affiliation,
|
||||
status: formData.status,
|
||||
remark: formData.remark || undefined,
|
||||
photoUrls: formData.photoUrls ?? '',
|
||||
certList: certList.value.map((c) => ({
|
||||
certId: c.certId,
|
||||
certName: c.certName.trim(),
|
||||
certNo: c.certNo || undefined,
|
||||
expireDate: c.expireDate || undefined,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || undefined
|
||||
}))
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await personnelApi.update(formData.id, payload)
|
||||
const res = await personnelApi.get(formData.id)
|
||||
const detail = pickBxData<PersonnelDetail>(res)
|
||||
if (!detail?.id) {
|
||||
ElMessage.error('编辑成功但未返回人员信息')
|
||||
return
|
||||
}
|
||||
ElMessage.success('编辑成功')
|
||||
emit('saved', { mode: 'edit', personnel: detail })
|
||||
} else {
|
||||
const res = await personnelApi.create(payload)
|
||||
const detail = pickBxData<PersonnelDetail>(res)
|
||||
if (!detail?.id) {
|
||||
ElMessage.error('新增成功但未返回人员信息')
|
||||
return
|
||||
}
|
||||
ElMessage.success('新增成功')
|
||||
emit('saved', { mode: 'create', personnel: detail })
|
||||
}
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
formRef.value?.resetFields()
|
||||
resetCertList()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.personnelType,
|
||||
() => {
|
||||
if (!formData.personnelType) return
|
||||
certList.value = mergePersonnelCertSlots(certList.value)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (open) => {
|
||||
if (open) {
|
||||
await openDialog()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -89,7 +89,6 @@
|
||||
v-model="formData.supplierContracts"
|
||||
:customer-id="formData.supplierId"
|
||||
:disabled="!formData.supplierId"
|
||||
:required="!isPlInternalCustomerName(formData.supplierName)"
|
||||
>
|
||||
<template #toolbar="{ open, disabled, hint, hintWarn }">
|
||||
<div class="pl-plan-party-select-row">
|
||||
@@ -134,7 +133,6 @@
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="供方合同"
|
||||
:required="!isPlInternalCustomerName(formData.supplierName)"
|
||||
class="pl-plan-party-select-row__contract"
|
||||
>
|
||||
<div class="pl-plan-contract-trigger">
|
||||
@@ -182,7 +180,6 @@
|
||||
v-model="formData.demanderContracts"
|
||||
:customer-id="formData.customerId"
|
||||
:disabled="!formData.customerId"
|
||||
:required="!isPlInternalCustomerName(formData.customerName)"
|
||||
>
|
||||
<template #toolbar="{ open, disabled, hint, hintWarn }">
|
||||
<div class="pl-plan-party-select-row">
|
||||
@@ -227,7 +224,6 @@
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="需方合同"
|
||||
:required="!isPlInternalCustomerName(formData.customerName)"
|
||||
class="pl-plan-party-select-row__contract"
|
||||
>
|
||||
<div class="pl-plan-contract-trigger">
|
||||
@@ -515,7 +511,6 @@ import {
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { usePlFreightCalc } from '@/composables/usePlFreightCalc'
|
||||
import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
@@ -1312,14 +1307,6 @@ function validateBusinessRules() {
|
||||
ElMessage.warning('请选择需方客户')
|
||||
return false
|
||||
}
|
||||
if (!isPlInternalCustomerName(formData.supplierName) && !formData.supplierContracts.length) {
|
||||
ElMessage.warning('请选择供方合同')
|
||||
return false
|
||||
}
|
||||
if (!isPlInternalCustomerName(formData.customerName) && !formData.demanderContracts.length) {
|
||||
ElMessage.warning('请选择需方合同')
|
||||
return false
|
||||
}
|
||||
if (!formData.products.length) {
|
||||
ElMessage.warning('请至少保留一条产品明细')
|
||||
return false
|
||||
|
||||
@@ -109,26 +109,19 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="姓名" min-width="160">
|
||||
<el-table-column label="姓名" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.personnelId"
|
||||
placeholder="请选择人员"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchPersonnel"
|
||||
<PlVehiclePersonnelSelectCell
|
||||
:row="row"
|
||||
:options="getPersonnelOptionsForRow(row)"
|
||||
:loading="personnelSelectLoading"
|
||||
@focus="onPersonnelSelectFocus"
|
||||
@change="(id) => handlePersonnelSelect(row, id)"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in getPersonnelOptionsForRow(row)"
|
||||
:key="p.id"
|
||||
:label="formatPersonnelLabel(p)"
|
||||
:value="p.id"
|
||||
/>
|
||||
</el-select>
|
||||
:affiliation="props.vehicleType"
|
||||
:search-personnel="searchPersonnel"
|
||||
:on-personnel-select-focus="onPersonnelSelectFocus"
|
||||
:format-personnel-label="formatPersonnelLabel"
|
||||
@select="(id) => handlePersonnelSelect(row, id)"
|
||||
@saved="(payload) => handlePersonnelFormSaved(row, payload)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机号" min-width="120">
|
||||
@@ -194,6 +187,8 @@
|
||||
<script setup lang="ts">
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehiclePersonnelSelectCell from '@/components/business/pl-vehicle-personnel-select-cell/index.vue'
|
||||
import { usePlVehiclePersonnelQuickCreate } from '@/composables/usePlVehiclePersonnelQuickCreate'
|
||||
import { Check, Close, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
@@ -308,6 +303,11 @@
|
||||
})
|
||||
})
|
||||
|
||||
const { handlePersonnelFormSaved } = usePlVehiclePersonnelQuickCreate({
|
||||
personnelOptions,
|
||||
onSelect: handlePersonnelSelect
|
||||
})
|
||||
|
||||
function createEmptyPersonnel(): VehiclePersonnelRow {
|
||||
return {
|
||||
personnelId: undefined,
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="panel dialog-list-panel">
|
||||
<el-tabs v-model="activeTab" class="pl-vehicle-library-select-tabs" @tab-change="handleTabChange">
|
||||
<el-tabs
|
||||
v-model="activeTab"
|
||||
class="pl-vehicle-library-select-tabs"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<el-tab-pane label="三方车辆" name="carrier" />
|
||||
<el-tab-pane label="三方驾押人员" name="carrier_personnel" />
|
||||
<el-tab-pane label="山峡车辆" name="self" />
|
||||
@@ -67,9 +71,14 @@
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
prop="licensePlate"
|
||||
label="车牌号"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleConfirm(row)">{{ row.licensePlate }}</el-link>
|
||||
</template>
|
||||
@@ -108,13 +117,12 @@
|
||||
<el-table-column
|
||||
prop="personnelSummary"
|
||||
label="关联人员摘要"
|
||||
min-width="160"
|
||||
min-width="220"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ normalizePlPersonnelSummaryDisplay(row.personnelSummary) }}
|
||||
<PlVehiclePersonnelSummaryCell :vehicle="row" @manage="handleOpenEdit(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -173,194 +181,191 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DocumentCopy, Edit, Plus, Search, Select } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
type PasteCommitResult,
|
||||
type Vehicle,
|
||||
type VehicleDetail
|
||||
} from '@/api/pl/index'
|
||||
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
||||
import PlPersonnelLibrarySelectPanel from '@/components/business/pl-personnel-library-select-panel/index.vue'
|
||||
import PlVehicleFormDialog from '@/components/business/pl-vehicle-form-dialog/index.vue'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
||||
import {
|
||||
getPlVehicleStatusLabel,
|
||||
getPlVehicleStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay
|
||||
} from '@/utils/pl-display'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
import { DocumentCopy, Edit, Plus, Search, Select } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
type PasteCommitResult,
|
||||
type Vehicle,
|
||||
type VehicleDetail
|
||||
} from '@/api/pl/index'
|
||||
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
||||
import PlPersonnelLibrarySelectPanel from '@/components/business/pl-personnel-library-select-panel/index.vue'
|
||||
import PlVehicleFormDialog from '@/components/business/pl-vehicle-form-dialog/index.vue'
|
||||
import PlVehiclePersonnelSummaryCell from '@/components/business/pl-vehicle-personnel-summary-cell/index.vue'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { getPlVehicleStatusLabel, getPlVehicleStatusTagType } from '@/utils/pl-display'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
type LibraryTab = 'carrier' | 'carrier_personnel' | 'self' | 'self_personnel'
|
||||
type VehicleTab = 'carrier' | 'self'
|
||||
type LibraryTab = 'carrier' | 'carrier_personnel' | 'self' | 'self_personnel'
|
||||
type VehicleTab = 'carrier' | 'self'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
select: [row: Vehicle]
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
select: [row: Vehicle]
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const activeTab = ref<LibraryTab>('carrier')
|
||||
const rows = ref<Vehicle[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_LIST_PAGE_SIZE,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const formVisible = ref(false)
|
||||
const editingId = ref<string | null>(null)
|
||||
const pasteRecognizeVisible = ref(false)
|
||||
const carrierPersonnelPanelRef = ref<InstanceType<typeof PlPersonnelLibrarySelectPanel>>()
|
||||
const selfPersonnelPanelRef = ref<InstanceType<typeof PlPersonnelLibrarySelectPanel>>()
|
||||
|
||||
const isVehicleTab = computed(() => activeTab.value === 'carrier' || activeTab.value === 'self')
|
||||
const vehicleTab = computed<VehicleTab>(() => (activeTab.value === 'self' ? 'self' : 'carrier'))
|
||||
|
||||
async function fetchList() {
|
||||
if (!isVehicleTab.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleApi.list({
|
||||
vehicleType: vehicleTab.value,
|
||||
q: keyword.value.trim() || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const { list, total } = pickBxPageResult<Vehicle>(res)
|
||||
rows.value = list
|
||||
pagination.total = total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function refreshPersonnelPanel() {
|
||||
if (activeTab.value === 'carrier_personnel') {
|
||||
carrierPersonnelPanelRef.value?.resetPanel()
|
||||
carrierPersonnelPanelRef.value?.fetchList()
|
||||
} else if (activeTab.value === 'self_personnel') {
|
||||
selfPersonnelPanelRef.value?.resetPanel()
|
||||
selfPersonnelPanelRef.value?.fetchList()
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabChange() {
|
||||
pagination.page = 1
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
keyword.value = ''
|
||||
if (isVehicleTab.value) {
|
||||
fetchList()
|
||||
return
|
||||
}
|
||||
nextTick(() => {
|
||||
refreshPersonnelPanel()
|
||||
const innerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
keyword.value = ''
|
||||
activeTab.value = 'carrier'
|
||||
pagination.page = 1
|
||||
pagination.pageSize = DEFAULT_LIST_PAGE_SIZE
|
||||
pagination.total = 0
|
||||
rows.value = []
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
fetchList()
|
||||
}
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const activeTab = ref<LibraryTab>('carrier')
|
||||
const rows = ref<Vehicle[]>([])
|
||||
|
||||
function handleOpenCreate() {
|
||||
editingId.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_LIST_PAGE_SIZE,
|
||||
total: 0
|
||||
})
|
||||
|
||||
function handleOpenEdit(row: Vehicle) {
|
||||
editingId.value = row.id
|
||||
formVisible.value = true
|
||||
}
|
||||
const formVisible = ref(false)
|
||||
const editingId = ref<string | null>(null)
|
||||
const pasteRecognizeVisible = ref(false)
|
||||
const carrierPersonnelPanelRef = ref<InstanceType<typeof PlPersonnelLibrarySelectPanel>>()
|
||||
const selfPersonnelPanelRef = ref<InstanceType<typeof PlPersonnelLibrarySelectPanel>>()
|
||||
|
||||
function handleFormSaved(payload: { mode: 'create' | 'edit'; vehicle: VehicleDetail }) {
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
if (payload.mode === 'create') {
|
||||
handleConfirm(payload.vehicle)
|
||||
return
|
||||
const isVehicleTab = computed(() => activeTab.value === 'carrier' || activeTab.value === 'self')
|
||||
const vehicleTab = computed<VehicleTab>(() => (activeTab.value === 'self' ? 'self' : 'carrier'))
|
||||
|
||||
async function fetchList() {
|
||||
if (!isVehicleTab.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleApi.list({
|
||||
vehicleType: vehicleTab.value,
|
||||
q: keyword.value.trim() || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const { list, total } = pickBxPageResult<Vehicle>(res)
|
||||
rows.value = list
|
||||
pagination.total = total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
fetchList()
|
||||
}
|
||||
|
||||
async function handlePasteRecognizeDone(result?: PasteCommitResult) {
|
||||
if (!result?.vehicleId) {
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await vehicleApi.get(result.vehicleId)
|
||||
const vehicle = pickBxData<VehicleDetail>(res)
|
||||
if (vehicle?.id) {
|
||||
handleConfirm(vehicle)
|
||||
|
||||
function refreshPersonnelPanel() {
|
||||
if (activeTab.value === 'carrier_personnel') {
|
||||
carrierPersonnelPanelRef.value?.resetPanel()
|
||||
carrierPersonnelPanelRef.value?.fetchList()
|
||||
} else if (activeTab.value === 'self_personnel') {
|
||||
selfPersonnelPanelRef.value?.resetPanel()
|
||||
selfPersonnelPanelRef.value?.fetchList()
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabChange() {
|
||||
pagination.page = 1
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
keyword.value = ''
|
||||
if (isVehicleTab.value) {
|
||||
fetchList()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
nextTick(() => {
|
||||
refreshPersonnelPanel()
|
||||
})
|
||||
}
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleConfirm(row: Vehicle) {
|
||||
emit('select', row)
|
||||
innerVisible.value = false
|
||||
}
|
||||
function handleOpen() {
|
||||
keyword.value = ''
|
||||
activeTab.value = 'carrier'
|
||||
pagination.page = 1
|
||||
pagination.pageSize = DEFAULT_LIST_PAGE_SIZE
|
||||
pagination.total = 0
|
||||
rows.value = []
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleOpenCreate() {
|
||||
editingId.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleOpenEdit(row: Vehicle) {
|
||||
editingId.value = row.id
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleFormSaved(payload: { mode: 'create' | 'edit'; vehicle: VehicleDetail }) {
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
if (payload.mode === 'create') {
|
||||
handleConfirm(payload.vehicle)
|
||||
return
|
||||
}
|
||||
fetchList()
|
||||
}
|
||||
|
||||
async function handlePasteRecognizeDone(result?: PasteCommitResult) {
|
||||
if (!result?.vehicleId) {
|
||||
fetchList()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await vehicleApi.get(result.vehicleId)
|
||||
const vehicle = pickBxData<VehicleDetail>(res)
|
||||
if (vehicle?.id) {
|
||||
handleConfirm(vehicle)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleConfirm(row: Vehicle) {
|
||||
emit('select', row)
|
||||
innerVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-vehicle-library-select-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
min-height: 560px;
|
||||
}
|
||||
.pl-vehicle-library-select-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.dialog-list-panel {
|
||||
.list-toolbar {
|
||||
margin-bottom: 0;
|
||||
padding: 10px 0 12px;
|
||||
.dialog-list-panel {
|
||||
.list-toolbar {
|
||||
margin-bottom: 0;
|
||||
padding: 10px 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pl-vehicle-library-select-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 0;
|
||||
.pl-vehicle-library-select-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pl-vehicle-library-select-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pl-vehicle-library-select-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pl-vehicle-library-select-search {
|
||||
width: 280px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.pl-vehicle-library-select-search {
|
||||
width: 280px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="pl-vehicle-personnel-select-cell">
|
||||
<el-select
|
||||
:model-value="row.personnelId"
|
||||
placeholder="请选择人员"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
class="pl-vehicle-personnel-select-cell__select"
|
||||
:remote-method="searchPersonnel"
|
||||
:loading="loading"
|
||||
@focus="onPersonnelSelectFocus"
|
||||
@update:model-value="handleSelect"
|
||||
>
|
||||
<el-option v-for="p in options" :key="p.id" :label="formatPersonnelLabel(p)" :value="p.id" />
|
||||
</el-select>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="pl-vehicle-personnel-select-cell__action"
|
||||
@click="handleOpenCreate"
|
||||
>
|
||||
新建
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.personnelId"
|
||||
link
|
||||
type="primary"
|
||||
class="pl-vehicle-personnel-select-cell__action"
|
||||
@click="handleOpenEdit"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<PlPersonnelFormDialog
|
||||
v-model="formVisible"
|
||||
:affiliation="affiliation"
|
||||
:personnel-id="editingPersonnelId"
|
||||
:initial-personnel-type="initialPersonnelType"
|
||||
@saved="handleFormSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PlPersonnelFormDialog from '@/components/business/pl-personnel-form-dialog/index.vue'
|
||||
import type { Personnel, PersonnelDetail, VehiclePersonnelRow } from '@/api/pl'
|
||||
|
||||
const props = defineProps<{
|
||||
row: VehiclePersonnelRow
|
||||
options: Personnel[]
|
||||
loading: boolean
|
||||
affiliation: 'carrier' | 'self'
|
||||
searchPersonnel: (keyword: string) => void
|
||||
onPersonnelSelectFocus: () => void
|
||||
formatPersonnelLabel: (person: Personnel) => string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [personnelId: string | undefined]
|
||||
saved: [payload: { mode: 'create' | 'edit'; personnel: PersonnelDetail }]
|
||||
}>()
|
||||
|
||||
const formVisible = ref(false)
|
||||
const editingPersonnelId = ref<string | null>(null)
|
||||
|
||||
const initialPersonnelType = computed(() => {
|
||||
if (editingPersonnelId.value) return null
|
||||
return props.row.role === 'escort' ? 'escort' : 'driver'
|
||||
})
|
||||
|
||||
function handleSelect(personnelId: string | undefined) {
|
||||
emit('select', personnelId)
|
||||
}
|
||||
|
||||
function handleOpenCreate() {
|
||||
editingPersonnelId.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleOpenEdit() {
|
||||
if (!props.row.personnelId) return
|
||||
editingPersonnelId.value = props.row.personnelId
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleFormSaved(payload: { mode: 'create' | 'edit'; personnel: PersonnelDetail }) {
|
||||
emit('saved', payload)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-vehicle-personnel-select-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pl-vehicle-personnel-select-cell__select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-vehicle-personnel-select-cell__action {
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="pl-vehicle-personnel-summary-cell" @click.stop>
|
||||
<span
|
||||
class="pl-vehicle-personnel-summary-cell__text"
|
||||
:class="{ 'is-empty': !summaryText }"
|
||||
:title="summaryText || '暂无关联人员'"
|
||||
>
|
||||
{{ summaryText || '—' }}
|
||||
</span>
|
||||
<div class="pl-vehicle-personnel-summary-cell__actions">
|
||||
<el-button link type="primary" @click="emit('manage')">
|
||||
<el-icon><Edit /></el-icon>管理
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
import type { Vehicle } from '@/api/pl'
|
||||
import { normalizePlPersonnelSummaryDisplay } from '@/utils/pl-display'
|
||||
|
||||
const props = defineProps<{
|
||||
vehicle: Vehicle
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
manage: []
|
||||
}>()
|
||||
|
||||
const summaryText = computed(() =>
|
||||
normalizePlPersonnelSummaryDisplay(props.vehicle.personnelSummary)
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-vehicle-personnel-summary-cell {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pl-vehicle-personnel-summary-cell__text {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text, #1e293b);
|
||||
font-size: 13px;
|
||||
|
||||
&.is-empty {
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.pl-vehicle-personnel-summary-cell__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user