diff --git a/web-admin/src/api/pl/index.ts b/web-admin/src/api/pl/index.ts index 4abcb84a8..84ab201d6 100644 --- a/web-admin/src/api/pl/index.ts +++ b/web-admin/src/api/pl/index.ts @@ -162,6 +162,8 @@ export interface Personnel { status: string remark?: string photoUrls?: string + /** 关联系统用户 ID */ + sysUserId?: string | null nearestExpireDate?: string certList?: CertRead[] createTime: string @@ -181,6 +183,8 @@ export interface PersonnelPayload { status: string remark?: string photoUrls?: string + /** 关联系统用户;传空串/null 表示清空 */ + sysUserId?: string | null certList?: CertRow[] } diff --git a/web-admin/src/components/business/pl-personnel-form-dialog/index.vue b/web-admin/src/components/business/pl-personnel-form-dialog/index.vue index f9ab4ed5a..2f4e5fbc2 100644 --- a/web-admin/src/components/business/pl-personnel-form-dialog/index.vue +++ b/web-admin/src/components/business/pl-personnel-form-dialog/index.vue @@ -30,6 +30,24 @@ + + + + + import { Check, Close } from '@element-plus/icons-vue' + import { fetchUserOptions } from '@/api/admin' 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' @@ -185,9 +204,38 @@ personnelAffiliation: props.affiliation, status: '1', remark: '', - photoUrls: '' + photoUrls: '', + sysUserId: '' as string }) + const userSelectLoading = ref(false) + const userOptions = ref>([]) + + async function ensureUserOptions() { + if (userOptions.value.length || userSelectLoading.value) return + userSelectLoading.value = true + try { + const res = await fetchUserOptions() + const data = (res as any)?.data ?? res + userOptions.value = (Array.isArray(data) ? data : []) + .map((u: Record) => { + const userId = String(u.value ?? u.id ?? u.userId ?? '') + const name = String(u.label ?? u.nickName ?? u.userName ?? userId) + const account = String(u.userName ?? u.account ?? '').trim() + return { + userId, + nickName: name, + displayName: account && account !== name ? `${name}(${account})` : name + } + }) + .filter((u) => u.userId) + } catch { + userOptions.value = [] + } finally { + userSelectLoading.value = false + } + } + const requiredPersonnelCertNames = computed(() => isCarrierPersonnel.value ? [] @@ -227,7 +275,8 @@ personnelAffiliation: props.affiliation, status: '1', remark: '', - photoUrls: '' + photoUrls: '', + sysUserId: '' }) resetCertList() } @@ -286,7 +335,8 @@ personnelAffiliation: props.affiliation, status: detail.status, remark: detail.remark || '', - photoUrls: detail.photoUrls || '' + photoUrls: detail.photoUrls || '', + sysUserId: detail.sysUserId || '' }) certList.value = mergePersonnelCertSlots( (detail.certList || []).map((c) => ({ @@ -310,6 +360,7 @@ async function openDialog() { resetForm() + void ensureUserOptions() if (props.personnelId) { await loadDetail(props.personnelId) } else if (props.initialPersonnelType) { @@ -341,6 +392,7 @@ status: formData.status, remark: formData.remark || undefined, photoUrls: formData.photoUrls ?? '', + sysUserId: (formData.sysUserId || '').trim() || null, certList: certList.value.map((c) => ({ certId: c.certId, certName: c.certName.trim(), @@ -372,6 +424,11 @@ emit('saved', { mode: 'create', personnel: detail }) } visible.value = false + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.response?.data?.message || e?.message || '' + if (typeof msg === 'string' && msg) { + ElMessage.error(msg) + } } finally { submitLoading.value = false } diff --git a/web-admin/src/views/pl/personnel/index.vue b/web-admin/src/views/pl/personnel/index.vue index 1dfa4cb44..bc1946387 100644 --- a/web-admin/src/views/pl/personnel/index.vue +++ b/web-admin/src/views/pl/personnel/index.vue @@ -224,6 +224,18 @@ + + + + + + + + import { Check, Close, Delete, DocumentCopy, Download, Edit, Filter, Plus, User } from '@element-plus/icons-vue' +import { fetchUserOptions } from '@/api/admin' 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' @@ -535,6 +566,7 @@ const listColumnDefs = [ { prop: 'personnelAffiliation', label: '人员归属' }, { prop: 'personnelType', label: '人员类型' }, { prop: 'phoneNo', label: '手机号' }, + { prop: 'sysUserId', label: '关联用户' }, { prop: 'age', label: '年龄' }, { prop: 'idCardNo', label: '身份证号' }, { prop: 'status', label: '状态' }, @@ -548,7 +580,40 @@ const { isIndeterminate, isColumnVisible, resetToDefault -} = usePersistedListColumns('pl-personnel-list-columns-v3', listColumnDefs) +} = usePersistedListColumns('pl-personnel-list-columns-v4', listColumnDefs) + +const userSelectLoading = ref(false) +const userOptions = ref>([]) + +async function ensureUserOptions() { + if (userOptions.value.length || userSelectLoading.value) return + userSelectLoading.value = true + try { + const res = await fetchUserOptions() + const data = (res as any)?.data ?? res + userOptions.value = (Array.isArray(data) ? data : []).map((u: Record) => { + const userId = String(u.value ?? u.id ?? u.userId ?? '') + const name = String(u.label ?? u.nickName ?? u.userName ?? userId) + const account = String(u.userName ?? u.account ?? '').trim() + return { + userId, + nickName: name, + displayName: account && account !== name ? `${name}(${account})` : name + } + }).filter((u) => u.userId) + } catch { + userOptions.value = [] + } finally { + userSelectLoading.value = false + } +} + +function resolveUserDisplay(sysUserId?: string | null) { + const uid = (sysUserId || '').trim() + if (!uid) return '—' + const hit = userOptions.value.find((u) => u.userId === uid) + return hit?.displayName || uid +} const { filters, @@ -653,7 +718,8 @@ const formData = reactive({ personnelAffiliation: 'self', status: '1', remark: '', - photoUrls: '' + photoUrls: '', + sysUserId: '' as string }) const requiredPersonnelCertNames = computed(() => @@ -720,6 +786,7 @@ const viewSections = computed(() => { fields: [ { label: '姓名', value: d.personnelName }, { label: '手机号', value: d.phoneNo || '—' }, + { label: '关联用户', value: resolveUserDisplay(d.sysUserId) }, { label: '人员归属', value: getPersonnelAffiliationLabel(d.personnelAffiliation), @@ -919,10 +986,12 @@ function handleAdd() { personnelAffiliation: affiliationMode.value, status: '1', remark: '', - photoUrls: '' + photoUrls: '', + sysUserId: '' }) resetCertList() dialogVisible.value = true + void ensureUserOptions() } async function handleView(row: Personnel) { @@ -930,6 +999,7 @@ async function handleView(row: Personnel) { viewLoading.value = true viewDetail.value = null try { + await ensureUserOptions() const res = await personnelApi.get(row.id) viewDetail.value = ((res.data ?? res) || null) as PersonnelDetail | null } catch { @@ -963,6 +1033,7 @@ async function handleEdit(row: Personnel) { isEdit.value = true dialogVisible.value = true try { + await ensureUserOptions() const res = await personnelApi.get(row.id) const detail = ((res.data ?? res) || {}) as PersonnelDetail Object.assign(formData, { @@ -975,7 +1046,8 @@ async function handleEdit(row: Personnel) { personnelAffiliation: affiliationMode.value, status: detail.status, remark: detail.remark || '', - photoUrls: detail.photoUrls || '' + photoUrls: detail.photoUrls || '', + sysUserId: detail.sysUserId || '' }) certList.value = mergePersonnelCertSlots( @@ -1019,6 +1091,7 @@ async function handleSubmit() { status: formData.status, remark: formData.remark || undefined, photoUrls: formData.photoUrls ?? '', + sysUserId: (formData.sysUserId || '').trim() || null, certList: certList.value.map(c => ({ certId: c.certId, certName: c.certName.trim(), @@ -1037,6 +1110,13 @@ async function handleSubmit() { ElMessage.success(isEdit.value ? '编辑成功' : '新增成功') dialogVisible.value = false fetchData() + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.response?.data?.message || e?.message || '' + if (typeof msg === 'string' && msg.includes('已关联')) { + ElMessage.error(msg) + } else if (msg) { + ElMessage.error(msg) + } } finally { submitLoading.value = false } @@ -1063,6 +1143,7 @@ async function handleDelete(row: Personnel) { } onMounted(() => { + void ensureUserOptions() fetchData() nextTick(() => recalcTableHeight())