import { type FormEvent, useEffect, useId, useState } from "react"; import "./account-settings.css"; interface SettingsPayload { account: { email: string; status: "active" }; csrf_token: string; local_data: { backup_enabled: false; capacity_status: "normal" | "warning" | "critical" | "full" | "unavailable"; hard_limit_bytes: number; location: "configured_local_data_root"; managed_content_bytes: number; migration_supported: false; }; profile: { creator_name: string; social_id: string }; } function formatBytes(bytes: number) { if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`; if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} MB`; return `${Math.max(0, bytes)} B`; } function capacityLabel(status: SettingsPayload["local_data"]["capacity_status"]) { if (status === "full") return "本机容量已满"; if (status === "unavailable") return "本机数据暂不可写"; if (status === "critical") return "本机容量接近上限"; if (status === "warning") return "本机容量需要关注"; return "本机容量正常"; } export function AccountSettingsPage() { const creatorNameId = useId(); const socialId = useId(); const confirmationId = useId(); const deletionCodeId = useId(); const [settings, setSettings] = useState(); const [creatorName, setCreatorName] = useState(""); const [socialHandle, setSocialHandle] = useState(""); const [loadingError, setLoadingError] = useState(false); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(false); const [saved, setSaved] = useState(false); const [deletionOpen, setDeletionOpen] = useState(false); const [deletionId, setDeletionId] = useState(); const [deletionCode, setDeletionCode] = useState(""); const [confirmation, setConfirmation] = useState(""); const [sendingCode, setSendingCode] = useState(false); const [deleting, setDeleting] = useState(false); const [deletionError, setDeletionError] = useState(false); useEffect(() => { let active = true; void fetch("/api/v1/account/settings", { credentials: "same-origin" }) .then(async (response) => { if (!response.ok) throw new Error("settings_load_failed"); return response.json() as Promise; }) .then((payload) => { if (!active) return; setSettings(payload); setCreatorName(payload.profile.creator_name); setSocialHandle(payload.profile.social_id); }) .catch(() => { if (active) setLoadingError(true); }); return () => { active = false; }; }, []); async function saveProfile(event: FormEvent) { event.preventDefault(); if (!settings || saving || !creatorName.trim() || !socialHandle.trim()) return; setSaving(true); setSaveError(false); setSaved(false); try { const response = await fetch("/api/v1/account/settings/profile", { body: JSON.stringify({ creator_name: creatorName, social_id: socialHandle }), credentials: "same-origin", headers: { "Content-Type": "application/json", "X-CSRF-Token": settings.csrf_token }, method: "PUT", }); if (!response.ok) throw new Error("profile_save_failed"); setSaved(true); } catch { setSaveError(true); } finally { setSaving(false); } } async function sendDeletionCode() { if (!settings || sendingCode || deleting) return; setSendingCode(true); setDeletionError(false); try { const response = await fetch("/api/v1/account/deletion/send", { credentials: "same-origin", headers: { "X-CSRF-Token": settings.csrf_token }, method: "POST", }); const body = await response.json() as { deletion_id?: string }; if (!response.ok || !body.deletion_id) throw new Error("deletion_code_failed"); setDeletionId(body.deletion_id); } catch { setDeletionError(true); } finally { setSendingCode(false); } } async function completeDeletion(event: FormEvent) { event.preventDefault(); if (!settings || !deletionId || deletionCode.length !== 6 || confirmation !== "注销账号" || deleting) return; setDeleting(true); setDeletionError(false); try { const response = await fetch("/api/v1/account/deletion/complete", { body: JSON.stringify({ confirmation, deletion_id: deletionId, verification_code: deletionCode }), credentials: "same-origin", headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", ""), "X-CSRF-Token": settings.csrf_token, }, method: "POST", }); if (!response.ok) throw new Error("account_deletion_failed"); window.location.assign("/"); } catch { setDeletionError(true); setDeleting(false); } } function closeDeletion() { if (deleting) return; setDeletionOpen(false); setDeletionId(undefined); setDeletionCode(""); setConfirmation(""); setDeletionError(false); } if (!settings && !loadingError) { return
正在读取设置
; } if (!settings) { return (

设置暂时无法读取。

); } const capacityPercent = Math.min(100, Math.round((settings.local_data.managed_content_bytes / settings.local_data.hard_limit_bytes) * 100)); return (
DADA

ACCOUNT / LOCAL DATA

设置与本机数据

个人资料

用于生成内容中的署名信息。

setCreatorName(event.target.value)} value={creatorName} /> setSocialHandle(event.target.value)} value={socialHandle} /> {saveError ?

保存失败,当前输入已保留,请重试。

: null} {saved ?

资料已保存。

: null}

账号

邮箱仅用于验证码认证。

邮箱
{settings.account.email}
状态
正常
登录方式
邮箱验证码

本机数据

逻辑位置:Dada 配置的本机数据目录

{capacityLabel(settings.local_data.capacity_status)} {formatBytes(settings.local_data.managed_content_bytes)} / {formatBytes(settings.local_data.hard_limit_bytes)}

测试数据仅保存在本机,不自动备份,也不会迁移到正式系统。

数据依赖当前 Windows 用户登录和文件系统权限,不提供 Dada 应用层加密或云备份。机器损坏、重装或删除本机数据目录后无法恢复。

注销账号

注销会立即删除账号资料、未使用点数和全部作品,并使所有会话失效。

{deletionOpen ? (
{ if (event.key === "Escape" && !deleting) closeDeletion(); }} role="dialog" >

DANGER ZONE

确认注销账号

账号、资料、项目、图片、坐标、成品和未使用点数将立即删除且不可恢复。

匿名生成与点数事件最多保留 180 天,只保留随机主体、时间、模型、结果、错误类别和点数变化;不会保留邮箱或作品内容。

setDeletionCode(event.target.value.replace(/\D/g, ""))} value={deletionCode} /> setConfirmation(event.target.value)} placeholder="输入:注销账号" value={confirmation} /> {deletionError ?

注销请求未完成,请核对验证码后重试。

: null}
) : null}
); }