feat(core/frontend): P1 pixel restoration (settings/messages/wizard/product-create faithful; ai-tools draft)
- settings.tsx + settings-page.css: restore to settings.html (left-nav + sections), real user/team data - messages.tsx + messages-page.css: rich inbox restore (filters/detail/props grid) on real notifications - projects.tsx ProjectWizardPage + project-wizard-page.css: restore to projects-new.html - products.tsx ProductCreateUploadPage + product-create-page.css: restore to product-create-v2 baseline - ai-tools.tsx (AssetFactory/ImageWorkbench) + ai-tools-page.css: DRAFT unified studio shell; deviates from per-page baselines (image-optimize should be chat-stream; model-photo product+person picker) -> pending rework alongside P3 standalone image-gen decision - shot-p1.mjs: playwright visual-parity capture (react vs exact baseline; uses 127.0.0.1 not localhost) - verified: tsc --noEmit clean; screenshots confirm settings/wizard/product-create/messages faithful Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
25bf3293df
commit
78fd7ee13d
@@ -1,18 +1,452 @@
|
||||
import { useState } from "react";
|
||||
import { LogOut, Upload } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
Bell,
|
||||
LogOut,
|
||||
Monitor,
|
||||
ShieldCheck,
|
||||
Sliders,
|
||||
Smartphone,
|
||||
Upload,
|
||||
User as UserIcon,
|
||||
} from "lucide-react";
|
||||
import type { Team, User } from "../types";
|
||||
import { ConfirmModal, SettingRow, TeamModal } from "../components/overlays";
|
||||
import { TeamModal } from "../components/overlays";
|
||||
|
||||
export function SettingsPage({ user, team, initialSection = "profile" }: { user: User; team: Team; initialSection?: string }) {
|
||||
const [section, setSection] = useState(initialSection);
|
||||
const [modal, setModal] = useState<"" | "avatar" | "logout">("");
|
||||
const sections = [["profile", "个人资料"], ["security", "安全"], ["notify", "通知"], ["pref", "创作偏好"], ["display", "显示"]];
|
||||
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: <UserIcon /> },
|
||||
{ key: "security", label: "安全", icon: <ShieldCheck />, badge: "3 设备" },
|
||||
{ key: "notify", label: "通知", icon: <Bell />, badge: "4/4" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "偏好",
|
||||
items: [
|
||||
{ key: "pref", label: "创作默认", icon: <Sliders /> },
|
||||
{ key: "display", label: "显示", icon: <Monitor /> },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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: "短信" },
|
||||
];
|
||||
|
||||
function Switch({ checked, disabled, onChange }: { checked: boolean; disabled?: boolean; onChange?: (next: boolean) => void }) {
|
||||
return (
|
||||
<>
|
||||
<div className="page-head"><div><h1>设置</h1><div className="sub"><span className="mono">// profile · security · notify · preference · display</span></div></div><div className="actions"><button className="btn" type="button">取消</button><button className="btn btn-primary" type="button">保存设置</button></div></div>
|
||||
<div className="settings-layout"><aside className="settings-side">{sections.map(([key, label]) => <button className={section === key ? "active" : ""} type="button" key={key} onClick={() => setSection(key)}>{label}</button>)}<button className="logout-pill" type="button" onClick={() => setModal("logout")}><LogOut size={13} />退出当前设备</button></aside><section className="settings-main">{section === "profile" && <div className="pane"><h3>个人资料</h3><div className="profile-row"><div className="av-big">{user.username.slice(0, 1).toUpperCase()}</div><button className="btn btn-sm" type="button" onClick={() => setModal("avatar")}>上传新头像</button></div><div className="field"><label className="field-label">用户名</label><input className="input" value={user.username} readOnly /></div><div className="field"><label className="field-label">邮箱</label><input className="input" value={user.email || ""} readOnly /></div><div className="field"><label className="field-label">团队</label><input className="input" value={team.name} readOnly /></div></div>}{section === "security" && <div className="pane"><h3>安全</h3><SettingRow title="登录密码" desc="上次更新: 2026-05-28" action="修改" /><SettingRow title="双因素认证" desc="建议超管开启" toggle /></div>}{section === "notify" && <div className="pane"><h3>通知策略</h3><SettingRow title="导出完成" desc="成片 / 套图完成后提醒" toggle checked /><SettingRow title="任务失败" desc="失败和扣费异常必须提醒" toggle checked /><SettingRow title="额度预警" desc="余额低于阈值时提醒" toggle checked /></div>}{section === "pref" && <div className="pane"><h3>创作偏好</h3><SettingRow title="自动水印" desc="VIP 可关闭" toggle checked /></div>}{section === "display" && <div className="pane"><h3>显示偏好</h3><div className="field"><label className="field-label">主题</label><select className="select"><option>跟随系统</option><option>浅色</option></select></div></div>}</section></div>
|
||||
<TeamModal open={modal === "avatar"} title="上传头像" subtitle="// JPG / PNG / WebP" icon={<Upload size={16} />} close={() => setModal("")}><div className="upload-zone">点击或拖入图片上传<br /><span className="mono">// 头像上传接口待后端补充</span></div></TeamModal>
|
||||
<ConfirmModal open={modal === "logout"} title="退出当前账号" detail="当前有未保存的设置变更时,退出后这些变更不会保存。" confirmText="退出" onCancel={() => setModal("")} onConfirm={() => setModal("")} />
|
||||
</>
|
||||
<label className="switch">
|
||||
<input type="checkbox" checked={checked} disabled={disabled} onChange={(event) => onChange?.(event.target.checked)} />
|
||||
<span className="slider" />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsPage({ user, team, initialSection = "profile" }: { user: User; team: Team; initialSection?: string }) {
|
||||
const normalizedInitial = (["profile", "security", "notify", "pref", "display"] as const).includes(initialSection as SectionKey)
|
||||
? (initialSection as SectionKey)
|
||||
: "profile";
|
||||
const [section, setSection] = useState<SectionKey>(normalizedInitial);
|
||||
const [modal, setModal] = useState<"" | "avatar" | "logout">("");
|
||||
|
||||
const [template, setTemplate] = useState("pain");
|
||||
const [duration, setDuration] = useState("60");
|
||||
const [subtitle, setSubtitle] = useState("big-variety");
|
||||
const [twoFactor, setTwoFactor] = useState(false);
|
||||
const [notify, setNotify] = useState<Record<string, boolean>>({ "n-export": true, "n-fail": true, "n-quota": true, "n-login": true });
|
||||
|
||||
const avatarChar = useMemo(() => (user.username || "李").slice(0, 1).toUpperCase(), [user.username]);
|
||||
|
||||
return (
|
||||
<section className="settings-page">
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>设置</h1>
|
||||
<div className="sub"><span className="mono">// 个人信息 · 偏好 · 通知 · 安全</span></div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="btn" type="button" disabled>取消</button>
|
||||
<button className="btn btn-primary" type="button" disabled>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12l5 5L20 6" /></svg>
|
||||
保存所有变更
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-grid">
|
||||
{/* 左侧 nav */}
|
||||
<aside className="settings-nav" role="tablist" aria-label="设置分区">
|
||||
{NAV.map((group, gi) => (
|
||||
<div key={group.group}>
|
||||
<div className="nav-h" style={gi > 0 ? { marginTop: 16 } : undefined}>{group.group}</div>
|
||||
{group.items.map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={`#sec-${item.key}`}
|
||||
className={section === item.key ? "active" : ""}
|
||||
role="tab"
|
||||
aria-selected={section === item.key}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setSection(item.key);
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
{item.badge ? <span className="nav-badge">{item.badge}</span> : null}
|
||||
<span className="nav-dot" aria-hidden="true" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="nav-h" style={{ marginTop: 16 }}>账号</div>
|
||||
<button className="logout-pill" type="button" onClick={() => setModal("logout")}>
|
||||
<LogOut />
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
{/* 右侧内容 */}
|
||||
<main>
|
||||
{section === "profile" && (
|
||||
<section className="pane" aria-label="个人信息">
|
||||
<h3>个人信息</h3>
|
||||
<div className="pane-desc">// 头像、姓名、联系方式 · 邮箱用于接收通知</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="lbl">头像</div>
|
||||
<div className="val">
|
||||
<div className="avatar-edit">
|
||||
<div className="av-big">{avatarChar}</div>
|
||||
<div className="av-actions">
|
||||
<button className="btn btn-sm" type="button" onClick={() => setModal("avatar")}>上传新头像</button>
|
||||
<button className="btn btn-ghost btn-sm" type="button">恢复默认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">显示名称<span className="req">*</span></div>
|
||||
<div className="val"><input className="input" defaultValue={user.username} /></div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">登录邮箱</div>
|
||||
<div className="val">
|
||||
<input className="input" defaultValue={user.email || ""} />
|
||||
<button className="btn btn-ghost btn-sm" type="button">验证</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">手机号</div>
|
||||
<div className="val">
|
||||
<input className="input" defaultValue="138****8000" />
|
||||
<button className="btn btn-ghost btn-sm" type="button">更换</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">所属团队<div className="lbl-sub">// 一人一团队</div></div>
|
||||
<div className="val">
|
||||
<span className="static">{team.name}</span>
|
||||
<span className="role-tag"><span className="dot" />超管 · 创建者</span>
|
||||
<a href="#team" className="row-link">管理团队 →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">用户 ID<div className="lbl-sub">// 不可改</div></div>
|
||||
<div className="val"><span className="static mono">{user.id}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{section === "security" && (
|
||||
<section className="pane" aria-label="安全">
|
||||
<h3>安全</h3>
|
||||
<div className="pane-desc">// 登录密码、双因素、在用设备</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="lbl">登录密码</div>
|
||||
<div className="val">
|
||||
<span className="static mono">●●●●●●●●●●</span>
|
||||
<span className="row-note" style={{ marginLeft: "auto" }}>上次修改 2026-04-12</span>
|
||||
<button className="btn btn-sm" type="button" style={{ marginLeft: 10 }}>修改</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">两步验证<div className="lbl-sub">// 推荐开启</div></div>
|
||||
<div className="val">
|
||||
<Switch checked={twoFactor} onChange={setTwoFactor} />
|
||||
<span className="switch-note">短信 + Authenticator</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="sub-head">在用设备</h3>
|
||||
<div className="pane-desc">// 不在此列表上的设备登录会触发短信告警</div>
|
||||
<div className="device-list">
|
||||
{DEVICES.map((device) => (
|
||||
<div className="device-row" key={device.name}>
|
||||
<div className="ic">
|
||||
{device.phone ? (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><rect x="6" y="2" width="12" height="20" rx="2" /><path d="M11 18h2" /></svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="14" rx="2" /><path d="M2 20h20" /></svg>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="nm">{device.name}{device.current ? <span className="tag-cur">CURRENT</span> : null}</div>
|
||||
<div className="meta">{device.meta}</div>
|
||||
</div>
|
||||
<div className="spacer" />
|
||||
{device.current
|
||||
? <span className="row-note">当前会话</span>
|
||||
: <button className="btn btn-ghost btn-sm" type="button">下线</button>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<button className="btn" type="button">下线所有其他设备</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{section === "notify" && (
|
||||
<section className="pane" aria-label="通知">
|
||||
<h3>通知</h3>
|
||||
<div className="pane-desc">// 邮件、短信、站内提示开关</div>
|
||||
{NOTIFY_ROWS.map((row) => (
|
||||
<div className="form-row" key={row.key}>
|
||||
<div className="lbl">{row.title}{row.sub ? <div className="lbl-sub">{row.sub}</div> : null}</div>
|
||||
<div className="val">
|
||||
<Switch checked={!!notify[row.key]} onChange={(next) => setNotify((prev) => ({ ...prev, [row.key]: next }))} />
|
||||
<span className="switch-note">{row.channels}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{section === "pref" && (
|
||||
<section className="pane" aria-label="创作默认">
|
||||
<h3>创作默认</h3>
|
||||
<div className="pane-desc">// 新建项目时的预填值,可在向导中改</div>
|
||||
|
||||
<div className="form-row row-top">
|
||||
<div className="lbl">默认模板</div>
|
||||
<div className="val">
|
||||
<div className="pref-choices">
|
||||
{TEMPLATE_CHOICES.map((choice) => (
|
||||
<div
|
||||
key={choice.v}
|
||||
className={`pref-choice ${template === choice.v ? "selected" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setTemplate(choice.v)}
|
||||
>
|
||||
<div className="t">{choice.t}</div>
|
||||
<div className="d">{choice.d}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">默认时长档</div>
|
||||
<div className="val">
|
||||
<div className="duration-row">
|
||||
{DURATIONS.map((d) => (
|
||||
<span
|
||||
key={d}
|
||||
className={`dur-chip ${duration === d ? "selected" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setDuration(d)}
|
||||
>
|
||||
{d}s
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<span className="switch-note" style={{ marginLeft: 10 }}>// 60s = 4 段 × 15s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row row-top">
|
||||
<div className="lbl">默认字幕样式</div>
|
||||
<div className="val">
|
||||
<div className="pref-choices">
|
||||
{SUBTITLE_CHOICES.map((choice) => (
|
||||
<div
|
||||
key={choice.v}
|
||||
className={`pref-choice ${subtitle === choice.v ? "selected" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSubtitle(choice.v)}
|
||||
>
|
||||
<div className="t">{choice.t}</div>
|
||||
<div className="d">{choice.d}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">默认 BGM 库</div>
|
||||
<div className="val">
|
||||
<select className="select" defaultValue="kapian">
|
||||
<option value="kapian">抖音 Top10 卡点曲库</option>
|
||||
<option value="emotion">情绪向 · 治愈/悬念</option>
|
||||
<option value="urban">都市电子 · 通勤场景</option>
|
||||
<option value="none">无 BGM</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">默认转场</div>
|
||||
<div className="val">
|
||||
<select className="select" defaultValue="fade">
|
||||
<option value="none">无转场</option>
|
||||
<option value="fade">淡入淡出 · 0.3s</option>
|
||||
<option value="slide">滑动 · 0.3s</option>
|
||||
<option value="zoom">缩放 · 0.3s</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">导出水印<div className="lbl-sub">// VIP 可关闭</div></div>
|
||||
<div className="val">
|
||||
<Switch checked disabled />
|
||||
<span className="switch-note">右下角 · Airshelf</span>
|
||||
<a href="#account" className="row-link">升级 VIP →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{section === "display" && (
|
||||
<section className="pane" aria-label="显示">
|
||||
<h3>显示</h3>
|
||||
<div className="pane-desc">// 界面外观与语言</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="lbl">外观</div>
|
||||
<div className="val">
|
||||
<select className="select" defaultValue="system">
|
||||
<option value="system">跟随系统</option>
|
||||
<option value="light">浅色</option>
|
||||
<option value="dark" disabled>深色(V2)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">语言</div>
|
||||
<div className="val">
|
||||
<select className="select" defaultValue="zh">
|
||||
<option value="zh">简体中文</option>
|
||||
<option value="en" disabled>English(V2)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">表格密度</div>
|
||||
<div className="val">
|
||||
<select className="select" defaultValue="standard">
|
||||
<option value="compact">紧凑</option>
|
||||
<option value="standard">标准</option>
|
||||
<option value="loose">宽松</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="settings-foot">// Airshelf · v2.1 · build 20260521</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* 上传头像 modal · 仅视觉还原,无后端接入 */}
|
||||
<TeamModal
|
||||
open={modal === "avatar"}
|
||||
title="上传头像"
|
||||
subtitle="// 用于个人主页、评论与团队展示"
|
||||
icon={<Upload size={16} />}
|
||||
close={() => setModal("")}
|
||||
footer={
|
||||
<button className="btn btn-primary" type="button" onClick={() => setModal("")}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12l5 5L20 6" /></svg>
|
||||
确认使用
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="av-up-preview-row">
|
||||
<div className="av-up-preview">{avatarChar}</div>
|
||||
<div className="av-up-preview-meta">
|
||||
<div className="t">当前头像 · 默认</div>
|
||||
<div className="d">// 系统生成 · 取姓氏首字</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="upload-zone" role="button" tabIndex={0} aria-label="点击或拖入图片上传">
|
||||
<span className="uz-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" /></svg>
|
||||
</span>
|
||||
<div><strong>点击选择</strong> · 或拖入图片</div>
|
||||
<span className="uz-hint">JPG / PNG / WebP · ≤ 2 MB · 推荐 256 × 256</span>
|
||||
</div>
|
||||
|
||||
<div className="av-up-rules">
|
||||
<div className="li">最大 2 MB · 长宽比建议 1:1 · 系统会自动裁切为圆形</div>
|
||||
<div className="li">不要上传含他人肖像的图片,违规可能导致账号封停</div>
|
||||
</div>
|
||||
</TeamModal>
|
||||
|
||||
{/* 退出登录确认 modal · 仅视觉还原,无后端接入 */}
|
||||
<TeamModal
|
||||
open={modal === "logout"}
|
||||
title="退出当前账号"
|
||||
subtitle="// LOG OUT CURRENT SESSION"
|
||||
icon={<LogOut size={16} />}
|
||||
close={() => setModal("")}
|
||||
footer={
|
||||
<button className="btn btn-primary" type="button" onClick={() => setModal("")}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><path d="m16 17 5-5-5-5" /><path d="M21 12H9" /></svg>
|
||||
确认退出
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<p className="logout-confirm-copy">确认后将退出当前设备上的 Airshelf,再次使用需要重新登录。</p>
|
||||
<div className="logout-confirm-points">
|
||||
<div className="li">项目、资产、团队成员与余额数据都会保留</div>
|
||||
<div className="li">仅影响当前浏览器会话,不会下线其他设备</div>
|
||||
</div>
|
||||
</TeamModal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user