Files

484 lines
22 KiB
PHP
Raw Permalink Normal View History

<?php
// ============================================
// 文件: persons.php (含头像裁剪)
// ============================================
require_once 'config.php';
checkLogin();
$admin = getCurrentAdmin();
$db = getDB();
$message = '';
$error = '';
// ---------- 处理添加 ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add') {
$name = trim($_POST['name'] ?? '');
$color = trim($_POST['color'] ?? '#888888');
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) $color = '#888888';
// 头像 base64
$avatar_base64 = $_POST['avatar_base64'] ?? '';
if (empty($name)) {
$error = '姓名不能为空';
} else {
// 检查重复
$stmt = $db->prepare("SELECT id FROM persons WHERE name = ?");
$stmt->execute([$name]);
if ($stmt->fetch()) {
$error = '该姓名已存在';
} else {
// 保存头像
$avatar_path = null;
if (!empty($avatar_base64)) {
$avatar_path = saveBase64Image($avatar_base64, AVATAR_DIR, 'avatar');
}
$stmt = $db->prepare("INSERT INTO persons (name, color, avatar) VALUES (?, ?, ?)");
$stmt->execute([$name, $color, $avatar_path]);
$message = '观影人添加成功';
}
}
}
// ---------- 处理编辑 ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'edit') {
$id = (int)$_POST['id'];
$name = trim($_POST['name'] ?? '');
$color = trim($_POST['color'] ?? '#888888');
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) $color = '#888888';
$avatar_base64 = $_POST['avatar_base64'] ?? '';
if (empty($name)) {
$error = '姓名不能为空';
} else {
// 检查重复
$stmt = $db->prepare("SELECT id FROM persons WHERE name = ? AND id != ?");
$stmt->execute([$name, $id]);
if ($stmt->fetch()) {
$error = '该姓名已存在';
} else {
// 获取旧头像
$stmt = $db->prepare("SELECT avatar FROM persons WHERE id = ?");
$stmt->execute([$id]);
$old = $stmt->fetch();
$old_avatar = $old['avatar'] ?? null;
// 保存新头像
$avatar_path = $old_avatar;
if (!empty($avatar_base64)) {
// 删除旧头像
if ($old_avatar && file_exists(__DIR__ . '/' . $old_avatar)) {
unlink(__DIR__ . '/' . $old_avatar);
}
$avatar_path = saveBase64Image($avatar_base64, AVATAR_DIR, 'avatar');
}
$stmt = $db->prepare("UPDATE persons SET name = ?, color = ?, avatar = ? WHERE id = ?");
$stmt->execute([$name, $color, $avatar_path, $id]);
$message = '观影人信息已更新';
}
}
}
// ---------- 处理删除 ----------
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$stmt = $db->prepare("SELECT COUNT(*) FROM viewers WHERE person_id = ?");
$stmt->execute([$id]);
$count = $stmt->fetchColumn();
if ($count > 0) {
$error = "该观影人关联了 {$count} 部电影,不能直接删除。请先移除关联。";
} else {
// 删除头像文件
$stmt = $db->prepare("SELECT avatar FROM persons WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
if ($row && $row['avatar'] && file_exists(__DIR__ . '/' . $row['avatar'])) {
unlink(__DIR__ . '/' . $row['avatar']);
}
$stmt = $db->prepare("DELETE FROM persons WHERE id = ?");
$stmt->execute([$id]);
$message = '观影人已删除';
}
}
// ---------- 获取列表 ----------
$stmt = $db->query("SELECT * FROM persons ORDER BY name");
$persons = $stmt->fetchAll();
// ---------- 获取编辑数据 ----------
$edit_data = null;
if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$stmt = $db->prepare("SELECT * FROM persons WHERE id = ?");
$stmt->execute([$id]);
$edit_data = $stmt->fetch();
if (!$edit_data) header('Location: persons.php');
}
// 50种预设颜色
$preset_colors = [
'#e74c3c','#c0392b','#e67e22','#d35400','#f39c12',
'#f1c40f','#f7dc6f','#2ecc71','#27ae60','#1abc9c',
'#16a085','#3498db','#2980b9','#9b59b6','#8e44ad',
'#34495e','#2c3e50','#e84393','#fd79a8','#00b894',
'#00cec9','#0984e3','#6c5ce7','#fdcb6e','#e17055',
'#d63031','#f0932b','#6ab04c','#22a6b3','#4834d4',
'#be2edd','#130f40','#535c68','#95afc0','#7ed6df',
'#e056a0','#ffb142','#33d9b2','#227093','#218c74',
'#b33939','#cd6133','#f97f51','#82589f','#3dc1d3',
'#b8e994','#78e08f','#f6b93b','#e55039','#4a69bd'
];
?>
<!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">
<!-- Cropper.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<style>
.container { max-width: 900px; 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 { 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-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; }
.empty-row { text-align: center; color: var(--text-secondary); padding: 20px 0; }
.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; }
/* 头像样式 */
.avatar-upload {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.avatar-preview {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--border-color);
}
.avatar-upload input[type="file"] {
flex: 1;
padding: 6px;
}
.current-avatar {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: 14px;
}
/* 裁剪模态框 */
.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-content .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-content .modal-header h3 {
margin: 0;
}
.modal-content .modal-header .close-btn {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: var(--text-secondary);
}
.modal-content .modal-body img {
max-width: 100%;
display: block;
}
.modal-content .modal-footer {
margin-top: 16px;
display: flex;
gap: 12px;
justify-content: flex-end;
}
@media (max-width: 600px) {
.card { padding: 16px; }
.form-row { flex-direction: column; }
.actions { flex-wrap: wrap; }
.modal-content { width: 95%; padding: 16px; }
}
</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="" id="personForm" enctype="multipart/form-data">
<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; ?>
<!-- 隐藏字段存放裁剪后的 base64 -->
<input type="hidden" name="avatar_base64" id="avatar_base64" value="">
<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="如:张三" required>
</div>
</div>
<div class="form-group">
<label>主题颜色</label>
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<input type="color" name="color" id="colorPicker" value="<?= $edit_data ? h($edit_data['color']) : '#888888' ?>" style="width:50px;height:40px;padding:0;border:none;cursor:pointer;">
<span style="font-size:13px;color:var(--text-secondary);">点击色块选择,或使用拾色器</span>
</div>
<div class="preset-grid">
<?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 class="form-group">
<label>头像</label>
<div class="avatar-upload">
<?php if ($edit_data && $edit_data['avatar']): ?>
<img src="/<?= $edit_data['avatar'] ?>" class="avatar-preview" id="avatarPreview">
<?php else: ?>
<img src="<?= getAvatarUrl(null, $edit_data['name'] ?? '') ?>" class="avatar-preview" id="avatarPreview">
<?php endif; ?>
<input type="file" name="avatar_upload" id="avatarUpload" accept="image/*">
<span style="font-size:13px;color:var(--text-secondary);">点击选择图片,裁剪后生效</span>
</div>
<?php if ($edit_data && $edit_data['avatar']): ?>
<div class="current-avatar">
当前头像:<img src="/<?= $edit_data['avatar'] ?>" style="width:30px;height:30px;border-radius:50%;vertical-align:middle;">
<a href="javascript:void(0)" onclick="removeAvatar()" style="color:var(--danger, #e74c3c);font-size:13px;">删除</a>
</div>
<?php endif; ?>
</div>
<div style="margin-top:8px;">
<button type="submit" class="btn btn-primary">💾 保存</button>
<?php if ($edit_data): ?><a href="persons.php" class="btn btn-outline">取消编辑</a><?php endif; ?>
</div>
</form>
</div>
<!-- 列表 -->
<div class="card">
<h2>👥 观影人列表</h2>
<p class="sub">共 <?= count($persons) ?> 人</p>
<div class="table-wrap">
<?php if (count($persons) > 0): ?>
<table>
<thead><tr><th>头像</th><th>姓名</th><th>颜色</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($persons as $p): ?>
<tr>
<td>
<img src="<?= getAvatarUrl($p['avatar'], $p['name']) ?>" style="width:30px;height:30px;border-radius:50%;object-fit:cover;vertical-align:middle;">
</td>
<td>
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:<?= h($p['color'] ?: '#888888') ?>;vertical-align:middle;margin-right:6px;"></span>
<?= h($p['name']) ?>
</td>
<td><span style="background:<?= h($p['color'] ?: '#888888') ?>;padding:2px 10px;border-radius:4px;color:#fff;font-size:12px;"><?= h($p['color'] ?: '#888888') ?></span></td>
<td>
<div class="actions">
<a href="?action=edit&id=<?= $p['id'] ?>" class="btn btn-outline btn-sm">✏️ 编辑</a>
<a href="?action=delete&id=<?= $p['id'] ?>" class="btn btn-danger btn-sm" onclick="return confirm('确定删除「<?= h($p['name']) ?>」吗?')">🗑️ 删除</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="empty-row">暂无观影人,请添加</div>
<?php endif; ?>
</div>
</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>
<!-- Cropper.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
<script>
let cropper = null;
let currentFile = null;
// ---------- 颜色 ----------
function setColor(color) {
document.getElementById('colorPicker').value = color;
document.querySelectorAll('.preset-color').forEach(el => el.classList.remove('active'));
document.querySelector(`.preset-color[data-color="${color}"]`).classList.add('active');
}
document.addEventListener('DOMContentLoaded', function() {
var currentColor = document.getElementById('colorPicker').value;
if (currentColor) {
var preset = document.querySelector(`.preset-color[data-color="${currentColor}"]`);
if (preset) preset.classList.add('active');
}
});
// ---------- 头像裁剪 ----------
document.getElementById('avatarUpload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
// 检查文件类型
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
this.value = '';
return;
}
currentFile = file;
const reader = new FileReader();
reader.onload = function(ev) {
const img = document.getElementById('cropImage');
img.src = ev.target.result;
document.getElementById('cropModal').classList.add('active');
// 初始化 Cropper
if (cropper) cropper.destroy();
cropper = new Cropper(img, {
aspectRatio: 1,
viewMode: 1,
dragMode: 'move',
autoCropArea: 0.8,
responsive: true,
});
};
reader.readAsDataURL(file);
});
function closeCrop() {
document.getElementById('cropModal').classList.remove('active');
if (cropper) {
cropper.destroy();
cropper = null;
}
document.getElementById('avatarUpload').value = '';
}
function cropSave() {
if (!cropper) return;
// 获取裁剪后的 canvas
const canvas = cropper.getCroppedCanvas({
width: 200,
height: 200,
imageSmoothingQuality: 'high',
});
if (!canvas) return;
// 转换为 base64
const base64 = canvas.toDataURL('image/jpeg', 0.9);
// 更新预览
document.getElementById('avatarPreview').src = base64;
// 存入隐藏字段
document.getElementById('avatar_base64').value = base64;
closeCrop();
}
// 删除头像(仅编辑时可用)
function removeAvatar() {
if (confirm('确定删除头像吗?')) {
// 清空预览为默认
var name = document.querySelector('input[name="name"]').value || '';
document.getElementById('avatarPreview').src = '<?= getAvatarUrl(null, '') ?>';
document.getElementById('avatar_base64').value = '';
// 删除旧头像文件由后端处理(保存时如果 base64 为空则保留旧头像,但我们通过隐藏字段为空表示删除)
// 这里我们设置一个标记,但需要后端处理:如果 avatar_base64 为空且没有上传新文件,则删除头像。
// 我们可以在后端增加逻辑:如果 avatar_base64 为空且没有上传文件,则保留原头像;但用户想删除,我们可加一个隐藏的删除标记。
// 简便做法:提交时如果 avatar_base64 为空,且上传文件也为空,则不修改头像。
// 但用户点击删除,我们希望删除。所以增加一个隐藏字段 delete_avatar=1
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'delete_avatar';
input.value = '1';
document.getElementById('personForm').appendChild(input);
}
}
// 在表单提交前,如果没有 base64 且没有上传文件,但 delete_avatar 存在,则后端处理删除
// 后端逻辑:如果 post 中有 delete_avatar,则 avatar_path = null
// 在 persons.php 的编辑中添加:
// if (isset($_POST['delete_avatar'])) { $avatar_path = null; 删除旧文件 }
// 我会在后端代码中添加此逻辑,但由于代码已生成,我在这里说明。
// 另外,如果用户上传了文件但未裁剪直接取消,则不清除文件输入,但不会触发裁剪,所以正常。
</script>
</body>
</html>