feat(web): redesign account settings and the sign-in page

- account settings: sidebar sections (profile, devices, security,
  invitations, sync and sign-in) instead of one long scroll; inline device
  rename, confirmations for destructive actions and toast feedback
- sign-in/register: split layout with a quiet brand column, text tabs and
  underlined inputs; registering asks for the password twice and explains
  that it cannot be recovered
- move invitation accept/label helpers into store/invitations so the
  toast and the settings inbox share them
- add the home page zone styles used by the next commit
This commit is contained in:
justme committed 2026-10-14 22:42:06 +08:00
1 parent 4b8d0b5a5f
commit 0d31e3d058
16 files changed
+882 -454

No files matched your search

+3 -20
View File
@@ -1,27 +1,10 @@
import { useEffect, useRef } from 'react';
import { accountApi, type AccountInvitation } from '@/core-bridge/account';
import { errorMessage } from '@/core-bridge/errors';
import { useChannels } from '@/store/channels';
import { accountApi } from '@/core-bridge/account';
import { acceptInvitation, invitationLabel } from '@/store/invitations';
import { useToast } from '@/store/toast';
import { channelPath, navigate } from './navigation';
const POLL_MS = 8000;
export function invitationLabel(row: AccountInvitation): string {
return row.channelName || `空间 ${row.channel_id.slice(0, 8)}…`;
}
async function accept(row: AccountInvitation): Promise<void> {
try {
await accountApi.acceptInvitation(row.channel_id);
await useChannels.getState().refresh();
useToast.getState().push({ kind: 'ok', text: `已加入 ${invitationLabel(row)}` });
navigate(channelPath(row.channel_id));
} catch (cause) {
useToast.getState().push({ kind: 'error', text: `加入失败:${errorMessage(cause)}` });
}
}
/** 登录后轮询账号邀请,出现新邀请就弹通知,可直接接受 */
export function useInvitationNotices(username: string | null): void {
const seen = useRef<Set<string> | null>(null);
@@ -37,7 +20,7 @@ export function useInvitationNotices(username: string | null): void {
const fresh = known ? rows.filter((row) => !known.has(row.channel_id)) : rows;
for (const row of fresh) {
useToast.getState().push({ kind: 'info', text: row.invitedBy ? `${row.invitedBy} 邀请你加入 ${invitationLabel(row)}` : `你收到了加入 ${invitationLabel(row)} 的邀请`,
action: { label: '接受', run: () => { void accept(row); } } });
action: { label: '接受', run: () => { void acceptInvitation(row); } } });
}
} catch { /* 下一轮再试,不打扰用户 */ }
}