diff --git a/frontend/admin/scripts/run-playwright-cdp-e2e.mjs b/frontend/admin/scripts/run-playwright-cdp-e2e.mjs index ae981bb..57c8cd2 100644 --- a/frontend/admin/scripts/run-playwright-cdp-e2e.mjs +++ b/frontend/admin/scripts/run-playwright-cdp-e2e.mjs @@ -1607,6 +1607,9 @@ async function verifyPublicRegistration(page) { page.locator(`input[placeholder="${TEXT.confirmPasswordPlaceholder}"]`).first(), password, ) + const agreementCheckbox = page.locator('.ant-form-item').filter({ has: page.locator('input[id="agreement"]') }).locator('.ant-checkbox').first() + await forceClick(agreementCheckbox) + await expect(agreementCheckbox).toHaveClass(/ant-checkbox-checked/, { timeout: 10 * 1000 }) const registerResponsePromise = waitForResponseSafe(page, (response) => { return response.url().includes('/api/v1/auth/register') && response.request().method() === 'POST' }) @@ -1642,6 +1645,9 @@ async function verifyEmailActivationWorkflow(page) { page.locator(`input[placeholder="${TEXT.confirmPasswordPlaceholder}"]`).first(), password, ) + const agreementCheckbox = page.locator('.ant-form-item').filter({ has: page.locator('input[id="agreement"]') }).locator('.ant-checkbox').first() + await forceClick(agreementCheckbox) + await expect(agreementCheckbox).toHaveClass(/ant-checkbox-checked/, { timeout: 10 * 1000 }) const registerResponsePromise = waitForResponseSafe(page, (response) => { return response.url().includes('/api/v1/auth/register') && response.request().method() === 'POST' diff --git a/frontend/admin/src/components/common/PasswordStrengthIndicator.tsx b/frontend/admin/src/components/common/PasswordStrengthIndicator.tsx new file mode 100644 index 0000000..3e7f8ae --- /dev/null +++ b/frontend/admin/src/components/common/PasswordStrengthIndicator.tsx @@ -0,0 +1,84 @@ +/** + * PasswordStrengthIndicator - 密码强度指示器 + */ + +import { Progress } from 'antd' +import { useMemo } from 'react' + +interface PasswordStrengthIndicatorProps { + password: string +} + +function calculateStrength(password: string): { score: number; level: 'weak' | 'fair' | 'good' | 'strong' } { + if (!password) { + return { score: 0, level: 'weak' } + } + + let score = 0 + + // 长度检查 + if (password.length >= 8) score += 25 + if (password.length >= 12) score += 10 + if (password.length >= 16) score += 5 + + // 字符类型检查 + if (/[a-z]/.test(password)) score += 15 + if (/[A-Z]/.test(password)) score += 20 + if (/[0-9]/.test(password)) score += 20 + if (/[^a-zA-Z0-9]/.test(password)) score += 20 + + // 正则匹配检查 + if (/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/.test(password)) score += 5 + if (/(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])/.test(password)) score += 5 + + // 扣分项 + if (/^[a-zA-Z0-9]+$/.test(password)) score -= 10 // 纯字母数字 + if (/^[a-z]+$|^[A-Z]+$|^[0-9]+$/.test(password)) score -= 15 // 单一种类 + + // 限制范围 + score = Math.max(0, Math.min(100, score)) + + let level: 'weak' | 'fair' | 'good' | 'strong' + if (score < 30) level = 'weak' + else if (score < 60) level = 'fair' + else if (score < 80) level = 'good' + else level = 'strong' + + return { score, level } +} + +const strengthConfig = { + weak: { color: '#ff4d4f', text: '弱' }, + fair: { color: '#faad14', text: '中等' }, + good: { color: '#52c41a', text: '良好' }, + strong: { color: '#52c41a', text: '强' }, +} + +export function PasswordStrengthIndicator({ password }: PasswordStrengthIndicatorProps) { + const { score, level } = useMemo(() => calculateStrength(password), [password]) + + if (!password) { + return null + } + + const config = strengthConfig[level] + + return ( +