All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
789 lines
40 KiB
HTML
789 lines
40 KiB
HTML
<!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>
|