Files
laoyang 40e1bc5e56 feat: 完善电影记录系统源码与项目文档
功能模块:
- 观影记录:列表(瀑布流)/ 添加 / 编辑 / 详情 / 删除,删除时同步清理图片文件
- 首页快捷搜索 + 高级搜索(片名、影院、观影人、日期区间组合筛选)
- 影院管理:三段式信息 + 50 种预设配色,删除已引用影院时二次确认
- 观影人管理:头像裁剪上传 + 专属配色,已引用者禁止删除
- 图片处理:Cropper.js 裁剪票根/海报/观影照片/头像,多图上传,首字母 SVG 头像兜底
- OMDb 自动获取影片信息(fetch_movie.php 代理,返回 JSON)
- CSV 批量导入:UTF-8/GBK 自动识别,影院去重,事务保护,逐行错误报告
- 统计仪表板:总览 + 按影院/观影人/年份/近 12 个月排行
- 年度报告:按账号关联的观影人视角生成,含 Chart.js 月度与星期分布图
- 多用户:账号增删改、账号与观影人绑定、修改自己的用户名与密码
- 登录鉴权:Session + password_hash,全站页面登录校验

文档:
- 重写 README:功能特性、技术栈、目录结构、数据库结构、部署与使用指南、接口说明、CSV 格式、安全注意事项、已知限制
- 新增 .gitignore,排除 db/ 与 uploads/ 等运行时数据
2026-09-21 08:20:56 +08:00

474 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ============================================
// 文件: cinemas.php (影院管理 - 含50色)
// ============================================
require_once 'config.php';
checkLogin();
$admin = getCurrentAdmin();
$db = getDB();
$message = '';
$error = '';
// ---------- 处理添加 ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add') {
$place = trim($_POST['place'] ?? '');
$name = trim($_POST['name'] ?? '');
$address = trim($_POST['address'] ?? '');
$color = trim($_POST['color'] ?? '#888888');
// 校验颜色格式
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) {
$color = '#888888';
}
if (empty($name)) {
$error = '影院名称不能为空';
} else {
$stmt = $db->prepare("INSERT INTO cinemas (place, name, address, color) VALUES (?, ?, ?, ?)");
$stmt->execute([$place, $name, $address, $color]);
$message = '影院添加成功';
}
}
// ---------- 处理编辑 ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'edit') {
$id = (int)$_POST['id'];
$place = trim($_POST['place'] ?? '');
$name = trim($_POST['name'] ?? '');
$address = trim($_POST['address'] ?? '');
$color = trim($_POST['color'] ?? '#888888');
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) {
$color = '#888888';
}
if (empty($name)) {
$error = '影院名称不能为空';
} else {
$stmt = $db->prepare("UPDATE cinemas SET place = ?, name = ?, address = ?, color = ? WHERE id = ?");
$stmt->execute([$place, $name, $address, $color, $id]);
$message = '影院信息已更新';
}
}
// ---------- 处理删除 ----------
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
// 检查是否有电影关联
$stmt = $db->prepare("SELECT COUNT(*) FROM movies WHERE cinema_id = ?");
$stmt->execute([$id]);
$count = $stmt->fetchColumn();
if ($count > 0) {
$confirm = isset($_GET['confirm']) ? $_GET['confirm'] : '';
if ($confirm !== 'yes') {
$error = "该影院关联了 {$count} 部电影,删除后这些电影的影院信息将置空。确定删除?请 <a href='?action=delete&id={$id}&confirm=yes' onclick=\"return confirm('确认删除?')\">点击确认删除</a>";
} else {
$stmt = $db->prepare("DELETE FROM cinemas WHERE id = ?");
$stmt->execute([$id]);
$message = '影院已删除,关联电影已置空影院信息';
}
} else {
$stmt = $db->prepare("DELETE FROM cinemas WHERE id = ?");
$stmt->execute([$id]);
$message = '影院已删除';
}
}
// ---------- 获取列表 ----------
$cinemas = getCinemas();
// ---------- 获取编辑数据 ----------
$edit_data = null;
if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$stmt = $db->prepare("SELECT * FROM cinemas WHERE id = ?");
$stmt->execute([$id]);
$edit_data = $stmt->fetch();
if (!$edit_data) {
header('Location: cinemas.php');
exit;
}
}
// ---------- 预设50色 ----------
$preset_colors = [
// 红色系 (5)
'#e74c3c', '#c0392b', '#e67e22', '#d35400', '#f39c12',
// 橙色系 (3)
'#f1c40f', '#f7dc6f', '#f5b041',
// 黄色系 (3)
'#f4d03f', '#f9e79f', '#f8c471',
// 绿色系 (5)
'#2ecc71', '#27ae60', '#1abc9c', '#16a085', '#58d68d',
// 青色系 (4)
'#48c9b0', '#45b39d', '#82e0aa', '#a3e4d7',
// 蓝色系 (7)
'#3498db', '#2980b9', '#2e86c1', '#5dade2', '#85c1e9', '#5499c7', '#aed6f1',
// 紫色系 (5)
'#9b59b6', '#8e44ad', '#af7ac5', '#bb8fce', '#d2b4de',
// 粉色系 (4)
'#e84393', '#fd79a8', '#e91e63', '#f06292',
// 灰色系 (6)
'#95a5a6', '#7f8c8d', '#bdc3c7', '#d5dbdb', '#aab7b8', '#99a3a4',
// 棕色/米色系 (4)
'#a67c52', '#8d6e63', '#d4a574', '#c9a87c',
// 其他 (4)
'#00b894', '#0984e3', '#6c5ce7', '#fd79a8'
];
// 只取前50个,如果有重复或不足可调整,但这里正好约50个(实际46个,再补几个)
$extra = ['#2d3436', '#636e72', '#b2bec3', '#dfe6e9', '#fab1a0', '#e17055', '#00cec9', '#a29bfe'];
$preset_colors = array_merge($preset_colors, $extra);
$preset_colors = array_slice($preset_colors, 0, 50); // 确保正好50个
?>
<!DOCTYPE html>
<html lang="zh-CN">
<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">
<style>
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.card {
background: var(--card-bg);
border-radius: 16px;
padding: 24px 28px;
box-shadow: var(--shadow);
margin-bottom: 24px;
}
.card h2 {
font-size: 22px;
font-weight: 700;
margin: 0 0 8px;
color: var(--text-primary);
}
.card .sub {
color: var(--text-secondary);
font-size: 14px;
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 input[type="text"],
.form-group input[type="color"] {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 15px;
box-sizing: border-box;
transition: border-color 0.2s;
}
.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[type="color"] {
padding: 4px;
height: 44px;
width: 60px;
cursor: pointer;
}
.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-danger {
background: #e74c3c;
color: #fff;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-outline {
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.btn-outline:hover {
background: var(--bg-secondary);
}
.btn-sm {
padding: 4px 12px;
font-size: 13px;
}
.message-box {
padding: 10px 14px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
}
.message-box.success {
background: rgba(39,174,96,0.08);
border: 1px solid rgba(39,174,96,0.2);
color: #27ae60;
}
.message-box.error {
background: rgba(231,76,60,0.08);
border: 1px solid rgba(231,76,60,0.2);
color: #e74c3c;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
table th {
text-align: left;
padding: 10px 8px;
border-bottom: 2px solid var(--border-color);
color: var(--text-secondary);
font-weight: 600;
}
table td {
padding: 10px 8px;
border-bottom: 1px solid var(--border-color);
}
table tr:hover td {
background: var(--bg-secondary);
}
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.empty-row {
text-align: center;
color: var(--text-secondary);
padding: 20px 0;
}
.color-preview {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 3px;
margin-right: 6px;
vertical-align: middle;
border: 1px solid rgba(0,0,0,0.1);
}
.preset-palette {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.preset-color {
display: inline-block;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s, transform 0.2s;
}
.preset-color:hover {
transform: scale(1.1);
}
.preset-color.active {
border-color: #333;
}
.color-input-group {
display: flex;
align-items: center;
gap: 12px;
}
.color-input-group input[type="color"] {
flex-shrink: 0;
}
.color-input-group .color-hex {
font-size: 14px;
color: var(--text-secondary);
font-family: monospace;
}
@media (max-width: 600px) {
.card {
padding: 16px;
}
.form-row {
flex-direction: column;
}
.actions {
flex-wrap: wrap;
}
}
</style>
</head>
<body>
<?php include 'nav.php'; ?>
<main class="main-content">
<div class="container">
<?php if ($message): ?>
<div class="message-box success"><?= h($message) ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="message-box error"><?= $error ?></div>
<?php endif; ?>
<!-- 添加/编辑表单 -->
<div class="card">
<h2><?= $edit_data ? '✏️ 编辑影院' : ' 添加影院' ?></h2>
<p class="sub"><?= $edit_data ? '修改影院信息' : '录入新的电影院' ?></p>
<form method="POST" action="">
<input type="hidden" name="action" value="<?= $edit_data ? 'edit' : 'add' ?>">
<?php if ($edit_data): ?>
<input type="hidden" name="id" value="<?= $edit_data['id'] ?>">
<?php endif; ?>
<div class="form-row">
<div class="form-group">
<label>影院名称 <span class="hint">*必填</span></label>
<input type="text" name="name" value="<?= $edit_data ? h($edit_data['name']) : '' ?>" placeholder="如:万达影城(IMAX店)" required>
</div>
<div class="form-group">
<label>所在地方</label>
<input type="text" name="place" value="<?= $edit_data ? h($edit_data['place']) : '' ?>" placeholder="如:万达广场">
</div>
<div class="form-group">
<label>详细地址</label>
<input type="text" name="address" value="<?= $edit_data ? h($edit_data['address']) : '' ?>" placeholder="详细街道地址">
</div>
</div>
<!-- 颜色选择 -->
<div class="form-group">
<label>主题颜色</label>
<div class="color-input-group">
<input type="color" name="color" id="colorPicker" value="<?= $edit_data ? h($edit_data['color']) : '#888888' ?>">
<span class="color-hex" id="colorHex"><?= $edit_data ? h($edit_data['color']) : '#888888' ?></span>
</div>
<div class="preset-palette">
<?php foreach ($preset_colors as $pc): ?>
<span class="preset-color" data-color="<?= $pc ?>" style="background: <?= $pc ?>;" onclick="setColor('<?= $pc ?>')"></span>
<?php endforeach; ?>
</div>
</div>
<div style="margin-top: 12px;">
<button type="submit" class="btn btn-primary">💾 保存</button>
<?php if ($edit_data): ?>
<a href="cinemas.php" class="btn btn-outline">取消编辑</a>
<?php endif; ?>
</div>
</form>
</div>
<!-- 影院列表 -->
<div class="card">
<h2>🏛️ 影院列表</h2>
<p class="sub">共 <?= count($cinemas) ?> 家影院</p>
<div class="table-wrap">
<?php if (count($cinemas) > 0): ?>
<table>
<thead>
<tr>
<th>名称</th>
<th>所在地方</th>
<th>详细地址</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($cinemas as $c): ?>
<tr>
<td>
<span class="color-preview" style="background: <?= h($c['color'] ?: '#888888') ?>;"></span>
<strong><?= h($c['name']) ?></strong>
</td>
<td><?= h($c['place']) ?: '-' ?></td>
<td><?= h($c['address']) ?: '-' ?></td>
<td>
<div class="actions">
<a href="?action=edit&id=<?= $c['id'] ?>" class="btn btn-outline btn-sm">✏️ 编辑</a>
<a href="?action=delete&id=<?= $c['id'] ?>" class="btn btn-danger btn-sm" onclick="return confirm('确定删除「<?= h($c['name']) ?>」吗?关联电影将置空影院信息。')">🗑️ 删除</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="empty-row">暂无影院,请添加</div>
<?php endif; ?>
</div>
</div>
</div>
</main>
<script>
function setColor(color) {
document.getElementById('colorPicker').value = color;
document.getElementById('colorHex').textContent = color;
// 高亮当前选中的预设色块
document.querySelectorAll('.preset-color').forEach(el => {
el.classList.remove('active');
});
document.querySelector(`.preset-color[data-color="${color}"]`).classList.add('active');
}
// 当颜色选择器手动变化时,更新hex显示和激活状态
document.getElementById('colorPicker').addEventListener('input', function(e) {
var color = this.value;
document.getElementById('colorHex').textContent = color;
document.querySelectorAll('.preset-color').forEach(el => {
el.classList.remove('active');
if (el.dataset.color === color) {
el.classList.add('active');
}
});
});
// 页面加载时,高亮当前颜色
document.addEventListener('DOMContentLoaded', function() {
var currentColor = document.getElementById('colorPicker').value;
if (currentColor) {
document.querySelectorAll('.preset-color').forEach(el => {
if (el.dataset.color === currentColor) {
el.classList.add('active');
}
});
document.getElementById('colorHex').textContent = currentColor;
}
});
</script>
</body>
</html>