评价区改成「按钮展开表单」,评论支持两级回复

页面默认不出现任何输入框,要写什么就点对应那个按钮:

- detail.php:评价区的「写评价 / 修改我的评价 / 评论 / 回复」都做成按钮,
  用原生 <details>/<summary> 折叠,点开才展开表单,提交后整页刷新自动收起。
  零 JS、键盘可用。没评价过只在顶部有「写评价」;评价过则该按钮消失,改在自己
  那条评价下面出现「修改我的评价」。一条评价都没有时显示「还没有评价,等你来评一个。」
- lib/reviews.php:评论改两级平铺。parent_id 为空 = 顶层评论,非空 = 对某条评论的
  回复;写「回复的回复」时由 addReviewComment 规范化回顶层评论,所以库里最多两层、
  页面永远只缩进一次,来回多少轮都不会越缩越窄。被回复者另存 reply_to_admin_id
  (存「人」不存评论 id,对方那条被删也不会悬空),页面用「回复 @某某」标出。
  deleteReviewComment 删顶层评论时连带清掉它的回复。
- lib/db.php:review_comments 增 parent_id / reply_to_admin_id 两列 + 幂等迁移,
  并给 parent_id 建索引。**修一个会让老库升级白屏的顺序 bug**:新列上的 CREATE INDEX
  必须排在 ALTER TABLE 补列之后 —— 建表用的是 CREATE TABLE IF NOT EXISTS,对线上
  已存在的旧表不生效,索引跑在补列之前会报 no such column: parent_id,整个 initDB
  挂掉。全新安装看不出来,是 e2e 的「造旧库再跑一遍初始化」专测抓到的。
- README.md:命名由「评价与讨论」改为「评价与评论」,补上按钮交互、两级平铺、
  回复 @某某 与新表字段的说明。

回归:reviews_e2e 84/84、fetch_movie 76/76、storage 196/196;A/B 47 个响应中
仅详情页相关 6 个有差异,且经折叠区块比对确认差异全部限定在评价区内。
线上 PHP 7.4.30 / 8.1.7 双版本迁移与写链路(顶层评论→回复→回复的回复→清理)全部通过。
This commit is contained in:
2026-09-22 16:09:48 +08:00
parent 558c0db576
commit 275bd9d80f
4 changed files with 458 additions and 173 deletions
+323 -143
View File
@@ -36,8 +36,9 @@ $rvError = ''; // 评价表单的错误
$rvFormRating = 0;
$rvFormContent = '';
$rvFormFeelings = []; // 评价表单里已勾选的观后感受
$rvCommentErrId = 0; // 哪一条评价的讨论表单报错
$rvCommentErr = '';
$rvCommentErrId = 0; // 哪一条评价下的「评论」表单报错
$rvReplyErrId = 0; // 哪一条评论下的「回复」表单报错
$rvCommentErr = ''; // 上面两种表单共用的错误文案
// 当前账号已有的评价(有则表单变成「修改」)
$myReview = getMyReview($id, $adminId);
@@ -88,36 +89,44 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $adminId > 0) {
} elseif ($rvAction === 'add_comment') {
$reviewId = (int)($_POST['review_id'] ?? 0);
$parentId = (int)($_POST['parent_id'] ?? 0); // 0 = 评论这条评价;>0 = 回复某条评论
$content = trim((string)($_POST['content'] ?? ''));
$feelings = normalizeFeelings($_POST['feelings'] ?? []);
$review = getReviewById($reviewId);
$isReply = ($parentId > 0);
if ($content === '') {
$rvCommentErrId = $reviewId;
$rvCommentErr = '讨论内容不能为空';
$rvCommentErr = $isReply ? '回复内容不能为空' : '评论内容不能为空';
} elseif (reviewTextLength($content) > REVIEW_COMMENT_MAX) {
$rvCommentErrId = $reviewId;
$rvCommentErr = '讨论内容太长了(最多 ' . REVIEW_COMMENT_MAX . ' 字)';
$rvCommentErr = ($isReply ? '回复' : '评论') . '内容太长了(最多 ' . REVIEW_COMMENT_MAX . ' 字)';
} elseif (!$review || (int)$review['movie_id'] !== $id) {
$rvCommentErrId = $reviewId;
$rvCommentErr = '这条评价不存在';
$rvCommentErr = '这条评价不存在';
} else {
$newId = addReviewComment($reviewId, $adminId, $content, $feelings);
$newId = addReviewComment($reviewId, $adminId, $content, $feelings, $parentId);
if ($newId) {
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => '讨论已发表'];
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => $isReply ? '回复已发表' : '评论已发表'];
header('Location: detail.php?id=' . $id . '#reviews');
exit;
}
$rvCommentErr = ($isReply && !getReviewCommentById($parentId))
? '要回复的那条评论不存在'
: '保存失败,请稍后再试';
}
// 提示挂回它来自的那个表单:渲染时命中就把 $rvCommentErrShown 置位;
// 若那个表单根本不在页面上(比如被改过的 id),末尾会退化成区块顶部的提示。
if ($isReply) {
$rvReplyErrId = $parentId;
} else {
$rvCommentErrId = $reviewId;
$rvCommentErr = '保存失败,请稍后再试';
}
} elseif ($rvAction === 'delete_comment') {
$commentId = (int)($_POST['comment_id'] ?? 0);
if (deleteReviewComment($commentId, $adminId)) {
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => '讨论已删除'];
$_SESSION['rv_flash'] = ['type' => 'success', 'msg' => '评论已删除'];
} else {
$_SESSION['rv_flash'] = ['type' => 'error', 'msg' => '只能删除自己的讨论'];
$_SESSION['rv_flash'] = ['type' => 'error', 'msg' => '只能删除自己的评论'];
}
header('Location: detail.php?id=' . $id . '#reviews');
exit;
@@ -128,18 +137,128 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $adminId > 0) {
$reviews = getMovieReviews($id);
$rvStats = reviewStats($id);
$feelings = feelingCatalog();
// 讨论的错误提示默认贴在那条评价自己的表单上;但如果出错的评价根本不在这部电影
// 下(比如被改过的 review_id),页面上找不到对应的表单,就退化成整段顶部的提示,
// 否则错误会被静默吞掉。
// 评论 / 回复的错误提示默认贴在它来自的那个表单上。到底贴上了没有,由渲染过程
// 自己记录(渲染到那个表单时把 $rvCommentErrShown 置为 true)——这样不必事先遍历
// 判断,也不会漏掉「回复表单」这种情况。若到最后都没贴上(那个表单不在页面上,
// 比如被改过的 review_id / comment_id),就退化成区块顶部的提示,不静默吞掉。
$rvCommentErrShown = false;
if ($rvCommentErrId > 0) {
foreach ($reviews as $rv) {
if ((int)$rv['id'] === $rvCommentErrId) { $rvCommentErrShown = true; break; }
}
}
// 1-10 分对应的文案,交给前端实时显示(下标 = 分值 - 1)
$rvRatingWords = array_map('ratingWord', range(1, 10));
// ---------- 评价区复用的模板片段 ----------
// 交互原则:页面上默认不出现任何输入框,要写什么就点对应那个按钮,表单当场展开;
// 提交后页面重新加载,表单自然又收起来。折叠用原生 <details>/<summary>:
// 不依赖 JS、没有 JS 也能用,键盘也能操作。
// 写成闭包是为了让「评价表单」「评论/回复表单」各只有一份代码,却能出现在多个位置
// (评价表单要在「写评价」和「修改我的评价」两处复用)。
// 可勾选的观后感受;$checked 里的是要预勾选的
$feelChips = function (array $checked = []) use ($feelings) {
ob_start(); ?>
<div class="feel-chips">
<?php foreach ($feelings as $fkey => $f): ?>
<label class="feel-chip feel-pick" title="<?= h($f['label']) ?>">
<input type="checkbox" name="feelings[]" value="<?= h($fkey) ?>"<?= in_array($fkey, $checked, true) ? ' checked' : '' ?>>
<span class="feel-inner">
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
</span>
</label>
<?php endforeach; ?>
</div>
<?php return ob_get_clean();
};
// 只读的观后感受标签(评价/评论已保存的感受)
$feelTags = function (array $keys) {
if (empty($keys)) return '';
ob_start(); ?>
<div class="rv-feelings">
<?php foreach ($keys as $fkey): ?>
<?php $f = feelingInfo($fkey); if (!$f) continue; ?>
<span class="feel-chip is-static" title="观后感受:<?= h($f['label']) ?>">
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
</span>
<?php endforeach; ?>
</div>
<?php return ob_get_clean();
};
// 我的评价表单(打分 + 正文 + 感受),外面套一层 details:
// 未评价时按钮写「写评价」,已评价时写「修改我的评价」。
// 注意:整页只会渲染出这一个表单,所以里面的 id 不会重复。
$rvFormHtml = function ($summary, $open = false) use ($id, $myReview, $rvFormRating, $rvFormContent, $rvFormFeelings, $feelChips, $rvError) {
ob_start(); ?>
<details class="rv-composer"<?= $open ? ' open' : '' ?>>
<summary class="rv-open-btn"><?= $summary ?></summary>
<form method="post" action="#reviews" class="rv-form" id="reviewForm">
<input type="hidden" name="action" value="save_review">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<div class="rv-rating-row">
<span class="rv-rating-label">我的评分</span>
<div class="rv-scale" role="radiogroup" aria-label="评分,1 分到 10 分">
<?php for ($i = 1; $i <= 10; $i++): ?>
<label class="rv-num" title="<?= $i ?> 分 · <?= h(ratingWord($i)) ?>">
<input type="radio" name="rating" value="<?= $i ?>"<?= $rvFormRating === $i ? ' checked' : '' ?>>
<span><?= $i ?></span>
</label>
<?php endfor; ?>
</div>
<span class="rv-word<?= $rvFormRating > 0 ? ' is-set' : '' ?>" id="rvWord">
<?= $rvFormRating > 0 ? $rvFormRating . ' 分 · ' . h(ratingWord($rvFormRating)) : '未评分' ?>
</span>
</div>
<?php if ($rvError): ?>
<div class="rv-error"><?= h($rvError) ?></div>
<?php endif; ?>
<textarea name="content" class="rv-textarea" maxlength="<?= REVIEW_CONTENT_MAX ?>" required
placeholder="这部电影怎么样?剧情、表演、画面、音效……随便聊聊"><?= h($rvFormContent) ?></textarea>
<div class="rv-feeling-pick">
<span class="rv-pick-label">我看完的感受(可多选)</span>
<?= $feelChips($rvFormFeelings) ?>
</div>
<div class="rv-form-foot">
<span class="rv-hint">每个账号对每部电影只有一条评价,再次提交即为修改。</span>
<button type="submit" class="btn btn-primary btn-sm"><?= $myReview ? '💾 保存修改' : '📝 发表评价' ?></button>
</div>
</form>
</details>
<?php return ob_get_clean();
};
// 评论 / 回复表单。$parentId = 0 是对这条评价发评论;>0 是回复 $parentId 那条评论
// (底层会不会再往下嵌由 addReviewComment 规范化,页面上永远只有两层)。
// 渲染命中错误时把 $rvCommentErrShown 置位,末尾就不必再兜底显示一遍。
$rvCommentBox = function ($reviewId, $parentId, $summary, $open = false) use ($id, $feelChips, $rvCommentErr, $rvCommentErrId, $rvReplyErrId, &$rvCommentErrShown) {
$parentId = (int)$parentId;
$errHere = $parentId > 0
? ($rvReplyErrId === $parentId)
: ($rvCommentErrId === (int)$reviewId);
ob_start(); ?>
<details class="rv-composer rv-inline"<?= $open ? ' open' : '' ?>>
<summary class="rv-act-btn"><?= $summary ?></summary>
<form method="post" action="#reviews" class="rv-form rv-form-inline">
<input type="hidden" name="action" value="add_comment">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<input type="hidden" name="review_id" value="<?= (int)$reviewId ?>">
<input type="hidden" name="parent_id" value="<?= $parentId ?>">
<?php if ($rvCommentErr !== '' && $errHere): ?>
<?php $rvCommentErrShown = true; ?>
<div class="rv-error"><?= h($rvCommentErr) ?></div>
<?php endif; ?>
<textarea name="content" class="rv-textarea rv-textarea-sm" maxlength="<?= REVIEW_COMMENT_MAX ?>" required
placeholder="<?= $parentId > 0 ? '回复这一条…' : '说说你的看法…' ?>"></textarea>
<div class="rv-feeling-pick">
<span class="rv-pick-label">观后感受(可多选)</span>
<?= $feelChips() ?>
</div>
<div class="rv-form-foot">
<button type="submit" class="btn btn-outline btn-sm"><?= $parentId > 0 ? '↩ 发表回复' : '💬 发表评论' ?></button>
</div>
</form>
</details>
<?php return ob_get_clean();
};
// ---------- 灯箱图片序列:海报 → 票根 → 观影照片 ----------
// 以前每张图都是用 JS 的 window.open 弹到新窗口,没有关闭按钮、体验很差。
// 现在把所有图片收进这一个序列:点任意一张都在当前页弹出查看,
@@ -438,12 +557,6 @@ $hasGallery = !empty($gallery);
padding: 16px 18px;
margin-bottom: 22px;
}
.rv-form-head {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.rv-rating-row {
display: flex;
align-items: center;
@@ -694,9 +807,6 @@ $hasGallery = !empty($gallery);
.rv-item > .rv-feelings {
margin-left: 44px;
}
.rv-comment-form {
margin-left: 44px;
}
.rv-feeling-pick {
margin-top: 8px;
}
@@ -764,14 +874,116 @@ $hasGallery = !empty($gallery);
color: var(--primary);
font-size: 12px;
}
/* ---------- 按需展开的输入框(原生 details,不依赖 JS) ----------
页面上默认看不到任何 textarea:写评价点「写评价」、评论点「评论」、
回复点「回复」,点开才展开;提交后页面刷新,自然又收起来。 */
.rv-composer > summary {
list-style: none; /* 去掉默认的三角标记(标准属性) */
cursor: pointer;
}
.rv-composer > summary::-webkit-details-marker {
display: none; /* 同上,兼容 WebKit / 旧 Chrome */
}
/* 评价表单的主按钮(那两处入口共用) */
.rv-open-btn {
display: inline-block;
padding: 8px 18px;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: #fff;
font-size: 14px;
font-weight: 600;
user-select: none;
transition: var(--transition);
}
.rv-open-btn:hover {
box-shadow: 0 4px 14px rgba(230, 126, 34, 0.32);
}
/* 展开状态下按钮退成浅色,避免和下方表单抢注意力 */
.rv-composer[open] > .rv-open-btn {
background: var(--bg-secondary);
color: var(--text-secondary);
box-shadow: none;
}
/* 列表内的轻量按钮:评论 / 回复 */
.rv-act-btn {
display: inline-block;
padding: 4px 12px;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--input-bg, var(--card-bg));
color: var(--text-secondary);
font-size: 12.5px;
line-height: 1.6;
user-select: none;
cursor: pointer;
transition: var(--transition);
}
.rv-act-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.rv-composer[open] > .rv-act-btn {
border-color: var(--primary);
background: rgba(230, 126, 34, 0.12);
color: var(--primary);
font-weight: 600;
}
/* 展开出来的表单:贴紧按钮,且不再要外面那圈大下边距 */
.rv-composer > .rv-form {
margin-top: 10px;
margin-bottom: 0;
}
.rv-form-inline {
padding: 12px 14px;
}
.rv-form-inline .rv-textarea-sm {
min-height: 56px;
}
/* 评价卡片底部的操作行 */
.rv-actions {
display: flex;
align-items: flex-start;
gap: 8px 10px;
flex-wrap: wrap;
margin: 12px 0 0 44px;
}
/* 一条评论 / 一条回复下面的「回复」按钮 */
.rv-c-acts {
margin-top: 6px;
}
/* 一个评论主题:顶层评论 + 它下面的全部回复 */
.rv-thread {
display: flex;
flex-direction: column;
gap: 6px;
}
/* 回复统一平铺在顶层评论之下,只缩进一点表示从属,不再逐层往里缩 */
.rv-comment-reply {
margin-left: 22px;
}
.rv-at {
font-size: 12.5px;
color: var(--text-secondary);
margin-bottom: 2px;
}
.rv-at span {
color: var(--primary);
font-weight: 600;
}
@media (max-width: 600px) {
.rv-content,
.rv-updated,
.rv-comments,
.rv-comment-form,
.rv-actions,
.rv-item > .rv-feelings {
margin-left: 0;
}
/* 手机上回复再收一点缩进,别把内容挤窄了 */
.rv-comment-reply {
margin-left: 10px;
}
}
/* ---------- 图片灯箱(替代原来的 window.open 弹窗) ----------
@@ -1040,11 +1252,13 @@ $hasGallery = !empty($gallery);
</div>
<?php endif; ?>
<!-- 评价与讨论
只要有登录账号就能打分(1-10)+ 写评价,并可以在任意一条评价下面参与讨论;
评价人是「账号」,与「观影人」无关——没去看过的人也能评。 -->
<!-- 评价与评论
只要有登录账号就能打分(1-10)+ 写评价,并可以在任意一条评价 / 评论下面回复;
评价人是「账号」,与「观影人」无关——没去看过的人也能评。
页面上默认不出现任何输入框:写评价要点「写评价」、评论要点「评论」、
回复要点「回复」,点开才展开,提交完页面刷新就自动收起来了。 -->
<div class="detail-section" id="reviews">
<h3>💬 评价与讨论
<h3>💬 评价与评论
<span class="badge">
<?php if ($rvStats['count'] > 0): ?>
<?= $rvStats['count'] ?> 条评价 · 均分 <?= $rvStats['avg'] ?>
@@ -1060,51 +1274,13 @@ $hasGallery = !empty($gallery);
</div>
<?php endif; ?>
<!-- 我的评分 + 评价(已评过时这里就是「修改」) -->
<form method="post" action="#reviews" class="rv-form" id="reviewForm">
<input type="hidden" name="action" value="save_review">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<div class="rv-form-head"><?= $myReview ? '✏️ 修改我的评价' : '⭐ 写下我的评价' ?></div>
<div class="rv-rating-row">
<span class="rv-rating-label">我的评分</span>
<div class="rv-scale" role="radiogroup" aria-label="评分,1 分到 10 分">
<?php for ($i = 1; $i <= 10; $i++): ?>
<label class="rv-num" title="<?= $i ?> 分 · <?= h(ratingWord($i)) ?>">
<input type="radio" name="rating" value="<?= $i ?>"<?= $rvFormRating === $i ? ' checked' : '' ?>>
<span><?= $i ?></span>
</label>
<?php endfor; ?>
</div>
<span class="rv-word<?= $rvFormRating > 0 ? ' is-set' : '' ?>" id="rvWord">
<?= $rvFormRating > 0 ? $rvFormRating . ' 分 · ' . h(ratingWord($rvFormRating)) : '未评分' ?>
</span>
</div>
<?php if ($rvError): ?>
<div class="rv-error"><?= h($rvError) ?></div>
<?php endif; ?>
<textarea name="content" class="rv-textarea" maxlength="<?= REVIEW_CONTENT_MAX ?>"
placeholder="这部电影怎么样?剧情、表演、画面、音效……随便聊聊"><?= h($rvFormContent) ?></textarea>
<div class="rv-feeling-pick">
<span class="rv-pick-label">我看完的感受(可多选)</span>
<div class="feel-chips">
<?php foreach ($feelings as $fkey => $f): ?>
<label class="feel-chip feel-pick" title="<?= h($f['label']) ?>">
<input type="checkbox" name="feelings[]" value="<?= h($fkey) ?>"<?= in_array($fkey, $rvFormFeelings, true) ? ' checked' : '' ?>>
<span class="feel-inner">
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
</span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="rv-form-foot">
<span class="rv-hint">每个账号对每部电影只有一条评价,再次提交即为修改。</span>
<button type="submit" class="btn btn-primary btn-sm"><?= $myReview ? '💾 保存修改' : '📝 发表评价' ?></button>
</div>
</form>
<!-- 没评价过:只显示一个「写评价」按钮,点开才出现输入框;评价完这个按钮就没了 -->
<?php if (!$myReview): ?>
<?= $rvFormHtml('📝 写评价', $rvError !== '') ?>
<?php endif; ?>
<?php if (empty($reviews)): ?>
<p class="rv-empty">还没有人评价这部电影,你可以是第一个 👆</p>
<p class="rv-empty">还没有评价,等你来评一个。</p>
<?php else: ?>
<div class="rv-list">
<?php foreach ($reviews as $rv): ?>
@@ -1124,7 +1300,7 @@ $hasGallery = !empty($gallery);
<span class="rv-score" title="<?= h(ratingWord($rvRating)) ?>"><?= $rvRating ?><span class="rv-score-max">/10</span></span>
<?php if ($rvMine): ?>
<form method="post" action="#reviews" class="rv-del"
onsubmit="return confirm('删除我的这条评价?下面的讨论也会一起删除,不可恢复。')">
onsubmit="return confirm('删除我的这条评价?下面的评论也会一起删除,不可恢复。')">
<input type="hidden" name="action" value="delete_review">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<input type="hidden" name="review_id" value="<?= $rv['id'] ?>">
@@ -1133,88 +1309,92 @@ $hasGallery = !empty($gallery);
<?php endif; ?>
</div>
<div class="rv-content"><?= h($rv['content']) ?></div>
<?php if (!empty($rv['feelings'])): ?>
<div class="rv-feelings">
<?php foreach ($rv['feelings'] as $fkey): ?>
<?php $f = feelingInfo($fkey); if (!$f) continue; ?>
<span class="feel-chip is-static" title="观后感受:<?= h($f['label']) ?>">
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?= $feelTags($rv['feelings']) ?>
<?php if (!empty($rv['updated_at']) && $rv['updated_at'] !== $rv['created_at']): ?>
<div class="rv-updated">已编辑 <?= h(substr((string)$rv['updated_at'], 0, 16)) ?></div>
<?php endif; ?>
<?php if (!empty($rv['comments'])): ?>
<div class="rv-comments">
<?php foreach ($rv['comments'] as $c): ?>
<?php foreach ($rv['comments'] as $thread): ?>
<?php
$c = $thread['comment'];
$cMine = ((int)$c['admin_id'] === $adminId);
$cName = ($c['username'] !== null && $c['username'] !== '') ? $c['username'] : '已注销账号';
?>
<div class="rv-comment">
<img class="rv-avatar rv-avatar-sm" src="<?= getAvatarUrl($c['avatar'] ?? null, $cName) ?>" alt="">
<div class="rv-c-body">
<div class="rv-c-head">
<span class="rv-name"><?= h($cName) ?></span>
<?php if ($cMine): ?><span class="rv-mine">我</span><?php endif; ?>
<span class="rv-time"><?= h(substr((string)$c['created_at'], 0, 16)) ?></span>
<?php if ($cMine): ?>
<form method="post" action="#reviews" class="rv-del"
onsubmit="return confirm('删除我的这条讨论?')">
<input type="hidden" name="action" value="delete_comment">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<input type="hidden" name="comment_id" value="<?= $c['id'] ?>">
<button type="submit" class="rv-del-btn" title="删除我的讨论" aria-label="删除我的讨论">✕</button>
</form>
<?php endif; ?>
</div>
<?php if (!empty($c['feelings'])): ?>
<div class="rv-feelings">
<?php foreach ($c['feelings'] as $fkey): ?>
<?php $f = feelingInfo($fkey); if (!$f) continue; ?>
<span class="feel-chip is-static" title="观后感受:<?= h($f['label']) ?>">
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
</span>
<?php endforeach; ?>
<div class="rv-thread">
<div class="rv-comment">
<img class="rv-avatar rv-avatar-sm" src="<?= getAvatarUrl($c['avatar'] ?? null, $cName) ?>" alt="">
<div class="rv-c-body">
<div class="rv-c-head">
<span class="rv-name"><?= h($cName) ?></span>
<?php if ($cMine): ?><span class="rv-mine">我</span><?php endif; ?>
<span class="rv-time"><?= h(substr((string)$c['created_at'], 0, 16)) ?></span>
<?php if ($cMine): ?>
<form method="post" action="#reviews" class="rv-del"
onsubmit="return confirm('删除我的这条评论?它下面的回复也会一起删除。')">
<input type="hidden" name="action" value="delete_comment">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<input type="hidden" name="comment_id" value="<?= $c['id'] ?>">
<button type="submit" class="rv-del-btn" title="删除我的评论" aria-label="删除我的评论">✕</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="rv-c-text"><?= h($c['content']) ?></div>
<?= $feelTags($c['feelings']) ?>
<div class="rv-c-text"><?= h($c['content']) ?></div>
<div class="rv-c-acts">
<?= $rvCommentBox($rv['id'], $c['id'], '↩ 回复', $rvReplyErrId === (int)$c['id']) ?>
</div>
</div>
</div>
<!-- 回复一律平铺在所属那条评论之下(不再逐层缩进),只用「回复 @谁」标出来 -->
<?php foreach ($thread['replies'] as $rep): ?>
<?php
$rMine = ((int)$rep['admin_id'] === $adminId);
$rName = ($rep['username'] !== null && $rep['username'] !== '') ? $rep['username'] : '已注销账号';
?>
<div class="rv-comment rv-comment-reply">
<img class="rv-avatar rv-avatar-sm" src="<?= getAvatarUrl($rep['avatar'] ?? null, $rName) ?>" alt="">
<div class="rv-c-body">
<div class="rv-c-head">
<span class="rv-name"><?= h($rName) ?></span>
<?php if ($rMine): ?><span class="rv-mine">我</span><?php endif; ?>
<span class="rv-time"><?= h(substr((string)$rep['created_at'], 0, 16)) ?></span>
<?php if ($rMine): ?>
<form method="post" action="#reviews" class="rv-del"
onsubmit="return confirm('删除我的这条回复?')">
<input type="hidden" name="action" value="delete_comment">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<input type="hidden" name="comment_id" value="<?= $rep['id'] ?>">
<button type="submit" class="rv-del-btn" title="删除我的回复" aria-label="删除我的回复">✕</button>
</form>
<?php endif; ?>
</div>
<?php if (!empty($rep['reply_to_username'])): ?>
<div class="rv-at">回复 <span>@<?= h($rep['reply_to_username']) ?></span></div>
<?php endif; ?>
<?= $feelTags($rep['feelings']) ?>
<div class="rv-c-text"><?= h($rep['content']) ?></div>
<div class="rv-c-acts">
<?= $rvCommentBox($rv['id'], $rep['id'], '↩ 回复', $rvReplyErrId === (int)$rep['id']) ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- 在这条评价下参与讨论:可多选观后感受 -->
<form method="post" action="#reviews" class="rv-comment-form">
<input type="hidden" name="action" value="add_comment">
<input type="hidden" name="movie_id" value="<?= $id ?>">
<input type="hidden" name="review_id" value="<?= $rv['id'] ?>">
<?php if ($rvCommentErrId === (int)$rv['id']): ?>
<div class="rv-error"><?= h($rvCommentErr) ?></div>
<!-- 操作行:默认都只是按钮,点开才出现输入框;
已评价过的用户,在「我这条评价」下面多一个「修改」 -->
<div class="rv-actions">
<?= $rvCommentBox($rv['id'], 0, '💬 评论', $rvCommentErrId === (int)$rv['id']) ?>
<?php if ($rvMine): ?>
<?= $rvFormHtml('✏️ 修改我的评价', $rvError !== '') ?>
<?php endif; ?>
<textarea name="content" class="rv-textarea rv-textarea-sm" maxlength="<?= REVIEW_COMMENT_MAX ?>"
placeholder="说说你的看法…"></textarea>
<div class="rv-feeling-pick">
<span class="rv-pick-label">看完的感受(可多选)</span>
<div class="feel-chips">
<?php foreach ($feelings as $fkey => $f): ?>
<label class="feel-chip feel-pick" title="<?= h($f['label']) ?>">
<input type="checkbox" name="feelings[]" value="<?= h($fkey) ?>">
<span class="feel-inner">
<span class="feel-emoji"><?= $f['emoji'] ?></span><span class="feel-label"><?= h($f['label']) ?></span>
</span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="rv-form-foot">
<button type="submit" class="btn btn-outline btn-sm">💬 发表讨论</button>
</div>
</form>
</div>
</div>
<?php endforeach; ?>
</div>