v9: 观影对齐 MovieLog —— 瀑布流/影院管理/评价评论/起止时间

- events 起止时间精确到秒(starts_at DATE→DATETIME + ends_at),_datetime.php 复用
- 影院独立成表(cinemas,地方/名称/地址/主题色 + 50 色板),movie_details 归一为 cinema_id
- 评价模型 MovieLog 同款:movie_reviews 每账号每片一条(1-10 分+正文+感受 key),
  movie_review_comments 两级平铺评论(回复的回复规范化到顶层、跨评价回复拦截)
- 感受目录迁到 MovieReview::CATALOG(12 种,Emoji 11.0 老码位)
- 观影列表改瀑布流(CSS grid 海报墙),详情页五大块 + 灯箱 + 评价区
- 升级路径:v8 老数据自动归一(影院三段按 (family,place,name) 去重建行、
  评价回填、中文感受反查英文 key),回填全部幂等;老库补 cinema_id 列+索引+外键
- 修三 bug:建表顺序(cinemas 必须在 movie_details 前,否则全新安装 1215)、
  layout 导航用键名当路由(影院链接指向 r=cinemas)、
  _datetime.php 分钟循环变量 $m 覆盖宿主模板的观影记录(编辑页简介/备注回填被清空)
This commit is contained in:
2026-10-01 00:12:04 +08:00
parent ff353b1eee
commit 746f02581e
13 changed files with 2001 additions and 356 deletions
+432 -77
View File
@@ -1,25 +1,126 @@
<?php
/**
* 观影详情页。
* 观影详情页(v9,展示方式照搬 MovieLog 的 detail.php)。
*
* 海报 / 票根 / 照片一律走 media_url() 代理(全站唯一出口,见 helpers.php);
* 感受标签是迁移时已翻成中文的 JSON 数组,这里只展示不加工。
* 访客(只读)看不到编辑/删除/传照片那些表单 —— 与名册同一口径。
* 五大块:英雄区(左海报右信息)→ 一起看的人 → 电影院信息 → 影像记录(灯箱)→ 评价与评论。
* 海报/票根/照片一律走 media_url() 代理;感受标签只存 key,emoji 与文案查 CATALOG。
* 访客(只读)看不到编辑/删除/传照片/评价表单 —— 与名册同一口径。
* 评价人是「登录账号背后的人」,与同行观影人无关(MovieLog 同款规则)。
*/
$canWrite = \App\Auth::canWrite();
$feel = [];
if (!empty($m['feelings'])) {
$feel = json_decode((string)$m['feelings'], true);
if (!is_array($feel)) {
$feel = [];
}
}
$hasPoster = !empty($m['poster_key']);
$hasTicket = !empty($m['ticket_key']);
$watchTxt = \App\Movie::formatWatch($m['starts_at'], $m['ends_at'] ?? null);
$fmtNum = static function ($v): string {
if ($v === null) {
return '';
}
return rtrim(rtrim(number_format((float)$v, 1, '.', ''), '0'), '.');
};
/** 头像或首字占位(persons.avatar 是存储相对键) */
$avatarHtml = static function (?string $avatar, string $name, string $cls = '') {
if ($avatar !== null && $avatar !== '') {
return '<img class="rv-avatar ' . e($cls) . '" src="' . e(media_url($avatar)) . '" alt="">';
}
return '<span class="rv-avatar rv-avatar-text ' . e($cls) . '">' . e(mb_substr($name, 0, 1, 'UTF-8')) . '</span>';
};
/** 可勾选的感受 chips(原生 checkbox,外观在 .feel-inner 上) */
$feelPick = static function (array $checked = []) use ($feelCat) {
ob_start(); ?>
<span class="feel-chips">
<?php foreach ($feelCat as $k => $f): ?>
<label class="feel-chip feel-pick" title="<?= e($f['label']) ?>">
<input type="checkbox" name="feelings[]" value="<?= e($k) ?>"<?= in_array($k, $checked, true) ? ' checked' : '' ?>>
<span class="feel-inner"><span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= e($f['label']) ?></span></span>
</label>
<?php endforeach; ?>
</span>
<?php return ob_get_clean();
};
/** 只读的感受标签 */
$feelTags = static function (array $keys) use ($feelCat) {
if (!$keys) {
return '';
}
ob_start(); ?>
<span class="rv-feelings">
<?php foreach ($keys as $k): ?>
<?php if (!isset($feelCat[$k])) { continue; } $f = $feelCat[$k]; ?>
<span class="feel-chip is-static" title="观后感受:<?= e($f['label']) ?>">
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= e($f['label']) ?></span>
</span>
<?php endforeach; ?>
</span>
<?php return ob_get_clean();
};
/** 我的评价表单(打分 + 正文 + 感受),details 按需展开;写评价与修改共用这一份 */
$rvForm = static function (string $summary, bool $open) use ($m, $myReview, $feelPick, $rvError) {
ob_start(); ?>
<details class="rv-composer"<?= $open ? ' open' : '' ?>>
<summary class="rv-open-btn"><?= e($summary) ?></summary>
<form method="post" action="<?= e(url('movies/review_save')) ?>" class="rv-form" id="reviewForm">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<div class="rv-rating-row">
<span class="rv-rating-label">我的评分</span>
<span class="rv-scale" role="radiogroup" aria-label="评分,1 分到 10 分">
<?php $myRating = (int)($myReview['rating'] ?? 0); ?>
<?php for ($i = 1; $i <= 10; $i++): ?>
<label class="rv-num" title="<?= $i ?> 分">
<input type="radio" name="rating" value="<?= $i ?>"<?= $myRating === $i ? ' checked' : '' ?>>
<span><?= $i ?></span>
</label>
<?php endfor; ?>
</span>
<span class="rv-word<?= $myRating > 0 ? ' is-set' : '' ?>" id="rvWord">
<?= $myRating > 0 ? $myRating . ' 分 · ' . e(\App\MovieReview::ratingWord($myRating)) : '未评分' ?>
</span>
</div>
<textarea name="content" class="rv-textarea" maxlength="<?= (int)\App\MovieReview::CONTENT_MAX ?>" required
placeholder="这部电影怎么样?剧情、表演、画面、音效……随便聊聊"><?= e((string)($myReview['content'] ?? '')) ?></textarea>
<div class="rv-feeling-pick">
<span class="rv-pick-label">我看完的感受(可多选)</span>
<?= $feelPick((array)($myReview['feelings'] ?? [])) ?>
</div>
<div class="rv-form-foot">
<span class="rv-hint">每个账号对每部电影只有一条评价,再次提交即为修改。</span>
<button type="submit" class="btn btn-sm"><?= $myReview ? '保存修改' : '发表评价' ?></button>
</div>
</form>
</details>
<?php return ob_get_clean();
};
/** 评论 / 回复表单(parent_id=0 是对评价发评论;>0 是回复某条评论) */
$commentBox = static function (int $reviewId, int $parentId, string $summary) use ($m, $feelPick) {
ob_start(); ?>
<details class="rv-composer rv-inline">
<summary class="rv-act-btn"><?= e($summary) ?></summary>
<form method="post" action="<?= e(url('movies/comment_add')) ?>" class="rv-form rv-form-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<input type="hidden" name="review_id" value="<?= $reviewId ?>">
<input type="hidden" name="parent_id" value="<?= $parentId ?>">
<textarea name="content" class="rv-textarea rv-textarea-sm" maxlength="<?= (int)\App\MovieReview::COMMENT_MAX ?>" required
placeholder="<?= $parentId > 0 ? '回复这一条…' : '说说你的看法…' ?>"></textarea>
<div class="rv-feeling-pick">
<span class="rv-pick-label">观后感受(可多选)</span>
<?= $feelPick() ?>
</div>
<div class="rv-form-foot">
<button type="submit" class="btn btn-ghost btn-sm"><?= $parentId > 0 ? '发表回复' : '发表评论' ?></button>
</div>
</form>
</details>
<?php return ob_get_clean();
};
?>
<div class="page-head">
<h1><?= e($m['title']) ?></h1>
<span class="sub"><?= e((string)($m['starts_at'] ?? '日期待补')) ?></span>
<span class="sub">观影记录</span>
<div class="actions">
<?php if ($canWrite): ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies/edit', ['id' => (int)$m['id']])) ?>">编辑</a>
@@ -30,80 +131,90 @@ $hasTicket = !empty($m['ticket_key']);
<button type="submit" class="btn btn-danger btn-sm">删除</button>
</form>
<?php endif; ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回</a>
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回列表</a>
</div>
</div>
<div class="movie-detail">
<div class="m-poster-col">
<!-- 主信息:英雄区 -->
<div class="detail-hero">
<div class="detail-poster<?= $hasPoster ? ' lb-openable' : '' ?>"
<?php if ($hasPoster): ?>data-lb-index="0" role="button" tabindex="0" aria-label="放大查看海报" title="点击查看大图"<?php endif; ?>>
<?php if ($hasPoster): ?>
<a href="<?= e(media_url($m['poster_key'])) ?>" target="_blank" rel="noopener">
<img class="m-poster-lg" src="<?= e(media_url($m['poster_key'])) ?>" alt="海报">
</a>
<img src="<?= e(media_url($m['poster_key'])) ?>" alt="海报">
<?php else: ?>
<span class="m-poster-lg m-poster-empty" aria-hidden="true">影</span>
<?php endif; ?>
<?php if ($hasTicket): ?>
<a href="<?= e(media_url($m['ticket_key'])) ?>" target="_blank" rel="noopener">
<img class="m-ticket" src="<?= e(media_url($m['ticket_key'])) ?>" alt="票根" loading="lazy">
</a>
<span class="placeholder-poster">
<span class="ph-icon" aria-hidden="true">影</span>
<span class="placeholder-title"><?= e($m['title']) ?></span>
</span>
<?php endif; ?>
</div>
<div class="card card-pad m-info">
<?php if ($m['rating'] !== null || $feel): ?>
<div class="m-score">
<?php if ($m['rating'] !== null): ?>
<span class="rating rating-lg"><?= e(rtrim(rtrim(number_format((float)$m['rating'], 1, '.', ''), '0'), '.')) ?></span>
<?php endif; ?>
<?php foreach ($feel as $f): ?>
<span class="badge badge-feel"><?= e($f) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<dl class="kv">
<div class="kv-row"><dt>观影日期</dt><dd><?= e((string)($m['starts_at'] ?? '日期待补')) ?></dd></div>
<?php if (!empty($m['cinema_place']) || !empty($m['cinema_name']) || !empty($m['cinema_address'])): ?>
<div class="kv-row"><dt>影院</dt><dd>
<?= e(trim(implode(' ', array_filter([
(string)($m['cinema_place'] ?? ''),
(string)($m['cinema_name'] ?? ''),
(string)($m['cinema_address'] ?? ''),
], static fn($s) => $s !== '')))) ?>
</dd></div>
<div class="detail-info">
<h1><?= e($m['title']) ?></h1>
<div class="meta-row">
<span>📅 <?= e($watchTxt) ?></span>
<?php if (!empty($m['cinema_name'])): ?>
<span style="color: <?= e((string)($m['cinema_color'] ?: '#888888')) ?>;">🏛️ <?= e($m['cinema_name']) ?></span>
<?php endif; ?>
<?php if (!empty($m['location'])): ?>
<div class="kv-row"><dt>地点</dt><dd><?= e($m['location']) ?></dd></div>
<?php endif; ?>
<div class="kv-row"><dt>同行的人</dt><dd>
<?php if ($m['attendees']): ?>
<?php foreach ($m['attendees'] as $i => $a): ?><?= $i > 0 ? '、' : '' ?><a href="<?= e(url('people/show', ['id' => (int)$a['id']])) ?>"><?= e($a['name']) ?></a><?php endforeach; ?>
<?php else: ?>一个人看的<?php endif; ?>
</dd></div>
</dl>
<span>👥 观影人 <?= count($m['attendees']) ?> 人</span>
</div>
<?php if (!empty($m['info'])): ?>
<div class="m-block"><h3>简介</h3><p><?= nl2br(e($m['info'])) ?></p></div>
<?php endif; ?>
<?php if (!empty($m['review'])): ?>
<div class="m-block"><h3>观后感</h3><p><?= nl2br(e($m['review'])) ?></p></div>
<div class="info-text"><?= nl2br(e($m['info'])) ?></div>
<?php endif; ?>
<?php if (!empty($m['note'])): ?>
<div class="m-block"><h3>备注</h3><p><?= nl2br(e($m['note'])) ?></p></div>
<div class="remark-text">📝 <?= nl2br(e($m['note'])) ?></div>
<?php endif; ?>
</div>
</div>
<?php if ($m['photos']): ?>
<section class="section">
<h2 class="section-title">现场照片 <span class="hint"><?= count($m['photos']) ?> 张</span></h2>
<div class="photo-grid">
<?php foreach ($m['photos'] as $ph): ?>
<div class="photo-cell">
<a href="<?= e(media_url($ph['key'])) ?>" target="_blank" rel="noopener">
<img src="<?= e(media_url($ph['key'])) ?>" alt="" loading="lazy">
</a>
<!-- 一起看的人 -->
<div class="detail-section">
<h3>一起看的人 <span class="badge"><?= count($m['attendees']) ?> 人</span></h3>
<?php if ($m['attendees']): ?>
<div class="viewers-list">
<?php foreach ($m['attendees'] as $a): ?>
<a class="viewer-tag" href="<?= e(url('people/show', ['id' => (int)$a['id']])) ?>">
<?= $avatarHtml($a['avatar'], (string)$a['name']) ?>
<?= e($a['name']) ?>
</a>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="empty-hint">一个人看的</p>
<?php endif; ?>
</div>
<!-- 电影院信息 -->
<div class="detail-section">
<h3>电影院信息</h3>
<?php if (!empty($m['cinema_name'])): ?>
<div class="cinema-info">
<span class="label">所在地方</span>
<span class="value"><?= e((string)($m['cinema_place'] ?? '-')) ?></span>
<span class="label">影院名称</span>
<span class="value" style="color: <?= e((string)($m['cinema_color'] ?: '#888888')) ?>;"><?= e($m['cinema_name']) ?></span>
<span class="label">详细地址</span>
<span class="value"><?= e((string)($m['cinema_address'] ?? '-')) ?></span>
</div>
<?php else: ?>
<p class="empty-hint">未记录电影院信息</p>
<?php endif; ?>
</div>
<!-- 影像记录(票根 + 现场照片,点开灯箱) -->
<?php if ($hasTicket || $m['photos']): ?>
<div class="detail-section">
<h3>影像记录 <span class="badge"><?= count($m['photos']) + ($hasTicket ? 1 : 0) ?> 张</span></h3>
<div class="gallery-grid">
<?php if ($hasTicket): ?>
<div class="gallery-item lb-openable" data-lb-index="1" role="button" tabindex="0" aria-label="放大查看票根" title="点击查看大图">
<img src="<?= e(media_url($m['ticket_key'])) ?>" alt="票根" loading="lazy">
<span class="label-badge">票根</span>
</div>
<?php endif; ?>
<?php foreach ($m['photos'] as $i => $ph): ?>
<div class="gallery-item lb-openable" data-lb-index="<?= $i + 2 ?>" role="button" tabindex="0" aria-label="放大查看照片" title="点击查看大图">
<img src="<?= e(media_url((string)$ph['key'])) ?>" alt="" loading="lazy">
<span class="label-badge">照片</span>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('movies/photo_del')) ?>"
onsubmit="return confirm('删除这张照片?')">
@@ -116,12 +227,11 @@ $hasTicket = !empty($m['ticket_key']);
</div>
<?php endforeach; ?>
</div>
</section>
</div>
<?php endif; ?>
<?php if ($canWrite): ?>
<section class="section">
<form method="post" enctype="multipart/form-data" action="<?= e(url('movies/photo_add')) ?>" class="card card-pad photo-add">
<div class="detail-section">
<form method="post" enctype="multipart/form-data" action="<?= e(url('movies/photo_add')) ?>" class="photo-add">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<label for="photo">补一张现场照片</label>
@@ -130,5 +240,250 @@ $hasTicket = !empty($m['ticket_key']);
<button type="submit" class="btn btn-ghost btn-sm">上传</button>
</div>
</form>
</section>
</div>
<?php endif; ?>
<!-- 评价与评论 -->
<div class="detail-section" id="reviews">
<h3>评价与评论
<span class="badge">
<?php if ($rvStats['count'] > 0): ?>
<?= (int)$rvStats['count'] ?> 条评价 · 均分 <?= e($fmtNum($rvStats['avg'])) ?>
<?php else: ?>
还没有评价
<?php endif; ?>
</span>
</h3>
<?php if ($canWrite && !$myReview): ?>
<?= $rvForm('写评价', $rvError !== '') ?>
<?php endif; ?>
<?php if (!$reviews): ?>
<p class="empty-hint">还没有评价,等你来评一个。</p>
<?php else: ?>
<div class="rv-list">
<?php foreach ($reviews as $rv): ?>
<?php
$rvMine = ((int)$rv['person_id'] === $meId);
$rvName = (string)($rv['person_name'] ?? '已注销');
$rvRate = (int)$rv['rating'];
?>
<div class="rv-item">
<div class="rv-head">
<?= $avatarHtml($rv['person_avatar'] ?? null, $rvName) ?>
<span class="rv-who">
<span class="rv-name"><?= e($rvName) ?></span>
<?php if ($rvMine): ?><span class="rv-mine">我</span><?php endif; ?>
<span class="rv-time"><?= e(substr((string)$rv['created_at'], 0, 16)) ?></span>
</span>
<span class="rv-score" title="<?= e(\App\MovieReview::ratingWord($rvRate)) ?>"><?= $rvRate ?><span class="rv-score-max">/10</span></span>
<?php if ($rvMine && $canWrite): ?>
<form method="post" action="<?= e(url('movies/review_del')) ?>" class="rv-del"
onsubmit="return confirm('删除我的这条评价?下面的评论也会一起删除,不可恢复。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<input type="hidden" name="review_id" value="<?= (int)$rv['id'] ?>">
<button type="submit" class="rv-del-btn" title="删除我的评价">×</button>
</form>
<?php endif; ?>
</div>
<div class="rv-content"><?= nl2br(e($rv['content'])) ?></div>
<?= $feelTags($rv['feelings']) ?>
<?php if (!empty($rv['updated_at']) && $rv['updated_at'] !== $rv['created_at']): ?>
<div class="rv-updated">已编辑 <?= e(substr((string)$rv['updated_at'], 0, 16)) ?></div>
<?php endif; ?>
<?php if (!empty($rv['comments'])): ?>
<div class="rv-comments">
<?php foreach ($rv['comments'] as $thread): ?>
<?php
$c = $thread['comment'];
$cMine = ((int)$c['person_id'] === $meId);
$cName = (string)($c['person_name'] ?? '已注销');
?>
<div class="rv-thread">
<div class="rv-comment">
<?= $avatarHtml($c['person_avatar'] ?? null, $cName, 'rv-avatar-sm') ?>
<span class="rv-c-body">
<span class="rv-c-head">
<span class="rv-name"><?= e($cName) ?></span>
<?php if ($cMine): ?><span class="rv-mine">我</span><?php endif; ?>
<span class="rv-time"><?= e(substr((string)$c['created_at'], 0, 16)) ?></span>
<?php if ($cMine && $canWrite): ?>
<form method="post" action="<?= e(url('movies/comment_del')) ?>" class="rv-del"
onsubmit="return confirm('删除我的这条评论?它下面的回复也会一起删除。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<input type="hidden" name="comment_id" value="<?= (int)$c['id'] ?>">
<button type="submit" class="rv-del-btn" title="删除我的评论">×</button>
</form>
<?php endif; ?>
</span>
<?= $feelTags($c['feelings']) ?>
<span class="rv-c-text"><?= nl2br(e($c['content'])) ?></span>
<span class="rv-c-acts"><?= $canWrite ? $commentBox((int)$rv['id'], (int)$c['id'], '回复') : '' ?></span>
</span>
</div>
<?php foreach ($thread['replies'] as $rep): ?>
<?php
$rMine = ((int)$rep['person_id'] === $meId);
$rName = (string)($rep['person_name'] ?? '已注销');
?>
<div class="rv-comment rv-comment-reply">
<?= $avatarHtml($rep['person_avatar'] ?? null, $rName, 'rv-avatar-sm') ?>
<span class="rv-c-body">
<span class="rv-c-head">
<span class="rv-name"><?= e($rName) ?></span>
<?php if ($rMine): ?><span class="rv-mine">我</span><?php endif; ?>
<span class="rv-time"><?= e(substr((string)$rep['created_at'], 0, 16)) ?></span>
<?php if ($rMine && $canWrite): ?>
<form method="post" action="<?= e(url('movies/comment_del')) ?>" class="rv-del"
onsubmit="return confirm('删除我的这条回复?')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<input type="hidden" name="comment_id" value="<?= (int)$rep['id'] ?>">
<button type="submit" class="rv-del-btn" title="删除我的回复">×</button>
</form>
<?php endif; ?>
</span>
<?php if (!empty($rep['reply_to_name'])): ?>
<span class="rv-at">回复 <span>@<?= e($rep['reply_to_name']) ?></span></span>
<?php endif; ?>
<?= $feelTags($rep['feelings']) ?>
<span class="rv-c-text"><?= nl2br(e($rep['content'])) ?></span>
<span class="rv-c-acts"><?= $canWrite ? $commentBox((int)$rv['id'], (int)$rep['id'], '回复') : '' ?></span>
</span>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($canWrite): ?>
<div class="rv-actions">
<?= $commentBox((int)$rv['id'], 0, '评论') ?>
<?php if ($rvMine): ?>
<?= $rvForm('修改我的评价', $rvError !== '') ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($hasPoster || $hasTicket || $m['photos']): ?>
<!-- 图片灯箱:点任意一张图在当前页弹出,可左右翻看 / Esc 关闭(MovieLog 同款) -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-hidden="true" aria-label="图片查看器">
<button type="button" class="lb-btn lb-close" id="lbClose" aria-label="关闭">✕</button>
<button type="button" class="lb-btn lb-nav lb-prev" id="lbPrev" aria-label="上一张">‹</button>
<button type="button" class="lb-btn lb-nav lb-next" id="lbNext" aria-label="下一张">›</button>
<div class="lb-stage" id="lbStage"><img class="lb-img" id="lbImg" alt=""></div>
<div class="lb-bar">
<span class="lb-caption" id="lbCaption"></span>
<span class="lb-counter" id="lbCounter"></span>
</div>
</div>
<script>
(function () {
var items = [];
<?php if ($hasPoster): ?>
items.push({url: <?= json_encode(media_url((string)$m['poster_key']), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, cap: <?= json_encode('海报 · ' . $m['title'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>});
<?php endif; ?>
<?php if ($hasTicket): ?>
items.push({url: <?= json_encode(media_url((string)$m['ticket_key']), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, cap: <?= json_encode('票根 · ' . $m['title'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>});
<?php endif; ?>
<?php foreach ($m['photos'] as $ph): ?>
items.push({url: <?= json_encode(media_url((string)$ph['key']), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, cap: <?= json_encode('照片 · ' . $m['title'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>});
<?php endforeach; ?>
var box = document.getElementById('lightbox');
var stageEl = document.getElementById('lbStage');
var imgEl = document.getElementById('lbImg');
var capEl = document.getElementById('lbCaption');
var cntEl = document.getElementById('lbCounter');
var closeEl = document.getElementById('lbClose');
var prevEl = document.getElementById('lbPrev');
var nextEl = document.getElementById('lbNext');
if (!box || !items.length) return;
var idx = 0, opener = null;
function render() {
imgEl.src = items[idx].url;
capEl.textContent = items[idx].cap;
cntEl.textContent = (idx + 1) + ' / ' + items.length;
var multi = items.length > 1;
prevEl.hidden = !multi;
nextEl.hidden = !multi;
}
function open(i, trigger) {
idx = Math.max(0, Math.min(items.length - 1, i));
opener = trigger || null;
box.classList.add('open');
box.setAttribute('aria-hidden', 'false');
render();
closeEl.focus();
}
function close() {
box.classList.remove('open');
box.setAttribute('aria-hidden', 'true');
if (opener && opener.focus) opener.focus();
opener = null;
}
function step(d) { idx = (idx + d + items.length) % items.length; render(); }
Array.prototype.forEach.call(document.querySelectorAll('[data-lb-index]'), function (el) {
var i = parseInt(el.getAttribute('data-lb-index'), 10);
if (isNaN(i)) return;
el.addEventListener('click', function (e) { e.preventDefault(); open(i, el); });
el.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(i, el); }
});
});
closeEl.addEventListener('click', close);
prevEl.addEventListener('click', function (e) { e.stopPropagation(); step(-1); });
nextEl.addEventListener('click', function (e) { e.stopPropagation(); step(1); });
box.addEventListener('click', function (e) { if (e.target === box || e.target === stageEl) close(); });
imgEl.addEventListener('click', function (e) { e.stopPropagation(); });
document.addEventListener('keydown', function (e) {
if (!box.classList.contains('open')) return;
if (e.key === 'Escape') { close(); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); step(-1); }
else if (e.key === 'ArrowRight') { e.preventDefault(); step(1); }
});
var tx = 0, ty = 0;
stageEl.addEventListener('touchstart', function (e) {
tx = e.changedTouches[0].clientX;
ty = e.changedTouches[0].clientY;
}, { passive: true });
stageEl.addEventListener('touchend', function (e) {
var dx = e.changedTouches[0].clientX - tx;
var dy = e.changedTouches[0].clientY - ty;
if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy)) step(dx < 0 ? 1 : -1);
}, { passive: true });
})();
</script>
<?php endif; ?>
<?php if ($canWrite): ?>
<!-- 打分实时提示「几分 · 怎么样」;纯增强,radio 没 JS 也能选 -->
<script>
(function () {
var form = document.getElementById('reviewForm');
var word = document.getElementById('rvWord');
if (!form || !word) return;
var WORDS = <?= json_encode(\App\MovieReview::ratingWords(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
function sync() {
var picked = form.querySelector('input[name="rating"]:checked');
if (!picked) { word.textContent = '未评分'; word.classList.remove('is-set'); return; }
var v = parseInt(picked.value, 10);
word.textContent = v + ' 分 · ' + (WORDS[v - 1] || '');
word.classList.add('is-set');
}
Array.prototype.forEach.call(form.querySelectorAll('input[name="rating"]'), function (el) {
el.addEventListener('change', sync);
});
sync();
})();
</script>
<?php endif; ?>