feat(web): call the login name the account ID and list sign-up requirements

This commit is contained in:
justme committed 2026-10-16 17:54:31 +08:00
1 parent 700be2ebae
commit 79bfd560cb
6 files changed
+49 -10

No files matched your search

+2 -2
View File
@@ -8,8 +8,8 @@ const messages: Record<string, string> = {
captcha_unavailable: '验证码服务暂时不可用,请稍后重试。', captcha_unavailable: '验证码服务暂时不可用,请稍后重试。',
api_contract_mismatch: '服务端 API 有破坏性更新,请刷新或更新前端后再试。', api_contract_mismatch: '服务端 API 有破坏性更新,请刷新或更新前端后再试。',
mode_mismatch: '空间加密模式与链接不符,已拒绝访问。', conflict: '上传内容发生冲突,已停止发送。', mode_mismatch: '空间加密模式与链接不符,已拒绝访问。', conflict: '上传内容发生冲突,已停止发送。',
username_unavailable: '这个用户名已被使用。', vault_conflict: '账号资源在其他设备上刚刚更新,请再次同步。', username_unavailable: '这个账号 ID 已被使用。', vault_conflict: '账号资源在其他设备上刚刚更新,请再次同步。',
account_invalid: '用户名或密码错误,或登录已过期。', account_required: '请先登录账号。', account_invalid: '账号 ID 或密码错误,或登录已过期。', account_required: '请先登录账号。',
invitation_required: '该空间仅允许受邀账号加入,请登录受邀账号或联系空间主。', invitation_required: '该空间仅允许受邀账号加入,请登录受邀账号或联系空间主。',
channel_password_required: '请输入空间加入密码。', channel_password_invalid: '空间加入密码不正确。', channel_password_required: '请输入空间加入密码。', channel_password_invalid: '空间加入密码不正确。',
space_name_required: '请填写空间名称。', space_name_required: '请填写空间名称。',
+1 -1
View File
@@ -35,7 +35,7 @@ export function unlockedAccount() {
let restoration: Promise<void> | undefined; let restoration: Promise<void> | undefined;
export function usernameOf(input: string): string { export function usernameOf(input: string): string {
const username = input.trim().toLowerCase(); const username = input.trim().toLowerCase();
if (!/^[a-z0-9_-]{3,32}$/.test(username)) throw new Error('用户名需为 3–32 位字母、数字、下划线或短横线'); if (!/^[a-z0-9_-]{3,32}$/.test(username)) throw new Error('账号 ID 需为 3–32 位字母、数字、下划线或短横线');
return username; return username;
} }
function headers(session: Pick<AccountSession, 'token'>): Record<string, string> { return { 'X-WD-Account': session.token }; } function headers(session: Pick<AccountSession, 'token'>): Record<string, string> { return { 'X-WD-Account': session.token }; }
+2 -2
View File
@@ -30,8 +30,8 @@ export function InviteSheet({ onClose, onInvited }: { onClose: () => void; onInv
return <Sheet title="邀请成员" onClose={() => { if (!busy) onClose(); }} placement="top"> return <Sheet title="邀请成员" onClose={() => { if (!busy) onClose(); }} placement="top">
<form className={styles.invite} onSubmit={(event) => { void submit(event); }}> <form className={styles.invite} onSubmit={(event) => { void submit(event); }}>
<label>用户名 <label>账号 ID
<input autoFocus maxLength={32} value={username} disabled={busy} placeholder="已注册用户的用户名" onChange={(event) => setUsername(event.target.value)} /> <input autoFocus maxLength={32} value={username} disabled={busy} placeholder="已注册用户的账号 ID" onChange={(event) => setUsername(event.target.value)} />
</label> </label>
<p className={styles.note}>对方登录后会收到通知,接受邀请后加入空间。能否读取加入前的历史,取决于各频道的历史开关。</p> <p className={styles.note}>对方登录后会收到通知,接受邀请后加入空间。能否读取加入前的历史,取决于各频道的历史开关。</p>
{error ? <p className={styles.error} role="alert">{error}</p> : null} {error ? <p className={styles.error} role="alert">{error}</p> : null}
@@ -150,6 +150,34 @@
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.rules {
display: grid;
gap: 2px;
margin: 0;
padding: 0;
list-style: none;
color: var(--text-dim);
font-size: 12px;
line-height: 1.6;
}
.rules li {
display: flex;
gap: var(--space-2);
}
.rules li[data-met='true'] {
color: var(--text);
}
.srOnly {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.fieldError { .fieldError {
color: var(--danger); color: var(--danger);
font-size: 12px; font-size: 12px;
+15 -4
View File
@@ -43,6 +43,14 @@ export function AccountPanel({ variant = 'default' }: { variant?: 'default' | 'a
}, [account]); }, [account]);
const creating = register && !unlockUsername; const creating = register && !unlockUsername;
const mismatch = creating && confirm !== '' && password !== confirm; const mismatch = creating && confirm !== '' && password !== confirm;
// 与服务端一致:ID 3–32 位,仅字母、数字、_、-
const rules = [
{ text: 'ID 为 3–32 个字符', met: username.length >= 3 && username.length <= 32 },
{ text: 'ID 只能包含字母、数字、下划线 _ 和连字符 -', met: username !== '' && /^[A-Za-z0-9_-]+$/.test(username) },
{ text: '密码至少 12 个字符', met: password.length >= 12 },
{ text: '两次输入的密码一致', met: confirm !== '' && password === confirm },
];
const unmet = creating && rules.some((rule) => !rule.met);
async function clearSaved() { async function clearSaved() {
edited.current = true; edited.current = true;
try { await clearSavedLogin(); setSaved(false); setRemember(false); setUsername(''); setPassword(''); setMessage(account ? '本机保存已清除,当前页面仍可使用;刷新后需要重新登录。' : '已清除本机保存的账号和密码。'); } try { await clearSavedLogin(); setSaved(false); setRemember(false); setUsername(''); setPassword(''); setMessage(account ? '本机保存已清除,当前页面仍可使用;刷新后需要重新登录。' : '已清除本机保存的账号和密码。'); }
@@ -81,14 +89,17 @@ export function AccountPanel({ variant = 'default' }: { variant?: 'default' | 'a
<button type="button" aria-pressed={register} onClick={() => { edited.current = true; setRegister(true); setMessage(''); }}>创建账号</button> <button type="button" aria-pressed={register} onClick={() => { edited.current = true; setRegister(true); setMessage(''); }}>创建账号</button>
</div> : null} </div> : null}
{unlockUsername ? <p className={styles.note}>已用Passkey确认账号,请输入账号密码解锁资源。</p> : null} {unlockUsername ? <p className={styles.note}>已用Passkey确认账号,请输入账号密码解锁资源。</p> : null}
<label>用户名<input name="username" autoComplete="username" minLength={3} maxLength={32} value={unlockUsername ?? username} disabled={Boolean(unlockUsername)} onChange={(event) => { edited.current = true; setUsername(event.target.value); }} required /></label> <label>账号 ID<input name="username" autoComplete="username" minLength={3} maxLength={32} value={unlockUsername ?? username} disabled={Boolean(unlockUsername)} onChange={(event) => { edited.current = true; setUsername(event.target.value); }} required /></label>
<label>密码<input name="password" autoComplete={register ? 'new-password' : 'current-password'} type="password" minLength={register ? 12 : undefined} value={password} onChange={(event) => { edited.current = true; setPassword(event.target.value); }} required /></label> <label>密码<input name="password" autoComplete={register ? 'new-password' : 'current-password'} type="password" minLength={register ? 12 : undefined} value={password} onChange={(event) => { edited.current = true; setPassword(event.target.value); }} required /></label>
{creating ? <label>确认密码<input name="confirm-password" autoComplete="new-password" type="password" minLength={12} value={confirm} aria-invalid={mismatch} onChange={(event) => { edited.current = true; setConfirm(event.target.value); }} required /> {creating ? <label>确认密码<input name="confirm-password" autoComplete="new-password" type="password" minLength={12} value={confirm} aria-invalid={mismatch} onChange={(event) => { edited.current = true; setConfirm(event.target.value); }} required />
{mismatch ? <span className={styles.fieldError} role="alert">两次输入的密码不一致</span> : null}</label> : null} </label> : null}
{creating ? <p className={styles.note}>密码至少 12 个字符。密码用于在本机加密你的资源保险库,<strong>忘记后无法找回</strong>,请妥善保管。</p> : null} {creating ? <ul className={styles.rules} aria-label="注册要求">
{rules.map((rule) => <li key={rule.text} data-met={rule.met}><span aria-hidden="true">{rule.met ? '✓' : '○'}</span>{rule.text}<span className={styles.srOnly}>{rule.met ? ',已满足' : ',未满足'}</span></li>)}
</ul> : null}
{creating ? <p className={styles.note}>账号 ID 是登录用的唯一标识,不是显示名称;显示名称可在登录后的个人资料里设置。密码用于在本机加密你的资源保险库,<strong>忘记后无法找回</strong>,请妥善保管。</p> : null}
<label className={styles.remember}><input type="checkbox" checked={remember} onChange={(event) => setRemember(event.target.checked)} />记住账号和密码(本机)</label> <label className={styles.remember}><input type="checkbox" checked={remember} onChange={(event) => setRemember(event.target.checked)} />记住账号和密码(本机)</label>
<div className={styles.actions}> <div className={styles.actions}>
<Button type="submit" variant="primary" disabled={busy || !channelsReady || !sharesReady || mismatch}>{busy ? '处理中…' : !channelsReady || !sharesReady ? '正在读取本机资源…' : unlockUsername ? '解锁资源' : register ? '创建账号' : '登录并同步'}</Button> <Button type="submit" variant="primary" disabled={busy || !channelsReady || !sharesReady || unmet}>{busy ? '处理中…' : !channelsReady || !sharesReady ? '正在读取本机资源…' : unlockUsername ? '解锁资源' : register ? '创建账号' : '登录并同步'}</Button>
{variant !== 'auth' ? <Button variant="ghost" onClick={() => { edited.current = true; setRegister((value) => !value); setMessage(''); }}>{register ? '已有账号' : '创建账号'}</Button> : null} {variant !== 'auth' ? <Button variant="ghost" onClick={() => { edited.current = true; setRegister((value) => !value); setMessage(''); }}>{register ? '已有账号' : '创建账号'}</Button> : null}
{saved ? <Button variant="ghost" disabled={busy} onClick={() => { void clearSaved(); }}>清除本机保存</Button> : null} {saved ? <Button variant="ghost" disabled={busy} onClick={() => { void clearSaved(); }}>清除本机保存</Button> : null}
{!register && !unlockUsername ? <Button variant="ghost" disabled={busy || !username || !passkeysAvailable()} onClick={() => { {!register && !unlockUsername ? <Button variant="ghost" disabled={busy || !username || !passkeysAvailable()} onClick={() => {
+1 -1
View File
@@ -20,7 +20,7 @@ export interface WorkspaceData {
locations: Record<string, { threadId: string; folderId?: string; authorAccount?: string }>; locations: Record<string, { threadId: string; folderId?: string; authorAccount?: string }>;
byId: Record<string, Item>; byId: Record<string, Item>;
authors: Record<string, SenderInfo>; authors: Record<string, SenderInfo>;
/** 空间成员的资料(昵称、头像),按用户名索引 */ /** 空间成员的资料(昵称、头像),按账号 ID 索引 */
people: Record<string, SenderInfo>; people: Record<string, SenderInfo>;
/** 新的在前 */ /** 新的在前 */
order: string[]; order: string[];