界面主题:5 套经典配色,按账号记住
系统设置里多一块「🎨 我的界面主题」,点卡片即时预览、保存后全站生效,不用重新登录。 - lib/theme.php(新增):主题注册表 ticket/indigo/forest/wine/midnight, 分别是「复古票根(暖橙,默认)/ 靛蓝 / 松林 / 绛红 / 暗夜(深色)」。 currentThemeKey() 以 session 为准,session 里没有就从 admins.theme 补读一次 —— 升级前就已经登录着的会话也能立刻用上自己账号存的主题。库里是脏值时回落默认主题。 - css/themes.css(新增):5 套调色板,全部以 [data-theme="<key>"] 规则块给出, 每套 16 个变量一个不少。**必须排在 css/style.css 之后加载**:两者优先级相同 (都是 0,1,0),靠后出现者胜。另加 --primary-rgb("R, G, B" 三元组), 页面里的淡色底/光晕一律 rgba(var(--primary-rgb), α),所以加一套主题只需动 CSS, 页面与 JS 一行都不用改;每套还要声明 color-scheme,否则暗色主题下原生控件 (日期选择器、下拉、滚动条)仍是刺眼的白底。 - css/style.css::root 保留默认主题那一份作兜底;导航底色改走 --nav-bg, 滚动条改走变量,按钮光晕改走 --primary-rgb。 - lib/db.php / lib/auth.php:admins 增列 theme(幂等 ALTER,老库自动补,空值 = 默认主题); setAdminTheme() 校验 key、写库、同步 session。 - settings.php:主题卡片区。每张预览卡自己套一层 data-theme,用的就是那套主题真实的 变量,因此「预览所见」与「换上去之后」必然一致,不必再抄一份色值;另有一小段 JS 在点选时改 <html data-theme> 做即时预览(没脚本时选中后提交同样生效)。 - 14 个页面:<html> 上输出 data-theme,并在 style.css 之后引入 themes.css; 内联样式里写死的橙色 rgba(230,126,34,…) 全部改走 rgba(var(--primary-rgb),…); 影院/观影人色板选中态的描边由 #333 改为 var(--text-primary)(暗色主题下才看得见)。 - annual_report.php:报告本身仍是深色「海报」版式,但点缀色跟随主题; Chart.js 画在 canvas 上读不到 CSS 变量,改为运行时从 <html> 的计算样式取色。 顺手修的两个坑: 1. 注释里**不能**出现 PHP 起止标签。PHP 的结束标签在 // 注释里同样会关闭 PHP 模式, 之后若再出现短标签就会被真的执行 —— lib/theme.php 的用法说明里写了完整标签示例, 导致该文件被 require 时直接 Fatal error、整站白屏,而 php -l 查不出来(只看语法)。 现在部署脚本里加了一道静态断言专门挡这个。 2. themesCssLink() 的返回值末尾补一个换行:PHP 会吃掉结束标签后面那一个换行, 不补的话渲染结果里这一行会和下一行黏成一行。 回归:theme_e2e 111/111(含老库自动补列、脏值回落、5 个选项渲染、切换后落库+落 session+ 立刻生效、跨账号隔离、非法 key 与注入被拒、themes.css 完整性、:root 与 ticket 一致、 无残留写死主色、无悬空 CSS 变量);reviews_e2e 84/84、fetch_movie 76/76、storage 196/196。 A/B 48 个响应:把「新增 head 行 / data-theme 属性 / 橙色→变量」三类等价替换归一化、 并把设置页新增区块整块删掉之后,**其余部分逐字节一致,零残留差异**。 线上 PHP 7.4.30 / 8.1.7 双版本冒烟全通过(迁移、5 套主题切换、跨账号隔离均验证, 写测试后已把 admin #1 的主题原样还原、其它账号一字未动)。
This commit is contained in:
@@ -67,3 +67,28 @@ function updateAdminPersonId($admin_id, $person_id) {
|
||||
$stmt = $db->prepare("UPDATE admins SET person_id = ? WHERE id = ?");
|
||||
return $stmt->execute([$person_id, $admin_id]);
|
||||
}
|
||||
|
||||
// 保存某个账号的界面主题(key 必须是 lib/theme.php 注册表里的)。
|
||||
// 同时更新 session,这样同一个请求后面渲染页面时就已经是新主题了。
|
||||
// 失败返回 false(非法 key、账号不存在、库报错都算失败)。
|
||||
function setAdminTheme($admin_id, $theme_key) {
|
||||
$admin_id = (int)$admin_id;
|
||||
if ($admin_id <= 0 || !isValidTheme($theme_key)) {
|
||||
return false;
|
||||
}
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("UPDATE admins SET theme = ? WHERE id = ?");
|
||||
if (!$stmt->execute([$theme_key, $admin_id]) || $stmt->rowCount() <= 0) {
|
||||
// rowCount 为 0 时可能是「账号不存在」,也可能是「主题没变」——后者不算失败
|
||||
$chk = $db->prepare("SELECT theme FROM admins WHERE id = ?");
|
||||
$chk->execute([$admin_id]);
|
||||
$rows = $chk->fetchAll(); // fetchAll:避免留下未读完的游标(会挂共享读锁)
|
||||
if (!$rows || (string)($rows[0]['theme'] ?? '') !== $theme_key) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ((int)($_SESSION['admin_id'] ?? 0) === $admin_id) {
|
||||
$_SESSION['admin_theme'] = $theme_key;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+8
-1
@@ -21,12 +21,15 @@ function initDB() {
|
||||
$db = getDB();
|
||||
|
||||
// 管理员表
|
||||
// theme:该账号选的界面配色主题(见 lib/theme.php 的主题注册表)。
|
||||
// 留空/NULL 表示用默认主题,所以老账号不需要回填数据。
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
avatar TEXT
|
||||
avatar TEXT,
|
||||
theme TEXT
|
||||
)
|
||||
");
|
||||
|
||||
@@ -163,6 +166,10 @@ function initDB() {
|
||||
try { $db->exec("ALTER TABLE review_comments ADD COLUMN parent_id INTEGER"); } catch (PDOException $e) {}
|
||||
try { $db->exec("ALTER TABLE review_comments ADD COLUMN reply_to_admin_id INTEGER"); } catch (PDOException $e) {}
|
||||
|
||||
// 新增:每个账号自己的界面配色主题(见 lib/theme.php)。
|
||||
// 只要空列,不需要给老账号回填任何数据 —— 空值即「用默认主题」。
|
||||
try { $db->exec("ALTER TABLE admins ADD COLUMN theme TEXT"); } catch (PDOException $e) {}
|
||||
|
||||
// 删顶层评论时要按 parent_id 找它下面的回复一并清掉,给它建个索引。
|
||||
// 这条索引必须放在上面的「补列」之后:老库的 review_comments 是旧结构、没有
|
||||
// parent_id 列,而上面建表用的是 IF NOT EXISTS(对已存在的表不生效),
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// 文件: lib/theme.php
|
||||
// 说明: 界面配色主题(5 套经典配色)。
|
||||
//
|
||||
// 主题是**每个账号自己的偏好**,存在 admins.theme 里(空 = 用默认主题)。
|
||||
// 颜色本身全部定义在 css/themes.css 的 [data-theme="<key>"] 规则里,
|
||||
// 本文件只负责「有哪些主题 / 当前用哪套 / 页面上怎么声明」,
|
||||
// 不重复存一份色值(唯一例外是下面 themeCatalog() 里给选色卡片用的
|
||||
// 一段渐变缩略图,那是纯装饰,不参与页面配色)。
|
||||
//
|
||||
// 页面接入方式(每个页面的 <html> 与样式表各加一处):
|
||||
// 1) <html lang="zh-CN"> 上紧跟着输出 themeAttr() 的返回值(它自带一个前导空格);
|
||||
// 2) css/style.css 的 link 之后,再输出一行 themesCssLink()。
|
||||
// themes.css 必须排在 style.css 之后 —— [data-theme] 与 style.css 里
|
||||
// 作为兜底的 :root 优先级相同,靠「后出现者胜」来覆盖。
|
||||
//
|
||||
// 注意:上面两行说明里**故意不写 PHP 短标签**。PHP 的「问号 + 右尖括号」
|
||||
// 在 // 注释里同样会关闭 PHP 模式,之后若再出现「左尖括号 + 问号 + 等号」
|
||||
// 就会被真的执行 —— 注释里写出完整标签示例,本文件被 require 时会直接
|
||||
// Fatal error,而 php -l 是查不出来的(它只看语法,不看语义)。
|
||||
// ============================================
|
||||
|
||||
// 主题注册表:key => [显示名, 一句话描述, 明暗]
|
||||
// 想加主题:这里加一项,并在 css/themes.css 里补一个同 key 的 [data-theme] 规则块
|
||||
// (规则块里那一组变量一个都不能少,缺哪个就会退回 style.css 的默认值)。
|
||||
function themeCatalog() {
|
||||
static $catalog = null;
|
||||
if ($catalog === null) {
|
||||
$catalog = [
|
||||
'ticket' => [
|
||||
'name' => '复古票根',
|
||||
'desc' => '暖橙 · 默认配色',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'indigo' => [
|
||||
'name' => '靛蓝',
|
||||
'desc' => '经典蓝 · 清爽利落',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'forest' => [
|
||||
'name' => '松林',
|
||||
'desc' => '墨绿 · 沉静耐看',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'wine' => [
|
||||
'name' => '绛红',
|
||||
'desc' => '酒红 · 复古胶片',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'midnight' => [
|
||||
'name' => '暗夜',
|
||||
'desc' => '深色 · 靛紫点缀',
|
||||
'scheme' => 'dark',
|
||||
],
|
||||
];
|
||||
}
|
||||
return $catalog;
|
||||
}
|
||||
|
||||
// 默认主题(没选过 / 选的 key 不认识了,都落到它上面)
|
||||
function defaultThemeKey() {
|
||||
return 'ticket';
|
||||
}
|
||||
|
||||
// key 是否有效
|
||||
function isValidTheme($key) {
|
||||
return is_string($key) && isset(themeCatalog()[$key]);
|
||||
}
|
||||
|
||||
// 取某套主题的元信息;非法 key 返回默认主题的(页面渲染不该因为脏数据而炸)
|
||||
function themeInfo($key) {
|
||||
$catalog = themeCatalog();
|
||||
return isValidTheme($key) ? $catalog[$key] : $catalog[defaultThemeKey()];
|
||||
}
|
||||
|
||||
// 当前生效的主题 key。
|
||||
// 优先读 session(登录时/切换主题时写入),session 里还没有就回库里补一次 ——
|
||||
// 这样「本次升级之前就已经登录着」的会话也能立刻用上自己账号存的主题。
|
||||
function currentThemeKey() {
|
||||
if (!isset($_SESSION['admin_theme'])) {
|
||||
$theme = '';
|
||||
if (!empty($_SESSION['admin_id'])) {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT theme FROM admins WHERE id = ?");
|
||||
$stmt->execute([(int)$_SESSION['admin_id']]);
|
||||
// 用 fetchAll 取:只取一行就放着不读完,游标会一直挂着共享读锁(本项目踩过)
|
||||
$rows = $stmt->fetchAll();
|
||||
if ($rows && isset($rows[0]['theme'])) {
|
||||
$theme = (string)$rows[0]['theme'];
|
||||
}
|
||||
}
|
||||
$_SESSION['admin_theme'] = $theme;
|
||||
}
|
||||
$key = (string)$_SESSION['admin_theme'];
|
||||
return isValidTheme($key) ? $key : defaultThemeKey();
|
||||
}
|
||||
|
||||
// <html> 上要输出的主题属性。始终输出(默认主题也输出),
|
||||
// 这样每个页面用的都是 themes.css 里那个明确的调色板,不用去猜 style.css 的兜底值。
|
||||
function themeAttr() {
|
||||
return ' data-theme="' . h(currentThemeKey()) . '"';
|
||||
}
|
||||
|
||||
// 主题样式表。必须放在 css/style.css 之后。
|
||||
//
|
||||
// 末尾**带一个换行符**:PHP 会吃掉「结束标签」后面紧跟的那一个换行。
|
||||
// 模板里把这个返回值单独放一行再换行的话,渲染结果会变成这一行和下一行黏在一起
|
||||
// (HTML 上无害,但会让 HTML 结构对比工具误判,也没必要)。这里补回来。
|
||||
function themesCssLink() {
|
||||
return '<link rel="stylesheet" href="css/themes.css">' . "\n";
|
||||
}
|
||||
Reference in New Issue
Block a user