界面主题: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:
@@ -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]` 完全一致」。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -67,3 +67,28 @@ function updateAdminPersonId($admin_id, $person_id) {
|
||||
$stmt = $db->prepare("UPDATE admins SET person_id = ? WHERE id = ?");
|
||||
return $stmt->execute([$person_id, $admin_id]);
|
||||
}
|
||||
|
||||
// 保存某个账号的界面主题(key 必须是 lib/theme.php 注册表里的)。
|
||||
// 同时更新 session,这样同一个请求后面渲染页面时就已经是新主题了。
|
||||
// 失败返回 false(非法 key、账号不存在、库报错都算失败)。
|
||||
function setAdminTheme($admin_id, $theme_key) {
|
||||
$admin_id = (int)$admin_id;
|
||||
if ($admin_id <= 0 || !isValidTheme($theme_key)) {
|
||||
return false;
|
||||
}
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("UPDATE admins SET theme = ? WHERE id = ?");
|
||||
if (!$stmt->execute([$theme_key, $admin_id]) || $stmt->rowCount() <= 0) {
|
||||
// rowCount 为 0 时可能是「账号不存在」,也可能是「主题没变」——后者不算失败
|
||||
$chk = $db->prepare("SELECT theme FROM admins WHERE id = ?");
|
||||
$chk->execute([$admin_id]);
|
||||
$rows = $chk->fetchAll(); // fetchAll:避免留下未读完的游标(会挂共享读锁)
|
||||
if (!$rows || (string)($rows[0]['theme'] ?? '') !== $theme_key) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ((int)($_SESSION['admin_id'] ?? 0) === $admin_id) {
|
||||
$_SESSION['admin_theme'] = $theme_key;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+8
-1
@@ -21,12 +21,15 @@ function initDB() {
|
||||
$db = getDB();
|
||||
|
||||
// 管理员表
|
||||
// theme:该账号选的界面配色主题(见 lib/theme.php 的主题注册表)。
|
||||
// 留空/NULL 表示用默认主题,所以老账号不需要回填数据。
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
avatar TEXT
|
||||
avatar TEXT,
|
||||
theme TEXT
|
||||
)
|
||||
");
|
||||
|
||||
@@ -163,6 +166,10 @@ function initDB() {
|
||||
try { $db->exec("ALTER TABLE review_comments ADD COLUMN parent_id INTEGER"); } catch (PDOException $e) {}
|
||||
try { $db->exec("ALTER TABLE review_comments ADD COLUMN reply_to_admin_id INTEGER"); } catch (PDOException $e) {}
|
||||
|
||||
// 新增:每个账号自己的界面配色主题(见 lib/theme.php)。
|
||||
// 只要空列,不需要给老账号回填任何数据 —— 空值即「用默认主题」。
|
||||
try { $db->exec("ALTER TABLE admins ADD COLUMN theme TEXT"); } catch (PDOException $e) {}
|
||||
|
||||
// 删顶层评论时要按 parent_id 找它下面的回复一并清掉,给它建个索引。
|
||||
// 这条索引必须放在上面的「补列」之后:老库的 review_comments 是旧结构、没有
|
||||
// parent_id 列,而上面建表用的是 IF NOT EXISTS(对已存在的表不生效),
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// 文件: lib/theme.php
|
||||
// 说明: 界面配色主题(5 套经典配色)。
|
||||
//
|
||||
// 主题是**每个账号自己的偏好**,存在 admins.theme 里(空 = 用默认主题)。
|
||||
// 颜色本身全部定义在 css/themes.css 的 [data-theme="<key>"] 规则里,
|
||||
// 本文件只负责「有哪些主题 / 当前用哪套 / 页面上怎么声明」,
|
||||
// 不重复存一份色值(唯一例外是下面 themeCatalog() 里给选色卡片用的
|
||||
// 一段渐变缩略图,那是纯装饰,不参与页面配色)。
|
||||
//
|
||||
// 页面接入方式(每个页面的 <html> 与样式表各加一处):
|
||||
// 1) <html lang="zh-CN"> 上紧跟着输出 themeAttr() 的返回值(它自带一个前导空格);
|
||||
// 2) css/style.css 的 link 之后,再输出一行 themesCssLink()。
|
||||
// themes.css 必须排在 style.css 之后 —— [data-theme] 与 style.css 里
|
||||
// 作为兜底的 :root 优先级相同,靠「后出现者胜」来覆盖。
|
||||
//
|
||||
// 注意:上面两行说明里**故意不写 PHP 短标签**。PHP 的「问号 + 右尖括号」
|
||||
// 在 // 注释里同样会关闭 PHP 模式,之后若再出现「左尖括号 + 问号 + 等号」
|
||||
// 就会被真的执行 —— 注释里写出完整标签示例,本文件被 require 时会直接
|
||||
// Fatal error,而 php -l 是查不出来的(它只看语法,不看语义)。
|
||||
// ============================================
|
||||
|
||||
// 主题注册表:key => [显示名, 一句话描述, 明暗]
|
||||
// 想加主题:这里加一项,并在 css/themes.css 里补一个同 key 的 [data-theme] 规则块
|
||||
// (规则块里那一组变量一个都不能少,缺哪个就会退回 style.css 的默认值)。
|
||||
function themeCatalog() {
|
||||
static $catalog = null;
|
||||
if ($catalog === null) {
|
||||
$catalog = [
|
||||
'ticket' => [
|
||||
'name' => '复古票根',
|
||||
'desc' => '暖橙 · 默认配色',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'indigo' => [
|
||||
'name' => '靛蓝',
|
||||
'desc' => '经典蓝 · 清爽利落',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'forest' => [
|
||||
'name' => '松林',
|
||||
'desc' => '墨绿 · 沉静耐看',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'wine' => [
|
||||
'name' => '绛红',
|
||||
'desc' => '酒红 · 复古胶片',
|
||||
'scheme' => 'light',
|
||||
],
|
||||
'midnight' => [
|
||||
'name' => '暗夜',
|
||||
'desc' => '深色 · 靛紫点缀',
|
||||
'scheme' => 'dark',
|
||||
],
|
||||
];
|
||||
}
|
||||
return $catalog;
|
||||
}
|
||||
|
||||
// 默认主题(没选过 / 选的 key 不认识了,都落到它上面)
|
||||
function defaultThemeKey() {
|
||||
return 'ticket';
|
||||
}
|
||||
|
||||
// key 是否有效
|
||||
function isValidTheme($key) {
|
||||
return is_string($key) && isset(themeCatalog()[$key]);
|
||||
}
|
||||
|
||||
// 取某套主题的元信息;非法 key 返回默认主题的(页面渲染不该因为脏数据而炸)
|
||||
function themeInfo($key) {
|
||||
$catalog = themeCatalog();
|
||||
return isValidTheme($key) ? $catalog[$key] : $catalog[defaultThemeKey()];
|
||||
}
|
||||
|
||||
// 当前生效的主题 key。
|
||||
// 优先读 session(登录时/切换主题时写入),session 里还没有就回库里补一次 ——
|
||||
// 这样「本次升级之前就已经登录着」的会话也能立刻用上自己账号存的主题。
|
||||
function currentThemeKey() {
|
||||
if (!isset($_SESSION['admin_theme'])) {
|
||||
$theme = '';
|
||||
if (!empty($_SESSION['admin_id'])) {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT theme FROM admins WHERE id = ?");
|
||||
$stmt->execute([(int)$_SESSION['admin_id']]);
|
||||
// 用 fetchAll 取:只取一行就放着不读完,游标会一直挂着共享读锁(本项目踩过)
|
||||
$rows = $stmt->fetchAll();
|
||||
if ($rows && isset($rows[0]['theme'])) {
|
||||
$theme = (string)$rows[0]['theme'];
|
||||
}
|
||||
}
|
||||
$_SESSION['admin_theme'] = $theme;
|
||||
}
|
||||
$key = (string)$_SESSION['admin_theme'];
|
||||
return isValidTheme($key) ? $key : defaultThemeKey();
|
||||
}
|
||||
|
||||
// <html> 上要输出的主题属性。始终输出(默认主题也输出),
|
||||
// 这样每个页面用的都是 themes.css 里那个明确的调色板,不用去猜 style.css 的兜底值。
|
||||
function themeAttr() {
|
||||
return ' data-theme="' . h(currentThemeKey()) . '"';
|
||||
}
|
||||
|
||||
// 主题样式表。必须放在 css/style.css 之后。
|
||||
//
|
||||
// 末尾**带一个换行符**:PHP 会吃掉「结束标签」后面紧跟的那一个换行。
|
||||
// 模板里把这个返回值单独放一行再换行的话,渲染结果会变成这一行和下一行黏在一起
|
||||
// (HTML 上无害,但会让 HTML 结构对比工具误判,也没必要)。这里补回来。
|
||||
function themesCssLink() {
|
||||
return '<link rel="stylesheet" href="css/themes.css">' . "\n";
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
// ---------- 驱动切换 ----------
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user