78 lines
4.0 KiB
TypeScript
78 lines
4.0 KiB
TypeScript
/**
|
|
* 账户资料编辑与新建工作区共用的客户端对话框。保存后通过回调更新宿主目录/资料,再关闭;
|
|
* demo分支只构造本地结果,真实分支走profile PATCH或workspaces POST,不由组件直接操作canvas runtime。
|
|
*/
|
|
"use client";
|
|
|
|
import { useState, type FormEvent } from "react";
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { api, errorText, type Profile } from "@/lib/api";
|
|
import type { Workspace } from "@/lib/workspace";
|
|
import { UserAvatar } from "./user-avatar";
|
|
import styles from "./workspace.module.css";
|
|
|
|
/** mode决定表单,busy控制当前提交和退出,error用于就地反馈;输入默认值来自打开时profile。 */
|
|
export function AccountPanel({ mode, profile, demo, onClose, onProfile, onWorkspace }: {
|
|
mode: "settings" | "workspace";
|
|
profile: Profile;
|
|
demo: boolean;
|
|
onClose: () => void;
|
|
onProfile: (profile: Profile) => void;
|
|
onWorkspace: (workspace: Workspace) => void;
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const settings = mode === "settings";
|
|
const name = profile.display_name || profile.username;
|
|
|
|
/** 阻止原生导航,捕获FormData后提交;成功先通知宿主再onClose,失败显示错误并恢复按钮。
|
|
* HTML输入限制是交互提示,服务端仍校验真实资料/空间名称。 */
|
|
async function submit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
if (busy) return;
|
|
const data = new FormData(event.currentTarget);
|
|
setBusy(true);
|
|
setError("");
|
|
try {
|
|
// 设置模式只提交display_name/username,避免将整个profile(含只读信息)作为修改payload。
|
|
if (settings) {
|
|
const input = { display_name: String(data.get("display_name")).trim(), username: String(data.get("username")).trim() };
|
|
const updated = demo ? { ...profile, ...input } : await api<Profile>("profile", { method: "PATCH", body: JSON.stringify(input) });
|
|
onProfile(updated);
|
|
} else {
|
|
const name = String(data.get("name")).trim();
|
|
const created = demo ? { id: crypto.randomUUID(), name, role: "admin" as const } : await api<Workspace>("workspaces", { method: "POST", body: JSON.stringify({ name }) });
|
|
onWorkspace(created);
|
|
}
|
|
onClose();
|
|
} catch (e) { setError(errorText(e)); }
|
|
finally { setBusy(false); }
|
|
}
|
|
|
|
// 保存中禁用取消/提交,并拒绝Dialog的关闭状态变化;请求未结束不假装已成功关闭。
|
|
return (
|
|
<Dialog open onOpenChange={(open) => { if (!open && !busy) onClose(); }}>
|
|
<DialogContent className={styles.settingsPanel}>
|
|
<DialogHeader>
|
|
<DialogTitle>{settings ? "账户设置" : "新建工作空间"}</DialogTitle>
|
|
<DialogDescription>{settings ? "管理你的个人资料。" : "为相关的图谱和资料创建一个空间。"}</DialogDescription>
|
|
</DialogHeader>
|
|
<form className={styles.settingsForm} onSubmit={submit}>
|
|
{settings ? (
|
|
<>
|
|
<div className={styles.settingsIdentity}><UserAvatar name={name} url={profile.avatar_url} large /><span>{profile.email || profile.username}</span></div>
|
|
<label>显示名称<input name="display_name" defaultValue={profile.display_name || ""} maxLength={80} /></label>
|
|
<label>用户名<input name="username" defaultValue={profile.username} pattern="[a-z0-9][a-z0-9_-]{2,39}" minLength={3} maxLength={40} required /></label>
|
|
</>
|
|
) : <label>空间名称<input name="name" placeholder="例如:我的创作空间" maxLength={50} required autoFocus /></label>}
|
|
{error && <p className={styles.error} role="alert">{error}</p>}
|
|
<div className={styles.settingsActions}>
|
|
<button type="button" onClick={onClose} disabled={busy}>取消</button>
|
|
<button type="submit" disabled={busy}>{busy ? "正在保存…" : settings ? "保存" : "创建空间"}</button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|