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:
+11
-7
@@ -37,21 +37,24 @@ $dtSec = $dtTs ? (int)date('s', $dtTs) : 0;
|
||||
aria-label="日期">
|
||||
<span class="dt-time">
|
||||
<select class="dt-hour" name="<?= e($dtName) ?>_hour" aria-label="时(24 小时制)">
|
||||
<?php for ($h = 0; $h < 24; $h++): $hv = sprintf('%02d', $h); ?>
|
||||
<option value="<?= $hv ?>"<?= $h === $dtHour ? ' selected' : '' ?>><?= $hv ?></option>
|
||||
<?php for ($dtH = 0; $dtH < 24; $dtH++): $hv = sprintf('%02d', $dtH); ?>
|
||||
<option value="<?= $hv ?>"<?= $dtH === $dtHour ? ' selected' : '' ?>><?= $hv ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<span class="dt-colon" aria-hidden="true">:</span>
|
||||
<select class="dt-minute" name="<?= e($dtName) ?>_minute" aria-label="分">
|
||||
<?php for ($m = 0; $m < 60; $m++): $mv = sprintf('%02d', $m); ?>
|
||||
<option value="<?= $mv ?>"<?= $m === $dtMin ? ' selected' : '' ?>><?= $mv ?></option>
|
||||
<?php /* ⚠️ 循环变量一律带 dt 前缀:这里曾被写成 $m,把宿主模板里
|
||||
表示「观影记录」的 $m 覆盖成了 int —— 编辑页的简介/备注回填
|
||||
被静默清空、取消链接变 id=0,还不止一个 Warning(2026-09-30 e2e 抓到)。 */ ?>
|
||||
<?php for ($dtM = 0; $dtM < 60; $dtM++): $mv = sprintf('%02d', $dtM); ?>
|
||||
<option value="<?= $mv ?>"<?= $dtM === $dtMin ? ' selected' : '' ?>><?= $mv ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<?php if ($dtSeconds): ?>
|
||||
<span class="dt-colon" aria-hidden="true">:</span>
|
||||
<select class="dt-second" name="<?= e($dtName) ?>_second" aria-label="秒">
|
||||
<?php for ($s = 0; $s < 60; $s++): $sv = sprintf('%02d', $s); ?>
|
||||
<option value="<?= $sv ?>"<?= $s === $dtSec ? ' selected' : '' ?>><?= $sv ?></option>
|
||||
<?php for ($dtS = 0; $dtS < 60; $dtS++): $sv = sprintf('%02d', $dtS); ?>
|
||||
<option value="<?= $sv ?>"<?= $dtS === $dtSec ? ' selected' : '' ?>><?= $sv ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
@@ -59,5 +62,6 @@ $dtSec = $dtTs ? (int)date('s', $dtTs) : 0;
|
||||
</div>
|
||||
<?php /* 用完即清,避免残留变量影响下一次 include */ unset(
|
||||
$dtName, $dtValue, $dtRequired, $dtId, $dtSeconds,
|
||||
$dtTs, $dtDate, $dtHour, $dtMin, $dtSec
|
||||
$dtTs, $dtDate, $dtHour, $dtMin, $dtSec,
|
||||
$dtH, $dtM, $dtS, $hv, $mv, $sv
|
||||
); ?>
|
||||
|
||||
@@ -7,6 +7,7 @@ $navItems = [
|
||||
'kin' => ['家谱', 'kin'],
|
||||
'people' => ['名册', 'people'],
|
||||
'movies' => ['观影', 'movies'],
|
||||
'cinemas' => ['影院', 'movies/cinemas'],
|
||||
];
|
||||
// ⚠️ 变量名不要叫 $me:layout 是在 extract($data) **之后**被 require 的,
|
||||
// 这里再赋值会把页面传进来的 $me(当前登录者的完整记录)覆盖掉。
|
||||
@@ -37,7 +38,7 @@ $isLoggedIn = \App\Auth::check();
|
||||
<span>家书</span>
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<?php foreach ($navItems as $route => list($label, $key)): ?>
|
||||
<?php foreach ($navItems as $key => list($label, $route)): ?>
|
||||
<a class="<?= $pageKey === $key ? 'on' : '' ?>" href="<?= e(url($route)) ?>"><?= e($label) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* 影院管理(v9,MovieLog cinemas.php 同款):上面加/编表单,下面列表。
|
||||
*
|
||||
* 颜色用「取色器 + 50 色预设板」两路输入(内联小段 JS 只是取色联动,
|
||||
* 不依赖 JS 也能手填 hex 或直接用取色器)。删除是 POST 小表单 + confirm;
|
||||
* 删掉的影院只把关联观影记录的 cinema_id 置空,记录本身不动。
|
||||
*/
|
||||
$editing = !empty($editing);
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>影院管理</h1>
|
||||
<span class="sub">共 <?= count($cinemas) ?> 家影院</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回观影记录</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$canWrite): ?>
|
||||
<div class="card card-pad empty"><b>只读账号</b>访客只能查看影院列表。</div>
|
||||
<?php else: ?>
|
||||
<div class="card card-pad cinema-form-card">
|
||||
<h2 class="cinema-h2"><?= $editing ? '编辑影院' : '添加影院' ?></h2>
|
||||
<form method="post" action="<?= e(url('movies/cinemas_save')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<?php if ($editing): ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
|
||||
<?php endif; ?>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="cname">影院名称 *</label>
|
||||
<input class="input" id="cname" name="name" required
|
||||
value="<?= e(old('name', $editing['name'] ?? '')) ?>" placeholder="如:万达影城(IMAX 店)">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="cplace">所在地方</label>
|
||||
<input class="input" id="cplace" name="place"
|
||||
value="<?= e(old('place', $editing['place'] ?? '')) ?>" placeholder="如:青岛 · 崂山区">
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="caddress">详细地址</label>
|
||||
<input class="input" id="caddress" name="address"
|
||||
value="<?= e(old('address', $editing['address'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="ccolor">主题颜色</label>
|
||||
<div class="color-row">
|
||||
<input type="color" id="ccolor" name="color" class="color-picker"
|
||||
value="<?= e(old('color', $editing['color'] ?? '#888888')) ?>">
|
||||
<span class="color-hex" id="colorHex"><?= e(old('color', $editing['color'] ?? '#888888')) ?></span>
|
||||
<span class="preset-palette">
|
||||
<?php foreach (\App\Cinema::PRESET_COLORS as $pc): ?>
|
||||
<span class="preset-color" data-color="<?= $pc ?>" style="background: <?= $pc ?>;" title="<?= $pc ?>"></span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button type="submit" class="btn"><?= $editing ? '保存' : '添加影院' ?></button>
|
||||
<?php if ($editing): ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('movies/cinemas')) ?>">取消编辑</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">影院列表</h2>
|
||||
<?php if (!$cinemas): ?>
|
||||
<p class="empty-hint">还没有影院,先在上面添加一家。</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>名称</th><th>所在地方</th><th>详细地址</th><th>观影</th><?php if ($canWrite): ?><th>操作</th><?php endif; ?></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($cinemas as $c): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="color-preview" style="background: <?= e((string)($c['color'] ?: '#888888')) ?>;"></span>
|
||||
<b><?= e($c['name']) ?></b>
|
||||
</td>
|
||||
<td><?= e((string)($c['place'] ?? '')) !== '' ? e($c['place']) : '—' ?></td>
|
||||
<td><?= !empty($c['address']) ? e($c['address']) : '—' ?></td>
|
||||
<td><?= (int)$c['movie_count'] ?> 场</td>
|
||||
<?php if ($canWrite): ?>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies/cinemas', ['edit' => (int)$c['id']])) ?>">编辑</a>
|
||||
<form method="post" action="<?= e(url('movies/cinemas_del')) ?>"
|
||||
onsubmit="return confirm('确定删除「<?= e($c['name']) ?>」吗?关联观影记录的影院信息将置空。')">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||||
<button type="submit" class="btn btn-danger btn-sm">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<script>
|
||||
(function () {
|
||||
var picker = document.getElementById('ccolor');
|
||||
var hex = document.getElementById('colorHex');
|
||||
if (!picker || !hex) return;
|
||||
function setActive(color) {
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.preset-color'), function (el) {
|
||||
el.classList.toggle('active', el.getAttribute('data-color') === color);
|
||||
});
|
||||
}
|
||||
picker.addEventListener('input', function () {
|
||||
hex.textContent = picker.value;
|
||||
setActive(picker.value);
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.preset-color'), function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
picker.value = el.getAttribute('data-color');
|
||||
hex.textContent = picker.value;
|
||||
setActive(picker.value);
|
||||
});
|
||||
});
|
||||
setActive(picker.value);
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
+43
-60
@@ -2,24 +2,31 @@
|
||||
/**
|
||||
* 观影表单(新增 / 编辑,一张表单两用 —— 与 people/form 同一思路)。
|
||||
*
|
||||
* 字段分三块:看什么(片名/日期/评分/感受)、跟谁(同行的人)、在哪看(影院三段),
|
||||
* 再加自由文本(简介/观后感/备注)。海报与票根是 free 槽位(v8):
|
||||
* 不进裁剪器、整图等比缩进框内,所以这里不放裁剪框,只放普通文件控件。
|
||||
* v9 变化:观影时间改为「开始 / 结束」两个秒级时刻(_datetime.php 控件,
|
||||
* 服务端 read_dt_input 取值);评分与观后感**移出表单** —— 搬到详情页的
|
||||
* 「评价与评论」区(MovieLog 同款:每账号一条评价,谁都能评);影院改为
|
||||
* 下拉选择已有影院 + 可选「新建影院」行内三段。
|
||||
*
|
||||
* 现场照片只在**新增**时提供多选;编辑时在详情页里逐张补/删 ——
|
||||
* 一次改一处的表单,比一个巨型表单不容易出错。
|
||||
* 海报与票根是 free 槽位(v8):不进裁剪器、整图等比缩进框内,普通文件控件即可。
|
||||
* 现场照片只在**新增**时提供多选;编辑时在详情页里逐张补/删。
|
||||
*/
|
||||
$isEdit = !empty($m['id']);
|
||||
|
||||
// 回填:校验失败重画时用用户刚提交的值(old()),否则用库里的值
|
||||
// 时间回填:校验失败重画时优先用 old() 里的分字段重组(否则用户刚选的时刻会丢);
|
||||
// 正常进编辑页时用库里的值。
|
||||
$dtFromOld = static function (string $name, ?string $dbValue): ?string {
|
||||
$date = trim((string)old($name . '_date', ''));
|
||||
if ($date !== '') {
|
||||
return sprintf('%s %02d:%02d:%02d', $date,
|
||||
(int)old($name . '_hour', 0), (int)old($name . '_minute', 0), (int)old($name . '_second', 0));
|
||||
}
|
||||
return ($dbValue !== null && $dbValue !== '') ? $dbValue : null;
|
||||
};
|
||||
$dtStart = $dtFromOld('starts_at', $m['starts_at'] ?? null);
|
||||
$dtEnd = $dtFromOld('ends_at', $m['ends_at'] ?? null);
|
||||
$selCinema = (int)old('cinema_id', $m['cinema_id'] ?? 0);
|
||||
$curAttendees = array_map('intval', array_column($m['attendees'] ?? [], 'id'));
|
||||
$curFeelings = [];
|
||||
if (!empty($m['feelings'])) {
|
||||
$curFeelings = json_decode((string)$m['feelings'], true) ?: [];
|
||||
}
|
||||
$selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
|
||||
$selFeelings = (array)old('feelings', $curFeelings);
|
||||
$rtVal = old('rating', $m['rating'] ?? '');
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1><?= $isEdit ? '编辑观影记录' : '记一场电影' ?></h1>
|
||||
@@ -41,34 +48,34 @@ $rtVal = old('rating', $m['rating'] ?? '');
|
||||
<?php if (!empty($errors['title'])): ?><span class="err"><?= e($errors['title']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="starts_at">观影日期</label>
|
||||
<input class="input" id="starts_at" name="starts_at" type="date"
|
||||
value="<?= e(old('starts_at', $m['starts_at'] ?? '')) ?>">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="rating">评分(1-10)</label>
|
||||
<select class="select" id="rating" name="rating">
|
||||
<option value="">不评</option>
|
||||
<?php for ($i = 10; $i >= 1; $i--): ?>
|
||||
<option value="<?= $i ?>" <?= (string)$rtVal === (string)$i ? 'selected' : '' ?>><?= $i ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<div class="field full">
|
||||
<label <?= $isEdit ? '' : 'for="starts_at_date"' ?>>开始时间(精确到秒)</label>
|
||||
<?php $dtName = 'starts_at'; $dtValue = $dtStart; $dtId = 'starts_at_date';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php if (!empty($errors['starts_at'])): ?><span class="err"><?= e($errors['starts_at']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label>观后感受</label>
|
||||
<div class="feel-grid">
|
||||
<?php foreach (\App\Movie::FEELINGS as $f): ?>
|
||||
<label class="feel-item">
|
||||
<input type="checkbox" name="feelings[]" value="<?= e($f) ?>"
|
||||
<?= in_array($f, $selFeelings, true) ? 'checked' : '' ?>>
|
||||
<span><?= e($f) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<label>结束时间(散场,可不填)</label>
|
||||
<?php $dtName = 'ends_at'; $dtValue = $dtEnd; $dtId = '';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php if (!empty($errors['ends_at'])): ?><span class="err"><?= e($errors['ends_at']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="cinema_id">影院</label>
|
||||
<div class="cinema-select-row">
|
||||
<select class="select" id="cinema_id" name="cinema_id">
|
||||
<option value="0">不记录影院</option>
|
||||
<?php foreach ($cinemas as $c): ?>
|
||||
<option value="<?= (int)$c['id'] ?>" <?= $selCinema === (int)$c['id'] ? 'selected' : '' ?>>
|
||||
<?= e(trim(((string)($c['place'] ?? '') !== '' ? $c['place'] . ' · ' : '') . $c['name'])) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies/cinemas')) ?>">管理影院</a>
|
||||
</div>
|
||||
<span class="tip">MovieLog 里记的英文标签(warm / excited…)搬过来时已经翻成中文。</span>
|
||||
<span class="tip">想记一家还没录过的影院?先去「管理影院」里添加,回来这里就能选。</span>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
@@ -88,36 +95,12 @@ $rtVal = old('rating', $m['rating'] ?? '');
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="cinema_place">影院 · 城市/商圈</label>
|
||||
<input class="input" id="cinema_place" name="cinema_place"
|
||||
value="<?= e(old('cinema_place', $m['cinema_place'] ?? '')) ?>" placeholder="如:青岛 · 崂山区">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="cinema_name">影院 · 名字</label>
|
||||
<input class="input" id="cinema_name" name="cinema_name"
|
||||
value="<?= e(old('cinema_name', $m['cinema_name'] ?? '')) ?>" placeholder="如:万达影城 CBD 店">
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="cinema_address">影院 · 地址</label>
|
||||
<input class="input" id="cinema_address" name="cinema_address"
|
||||
value="<?= e(old('cinema_address', $m['cinema_address'] ?? '')) ?>">
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="info">简介</label>
|
||||
<textarea class="input" id="info" name="info" rows="3"
|
||||
placeholder="剧情简介(可选)"><?= e(old('info', $m['info'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="review">观后感</label>
|
||||
<textarea class="input" id="review" name="review" rows="4"
|
||||
placeholder="看完想记下来的话"><?= e(old('review', $m['review'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="note">备注</label>
|
||||
<textarea class="input" id="note" name="note" rows="2"><?= e(old('note', $m['note'] ?? '')) ?></textarea>
|
||||
|
||||
+58
-40
@@ -1,61 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* 观影时间线 —— 按年分组倒序(v8,MovieLog 迁入后的新家)。
|
||||
* 观影瀑布流(v9,MovieLog 同款海报墙)。
|
||||
*
|
||||
* 没填日期的记录落在键 0,沉到最底下单独一组「日期待补」,
|
||||
* 不让它混进任何一个年份里(跟名册「未分类」栏是同一个思路)。
|
||||
* 卡片 = 海报(2:3,无海报给渐变占位)+ 片名 + 日期/影院(主题色)/评分均分 + 同行人头像标签。
|
||||
* 顶部搜索栏:关键词搜片名 / 影院 / 同行人(Movie::all 的 LIKE 查询)。
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$fmtNum = static function ($v): string {
|
||||
if ($v === null) {
|
||||
return '';
|
||||
}
|
||||
return rtrim(rtrim(number_format((float)$v, 1, '.', ''), '0'), '.');
|
||||
};
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>观影记录</h1>
|
||||
<span class="sub">一共 <?= (int)$total ?> 场</span>
|
||||
<span class="sub">
|
||||
<?php if ($keyword !== ''): ?>
|
||||
找到 <?= (int)$total ?> 部匹配「<b><?= e($keyword) ?></b>」的电影
|
||||
<?php else: ?>
|
||||
共 <?= (int)$total ?> 部 · 按观影时间排序
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies/cinemas')) ?>">影院管理</a>
|
||||
<?php if ($canWrite): ?>
|
||||
<a class="btn" href="<?= e(url('movies/new')) ?>">记一场电影</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" action="index.php" class="search-bar">
|
||||
<input type="hidden" name="r" value="movies">
|
||||
<input type="text" name="keyword" value="<?= e($keyword) ?>" placeholder="搜索片名、影院、观影人…" aria-label="搜索">
|
||||
<button type="submit">搜索</button>
|
||||
<?php if ($keyword !== ''): ?>
|
||||
<a class="search-clear" href="<?= e(url('movies')) ?>">清除</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<?php if ($total === 0): ?>
|
||||
<div class="card card-pad empty">
|
||||
<b>还没有观影记录</b>
|
||||
一家人一起看过的电影,都会按年份排在这里。
|
||||
<b><?= $keyword !== '' ? '没有找到匹配的电影' : '还没有观影记录' ?></b>
|
||||
<?= $keyword !== '' ? '换个关键词试试。' : '一家人一起看过的电影,都会以海报墙的方式排在这里。' ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($years as $y => $list): ?>
|
||||
<section class="section">
|
||||
<h2 class="section-title"><?= $y > 0 ? (int)$y : '日期待补' ?> <span class="hint"><?= count($list) ?> 场</span></h2>
|
||||
<div class="movie-list">
|
||||
<?php foreach ($list as $mv): ?>
|
||||
<?php
|
||||
// 评分统一一位小数内显示:9 → "9",8.5 → "8.5"(库里是 0-10 整数,这里只做展示兜底)
|
||||
$rtTxt = $mv['rating'] !== null
|
||||
? rtrim(rtrim(number_format((float)$mv['rating'], 1, '.', ''), '0'), '.')
|
||||
: '';
|
||||
?>
|
||||
<a class="movie-card" href="<?= e(url('movies/show', ['id' => (int)$mv['id']])) ?>">
|
||||
<?php if (!empty($mv['poster_key'])): ?>
|
||||
<img class="m-poster" src="<?= e(media_url($mv['poster_key'])) ?>" alt="" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="m-poster m-poster-empty" aria-hidden="true">影</span>
|
||||
<?php endif; ?>
|
||||
<span class="m-main">
|
||||
<span class="m-title">
|
||||
<?= e($mv['title']) ?>
|
||||
<?php if ($rtTxt !== ''): ?><span class="rating"><?= e($rtTxt) ?></span><?php endif; ?>
|
||||
</span>
|
||||
<span class="m-meta">
|
||||
<?= e((string)($mv['starts_at'] ?? '日期待补')) ?>
|
||||
<?php if (!empty($mv['cinema_name'])): ?> · <?= e($mv['cinema_name']) ?><?php endif; ?>
|
||||
</span>
|
||||
<?php if (!empty($mv['attendee_names'])): ?>
|
||||
<span class="m-who">和 <?= e($mv['attendee_names']) ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="waterfall">
|
||||
<?php foreach ($rows as $mv): ?>
|
||||
<a class="movie-card" href="<?= e(url('movies/show', ['id' => (int)$mv['id']])) ?>">
|
||||
<span class="card-image">
|
||||
<?php if (!empty($mv['poster_key'])): ?>
|
||||
<img src="<?= e(media_url($mv['poster_key'])) ?>" alt="<?= e($mv['title']) ?>" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="placeholder-poster">
|
||||
<span class="ph-icon" aria-hidden="true">影</span>
|
||||
<span class="placeholder-title"><?= e($mv['title']) ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span class="card-body">
|
||||
<span class="card-title"><?= e($mv['title']) ?></span>
|
||||
<span class="card-meta">
|
||||
<span class="meta-date">📅 <?= e(\App\Movie::formatWatch($mv['starts_at'], $mv['ends_at'])) ?></span>
|
||||
<?php if (!empty($mv['cinema_name'])): ?>
|
||||
<span class="meta-cinema" style="color: <?= e((string)($mv['cinema_color'] ?: '#888888')) ?>;">🏛️ <?= e($mv['cinema_name']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($mv['review_count'])): ?>
|
||||
<span class="meta-rating" title="共 <?= (int)$mv['review_count'] ?> 条评价">⭐ <?= e($fmtNum($mv['review_avg'])) ?><span class="meta-rating-max">/10</span></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php if (!empty($mv['attendee_names'])): ?>
|
||||
<span class="card-viewers"><span class="viewer-tag">👥 <?= e($mv['attendee_names']) ?></span></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
+432
-77
@@ -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; ?>
|
||||
|
||||
Reference in New Issue
Block a user