diff --git a/clinical-web/src/utils/password-policy.ts b/clinical-web/src/utils/password-policy.ts
new file mode 100644
index 0000000..69670bc
--- /dev/null
+++ b/clinical-web/src/utils/password-policy.ts
@@ -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
+}
diff --git a/clinical-web/src/views/management/users/components/PasswordPolicyHints.vue b/clinical-web/src/views/management/users/components/PasswordPolicyHints.vue
new file mode 100644
index 0000000..400f5cd
--- /dev/null
+++ b/clinical-web/src/views/management/users/components/PasswordPolicyHints.vue
@@ -0,0 +1,49 @@
+
+
+
+
+ -
+
+ {{ checks[rule.key] ? '✓' : '○' }}
+
+ {{ rule.label }}
+
+
+
+
+
diff --git a/clinical-web/src/views/management/users/components/UserEditorDialog.vue b/clinical-web/src/views/management/users/components/UserEditorDialog.vue
index 4d1009a..87579f8 100644
--- a/clinical-web/src/views/management/users/components/UserEditorDialog.vue
+++ b/clinical-web/src/views/management/users/components/UserEditorDialog.vue
@@ -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
+ 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="请输入初始密码"
/>
+