feat(users): 统一员工密码策略并修复新增员工表单残留
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 员工账号密码策略。
|
||||
*
|
||||
* 组织架构的「新增员工」与「重置密码」共用同一套规则,
|
||||
* 避免两处各写一份导致口径分叉:长度不少于 12 位,且同时包含大写和小写字母。
|
||||
*/
|
||||
|
||||
/** 密码最小长度 */
|
||||
export const PASSWORD_MIN_LENGTH = 12
|
||||
|
||||
export interface PasswordPolicyChecks {
|
||||
/** 长度是否达到最小值 */
|
||||
minLength: boolean
|
||||
/** 是否包含大写字母 */
|
||||
hasUppercase: boolean
|
||||
/** 是否包含小写字母 */
|
||||
hasLowercase: boolean
|
||||
}
|
||||
|
||||
export interface PasswordPolicyRule {
|
||||
key: keyof PasswordPolicyChecks
|
||||
label: string
|
||||
}
|
||||
|
||||
/** 规则清单,供表单实时展示校验进度(顺序即提示顺序) */
|
||||
export const PASSWORD_POLICY_RULES: PasswordPolicyRule[] = [
|
||||
{ key: 'minLength', label: `长度不少于 ${PASSWORD_MIN_LENGTH} 位` },
|
||||
{ key: 'hasUppercase', label: '包含大写字母 A-Z' },
|
||||
{ key: 'hasLowercase', label: '包含小写字母 a-z' },
|
||||
]
|
||||
|
||||
/** 逐条计算密码是否满足策略,返回每个规则的通过情况 */
|
||||
export function checkPasswordPolicy(password: string): PasswordPolicyChecks {
|
||||
return {
|
||||
minLength: password.length >= PASSWORD_MIN_LENGTH,
|
||||
hasUppercase: /[A-Z]/.test(password),
|
||||
hasLowercase: /[a-z]/.test(password),
|
||||
}
|
||||
}
|
||||
|
||||
/** 密码是否完全满足策略 */
|
||||
export function isPasswordPolicySatisfied(password: string): boolean {
|
||||
const checks = checkPasswordPolicy(password)
|
||||
return checks.minLength && checks.hasUppercase && checks.hasLowercase
|
||||
}
|
||||
|
||||
/** 返回第一条未满足的规则提示,全部满足时返回 null */
|
||||
export function firstPasswordPolicyIssue(password: string): string | null {
|
||||
if (!password) return '请输入密码'
|
||||
const checks = checkPasswordPolicy(password)
|
||||
if (!checks.minLength) return `密码长度不少于 ${PASSWORD_MIN_LENGTH} 位`
|
||||
if (!checks.hasUppercase || !checks.hasLowercase) return '密码必须同时包含大写和小写字母'
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { PASSWORD_POLICY_RULES, checkPasswordPolicy } from '@/utils/password-policy'
|
||||
|
||||
const props = defineProps<{ password: string }>()
|
||||
|
||||
const checks = computed(() => checkPasswordPolicy(props.password))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="password-hints" aria-label="密码规则">
|
||||
<li v-for="rule in PASSWORD_POLICY_RULES" :key="rule.key" :class="{ 'is-ok': checks[rule.key] }">
|
||||
<span class="password-hints__mark" aria-hidden="true">
|
||||
{{ checks[rule.key] ? '✓' : '○' }}
|
||||
</span>
|
||||
<span>{{ rule.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.password-hints {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--mut);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.password-hints li {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-hints__mark {
|
||||
color: #b3c3ca;
|
||||
}
|
||||
|
||||
.password-hints li.is-ok,
|
||||
.password-hints li.is-ok .password-hints__mark {
|
||||
color: var(--brand);
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,13 @@
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
import type { CampusRecord, DepartmentRecord, RoleRecord } from '@/api/management/types'
|
||||
import {
|
||||
PASSWORD_MIN_LENGTH,
|
||||
firstPasswordPolicyIssue,
|
||||
isPasswordPolicySatisfied,
|
||||
} from '@/utils/password-policy'
|
||||
|
||||
import PasswordPolicyHints from './PasswordPolicyHints.vue'
|
||||
import type { UserEditorForm } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -51,16 +57,22 @@ function createEmptyForm(): UserEditorForm {
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(
|
||||
form,
|
||||
props.initial
|
||||
? {
|
||||
...props.initial,
|
||||
roleIds: [...props.initial.roleIds],
|
||||
roleLabels: [...(props.initial.roleLabels ?? [])],
|
||||
}
|
||||
: createEmptyForm(),
|
||||
)
|
||||
const next: UserEditorForm = props.initial
|
||||
? {
|
||||
...createEmptyForm(),
|
||||
...props.initial,
|
||||
roleIds: [...props.initial.roleIds],
|
||||
roleLabels: [...(props.initial.roleLabels ?? [])],
|
||||
}
|
||||
: createEmptyForm()
|
||||
|
||||
// 先清空全部字段再写入:编辑态会带入 id / employeeId / version / phoneMasked 等键,
|
||||
// 若只做增量合并,这些键会残留到「新增员工」表单,使新增被误判为编辑态并提交成更新。
|
||||
const mutableForm = form as unknown as Record<string, unknown>
|
||||
for (const key of Object.keys(mutableForm)) {
|
||||
delete mutableForm[key]
|
||||
}
|
||||
Object.assign(form, next)
|
||||
error.message = ''
|
||||
}
|
||||
|
||||
@@ -98,8 +110,8 @@ function submit() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEditing.value && form.password.length < 8) {
|
||||
error.message = '初始密码至少需要 8 位'
|
||||
if (!isEditing.value && !isPasswordPolicySatisfied(form.password)) {
|
||||
error.message = firstPasswordPolicyIssue(form.password) ?? '初始密码不符合安全要求'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -201,8 +213,10 @@ watch(
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:minlength="PASSWORD_MIN_LENGTH"
|
||||
placeholder="请输入初始密码"
|
||||
/>
|
||||
<PasswordPolicyHints :password="form.password" />
|
||||
</label>
|
||||
<label>
|
||||
<span>工号 <em>*</em></span>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue'
|
||||
|
||||
import {
|
||||
PASSWORD_MIN_LENGTH,
|
||||
firstPasswordPolicyIssue,
|
||||
isPasswordPolicySatisfied,
|
||||
} from '@/utils/password-policy'
|
||||
|
||||
import PasswordPolicyHints from './PasswordPolicyHints.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
userName: string
|
||||
@@ -33,8 +41,8 @@ function resetForm() {
|
||||
function submit() {
|
||||
error.message = ''
|
||||
|
||||
if (form.newPassword.length < 12) {
|
||||
error.message = '新密码至少需要 12 位'
|
||||
if (!isPasswordPolicySatisfied(form.newPassword)) {
|
||||
error.message = firstPasswordPolicyIssue(form.newPassword) ?? '新密码不符合安全要求'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,9 +105,10 @@ watch(
|
||||
v-model="form.newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
minlength="12"
|
||||
placeholder="至少 12 位"
|
||||
:minlength="PASSWORD_MIN_LENGTH"
|
||||
placeholder="至少 12 位,含大小写字母"
|
||||
/>
|
||||
<PasswordPolicyHints :password="form.newPassword" />
|
||||
</label>
|
||||
<label>
|
||||
<span>确认新密码 <em>*</em></span>
|
||||
@@ -107,7 +116,7 @@ watch(
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
minlength="12"
|
||||
:minlength="PASSWORD_MIN_LENGTH"
|
||||
placeholder="再次输入新密码"
|
||||
/>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user