feat(users): 员工表单对齐原型并优化密码策略与错误文案
- 新增胶囊开关组件,账号状态与强制改密改用开关 - 角色由多选改为互斥单选,编辑态多角色给出收敛提示 - 密码策略增加特殊字符要求,规则提示改为短标签横排 - 移除邮箱字段及相关脱敏、清除逻辑 - 接口错误经 toUserMessage 中文化,透传后端校验原因
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 胶囊开关(对齐原型 `.switch`:36×20 胶囊 + 圆形滑块,开启为 --ok)。
|
||||
* 用 button + role="switch" 而不是 checkbox,避免各浏览器对 checkbox 的默认外观差异。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
disabled?: boolean
|
||||
/** 供读屏使用的名称;视觉标签由调用方提供 */
|
||||
label?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
function toggle() {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', !props.modelValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="switch"
|
||||
:class="{ 'is-on': modelValue }"
|
||||
role="switch"
|
||||
:aria-checked="modelValue"
|
||||
:aria-label="label"
|
||||
:disabled="disabled"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="switch__thumb" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.switch {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
background: #cbd8de;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch.is-on {
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.switch:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.switch:focus-visible {
|
||||
outline: 2px solid var(--brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.switch__thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.switch.is-on .switch__thumb {
|
||||
left: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,5 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import type { ApiResponse } from '@/types/common'
|
||||
|
||||
export class ApiResponseError extends Error {
|
||||
@@ -12,6 +14,76 @@ export class ApiResponseError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端字段校验文案里的字段名 → 界面用词 */
|
||||
const BACKEND_FIELD_LABELS: Record<string, string> = {
|
||||
password: '密码',
|
||||
newPassword: '新密码',
|
||||
username: '登录账号',
|
||||
displayName: '姓名',
|
||||
employeeNo: '工号',
|
||||
position: '岗位',
|
||||
professionalTitle: '职称',
|
||||
phone: '手机号',
|
||||
reason: '重置原因',
|
||||
campusId: '院区',
|
||||
roleIds: '角色',
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端的字段校验是 Spring Validation,message 形如
|
||||
* `password: size must be between 8 and 128` / `displayName: must not be blank`(英文技术文案)。
|
||||
* 直接甩给用户不友好,这里只对**已知格式**做中文化;其它文案一律原样返回,不猜。
|
||||
*/
|
||||
function localizeBackendMessage(message: string): string {
|
||||
const text = message.trim()
|
||||
|
||||
const sizeRange = /^(\w+):\s*size must be between (\d+) and (\d+)$/.exec(text)
|
||||
if (sizeRange) {
|
||||
const [, field, min, max] = sizeRange
|
||||
return `${BACKEND_FIELD_LABELS[field] ?? field}长度需在 ${min}–${max} 位之间`
|
||||
}
|
||||
|
||||
const notBlank = /^(\w+):\s*must not be blank$/.exec(text)
|
||||
if (notBlank) {
|
||||
return `请填写${BACKEND_FIELD_LABELS[notBlank[1]] ?? notBlank[1]}`
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* 把接口错误转成可以直接展示给用户的文案。
|
||||
*
|
||||
* 两处来源都要覆盖:
|
||||
* - 业务错误码(HTTP 200 + code≠0)由 `unwrapApiResponse` 抛成 `ApiResponseError`;
|
||||
* - 参数校验失败是 **HTTP 400**,axios 抛 `AxiosError`,文案在 `response.data.message` 里。
|
||||
* 兜底才用调用方给的 fallback —— 不要把后端给的原因吞掉换成"操作失败,请稍后重试"。
|
||||
*/
|
||||
export function toUserMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof ApiResponseError) {
|
||||
return error.message ? localizeBackendMessage(error.message) : fallback
|
||||
}
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const backendMessage = (error.response?.data as { message?: unknown } | undefined)?.message
|
||||
if (typeof backendMessage === 'string' && backendMessage) {
|
||||
return localizeBackendMessage(backendMessage)
|
||||
}
|
||||
|
||||
if (!error.response) {
|
||||
return '网络异常,请检查网络后重试'
|
||||
}
|
||||
|
||||
return `${fallback}(HTTP ${error.response.status})`
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function isSuccessCode(code: number | string) {
|
||||
return code === 0 || code === '0'
|
||||
}
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
* 员工账号密码策略。
|
||||
*
|
||||
* 组织架构的「新增员工」与「重置密码」共用同一套规则,
|
||||
* 避免两处各写一份导致口径分叉:长度不少于 12 位,且同时包含大写和小写字母。
|
||||
* 避免两处各写一份导致口径分叉:长度不少于 12 位,且同时包含大写字母、小写字母、特殊字符。
|
||||
*/
|
||||
|
||||
/** 密码最小长度 */
|
||||
export const PASSWORD_MIN_LENGTH = 12
|
||||
|
||||
/**
|
||||
* 特殊字符:常见 ASCII 可见符号。
|
||||
* 用显式符号集而不是 `[^A-Za-z0-9]`,避免把中文、emoji、空白也算成「特殊字符」。
|
||||
*/
|
||||
const SPECIAL_CHAR_PATTERN = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~`]/
|
||||
|
||||
export interface PasswordPolicyChecks {
|
||||
/** 长度是否达到最小值 */
|
||||
minLength: boolean
|
||||
@@ -15,6 +21,8 @@ export interface PasswordPolicyChecks {
|
||||
hasUppercase: boolean
|
||||
/** 是否包含小写字母 */
|
||||
hasLowercase: boolean
|
||||
/** 是否包含特殊字符 */
|
||||
hasSpecialChar: boolean
|
||||
}
|
||||
|
||||
export interface PasswordPolicyRule {
|
||||
@@ -22,11 +30,12 @@ export interface PasswordPolicyRule {
|
||||
label: string
|
||||
}
|
||||
|
||||
/** 规则清单,供表单实时展示校验进度(顺序即提示顺序) */
|
||||
/** 规则清单,供表单实时展示校验进度(顺序即提示顺序;文案取短版,保证半列宽也能横排一行) */
|
||||
export const PASSWORD_POLICY_RULES: PasswordPolicyRule[] = [
|
||||
{ key: 'minLength', label: `长度不少于 ${PASSWORD_MIN_LENGTH} 位` },
|
||||
{ key: 'hasUppercase', label: '包含大写字母 A-Z' },
|
||||
{ key: 'hasLowercase', label: '包含小写字母 a-z' },
|
||||
{ key: 'minLength', label: `≥ ${PASSWORD_MIN_LENGTH} 位` },
|
||||
{ key: 'hasUppercase', label: '含大写' },
|
||||
{ key: 'hasLowercase', label: '含小写' },
|
||||
{ key: 'hasSpecialChar', label: '含符号' },
|
||||
]
|
||||
|
||||
/** 逐条计算密码是否满足策略,返回每个规则的通过情况 */
|
||||
@@ -35,13 +44,14 @@ export function checkPasswordPolicy(password: string): PasswordPolicyChecks {
|
||||
minLength: password.length >= PASSWORD_MIN_LENGTH,
|
||||
hasUppercase: /[A-Z]/.test(password),
|
||||
hasLowercase: /[a-z]/.test(password),
|
||||
hasSpecialChar: SPECIAL_CHAR_PATTERN.test(password),
|
||||
}
|
||||
}
|
||||
|
||||
/** 密码是否完全满足策略 */
|
||||
export function isPasswordPolicySatisfied(password: string): boolean {
|
||||
const checks = checkPasswordPolicy(password)
|
||||
return checks.minLength && checks.hasUppercase && checks.hasLowercase
|
||||
return checks.minLength && checks.hasUppercase && checks.hasLowercase && checks.hasSpecialChar
|
||||
}
|
||||
|
||||
/** 返回第一条未满足的规则提示,全部满足时返回 null */
|
||||
@@ -50,5 +60,6 @@ export function firstPasswordPolicyIssue(password: string): string | null {
|
||||
const checks = checkPasswordPolicy(password)
|
||||
if (!checks.minLength) return `密码长度不少于 ${PASSWORD_MIN_LENGTH} 位`
|
||||
if (!checks.hasUppercase || !checks.hasLowercase) return '密码必须同时包含大写和小写字母'
|
||||
if (!checks.hasSpecialChar) return '密码必须包含特殊字符'
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ const checks = computed(() => checkPasswordPolicy(props.password))
|
||||
|
||||
<style scoped>
|
||||
.password-hints {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 14px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--mut);
|
||||
@@ -34,8 +35,9 @@ const checks = computed(() => checkPasswordPolicy(props.password))
|
||||
|
||||
.password-hints li {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.password-hints__mark {
|
||||
@@ -44,6 +46,6 @@ const checks = computed(() => checkPasswordPolicy(props.password))
|
||||
|
||||
.password-hints li.is-ok,
|
||||
.password-hints li.is-ok .password-hints__mark {
|
||||
color: var(--brand);
|
||||
color: var(--ok);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
import type { CampusRecord, DepartmentRecord, RoleRecord } from '@/api/management/types'
|
||||
import ToggleSwitch from '@/components/ToggleSwitch.vue'
|
||||
import {
|
||||
PASSWORD_MIN_LENGTH,
|
||||
firstPasswordPolicyIssue,
|
||||
@@ -36,6 +37,41 @@ const availableDepartments = computed(() =>
|
||||
props.departments.filter((department) => !form.campusId || department.campusId === form.campusId),
|
||||
)
|
||||
|
||||
/**
|
||||
* 账号状态用两态胶囊开关(对齐原型),但数据模型里还有后端下发的 LOCKED。
|
||||
* get 只在 ENABLED 时置开;用户不碰开关时 set 不会被调用,LOCKED 原样保留提交,
|
||||
* 避免"编辑一个已锁定员工顺手把它改回启用"。
|
||||
*/
|
||||
const statusEnabled = computed({
|
||||
get: () => form.status === 'enabled',
|
||||
set: (value: boolean) => {
|
||||
form.status = value ? 'enabled' : 'disabled'
|
||||
},
|
||||
})
|
||||
|
||||
/** 表单里该字段是可选的,开关组件要求严格 boolean,这里做一层桥接 */
|
||||
const forcePasswordChange = computed({
|
||||
get: () => form.passwordChangeRequired ?? false,
|
||||
set: (value: boolean) => {
|
||||
form.passwordChangeRequired = value
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* 角色互斥单选(对齐原型 `type:'radio'`)。
|
||||
* 接口 `PUT /v1/users/{id}/roles` 收的是 `roleIds` 数组,这里用「数组首元素」桥接,
|
||||
* 单选后数组长度恒为 0 或 1,API 层无需改动。
|
||||
*/
|
||||
const selectedRoleId = computed({
|
||||
get: () => form.roleIds[0] ?? '',
|
||||
set: (value: string) => {
|
||||
form.roleIds = value ? [value] : []
|
||||
},
|
||||
})
|
||||
|
||||
/** 编辑态且原本绑了多个角色:单选保存会收敛成一个,界面上得说清楚,避免静默丢角色 */
|
||||
const hadMultipleRoles = computed(() => isEditing.value && form.roleIds.length > 1)
|
||||
|
||||
function createEmptyForm(): UserEditorForm {
|
||||
return {
|
||||
username: '',
|
||||
@@ -45,14 +81,12 @@ function createEmptyForm(): UserEditorForm {
|
||||
position: '',
|
||||
professionalTitle: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
campusId: props.defaultCampusId ?? props.campuses[0]?.id ?? '',
|
||||
departmentId: props.defaultDepartmentId ?? '',
|
||||
status: 'enabled',
|
||||
roleIds: [],
|
||||
passwordChangeRequired: true,
|
||||
clearPhone: false,
|
||||
clearEmail: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +121,6 @@ function markPhoneEdited() {
|
||||
form.clearPhone = !form.phone
|
||||
}
|
||||
|
||||
function markEmailEdited() {
|
||||
form.emailMasked = false
|
||||
form.clearEmail = !form.email
|
||||
}
|
||||
|
||||
function submit() {
|
||||
error.message = ''
|
||||
|
||||
@@ -140,7 +169,6 @@ function submit() {
|
||||
displayName: form.displayName.trim(),
|
||||
employeeNo: form.employeeNo.trim(),
|
||||
phone: form.phone.trim(),
|
||||
email: form.email.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,29 +223,10 @@ watch(
|
||||
<span>系统用户 ID</span>
|
||||
<input :value="isEditing ? (form.employeeId ?? '—') : '保存后自动生成'" disabled />
|
||||
</label>
|
||||
<label v-if="!isEditing">
|
||||
<span>登录账号 <em>*</em></span>
|
||||
<input v-model="form.username" autocomplete="username" placeholder="如:zhangsan" />
|
||||
</label>
|
||||
<label v-else>
|
||||
<span>登录账号</span>
|
||||
<input :value="form.username" disabled />
|
||||
</label>
|
||||
<label>
|
||||
<span>姓名 <em>*</em></span>
|
||||
<input v-model="form.displayName" autocomplete="name" placeholder="请输入姓名" />
|
||||
</label>
|
||||
<label v-if="!isEditing">
|
||||
<span>初始密码 <em>*</em></span>
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:minlength="PASSWORD_MIN_LENGTH"
|
||||
placeholder="请输入初始密码"
|
||||
/>
|
||||
<PasswordPolicyHints :password="form.password" />
|
||||
</label>
|
||||
<label>
|
||||
<span>工号 <em>*</em></span>
|
||||
<input v-model="form.employeeNo" :disabled="isEditing" placeholder="请输入工号" />
|
||||
@@ -241,16 +250,6 @@ watch(
|
||||
@input="markPhoneEdited"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>邮箱</span>
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
placeholder="请输入邮箱"
|
||||
@input="markEmailEdited"
|
||||
/>
|
||||
</label>
|
||||
<div class="form-section">② 角色(权限和数据范围由角色统一配置)</div>
|
||||
<label>
|
||||
<span>院区 <em>*</em></span>
|
||||
@@ -274,17 +273,23 @@ watch(
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="canAssignRoles" class="form-grid__full">
|
||||
<span>角色(可多选)</span>
|
||||
<select v-model="form.roleIds" multiple size="4">
|
||||
<option v-for="role in roles" :key="role.id" :value="role.id">
|
||||
{{ role.name }}({{ role.code }}){{
|
||||
role.status === 'DISABLED' ? '(已停用)' : ''
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
<small>多个角色的菜单、操作与数据权限按系统规则合并。</small>
|
||||
</label>
|
||||
<div v-if="canAssignRoles" class="form-grid__full role-field">
|
||||
<span>角色</span>
|
||||
<div class="role-options">
|
||||
<label v-for="role in roles" :key="role.id" class="role-option">
|
||||
<input v-model="selectedRoleId" type="radio" name="user-role" :value="role.id" />
|
||||
<span>
|
||||
{{ role.name }}({{ role.code }}){{
|
||||
role.status === 'DISABLED' ? '(已停用)' : ''
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<small v-if="hadMultipleRoles" class="role-warning">
|
||||
该员工当前绑定 {{ form.roleIds.length }} 个角色,改为互斥单选后保存将只保留所选角色。
|
||||
</small>
|
||||
<small v-else>角色的菜单、操作与数据权限互斥,一个员工只能绑定一个角色。</small>
|
||||
</div>
|
||||
<label v-else class="form-grid__full">
|
||||
<span>已绑定角色</span>
|
||||
<input :value="form.roleLabels?.join('、') || '未分配角色'" disabled />
|
||||
@@ -292,25 +297,42 @@ watch(
|
||||
</label>
|
||||
<div class="form-section">③ 账号与安全</div>
|
||||
<label>
|
||||
<span>状态</span>
|
||||
<select v-model="form.status">
|
||||
<option value="enabled">启用</option>
|
||||
<option value="disabled">停用</option>
|
||||
<option value="locked">锁定</option>
|
||||
</select>
|
||||
<span>登录账号 <em>*</em></span>
|
||||
<input
|
||||
v-if="!isEditing"
|
||||
v-model="form.username"
|
||||
autocomplete="username"
|
||||
placeholder="如:zhangsan"
|
||||
/>
|
||||
<input v-else :value="form.username" disabled />
|
||||
</label>
|
||||
<label v-if="!isEditing" class="checkbox-field">
|
||||
<input v-model="form.passwordChangeRequired" type="checkbox" />
|
||||
<span>首次登录强制修改密码</span>
|
||||
<label v-if="!isEditing">
|
||||
<span>初始密码 <em>*</em></span>
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:minlength="PASSWORD_MIN_LENGTH"
|
||||
placeholder="请输入初始密码"
|
||||
/>
|
||||
<PasswordPolicyHints :password="form.password" />
|
||||
</label>
|
||||
<label v-else>
|
||||
<label>
|
||||
<span>首次登录强制修改密码</span>
|
||||
<input :value="form.passwordChangeRequired ? '是' : '否'" disabled />
|
||||
<ToggleSwitch
|
||||
v-model="forcePasswordChange"
|
||||
:disabled="isEditing"
|
||||
label="首次登录强制修改密码"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>账号状态</span>
|
||||
<ToggleSwitch v-model="statusEnabled" label="账号状态" />
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="error.message" class="form-error">{{ error.message }}</p>
|
||||
<p class="form-tip">
|
||||
接口返回的手机号和邮箱可能已脱敏,未重新填写的脱敏字段不会提交;清空已保存联系方式会明确提交清除操作。
|
||||
接口返回的手机号可能已脱敏,未重新填写的脱敏字段不会提交;清空已保存的联系方式会明确提交清除操作。
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -444,8 +466,51 @@ watch(
|
||||
background: #f5f8f9;
|
||||
}
|
||||
|
||||
.form-grid select[multiple] {
|
||||
min-height: 94px;
|
||||
.role-field {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.role-field > span {
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.role-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 16px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.role-field .role-option {
|
||||
display: flex;
|
||||
/* 必须显式声明:.form-grid label 的 flex-direction:column 同样命中这里的 label,
|
||||
只写 display:flex 的话方向仍是 column,radio 和文字会各占一行 */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.role-field .role-option > span {
|
||||
color: var(--ink);
|
||||
font-size: 12.5px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.role-field .role-option input {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
accent-color: var(--brand);
|
||||
}
|
||||
|
||||
.role-field .role-warning {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.form-grid small {
|
||||
@@ -455,18 +520,6 @@ watch(
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.checkbox-field {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: 8px !important;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.checkbox-field input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
margin: 12px 0 0;
|
||||
color: var(--err);
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
} from '@/api/management/types'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
import { toUserMessage } from '@/utils/api-response'
|
||||
|
||||
import OrganizationNodeDialog, {
|
||||
type OrganizationNodeSave,
|
||||
@@ -258,7 +259,6 @@ function mapUserRow(
|
||||
position: user.position,
|
||||
professionalTitle: user.professionalTitle,
|
||||
phone: user.phone ?? '',
|
||||
email: user.email ?? '',
|
||||
campus: campusNames.get(user.campusId ?? '') ?? user.campusName ?? '未指定院区',
|
||||
campusId: user.campusId,
|
||||
department: departmentNames.get(user.departmentId ?? '') ?? user.department ?? '未指定科室',
|
||||
@@ -426,8 +426,6 @@ async function editUser(row: UserTableRow) {
|
||||
professionalTitle: user.professionalTitle ?? '',
|
||||
phone: user.phone ?? '',
|
||||
phoneMasked: masked(user.phone),
|
||||
email: user.email ?? '',
|
||||
emailMasked: masked(user.email),
|
||||
campusId: user.campusId ?? row.campusId ?? '',
|
||||
departmentId: user.departmentId ?? row.departmentId ?? '',
|
||||
status: user.status,
|
||||
@@ -435,12 +433,11 @@ async function editUser(row: UserTableRow) {
|
||||
roleLabels: user.roleNames ?? row.roleNames ?? [],
|
||||
passwordChangeRequired: user.passwordChangeRequired ?? false,
|
||||
clearPhone: false,
|
||||
clearEmail: false,
|
||||
version: user.version,
|
||||
}
|
||||
editorVisible.value = true
|
||||
} catch {
|
||||
ElMessage.error('员工详情加载失败,请稍后重试')
|
||||
} catch (error) {
|
||||
ElMessage.error(toUserMessage(error, '员工详情加载失败,请稍后重试'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,8 +465,8 @@ async function handlePasswordReset(payload: {
|
||||
passwordResetVisible.value = false
|
||||
passwordResetTarget.value = null
|
||||
ElMessage.success(`员工“${target.name}”的密码已重置`)
|
||||
} catch {
|
||||
ElMessage.error('密码重置失败,请稍后重试')
|
||||
} catch (error) {
|
||||
ElMessage.error(toUserMessage(error, '密码重置失败,请稍后重试'))
|
||||
} finally {
|
||||
passwordResetSaving.value = false
|
||||
}
|
||||
@@ -538,7 +535,6 @@ async function saveUser(form: UserEditorForm) {
|
||||
position: form.position,
|
||||
professionalTitle: form.professionalTitle,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
campus: campuses.value.find((campus) => campus.id === form.campusId)?.name ?? '未指定院区',
|
||||
campusId: form.campusId,
|
||||
department:
|
||||
@@ -570,11 +566,6 @@ async function saveUser(form: UserEditorForm) {
|
||||
: form.clearPhone
|
||||
? { clearPhone: true }
|
||||
: { phone: form.phone || undefined }),
|
||||
...(form.emailMasked
|
||||
? {}
|
||||
: form.clearEmail
|
||||
? { clearEmail: true }
|
||||
: { email: form.email || undefined }),
|
||||
campusId: form.campusId,
|
||||
departmentId: form.departmentId || null,
|
||||
status: toBackendUserStatus(form.status),
|
||||
@@ -614,8 +605,10 @@ async function saveUser(form: UserEditorForm) {
|
||||
} else {
|
||||
ElMessage.success(form.id ? '员工资料已更新' : '员工已创建')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error(form.id ? '员工更新失败,请稍后重试' : '员工创建失败,请稍后重试')
|
||||
} catch (error) {
|
||||
ElMessage.error(
|
||||
toUserMessage(error, form.id ? '员工更新失败,请稍后重试' : '员工创建失败,请稍后重试'),
|
||||
)
|
||||
} finally {
|
||||
editorSaving.value = false
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface UserTableRow {
|
||||
position?: string | null
|
||||
professionalTitle?: string | null
|
||||
phone?: string
|
||||
email?: string
|
||||
campus: string
|
||||
campusId?: string | null
|
||||
department: string
|
||||
@@ -34,8 +33,6 @@ export interface UserEditorForm {
|
||||
professionalTitle: string
|
||||
phone: string
|
||||
phoneMasked?: boolean
|
||||
email: string
|
||||
emailMasked?: boolean
|
||||
campusId: string
|
||||
departmentId: string
|
||||
status: UserStatus
|
||||
@@ -43,6 +40,5 @@ export interface UserEditorForm {
|
||||
roleLabels?: string[]
|
||||
passwordChangeRequired?: boolean
|
||||
clearPhone?: boolean
|
||||
clearEmail?: boolean
|
||||
version?: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user