Files
FamilyBook/templates/movies/show.php
T
laoyang 7f00d3e553 feat(kin,movies): 家谱改用第三方库 family-chart;观影设置按钮改纯图标并统一页头度量
家谱(v13):
- 自研布局是死结,不是样式问题:连线的横杆是网格项,只能横跨「父母列→子女列」
  整段区间,中间夹着与血缘无关的人(如媳妇那侧的岳父母)时必然从卡片上穿过。
  换成 family-chart(D3 生态,MIT)后,排布只走本家血脉 + 配偶,姻亲不进画布,
  这个死结在构造上就不存在了。
- 接入踩到并修掉的坑:main_id = data[0].id(「我」必须排最前);
  卡片回调里自定义字段在 d.data.data(双层);库的 family-chart.css 是死文件
  (全挂在 .f3 作用域,运行时没有 .f3 元素);库的 svg 撑满选择器永不命中
  (必须自己补 .kin-chart svg.main_svg);setCanAdd/setOnCardClick 不在 chart 上
  (写错会抛异常,把 updateTree 一起带走 → 整张图空白,页面照样 200)。
- 关掉 setSingleParentEmptyCard:库默认给孩子只登记一位父母的人自动补一张无名
  配偶卡,看着像「凭空多出一个配偶」。
- 卡片用原生 <a> 链成员页,不依赖库的点击回调。

观影按钮(用户本轮原话):
- 「记一场电影」与「设置」高度仍不一致 —— 根因是文字按钮高度由 padding+line-height
  自然推出(43.8px),而 .btn-icon 写死了 40px。抽出 --act-* / --act-h 变量,两边同源。
- 设置按钮去掉「设置」二字,只留图标,图标换成三条横线(☰)。
- 设置按钮在所有页面排最右(.movie-set 用 margin-left:auto 顶到行尾)。
- 除设置外其他按钮都有颜色,与设置区分(主按钮实心 / 次要浅底 / 危险淡红)。
  顺带修掉一个特异性 bug:删除按钮被 .btn 品牌底盖成实心红,与「编辑」难分辨。
- 设置下拉四个条目文字前都加图标(行内 SVG,不引外部字体)。

测试与部署:
- 新增 _fb_kin_check.py(桌面,12 条)+ _fb_kin_falsify.py(6/6)证明
  「整张家谱空白」这类只靠通用几何抓不到的坏法能被抓住。
- _fb_geom_mobile.py 家谱段重写为窄屏语义(库按容器 fit 缩放是正常行为,
  改为断「比例没被压扁 / 缩放不至于看不清」),并加 _fb_kin_mobile_falsify.py(4/4)。
- _fb_btn_check.py(59 条)+ _fb_btn_falsify.py(3/3)覆盖观影页头按钮五条需求。
- familybook_deploy.sh / familybook_smoke.sh 的家谱断言改为第三方库形态,
  设置按钮断言改为 btn-icon 纯图标 + 三条横线 + 条目图标。
- 新增 .gitattributes:vendor 库与 .min.js 标 -text,原样入库不被改写行尾。
2026-10-01 19:35:48 +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 /* 设置入口放在 actions 最后 → 永远在最右侧(用户 2026-10-01 要求) */ ?>
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回列表</a>
<?php $setCurrent = 'show'; $setCanWrite = $canWrite; require __DIR__ . '/_settings_menu.php'; ?>
</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; ?>