界面主题: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` 加盐哈希存储口令,全站页面均需登录。 - **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`)。 - **谁可以评价**:**只要有登录账号就能评价**,与「是不是观影人」无关——没一起去看过的人也能打分、写评价、参与讨论。评价的归属是账号(`admins`),不是观影人(`persons`)。
- **页面上默认不出现任何输入框**:详情页「💬 评价与评论」区块上来只展示已有内容;要写什么就点对应那个按钮——「📝 写评价」「✏️ 修改我的评价」「💬 评论」「↩ 回复」,点开才当场展开表单,提交后页面刷新、表单自动收起。折叠用的是原生 `<details>/<summary>`,**不依赖 JS**,键盘也能操作。 - **页面上默认不出现任何输入框**:详情页「💬 评价与评论」区块上来只展示已有内容;要写什么就点对应那个按钮——「📝 写评价」「✏️ 修改我的评价」「💬 评论」「↩ 回复」,点开才当场展开表单,提交后页面刷新、表单自动收起。折叠用的是原生 `<details>/<summary>`,**不依赖 JS**,键盘也能操作。
@@ -71,7 +89,8 @@
- **观影人管理**:增删改查,支持头像裁剪上传与专属配色;已被电影引用的观影人不允许直接删除。 - **观影人管理**:增删改查,支持头像裁剪上传与专属配色;已被电影引用的观影人不允许直接删除。
### 界面与体验 ### 界面与体验
- 复古票根色系(主色 `#e67e22` 橙)的响应式设计,移动端断点适配(600 / 768px)。 - **5 套配色主题可选**(详见下面的「界面主题」),主色、底色、卡片、边框、阴影、滚动条全部跟随主题。
- 响应式设计,移动端断点适配(600 / 768px)。
- 导航为下拉菜单,自动高亮当前页;顶部始终显示当前用户头像与用户名。 - 导航为下拉菜单,自动高亮当前页;顶部始终显示当前用户头像与用户名。
- **点导航栏左上角的「🎬 电影日志」即可回到首页**(右键 / Ctrl+点击还能在新标签页打开),不必再展开右侧折叠菜单去找「首页」。 - **点导航栏左上角的「🎬 电影日志」即可回到首页**(右键 / Ctrl+点击还能在新标签页打开),不必再展开右侧折叠菜单去找「首页」。
- 登录页为深色渐变 + 毛玻璃风格。 - 登录页为深色渐变 + 毛玻璃风格。
@@ -101,6 +120,7 @@ MovieLog_Server/
├── lib/ # 功能模块(被 config.php 统一引入,页面无需单独 require) ├── lib/ # 功能模块(被 config.php 统一引入,页面无需单独 require)
│ ├── db.php # DB 连接、建表、字段与历史数据迁移、默认管理员 │ ├── db.php # DB 连接、建表、字段与历史数据迁移、默认管理员
│ ├── helpers.php # 输出转义、文件名、颜色校验、上传/删除文件、图片 URL │ ├── helpers.php # 输出转义、文件名、颜色校验、上传/删除文件、图片 URL
│ ├── theme.php # 界面主题注册表(5 套配色)、当前主题、页面上怎么声明
│ ├── settings.php # 系统设置读写;云厂商密钥以 enc:v1:gcm 密文落库 │ ├── settings.php # 系统设置读写;云厂商密钥以 enc:v1:gcm 密文落库
│ ├── storage.php # 存储门面:本地优先读取、URL 生成、上传/删除、迁移、代理读取 │ ├── storage.php # 存储门面:本地优先读取、URL 生成、上传/删除、迁移、代理读取
│ ├── storage/ # 各存储后端驱动 │ ├── storage/ # 各存储后端驱动
@@ -135,7 +155,8 @@ MovieLog_Server/
├── media.php # 服务端图片代理:鉴权后由服务器读取对象并同源回吐 ├── media.php # 服务端图片代理:鉴权后由服务器读取对象并同源回吐
├── fetch_movie.php # 豆瓣接口(片名候选 / 详情 + 海报入库 / 缩略图代理),返回 JSON ├── fetch_movie.php # 豆瓣接口(片名候选 / 详情 + 海报入库 / 缩略图代理),返回 JSON
├── test_crop.php # 开发调试页:验证 Base64 图片落盘是否正常 ├── test_crop.php # 开发调试页:验证 Base64 图片落盘是否正常
├── css/style.css # 全局样式 ├── css/style.css # 全局样式(:root 里是默认主题的兜底色值)
├── css/themes.css # 5 套主题的调色板(必须在 style.css 之后加载)
├── js/main.js # 公共脚本(含各页面共用的表单联动函数、破图兜底) ├── js/main.js # 公共脚本(含各页面共用的表单联动函数、破图兜底)
├── assets/ # 静态资源 ├── assets/ # 静态资源
│ └── default-avatar.png # 默认头像(未上传头像时统一使用) │ └── default-avatar.png # 默认头像(未上传头像时统一使用)
@@ -147,7 +168,7 @@ MovieLog_Server/
└── avatars/ # 观影人头像 └── 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`(专属配色) | | `cinemas` | 电影院 | `place`(所在地方)、`name`、`address`、`color`(专属配色) |
| `movies` | 电影记录 | `title`、`info`、`remark`、`poster_path`、`ticket_path`、`photo_path`(历史遗留单图)、`cinema_id`、`watch_date`、`created_at` | | `movies` | 电影记录 | `title`、`info`、`remark`、`poster_path`、`ticket_path`、`photo_path`(历史遗留单图)、`cinema_id`、`watch_date`、`created_at` |
| `persons` | 观影人 | `name`(唯一)、`color`、`avatar` | | `persons` | 观影人 | `name`(唯一)、`color`、`avatar` |
@@ -284,6 +305,13 @@ server {
> 评价人是**登录账号**。家里几个人各有一个账号时,同一部电影可以各自打分,标题栏会显示「N 条评价 · 均分 X」。 > 评价人是**登录账号**。家里几个人各有一个账号时,同一部电影可以各自打分,标题栏会显示「N 条评价 · 均分 X」。
### 换一套界面配色
1. 右上角展开菜单 → 「系统设置」,找到最上面的「🎨 我的界面主题」。
2. 点任意一张主题卡片,**整页立刻换成那套配色**(包括导航、卡片、按钮、表单控件,以及年度报告的图表)——这一步只是预览。
3. 满意就点「保存主题」。保存后**全站页面都用这套配色,并且不用重新登录**;不满意就再点别的卡片。
4. 主题是**你自己的偏好**,别人登录看到的仍是他们各自选的那套。默认是「复古票根」。
### CSV 批量导入 ### CSV 批量导入
CSV **共 9 列,顺序固定**: CSV **共 9 列,顺序固定**:
@@ -684,6 +712,8 @@ curl -sk -H "Host: 你的域名" -H "Cookie: PHPSESSID=xxxx" -o /dev/null \
- 迁移到云端后**本地文件不会自动删除**,需要自行确认后再清理;本地优先策略意味着本地文件存在时始终读本地,云端副本仅作备份。 - 迁移到云端后**本地文件不会自动删除**,需要自行确认后再清理;本地优先策略意味着本地文件存在时始终读本地,云端副本仅作备份。
- **下拉联想会随输入向豆瓣实时发请求**(已做 320 ms 防抖 + 会话内结果缓存 + 丢弃过期请求,同一关键词只查一次),但**服务端本身没有做接口级限流**。若担心被豆瓣限流,可把 `add.php` 里的 `DEBOUNCE` 调大,或让下拉只在按下「📡 自动获取」时才工作。 - **下拉联想会随输入向豆瓣实时发请求**(已做 320 ms 防抖 + 会话内结果缓存 + 丢弃过期请求,同一关键词只查一次),但**服务端本身没有做接口级限流**。若担心被豆瓣限流,可把 `add.php` 里的 `DEBOUNCE` 调大,或让下拉只在按下「📡 自动获取」时才工作。
- **缩略图代理(`action=thumb`)每张图都要经服务器回源一次**。已加 `Cache-Control: max-age=86400`,同一张图在浏览器侧一天内只回源一次,正常使用量很小;若把下拉列表做得很大、或候选很多,建议改成在服务端落小图缓存。 - **缩略图代理(`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> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>添加电影 - 电影日志</title> <title>添加电影 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <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"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style> <style>
.form-container { .form-container {
@@ -148,7 +149,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.form-group input:focus, .form-group textarea:focus, .form-group select:focus { .form-group input:focus, .form-group textarea:focus, .form-group select:focus {
outline: none; outline: none;
border-color: var(--primary); 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 { .form-group textarea {
min-height: 100px; min-height: 100px;
@@ -214,7 +215,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.btn-add-viewer:hover { .btn-add-viewer:hover {
border-color: var(--primary); border-color: var(--primary);
color: var(--primary); color: var(--primary);
background: rgba(230,126,34,0.05); background: rgba(var(--primary-rgb),0.05);
} }
.form-actions { .form-actions {
display: flex; display: flex;
+28 -7
View File
@@ -31,12 +31,13 @@ $weekday_labels = ['周日','周一','周二','周三','周四','周五','周六
$weekday_values = array_values($stats['weekday_distribution']); $weekday_values = array_values($stats['weekday_distribution']);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $selected_year ?> 年度观影报告 - 电影日志</title> <title><?= $selected_year ?> 年度观影报告 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<!-- Chart.js --> <!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style> <style>
@@ -45,8 +46,10 @@ $weekday_values = array_values($stats['weekday_distribution']);
--report-card: #1a1a2e; --report-card: #1a1a2e;
--report-text: #fffffe; --report-text: #fffffe;
--report-muted: #a7a9be; --report-muted: #a7a9be;
--report-primary: #e67e22; /* 年度报告本身是一套深色「海报」版式,底色固定不动;
--report-secondary: #f39c12; 点缀色跟随用户选的主题,这样蓝色主题下报告也是蓝的。 */
--report-primary: var(--primary);
--report-secondary: var(--primary-light);
} }
body { body {
background: var(--report-bg); background: var(--report-bg);
@@ -507,6 +510,24 @@ $weekday_values = array_values($stats['weekday_distribution']);
<script> <script>
<?php if ($has_data): ?> <?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'); const monthCtx = document.getElementById('monthChart').getContext('2d');
new Chart(monthCtx, { new Chart(monthCtx, {
@@ -516,8 +537,8 @@ $weekday_values = array_values($stats['weekday_distribution']);
datasets: [{ datasets: [{
label: '观影数量', label: '观影数量',
data: <?= json_encode($month_values) ?>, data: <?= json_encode($month_values) ?>,
backgroundColor: 'rgba(230, 126, 34, 0.7)', backgroundColor: 'rgba(' + PRIMARY_RGB + ', 0.7)',
borderColor: '#e67e22', borderColor: PRIMARY_HEX,
borderWidth: 1, borderWidth: 1,
borderRadius: 4 borderRadius: 4
}] }]
@@ -551,8 +572,8 @@ $weekday_values = array_values($stats['weekday_distribution']);
datasets: [{ datasets: [{
label: '观影数量', label: '观影数量',
data: <?= json_encode($weekday_values) ?>, data: <?= json_encode($weekday_values) ?>,
backgroundColor: 'rgba(243, 156, 18, 0.7)', backgroundColor: 'rgba(' + LIGHT_RGB + ', 0.7)',
borderColor: '#f39c12', borderColor: LIGHT_HEX,
borderWidth: 1, borderWidth: 1,
borderRadius: 4 borderRadius: 4
}] }]
+3 -2
View File
@@ -55,12 +55,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>修改密码 - 电影全纪录</title> <title>修改密码 - 电影全纪录</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.form-container { .form-container {
max-width: 500px; max-width: 500px;
@@ -108,7 +109,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.form-group input:focus { .form-group input:focus {
outline: none; outline: none;
border-color: var(--primary); 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 { .form-actions {
display: flex; display: flex;
+6 -4
View File
@@ -99,12 +99,13 @@ $preset_colors = [
]; ];
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>影院管理 - 电影全纪录</title> <title>影院管理 - 电影全纪录</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.container { .container {
max-width: 1000px; max-width: 1000px;
@@ -152,7 +153,7 @@ $preset_colors = [
.form-group input:focus { .form-group input:focus {
outline: none; outline: none;
border-color: var(--primary); 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"] { .form-group input[type="color"] {
padding: 4px; padding: 4px;
@@ -186,7 +187,7 @@ $preset_colors = [
} }
.btn-primary:hover { .btn-primary:hover {
transform: translateY(-2px); 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 { .btn-danger {
background: #e74c3c; background: #e74c3c;
@@ -283,7 +284,8 @@ $preset_colors = [
transform: scale(1.1); transform: scale(1.1);
} }
.preset-color.active { .preset-color.active {
border-color: #333; /* 用前景色描边:色块底色是用户自选的固定色,暗色主题下只有浅色描边才看得见 */
border-color: var(--text-primary);
} }
.color-input-group { .color-input-group {
display: flex; 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/settings.php'; // 系统设置读写
require_once ROOT_DIR . '/lib/storage.php'; // 对象存储门面(含各云厂商驱动) require_once ROOT_DIR . '/lib/storage.php'; // 对象存储门面(含各云厂商驱动)
require_once ROOT_DIR . '/lib/helpers.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/auth.php'; // 登录校验与账号管理
require_once ROOT_DIR . '/lib/movies.php'; // 电影 / 影院 / 搜索 require_once ROOT_DIR . '/lib/movies.php'; // 电影 / 影院 / 搜索
require_once ROOT_DIR . '/lib/reviews.php'; // 评价与评价下的讨论 require_once ROOT_DIR . '/lib/reviews.php'; // 评价与评价下的讨论
+20 -5
View File
@@ -1,20 +1,34 @@
/* ============================================ /* ============================================
文件: css/style.css 文件: css/style.css
说明: 全局样式 - 支持 Grid 网格瀑布流 说明: 全局样式 - 支持 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 { :root {
--primary: #e67e22; --primary: #e67e22;
--primary-dark: #d35400; --primary-dark: #d35400;
--primary-light: #f39c12; --primary-light: #f39c12;
/* 与 --primary 同色的 "R, G, B" 三元组:淡色底/光晕统一用
rgba(var(--primary-rgb), α) 取色,换主题时不用改动任何一条具体规则。 */
--primary-rgb: 230, 126, 34;
--bg-primary: #f0f2f5; --bg-primary: #f0f2f5;
--bg-secondary: #e8eaed; --bg-secondary: #e8eaed;
--card-bg: #ffffff; --card-bg: #ffffff;
--input-bg: #ffffff;
--nav-bg: rgba(255, 255, 255, 0.92);
--text-primary: #1a1a2e; --text-primary: #1a1a2e;
--text-secondary: #6b7280; --text-secondary: #6b7280;
--border-color: #e5e7eb; --border-color: #e5e7eb;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04); --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); --shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.12);
--scroll-thumb: #d1d5db;
--scroll-thumb-hover: #b0b4ba;
--radius: 12px; --radius: 12px;
--radius-lg: 16px; --radius-lg: 16px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -43,11 +57,11 @@ body {
background: transparent; background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #d1d5db; background: var(--scroll-thumb);
border-radius: 8px; border-radius: 8px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #b0b4ba; background: var(--scroll-thumb-hover);
} }
/* ===== 导航栏 ===== */ /* ===== 导航栏 ===== */
@@ -62,7 +76,8 @@ body {
top: 0; top: 0;
z-index: 100; z-index: 100;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
background: rgba(255, 255, 255, 0.92); /* 半透明的导航底:明色主题是白色磨砂,暗色主题自动换成深色磨砂 */
background: var(--nav-bg);
} }
.nav-container { .nav-container {
@@ -140,11 +155,11 @@ body {
.btn-primary { .btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-light)); background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: #fff; 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 { .btn-primary:hover {
transform: translateY(-2px); 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 { .btn-primary:active {
transform: translateY(0); 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); $hasGallery = !empty($gallery);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= h($movie['title']) ?> - 电影日志</title> <title><?= h($movie['title']) ?> - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.detail-container { .detail-container {
max-width: 900px; max-width: 900px;
@@ -610,7 +611,7 @@ $hasGallery = !empty($gallery);
background: linear-gradient(135deg, var(--primary), var(--primary-light)); background: linear-gradient(135deg, var(--primary), var(--primary-light));
border-color: transparent; border-color: transparent;
color: #fff; 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 { .rv-num:focus-within span {
outline: 2px solid var(--primary); outline: 2px solid var(--primary);
@@ -641,7 +642,7 @@ $hasGallery = !empty($gallery);
.rv-textarea:focus { .rv-textarea:focus {
outline: none; outline: none;
border-color: var(--primary); 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 { .rv-textarea-sm {
min-height: 62px; min-height: 62px;
@@ -853,7 +854,7 @@ $hasGallery = !empty($gallery);
} }
.feel-pick input:checked + .feel-inner { .feel-pick input:checked + .feel-inner {
border-color: var(--primary); border-color: var(--primary);
background: rgba(230, 126, 34, 0.12); background: rgba(var(--primary-rgb), 0.12);
color: var(--primary); color: var(--primary);
font-weight: 600; font-weight: 600;
} }
@@ -870,7 +871,7 @@ $hasGallery = !empty($gallery);
gap: 4px; gap: 4px;
padding: 2px 10px; padding: 2px 10px;
border-radius: 20px; border-radius: 20px;
background: rgba(230, 126, 34, 0.10); background: rgba(var(--primary-rgb), 0.10);
color: var(--primary); color: var(--primary);
font-size: 12px; font-size: 12px;
} }
@@ -898,7 +899,7 @@ $hasGallery = !empty($gallery);
transition: var(--transition); transition: var(--transition);
} }
.rv-open-btn:hover { .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 { .rv-composer[open] > .rv-open-btn {
@@ -926,7 +927,7 @@ $hasGallery = !empty($gallery);
} }
.rv-composer[open] > .rv-act-btn { .rv-composer[open] > .rv-act-btn {
border-color: var(--primary); border-color: var(--primary);
background: rgba(230, 126, 34, 0.12); background: rgba(var(--primary-rgb), 0.12);
color: var(--primary); color: var(--primary);
font-weight: 600; font-weight: 600;
} }
+4 -3
View File
@@ -140,12 +140,13 @@ if (empty($current_person_ids)) {
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>编辑电影 - 电影日志</title> <title>编辑电影 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <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"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style> <style>
/* ===== 全局样式 ===== */ /* ===== 全局样式 ===== */
@@ -161,7 +162,7 @@ if (empty($current_person_ids)) {
transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box; 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 { .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 textarea { min-height: 100px; resize: vertical; }
.form-group input[type="file"] { padding: 10px 12px; background: var(--bg-secondary); border: 1px dashed var(--border-color); } .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 { 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-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 { 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 { 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; } .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); } .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> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSV导入 - 电影日志</title> <title>CSV导入 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.form-container { .form-container {
max-width: 720px; max-width: 720px;
@@ -216,7 +217,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
} }
.btn-primary:hover { .btn-primary:hover {
transform: translateY(-2px); 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 { .btn-outline {
background: transparent; background: transparent;
+2 -1
View File
@@ -21,12 +21,13 @@ if (!empty($keyword)) {
attachReviewStatsToMovies($movies); attachReviewStatsToMovies($movies);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电影日志 - 我的电影</title> <title>电影日志 - 我的电影</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
/* 搜索栏样式 */ /* 搜索栏样式 */
.search-bar { .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 = ?"); $stmt = $db->prepare("UPDATE admins SET person_id = ? WHERE id = ?");
return $stmt->execute([$person_id, $admin_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(); $db = getDB();
// 管理员表 // 管理员表
// theme:该账号选的界面配色主题(见 lib/theme.php 的主题注册表)。
// 留空/NULL 表示用默认主题,所以老账号不需要回填数据。
$db->exec(" $db->exec("
CREATE TABLE IF NOT EXISTS admins ( CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
password TEXT 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 parent_id INTEGER"); } catch (PDOException $e) {}
try { $db->exec("ALTER TABLE review_comments ADD COLUMN reply_to_admin_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 找它下面的回复一并清掉,给它建个索引。 // 删顶层评论时要按 parent_id 找它下面的回复一并清掉,给它建个索引。
// 这条索引必须放在上面的「补列」之后:老库的 review_comments 是旧结构、没有 // 这条索引必须放在上面的「补列」之后:老库的 review_comments 是旧结构、没有
// parent_id 列,而上面建表用的是 IF NOT EXISTS(对已存在的表不生效), // 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'])) { if ($admin && password_verify($password, $admin['password'])) {
$_SESSION['admin_id'] = $admin['id']; $_SESSION['admin_id'] = $admin['id'];
$_SESSION['admin_username'] = $admin['username']; $_SESSION['admin_username'] = $admin['username'];
// 把他自己选的界面主题一起带进会话(空 = 用默认主题)
$_SESSION['admin_theme'] = isset($admin['theme']) ? (string)$admin['theme'] : '';
header('Location: index.php'); header('Location: index.php');
exit; exit;
} else { } else {
@@ -38,12 +40,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电影全纪录 - 登录</title> <title>电影全纪录 - 登录</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
/* 登录页专用样式 */ /* 登录页专用样式 */
body { body {
@@ -114,9 +117,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
outline: none; outline: none;
} }
.form-group input:focus { .form-group input:focus {
border-color: #e67e22; border-color: var(--primary);
background: rgba(255,255,255,0.12); 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 { .form-group input::placeholder {
color: rgba(255,255,255,0.3); color: rgba(255,255,255,0.3);
@@ -124,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.btn-login { .btn-login {
width: 100%; width: 100%;
padding: 14px; padding: 14px;
background: linear-gradient(135deg, #e67e22, #f39c12); background: linear-gradient(135deg, var(--primary), var(--primary-light));
border: none; border: none;
border-radius: 12px; border-radius: 12px;
color: #fff; color: #fff;
@@ -137,7 +140,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
.btn-login:hover { .btn-login:hover {
transform: translateY(-2px); 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 { .btn-login:active {
transform: translateY(0); transform: translateY(0);
+5 -4
View File
@@ -137,12 +137,13 @@ $preset_colors = [
]; ];
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>观影人管理 - 电影日志</title> <title>观影人管理 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<!-- Cropper.js --> <!-- Cropper.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style> <style>
@@ -153,12 +154,12 @@ $preset_colors = [
.form-group { margin-bottom: 16px; } .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 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 { 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 { display: flex; gap: 16px; flex-wrap: wrap; }
.form-row .form-group { flex: 1; min-width: 180px; } .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 { 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 { 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 { background: #e74c3c; color: #fff; }
.btn-danger:hover { background: #c0392b; } .btn-danger:hover { background: #c0392b; }
.btn-outline { background: transparent; color: var(--text-secondary); border: 1px solid var(--border-color); } .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-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 { 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:hover { transform: scale(1.15); }
.preset-color.active { border-color: #333; } .preset-color.active { border-color: var(--text-primary); }
/* 头像样式 */ /* 头像样式 */
.avatar-upload { .avatar-upload {
display: flex; display: flex;
+3 -2
View File
@@ -38,12 +38,13 @@ if ($has_conditions) {
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高级搜索 - 电影日志</title> <title>高级搜索 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.search-container { .search-container {
max-width: 900px; max-width: 900px;
@@ -97,7 +98,7 @@ if ($has_conditions) {
.form-group input:focus, .form-group select:focus { .form-group input:focus, .form-group select:focus {
outline: none; outline: none;
border-color: var(--primary); 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 { .form-actions {
display: flex; display: flex;
+125 -4
View File
@@ -108,6 +108,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'clear
$message = '已清除错误提示'; $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(); $drivers = storageDriverLabels();
$current = currentStorageDriver(); $current = currentStorageDriver();
$configs = []; $configs = [];
@@ -138,12 +154,13 @@ function localImageStat() {
$localStat = localImageStat(); $localStat = localImageStat();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统设置 - 电影日志</title> <title>系统设置 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.page-wrap { max-width: 880px; margin: 0 auto; padding: 20px; } .page-wrap { max-width: 880px; margin: 0 auto; padding: 20px; }
.card { .card {
@@ -160,7 +177,7 @@ $localStat = localImageStat();
font-family: inherit; font-family: inherit;
} }
.form-group input:focus, .form-group select:focus { .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 { 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; } .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); cursor: pointer; transition: var(--transition); background: var(--card-bg);
} }
.driver-opt:hover { border-color: var(--primary-light); } .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 .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 .d { font-size: 12.5px; color: var(--text-secondary); margin-top: 4px; }
.driver-opt input { margin-right: 6px; } .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; .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; } 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 { 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 { background: transparent; color: var(--text-secondary); border: 1px solid var(--border-color); }
.btn-outline:hover { background: var(--bg-secondary); } .btn-outline:hover { background: var(--bg-secondary); }
.btn-sm { padding: 6px 14px; font-size: 13px; } .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; } .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 { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.chk input { margin: 0; } .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> </style>
</head> </head>
<body> <body>
@@ -223,6 +290,47 @@ $localStat = localImageStat();
</div> </div>
<?php endif; ?> <?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"> <div class="card">
<h2>图片存储方式</h2> <h2>图片存储方式</h2>
<div class="sub"> <div class="sub">
@@ -656,6 +764,19 @@ $localStat = localImageStat();
<script> <script>
(function () { (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'; 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; $max_year = !empty($year_stats) ? max(array_column($year_stats, 'count')) : 1;
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>统计仪表板 - 电影日志</title> <title>统计仪表板 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.stats-container { .stats-container {
max-width: 1100px; max-width: 1100px;
+4 -3
View File
@@ -132,12 +132,13 @@ if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id']))
$users = getAllAdmins(); $users = getAllAdmins();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN"<?= themeAttr() ?>>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - 电影日志</title> <title>用户管理 - 电影日志</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<?= themesCssLink() ?>
<style> <style>
.form-container { .form-container {
max-width: 800px; max-width: 800px;
@@ -184,7 +185,7 @@ $users = getAllAdmins();
.form-group input:focus, .form-group select:focus { .form-group input:focus, .form-group select:focus {
outline: none; outline: none;
border-color: var(--primary); 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 { .btn {
padding: 10px 20px; padding: 10px 20px;
@@ -203,7 +204,7 @@ $users = getAllAdmins();
} }
.btn-primary:hover { .btn-primary:hover {
transform: translateY(-2px); 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 { .btn-danger {
background: #e74c3c; background: #e74c3c;