diff options
Diffstat (limited to 'frontend/src/pages/AccountPage.svelte')
| -rw-r--r-- | frontend/src/pages/AccountPage.svelte | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/frontend/src/pages/AccountPage.svelte b/frontend/src/pages/AccountPage.svelte new file mode 100644 index 0000000..ef6d1a3 --- /dev/null +++ b/frontend/src/pages/AccountPage.svelte @@ -0,0 +1,189 @@ +<script lang="ts"> + import Page from '../components/Page.svelte'; + import Status from '../components/Status.svelte'; + import { session } from '../lib/session.svelte'; + import { getSessions, patchProfile, postPassword, deleteSession } from '../lib/client'; + import { extractStatusMessage } from '../lib/state'; + import { fmtDateTime } from '../lib/format'; + import type { SessionInfo } from '../lib/types'; + + let nameInput = $state(session.me?.name ?? ''); + let profileBusy = $state(false); + let profileOk = $state<string | null>(null); + let profileErr = $state<string | null>(null); + + let curPw = $state(''); + let newPw = $state(''); + let newPw2 = $state(''); + let pwBusy = $state(false); + let pwErr = $state<string | null>(null); + let pwOk = $state<string | null>(null); + + let sessions: SessionInfo[] = $state([]); + let sessBusy = $state(true); + let sessErr = $state<string | null>(null); + let sessMsg = $state<string | null>(null); + + $effect(() => { + nameInput = session.me?.name ?? ''; + void loadSessions(); + }); + + async function loadSessions() { + sessBusy = true; + sessErr = null; + try { + const { items } = await getSessions(); + sessions = items; + } catch (e) { + sessErr = extractStatusMessage(e); + } finally { + sessBusy = false; + } + } + + async function saveProfile(e: SubmitEvent) { + e.preventDefault(); + profileBusy = true; + profileOk = null; + profileErr = null; + try { + await patchProfile(nameInput.trim()); + profileOk = '资料已更新'; + await session.refresh(); + } catch (err) { + profileErr = extractStatusMessage(err); + } finally { + profileBusy = false; + } + } + + async function changePassword(e: SubmitEvent) { + e.preventDefault(); + pwErr = null; + if (newPw.length < 8) { + pwErr = '新密码长度需至少 8 位'; + return; + } + if (newPw !== newPw2) { + pwErr = '两次输入的新密码不一致'; + return; + } + pwBusy = true; + try { + await postPassword(curPw, newPw); + pwOk = '密码已更新;其他会话已被登出,请用新密码重新登录本机外的应用。'; + curPw = ''; + newPw = ''; + newPw2 = ''; + } catch (err) { + pwErr = extractStatusMessage(err); + } finally { + pwBusy = false; + } + } + + async function revoke(s: SessionInfo) { + sessMsg = null; + sessErr = null; + try { + await deleteSession(s.id); + sessMsg = s.current ? '该会话已注销;如果你注销的是当前会话,会稍后自动退出。' : '会话已撤销'; + await loadSessions(); + await session.refresh(); + } catch (e) { + sessErr = extractStatusMessage(e); + } + } +</script> + +<Page title="账户与安全" subtitle="资料、密码与个人会话管理"> + <div class="stack"> + <section class="card stack"> + <h2>个人信息</h2> + <form onsubmit={saveProfile} class="row"> + <label style="flex:1 1 200px"> + 姓名 + <input bind:value={nameInput} autocomplete="name" /> + </label> + <div> + 邮箱 + <div class="fixed">{session.me?.email ?? '—'}</div> + </div> + <div> + 角色 + <div>{session.me?.role === 'admin' ? '管理员' : '成员'}{session.me?.active === false ? ' · 已停用' : ''}</div> + </div> + <button class="btn primary" disabled={profileBusy || nameInput.trim().length === 0}> + {profileBusy ? '保存中…' : '保存资料'} + </button> + </form> + <Status busy={false} error={profileErr} empty={profileOk} /> + </section> + + <section class="card stack"> + <h2>修改密码</h2> + <form onsubmit={changePassword} class="stack"> + <div class="grid3 wrap-3"> + <label> + 当前密码 + <input type="password" bind:value={curPw} required autocomplete="current-password" /> + </label> + <label> + 新密码 + <input type="password" bind:value={newPw} required autocomplete="new-password" minlength={8} /> + </label> + <label> + 确认新密码 + <input type="password" bind:value={newPw2} required autocomplete="new-password" minlength={8} /> + </label> + </div> + <div class="row"> + <button class="btn primary" disabled={pwBusy}> + {pwBusy ? '更新中…' : '更新密码'} + </button> + </div> + <Status busy={false} error={pwErr} empty={pwOk} /> + </form> + </section> + + <section class="card stack"> + <h2>登录会话</h2> + <Status busy={sessBusy} error={sessErr} busyText="载入会话…" empty={null} /> + {#if !sessBusy && !sessErr && sessions.length === 0} + <Status busy={false} empty="没有其他会话记录" /> + {/if} + {#if sessions.length > 0} + <table class="data"> + <thead> + <tr> + <th>创建</th> + <th>最近活跃</th> + <th>设备/UA</th> + <th></th> + </tr> + </thead> + <tbody> + {#each sessions as s (s.id)} + <tr> + <td>{fmtDateTime(s.created_at)}</td> + <td>{fmtDateTime(s.last_seen_at)}</td> + <td class="hint">{s.user_agent?.slice(0, 60) ?? '—'}</td> + <td> + <div class="row"> + {#if s.current}<span class="badge teal">当前会话</span>{/if} + <button class="btn small danger" disabled={s.current && sessions.length === 1} onclick={() => void revoke(s)}> + {s.current ? '注销当前' : '撤销'} + </button> + </div> + </td> + </tr> + {/each} + </tbody> + </table> + {/if} + <Status busy={false} error={null} empty={sessMsg} /> + <p class="hint">更改密码或被管理员停用时,其他会话将自动失效。</p> + </section> + </div> +</Page> |
