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 @@
+
+
+ {{ resolveUserDisplay(row.sysUserId) }}
+
+
+
+
+
+
+
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())