Files
MovieLog_Server/stats.php
T
laoyang d7c6ac2559 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:16:25 +08:00

318 lines
12 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
// ============================================
// 文件: stats.php (统计仪表板,含观影人头像)
// ============================================
require_once 'config.php';
checkLogin();
$admin = getCurrentAdmin();
// 获取统计数据
$overview = getStatsOverview();
$cinema_stats = statsByCinema();
$person_stats = statsByPerson();
$month_stats = statsByMonth(12);
$year_stats = statsByYear();
// 计算最大值
$max_cinema = !empty($cinema_stats) ? max(array_column($cinema_stats, 'count')) : 1;
$max_person = !empty($person_stats) ? max(array_column($person_stats, 'count')) : 1;
$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">
<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>
.stats-container {
max-width: 1100px;
margin: 0 auto;
padding: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 28px;
}
.stat-card {
background: var(--card-bg);
border-radius: 12px;
padding: 18px 20px;
box-shadow: var(--shadow);
text-align: center;
border: 1px solid var(--border-color);
}
.stat-card .number {
font-size: 32px;
font-weight: 700;
color: var(--primary);
line-height: 1.2;
}
.stat-card .label {
font-size: 14px;
color: var(--text-secondary);
margin-top: 4px;
}
.stat-card .sub {
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
}
.chart-section {
background: var(--card-bg);
border-radius: 12px;
padding: 20px 24px;
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
margin-bottom: 24px;
}
.chart-section h3 {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.chart-section .subtitle {
font-size: 13px;
color: var(--text-secondary);
margin-left: 8px;
font-weight: 400;
}
.bar-chart {
display: flex;
flex-direction: column;
gap: 8px;
}
.bar-item {
display: flex;
align-items: center;
gap: 12px;
}
.bar-item .label {
width: 140px;
font-size: 14px;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
}
.bar-item .label .avatar {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.bar-item .label .color-dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.bar-item .bar-track {
flex: 1;
height: 22px;
background: var(--bg-secondary);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.bar-item .bar-fill {
height: 100%;
border-radius: 6px;
transition: width 0.6s ease;
background: var(--primary);
}
.bar-item .count {
width: 40px;
text-align: right;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
flex-shrink: 0;
}
.no-data {
color: var(--text-secondary);
font-size: 14px;
padding: 12px 0;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.two-col {
grid-template-columns: 1fr;
}
.bar-item .label {
width: 100px;
font-size: 13px;
}
.stat-card .number {
font-size: 26px;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr 1fr;
}
.bar-item {
gap: 6px;
}
.bar-item .label {
width: 70px;
font-size: 12px;
}
.bar-item .count {
width: 30px;
font-size: 12px;
}
}
</style>
</head>
<body>
<?php include 'nav.php'; ?>
<main class="main-content">
<div class="stats-container">
<h2 style="font-size: 26px; margin-bottom: 8px;">📊 统计仪表板</h2>
<p style="color: var(--text-secondary); margin-bottom: 24px;">综合观影数据概览</p>
<!-- 总览卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="number"><?= $overview['total_movies'] ?></div>
<div class="label">🎬 电影总数</div>
</div>
<div class="stat-card">
<div class="number"><?= $overview['total_cinemas'] ?></div>
<div class="label">🏛️ 影院数</div>
</div>
<div class="stat-card">
<div class="number"><?= $overview['total_persons'] ?></div>
<div class="label">👥 观影人数</div>
</div>
<div class="stat-card">
<div class="number"><?= $overview['total_viewership'] ?></div>
<div class="label">🎫 总观影人次</div>
</div>
<?php if ($overview['first_date'] && $overview['last_date']): ?>
<div class="stat-card" style="grid-column: span 2;">
<div class="label">📅 观影跨度</div>
<div style="font-size: 16px; font-weight: 500; margin-top: 4px;">
<?= date('Y年m月d日', strtotime($overview['first_date'])) ?> <?= date('Y年m月d日', strtotime($overview['last_date'])) ?>
</div>
</div>
<?php endif; ?>
</div>
<!-- 按影院统计 -->
<div class="chart-section">
<h3>🏛️ 按影院统计 <span class="subtitle">电影数量</span></h3>
<?php if (!empty($cinema_stats)): ?>
<div class="bar-chart">
<?php foreach ($cinema_stats as $item): ?>
<div class="bar-item">
<span class="label">
<span class="color-dot" style="background: <?= h($item['color'] ?: '#888888') ?>;"></span>
<?= h($item['name']) ?>
</span>
<div class="bar-track">
<div class="bar-fill" style="width: <?= ($max_cinema > 0) ? round($item['count'] / $max_cinema * 100) : 0 ?>%; background: <?= h($item['color'] ?: '#888888') ?>;"></div>
</div>
<span class="count"><?= $item['count'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="no-data">暂无影院数据</div>
<?php endif; ?>
</div>
<!-- 按观影人统计 + 按年统计(两列) -->
<div class="two-col">
<!-- 按观影人统计(带头像) -->
<div class="chart-section">
<h3>👥 按观影人统计 <span class="subtitle">参与电影数</span></h3>
<?php if (!empty($person_stats)): ?>
<div class="bar-chart">
<?php foreach ($person_stats as $item): ?>
<div class="bar-item">
<span class="label">
<img src="<?= getAvatarUrl($item['avatar'] ?? null, $item['name']) ?>" class="avatar" alt="<?= h($item['name']) ?>">
<span class="color-dot" style="background: <?= h($item['color'] ?: '#888888') ?>;"></span>
<?= h($item['name']) ?>
</span>
<div class="bar-track">
<div class="bar-fill" style="width: <?= ($max_person > 0) ? round($item['count'] / $max_person * 100) : 0 ?>%; background: <?= h($item['color'] ?: '#888888') ?>;"></div>
</div>
<span class="count"><?= $item['count'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="no-data">暂无观影人数据</div>
<?php endif; ?>
</div>
<!-- 按年统计 -->
<div class="chart-section">
<h3>📆 按年统计 <span class="subtitle">观影数量</span></h3>
<?php if (!empty($year_stats)): ?>
<div class="bar-chart">
<?php foreach ($year_stats as $item): ?>
<div class="bar-item">
<span class="label"><?= h($item['year']) ?></span>
<div class="bar-track">
<div class="bar-fill" style="width: <?= ($max_year > 0) ? round($item['count'] / $max_year * 100) : 0 ?>%; background: #3498db;"></div>
</div>
<span class="count"><?= $item['count'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="no-data">暂无年份数据</div>
<?php endif; ?>
</div>
</div>
<!-- 按月趋势 -->
<div class="chart-section">
<h3>📈 月度趋势 <span class="subtitle">最近12个月观影数量</span></h3>
<?php if (!empty($month_stats)): ?>
<div class="bar-chart">
<?php foreach ($month_stats as $item): ?>
<div class="bar-item">
<span class="label"><?= h($item['month']) ?></span>
<div class="bar-track">
<div class="bar-fill" style="width: <?= ($max_month > 0) ? round($item['count'] / $max_month * 100) : 0 ?>%; background: #2ecc71;"></div>
</div>
<span class="count"><?= $item['count'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="no-data">暂无月度数据</div>
<?php endif; ?>
</div>
</div>
</main>
<script src="js/main.js"></script>
</body>
</html>