feat(gerp): 用户详情页员工档案也加身份证解锁
- 抽出 useIdCardUnlock(解锁态 + 校验逻辑)与 IdCardUnlockDialog(密码弹窗),档案管理页与用户详情页共用同一套解锁态 - UserDetailPanel 员工档案卡片头加「查看身份证」(已解锁显示标签);未解锁时身份证只读行整行隐藏、编辑表单禁用 + 提示 - Profile.vue 改为复用公共 composable/弹窗,去掉内联实现
This commit is contained in:
1 parent
e2006534be
commit
debc0d7745
4 files changed
+215
-89
No files matched your search
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 身份证查看密码弹窗(HR 档案管理页 / 用户详情页共用)
|
||||
*
|
||||
* 只负责收集密码并触发校验;解锁态与接口调用由父组件的 useIdCardUnlock 维护。
|
||||
* 失败(如密码错误)时弹窗保留,输入内容不清空,便于重试。
|
||||
*/
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
/** 弹窗可见性(v-model:visible) */
|
||||
visible: boolean
|
||||
/** 提交中(「验证」按钮 loading) */
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [v: boolean]
|
||||
/** 校验通过后请求解锁(携带明文密码) */
|
||||
submit: [password: string]
|
||||
}>()
|
||||
|
||||
const password = ref('')
|
||||
|
||||
// 每次打开清空输入(关闭不清,失败重试时保留)
|
||||
watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (v) password.value = ''
|
||||
},
|
||||
)
|
||||
|
||||
function onSubmit(): void {
|
||||
if (!password.value) {
|
||||
ElMessage.warning('请输入密码')
|
||||
return
|
||||
}
|
||||
emit('submit', password.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="查看身份证号"
|
||||
width="440px"
|
||||
append-to-body
|
||||
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
||||
>
|
||||
<p class="idcard-tip">
|
||||
身份证号为敏感信息,请输入当前登录账号的密码完成二次验证。
|
||||
验证通过后 10 分钟内,切换到其他员工档案也可查看。
|
||||
</p>
|
||||
<el-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="请输入当前登录账号密码"
|
||||
show-password
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="emit('update:visible', false)">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="onSubmit">验证</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.idcard-tip {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -19,10 +19,12 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import InfoGrid from '@/components/InfoGrid.vue'
|
||||
import CrudTable from '@/components/CrudTable.vue'
|
||||
import CrudFormPanel from '@/components/CrudFormPanel.vue'
|
||||
import IdCardUnlockDialog from '@/components/IdCardUnlockDialog.vue'
|
||||
import EntityPicker from '@/components/EntityPicker.vue'
|
||||
import { formApi } from '@/api/form'
|
||||
import { get } from '@/api/request'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useIdCardUnlock } from '@/composables/useIdCardUnlock'
|
||||
import { getServerScopeParams } from '@/utils/serverScope'
|
||||
import { formatUtcTime as formatTime } from '@/utils/dateTime'
|
||||
import { isColumnVisible, columnDateValueFormat } from '@/utils/column'
|
||||
@@ -213,6 +215,17 @@ const EHRM_PROFILE_PATH = '/ehrm/profile'
|
||||
*(员工=本页用户;部门用 DeptUUID 的关联标签展示为「部门名称」;联系方式子表单独成块) */
|
||||
const PROFILE_HIDDEN_FIELDS = ['EmployeeProfileUUID', 'UserUUID', 'TenantUUID', 'CompanyUUID']
|
||||
|
||||
// 身份证二次密码解锁:与档案管理页共用同一套后端解锁态(按登录用户 10 分钟共享),
|
||||
// 所以任一处解锁后,这里(含切换查看别的员工)也能直接看到身份证明文。
|
||||
const {
|
||||
unlocked: idCardUnlocked,
|
||||
dialogVisible: idCardDialogVisible,
|
||||
submitting: idCardSubmitting,
|
||||
refreshStatus: loadIdCardStatus,
|
||||
open: openIdCardUnlock,
|
||||
submit: submitIdCardUnlock,
|
||||
} = useIdCardUnlock(loadProfile)
|
||||
|
||||
const profileLoading = ref(false)
|
||||
/** 该员工的档案记录(一人一份,最多一条) */
|
||||
const profileRecord = ref<Record<string, any> | null>(null)
|
||||
@@ -243,6 +256,8 @@ const profileDisplayColumns = computed(() =>
|
||||
c.type !== 'divider' &&
|
||||
c.type !== 'html' &&
|
||||
!PROFILE_HIDDEN_FIELDS.includes(c.field) &&
|
||||
// 身份证号:未解锁时整行隐藏(后端此时也不会返回值,避免出现空行)
|
||||
(idCardUnlocked.value || c.field !== 'IdCard') &&
|
||||
// 条件字段(如「其它志愿工作」):只有条件命中才展示,与表单口径一致
|
||||
isColumnVisible(c, profileRecord.value),
|
||||
),
|
||||
@@ -250,18 +265,30 @@ const profileDisplayColumns = computed(() =>
|
||||
|
||||
/** 填写/编辑表单列:排除隐藏/主键/非表单类型,以及由本页上下文注入的 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' &&
|
||||
// 部门员工关联:按本页员工自动带出后隐藏(查不到时才显示让用户手动选)
|
||||
!(profilePickerHidden.value && c.field === 'UserRoleDepartmentUUID'),
|
||||
),
|
||||
profileColumns.value
|
||||
.filter(
|
||||
(c) =>
|
||||
c.editable !== false &&
|
||||
!c.hideInForm &&
|
||||
c.type !== 'hidden' &&
|
||||
c.type !== 'divider' &&
|
||||
c.type !== 'html' &&
|
||||
c.field !== 'EmployeeProfileUUID' &&
|
||||
c.field !== 'UserUUID' &&
|
||||
// 部门员工关联:按本页员工自动带出后隐藏(查不到时才显示让用户手动选)
|
||||
!(profilePickerHidden.value && c.field === 'UserRoleDepartmentUUID'),
|
||||
)
|
||||
.map((c) =>
|
||||
// 身份证号:未解锁时禁用输入 + 明确提示;其余字段照常可改
|
||||
!idCardUnlocked.value && c.field === 'IdCard'
|
||||
? {
|
||||
...c,
|
||||
disabled: true,
|
||||
placeholder: '需先验证密码才能查看/修改',
|
||||
tip: '身份证号需先点右上角「查看身份证」验证密码后才能查看或修改;不验证不影响修改其它字段。',
|
||||
}
|
||||
: c,
|
||||
),
|
||||
)
|
||||
|
||||
/** 只读展示值:实体标签(*_label)优先 → 选项映射 → 布尔/多选/日期 → 原值 */
|
||||
@@ -468,6 +495,8 @@ watch(
|
||||
// 列定义与 2FA 信息只依赖登录用户,挂载时拉一次
|
||||
void loadColumns()
|
||||
void loadTwoFactor()
|
||||
// 身份证解锁态也只依赖登录用户,挂载时查一次(已解锁则档案直接显示明文)
|
||||
void loadIdCardStatus()
|
||||
|
||||
// 供调用方的「编辑」按钮触发本面板的编辑抽屉
|
||||
defineExpose({ handleEdit })
|
||||
@@ -590,16 +619,21 @@ defineExpose({ handleEdit })
|
||||
<div class="detail-card-header">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>员工档案(eHRM)</span>
|
||||
<template v-if="profileRecord && canEditProfile">
|
||||
<span class="profile-header-actions">
|
||||
<span class="profile-header-actions">
|
||||
<!-- 身份证二次密码解锁:解锁态按登录用户 10 分钟共享(与档案管理页一致) -->
|
||||
<el-tag v-if="idCardUnlocked" type="success" effect="plain" size="small">身份证已解锁</el-tag>
|
||||
<el-button v-else link type="warning" size="small" @click="openIdCardUnlock">
|
||||
<el-icon><Lock /></el-icon>查看身份证
|
||||
</el-button>
|
||||
<template v-if="profileRecord && canEditProfile">
|
||||
<el-button link type="primary" size="small" @click="openProfileEdit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteProfile">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -776,6 +810,13 @@ defineExpose({ handleEdit })
|
||||
mode="drawer"
|
||||
@submit="submitProfile"
|
||||
/>
|
||||
|
||||
<!-- 身份证查看密码弹窗(解锁后自动重载档案,显示明文) -->
|
||||
<IdCardUnlockDialog
|
||||
v-model:visible="idCardDialogVisible"
|
||||
:loading="idCardSubmitting"
|
||||
@submit="submitIdCardUnlock"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ehrmIdCardApi } from '@/api/form'
|
||||
|
||||
/**
|
||||
* 身份证二次密码解锁(HR 档案管理页 / 用户详情页共用)
|
||||
*
|
||||
* 解锁态由后端按**登录用户**维护,窗口 10 分钟、与具体档案无关,
|
||||
* 所以一处解锁后切换到别的员工档案同样可查看。
|
||||
*
|
||||
* 用法:
|
||||
* const idcard = useIdCardUnlock(loadProfile)
|
||||
* onMounted(idcard.refreshStatus)
|
||||
* <IdCardUnlockDialog v-model:visible="idcard.dialogVisible.value" ... />
|
||||
*
|
||||
* @param onUnlocked 解锁成功后的回调(重新拉取数据,以拿到后端此时才返回的身份证明文)
|
||||
*/
|
||||
export function useIdCardUnlock(onUnlocked?: () => void | Promise<void>) {
|
||||
/** 当前登录用户是否处于解锁窗口内 */
|
||||
const unlocked = ref(false)
|
||||
/** 密码弹窗可见性 */
|
||||
const dialogVisible = ref(false)
|
||||
/** 提交中(弹窗「验证」按钮 loading) */
|
||||
const submitting = ref(false)
|
||||
|
||||
/** 查询后端解锁态(页面挂载时调用;已解锁则直接显示明文) */
|
||||
async function refreshStatus(): Promise<void> {
|
||||
try {
|
||||
const res = await ehrmIdCardApi.status()
|
||||
unlocked.value = !!res?.unlocked
|
||||
} catch {
|
||||
unlocked.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function open(): void {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 校验密码并开启解锁窗口;成功后关闭弹窗并回调 onUnlocked */
|
||||
async function submit(password: string): Promise<boolean> {
|
||||
if (!password) {
|
||||
ElMessage.warning('请输入密码')
|
||||
return false
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await ehrmIdCardApi.unlock(password)
|
||||
unlocked.value = true
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('验证成功,10 分钟内可查看身份证号')
|
||||
await onUnlocked?.()
|
||||
return true
|
||||
} catch {
|
||||
// 失败提示由响应拦截器统一弹出(如「密码错误」),弹窗保留供重试
|
||||
return false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { unlocked, dialogVisible, submitting, refreshStatus, open, submit }
|
||||
}
|
||||
+19
-73
@@ -9,12 +9,12 @@
|
||||
* 任意员工档案(含切换到别人)都能查看——解锁态由后端按登录用户维度维护。
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import CrudTable from '@/components/CrudTable.vue'
|
||||
import AdminBadge from '@/components/AdminBadge.vue'
|
||||
import IdCardUnlockDialog from '@/components/IdCardUnlockDialog.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { setPageTitle } from '@/utils/serverScope'
|
||||
import { ehrmIdCardApi } from '@/api/form'
|
||||
import { useIdCardUnlock } from '@/composables/useIdCardUnlock'
|
||||
import type { ColumnSchema } from '@/types/api'
|
||||
|
||||
const userStore = useUserStore()
|
||||
@@ -23,12 +23,18 @@ setPageTitle('档案管理')
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// ===== 身份证二次密码验证 =====
|
||||
// 解锁窗口(10 分钟)由后端记录,前端只负责触发解锁并在成功后刷新列表取明文
|
||||
// 解锁窗口(10 分钟)由后端按登录用户维护;解锁后刷新列表以拿到身份证明文
|
||||
const tableRef = ref()
|
||||
const idCardUnlocked = ref(false)
|
||||
const unlockDialogVisible = ref(false)
|
||||
const unlockPassword = ref('')
|
||||
const unlocking = ref(false)
|
||||
const {
|
||||
unlocked: idCardUnlocked,
|
||||
dialogVisible: idCardDialogVisible,
|
||||
submitting: idCardSubmitting,
|
||||
refreshStatus: loadIdCardStatus,
|
||||
open: openIdCardUnlock,
|
||||
submit: submitIdCardUnlock,
|
||||
} = useIdCardUnlock(async () => {
|
||||
await tableRef.value?.refresh?.()
|
||||
})
|
||||
|
||||
/**
|
||||
* 未解锁时对新增/编辑表单里身份证列做运行态覆盖:
|
||||
@@ -46,42 +52,6 @@ const columnOverrides = computed<Record<string, Partial<ColumnSchema>>>(() => {
|
||||
return overrides
|
||||
})
|
||||
|
||||
/** 查询当前登录用户是否已处于解锁窗口内(页面刷新后恢复按钮状态) */
|
||||
async function loadIdCardStatus(): Promise<void> {
|
||||
try {
|
||||
const res = await ehrmIdCardApi.status()
|
||||
idCardUnlocked.value = !!res?.unlocked
|
||||
} catch {
|
||||
idCardUnlocked.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openUnlockDialog(): void {
|
||||
unlockPassword.value = ''
|
||||
unlockDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 提交密码:成功后刷新列表(此时后端才会在 EmployeeProfile 记录里返回身份证明文) */
|
||||
async function submitUnlock(): Promise<void> {
|
||||
if (!unlockPassword.value) {
|
||||
ElMessage.warning('请输入密码')
|
||||
return
|
||||
}
|
||||
unlocking.value = true
|
||||
try {
|
||||
await ehrmIdCardApi.unlock(unlockPassword.value)
|
||||
idCardUnlocked.value = true
|
||||
unlockDialogVisible.value = false
|
||||
unlockPassword.value = ''
|
||||
ElMessage.success('验证成功,10 分钟内可查看身份证号')
|
||||
await tableRef.value?.refresh?.()
|
||||
} catch {
|
||||
// 失败提示由响应拦截器统一弹出(如「密码错误」),弹窗保留供重试
|
||||
} finally {
|
||||
unlocking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadIdCardStatus)
|
||||
</script>
|
||||
|
||||
@@ -101,34 +71,17 @@ onMounted(loadIdCardStatus)
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-tag v-if="idCardUnlocked" type="success" effect="plain">身份证已解锁</el-tag>
|
||||
<el-button v-else type="warning" plain @click="openUnlockDialog">
|
||||
<el-button v-else type="warning" plain @click="openIdCardUnlock">
|
||||
<el-icon><Lock /></el-icon>查看身份证
|
||||
</el-button>
|
||||
</template>
|
||||
</CrudTable>
|
||||
|
||||
<el-dialog
|
||||
v-model="unlockDialogVisible"
|
||||
title="查看身份证号"
|
||||
width="440px"
|
||||
append-to-body
|
||||
>
|
||||
<p class="idcard-tip">
|
||||
身份证号为敏感信息,请输入当前登录账号的密码完成二次验证。
|
||||
验证通过后 10 分钟内,切换到其他员工档案也可查看。
|
||||
</p>
|
||||
<el-input
|
||||
v-model="unlockPassword"
|
||||
type="password"
|
||||
placeholder="请输入当前登录账号密码"
|
||||
show-password
|
||||
@keyup.enter="submitUnlock"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="unlockDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="unlocking" @click="submitUnlock">验证</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<IdCardUnlockDialog
|
||||
v-model:visible="idCardDialogVisible"
|
||||
:loading="idCardSubmitting"
|
||||
@submit="submitIdCardUnlock"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -140,11 +93,4 @@ onMounted(loadIdCardStatus)
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.idcard-tip {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user