import { useEffect, useMemo, useRef, useState } from "react"; import type { ChangeEvent, ReactNode } from "react"; import { Bell, KeyRound, LogOut, Monitor, ShieldCheck, Sliders, Smartphone, Upload, User as UserIcon, } from "lucide-react"; import type { Team, User } from "../types"; import { TeamModal } from "../components/overlays"; type SectionKey = "profile" | "security" | "notify" | "pref" | "display"; const NAV: Array<{ group: string; items: Array<{ key: SectionKey; label: string; icon: ReactNode; badge?: string }> }> = [ { group: "个人", items: [ { key: "profile", label: "个人信息", icon: }, { key: "security", label: "安全", icon: , badge: "3 设备" }, { key: "notify", label: "通知", icon: , badge: "4/4" }, ], }, { group: "偏好", items: [ { key: "pref", label: "创作默认", icon: }, { key: "display", label: "显示", icon: }, ], }, ]; const TEMPLATE_CHOICES = [ { v: "pain", t: "痛点种草", d: "// 30s 默认档" }, { v: "unbox", t: "开箱测评", d: "// 45s 默认档" }, { v: "compare", t: "对比展示", d: "// 45s 默认档" }, { v: "howto", t: "教程演示", d: "// 60s 默认档" }, { v: "drama", t: "剧情带货", d: "// 60s 默认档" }, ]; const SUBTITLE_CHOICES = [ { v: "big-variety", t: "大字综艺", d: "// 抖音热门" }, { v: "clean-ec", t: "简洁电商", d: "// 信息清晰" }, { v: "premium", t: "高级排版", d: "// 居中衬线" }, { v: "bullet", t: "弹幕轻量", d: "// 滚动出现" }, { v: "emphasis", t: "强调爆款", d: "// 高对比" }, ]; const DURATIONS = ["30", "45", "60"]; const DEVICES: Array<{ name: string; meta: string; current?: boolean; phone?: boolean }> = [ { name: "MacBook Pro · Chrome", meta: "// 上海 · 2026-05-21 14:08 · IP 116.xxx.xxx.42", current: true }, { name: "iPhone 15 · Safari", meta: "// 上海 · 2026-05-20 21:43", phone: true }, { name: "Windows · Edge", meta: "// 杭州 · 2026-05-18 09:12" }, ]; const NOTIFY_ROWS: Array<{ key: string; title: string; sub?: string; channels: string }> = [ { key: "n-export", title: "项目完成通知", sub: "// 视频导出后", channels: "站内 · 邮件 · 短信" }, { key: "n-fail", title: "任务失败告警", channels: "站内 · 邮件" }, { key: "n-quota", title: "额度不足提醒", sub: "// 团队或个人剩余 < 20%", channels: "站内 · 短信" }, { key: "n-login", title: "异地登录告警", channels: "短信" }, ]; // ─── 偏好持久化 · 后端无字段,纯本地 localStorage ─── const PREFS_KEY = "airshelf_settings_prefs"; type Prefs = { template: string; duration: string; subtitle: string; twoFactor: boolean; notify: Record; appearance: string; language: string; density: string; }; const DEFAULT_PREFS: Prefs = { template: "pain", duration: "60", subtitle: "big-variety", twoFactor: false, notify: { "n-export": true, "n-fail": true, "n-quota": true, "n-login": true }, appearance: "system", language: "zh", density: "standard", }; function loadPrefs(): Prefs { try { const raw = localStorage.getItem(PREFS_KEY); if (!raw) return DEFAULT_PREFS; const parsed = JSON.parse(raw) as Partial; return { ...DEFAULT_PREFS, ...parsed, notify: { ...DEFAULT_PREFS.notify, ...(parsed.notify ?? {}) }, }; } catch { return DEFAULT_PREFS; } } function Switch({ checked, disabled, onChange }: { checked: boolean; disabled?: boolean; onChange?: (next: boolean) => void }) { return ( ); } export function SettingsPage({ user, team, initialSection = "profile", onSaveProfile, onChangePassword, onUploadAvatar, }: { user: User; team: Team; initialSection?: string; onSaveProfile: (payload: { name?: string; phone?: string; email?: string }) => void | Promise; onChangePassword: (payload: { old_password: string; new_password: string }) => void | Promise; onUploadAvatar: (formData: FormData) => void | Promise; }) { const normalizedInitial = (["profile", "security", "notify", "pref", "display"] as const).includes(initialSection as SectionKey) ? (initialSection as SectionKey) : "profile"; const [section, setSection] = useState(normalizedInitial); const [modal, setModal] = useState<"" | "avatar" | "logout" | "password">(""); // 个人信息 · 受控输入(初值取真实用户数据) const [name, setName] = useState(user.username || ""); const [email, setEmail] = useState(user.email || ""); const [phone, setPhone] = useState(""); const [savingProfile, setSavingProfile] = useState(false); // 偏好 · localStorage 持久化(读 localStorage 初始化) const initialPrefs = useMemo(() => loadPrefs(), []); const [template, setTemplate] = useState(initialPrefs.template); const [duration, setDuration] = useState(initialPrefs.duration); const [subtitle, setSubtitle] = useState(initialPrefs.subtitle); const [twoFactor, setTwoFactor] = useState(initialPrefs.twoFactor); const [notify, setNotify] = useState>(initialPrefs.notify); const [appearance, setAppearance] = useState(initialPrefs.appearance); const [language, setLanguage] = useState(initialPrefs.language); const [density, setDensity] = useState(initialPrefs.density); // 偏好改动即写回 localStorage(不调后端) useEffect(() => { const prefs: Prefs = { template, duration, subtitle, twoFactor, notify, appearance, language, density }; try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } catch { /* localStorage 不可用时静默降级 */ } }, [template, duration, subtitle, twoFactor, notify, appearance, language, density]); // 改密 · 受控输入 const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [pwSubmitted, setPwSubmitted] = useState(false); const [savingPassword, setSavingPassword] = useState(false); const pwTooShort = newPassword.length > 0 && newPassword.length < 8; const pwReady = oldPassword.length > 0 && newPassword.length >= 8; // 头像 · 文件选择 + 本地预览 const fileInputRef = useRef(null); const [avatarFile, setAvatarFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(""); const [savingAvatar, setSavingAvatar] = useState(false); const avatarChar = useMemo(() => (name || user.username || "李").slice(0, 1).toUpperCase(), [name, user.username]); function resetProfile() { setName(user.username || ""); setEmail(user.email || ""); setPhone(""); } async function handleSaveProfile() { if (savingProfile) return; setSavingProfile(true); try { await onSaveProfile({ name: name.trim(), email: email.trim(), phone: phone.trim() }); } finally { setSavingProfile(false); } } function openPasswordModal() { setOldPassword(""); setNewPassword(""); setPwSubmitted(false); setModal("password"); } async function handleChangePassword() { setPwSubmitted(true); if (!pwReady || savingPassword) return; setSavingPassword(true); try { await onChangePassword({ old_password: oldPassword, new_password: newPassword }); setModal(""); setOldPassword(""); setNewPassword(""); setPwSubmitted(false); } finally { setSavingPassword(false); } } function openAvatarModal() { setAvatarFile(null); setAvatarPreview(""); setModal("avatar"); } function onPickAvatar(event: ChangeEvent) { const file = event.target.files?.[0]; if (!file) return; setAvatarFile(file); setAvatarPreview(URL.createObjectURL(file)); } async function handleUploadAvatar() { if (!avatarFile || savingAvatar) return; setSavingAvatar(true); try { const fd = new FormData(); fd.append("file", avatarFile); await onUploadAvatar(fd); setModal(""); setAvatarFile(null); setAvatarPreview(""); } finally { setSavingAvatar(false); } } // 预览 URL 在切换/卸载时释放,避免内存泄漏 useEffect(() => { if (!avatarPreview) return; return () => URL.revokeObjectURL(avatarPreview); }, [avatarPreview]); return (

设置

// 个人信息 · 偏好 · 通知 · 安全
{/* 左侧 nav */} {/* 右侧内容 */}
{section === "profile" && (

个人信息

// 头像、姓名、联系方式 · 邮箱用于接收通知
头像
{avatarChar}
显示名称*
setName(event.target.value)} />
登录邮箱
setEmail(event.target.value)} />
手机号
setPhone(event.target.value)} placeholder="138****8000" />
所属团队
// 一人一团队
{team.name} 超管 · 创建者 管理团队 →
用户 ID
// 不可改
{user.id}
)} {section === "security" && (

安全

// 登录密码、双因素、在用设备
登录密码
●●●●●●●●●● 上次修改 2026-04-12
两步验证
// 推荐开启
短信 + Authenticator

在用设备

// 不在此列表上的设备登录会触发短信告警
{DEVICES.map((device) => (
{device.phone ? ( ) : ( )}
{device.name}{device.current ? CURRENT : null}
{device.meta}
{device.current ? 当前会话 : }
))}
)} {section === "notify" && (

通知

// 邮件、短信、站内提示开关
{NOTIFY_ROWS.map((row) => (
{row.title}{row.sub ?
{row.sub}
: null}
setNotify((prev) => ({ ...prev, [row.key]: next }))} /> {row.channels}
))}
)} {section === "pref" && (

创作默认

// 新建项目时的预填值,可在向导中改
默认模板
{TEMPLATE_CHOICES.map((choice) => (
setTemplate(choice.v)} >
{choice.t}
{choice.d}
))}
默认时长档
{DURATIONS.map((d) => ( setDuration(d)} > {d}s ))}
// 60s = 4 段 × 15s
默认字幕样式
{SUBTITLE_CHOICES.map((choice) => (
setSubtitle(choice.v)} >
{choice.t}
{choice.d}
))}
默认 BGM 库
默认转场
导出水印
// VIP 可关闭
右下角 · Airshelf 升级 VIP →
)} {section === "display" && (

显示

// 界面外观与语言
外观
语言
表格密度
)}
// Airshelf · v2.1 · build 20260521
{/* 上传头像 modal · 选图 → FormData(file) → onUploadAvatar */} } close={() => setModal("")} footer={ } >
{avatarPreview ? 头像预览 : avatarChar}
{avatarFile ? avatarFile.name : "当前头像 · 默认"}
{avatarFile ? `// ${(avatarFile.size / 1024).toFixed(0)} KB · 已选择` : "// 系统生成 · 取姓氏首字"}
fileInputRef.current?.click()} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); fileInputRef.current?.click(); } }} >
点击选择 · 图片文件
JPG / PNG / WebP · ≤ 2 MB · 推荐 256 × 256
最大 2 MB · 长宽比建议 1:1 · 系统会自动裁切为圆形
不要上传含他人肖像的图片,违规可能导致账号封停
{/* 修改密码 modal · 原密码 + 新密码(≥8)→ onChangePassword */} } close={() => setModal("")} footer={ } >
setOldPassword(event.target.value)} placeholder="输入当前密码" /> {pwSubmitted && !oldPassword ? 请输入原密码 : null}
setNewPassword(event.target.value)} placeholder="至少 8 位" /> {pwTooShort || (pwSubmitted && newPassword.length < 8) ? 新密码至少 8 位 : // 建议混合字母、数字与符号}
{/* 退出登录确认 modal · 仅视觉还原,无后端接入 */} } close={() => setModal("")} footer={ } >

确认后将退出当前设备上的 Airshelf,再次使用需要重新登录。

项目、资产、团队成员与余额数据都会保留
仅影响当前浏览器会话,不会下线其他设备
); }