Files
laoyang ee256adc4b 界面主题: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 的主题原样还原、其它账号一字未动)。
2026-09-22 20:33:47 +08:00

113 lines
4.9 KiB
PHP
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.
<?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";
}