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/ 等运行时数据
This commit is contained in:
laoyang
2026-09-21 08:16:25 +08:00
parent 7392b867c5
commit d7c6ac2559
24 changed files with 7238 additions and 2 deletions
+128
View File
@@ -0,0 +1,128 @@
<?php
// 测试裁剪保存功能
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ticket_crop'])) {
$base64 = $_POST['ticket_crop'];
if (strlen($base64) > 100) {
$path = saveBase64Image($base64, TICKET_DIR, 'test_crop');
if ($path) {
echo "✅ 保存成功: <a href='/$path' target='_blank'>查看图片</a>";
} else {
echo "❌ 保存失败,检查目录权限和 fileinfo 扩展";
}
} else {
echo "⚠️ 数据太短,可能未裁剪";
}
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试裁剪</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; }
img { max-width: 100%; }
.btn { padding: 8px 16px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
.modal { display: none; position: fixed; top:0; left:0; right:0; bottom:0; background: rgba(0,0,0,0.5); z-index:999; justify-content:center; align-items:center; }
.modal.active { display: flex; }
.modal-content { background: #fff; padding: 20px; max-width: 600px; width: 90%; }
.modal-footer { margin-top: 16px; display: flex; gap: 12px; justify-content: flex-end; }
</style>
</head>
<body>
<h1>测试裁剪保存</h1>
<p>请上传一张图片,裁剪后提交,检查是否保存成功。</p>
<div>
<input type="file" id="upload" accept="image/*">
<br><br>
<img id="preview" style="max-width:300px; display:none;">
<br>
<button id="cropBtn" style="display:none;" class="btn">✂️ 裁剪</button>
</div>
<form id="form" method="POST">
<input type="hidden" name="ticket_crop" id="ticket_crop">
<button type="submit" class="btn" style="background:#2ecc71;">提交保存</button>
</form>
<div id="result"></div>
<!-- 裁剪模态框 -->
<div class="modal" id="cropModal">
<div class="modal-content">
<h3>裁剪</h3>
<img id="cropImage" src="">
<div class="modal-footer">
<button class="btn" onclick="closeCrop()">取消</button>
<button class="btn" onclick="cropSave()" style="background:#2ecc71;">确认裁剪</button>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
<script>
let cropper = null;
document.getElementById('upload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
const img = document.getElementById('preview');
img.src = ev.target.result;
img.style.display = 'block';
document.getElementById('cropBtn').style.display = 'inline-block';
};
reader.readAsDataURL(file);
});
document.getElementById('cropBtn').addEventListener('click', function() {
const img = document.getElementById('cropImage');
img.src = document.getElementById('preview').src;
document.getElementById('cropModal').classList.add('active');
if (cropper) cropper.destroy();
setTimeout(() => {
cropper = new Cropper(img, { aspectRatio: NaN, viewMode: 1, dragMode: 'move', autoCropArea: 0.8 });
}, 150);
});
function closeCrop() {
document.getElementById('cropModal').classList.remove('active');
if (cropper) { cropper.destroy(); cropper = null; }
}
function cropSave() {
if (!cropper) return;
const canvas = cropper.getCroppedCanvas({ width: 400, height: 400 });
if (!canvas) return;
const base64 = canvas.toDataURL('image/jpeg', 0.9);
document.getElementById('ticket_crop').value = base64;
document.getElementById('preview').src = base64;
closeCrop();
}
document.getElementById('form').addEventListener('submit', function(e) {
e.preventDefault();
const base64 = document.getElementById('ticket_crop').value;
if (base64.length < 100) {
document.getElementById('result').innerHTML = '⚠️ 请先裁剪图片';
return;
}
const formData = new FormData();
formData.append('ticket_crop', base64);
fetch('test_crop.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(html => {
document.getElementById('result').innerHTML = html;
})
.catch(err => {
document.getElementById('result').innerHTML = '❌ 请求失败: ' + err.message;
});
});
</script>
</body>
</html>