Files
FamilyBook/templates/movies/stats.php
T
laoyang 7544fb9f6a v41 第 41 批:流水线提速 + 12 项界面/交互开发 + DAV 彻底修复
DAV(用户「务必彻底解决」):
- 真根因是 home-set 层级错:CalDav::HOME_PATH 原为 /dav/calendars/、
  CardDav 原为 /carddav/addressbooks/,而日历本/通讯录的 href 在
  /dav/N.ics/ 与 /carddav/N.vcf/ —— home-set 下面一本都没有。
  Thunderbird 按 home-set 前缀认成员,于是「找不到日历本 / 找不到联系人」。
  改为 /dav/ 与 /carddav/(所有既有 URL 一字不变,老配置不作废)。
- 又发现并修掉 Depth:0 违反 RFC 4918:原实现无论 depth 都列出全部子集合,
  且第一条 response 是日历本(不含 current-user-principal)。改为按 depth 分流,
  Depth:0 只回资源自身、Depth:1 才列出子集合。

界面与交互:
- 观影:新建影院弹窗层级修正 + 详细地址;照片左右按钮悬停不再位移;
  弹框底栏改「确定 / 取消」。
- 旅程:新建旅程改弹窗并按观影式排版;详情页四个动作按钮按用户指定顺序与配色;
  编辑与新增行程弹窗化(单容器 + AJAX);行程时间控件改 AntD(精确到分);
  参与人默认带上自己;车次号/车牌号紧跟交通方式词。
- 资源:新建/编辑改弹窗 + 观影式排版 + 新建时即定共享人。
- 纪念日:新建改弹框 + 观影式排版。

验证:
- e2e 1510/0;v41 十二项真机 48/0;DAV 反向验证 8/0;
  部署断言空跑 337/0;模板 div 深度三形态穷举 33 组全配平。
- 线上 DAV 终验(--resolve 真实入口 + 真 DAV 口令)12/0;
  线上确认 Depth:0 恰好 1 个 response。
2026-10-11 10:12:11 +08:00

137 lines
6.5 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
/**
* 观影统计仪表板(v11,从 MovieLog stats.php 迁入)。
*
* 版式对齐家书的暖纸配色(MovieLog 是深色卡片风 —— 只搬功能、不搬皮肤,
* 免得进到观影模块里突然变成另一个站点)。
* 条形图是纯 CSS 宽度百分比,不引任何图表库(线上不依赖 CDN)。
*/
$fmtPct = static function (int $v, int $max): float {
return $max > 0 ? round($v / $max * 100, 2) : 0.0;
};
$maxCinema = 1;
foreach ($byCinema as $r) { $maxCinema = max($maxCinema, (int)$r['cnt']); }
$maxPerson = 1;
foreach ($byPerson as $r) { $maxPerson = max($maxPerson, (int)$r['cnt']); }
$maxMonth = 1;
foreach ($byMonth as $r) { $maxMonth = max($maxMonth, (int)$r['cnt']); }
$maxYear = 1;
foreach ($byYear as $r) { $maxYear = max($maxYear, (int)$r['cnt']); }
$wd = static function (?string $d): string {
return ($d !== null && $d !== '') ? date('Y年n月j日', strtotime($d)) : '—';
};
?>
<div class="page-head">
<h1>观影统计</h1>
<span class="sub">一家人的观影概览</span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回观影</a>
<?php $setCurrent = 'stats'; $setCanWrite = \App\Auth::canWrite(); require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<div class="stats-grid">
<div class="stat-card"><div class="number"><?= (int)$ov['movies'] ?></div><div class="label">电影总数</div></div>
<div class="stat-card"><div class="number"><?= (int)$ov['cinemas'] ?></div><div class="label">影院数</div></div>
<div class="stat-card"><div class="number"><?= (int)$ov['persons'] ?></div><div class="label">观影人数</div></div>
<div class="stat-card"><div class="number"><?= (int)$ov['viewership'] ?></div><div class="label">总观影人次</div></div>
</div>
<?php if ($ov['first_date'] !== null && $ov['last_date'] !== null): ?>
<div class="card card-pad stats-span">
<span class="label">观影跨度</span>
<span class="value"><?= e($wd($ov['first_date'])) ?> ~ <?= e($wd($ov['last_date'])) ?></span>
</div>
<?php endif; ?>
<div class="chart-card">
<h3>按影院统计 <span class="hint">电影数量</span></h3>
<?php if ($byCinema): ?>
<div class="bar-chart">
<?php foreach ($byCinema as $r): $c = (string)($r['color'] ?: '#888888'); ?>
<div class="bar-item">
<span class="bar-label">
<span class="color-dot" style="background: <?= e($c) ?>"></span>
<?= e((string)$r['name']) ?><?php if (trim((string)$r['place']) !== ''): ?><span class="bar-sub"><?= e((string)$r['place']) ?></span><?php endif; ?>
</span>
<span class="bar-track"><span class="bar-fill" style="width: <?= e((string)$fmtPct((int)$r['cnt'], $maxCinema)) ?>%; background: <?= e($c) ?>"></span></span>
<span class="bar-count"><?= (int)$r['cnt'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="empty-hint">还没有影院数据。到「影院管理」加几家影院,记观影时就能选了。</p>
<?php endif; ?>
</div>
<div class="two-col">
<div class="chart-card">
<h3>按观影人统计 <span class="hint">参与场次</span></h3>
<?php /* 🔴 v41(用户 2026-10-10):「每个人不是根据头像,计算出来一个颜色吗?
按观影人统计那里,那个柱状图显示这个人的颜色。」
⇒ 用 `AvatarColor::for()`(全站头像配色**唯一来源**)给柱子上色,
与这个人在名册/头像圈上的颜色**同一套算法**(同名/同头像必同色)。 */ ?>
<?php if ($byPerson): ?>
<div class="bar-chart">
<?php foreach ($byPerson as $r): $pcol = \App\AvatarColor::for($r); ?>
<div class="bar-item">
<span class="bar-label">
<img class="bar-avatar" src="<?= e(avatar_url($r['avatar'] ?? null)) ?>" alt="">
<?= e(person_name($r)) ?>
</span>
<span class="bar-track"><span class="bar-fill" style="width: <?= e((string)$fmtPct((int)$r['cnt'], $maxPerson)) ?>%; background: <?= e($pcol) ?>"></span></span>
<span class="bar-count"><?= (int)$r['cnt'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="empty-hint">还没有观影人数据。</p>
<?php endif; ?>
</div>
<div class="chart-card">
<h3>按年统计 <span class="hint">观影数量</span></h3>
<?php /* 🔴 v41:「按年统计,也用不同的颜色来显示。」→ `MovieStats::yearColor()`,
按年份在色相环上取点(相邻年份色相拉开,且同一年永远同色)。 */ ?>
<?php if ($byYear): ?>
<div class="bar-chart">
<?php foreach ($byYear as $r): ?>
<div class="bar-item">
<span class="bar-label"><?= e((string)$r['y']) ?> 年</span>
<span class="bar-track"><span class="bar-fill" style="width: <?= e((string)$fmtPct((int)$r['cnt'], $maxYear)) ?>%; background: <?= e(\App\MovieStats::yearColor((int)$r['y'])) ?>"></span></span>
<span class="bar-count"><?= (int)$r['cnt'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="empty-hint">还没有年份数据。</p>
<?php endif; ?>
</div>
</div>
<div class="chart-card">
<h3>月度趋势 <span class="hint">最近 12 个月</span></h3>
<?php /* 🔴 v40:与年度报告的「月份分布」**用同一套月配**(`MovieStats::MONTH_COLORS`)——
同一个月在两个页面必须是同一种颜色,否则用户会以为那是两件不同的事。
`$r['ym']` 形如 '2026-10',取月份部分查色。 */ ?>
<?php if ($byMonth): ?>
<div class="bar-chart">
<?php foreach ($byMonth as $r): $ymM = (int)substr((string)$r['ym'], 5, 2); ?>
<div class="bar-item">
<span class="bar-label"><?= e((string)$r['ym']) ?></span>
<span class="bar-track"><span class="bar-fill"
style="width: <?= e((string)$fmtPct((int)$r['cnt'], $maxMonth)) ?>%;
background: <?= e(\App\MovieStats::monthColor($ymM)) ?>"></span></span>
<span class="bar-count"><?= (int)$r['cnt'] ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="empty-hint">还没有月度数据。</p>
<?php endif; ?>
</div>
<div class="btn-row">
<a class="btn btn-ghost" href="<?= e(url('movies/report')) ?>">看年度报告</a>
<a class="btn btn-ghost" href="<?= e(url('movies/search')) ?>">高级搜索</a>
</div>