前端修复: - calendar_page: 移除不存在的 JournalEntry.content getter - responsive_scaffold: 移除不存在的 notchThickness 参数 - splash_page: SingleTickerProvider → TickerProvider (多 AnimationController) - profile_page: UserRoleType.name → .code (修复运行时崩溃) - 导入缺失的 user.dart 后端修复: - class_service: generate_class_code 取 UUID 后6位(随机部分)避免碰撞 - diary_role_seed: 移除不存在的 id 列,使用复合主键 ON CONFLICT 基础设施: - config/default.toml: CORS 改为通配符(开发模式) - scripts/dev.sh: 统一启动脚本(自动清理端口) - docs/opendesign/: Open Design 设计规格 HTML 原型稿 验证结果: flutter analyze 0 error, cargo test 77/77 通过, 17个页面全部渲染正常
104 lines
3.8 KiB
JavaScript
104 lines
3.8 KiB
JavaScript
/* ─────────────────────────────────────────────────────────
|
|
* 暖记 Theme Switcher
|
|
* Handles theme toggling + localStorage persistence
|
|
* ───────────────────────────────────────────────────────── */
|
|
(function () {
|
|
var THEMES = [
|
|
{ id: 'warm', name: '暖阳', swatch: '#E07A5F' },
|
|
{ id: 'pine', name: '松风', swatch: '#4A7B9D' }
|
|
];
|
|
var STORAGE_KEY = 'warmnotes-theme';
|
|
|
|
function getCurrent() {
|
|
return localStorage.getItem(STORAGE_KEY) || 'warm';
|
|
}
|
|
|
|
function apply(id) {
|
|
if (id === 'warm') {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme', id);
|
|
}
|
|
localStorage.setItem(STORAGE_KEY, id);
|
|
}
|
|
|
|
function cycle() {
|
|
var cur = getCurrent();
|
|
var idx = 0;
|
|
for (var i = 0; i < THEMES.length; i++) {
|
|
if (THEMES[i].id === cur) { idx = i; break; }
|
|
}
|
|
var next = THEMES[(idx + 1) % THEMES.length];
|
|
apply(next.id);
|
|
updateBtn(next);
|
|
}
|
|
|
|
function updateBtn(theme) {
|
|
var btn = document.getElementById('theme-toggle-btn');
|
|
if (!btn) return;
|
|
var dot = btn.querySelector('.theme-dot');
|
|
var label = btn.querySelector('.theme-label');
|
|
if (dot) dot.style.background = theme.swatch;
|
|
if (label) label.textContent = theme.name;
|
|
btn.setAttribute('aria-label', 'Switch theme (current: ' + theme.name + ')');
|
|
}
|
|
|
|
// Apply stored theme immediately (prevent flash)
|
|
var saved = getCurrent();
|
|
apply(saved);
|
|
|
|
// Wait for DOM then inject toggle button
|
|
function init() {
|
|
if (document.getElementById('theme-toggle-btn')) return;
|
|
|
|
var cur = THEMES.filter(function (t) { return t.id === saved; })[0] || THEMES[0];
|
|
|
|
var wrap = document.createElement('div');
|
|
wrap.id = 'theme-toggle-btn';
|
|
wrap.setAttribute('role', 'button');
|
|
wrap.setAttribute('tabindex', '0');
|
|
wrap.setAttribute('aria-label', 'Switch theme (current: ' + cur.name + ')');
|
|
wrap.onclick = function () { saved = getCurrent(); cycle(); };
|
|
wrap.onkeydown = function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); cycle(); } };
|
|
|
|
var style = document.createElement('style');
|
|
style.textContent = '\
|
|
#theme-toggle-btn{position:fixed;top:8px;right:8px;z-index:10000;display:flex;align-items:center;gap:6px;\
|
|
padding:5px 12px 5px 8px;border-radius:20px;border:1px solid var(--border);\
|
|
background:var(--surface);color:var(--fg);font-size:11px;font-family:var(--font-body);\
|
|
cursor:pointer;box-shadow:var(--elev-medium);transition:all .2s ease;user-select:none;line-height:1;}\
|
|
#theme-toggle-btn:hover{box-shadow:var(--elev-float);}\
|
|
#theme-toggle-btn:focus-visible{outline:none;box-shadow:var(--focus-ring);}\
|
|
.theme-dot{width:14px;height:14px;border-radius:50%;flex-shrink:0;border:1.5px solid var(--border);}\
|
|
.theme-label{white-space:nowrap;font-weight:600;}';
|
|
document.head.appendChild(style);
|
|
|
|
var dot = document.createElement('span');
|
|
dot.className = 'theme-dot';
|
|
dot.style.background = cur.swatch;
|
|
|
|
var label = document.createElement('span');
|
|
label.className = 'theme-label';
|
|
label.textContent = cur.name;
|
|
|
|
wrap.appendChild(dot);
|
|
wrap.appendChild(label);
|
|
document.body.appendChild(wrap);
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
// Cross-tab sync
|
|
window.addEventListener('storage', function (e) {
|
|
if (e.key === STORAGE_KEY && e.newValue) {
|
|
apply(e.newValue);
|
|
var t = THEMES.filter(function (th) { return th.id === e.newValue; })[0] || THEMES[0];
|
|
updateBtn(t);
|
|
}
|
|
});
|
|
})();
|