AirShelf/电商AI平台/settings.html
iye 553014cc79
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
更新电商AI平台原型交互
2026-05-25 19:12:56 +08:00

789 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>设置 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<style>
/* ─── 设置布局:左 nav + 右 panel ─── */
.settings-grid { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 24px; align-items: start; }
.settings-nav { position: sticky; top: 16px; }
.settings-nav .nav-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; padding: 0 12px 8px; }
.settings-nav a { display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13px; color: var(--accent-black); border-radius: var(--r-md); border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: background var(--t-base), border-color var(--t-base); position: relative; }
.settings-nav a:hover { background: var(--background-lighter); }
.settings-nav a:focus-visible { outline: 2px solid var(--heat); outline-offset: 2px; }
.settings-nav a.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.settings-nav a svg { width: 16px; height: 16px; stroke-width: 1.5; }
.settings-nav a .nav-badge { margin-left: auto; font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); padding: 1px 6px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-pill); letter-spacing: .02em; line-height: 14px; }
.settings-nav a.active .nav-badge { color: var(--heat); background: var(--accent-white); border-color: var(--heat-20); }
.settings-nav a .nav-dot { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 6px; height: 6px; border-radius: 50%; background: var(--heat); display: none; }
.settings-nav a.has-changes .nav-dot { display: block; }
.settings-nav a.active .nav-dot { right: -4px; }
/* ─── pane ─── */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 24px; margin-bottom: 16px; }
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.pane .pane-desc { font-size: 12px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; margin-bottom: 18px; }
.pane.danger { border-color: rgba(180,30,30,.25); background: rgba(180,30,30,.03); }
.pane.danger h3 { color: var(--accent-crimson); }
/* ─── form row ─── */
.form-row { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--border-faint); align-items: center; }
.form-row:last-child { border-bottom: 0; }
.form-row .lbl { font-size: 12.5px; color: var(--black-alpha-56); }
.form-row .lbl .req { color: var(--accent-crimson); margin-left: 2px; }
.form-row .lbl-sub { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
.form-row .val { display: flex; align-items: center; gap: 10px; min-width: 0; }
.form-row .val .input, .form-row .val .select { width: 100%; max-width: 380px; }
.form-row .val .static { font-size: 13px; color: var(--accent-black); font-variant-numeric: tabular-nums; }
.form-row .val .static.mono { font-family: var(--font-mono); font-size: 12.5px; color: var(--black-alpha-56); }
.form-row .val .role-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; background: var(--heat-12); color: var(--heat); }
.form-row .val .role-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--heat); }
/* ─── 头像上传 ─── */
.avatar-edit { display: flex; align-items: center; gap: 16px; }
.avatar-edit .av-big { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 24px; font-weight: 600; color: var(--accent-black); }
.avatar-edit .av-actions { display: flex; gap: 8px; }
/* ─── toggle switch ─── */
.switch { position: relative; width: 36px; height: 20px; flex: 0 0 36px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider { position: absolute; inset: 0; background: var(--black-alpha-24); border-radius: 20px; cursor: pointer; transition: background var(--t-base); }
.switch .slider::before { content: ''; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; background: var(--accent-white); border-radius: 50%; transition: transform var(--t-base); }
.switch input:checked + .slider { background: var(--heat); }
.switch input:checked + .slider::before { transform: translateX(16px); }
/* ─── 偏好选项卡 ─── */
.pref-choices { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; max-width: 540px; }
.pref-choice { padding: 10px 12px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
.pref-choice:hover { background: var(--background-lighter); }
.pref-choice.selected { border-color: var(--heat); background: var(--heat-12); }
.pref-choice .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
.pref-choice .d { font-size: 11px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
.pref-choice.selected .t { color: var(--heat); }
/* ─── 时长档 ─── */
.duration-row { display: flex; gap: 8px; }
.dur-chip { padding: 6px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 13px; cursor: pointer; font-family: var(--font-mono); font-variant-numeric: tabular-nums; transition: all var(--t-base); background: var(--surface); }
.dur-chip:hover { background: var(--background-lighter); }
.dur-chip.selected { border-color: var(--heat); background: var(--heat-12); color: var(--heat); font-weight: 600; }
/* ─── 设备列表 ─── */
.device-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-faint); }
.device-row:last-child { border-bottom: 0; }
.device-row .ic { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--background-lighter); display: grid; place-items: center; color: var(--black-alpha-56); }
.device-row .ic svg { width: 18px; height: 18px; }
.device-row .nm { font-size: 13px; font-weight: 500; }
.device-row .meta { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
.device-row .tag-cur { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: var(--accent-forest); color: var(--accent-white); border-radius: var(--r-sm); margin-left: 8px; letter-spacing: .04em; font-weight: 600; }
.device-row .spacer { margin-left: auto; }
/* ─── 头像上传 modal · V2.1 Restraint ─── */
.av-up-modal { width: min(440px, 92vw); max-width: min(440px, 92vw); }
/* 预览区:左圆头像 + 右 mono 元数据 · 装订线点划线分隔 */
.av-up-preview-row { display: flex; align-items: center; gap: 14px; padding-bottom: 14px; margin-bottom: 14px; position: relative; }
.av-up-preview-row::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: repeating-linear-gradient(to right, var(--border-faint) 0, var(--border-faint) 4px, transparent 4px, transparent 8px); }
.av-up-preview { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 22px; font-weight: 600; color: var(--accent-black); overflow: hidden; flex: 0 0 64px; }
.av-up-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.av-up-preview-meta { min-width: 0; }
.av-up-preview-meta .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); margin-bottom: 3px; letter-spacing: .01em; }
.av-up-preview-meta .d { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; line-height: 1.55; }
/* 规则文本 · 纯 mono, 装订线分隔 */
.av-up-rules { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--border-faint); font-size: 11px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; line-height: 1.7; }
.av-up-rules .li { display: flex; gap: 8px; }
.av-up-rules .li::before { content: '//'; color: var(--black-alpha-32); flex: 0 0 auto; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>设置</h1>
<div class="sub"><span class="mono">// 个人信息 · 偏好 · 通知 · 安全</span></div>
</div>
<div class="actions">
<button class="btn" id="save-cancel" disabled style="opacity:.5; cursor:not-allowed;">取消</button>
<button class="btn btn-primary" id="save-btn" disabled style="opacity:.5; cursor:not-allowed;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
保存所有变更
</button>
</div>
</div>
<div class="settings-grid">
<!-- 左侧 nav -->
<aside class="settings-nav" role="tablist" aria-label="设置分区">
<div class="nav-h">个人</div>
<a href="#sec-profile" class="active" data-jump="sec-profile" role="tab" aria-controls="sec-profile">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
<span>个人信息</span>
<span class="nav-dot" aria-hidden="true"></span>
</a>
<a href="#sec-security" data-jump="sec-security" role="tab" aria-controls="sec-security">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
<span>安全</span>
<span class="nav-badge" data-count-source="sec-security">3</span>
<span class="nav-dot" aria-hidden="true"></span>
</a>
<a href="#sec-notify" data-jump="sec-notify" role="tab" aria-controls="sec-notify">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
<span>通知</span>
<span class="nav-badge" data-count-source="sec-notify">5</span>
<span class="nav-dot" aria-hidden="true"></span>
</a>
<div class="nav-h" style="margin-top: 16px;">偏好</div>
<a href="#sec-pref" data-jump="sec-pref" role="tab" aria-controls="sec-pref">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4"/></svg>
<span>创作默认</span>
<span class="nav-dot" aria-hidden="true"></span>
</a>
<a href="#sec-display" data-jump="sec-display" role="tab" aria-controls="sec-display">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
<span>显示</span>
<span class="nav-dot" aria-hidden="true"></span>
</a>
<div class="nav-h" style="margin-top: 16px;">账号</div>
<a href="#sec-danger" data-jump="sec-danger" role="tab" aria-controls="sec-danger" style="color: var(--accent-crimson);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
<span>危险操作</span>
</a>
</aside>
<!-- 右侧内容 -->
<main>
<!-- ─── 个人信息 ─── -->
<section class="pane" id="sec-profile">
<h3>个人信息</h3>
<div class="pane-desc">// 头像、姓名、联系方式 · 邮箱用于接收通知</div>
<div class="form-row">
<div class="lbl">头像</div>
<div class="val">
<div class="avatar-edit">
<div class="av-big" id="prof-avatar-preview"></div>
<div class="av-actions">
<button class="btn btn-sm" type="button" onclick="Shell.openModal('avatar-up-bg')">上传新头像</button>
<button class="btn btn-ghost btn-sm" type="button" id="prof-avatar-reset">恢复默认</button>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">显示名称<span class="req">*</span></div>
<div class="val"><input class="input" id="prof-name" value="小李" data-track></div>
</div>
<div class="form-row">
<div class="lbl">登录邮箱</div>
<div class="val">
<input class="input" id="prof-email" value="li@shop.com" data-track>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送验证邮件', 'li@shop.com')">验证</button>
</div>
</div>
<div class="form-row">
<div class="lbl">手机号</div>
<div class="val">
<input class="input" id="prof-phone" value="138****8000" data-track>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送短信验证码', '+86 138****8000')">更换</button>
</div>
</div>
<div class="form-row">
<div class="lbl">所属团队<div class="lbl-sub">// 一人一团队</div></div>
<div class="val">
<span class="static">小李的店</span>
<span class="role-tag"><span class="dot"></span>超管 · 创建者</span>
<a href="team.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">管理团队 →</a>
</div>
</div>
<div class="form-row">
<div class="lbl">用户 ID<div class="lbl-sub">// 不可改</div></div>
<div class="val"><span class="static mono">USR-2026-A8F2-001</span></div>
</div>
</section>
<!-- ─── 安全 ─── -->
<section class="pane" id="sec-security">
<h3>安全</h3>
<div class="pane-desc">// 登录密码、双因素、在用设备</div>
<div class="form-row">
<div class="lbl">登录密码</div>
<div class="val">
<span class="static mono">●●●●●●●●●●</span>
<span class="muted-2 mono" style="font-size: 11px; margin-left: auto;">上次修改 2026-04-12</span>
<button class="btn btn-sm" onclick="Shell.toast('修改密码', '/settings/password')" style="margin-left: 10px;">修改</button>
</div>
</div>
<div class="form-row">
<div class="lbl">两步验证<div class="lbl-sub">// 推荐开启</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="opt-2fa"><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信 + Authenticator</span>
</div>
</div>
<h3 style="margin-top: 24px;">在用设备</h3>
<div class="pane-desc">// 不在此列表上的设备登录会触发短信告警</div>
<div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
<div>
<div class="nm">MacBook Pro · Chrome<span class="tag-cur">CURRENT</span></div>
<div class="meta">// 上海 · 2026-05-21 14:08 · IP 116.xxx.xxx.42</div>
</div>
<div class="spacer"></div>
<span class="muted-2 mono" style="font-size: 11px;">当前会话</span>
</div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2" width="12" height="20" rx="2"/><path d="M11 18h2"/></svg></div>
<div>
<div class="nm">iPhone 15 · Safari</div>
<div class="meta">// 上海 · 2026-05-20 21:43</div>
</div>
<div class="spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'iPhone 15')">下线</button>
</div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
<div>
<div class="nm">Windows · Edge</div>
<div class="meta">// 杭州 · 2026-05-18 09:12</div>
</div>
<div class="spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'Windows Edge')">下线</button>
</div>
</div>
<div style="margin-top: 14px;">
<button class="btn" onclick="if(confirm('下线所有其他设备?')) Shell.toast('已下线其他设备', '2 个')">下线所有其他设备</button>
</div>
</section>
<!-- ─── 通知 ─── -->
<section class="pane" id="sec-notify">
<h3>通知</h3>
<div class="pane-desc">// 邮件、短信、站内提示开关</div>
<div class="form-row">
<div class="lbl">项目完成通知<div class="lbl-sub">// 视频导出后</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-export" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件 · 短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">任务失败告警</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-fail" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件</span>
</div>
</div>
<div class="form-row">
<div class="lbl">额度不足提醒<div class="lbl-sub">// 团队或个人剩余 < 20%</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-quota" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">异地登录告警</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-login" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信</span>
</div>
</div>
</section>
<!-- ─── 创作默认 ─── -->
<section class="pane" id="sec-pref">
<h3>创作默认</h3>
<div class="pane-desc">// 新建项目时的预填值,可在向导中改</div>
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
<div class="lbl" style="padding-top: 4px;">默认模板</div>
<div class="val" style="display: block;">
<div class="pref-choices" id="pref-template">
<div class="pref-choice selected" data-v="pain"><div class="t">痛点种草</div><div class="d">// 30s 默认档</div></div>
<div class="pref-choice" data-v="unbox"><div class="t">开箱测评</div><div class="d">// 45s 默认档</div></div>
<div class="pref-choice" data-v="compare"><div class="t">对比展示</div><div class="d">// 45s 默认档</div></div>
<div class="pref-choice" data-v="howto"><div class="t">教程演示</div><div class="d">// 60s 默认档</div></div>
<div class="pref-choice" data-v="drama"><div class="t">剧情带货</div><div class="d">// 60s 默认档</div></div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">默认时长档</div>
<div class="val">
<div class="duration-row" id="pref-duration">
<span class="dur-chip" data-v="30">30s</span>
<span class="dur-chip" data-v="45">45s</span>
<span class="dur-chip selected" data-v="60">60s</span>
</div>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48); margin-left: 10px;">// 60s = 4 段 × 15s</span>
</div>
</div>
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
<div class="lbl" style="padding-top: 4px;">默认字幕样式</div>
<div class="val" style="display: block;">
<div class="pref-choices" id="pref-subtitle">
<div class="pref-choice selected" data-v="big-variety"><div class="t">大字综艺</div><div class="d">// 抖音热门</div></div>
<div class="pref-choice" data-v="clean-ec"><div class="t">简洁电商</div><div class="d">// 信息清晰</div></div>
<div class="pref-choice" data-v="premium"><div class="t">高级排版</div><div class="d">// 居中衬线</div></div>
<div class="pref-choice" data-v="bullet"><div class="t">弹幕轻量</div><div class="d">// 滚动出现</div></div>
<div class="pref-choice" data-v="emphasis"><div class="t">强调爆款</div><div class="d">// 高对比</div></div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">默认 BGM 库</div>
<div class="val">
<select class="select" id="pref-bgm" data-track>
<option value="kapian">抖音 Top10 卡点曲库</option>
<option value="emotion">情绪向 · 治愈/悬念</option>
<option value="urban">都市电子 · 通勤场景</option>
<option value="none">无 BGM</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">默认转场</div>
<div class="val">
<select class="select" id="pref-transition" data-track>
<option>无转场</option>
<option selected>淡入淡出 · 0.3s</option>
<option>滑动 · 0.3s</option>
<option>缩放 · 0.3s</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">导出水印<div class="lbl-sub">// VIP 可关闭</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="opt-watermark" checked disabled><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">右下角 · 流·Studio</span>
<a href="account.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">升级 VIP →</a>
</div>
</div>
</section>
<!-- ─── 显示 ─── -->
<section class="pane" id="sec-display">
<h3>显示</h3>
<div class="pane-desc">// 界面外观与语言</div>
<div class="form-row">
<div class="lbl">外观</div>
<div class="val">
<select class="select" id="pref-theme" data-track>
<option selected>跟随系统</option>
<option>浅色</option>
<option disabled>深色(V2)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">语言</div>
<div class="val">
<select class="select" id="pref-lang" data-track>
<option selected>简体中文</option>
<option disabled>English(V2)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">表格密度</div>
<div class="val">
<select class="select" id="pref-density" data-track>
<option>紧凑</option>
<option selected>标准</option>
<option>宽松</option>
</select>
</div>
</div>
</section>
<!-- ─── 危险操作 ─── -->
<section class="pane danger" id="sec-danger">
<h3>危险操作</h3>
<div class="pane-desc">// 这些操作不可撤销,请确认后再执行</div>
<div class="form-row">
<div class="lbl">导出我的数据<div class="lbl-sub">// 项目 + 资产元数据</div></div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 准备时间约 24 小时,完成后邮件通知</span>
<button class="btn btn-sm" style="margin-left: auto;" onclick="Shell.toast('已申请导出', '约 24 小时后邮件发送')">申请导出</button>
</div>
</div>
<div class="form-row">
<div class="lbl">退出登录</div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 仅退出当前设备,数据保留</span>
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('确定退出登录?')){Shell.toast('已退出','正在跳转登录页');setTimeout(()=>location.href='login.html',600);}">退出登录</button>
</div>
</div>
<div class="form-row">
<div class="lbl">注销账号</div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 团队余额清零、所有项目作为孤儿归档</span>
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('彻底注销当前账号? 此操作不可恢复')) Shell.toast('已提交注销申请', '24 小时内人工复核')">注销账号</button>
</div>
</div>
</section>
<div style="text-align: center; padding: 24px 0 8px; color: var(--black-alpha-32); font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;">
// 流·Studio · v2.1 · build 20260521
</div>
</main>
</div>
<!-- ─── 上传头像 modal ─── -->
<div class="modal-bg" id="avatar-up-bg" onclick="if(event.target===this)Shell.closeModal('avatar-up-bg')">
<div class="modal av-up-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
</div>
<div class="ti">上传头像<span>// 用于个人主页、评论与团队展示</span></div>
</div>
<div class="modal-b np-body">
<div class="av-up-preview-row">
<div class="av-up-preview" id="av-up-preview"></div>
<div class="av-up-preview-meta">
<div class="t" id="av-up-preview-name">当前头像 · 默认</div>
<div class="d" id="av-up-preview-info">// 系统生成 · 取姓氏首字</div>
</div>
</div>
<div class="upload-zone" id="av-up-zone" tabindex="0" role="button" aria-label="点击或拖入图片上传">
<span class="uz-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="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 class="uz-hint">JPG / PNG / WebP · ≤ 2 MB · 推荐 256 × 256</span>
<input type="file" id="av-up-file" accept="image/jpeg,image/png,image/webp" hidden>
</div>
<div class="av-up-rules">
<div class="li">最大 2 MB · 长宽比建议 1:1 · 系统会自动裁切为圆形</div>
<div class="li">不要上传含他人肖像的图片,违规可能导致账号封停</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('avatar-up-bg')">取消</button>
<button class="btn btn-primary" type="button" id="av-up-confirm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
确认使用
</button>
</div>
</div>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script>
Shell.render({
active: 'settings',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '设置' }]
});
/* ─── 配置 ─── */
const SECTIONS = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display', 'sec-danger'];
const navLinks = document.querySelectorAll('.settings-nav a[data-jump]');
/* ─── 1. 点击 nav → 只显示对应 section,其余隐藏 + 同步 URL hash ─── */
function showSection(id) {
if (!SECTIONS.includes(id)) id = SECTIONS[0];
SECTIONS.forEach(sid => {
const el = document.getElementById(sid);
if (el) el.style.display = (sid === id) ? '' : 'none';
});
navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));
if (location.hash.slice(1) !== id) {
history.replaceState(null, '', '#' + id);
}
// 切换面板后回到顶端,避免长面板留下的滚动位置错乱
window.scrollTo({ top: 0, behavior: 'instant' });
}
navLinks.forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
showSection(a.dataset.jump);
a.focus();
});
a.addEventListener('keydown', e => {
const idx = [...navLinks].indexOf(a);
if (e.key === 'ArrowDown') { e.preventDefault(); navLinks[(idx + 1) % navLinks.length].focus(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); navLinks[(idx - 1 + navLinks.length) % navLinks.length].focus(); }
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); a.click(); }
});
});
/* ─── 2. 初始加载:读 URL hash 显示对应 section(无 hash 时默认 sec-profile) ─── */
(function initSection() {
const hash = location.hash.slice(1);
showSection(SECTIONS.includes(hash) ? hash : 'sec-profile');
})();
/* hash 外部变化(如浏览器前进后退)也跟着切 */
window.addEventListener('hashchange', () => {
const hash = location.hash.slice(1);
if (SECTIONS.includes(hash)) showSection(hash);
});
/* ─── 4. 偏好 chip 选择 ─── */
function bindChoice(containerId) {
const ct = document.getElementById(containerId);
if (!ct) return;
ct.querySelectorAll('.pref-choice').forEach(c => {
c.addEventListener('click', () => {
ct.querySelectorAll('.pref-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
markDirty(c);
});
});
}
bindChoice('pref-template');
bindChoice('pref-subtitle');
document.querySelectorAll('#pref-duration .dur-chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#pref-duration .dur-chip').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
markDirty(c);
});
});
/* ─── 5. dirty state · 追踪改了哪几节 + save btn 显示变更条数 ─── */
const dirtyFields = new Set(); // 改过的字段 id 集合
const dirtySections = new Set(); // 涉及的 section id 集合
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('save-cancel');
const saveBtnDefaultLabel = saveBtn.innerHTML;
function sectionOf(el) {
const sec = el.closest('section[id]');
return sec ? sec.id : null;
}
function syncSaveBtn() {
const n = dirtyFields.size;
if (n === 0) {
saveBtn.disabled = true;
cancelBtn.disabled = true;
saveBtn.style.opacity = '.5';
saveBtn.style.cursor = 'not-allowed';
cancelBtn.style.opacity = '.5';
cancelBtn.style.cursor = 'not-allowed';
saveBtn.innerHTML = saveBtnDefaultLabel;
} else {
saveBtn.disabled = false;
cancelBtn.disabled = false;
saveBtn.style.opacity = '';
saveBtn.style.cursor = '';
cancelBtn.style.opacity = '';
cancelBtn.style.cursor = '';
saveBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg> 保存所有变更 <span style="opacity:.75;font-family:var(--font-mono);font-size:11px;margin-left:4px;">· ${n} 项</span>`;
}
// nav 上加 dirty dot
navLinks.forEach(a => a.classList.toggle('has-changes', dirtySections.has(a.dataset.jump)));
}
function markDirty(el) {
const id = el.id || (el.dataset && el.dataset.v) || ('anon-' + Math.random().toString(36).slice(2, 8));
if (dirtyFields.has(id)) return;
dirtyFields.add(id);
const sec = sectionOf(el);
if (sec) dirtySections.add(sec);
syncSaveBtn();
}
function clearDirty() {
dirtyFields.clear();
dirtySections.clear();
syncSaveBtn();
}
document.querySelectorAll('[data-track], input[type="checkbox"], select').forEach(el => {
el.addEventListener('change', () => markDirty(el));
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', () => markDirty(el));
});
/* ─── 6. 表单校验:必填 + 邮箱格式 ─── */
const VALIDATORS = {
'prof-name': v => v.trim().length === 0 ? '显示名称不能为空' : (v.length > 20 ? '显示名称最多 20 字' : null),
'prof-email': v => !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) ? '邮箱格式不正确' : null,
'prof-phone': v => !/^\d{3}\*{4}\d{4}$|^1\d{10}$/.test(v.trim()) ? '手机号格式不正确' : null,
};
function validate() {
const errors = [];
Object.entries(VALIDATORS).forEach(([id, fn]) => {
const el = document.getElementById(id);
if (!el) return;
const err = fn(el.value);
el.classList.toggle('invalid', !!err);
if (err) errors.push(`${id}: ${err}`);
});
return errors;
}
/* ─── 7. 保存 / 取消 ─── */
saveBtn.addEventListener('click', () => {
if (dirtyFields.size === 0) return;
const errors = validate();
if (errors.length) {
Shell.toast('校验未通过', errors[0].split(': ')[1] + ' · 共 ' + errors.length + ' 项');
return;
}
Shell.toast('设置已保存', `${dirtyFields.size} 项变更已生效 · 涉及 ${dirtySections.size} 个分区`);
clearDirty();
});
cancelBtn.addEventListener('click', () => {
if (dirtyFields.size === 0) return;
if (confirm(`放弃 ${dirtyFields.size} 项未保存的变更?`)) location.reload();
});
/* ─── 8. 离开页面前提醒(有未保存变更时)─── */
window.addEventListener('beforeunload', e => {
if (dirtyFields.size > 0) {
e.preventDefault();
e.returnValue = '';
}
});
/* ─── 9. invalid input 视觉反馈样式 ─── */
const _invalidStyle = document.createElement('style');
_invalidStyle.textContent = `
.input.invalid { border-color: var(--accent-crimson); box-shadow: 0 0 0 3px rgba(235,52,36,.12); }
.input.invalid:focus { border-color: var(--accent-crimson); box-shadow: 0 0 0 3px rgba(235,52,36,.18); }
`;
document.head.appendChild(_invalidStyle);
/* ─── 10. nav badge 计数自动同步(从 section 内 switch 开启数 / device 数等) ─── */
function syncBadges() {
const secNotify = document.getElementById('sec-notify');
if (secNotify) {
const onCount = secNotify.querySelectorAll('input[type="checkbox"]:checked').length;
const totalCount = secNotify.querySelectorAll('input[type="checkbox"]').length;
const badge = document.querySelector('.settings-nav a[data-jump="sec-notify"] .nav-badge');
if (badge) badge.textContent = `${onCount}/${totalCount}`;
}
const secSecurity = document.getElementById('sec-security');
if (secSecurity) {
const devCount = secSecurity.querySelectorAll('.device-row').length;
const badge = document.querySelector('.settings-nav a[data-jump="sec-security"] .nav-badge');
if (badge) badge.textContent = `${devCount} 设备`;
}
}
syncBadges();
document.querySelectorAll('#sec-notify input[type="checkbox"]').forEach(cb => cb.addEventListener('change', syncBadges));
/* ─── 11. 头像上传 modal ─── */
const DEFAULT_AVATAR = { kind: 'default', label: '李', name: '默认', info: '// 系统生成 · 取姓氏首字' };
let _draftAvatar = { ...DEFAULT_AVATAR };
let _currentAvatar = { ...DEFAULT_AVATAR };
const avPreview = document.getElementById('av-up-preview');
const avPreviewName = document.getElementById('av-up-preview-name');
const avPreviewInfo = document.getElementById('av-up-preview-info');
const avProfPreview = document.getElementById('prof-avatar-preview');
const avZone = document.getElementById('av-up-zone');
const avFile = document.getElementById('av-up-file');
const avConfirm = document.getElementById('av-up-confirm');
const avResetBtn = document.getElementById('prof-avatar-reset');
function paintPreview() {
if (_draftAvatar.kind === 'image') {
avPreview.innerHTML = `<img src="${_draftAvatar.src}" alt="头像预览">`;
} else {
avPreview.innerHTML = '';
avPreview.textContent = _draftAvatar.label || '李';
}
avPreviewName.textContent = `预览 · ${_draftAvatar.name}`;
avPreviewInfo.textContent = _draftAvatar.info;
}
function applyAvatarTo(el, avatar) {
if (avatar.kind === 'image') {
el.innerHTML = `<img src="${avatar.src}" alt="头像" style="width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;">`;
} else {
el.innerHTML = '';
el.textContent = avatar.label || '李';
}
}
avZone.addEventListener('click', () => avFile.click());
avZone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); avFile.click(); } });
avZone.addEventListener('dragover', e => { e.preventDefault(); avZone.classList.add('dragover'); });
avZone.addEventListener('dragleave', () => avZone.classList.remove('dragover'));
avZone.addEventListener('drop', e => {
e.preventDefault();
avZone.classList.remove('dragover');
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (f) handleAvatarFile(f);
});
avFile.addEventListener('change', () => { if (avFile.files[0]) handleAvatarFile(avFile.files[0]); });
function handleAvatarFile(f) {
if (!/^image\/(jpeg|png|webp)$/.test(f.type)) {
Shell.toast('格式不支持', '仅支持 JPG / PNG / WebP');
return;
}
if (f.size > 2 * 1024 * 1024) {
Shell.toast('文件过大', `${(f.size / 1024 / 1024).toFixed(2)} MB · 限 2 MB`);
return;
}
const url = URL.createObjectURL(f);
_draftAvatar = { kind: 'image', src: url, name: f.name, info: `// ${(f.size / 1024).toFixed(0)} KB · ${f.type.split('/')[1].toUpperCase()}` };
paintPreview();
}
avConfirm.addEventListener('click', () => {
_currentAvatar = { ..._draftAvatar };
applyAvatarTo(avProfPreview, _currentAvatar);
Shell.closeModal('avatar-up-bg');
Shell.toast('头像已更新', _currentAvatar.name);
markDirty(avProfPreview);
});
avResetBtn.addEventListener('click', () => {
_currentAvatar = { ...DEFAULT_AVATAR };
applyAvatarTo(avProfPreview, _currentAvatar);
_draftAvatar = { ...DEFAULT_AVATAR };
paintPreview();
Shell.toast('已恢复默认头像', _currentAvatar.name);
markDirty(avProfPreview);
});
// 打开 modal 时同步当前到 draft + preview
document.querySelector('.av-actions button.btn-sm').addEventListener('click', () => {
_draftAvatar = { ..._currentAvatar };
paintPreview();
});
paintPreview();
/* ─── 12. 实时邮箱 / 名称 input 校验反馈(blur 时显示错) ─── */
['prof-name', 'prof-email', 'prof-phone'].forEach(id => {
const el = document.getElementById(id);
if (!el || !VALIDATORS[id]) return;
el.addEventListener('blur', () => {
const err = VALIDATORS[id](el.value);
el.classList.toggle('invalid', !!err);
});
el.addEventListener('input', () => {
if (el.classList.contains('invalid')) {
const err = VALIDATORS[id](el.value);
if (!err) el.classList.remove('invalid');
}
});
});
</script>
</body>
</html>