Files
2026-08-11 22:07:55 +08:00

416 lines
21 KiB
HTML
Raw Permalink 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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>控制面板 - Cal Display</title>
<link rel="stylesheet" href="/static/css/control.css">
</head>
<body>
<div id="app">
<header>
<h1>Cal Display 控制面板 <span id="current-user-badge" style="font-size:13px;font-weight:400;color:var(--text-dim);margin-left:8px;"></span></h1>
<nav id="tabs">
<button class="tab active" data-tab="calendar">日历</button>
<button class="tab" data-tab="weather">天气</button>
<button class="tab" data-tab="alarms">闹钟</button>
<button class="tab" data-tab="pomodoro">番茄钟</button>
<button class="tab" data-tab="users" id="users-tab" style="display:none;">用户管理</button>
<button class="tab" data-tab="settings">设置</button>
</nav>
</header>
<main>
<!-- Calendar Tab -->
<section id="tab-calendar" class="tab-content active">
<!-- Groups -->
<div class="section">
<h3>日历分组</h3>
<div id="group-list"></div>
<form id="group-form" class="inline-form">
<input type="text" id="group-name" placeholder="分组名称" required>
<input type="color" id="group-color" value="#4A90D9">
<button type="submit">添加分组</button>
</form>
</div>
<!-- Upload ICS -->
<div class="section">
<h3>上传 ICS 文件</h3>
<form id="upload-form" class="inline-form">
<select id="upload-group"></select>
<input type="text" id="upload-name" placeholder="来源名称" required>
<input type="file" id="upload-file" accept=".ics" required>
<button type="submit">上传</button>
</form>
</div>
<!-- Subscribe ICS -->
<div class="section">
<h3>订阅 ICS 链接</h3>
<form id="subscribe-form" class="inline-form">
<select id="subscribe-group"></select>
<input type="text" id="subscribe-name" placeholder="来源名称" required>
<input type="url" id="subscribe-url" placeholder="https://example.com/calendar.ics" required>
<button type="submit">订阅</button>
</form>
</div>
<!-- Reminders -->
<div class="section">
<h3>提醒设置</h3>
<form id="reminder-form" class="inline-form">
<select id="reminder-group"></select>
<input type="number" id="reminder-minutes" value="15" min="0" max="1440" style="width:70px">
<label>分钟前</label>
<label><input type="checkbox" id="reminder-enabled" checked> 启用</label>
<button type="submit">保存</button>
</form>
</div>
<!-- Manual Add Event -->
<div class="section">
<h3>手动添加日程</h3>
<form id="event-form" class="inline-form">
<select id="event-group"></select>
<input type="text" id="event-summary" placeholder="日程标题" required>
<input type="datetime-local" id="event-dtstart" required>
<input type="datetime-local" id="event-dtend">
<input type="text" id="event-location" placeholder="地点 (可选)">
<button type="submit">添加</button>
</form>
</div>
<!-- Group Events -->
<div class="section">
<h3>分组日程管理</h3>
<div class="inline-form" style="margin-bottom:12px;">
<select id="browse-group"></select>
<button type="button" id="browse-events-btn">查看日程</button>
</div>
<div id="group-events-list"></div>
</div>
</section>
<!-- Weather Tab -->
<section id="tab-weather" class="tab-content">
<!-- JWT Key Management -->
<div class="section">
<h3>JWT 密钥管理(和风天气)</h3>
<p style="font-size:13px;color:var(--text-dim);margin-bottom:12px;">
生成本地 Ed25519 密钥对,将公钥复制到
<a href="https://console.qweather.com" target="_blank" style="color:var(--accent);">和风天气控制台</a>
→ 凭据管理 → 添加凭据。
</p>
<div class="form-row">
<label>公钥(复制到和风控制台)</label>
<textarea id="weather-public-key" rows="4" readonly
style="width:100%;font-family:monospace;font-size:12px;resize:none;"></textarea>
</div>
<button type="button" id="generate-keys-btn">生成密钥对</button>
<span id="key-gen-status" style="margin-left:10px;font-size:13px;color:var(--text-dim);"></span>
</div>
<!-- City Search -->
<div class="section">
<h3>城市搜索</h3>
<div class="inline-form">
<input type="text" id="weather-city-search" placeholder="输入城市名搜索…" style="min-width:200px;">
<button type="button" id="city-search-btn">搜索</button>
</div>
<div id="city-results" style="margin-top:8px;"></div>
</div>
<!-- QWeather Config -->
<div class="section">
<h3>和风天气配置</h3>
<form id="weather-form">
<!-- JWT fields(推荐;未填时自动回退到 API Key) -->
<div id="jwt-fields">
<div class="form-row">
<label>Credential ID (kid)</label>
<input type="text" id="weather-kid" placeholder="从和风控制台获取">
</div>
<div class="form-row">
<label>Project ID (sub)</label>
<input type="text" id="weather-sub" placeholder="从和风控制台获取">
</div>
<div class="form-row">
<label>专属 API Host(必填!)</label>
<input type="text" id="weather-host" placeholder="如 abc1234.def.qweatherapi.com">
<div style="font-size:12px;color:var(--accent-red);margin-top:4px;">
⚠️ 共享域名已弃用!登录 <a href="https://console.qweather.com" target="_blank" style="color:var(--accent);">和风控制台</a>
→ 产品管理 → 你的项目 → API Host,复制完整域名填入此处
</div>
</div>
</div>
<!-- API Key(可选):和风旧版 API Key,JWT 配置完整时自动优先 JWT -->
<div id="legacy-key-field">
<div class="form-row">
<label>API Key(可选)</label>
<input type="text" id="weather-apikey" placeholder="输入和风 API Key(未配置 JWT 时使用)">
</div>
</div>
<!-- Common fields -->
<div class="form-row">
<label>城市名称</label>
<input type="text" id="weather-city" placeholder="Beijing">
</div>
<div class="form-row">
<label>纬度</label>
<input type="number" id="weather-lat" step="0.0001" placeholder="39.9042">
</div>
<div class="form-row">
<label>经度</label>
<input type="number" id="weather-lon" step="0.0001" placeholder="116.4074">
</div>
<div class="form-row">
<label>单位</label>
<select id="weather-units">
<option value="metric">摄氏度 (metric)</option>
<option value="imperial">华氏度 (imperial)</option>
</select>
</div>
<button type="submit">保存配置</button>
<button type="button" id="weather-refresh" class="btn-secondary">立即刷新天气</button>
<span id="weather-save-status" style="margin-left:10px;font-size:13px;color:var(--text-dim);"></span>
</form>
</div>
<!-- Current weather preview -->
<div class="section">
<h3>当前天气预览</h3>
<div id="weather-preview" style="font-size:14px;color:var(--text-dim);">加载中...</div>
</div>
</section>
<!-- Alarms Tab -->
<section id="tab-alarms" class="tab-content">
<!-- Alarms -->
<div class="section">
<h3>闹钟列表</h3>
<div id="alarm-list"></div>
<form id="alarm-form" class="inline-form">
<input type="text" id="alarm-label" placeholder="标签">
<input type="time" id="alarm-time" required>
<select id="alarm-sound">
<option value="default">默认铃声</option>
</select>
<label><input type="checkbox" id="alarm-once"> 仅一次</label>
<div class="day-picker" id="alarm-days-picker">
<label><input type="checkbox" value="1" checked>一</label>
<label><input type="checkbox" value="2" checked>二</label>
<label><input type="checkbox" value="3" checked>三</label>
<label><input type="checkbox" value="4" checked>四</label>
<label><input type="checkbox" value="5" checked>五</label>
<label><input type="checkbox" value="6" checked>六</label>
<label><input type="checkbox" value="7" checked>日</label>
</div>
<button type="submit">添加闹钟</button>
</form>
</div>
<!-- Ringtone Upload -->
<div class="section">
<h3>自定义铃声</h3>
<form id="ringtone-form" class="inline-form">
<input type="file" id="ringtone-file" accept=".mp3,.wav,.ogg" required>
<button type="submit">上传铃声</button>
</form>
<div id="ringtone-list" style="margin-top:8px;font-size:13px;color:var(--text-dim);"></div>
</div>
</section>
<!-- Pomodoro Tab -->
<section id="tab-pomodoro" class="tab-content">
<div class="section">
<h3>番茄钟设置</h3>
<div id="pomodoro-status" style="margin-bottom:12px;font-size:14px;color:var(--text-dim);"></div>
<form id="pomodoro-form" class="inline-form">
<label>工作 <input type="number" id="pomodoro-work" value="25" min="1" max="120" style="width:70px"> 分钟</label>
<label>休息 <input type="number" id="pomodoro-break" value="5" min="1" max="60" style="width:70px"> 分钟</label>
<label>长休息 <input type="number" id="pomodoro-long-break" value="15" min="1" max="120" style="width:70px"> 分钟</label>
<label>每 <input type="number" id="pomodoro-cycles-before-long" value="4" min="1" max="20" style="width:60px"> 组后长休息</label>
<label><input type="text" id="pomodoro-work-label" placeholder="工作中" style="width:80px"> 工作标签</label>
<label><input type="text" id="pomodoro-break-label" placeholder="休息中" style="width:80px"> 休息标签</label>
<label><input type="checkbox" id="pomodoro-display-controls" checked> 允许显示端控制</label>
<button type="submit">保存设置</button>
</form>
<div style="margin-top:12px;display:flex;gap:8px;">
<button type="button" id="pomodoro-start-btn">开始</button>
<button type="button" id="pomodoro-pause-btn" class="btn-secondary">暂停</button>
<button type="button" id="pomodoro-skip-btn" class="btn-secondary">跳过</button>
<button type="button" id="pomodoro-stop-btn" class="btn-danger">停止</button>
</div>
</div>
</section>
<!-- Users Tab (admin only) -->
<section id="tab-users" class="tab-content">
<div class="section">
<h3>用户管理</h3>
<p style="font-size:13px;color:var(--text-dim);margin-bottom:12px;">
仅管理员可用。注册/编辑/删除用户均为高危操作,每次操作前需二次验证管理员身份。
新注册用户的全部数据相互隔离,且不拥有用户管理权限。
</p>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<button type="button" id="user-add-admin" class="btn-primary">👨‍💼 管理员代注册</button>
<button type="button" id="user-add-temp" class="btn-secondary">🕐 创建临时账户(用户填写信息模式)</button>
<button type="button" id="user-refresh-btn" class="btn-secondary">刷新列表</button>
<button type="button" id="kick-all-users-btn" class="btn-danger">🚫 踢掉所有用户</button>
</div>
<p style="font-size:13px;color:#E05555;margin-bottom:12px;">🚫 踢掉所有用户:所有用户的控制端与显示端立即全部下线(含当前设备),需二次验证身份。</p>
<div id="user-list"></div>
</div>
</section>
<!-- Settings Tab -->
<section id="tab-settings" class="tab-content">
<div class="section">
<h3>主题模式</h3>
<p style="font-size:13px;color:var(--text-dim);margin-bottom:12px;">
跟随账户独立设置,显示端实时同步生效。
<strong>自动</strong> 模式按你天气设置中的城市经纬度计算日出日落,白天浅色、夜晚深色。
</p>
<div class="inline-form">
<select id="theme-mode">
<option value="dark">深色模式</option>
<option value="light">浅色模式</option>
<option value="auto">自动(日出日落)</option>
</select>
<button type="button" id="theme-save-btn">保存主题</button>
</div>
</div>
<div class="section">
<h3>显示端模式</h3> <form id="mode-form" class="inline-form">
<select id="display-mode">
<option value="default">默认页面</option>
<option value="custom">自定义 HTML</option>
</select>
<button type="submit">切换</button>
</form>
<div id="html-editor-wrap" style="margin-top:12px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<label style="font-size:13px;color:var(--text-dim);">当前编辑: <strong id="html-editor-label">HTML 0</strong></label>
<div style="display:flex;gap:6px;">
<button type="button" class="small-btn html-tab-btn active" data-idx="0">HTML 0</button>
<button type="button" class="small-btn html-tab-btn" data-idx="1">HTML 1</button>
<button type="button" class="small-btn html-tab-btn" data-idx="2">HTML 2</button>
<button type="button" class="small-btn html-tab-btn" data-idx="3">HTML 3</button>
</div>
</div>
<textarea id="custom-html-editor" rows="8" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:6px;font-family:monospace;font-size:13px;resize:vertical;" placeholder="在此输入 HTML..."></textarea>
<div style="margin-top:8px;display:flex;gap:8px;">
<button type="button" id="save-html-btn">保存当前模板</button>
<button type="button" id="apply-html-btn" class="btn-secondary">使用当前 HTML</button>
<button type="button" id="refresh-display-btn" class="btn-secondary">强制刷新显示端</button>
</div>
</div>
<div style="margin-top:8px;display:flex;gap:8px;">
<button type="button" id="kick-all-btn" class="btn-danger">踢掉当前账号的所有设备</button>
</div>
<div style="margin-top:6px;font-size:12px;color:var(--text-dim);">包含当前控制端与所有显示端,全部立即下线。</div>
<div id="mode-msg" style="margin-top:8px;font-size:13px;color:var(--text-dim);"></div>
</div>
<div class="section" id="db-admin-section" style="display:none;">
<h3>数据库管理</h3>
<p style="font-size:13px;color:var(--text-dim);margin-bottom:12px;">
仅 Root 可用。可导入旧版 SQLite 数据库文件(*.db)或本系统导出的备份(JSON),导入会<strong>覆盖当前全部数据(含所有账户的登录凭据)</strong>(单事务执行,失败自动回滚、不影响现有数据),且<strong>所有设备将下线需重新登录</strong>。导出的备份同样包含凭据,请妥善保管。请勿导入不可信的备份文件。
</p>
<div style="font-size:13px;margin-bottom:10px;">
当前状态: <span id="db-status" style="color:var(--text-dim);">加载中...</span>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
<button type="button" id="db-export-btn" class="btn-secondary">⬇️ 导出现有数据库</button>
</div>
<form id="db-import-form" class="inline-form">
<input type="file" id="db-import-file" accept=".db,.sqlite,.sqlite3,.json" style="max-width:260px;">
<button type="submit" id="db-import-btn" class="btn-primary">⬆️ 导入数据库(自动识别 SQLite / 备份 JSON)</button>
</form>
<div id="db-admin-msg" style="margin-top:8px;font-size:13px;color:var(--text-dim);"></div>
</div>
<div class="section">
<h3>时区设置</h3>
<div style="font-size:13px;color:var(--text-dim);margin-bottom:10px;">
所有本地时间将根据此设置显示。使用 <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" target="_blank" style="color:var(--accent);">IANA 时区名称</a> 或 UTC+X 格式。
</div>
<form id="timezone-form" class="inline-form">
<input type="text" id="timezone-input" value="Asia/Shanghai" style="min-width:220px;" placeholder="Asia/Shanghai">
<button type="submit">保存时区</button>
<span id="timezone-msg" style="margin-left:10px;font-size:13px;color:var(--text-dim);"></span>
</form>
</div>
<div class="section">
<h3>手动推送提醒</h3>
<form id="notify-form" class="inline-form">
<input type="text" id="notify-title" placeholder="标题" value="提醒" required>
<input type="text" id="notify-message" placeholder="内容" required>
<label><input type="checkbox" id="notify-sound" checked> 播放声音</label>
<button type="submit">推送</button>
</form>
<div id="notify-msg" style="margin-top:8px;font-size:13px;color:var(--text-dim);"></div>
</div>
<div class="section">
<h3>双因素认证 (MFA)</h3>
<div id="mfa-status" style="font-size:13px;color:var(--text-dim);margin-bottom:12px;">加载中...</div>
<!-- Passkey -->
<div style="margin-bottom:16px;padding:14px;border:1px solid var(--border);border-radius:8px;">
<h4 style="margin-bottom:8px;font-size:14px;">🔑 Passkey (设备密钥)</h4>
<p style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">
使用指纹、面容或安全密钥快速登录。
</p>
<button type="button" id="mfa-register-passkey" class="btn-secondary">注册 Passkey</button>
<button type="button" id="mfa-remove-passkey" class="btn-danger" style="display:none;">移除 Passkey</button>
<div id="mfa-passkey-status" style="font-size:13px;color:var(--text-dim);margin-top:6px;"></div>
</div>
<!-- TOTP -->
<div style="margin-bottom:16px;padding:14px;border:1px solid var(--border);border-radius:8px;">
<h4 style="margin-bottom:8px;font-size:14px;">📱 TOTP (身份验证器)</h4>
<p style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">
使用 Google Authenticator、Microsoft Authenticator 等应用。
</p>
<button type="button" id="mfa-setup-totp" class="btn-secondary">设置 TOTP</button>
<button type="button" id="mfa-remove-totp" class="btn-danger" style="display:none;">停用 TOTP</button>
<div id="mfa-totp-area" style="display:none;margin-top:12px;padding:12px;background:#fafafa;border-radius:8px;">
<div id="mfa-totp-qr" style="text-align:center;"></div>
<div id="mfa-totp-secret" style="font-family:monospace;font-size:12px;text-align:center;margin:8px 0;"></div>
<div class="inline-form">
<input type="text" id="mfa-totp-code" placeholder="6 位验证码" maxlength="6" inputmode="numeric" style="width:140px;">
<button type="button" id="mfa-totp-verify-btn">验证</button>
</div>
<div id="mfa-totp-msg" style="font-size:13px;margin-top:6px;"></div>
</div>
<div id="mfa-totp-status" style="font-size:13px;color:var(--text-dim);margin-top:6px;"></div>
</div>
<!-- Recovery codes -->
<div id="mfa-recovery-section" style="display:none;margin-bottom:16px;padding:14px;background:#fff8e1;border:1px solid #ffe082;border-radius:8px;">
<h4 style="font-size:14px;color:#e65100;margin-bottom:8px;">🔐 恢复码</h4>
<p style="font-size:13px;color:#e65100;margin-bottom:8px;">每个恢复码仅可使用一次。请妥善保管。</p>
<div id="mfa-recovery-codes" style="font-family:monospace;font-size:13px;line-height:2;"></div>
<div style="margin-top:8px;display:flex;gap:8px;">
<button type="button" id="mfa-copy-recovery" class="small-btn">复制</button>
<button type="button" id="mfa-regenerate-recovery" class="small-btn">重新生成</button>
</div>
<div id="mfa-recovery-msg" style="font-size:13px;color:var(--accent-red);margin-top:6px;"></div>
</div>
</div>
<div class="section">
<h3>退出登录</h3>
<button type="button" id="logout-btn" class="btn-danger">退出登录</button>
</div>
</section>
</main>
</div>
<script src="/static/js/token.js"></script>
<script src="/static/js/theme.js"></script>
<script src="/static/js/passkey_bridge.js"></script>
<script src="/static/js/control.js"></script>
</body>
</html>