界面主题: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:
2026-09-22 20:33:47 +08:00
parent 275bd9d80f
commit ee256adc4b
21 changed files with 530 additions and 58 deletions
+34 -4
View File
@@ -47,9 +47,27 @@
- **Session 登录**:`password_hash` / `password_verify` 加盐哈希存储口令,全站页面均需登录。
- **用户管理**:可新增、编辑、删除管理员账号,可修改用户名与密码。
- **账号 ↔ 观影人绑定**:每个管理员账号可关联一个观影人,绑定后即可查看个人年度报告。
- **界面主题**:每个账号可以给自己挑一套配色(详见「界面主题」一节),别人登录看到的还是他们各自选的那套。
- **安全约束**:不允许删除最后一个管理员,也不允许删除自己。
- **个人资料**:修改自己的用户名与密码(需验证旧密码)。
### 界面主题
- **5 套经典配色**,在「系统设置 → 🎨 我的界面主题」里选,点卡片即时预览、点「保存主题」后全站生效(**不用重新登录**):
| key | 名称 | 风格 |
| --- | --- | --- |
| `ticket` | 复古票根 | 暖橙 `#e67e22` + 浅灰白,**默认主题** |
| `indigo` | 靛蓝 | 经典蓝 `#2563eb` + 冷调浅灰 |
| `forest` | 松林 | 墨绿 `#2f855a` + 米灰白 |
| `wine` | 绛红 | 酒红 `#b03a5b` + 暖白 |
| `midnight` | 暗夜 | 深灰蓝底 + 靛紫 `#7c5cf5`(**深色主题**) |
- **主题是每个账号自己的偏好**:存在 `admins.theme`(空 = 用默认主题),只影响自己;切换不需要重新登录。
- **实现方式**:页面在 `<html data-theme="<key>">` 上声明用哪套,色值全部集中在 `css/themes.css` 的 `[data-theme="<key>"]` 规则块里;`css/style.css` 的 `:root` 只作为**兜底**(放着默认主题「复古票根」的那一份)。因此 `themes.css` 必须排在 `style.css` **之后**(两者优先级相同,靠后出现者胜)。
- **页面里一律不写死颜色**:淡色底 / 光晕统一用 `rgba(var(--primary-rgb), α)` 取色,所以新增一套主题**只需要动 CSS**,页面与 JS 一行都不用改。年度报告的 Chart.js 图表读不到 CSS 变量,改为运行时从 `<html>` 的计算样式里取色。
- `--primary-rgb` 必须与 `--primary` 同色(写成 `"R, G, B"`);每套主题都要声明 `color-scheme`(`light` / `dark`),否则暗色主题下日期选择器、下拉框、滚动条仍是刺眼的白底。
- **加一套主题**:在 `lib/theme.php` 的 `themeCatalog()` 里加一项,并在 `css/themes.css` 里补一个同 key 的规则块(那一组变量一个都不能少)。
### 评价与评论
- **谁可以评价**:**只要有登录账号就能评价**,与「是不是观影人」无关——没一起去看过的人也能打分、写评价、参与讨论。评价的归属是账号(`admins`),不是观影人(`persons`)。
- **页面上默认不出现任何输入框**:详情页「💬 评价与评论」区块上来只展示已有内容;要写什么就点对应那个按钮——「📝 写评价」「✏️ 修改我的评价」「💬 评论」「↩ 回复」,点开才当场展开表单,提交后页面刷新、表单自动收起。折叠用的是原生 `<details>/<summary>`,**不依赖 JS**,键盘也能操作。
@@ -71,7 +89,8 @@
- **观影人管理**:增删改查,支持头像裁剪上传与专属配色;已被电影引用的观影人不允许直接删除。
### 界面与体验
- 复古票根色系(主色 `#e67e22` 橙)的响应式设计,移动端断点适配(600 / 768px)。
- **5 套配色主题可选**(详见下面的「界面主题」),主色、底色、卡片、边框、阴影、滚动条全部跟随主题。
- 响应式设计,移动端断点适配(600 / 768px)。
- 导航为下拉菜单,自动高亮当前页;顶部始终显示当前用户头像与用户名。
- **点导航栏左上角的「🎬 电影日志」即可回到首页**(右键 / Ctrl+点击还能在新标签页打开),不必再展开右侧折叠菜单去找「首页」。
- 登录页为深色渐变 + 毛玻璃风格。
@@ -101,6 +120,7 @@ MovieLog_Server/
├── lib/ # 功能模块(被 config.php 统一引入,页面无需单独 require)
│ ├── db.php # DB 连接、建表、字段与历史数据迁移、默认管理员
│ ├── helpers.php # 输出转义、文件名、颜色校验、上传/删除文件、图片 URL
│ ├── theme.php # 界面主题注册表(5 套配色)、当前主题、页面上怎么声明
│ ├── settings.php # 系统设置读写;云厂商密钥以 enc:v1:gcm 密文落库
│ ├── storage.php # 存储门面:本地优先读取、URL 生成、上传/删除、迁移、代理读取
│ ├── storage/ # 各存储后端驱动
@@ -135,7 +155,8 @@ MovieLog_Server/
├── media.php # 服务端图片代理:鉴权后由服务器读取对象并同源回吐
├── fetch_movie.php # 豆瓣接口(片名候选 / 详情 + 海报入库 / 缩略图代理),返回 JSON
├── test_crop.php # 开发调试页:验证 Base64 图片落盘是否正常
├── css/style.css # 全局样式
├── css/style.css # 全局样式(:root 里是默认主题的兜底色值)
├── css/themes.css # 5 套主题的调色板(必须在 style.css 之后加载)
├── js/main.js # 公共脚本(含各页面共用的表单联动函数、破图兜底)
├── assets/ # 静态资源
│ └── default-avatar.png # 默认头像(未上传头像时统一使用)
@@ -147,7 +168,7 @@ MovieLog_Server/
└── avatars/ # 观影人头像
```
> 页面只需 `require_once 'config.php'`,`lib/` 下的模块会由引导文件统一加载,并保证依赖顺序(db → settings → storage → helpers → auth → movies → reviews → stats)。
> 页面只需 `require_once 'config.php'`,`lib/` 下的模块会由引导文件统一加载,并保证依赖顺序(db → settings → storage → helpers → theme → auth → movies → reviews → stats)。
---
@@ -157,7 +178,7 @@ MovieLog_Server/
| 表 | 作用 | 关键字段 |
| --- | --- | --- |
| `admins` | 管理员账号 | `username`(唯一)、`password`(哈希)、`avatar`、`person_id`(关联观影人) |
| `admins` | 管理员账号 | `username`(唯一)、`password`(哈希)、`avatar`、`person_id`(关联观影人)、`theme`(该账号选的界面主题 key,空 = 用默认主题;见 `lib/theme.php`) |
| `cinemas` | 电影院 | `place`(所在地方)、`name`、`address`、`color`(专属配色) |
| `movies` | 电影记录 | `title`、`info`、`remark`、`poster_path`、`ticket_path`、`photo_path`(历史遗留单图)、`cinema_id`、`watch_date`、`created_at` |
| `persons` | 观影人 | `name`(唯一)、`color`、`avatar` |
@@ -284,6 +305,13 @@ server {
> 评价人是**登录账号**。家里几个人各有一个账号时,同一部电影可以各自打分,标题栏会显示「N 条评价 · 均分 X」。
### 换一套界面配色
1. 右上角展开菜单 → 「系统设置」,找到最上面的「🎨 我的界面主题」。
2. 点任意一张主题卡片,**整页立刻换成那套配色**(包括导航、卡片、按钮、表单控件,以及年度报告的图表)——这一步只是预览。
3. 满意就点「保存主题」。保存后**全站页面都用这套配色,并且不用重新登录**;不满意就再点别的卡片。
4. 主题是**你自己的偏好**,别人登录看到的仍是他们各自选的那套。默认是「复古票根」。
### CSV 批量导入
CSV **共 9 列,顺序固定**:
@@ -684,6 +712,8 @@ curl -sk -H "Host: 你的域名" -H "Cookie: PHPSESSID=xxxx" -o /dev/null \
- 迁移到云端后**本地文件不会自动删除**,需要自行确认后再清理;本地优先策略意味着本地文件存在时始终读本地,云端副本仅作备份。
- **下拉联想会随输入向豆瓣实时发请求**(已做 320 ms 防抖 + 会话内结果缓存 + 丢弃过期请求,同一关键词只查一次),但**服务端本身没有做接口级限流**。若担心被豆瓣限流,可把 `add.php` 里的 `DEBOUNCE` 调大,或让下拉只在按下「📡 自动获取」时才工作。
- **缩略图代理(`action=thumb`)每张图都要经服务器回源一次**。已加 `Cache-Control: max-age=86400`,同一张图在浏览器侧一天内只回源一次,正常使用量很小;若把下拉列表做得很大、或候选很多,建议改成在服务端落小图缓存。
- **主题只做到「换色」这一层**:本项目自己的界面全部跟随主题,但第三方组件里只有 Chart.js 的图表跟着换(运行时从计算样式取色);Cropper.js 的裁剪遮罩、拖拽框是它自带的固定样式,不随主题变化。
- **加一套主题要改两处**:`lib/theme.php` 的 `themeCatalog()`(有哪些主题)与 `css/themes.css` 的同 key 规则块(色值)。两边不一致时页面会静默退回默认值,所以 `theme_e2e.py` 会断言「每套主题的变量都齐全」以及「`:root` 与 `[data-theme=ticket]` 完全一致」。
---
+4 -3
View File
@@ -88,12 +88,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>添加电影 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style>
.form-container {
@@ -148,7 +149,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(230,126,34,0.12);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.12);
}
.form-group textarea {
min-height: 100px;
@@ -214,7 +215,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.btn-add-viewer:hover {
border-color: var(--primary);
color: var(--primary);
background: rgba(230,126,34,0.05);
background: rgba(var(--primary-rgb),0.05);
}
.form-actions {
display: flex;
+28 -7
View File
@@ -31,12 +31,13 @@ $weekday_labels = ['周日','周一','周二','周三','周四','周五','周六
$weekday_values = array_values($stats['weekday_distribution']);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $selected_year ?> 年度观影报告 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
@@ -45,8 +46,10 @@ $weekday_values = array_values($stats['weekday_distribution']);
--report-card: #1a1a2e;
--report-text: #fffffe;
--report-muted: #a7a9be;
--report-primary: #e67e22;
--report-secondary: #f39c12;
/* 年度报告本身是一套深色「海报」版式,底色固定不动;
点缀色跟随用户选的主题,这样蓝色主题下报告也是蓝的。 */
--report-primary: var(--primary);
--report-secondary: var(--primary-light);
}
body {
background: var(--report-bg);
@@ -507,6 +510,24 @@ $weekday_values = array_values($stats['weekday_distribution']);
<script>
<?php if ($has_data): ?>
// Chart.js 画在 <canvas> 上,读不到 CSS 变量,必须先把当前主题的颜色值取出来。
// 从 <html> 的计算样式里读,用户在设置里换了主题,图表颜色就跟着变,
// 不用在 PHP 里再抄一份调色板。
function themeColor(name, fallback) {
var v = getComputedStyle(document.documentElement).getPropertyValue(name);
return (v || '').trim() || fallback;
}
function rgbTriplet(hex) {
var h = (hex || '').replace('#', '');
if (h.length === 3) { h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; }
if (h.length !== 6) { return '230, 126, 34'; }
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)].join(', ');
}
var PRIMARY_HEX = themeColor('--primary', '#e67e22');
var PRIMARY_RGB = rgbTriplet(PRIMARY_HEX);
var LIGHT_HEX = themeColor('--primary-light', '#f39c12');
var LIGHT_RGB = rgbTriplet(LIGHT_HEX);
// 月度图表
const monthCtx = document.getElementById('monthChart').getContext('2d');
new Chart(monthCtx, {
@@ -516,8 +537,8 @@ $weekday_values = array_values($stats['weekday_distribution']);
datasets: [{
label: '观影数量',
data: <?= json_encode($month_values) ?>,
backgroundColor: 'rgba(230, 126, 34, 0.7)',
borderColor: '#e67e22',
backgroundColor: 'rgba(' + PRIMARY_RGB + ', 0.7)',
borderColor: PRIMARY_HEX,
borderWidth: 1,
borderRadius: 4
}]
@@ -551,8 +572,8 @@ $weekday_values = array_values($stats['weekday_distribution']);
datasets: [{
label: '观影数量',
data: <?= json_encode($weekday_values) ?>,
backgroundColor: 'rgba(243, 156, 18, 0.7)',
borderColor: '#f39c12',
backgroundColor: 'rgba(' + LIGHT_RGB + ', 0.7)',
borderColor: LIGHT_HEX,
borderWidth: 1,
borderRadius: 4
}]
+3 -2
View File
@@ -55,12 +55,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>修改密码 - 电影全纪录</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.form-container {
max-width: 500px;
@@ -108,7 +109,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.form-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(230,126,34,0.12);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.12);
}
.form-actions {
display: flex;
+6 -4
View File
@@ -99,12 +99,13 @@ $preset_colors = [
];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>影院管理 - 电影全纪录</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.container {
max-width: 1000px;
@@ -152,7 +153,7 @@ $preset_colors = [
.form-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(230,126,34,0.12);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.12);
}
.form-group input[type="color"] {
padding: 4px;
@@ -186,7 +187,7 @@ $preset_colors = [
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(230,126,34,0.35);
box-shadow: 0 8px 28px rgba(var(--primary-rgb),0.35);
}
.btn-danger {
background: #e74c3c;
@@ -283,7 +284,8 @@ $preset_colors = [
transform: scale(1.1);
}
.preset-color.active {
border-color: #333;
/* 用前景色描边:色块底色是用户自选的固定色,暗色主题下只有浅色描边才看得见 */
border-color: var(--text-primary);
}
.color-input-group {
display: flex;
+1
View File
@@ -43,6 +43,7 @@ require_once ROOT_DIR . '/lib/db.php'; // 数据库连接与初始化
require_once ROOT_DIR . '/lib/settings.php'; // 系统设置读写
require_once ROOT_DIR . '/lib/storage.php'; // 对象存储门面(含各云厂商驱动)
require_once ROOT_DIR . '/lib/helpers.php'; // 通用工具函数
require_once ROOT_DIR . '/lib/theme.php'; // 界面配色主题(注册表 / 当前主题)
require_once ROOT_DIR . '/lib/auth.php'; // 登录校验与账号管理
require_once ROOT_DIR . '/lib/movies.php'; // 电影 / 影院 / 搜索
require_once ROOT_DIR . '/lib/reviews.php'; // 评价与评价下的讨论
+20 -5
View File
@@ -1,20 +1,34 @@
/* ============================================
文件: css/style.css
说明: 全局样式 - 支持 Grid 网格瀑布流
配色(--primary / --bg-* / --card-bg / --text-* / --border-color / --shadow* …)
由「主题」决定:页面在 <html data-theme="<key>"> 上声明,具体色值见 css/themes.css。
下面 :root 里保留的是**默认主题「复古票根」**的那一份,
作用是兜底 —— 万一 themes.css 没加载出来,页面也不会掉成无色。
改默认配色时,两处(这里和 themes.css 的 [data-theme="ticket"])要一起改,
theme_e2e 测试会断言两边的值一致。
============================================ */
:root {
--primary: #e67e22;
--primary-dark: #d35400;
--primary-light: #f39c12;
/* 与 --primary 同色的 "R, G, B" 三元组:淡色底/光晕统一用
rgba(var(--primary-rgb), α) 取色,换主题时不用改动任何一条具体规则。 */
--primary-rgb: 230, 126, 34;
--bg-primary: #f0f2f5;
--bg-secondary: #e8eaed;
--card-bg: #ffffff;
--input-bg: #ffffff;
--nav-bg: rgba(255, 255, 255, 0.92);
--text-primary: #1a1a2e;
--text-secondary: #6b7280;
--border-color: #e5e7eb;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04);
--shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.12);
--scroll-thumb: #d1d5db;
--scroll-thumb-hover: #b0b4ba;
--radius: 12px;
--radius-lg: 16px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -43,11 +57,11 @@ body {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #d1d5db;
background: var(--scroll-thumb);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
background: #b0b4ba;
background: var(--scroll-thumb-hover);
}
/* ===== 导航栏 ===== */
@@ -62,7 +76,8 @@ body {
top: 0;
z-index: 100;
backdrop-filter: blur(12px);
background: rgba(255, 255, 255, 0.92);
/* 半透明的导航底:明色主题是白色磨砂,暗色主题自动换成深色磨砂 */
background: var(--nav-bg);
}
.nav-container {
@@ -140,11 +155,11 @@ body {
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: #fff;
box-shadow: 0 4px 14px rgba(230, 126, 34, 0.25);
box-shadow: 0 4px 14px rgba(var(--primary-rgb), 0.25);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(230, 126, 34, 0.35);
box-shadow: 0 8px 28px rgba(var(--primary-rgb), 0.35);
}
.btn-primary:active {
transform: translateY(0);
+125
View File
@@ -0,0 +1,125 @@
/* ============================================
文件: css/themes.css
说明: 界面配色主题(5 套经典配色)。
生效方式:<html data-theme="<key>">(由 lib/theme.php 的 themeAttr() 输出)。
本文件必须排在 css/style.css **之后**加载:[data-theme] 与 style.css 里作为
兜底的 :root 优先级相同(都是 0,1,0),靠「后出现者胜」来覆盖。
每套主题都要给全下面这一组变量,缺一个就会退回 style.css 的默认值:
--primary / --primary-dark / --primary-light / --primary-rgb
--bg-primary / --bg-secondary / --card-bg / --input-bg / --nav-bg
--text-primary / --text-secondary / --border-color
--shadow / --shadow-hover
--scroll-thumb / --scroll-thumb-hover
注意 --primary-rgb 必须与 --primary 同色(写成 "R, G, B"),
页面里的淡色底/光晕一律用 rgba(var(--primary-rgb), α) 取色。
另外每套都要声明 color-scheme:它决定浏览器原生控件(下拉、日期选择器、
滚动条)走明色还是暗色渲染 —— 暗色主题少了这一条,日期输入框会是刺眼的白底。
============================================ */
/* ---------- 1. 复古票根(默认):暖橙 + 浅灰白 ---------- */
[data-theme="ticket"] {
color-scheme: light;
--primary: #e67e22;
--primary-dark: #d35400;
--primary-light: #f39c12;
--primary-rgb: 230, 126, 34;
--bg-primary: #f0f2f5;
--bg-secondary: #e8eaed;
--card-bg: #ffffff;
--input-bg: #ffffff;
--nav-bg: rgba(255, 255, 255, 0.92);
--text-primary: #1a1a2e;
--text-secondary: #6b7280;
--border-color: #e5e7eb;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04);
--shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.12);
--scroll-thumb: #d1d5db;
--scroll-thumb-hover: #b0b4ba;
}
/* ---------- 2. 靛蓝:经典蓝 + 冷调浅灰 ---------- */
[data-theme="indigo"] {
color-scheme: light;
--primary: #2563eb;
--primary-dark: #1d4ed8;
--primary-light: #60a5fa;
--primary-rgb: 37, 99, 235;
--bg-primary: #f4f6fb;
--bg-secondary: #e7ecf6;
--card-bg: #ffffff;
--input-bg: #ffffff;
--nav-bg: rgba(255, 255, 255, 0.92);
--text-primary: #141a2e;
--text-secondary: #64748b;
--border-color: #e2e8f4;
--shadow: 0 4px 24px rgba(30, 58, 138, 0.07), 0 1px 4px rgba(30, 58, 138, 0.05);
--shadow-hover: 0 12px 48px rgba(30, 58, 138, 0.14);
--scroll-thumb: #c7d2e4;
--scroll-thumb-hover: #a8b8d2;
}
/* ---------- 3. 松林:墨绿 + 米灰白 ---------- */
[data-theme="forest"] {
color-scheme: light;
--primary: #2f855a;
--primary-dark: #276749;
--primary-light: #48bb78;
--primary-rgb: 47, 133, 90;
--bg-primary: #f2f6f3;
--bg-secondary: #e6eee9;
--card-bg: #ffffff;
--input-bg: #ffffff;
--nav-bg: rgba(255, 255, 255, 0.92);
--text-primary: #15251d;
--text-secondary: #647a70;
--border-color: #dfe8e2;
--shadow: 0 4px 24px rgba(20, 60, 40, 0.07), 0 1px 4px rgba(20, 60, 40, 0.05);
--shadow-hover: 0 12px 48px rgba(20, 60, 40, 0.14);
--scroll-thumb: #cbd8d0;
--scroll-thumb-hover: #aec2b6;
}
/* ---------- 4. 绛红:酒红 + 暖白 ---------- */
[data-theme="wine"] {
color-scheme: light;
--primary: #b03a5b;
--primary-dark: #8e2d48;
--primary-light: #d45c7d;
--primary-rgb: 176, 58, 91;
--bg-primary: #faf5f6;
--bg-secondary: #f2e7ea;
--card-bg: #ffffff;
--input-bg: #ffffff;
--nav-bg: rgba(255, 255, 255, 0.92);
--text-primary: #26161c;
--text-secondary: #7a6a70;
--border-color: #ece0e4;
--shadow: 0 4px 24px rgba(90, 20, 40, 0.07), 0 1px 4px rgba(90, 20, 40, 0.05);
--shadow-hover: 0 12px 48px rgba(90, 20, 40, 0.14);
--scroll-thumb: #e0cdd4;
--scroll-thumb-hover: #cbb0b9;
}
/* ---------- 5. 暗夜:深灰蓝底 + 靛紫点缀 ---------- */
[data-theme="midnight"] {
color-scheme: dark;
--primary: #7c5cf5;
--primary-dark: #6a45e0;
--primary-light: #9d84f8;
--primary-rgb: 124, 92, 245;
--bg-primary: #14161c;
--bg-secondary: #1b1e26;
--card-bg: #22262f;
--input-bg: #1b1e26;
--nav-bg: rgba(34, 38, 47, 0.92);
--text-primary: #e8eaf0;
--text-secondary: #9aa3b2;
--border-color: #333846;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.45), 0 1px 4px rgba(0, 0, 0, 0.3);
--shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.6);
--scroll-thumb: #3a3f4d;
--scroll-thumb-hover: #4a5060;
}
+8 -7
View File
@@ -311,12 +311,13 @@ foreach ($all_photos as $photo) {
$hasGallery = !empty($gallery);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= h($movie['title']) ?> - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.detail-container {
max-width: 900px;
@@ -610,7 +611,7 @@ $hasGallery = !empty($gallery);
background: linear-gradient(135deg, var(--primary), var(--primary-light));
border-color: transparent;
color: #fff;
box-shadow: 0 3px 10px rgba(230, 126, 34, 0.30);
box-shadow: 0 3px 10px rgba(var(--primary-rgb), 0.30);
}
.rv-num:focus-within span {
outline: 2px solid var(--primary);
@@ -641,7 +642,7 @@ $hasGallery = !empty($gallery);
.rv-textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(230, 126, 34, 0.12);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb), 0.12);
}
.rv-textarea-sm {
min-height: 62px;
@@ -853,7 +854,7 @@ $hasGallery = !empty($gallery);
}
.feel-pick input:checked + .feel-inner {
border-color: var(--primary);
background: rgba(230, 126, 34, 0.12);
background: rgba(var(--primary-rgb), 0.12);
color: var(--primary);
font-weight: 600;
}
@@ -870,7 +871,7 @@ $hasGallery = !empty($gallery);
gap: 4px;
padding: 2px 10px;
border-radius: 20px;
background: rgba(230, 126, 34, 0.10);
background: rgba(var(--primary-rgb), 0.10);
color: var(--primary);
font-size: 12px;
}
@@ -898,7 +899,7 @@ $hasGallery = !empty($gallery);
transition: var(--transition);
}
.rv-open-btn:hover {
box-shadow: 0 4px 14px rgba(230, 126, 34, 0.32);
box-shadow: 0 4px 14px rgba(var(--primary-rgb), 0.32);
}
/* 展开状态下按钮退成浅色,避免和下方表单抢注意力 */
.rv-composer[open] > .rv-open-btn {
@@ -926,7 +927,7 @@ $hasGallery = !empty($gallery);
}
.rv-composer[open] > .rv-act-btn {
border-color: var(--primary);
background: rgba(230, 126, 34, 0.12);
background: rgba(var(--primary-rgb), 0.12);
color: var(--primary);
font-weight: 600;
}
+4 -3
View File
@@ -140,12 +140,13 @@ if (empty($current_person_ids)) {
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>编辑电影 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style>
/* ===== 全局样式 ===== */
@@ -161,7 +162,7 @@ if (empty($current_person_ids)) {
transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box;
}
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(230,126,34,0.12);
outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.12);
}
.form-group textarea { min-height: 100px; resize: vertical; }
.form-group input[type="file"] { padding: 10px 12px; background: var(--bg-secondary); border: 1px dashed var(--border-color); }
@@ -174,7 +175,7 @@ if (empty($current_person_ids)) {
.btn-remove-viewer { background: none; border: none; color: #e74c3c; font-size: 20px; cursor: pointer; padding: 0 8px; line-height: 1; transition: transform 0.2s; }
.btn-remove-viewer:hover { transform: scale(1.2); }
.btn-add-viewer { background: var(--bg-secondary); border: 1px dashed var(--border-color); border-radius: 8px; padding: 8px 16px; color: var(--text-secondary); cursor: pointer; font-size: 14px; transition: all 0.2s; width: 100%; }
.btn-add-viewer:hover { border-color: var(--primary); color: var(--primary); background: rgba(230,126,34,0.05); }
.btn-add-viewer:hover { border-color: var(--primary); color: var(--primary); background: rgba(var(--primary-rgb),0.05); }
.form-actions { display: flex; gap: 12px; margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border-color); }
.form-actions .btn { padding: 12px 32px; font-size: 16px; }
.btn-secondary { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); }
+3 -2
View File
@@ -152,12 +152,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSV导入 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.form-container {
max-width: 720px;
@@ -216,7 +217,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(230,126,34,0.35);
box-shadow: 0 8px 28px rgba(var(--primary-rgb),0.35);
}
.btn-outline {
background: transparent;
+2 -1
View File
@@ -21,12 +21,13 @@ if (!empty($keyword)) {
attachReviewStatsToMovies($movies);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电影日志 - 我的电影</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
/* 搜索栏样式 */
.search-bar {
+25
View File
@@ -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
View File
@@ -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
View File
@@ -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";
}
+8 -5
View File
@@ -29,6 +29,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($admin && password_verify($password, $admin['password'])) {
$_SESSION['admin_id'] = $admin['id'];
$_SESSION['admin_username'] = $admin['username'];
// 把他自己选的界面主题一起带进会话(空 = 用默认主题)
$_SESSION['admin_theme'] = isset($admin['theme']) ? (string)$admin['theme'] : '';
header('Location: index.php');
exit;
} else {
@@ -38,12 +40,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电影全纪录 - 登录</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
/* 登录页专用样式 */
body {
@@ -114,9 +117,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
outline: none;
}
.form-group input:focus {
border-color: #e67e22;
border-color: var(--primary);
background: rgba(255,255,255,0.12);
box-shadow: 0 0 0 4px rgba(230,126,34,0.15);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.15);
}
.form-group input::placeholder {
color: rgba(255,255,255,0.3);
@@ -124,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #e67e22, #f39c12);
background: linear-gradient(135deg, var(--primary), var(--primary-light));
border: none;
border-radius: 12px;
color: #fff;
@@ -137,7 +140,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(230,126,34,0.3);
box-shadow: 0 10px 30px rgba(var(--primary-rgb),0.3);
}
.btn-login:active {
transform: translateY(0);
+5 -4
View File
@@ -137,12 +137,13 @@ $preset_colors = [
];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>观影人管理 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<!-- Cropper.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style>
@@ -153,12 +154,12 @@ $preset_colors = [
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-weight: 600; font-size: 14px; color: var(--text-primary); margin-bottom: 4px; }
.form-group input { width: 100%; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: 8px; font-size: 15px; box-sizing: border-box; }
.form-group input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(230,126,34,0.12); }
.form-group input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.12); }
.form-row { display: flex; gap: 16px; flex-wrap: wrap; }
.form-row .form-group { flex: 1; min-width: 180px; }
.btn { padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 600; border: none; cursor: pointer; transition: var(--transition); text-decoration: none; display: inline-block; }
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #fff; }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(230,126,34,0.35); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(var(--primary-rgb),0.35); }
.btn-danger { background: #e74c3c; color: #fff; }
.btn-danger:hover { background: #c0392b; }
.btn-outline { background: transparent; color: var(--text-secondary); border: 1px solid var(--border-color); }
@@ -177,7 +178,7 @@ $preset_colors = [
.preset-grid { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.preset-color { display: inline-block; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; transition: 0.2s; }
.preset-color:hover { transform: scale(1.15); }
.preset-color.active { border-color: #333; }
.preset-color.active { border-color: var(--text-primary); }
/* 头像样式 */
.avatar-upload {
display: flex;
+3 -2
View File
@@ -38,12 +38,13 @@ if ($has_conditions) {
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高级搜索 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.search-container {
max-width: 900px;
@@ -97,7 +98,7 @@ if ($has_conditions) {
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(230,126,34,0.12);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.12);
}
.form-actions {
display: flex;
+125 -4
View File
@@ -108,6 +108,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'clear
$message = '已清除错误提示';
}
// ---------- 切换界面主题(个人偏好,只影响当前账号)----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_theme') {
$themeKey = (string)($_POST['theme'] ?? '');
if (!isValidTheme($themeKey)) {
$error = '没有这套主题,请从下面重新选一个。';
} elseif (setAdminTheme((int)$admin['id'], $themeKey)) {
// setAdminTheme 已经把 session 一起更新了,所以本次请求往下渲染时就已经是新主题
$message = '界面主题已切换为「' . themeInfo($themeKey)['name'] . '」。';
} else {
$error = '主题保存失败,请稍后再试。';
}
}
$themes = themeCatalog();
$curThemeKey = currentThemeKey();
$drivers = storageDriverLabels();
$current = currentStorageDriver();
$configs = [];
@@ -138,12 +154,13 @@ function localImageStat() {
$localStat = localImageStat();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统设置 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.page-wrap { max-width: 880px; margin: 0 auto; padding: 20px; }
.card {
@@ -160,7 +177,7 @@ $localStat = localImageStat();
font-family: inherit;
}
.form-group input:focus, .form-group select:focus {
outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(230,126,34,.12);
outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(var(--primary-rgb),.12);
}
.hint { font-size: 12.5px; color: var(--text-secondary); margin-top: 5px; line-height: 1.6; }
.hint code { background: var(--bg-secondary); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
@@ -170,7 +187,7 @@ $localStat = localImageStat();
cursor: pointer; transition: var(--transition); background: var(--card-bg);
}
.driver-opt:hover { border-color: var(--primary-light); }
.driver-opt.active { border-color: var(--primary); background: #fff8f0; }
.driver-opt.active { border-color: var(--primary); background: rgba(var(--primary-rgb), .07); }
.driver-opt .t { font-weight: 600; font-size: 15px; display: flex; align-items: center; gap: 8px; }
.driver-opt .d { font-size: 12.5px; color: var(--text-secondary); margin-top: 4px; }
.driver-opt input { margin-right: 6px; }
@@ -180,7 +197,7 @@ $localStat = localImageStat();
.btn { padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 600;
border: none; cursor: pointer; transition: var(--transition); text-decoration: none; display: inline-block; }
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #fff; }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(230,126,34,.35); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(var(--primary-rgb),.35); }
.btn-outline { background: transparent; color: var(--text-secondary); border: 1px solid var(--border-color); }
.btn-outline:hover { background: var(--bg-secondary); }
.btn-sm { padding: 6px 14px; font-size: 13px; }
@@ -202,6 +219,56 @@ $localStat = localImageStat();
.row-flex { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.chk { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.chk input { margin: 0; }
/* ===== 主题选择卡片 =====
每张预览卡片自己带一个 data-theme,用的就是那套主题真实的那组变量,
所以缩略图里的颜色与换上之后的页面必然一致(不必在这里再抄一遍色值)。
卡片内部一律用 var(--xxx) 取色 → 暗色主题的卡片自然也是深底浅字。 */
.theme-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); gap: 14px; }
.theme-opt { display: block; margin: 0; cursor: pointer; position: relative; }
/* 单选圈本身隐藏,但保留在文档流里且可聚焦(键盘 Tab + 方向键照常可用) */
.theme-opt > input {
position: absolute; width: 1px; height: 1px; opacity: 0;
margin: 0; pointer-events: none;
}
.theme-card {
display: block; border: 2px solid var(--border-color); border-radius: 12px;
overflow: hidden; background: var(--bg-primary); transition: var(--transition);
}
.theme-opt:hover .theme-card { border-color: var(--primary-light); transform: translateY(-2px); }
/* 选中的那张:描边用「这张卡片自己的」主色,一眼能认出选的是哪套 */
.theme-opt > input:checked + .theme-card {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb), .14);
}
.theme-opt > input:focus-visible + .theme-card {
outline: 2px solid var(--primary); outline-offset: 2px;
}
/* 缩略图:一条主色横条 + 两行灰字 + 一个主色按钮,够看出这套配色的性格 */
.theme-stage { display: block; padding: 11px 12px 12px; background: var(--bg-primary); }
.ts-bar {
display: block; height: 7px; border-radius: 4px; margin-bottom: 9px;
background: linear-gradient(90deg, var(--primary), var(--primary-light));
}
.ts-line {
display: block; height: 5px; border-radius: 3px; margin-bottom: 5px;
background: var(--text-secondary); opacity: .35;
}
.ts-line.ts-short { width: 58%; }
.ts-btn {
display: block; width: 46px; height: 14px; border-radius: 7px; margin-top: 10px;
background: var(--primary); box-shadow: 0 3px 10px rgba(var(--primary-rgb), .4);
}
.theme-meta { display: block; padding: 10px 12px 12px; background: var(--card-bg); }
.theme-name {
display: flex; align-items: center; gap: 7px;
font-size: 14px; font-weight: 600; color: var(--text-primary);
}
.theme-cur {
padding: 1px 7px; border-radius: 20px; font-size: 11px; font-weight: 600;
color: #fff; background: var(--primary);
}
.theme-desc { display: block; margin-top: 3px; font-size: 12px; line-height: 1.5; color: var(--text-secondary); }
</style>
</head>
<body>
@@ -223,6 +290,47 @@ $localStat = localImageStat();
</div>
<?php endif; ?>
<div class="card">
<h2>🎨 我的界面主题</h2>
<div class="sub">
挑一套配色,只作用于<strong>你自己的账号</strong>——别人登录看到的还是他们各自选的那套。<br>
想先看看效果就点一下主题卡片,整页会立刻换色(只是预览,按「保存主题」才真正记住)。
按钮、链接、卡片边框、评分徽标与年度报告的图表颜色都会一起换。
</div>
<form method="post" id="themeForm">
<input type="hidden" name="action" value="save_theme">
<div class="theme-grid" id="themeGrid">
<?php foreach ($themes as $tkey => $t): ?>
<label class="theme-opt">
<input type="radio" name="theme" value="<?= h($tkey) ?>"
<?= $tkey === $curThemeKey ? 'checked' : '' ?>>
<!-- 预览卡片自己套一层 data-theme,用的就是该主题真实的那组变量,
所以「预览所见」与「换上去之后」必然一致,不必在 PHP 里再抄一份色值。 -->
<span class="theme-card" data-theme="<?= h($tkey) ?>">
<span class="theme-stage">
<span class="ts-bar"></span>
<span class="ts-line"></span>
<span class="ts-line ts-short"></span>
<span class="ts-btn"></span>
</span>
<span class="theme-meta">
<span class="theme-name"><?= h($t['name']) ?><?php
if ($tkey === $curThemeKey): ?><span class="theme-cur">当前</span><?php
endif; ?></span>
<span class="theme-desc"><?= h($t['desc']) ?></span>
</span>
</span>
</label>
<?php endforeach; ?>
</div>
<div class="row-flex" style="margin-top: 18px;">
<button class="btn btn-primary" type="submit">保存主题</button>
<span class="hint" style="margin: 0;">保存后立刻生效,全站页面都用这套配色;不用重新登录。</span>
</div>
</form>
</div>
<div class="card">
<h2>图片存储方式</h2>
<div class="sub">
@@ -656,6 +764,19 @@ $localStat = localImageStat();
<script>
(function () {
// ---------- 主题即时预览 ----------
// 点一下卡片就把 <html data-theme> 换成那套主题 —— 整页(含导航、卡片、按钮、
// 表单控件)立刻换色,所见即保存后的样子。这只是预览,真正记住要靠「保存主题」;
// 没脚本也能用:选中单选后提交同样生效。
var grid = document.getElementById('themeGrid');
if (grid) {
grid.querySelectorAll('input[name=theme]').forEach(function (r) {
r.addEventListener('change', function () {
if (r.checked) document.documentElement.setAttribute('data-theme', r.value);
});
});
}
var API = 'storage_api.php';
// ---------- 驱动切换 ----------
+2 -1
View File
@@ -22,12 +22,13 @@ $max_month = !empty($month_stats) ? max(array_column($month_stats, 'count')) : 1
$max_year = !empty($year_stats) ? max(array_column($year_stats, 'count')) : 1;
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>统计仪表板 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.stats-container {
max-width: 1100px;
+4 -3
View File
@@ -132,12 +132,13 @@ if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id']))
$users = getAllAdmins();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN"<?= themeAttr() ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - 电影日志</title>
<link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style>
.form-container {
max-width: 800px;
@@ -184,7 +185,7 @@ $users = getAllAdmins();
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(230,126,34,0.12);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.12);
}
.btn {
padding: 10px 20px;
@@ -203,7 +204,7 @@ $users = getAllAdmins();
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(230,126,34,0.35);
box-shadow: 0 8px 28px rgba(var(--primary-rgb),0.35);
}
.btn-danger {
background: #e74c3c;