Files
MovieLog_Server/edit.php
T
laoyang dd957d45bd refactor: 拆分 config.php 为 lib/ 模块,消除页面层重复代码
纯结构性重构,不改变任何功能。

- config.php 由 739 行缩减为 42 行,改为纯引导入口;新增 ROOT_DIR 常量
  统一拼接绝对路径(lib/ 内模块的 __DIR__ 会指向 lib/,必须替换)
- 新增 lib/ 五个模块 db.php / helpers.php / auth.php / movies.php / stats.php,
  由 config.php 按 db → helpers → auth → movies → stats 顺序加载
- add.php、edit.php 各自约 80 行重复的 POST 处理改为调用
  resolveCinemaId / resolvePersonIds / linkViewers / uploadImageFile /
  saveBase64Image / uploadMultiplePhotos / deleteStoredFile
- cinemas.php、persons.php 中重复 4 次的颜色校验改用 normalizeHexColor()
- toggleNewViewer / removeViewerRow / toggleNewCinema 下沉至 js/main.js,
  add.php 中的 removeViewer 统一更名为 removeViewerRow
- 删除死代码:imageUrl()(全项目无调用);cinemas.php 配色数组中本就会被
  array_slice 丢弃的 extra 颜色逻辑,简化为单条扁平 50 元素数组
- README 目录结构补充 lib/ 说明

验证方式:以便携 PHP 8.4 搭建 A/B 回归环境,播种固定数据后遍历 47 个
GET/POST 端点录制「状态码 + 响应体」。重构前后 42 个逐字节相同、2 个仅
上传文件名时间戳不同、3 个差异均为内联脚本下沉(已单独校验函数体逐字符
等价,剥离脚本后 HTML 标记完全一致)。24 个 PHP 文件语法检查全部通过。
2026-09-21 12:56:30 +08:00

538 lines
24 KiB
PHP
Raw 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
// ============================================
// 文件: edit.php (完整可用)
// 说明: 支持海报、票根、合影裁剪,观影人管理
// ============================================
require_once 'config.php';
checkLogin();
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
header('Location: index.php');
exit;
}
$movie = getMovie($id);
if (!$movie) {
header('Location: index.php');
exit;
}
$admin = getCurrentAdmin();
$cinemas = getCinemas();
// 获取所有观影人
$db = getDB();
$stmt = $db->query("SELECT * FROM persons ORDER BY name");
$all_persons = $stmt->fetchAll();
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title'] ?? '');
$info = trim($_POST['info'] ?? '');
$watch_date = $_POST['watch_date'] ?? '';
$cinema_id = $_POST['cinema_id'] ?? '';
$viewer_selects = $_POST['viewer_select'] ?? [];
$viewer_news = $_POST['viewers_new'] ?? [];
$poster_crop = $_POST['poster_crop'] ?? '';
$ticket_crop = $_POST['ticket_crop'] ?? '';
$photo_crops = $_POST['photo_crop'] ?? [];
// 电影院:选择已有影院、新建影院或留空
$cinema_id = resolveCinemaId(
$cinema_id,
trim($_POST['new_cinema_place'] ?? ''),
trim($_POST['new_cinema_name'] ?? ''),
trim($_POST['new_cinema_address'] ?? '')
);
if (empty($title)) {
$error = '请输入电影名称';
} elseif (empty($watch_date)) {
$error = '请选择观影日期';
} else {
// ----- 海报:裁剪结果优先,其次上传新文件 -----
$poster_path = $movie['poster_path'];
if (!empty($poster_crop)) {
$new_path = saveBase64Image($poster_crop, POSTER_DIR, 'poster');
if ($new_path) {
deleteStoredFile($poster_path);
$poster_path = $new_path;
} else {
$error = '海报裁剪保存失败,请重试';
}
} else {
$uploaded = uploadImageFile('poster', POSTER_DIR, 'uploads/posters/');
if ($uploaded) {
deleteStoredFile($poster_path);
$poster_path = $uploaded;
}
}
// ----- 票根:裁剪结果优先,其次上传新文件 -----
$ticket_path = $movie['ticket_path'];
if (!empty($ticket_crop)) {
$new_path = saveBase64Image($ticket_crop, TICKET_DIR, 'ticket');
if ($new_path) {
deleteStoredFile($ticket_path);
$ticket_path = $new_path;
} else {
$error = '票根裁剪保存失败,请重试';
}
} else {
$uploaded = uploadImageFile('ticket', TICKET_DIR, 'uploads/tickets/');
if ($uploaded) {
deleteStoredFile($ticket_path);
$ticket_path = $uploaded;
}
}
// 前面处理没有错误,才更新数据库
if (empty($error)) {
// 更新电影基本信息
$stmt = $db->prepare("
UPDATE movies
SET title = ?, info = ?, poster_path = ?, ticket_path = ?, cinema_id = ?, watch_date = ?
WHERE id = ?
");
$stmt->execute([$title, $info, $poster_path, $ticket_path, $cinema_id, $watch_date, $id]);
// ----- 合影:替换已裁剪的图片 -----
if (!empty($photo_crops)) {
foreach ($photo_crops as $photo_id => $base64_data) {
if (empty($base64_data)) continue;
$stmt = $db->prepare("SELECT photo_path FROM movie_photos WHERE id = ? AND movie_id = ?");
$stmt->execute([$photo_id, $id]);
$old = $stmt->fetch();
if ($old) {
$new_path = saveBase64Image($base64_data, PHOTO_DIR, 'photo');
if ($new_path) {
deleteStoredFile($old['photo_path']);
$stmt = $db->prepare("UPDATE movie_photos SET photo_path = ? WHERE id = ?");
$stmt->execute([$new_path, $photo_id]);
}
}
}
}
// ----- 新增合影照片 -----
uploadMultiplePhotos('new_photos', $id);
// ----- 一起看的人 -----
$stmt = $db->prepare("DELETE FROM viewers WHERE movie_id = ?");
$stmt->execute([$id]);
linkViewers($id, resolvePersonIds($viewer_selects, $viewer_news));
header('Location: detail.php?id=' . $id);
exit;
}
}
}
// 准备观影人数据
$current_person_ids = array_column($movie['viewers'], 'person_id');
$current_person_ids = array_filter($current_person_ids, function($v) { return $v > 0; });
if (empty($current_person_ids)) {
$current_person_ids = [0];
}
?>
<!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">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style>
/* ===== 全局样式 ===== */
.form-container { max-width: 720px; margin: 0 auto; padding: 20px; }
.form-card { background: var(--card-bg); border-radius: 16px; padding: 32px 36px; box-shadow: var(--shadow); }
.form-card h2 { font-size: 24px; font-weight: 700; margin: 0 0 8px; }
.form-card .sub { color: var(--text-secondary); font-size: 14px; margin-bottom: 24px; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-weight: 600; font-size: 14px; color: var(--text-primary); margin-bottom: 6px; }
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 12px 16px; border: 1px solid var(--border-color); border-radius: 10px;
font-size: 15px; font-family: inherit; background: var(--input-bg); color: var(--text-primary);
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);
}
.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-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-hint { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }
.viewer-input-group { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; }
.viewer-input-group select, .viewer-input-group input { flex: 1; }
.viewer-input-group select { width: 60%; }
.viewer-input-group input[type="text"] { width: 40%; }
.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); }
.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); }
.btn-secondary:hover { background: var(--border-color); }
.error-box { background: rgba(231,76,60,0.08); border: 1px solid rgba(231,76,60,0.2); color: #e74c3c; padding: 12px 16px; border-radius: 10px; margin-bottom: 20px; font-size: 14px; }
.cinema-new-group { margin-top: 12px; padding: 16px 18px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--border-color); }
.cinema-new-group .form-group:last-child { margin-bottom: 0; }
.current-files { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }
.current-files a { color: var(--primary); text-decoration: none; }
.current-files a:hover { text-decoration: underline; }
.crop-btn {
display: inline-block;
padding: 4px 12px;
background: #3498db;
color: #fff;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-top: 4px;
}
.crop-btn:hover { background: #2980b9; }
.image-preview {
max-width: 120px;
max-height: 120px;
border-radius: 4px;
border: 1px solid var(--border-color);
}
.photo-item {
display: inline-block;
margin: 4px;
text-align: center;
}
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 999;
justify-content: center;
align-items: center;
}
.modal-overlay.active { display: flex; }
.modal-content {
background: var(--card-bg);
max-width: 600px;
width: 90%;
padding: 24px;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-header h3 { margin: 0; }
.modal-header .close-btn {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: var(--text-secondary);
}
.modal-body img { max-width: 100%; display: block; }
.modal-footer {
margin-top: 16px;
display: flex;
gap: 12px;
justify-content: flex-end;
}
@media (max-width: 600px) {
.form-card { padding: 20px 16px; }
.form-row { grid-template-columns: 1fr; }
.form-actions { flex-direction: column; }
.form-actions .btn { width: 100%; text-align: center; }
}
</style>
</head>
<body>
<!-- 导航栏 -->
<?php include 'nav.php'; ?>
<main class="main-content">
<div class="form-container">
<div class="form-card">
<h2>✏️ 编辑电影</h2>
<p class="sub">修改「<?= h($movie['title']) ?>」的信息</p>
<?php if ($error): ?><div class="error-box"><?= h($error) ?></div><?php endif; ?>
<form method="POST" action="" enctype="multipart/form-data" id="editForm">
<!-- 电影名称 -->
<div class="form-group">
<label>电影名称 <span class="hint">*必填</span></label>
<input type="text" name="title" value="<?= h($movie['title']) ?>" required>
</div>
<!-- 电影信息 -->
<div class="form-group">
<label>电影信息</label>
<textarea name="info"><?= h($movie['info']) ?></textarea>
</div>
<!-- 观影日期 -->
<div class="form-group">
<label>观影日期 <span class="hint">*必填</span></label>
<input type="date" name="watch_date" value="<?= $movie['watch_date'] ?>" required>
</div>
<!-- 电影院 -->
<div class="form-group">
<label>电影院</label>
<select name="cinema_id" id="cinemaSelect" onchange="toggleNewCinema()">
<option value="">-- 不选择 --</option>
<?php foreach ($cinemas as $c): ?>
<option value="<?= $c['id'] ?>" <?= ($movie['cinema_id'] == $c['id']) ? 'selected' : '' ?>>
<?= h($c['place']) ?> · <?= h($c['name']) ?>
</option>
<?php endforeach; ?>
<option value="new"> 新建电影院</option>
</select>
</div>
<div class="cinema-new-group" id="newCinemaGroup" style="display: none;">
<div class="form-group">
<label>所在地方 <span class="hint">如: 万达广场</span></label>
<input type="text" name="new_cinema_place" placeholder="如: 万达广场">
</div>
<div class="form-group">
<label>电影院名称 <span class="hint">*必填</span></label>
<input type="text" name="new_cinema_name" placeholder="如: 万达影城(IMAX店)">
</div>
<div class="form-group">
<label>详细地址</label>
<input type="text" name="new_cinema_address" placeholder="详细地址">
</div>
</div>
<!-- 观影人 -->
<div class="form-group">
<label>一起看的人</label>
<div id="viewerContainer"></div>
<button type="button" class="btn-add-viewer" onclick="addViewerRow()"> 添加一位</button>
<div class="form-hint">选择已有观影人,或选择“新增”后输入姓名</div>
</div>
<!-- 海报 -->
<div class="form-group">
<label>电影海报</label>
<div>
<?php if ($movie['poster_path'] && file_exists(__DIR__ . '/' . $movie['poster_path'])): ?>
<img src="/<?= $movie['poster_path'] ?>" class="image-preview" id="posterPreview">
<br>
<button type="button" class="crop-btn" onclick="openCrop('poster', '/<?= $movie['poster_path'] ?>')">✂️ 裁剪海报</button>
<input type="hidden" name="poster_crop" id="poster_crop">
<?php else: ?>
<span style="color:var(--text-secondary);font-size:14px;">暂无海报</span>
<?php endif; ?>
</div>
<div style="margin-top:6px;">
<label style="font-weight:400;font-size:13px;">或上传新海报:</label>
<input type="file" name="poster" accept="image/*">
</div>
</div>
<!-- 票根 -->
<div class="form-group">
<label>电影票扫描件</label>
<div>
<?php if ($movie['ticket_path'] && file_exists(__DIR__ . '/' . $movie['ticket_path'])): ?>
<img src="/<?= $movie['ticket_path'] ?>" class="image-preview" id="ticketPreview">
<br>
<button type="button" class="crop-btn" onclick="openCrop('ticket', '/<?= $movie['ticket_path'] ?>')">✂️ 裁剪票根</button>
<input type="hidden" name="ticket_crop" id="ticket_crop">
<?php else: ?>
<span style="color:var(--text-secondary);font-size:14px;">暂无票根</span>
<?php endif; ?>
</div>
<div style="margin-top:6px;">
<label style="font-weight:400;font-size:13px;">或上传新票根:</label>
<input type="file" name="ticket" accept="image/*">
</div>
</div>
<!-- 观影照片 -->
<div class="form-group">
<label>观影照片</label>
<div>
<?php if (!empty($movie['photos'])): ?>
<?php foreach ($movie['photos'] as $photo): ?>
<div class="photo-item" id="photo_<?= $photo['id'] ?>">
<img src="/<?= $photo['photo_path'] ?>" class="image-preview" style="display:block;">
<button type="button" class="crop-btn" onclick="openCrop('photo_<?= $photo['id'] ?>', '/<?= $photo['photo_path'] ?>')">✂️ 裁剪</button>
<input type="hidden" name="photo_crop[<?= $photo['id'] ?>]" id="photo_crop_<?= $photo['id'] ?>">
</div>
<?php endforeach; ?>
<?php else: ?>
<span style="color:var(--text-secondary);font-size:14px;">暂无观影照片</span>
<?php endif; ?>
</div>
<div style="margin-top:6px;">
<label style="font-weight:400;font-size:13px;">新增照片(可多选):</label>
<input type="file" name="new_photos[]" accept="image/*" multiple>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 保存修改</button>
<a href="detail.php?id=<?= $id ?>" class="btn btn-secondary">取消</a>
</div>
</form>
</div>
</div>
</main>
<!-- 裁剪模态框 -->
<div class="modal-overlay" id="cropModal">
<div class="modal-content">
<div class="modal-header">
<h3>✂️ 裁剪图片</h3>
<button class="close-btn" onclick="closeCrop()">&times;</button>
</div>
<div class="modal-body">
<img id="cropImage" src="">
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeCrop()">取消</button>
<button class="btn btn-primary" onclick="cropSave()">✅ 确认裁剪</button>
</div>
</div>
</div>
<script src="js/main.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
<script>
// ============================================================
// 完整 JavaScript
// ============================================================
// ---------- 观影人 ----------
var allPersons = <?= json_encode($all_persons) ?>;
var selectedPersonIds = <?= json_encode($current_person_ids) ?>;
function createViewerRow(value) {
var container = document.getElementById('viewerContainer');
var div = document.createElement('div');
div.className = 'viewer-input-group';
var selectHtml = '<select name="viewer_select[]" onchange="toggleNewViewer(this)" class="viewer-select" style="flex:1;padding:8px;border:1px solid #ddd;border-radius:6px;">';
selectHtml += '<option value="">-- 选择 --</option>';
allPersons.forEach(function(p) {
var selected = (p.id == value) ? 'selected' : '';
selectHtml += '<option value="' + p.id + '" ' + selected + '>' + p.name + '</option>';
});
selectHtml += '<option value="new" ' + (value === 'new' ? 'selected' : '') + '> 新增</option>';
selectHtml += '</select>';
var inputHtml = '<input type="text" name="viewers_new[]" placeholder="输入新姓名" style="flex:1;display:' + (value === 'new' ? 'block' : 'none') + ';padding:8px;border:1px solid #ddd;border-radius:6px;">';
var removeBtn = '<button type="button" class="btn-remove-viewer" onclick="removeViewerRow(this)">×</button>';
div.innerHTML = selectHtml + inputHtml + removeBtn;
container.appendChild(div);
}
function addViewerRow(value) {
value = value || '';
createViewerRow(value);
}
document.addEventListener('DOMContentLoaded', function() {
var container = document.getElementById('viewerContainer');
container.innerHTML = '';
if (selectedPersonIds.length > 0) {
selectedPersonIds.forEach(function(pid) {
if (pid === 0) createViewerRow('');
else createViewerRow(pid);
});
} else {
createViewerRow('');
}
toggleNewCinema();
});
// ---------- 裁剪 ----------
let cropper = null;
let currentTarget = '';
function openCrop(target, imgSrc) {
currentTarget = target;
var img = document.getElementById('cropImage');
img.src = imgSrc;
document.getElementById('cropModal').classList.add('active');
if (cropper) {
cropper.destroy();
cropper = null;
}
setTimeout(function() {
cropper = new Cropper(img, {
aspectRatio: NaN,
viewMode: 1,
dragMode: 'move',
autoCropArea: 0.8,
responsive: true,
restore: false,
});
}, 150);
}
function closeCrop() {
document.getElementById('cropModal').classList.remove('active');
if (cropper) {
cropper.destroy();
cropper = null;
}
}
function cropSave() {
if (!cropper) {
alert('请先加载图片');
return;
}
try {
var canvas = cropper.getCroppedCanvas({
width: 600,
height: 600,
imageSmoothingQuality: 'high',
});
if (!canvas) {
alert('裁剪失败,请重试');
return;
}
var base64 = canvas.toDataURL('image/jpeg', 0.9);
if (currentTarget === 'poster') {
document.getElementById('poster_crop').value = base64;
var preview = document.getElementById('posterPreview');
if (preview) preview.src = base64;
} else if (currentTarget === 'ticket') {
document.getElementById('ticket_crop').value = base64;
var preview = document.getElementById('ticketPreview');
if (preview) preview.src = base64;
} else if (currentTarget.startsWith('photo_')) {
var photoId = currentTarget.replace('photo_', '');
document.getElementById('photo_crop_' + photoId).value = base64;
var preview = document.querySelector('#photo_' + photoId + ' .image-preview');
if (preview) preview.src = base64;
}
closeCrop();
} catch (e) {
alert('裁剪失败:' + e.message);
}
}
// 点击模态框外部关闭
document.getElementById('cropModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCrop();
}
});
</script>
</body>
</html>