Files
FamilyBook/templates/movies/show.php
T
laoyang 4060c18f86 fix(kin,movies): 家谱连线重构为单一网格 + 观影设置统一页头
需求三点:
1. 观影模块「⚙ 设置」与「记一场电影」按钮高度不一致 → 统一页头动作区
   四个选择器的 padding/font-size(.btn 与 .btn-sm 混用是根因);并把设置
   下拉推广到观影全部页面(详情/影院/搜索/统计/报告/表单),落在返回按钮旁。
2. 家谱连线「线乱」→ 根因三条:每代各建一个网格(列宽最小值由内容决定,
   各代不一致,窄屏实测 93.5px vs 164/78/78/78px);落点用百分比算而 grid
   间距不参与百分比;连线行与卡片行不是同一套列。
   改法:整棵树只留一个网格,辈分行/卡片行/连线行全是它的行,连线元素本身
   也是网格项(靠 justify-self 落位);列宽固定为一卡之宽(不用 minmax/1fr,
   否则宽屏卡片中心 ≠ 列中心)。
3. 亲属关系接口改白名单(parent/child/spouse)——原先未知 kind 被静默记成
   配偶,夹具传 kind=sibling 让家谱断言长期拿错数据自证。

设置下拉抽成可复用片段 _settings_menu.php($setCurrent 高亮当前页、
$setCanWrite 控「影院管理」显隐),六七个页头不再各抄一份。

验证:e2e 456/0、桌面几何 268/0、移动端几何 504/0、交互 97/0、
家谱连线诊断 0 失败、静态空跑 exit=0、静态断言证伪 13/13、
反向验证 68/68 全抓住。
2026-10-01 17:42:15 +08:00

556 lines
27 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
/**
* 观影详情页(v9,展示方式照搬 MovieLog 的 detail.php)。
*
* 五大块:英雄区(左海报右信息)→ 一起看的人 → 电影院信息 → 影像记录(灯箱)→ 评价与评论。
* 海报/票根/照片一律走 media_url() 代理;感受标签只存 key,emoji 与文案查 CATALOG。
* 访客(只读)看不到编辑/删除/传照片/评价表单 —— 与名册同一口径。
* 评价人是「登录账号背后的人」,与同行观影人无关(MovieLog 同款规则)。
*/
$canWrite = \App\Auth::canWrite();
$hasPoster = !empty($m['poster_key']);
$hasTicket = !empty($m['ticket_key']);
// 豆瓣元数据(v10):整体 JSON 存 douban_meta,随用随解;解不开就当没有
$doubanMeta = [];
if (!empty($m['douban_meta'])) {
$decoded = json_decode((string)$m['douban_meta'], true);
if (is_array($decoded)) {
$doubanMeta = $decoded;
}
}
$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();
};
/** 静态 10 星展示(v10):左 n 颗实心金黄,其余空心灰;n 越界按 0/10 兜底 */
$starsHtml = static function (int $n): string {
$n = max(0, min(10, $n));
$out = '<span class="stars-disp" aria-hidden="true">';
for ($i = 1; $i <= 10; $i++) {
$out .= '<span class="' . ($i <= $n ? 'd-on' : 'd-off') . '">' . ($i <= $n ? '★' : '☆') . '</span>';
}
return $out . '</span>';
};
/** 我的评价表单(打分 + 正文 + 感受),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>
<?php /* v10:10 星制。DOM 倒序(10..1)+ CSS row-reverse,视觉 1..10 从左到右;
点亮由 JS 加 .lit(含悬停预览), radios 保持 name="rating" 原语义。 */ ?>
<span class="rv-scale rv-stars no-js" role="radiogroup" aria-label="评分,1 分到 10 分">
<?php $myRating = (int)($myReview['rating'] ?? 0); ?>
<?php for ($i = 10; $i >= 1; $i--): ?>
<label class="rv-star" data-v="<?= $i ?>" title="<?= $i ?> 分 · <?= e(\App\MovieReview::ratingWord($i)) ?>">
<input type="radio" name="rating" value="<?= $i ?>"<?= $myRating === $i ? ' checked' : '' ?>>
<span class="st-glyph st-off" aria-hidden="true">☆</span><span class="st-glyph st-on" aria-hidden="true">★</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">
<span class="sub">观影记录</span>
<div class="actions">
<?php if ($canWrite): ?>
<a class="btn btn-ghost" href="<?= e(url('movies/edit', ['id' => (int)$m['id']])) ?>">编辑</a>
<form method="post" action="<?= e(url('movies/destroy')) ?>"
onsubmit="return confirm('删除这条观影记录?海报、票根、照片也会一并删除,不可恢复。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<button type="submit" class="btn btn-danger">删除</button>
</form>
<?php endif; ?>
<?php /* 设置入口要出现在「返回等按钮的旁边」(用户原话) */ ?>
<?php $setCurrent = 'show'; $setCanWrite = $canWrite; require __DIR__ . '/_settings_menu.php'; ?>
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回列表</a>
</div>
</div>
<!-- 主信息:英雄区 -->
<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): ?>
<img src="<?= e(media_url($m['poster_key'])) ?>" alt="海报">
<?php else: ?>
<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="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; ?>
<span>👥 观影人 <?= count($m['attendees']) ?> 人</span>
</div>
<?php if ($doubanMeta): ?>
<div class="db-meta">
<?php if (!empty($doubanMeta['rating'])): ?>
<span class="db-rating" title="豆瓣评分">豆瓣 <b><?= e((string)$doubanMeta['rating']) ?></b></span>
<?php endif; ?>
<?php foreach ([
'年份' => (string)($doubanMeta['year'] ?? ''),
'类型' => (string)($doubanMeta['genre'] ?? ''),
'片长' => (string)($doubanMeta['runtime'] ?? ''),
'导演' => (string)($doubanMeta['director'] ?? ''),
'主演' => (string)($doubanMeta['actors'] ?? ''),
'制片国家/地区' => (string)($doubanMeta['country'] ?? ''),
'又名' => (string)($doubanMeta['alias'] ?? ''),
] as $dbLbl => $dbVal): ?>
<?php if ($dbVal !== ''): ?>
<span class="db-line"><span class="db-label"><?= $dbLbl ?></span><?= e($dbVal) ?></span>
<?php endif; ?>
<?php endforeach; ?>
<?php if (!empty($doubanMeta['douban_url'])): ?>
<a class="db-link" href="<?= e((string)$doubanMeta['douban_url']) ?>"
target="_blank" rel="noopener noreferrer">在豆瓣查看 ↗</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($m['info'])): ?>
<div class="info-text"><?= nl2br(e($m['info'])) ?></div>
<?php endif; ?>
<?php if (!empty($m['note'])): ?>
<div class="remark-text">📝 <?= nl2br(e($m['note'])) ?></div>
<?php endif; ?>
</div>
</div>
<!-- 一起看的人 -->
<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('删除这张照片?')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
<input type="hidden" name="photo_id" value="<?= (int)$ph['id'] ?>">
<button type="submit" class="photo-del" title="删除这张照片">×</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if ($canWrite): ?>
<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>
<div class="photo-add-row">
<input class="input" id="photo" name="photo" type="file" accept="image/*" required>
<button type="submit" class="btn btn-ghost btn-sm">上传</button>
</div>
</form>
</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">
<?php /* v11:这里原本还有个写着「我」的徽标(类名与这个区块的首行同名)——
.rv-who 是 flex column,徽标被 align-items:stretch 拉成整行宽,
在星星旁边显出一条丑长的品牌色色带(用户反馈「特别长的条」),已删。
「是不是我」由删除按钮的存在表达,不需要徽标。 */ ?>
<span class="rv-name"><?= e($rvName) ?></span>
<span class="rv-time"><?= e(substr((string)$rv['created_at'], 0, 16)) ?></span>
</span>
<span class="rv-score" title="<?= e(\App\MovieReview::ratingWord($rvRate)) ?>"><?= $starsHtml($rvRate) ?><span class="rv-score-max"><?= $rvRate ?>/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>
<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>
<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 也能选(:has 兜底点亮) -->
<script>
(function () {
var form = document.getElementById('reviewForm');
var word = document.getElementById('rvWord');
if (!form || !word) return;
var scale = form.querySelector('.rv-stars');
if (scale) scale.classList.remove('no-js');
var WORDS = <?= json_encode(\App\MovieReview::ratingWords(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
function pickedV() {
var picked = form.querySelector('input[name="rating"]:checked');
return picked ? parseInt(picked.value, 10) : 0;
}
function paint(v) {
if (!scale) return;
Array.prototype.forEach.call(scale.querySelectorAll('.rv-star'), function (el) {
el.classList.toggle('lit', parseInt(el.getAttribute('data-v'), 10) <= v);
});
}
function sync() {
var v = pickedV();
if (!v) { word.textContent = '未评分'; word.classList.remove('is-set'); paint(0); return; }
word.textContent = v + ' 分 · ' + (WORDS[v - 1] || '');
word.classList.add('is-set');
paint(v);
}
Array.prototype.forEach.call(form.querySelectorAll('input[name="rating"]'), function (el) {
el.addEventListener('change', sync);
});
if (scale) {
scale.addEventListener('mouseover', function (e) {
var lab = e.target.closest ? e.target.closest('.rv-star') : null;
if (lab) paint(parseInt(lab.getAttribute('data-v'), 10));
});
scale.addEventListener('mouseleave', function () { paint(pickedV()); });
}
sync();
})();
</script>
<?php endif; ?>