Files
plainleaf/client/spaces_ui/components/AccountView.tsx
T
wushenghuaandCodex db73584e2b
CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
feat: 完善账号管理与扩展体验
补齐普通用户自助账号、中文设置与快捷键提示,修复大型扩展安装和移动端弹窗,并按服务器能力限制不兼容扩展。

Co-Authored-By: Codex <noreply@anthropic.com>
2026-08-12 22:13:23 +08:00

161 lines
4.9 KiB
TypeScript

import { Alert, Badge, Button, Input } from "@silverbulletmd/silverbullet/ui";
import { useEffect, useState } from "preact/hooks";
import {
changeOwnPassword,
createOwnToken,
deleteOwnToken,
formatApiError,
getOwnAccount,
} from "../api.ts";
import { spacesUrl } from "../routes.ts";
import type { AuthState, UserInfo } from "../types.ts";
export function AccountView({
auth,
onUnauthorized,
}: {
auth: Extract<AuthState, { phase: "authed" }>;
onUnauthorized: () => void;
}) {
const [user, setUser] = useState<UserInfo>();
const [error, setError] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [tokenName, setTokenName] = useState("");
const [shownToken, setShownToken] = useState<string>();
async function reload() {
try {
const account = await getOwnAccount();
setUser(account.user);
setError("");
} catch (error: any) {
if (error.unauthorized) onUnauthorized();
else setError(formatApiError(error));
}
}
useEffect(() => {
void reload();
}, []);
async function run(action: () => Promise<void>) {
try {
await action();
setError("");
} catch (error: any) {
if (error.unauthorized) onUnauthorized();
else setError(formatApiError(error));
}
}
if (!user && !error) return <p>正在加载…</p>;
return (
<div>
<h1>
{auth.username} <Badge>{auth.admin ? "管理员" : "普通用户"}</Badge>
</h1>
{error && <Alert variant="error">{error}</Alert>}
{auth.admin && (
<p>
<a class="sb-button" href={spacesUrl("/users")}>
管理全部用户
</a>
</p>
)}
<section>
<h2>修改密码</h2>
<p class="sb-help-text">修改后其他设备上的登录会话将失效。</p>
<div class="row">
<Input
type="password"
aria-label="当前密码"
placeholder="当前密码"
value={currentPassword}
onInput={(event) => setCurrentPassword(event.currentTarget.value)}
/>
<Input
type="password"
aria-label="新密码"
placeholder="新密码"
value={newPassword}
onInput={(event) => setNewPassword(event.currentTarget.value)}
/>
<Button
variant="primary"
disabled={!currentPassword || !newPassword}
onClick={() =>
void run(async () => {
await changeOwnPassword(currentPassword, newPassword);
location.assign(
`${spacesUrl("/login")}?next=${encodeURIComponent(spacesUrl("/account"))}`,
);
})
}
>
更新密码
</Button>
</div>
</section>
<section>
<h2>我的 API 令牌</h2>
<p class="sb-help-text">用于外部工具访问,请像密码一样妥善保管。</p>
{user && Object.keys(user.tokens).length === 0 && <p>还没有令牌。</p>}
{user && Object.keys(user.tokens).length > 0 && (
<ul class="sb-token-list">
{Object.entries(user.tokens).map(([name, token]) => (
<li key={name}>
<strong>{name}</strong>
<span>创建于 {new Date(token.createdAt).toLocaleString()}</span>
<Button
onClick={() => {
if (!confirm(`确定撤销令牌“${name}”吗?`)) return;
void run(async () => {
await deleteOwnToken(name);
await reload();
});
}}
>
撤销
</Button>
</li>
))}
</ul>
)}
<div class="row">
<Input
aria-label="令牌名称"
placeholder="令牌名称,例如:自动备份"
value={tokenName}
onInput={(event) => setTokenName(event.currentTarget.value)}
/>
<Button
variant="primary"
disabled={!tokenName.trim()}
onClick={() =>
void run(async () => {
setShownToken(await createOwnToken(tokenName.trim()));
setTokenName("");
await reload();
})
}
>
创建令牌
</Button>
</div>
{shownToken && (
<div class="sb-token-reveal">
<Alert variant="warning">此令牌只显示一次,请立即妥善保存。</Alert>
<Input
readOnly
value={shownToken}
onClick={(event) => event.currentTarget.select()}
/>
<Button onClick={() => setShownToken(undefined)}>我已保存</Button>
</div>
)}
</section>
</div>
);
}