74 lines
3.7 KiB
TypeScript
74 lines
3.7 KiB
TypeScript
// 管理员用户管理已迁「系统管理」分区(components/adminSections.tsx UsersPanel,
|
||
// 重设计方案 阶段②)。本文件只保留登录者主动改密弹窗(与强制改密共用 API)。
|
||
import { useState } from 'react'
|
||
import { api } from '../api'
|
||
import { store, useStore } from '../store'
|
||
import { Icon } from '@ui'
|
||
import { humanChangePasswordError } from './loginErrors'
|
||
|
||
function Shell({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) {
|
||
return (
|
||
<div className="overlay" onClick={e => { if (e.target === e.currentTarget) onClose() }}>
|
||
<div className="modal ad-card" role="dialog" aria-label={title} style={{ maxWidth: 640 }}>
|
||
<div className="modal-header">
|
||
<b>{title}</b>
|
||
<span className="spacer" />
|
||
<button className="ad-mini" onClick={onClose} aria-label="关闭"><Icon name="x" size={13} /></button>
|
||
</div>
|
||
<div className="ad-body">{children}</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 主动改密(账户菜单入口):用户名(只读关联)+ 当前密码 + 新密码 + 确认。
|
||
* 表单语义(2026-10-12 增量 3):<form onSubmit> 承载提交(Enter 可用)、autocomplete
|
||
* 三段标注(username/current-password/new-password)、label 与 input 关联;
|
||
* 错误文案复用 humanChangePasswordError——401 不再一刀切成「当前密码不正确」。 */
|
||
export function ChangePasswordModal({ onClose }: { onClose: () => void }) {
|
||
const me = useStore(s => s.me)
|
||
const [current, setCurrent] = useState('')
|
||
const [next, setNext] = useState('')
|
||
const [confirm, setConfirm] = useState('')
|
||
const [err, setErr] = useState('')
|
||
const [ok, setOk] = useState(false)
|
||
const [busy, setBusy] = useState(false)
|
||
|
||
const submit = async () => {
|
||
setErr('')
|
||
if (next !== confirm) { setErr('两次输入的新密码不一致'); return }
|
||
if (next.length < 8) { setErr('新密码至少 8 位'); return }
|
||
setBusy(true)
|
||
try {
|
||
await api.changePassword(current, next)
|
||
setOk(true)
|
||
store.toast('密码已更新')
|
||
setTimeout(onClose, 900)
|
||
} catch (e) {
|
||
setErr(humanChangePasswordError(String(e)))
|
||
} finally { setBusy(false) }
|
||
}
|
||
|
||
return (
|
||
<Shell title="修改密码" onClose={onClose}>
|
||
{ok ? <div className="ad-notice">密码已更新,其他设备的会话已注销。</div> : (
|
||
<form className="ad-changepw" onSubmit={e => { e.preventDefault(); submit() }}>
|
||
<label className="lg-label" htmlFor="changepw-username">用户名</label>
|
||
<input id="changepw-username" className="lg-input" value={me?.owner ?? ''} readOnly autoComplete="username" />
|
||
<label className="lg-label" htmlFor="changepw-current">当前密码</label>
|
||
<input id="changepw-current" className="lg-input" type="password" autoComplete="current-password"
|
||
value={current} onChange={e => setCurrent(e.target.value)} autoFocus />
|
||
<label className="lg-label" htmlFor="changepw-next">新密码(≥8 位)</label>
|
||
<input id="changepw-next" className="lg-input" type="password" autoComplete="new-password"
|
||
value={next} onChange={e => setNext(e.target.value)} />
|
||
<label className="lg-label" htmlFor="changepw-confirm">确认新密码</label>
|
||
<input id="changepw-confirm" className="lg-input" type="password" autoComplete="new-password"
|
||
value={confirm} onChange={e => setConfirm(e.target.value)} />
|
||
{err && <div className="lg-err"><Icon name="alert" size={14} />{err}</div>}
|
||
<button className="lg-submit" type="submit" disabled={busy || !current || !next || !confirm}>{busy ? '提交中…' : '更新密码'}</button>
|
||
</form>
|
||
)}
|
||
</Shell>
|
||
)
|
||
}
|