AirShelf/v2/settings.html
UI 设计 e293aa43be
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
2026-05-21 16:18:28 +08:00

525 lines
27 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">
<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); }
.settings-nav a:hover { background: var(--background-lighter); }
.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; }
/* ─── 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; }
</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">
<div class="nav-h">个人</div>
<a href="#sec-profile" class="active" data-jump="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>
个人信息
</a>
<a href="#sec-security" data-jump="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>
安全
</a>
<a href="#sec-notify" data-jump="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>
通知
</a>
<div class="nav-h" style="margin-top: 16px;">偏好</div>
<a href="#sec-pref" data-jump="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>
创作默认
</a>
<a href="#sec-display" data-jump="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>
显示
</a>
<div class="nav-h" style="margin-top: 16px;">账号</div>
<a href="#sec-danger" data-jump="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>
危险操作
</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"></div>
<div class="av-actions">
<button class="btn btn-sm" onclick="Shell.toast('上传头像', '占位 · 选择本地图片')">上传新头像</button>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('恢复默认头像', '已重置')">恢复默认</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>
<div class="form-row">
<div class="lbl">营销 / 产品更新</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-marketing"><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('已退出', '正在跳转登录页')">退出登录</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>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'settings',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '设置' }]
});
/* ─── 侧边 nav 高亮 + 滚动联动 ─── */
const sections = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display', 'sec-danger'];
const navLinks = document.querySelectorAll('.settings-nav a[data-jump]');
navLinks.forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const id = a.dataset.jump;
const el = document.getElementById(id);
if (el) {
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
const offset = 16;
if (contentEl) {
contentEl.scrollTo({ top: el.offsetTop - contentEl.offsetTop - offset, behavior: 'smooth' });
} else {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
});
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
const observer = new IntersectionObserver(entries => {
entries.forEach(en => {
if (en.isIntersecting) {
const id = en.target.id;
navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));
}
});
}, { root: contentEl || null, rootMargin: '-20% 0px -60% 0px', threshold: 0 });
sections.forEach(id => {
const el = document.getElementById(id);
if (el) observer.observe(el);
});
/* ─── 偏好 chip 选择 ─── */
function bindChoice(containerId, label) {
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();
});
});
}
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();
});
});
/* ─── dirty state ─── */
let dirty = false;
function markDirty() {
if (dirty) return;
dirty = true;
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('save-cancel');
[saveBtn, cancelBtn].forEach(b => {
b.disabled = false;
b.style.opacity = '';
b.style.cursor = '';
});
}
function clearDirty() {
dirty = false;
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('save-cancel');
[saveBtn, cancelBtn].forEach(b => {
b.disabled = true;
b.style.opacity = '.5';
b.style.cursor = 'not-allowed';
});
}
document.querySelectorAll('[data-track], input[type="checkbox"], select').forEach(el => {
el.addEventListener('change', markDirty);
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', markDirty);
});
document.getElementById('save-btn').addEventListener('click', () => {
if (!dirty) return;
Shell.toast('设置已保存', '所有变更已生效');
clearDirty();
});
document.getElementById('save-cancel').addEventListener('click', () => {
if (!dirty) return;
if (confirm('放弃未保存的变更?')) location.reload();
});
</script>
</body>
</html>