feat(web): call the login name the account ID and list sign-up requirements
This commit is contained in:
1 parent
700be2ebae
commit
79bfd560cb
6 files changed
+49
-10
No files matched your search
@@ -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: '请填写空间名称。',
|
||||||
|
|||||||
@@ -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 }; }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
Reference in new issue
Block a user