From cf621cb9ec8d0f3dc8dbaa91c93c83b19c9211d8 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 30 Sep 2026 23:37:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(ehrm):=20=E7=94=A8=E6=88=B7=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=91=98=E5=B7=A5=E6=A1=A3=E6=A1=88=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8D=95=E6=9D=A1=E6=A1=A3=E6=A1=88=E9=9D=A2=E6=9D=BF=EF=BC=88?= =?UTF-8?q?=E5=8E=BB=E8=A1=A8=E6=A0=BC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 去掉员工档案子表,改为一人一份的档案面板:无档案且有 eHRM 权限(菜单可见)时居中显示「点击填写」(抽屉动态表单),无权限显示「无权修改」;已有档案时用 el-descriptions 键值只读展示,卡片右上角提供编辑/删除。表单与列表共用 /API/FormAPI/Columns/EmployeeProfile 列定义,UserUUID 由本页上下文注入。撤回上一版 CrudTable 的 singleRow 属性(该方案已废弃)。 --- src/views/system/UserDetail.vue | 258 ++++++++++++++++++++++++++++++-- 1 file changed, 244 insertions(+), 14 deletions(-) diff --git a/src/views/system/UserDetail.vue b/src/views/system/UserDetail.vue index fab79ea..80d133f 100644 --- a/src/views/system/UserDetail.vue +++ b/src/views/system/UserDetail.vue @@ -25,17 +25,18 @@ */ import { ref, reactive, computed, onMounted, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' import DetailPage from '@/components/DetailPage.vue' import InfoGrid from '@/components/InfoGrid.vue' import CrudTable from '@/components/CrudTable.vue' +import CrudFormPanel from '@/components/CrudFormPanel.vue' import EntityPicker from '@/components/EntityPicker.vue' import { formApi } from '@/api/form' import { get } from '@/api/request' import { useUserStore } from '@/stores/user' import { getServerScopeParams, setPageTitle } from '@/utils/serverScope' import { formatUtcTime as formatTime } from '@/utils/dateTime' -import type { ColumnSchema } from '@/types/api' +import type { ColumnSchema, MenuItem } from '@/types/api' const route = useRoute() const router = useRouter() @@ -199,8 +200,171 @@ async function handleSubmit(): Promise { } } -// === 子表跳转 === +// ===== 员工档案(eHRM)===== +// 一人一份:无档案时给「点击填写」入口,已有档案时键值只读展示 + 右上角编辑/删除。 +// 是否能写按 eHRM 权限判定(后端按应用安装 + 职务绑定过滤菜单,菜单可见即视为有权限)。 +// 后端 form_service 已对「同一员工重复建档 / 改绑到已有档案的员工」做拦截。 +/** eHRM 档案管理菜单路由(清单 MenuLink /ehrm/profile 规范化后的 Path) */ +const EHRM_PROFILE_PATH = '/ehrm/profile' + +/** 档案只读展示时排除的列:主键与纯 UUID 引用列(员工=本页用户,部门已有 DeptName 展示) */ +const PROFILE_HIDDEN_FIELDS = ['EmployeeProfileUUID', 'UserUUID', 'DeptUUID', 'TenantUUID', 'CompanyUUID'] + +const profileLoading = ref(false) +/** 该员工的档案记录(一人一份,最多一条) */ +const profileRecord = ref | null>(null) +/** 档案列定义(只读展示与填写表单共用) */ +const profileColumns = ref([]) +const profileDialogVisible = ref(false) +const profileSubmitting = ref(false) +/** 表单是否为编辑态(false=首次填写) */ +const profileIsEdit = ref(false) +const profileForm = reactive>({}) + +/** 菜单树中是否存在指定路由(判断 eHRM 档案管理菜单是否可见) */ +function menuTreeHasPath(items: MenuItem[], path: string): boolean { + return items.some( + (m) => m.Path === path || (m.Children?.length ? menuTreeHasPath(m.Children, path) : false), + ) +} + +/** 是否具备 eHRM 档案修改权限(菜单可见即视为有权限) */ +const canEditProfile = computed(() => menuTreeHasPath(userStore.menus, EHRM_PROFILE_PATH)) + +/** 只读展示列:在表格可见列基础上排除主键与纯 UUID 引用列 */ +const profileDisplayColumns = computed(() => + profileColumns.value.filter( + (c) => + c.visible !== false && + c.type !== 'hidden' && + c.type !== 'divider' && + c.type !== 'html' && + !PROFILE_HIDDEN_FIELDS.includes(c.field), + ), +) + +/** 填写/编辑表单列:排除隐藏/主键/非表单类型,以及由本页上下文注入的 UserUUID */ +const profileFormColumns = computed(() => + profileColumns.value.filter( + (c) => + c.editable !== false && + !c.hideInForm && + c.type !== 'hidden' && + c.type !== 'divider' && + c.type !== 'html' && + c.field !== 'EmployeeProfileUUID' && + c.field !== 'UserUUID', + ), +) + +/** 只读展示值:实体标签(*_label)优先 → 选项映射 → 日期格式化 → 原值 */ +function profileValue(col: ColumnSchema): string { + const row = profileRecord.value + if (!row) return '-' + const label = row[`${col.field}_label`] + if (label) return String(label) + const raw = row[col.field] + if (raw === null || raw === undefined || raw === '') return '-' + if (col.options?.length) { + const hit = col.options.find((o) => String(o.value) === String(raw)) + if (hit) return hit.label + } + if (col.type === 'date' || col.type === 'datetime') return formatTime(String(raw)) + return String(raw) +} + +/** 加载该员工的档案(列表按 UserUUID 过滤,取首条) */ +async function loadProfile(): Promise { + if (!userUUID.value) { + profileRecord.value = null + return + } + profileLoading.value = true + try { + if (!profileColumns.value.length) { + profileColumns.value = await formApi.getColumns('EmployeeProfile') + } + } catch { + // 列定义加载失败不阻塞只读展示 + } + try { + const res = await formApi.list>('EmployeeProfile', { + UserUUID: userUUID.value, + pageSize: 1, + }) + const list = Array.isArray(res) ? res : ((res as { list?: Record[] })?.list ?? []) + profileRecord.value = list[0] ?? null + } catch { + // 错误提示已由响应拦截器统一弹出 + profileRecord.value = null + } finally { + profileLoading.value = false + } +} + +/** 「点击填写」:新建档案(UserUUID 由本页上下文注入,表单内不出现该字段) */ +function openProfileCreate(): void { + if (!canEditProfile.value) return + Object.keys(profileForm).forEach((k) => delete profileForm[k]) + profileForm.UserUUID = userUUID.value + profileIsEdit.value = false + profileDialogVisible.value = true +} + +/** 「编辑」:回填现有档案 */ +function openProfileEdit(): void { + if (!canEditProfile.value || !profileRecord.value) return + Object.keys(profileForm).forEach((k) => delete profileForm[k]) + Object.assign(profileForm, profileRecord.value) + profileForm.UserUUID = userUUID.value + profileIsEdit.value = true + profileDialogVisible.value = true +} + +/** 提交档案表单(新增 Add / 编辑 Modify) */ +async function submitProfile(): Promise { + profileSubmitting.value = true + try { + if (profileIsEdit.value && profileRecord.value) { + await formApi.update('EmployeeProfile', profileRecord.value.EmployeeProfileUUID as string, { + ...profileForm, + }) + } else { + await formApi.create('EmployeeProfile', { ...profileForm }) + } + ElMessage.success(profileIsEdit.value ? '保存成功' : '填写成功') + profileDialogVisible.value = false + await loadProfile() + } catch { + // 错误提示已由响应拦截器统一弹出(如"该员工已存在档案") + } finally { + profileSubmitting.value = false + } +} + +/** 「删除」:删除后可重新填写 */ +async function deleteProfile(): Promise { + if (!canEditProfile.value || !profileRecord.value) return + try { + await ElMessageBox.confirm('确认删除该员工的档案?删除后可重新填写。', '删除档案', { + type: 'warning', + confirmButtonText: '删除', + cancelButtonText: '取消', + }) + } catch { + return // 用户取消 + } + try { + await formApi.remove('EmployeeProfile', profileRecord.value.EmployeeProfileUUID as string) + ElMessage.success('已删除') + await loadProfile() + } catch { + // 错误提示已由响应拦截器统一弹出 + } +} + +// === 子表跳转 === /** UserRole 子表查看 → 角色详情 */ function goRoleDetail(row: Record): void { router.push({ path: '/system/role-detail', query: { RoleUUID: row.RoleUUID || '' } }) @@ -216,6 +380,7 @@ onMounted(async () => { // 空值场景静默显示空态(loadData 内处理),不再弹警告 watch(userUUID, () => { void loadData() + void loadProfile() }, { immediate: true }) @@ -337,24 +502,47 @@ watch(userUUID, () => { /> - + - + +
+ + + + {{ profileValue(col) }} + + + + +
+ + 点击填写 + +
+ + + +
@@ -481,6 +669,17 @@ watch(userUUID, () => { 保存 + + + @@ -504,4 +703,35 @@ watch(userUUID, () => { flex-shrink: 0; background: var(--el-fill-color-light); } + +// ===== 员工档案面板(eHRM)===== +// 卡片头右侧操作按钮(编辑/删除)推到最右 +.profile-header-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 4px; +} + +// 面板主体:加载态用 v-loading,故给最小高度避免抖动 +.profile-panel { + min-height: 72px; +} + +// 只读键值表:标签区紧凑 + 内容换行 +.profile-desc { + :deep(.el-descriptions__label) { + width: 110px; + font-weight: 500; + color: var(--el-text-color-regular); + } +} + +// 无档案空态:居中「点击填写」 +.profile-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 24px 0; +}