diff --git a/web/package-lock.json b/web/package-lock.json
index e1292d5d..9407dc46 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -16,6 +16,7 @@
"lucide-react": "^0.552.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-password-checklist": "^1.8.1",
"recharts": "^2.15.2",
"swr": "^2.2.5",
"tailwind-merge": "^3.3.1",
@@ -6982,6 +6983,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
+ "node_modules/react-password-checklist": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmmirror.com/react-password-checklist/-/react-password-checklist-1.8.1.tgz",
+ "integrity": "sha512-QHIU/OejxoH4/cIfYLHaHLb+yYc8mtL0Vr4HTmULxQg3ZNdI9Ni/yYf7pwLBgsUh4sseKCV/GzzYHWpHqejTGw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">16.0.0-alpha || >17.0.0-alpha || >18.0.0-alpha"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
diff --git a/web/package.json b/web/package.json
index 53c6f31e..0825e2c1 100644
--- a/web/package.json
+++ b/web/package.json
@@ -22,6 +22,7 @@
"lucide-react": "^0.552.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-password-checklist": "^1.8.1",
"recharts": "^2.15.2",
"swr": "^2.2.5",
"tailwind-merge": "^3.3.1",
diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx
index 1c2eae74..abf63959 100644
--- a/web/src/components/LoginPage.tsx
+++ b/web/src/components/LoginPage.tsx
@@ -3,6 +3,8 @@ import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
import HeaderBar from './landing/HeaderBar'
+import { Eye, EyeOff } from 'lucide-react'
+import { Input } from './ui/input'
export function LoginPage() {
const { language } = useLanguage()
@@ -10,6 +12,7 @@ export function LoginPage() {
const [step, setStep] = useState<'login' | 'otp'>('login')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
const [otpCode, setOtpCode] = useState('')
const [userID, setUserID] = useState('')
const [error, setError] = useState('')
@@ -172,16 +175,10 @@ export function LoginPage() {
>
{t('email', language)}
- setEmail(e.target.value)}
- className="w-full px-3 py-2 rounded"
- style={{
- background: 'var(--brand-black)',
- border: '1px solid var(--panel-border)',
- color: 'var(--brand-light-gray)',
- }}
placeholder={t('emailPlaceholder', language)}
required
/>
@@ -194,19 +191,26 @@ export function LoginPage() {
>
{t('password', language)}
- setPassword(e.target.value)}
- className="w-full px-3 py-2 rounded"
- style={{
- background: 'var(--brand-black)',
- border: '1px solid var(--panel-border)',
- color: 'var(--brand-light-gray)',
- }}
- placeholder={t('passwordPlaceholder', language)}
- required
- />
+
+ setPassword(e.target.value)}
+ className="pr-10"
+ placeholder={t('passwordPlaceholder', language)}
+ required
+ />
+
+
+
+ {/* 密码规则清单(通过才允许提交) */}
+
+
+ {t('passwordRequirements', language)}
+
+
setPasswordValid(isValid)}
/>
@@ -256,7 +307,9 @@ export function RegisterPage() {
)
}
+
+// 本地密码强度校验(与 UI 规则一致)
+function isStrongPassword(pwd: string): boolean {
+ if (!pwd || pwd.length < 8) return false
+ const hasUpper = /[A-Z]/.test(pwd)
+ const hasLower = /[a-z]/.test(pwd)
+ const hasNumber = /\d/.test(pwd)
+ const hasSpecial = /[@#$%!&*?]/.test(pwd)
+ return hasUpper && hasLower && hasNumber && hasSpecial
+}
diff --git a/web/src/components/ResetPasswordPage.tsx b/web/src/components/ResetPasswordPage.tsx
index 95e9918e..6cf2cef5 100644
--- a/web/src/components/ResetPasswordPage.tsx
+++ b/web/src/components/ResetPasswordPage.tsx
@@ -4,6 +4,8 @@ import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
import { Header } from './Header'
import { ArrowLeft, KeyRound, Eye, EyeOff } from 'lucide-react'
+import PasswordChecklist from 'react-password-checklist'
+import { Input } from './ui/input'
export function ResetPasswordPage() {
const { language } = useLanguage()
@@ -17,6 +19,7 @@ export function ResetPasswordPage() {
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
+ const [passwordValid, setPasswordValid] = useState(false)
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault()
@@ -112,16 +115,10 @@ export function ResetPasswordPage() {
>
{t('email', language)}
- setEmail(e.target.value)}
- className="w-full px-3 py-2 rounded"
- style={{
- background: '#0B0E11',
- border: '1px solid #2B3139',
- color: '#EAECEF',
- }}
placeholder={t('emailPlaceholder', language)}
required
/>
@@ -135,24 +132,20 @@ export function ResetPasswordPage() {
{t('newPassword', language)}
-
setNewPassword(e.target.value)}
- className="w-full px-3 py-2 pr-10 rounded"
- style={{
- background: '#0B0E11',
- border: '1px solid #2B3139',
- color: '#EAECEF',
- }}
+ className="pr-10"
placeholder={t('newPasswordPlaceholder', language)}
required
- minLength={6}
/>
e.preventDefault()}
onClick={() => setShowPassword(!showPassword)}
- className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300"
+ className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
+ style={{ color: 'var(--text-secondary)' }}
>
{showPassword ? (
@@ -171,26 +164,22 @@ export function ResetPasswordPage() {
{t('confirmPassword', language)}
- setConfirmPassword(e.target.value)}
- className="w-full px-3 py-2 pr-10 rounded"
- style={{
- background: '#0B0E11',
- border: '1px solid #2B3139',
- color: '#EAECEF',
- }}
+ className="pr-10"
placeholder={t('confirmPasswordPlaceholder', language)}
required
- minLength={6}
/>
e.preventDefault()}
onClick={() =>
setShowConfirmPassword(!showConfirmPassword)
}
- className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300"
+ className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
+ style={{ color: 'var(--text-secondary)' }}
>
{showConfirmPassword ? (
@@ -201,6 +190,42 @@ export function ResetPasswordPage() {
+ {/* 密码强度检查(必须通过才允许提交) */}
+
+
+ {t('passwordRequirements', language)}
+
+
setPasswordValid(isValid)}
+ />
+
+