feat(web-admin): link system user on personnel form

Allow binding a login account on driver/escort personnel so
miniprogram driver-plan visibility can resolve by sys_user_id.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-13 10:42:19 +08:00
1 parent 26b096ae38
commit 3dc71788d4
3 files changed
+149 -7

No files matched your search

+4
View File
@@ -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[]
}
@@ -30,6 +30,24 @@
<el-form-item label="手机号" prop="phoneNo">
<el-input v-model="formData.phoneNo" placeholder="请输入手机号(选填)" maxlength="32" />
</el-form-item>
<el-form-item label="关联用户账号" prop="sysUserId">
<el-select
v-model="formData.sysUserId"
placeholder="可选,绑定小程序登录账号"
filterable
clearable
:loading="userSelectLoading"
style="width: 100%"
@focus="ensureUserOptions"
>
<el-option
v-for="user in userOptions"
:key="user.userId"
:label="user.displayName"
:value="user.userId"
/>
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="formData.age"
@@ -110,6 +128,7 @@
<script setup lang="ts">
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<Array<{ userId: string; nickName: string; displayName: string }>>([])
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<string, unknown>) => {
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
}
+85 -4
View File
@@ -224,6 +224,18 @@
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('sysUserId')"
prop="sysUserId"
label="关联用户"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ resolveUserDisplay(row.sysUserId) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('age')"
prop="age"
@@ -338,6 +350,24 @@
<el-form-item label="手机号" prop="phoneNo">
<el-input v-model="formData.phoneNo" placeholder="请输入手机号(选填)" maxlength="32" />
</el-form-item>
<el-form-item label="关联用户账号" prop="sysUserId">
<el-select
v-model="formData.sysUserId"
placeholder="可选,绑定小程序登录账号"
filterable
clearable
:loading="userSelectLoading"
style="width: 100%"
@focus="ensureUserOptions"
>
<el-option
v-for="user in userOptions"
:key="user.userId"
:label="user.displayName"
:value="user.userId"
/>
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="formData.age"
@@ -466,6 +496,7 @@
<script setup lang="ts">
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<Array<{ userId: string; nickName: string; displayName: string }>>([])
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<string, unknown>) => {
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<ViewSection[]>(() => {
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())