活动页:每个节点卡片底部渲染参与人(头像 + 名字)
之前节点卡片上只有「👥 N」计数,要看是谁参加得点进节点详情。
现在卡片底部直接列出参与人:头像(缺头像用首字母兜底)+ 名字,
有心情顺带一个小 emoji;点任一人跳到该节点的参与区。
数据侧 Members::forNodes() 一条 IN 查询带回全部节点的参与人
(与 attachCounts 同样的防 N+1 思路),计划列与执行列共用。
无参与人的节点不渲染这一行。
已验证:本地全链 11 步全绿(渲染检查 292 项、表单交互 73 项);
远端 e2e 299 项 0 失败(新增活动页卡片参与人行专项断言);
部署后双 PHP 渲染逐字节一致、60 文件哈希一致。
This commit is contained in:
@@ -1159,3 +1159,36 @@ input[type="date"]::-webkit-calendar-picker-indicator { cursor: pointer; padding
|
|||||||
.lightbox-next { right: 8px; }
|
.lightbox-next { right: 8px; }
|
||||||
.lightbox-img { max-height: 66vh; }
|
.lightbox-img { max-height: 66vh; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 18. 活动页节点卡片:参与人(头像 + 名字) ────────────────────────────── */
|
||||||
|
.node-members {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 5px;
|
||||||
|
margin-top: 8px; padding-top: 8px;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.node-member-chip {
|
||||||
|
display: inline-flex; align-items: center; gap: 5px;
|
||||||
|
padding: 2px 9px 2px 3px;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px; color: var(--text-soft);
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.node-member-chip:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.node-member-chip img,
|
||||||
|
.node-member-avatar-fallback {
|
||||||
|
width: 20px; height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.node-member-avatar-fallback {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
background: var(--accent-soft, var(--bg-soft));
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 11px; font-weight: 650;
|
||||||
|
}
|
||||||
|
.node-member-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.node-member-emotion { font-size: 13px; line-height: 1; }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Auth;
|
|||||||
use App\Comments;
|
use App\Comments;
|
||||||
use App\Database;
|
use App\Database;
|
||||||
use App\Expenses;
|
use App\Expenses;
|
||||||
|
use App\Members;
|
||||||
use App\Mime;
|
use App\Mime;
|
||||||
|
|
||||||
class ActivityController
|
class ActivityController
|
||||||
@@ -214,11 +215,18 @@ class ActivityController
|
|||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$executedPlanIds = array_map('intval', $stmt->fetchAll(\PDO::FETCH_COLUMN));
|
$executedPlanIds = array_map('intval', $stmt->fetchAll(\PDO::FETCH_COLUMN));
|
||||||
|
|
||||||
|
// 每个节点的参与人(头像 / 姓名),渲染在节点卡片底部 —— 一条 IN 查询,不 N+1
|
||||||
|
$nodeMembers = Members::forNodes(array_merge(
|
||||||
|
array_column($planNodes, 'id'),
|
||||||
|
array_column($realityNodes, 'id')
|
||||||
|
));
|
||||||
|
|
||||||
view('activities/show', [
|
view('activities/show', [
|
||||||
'activity' => $activity,
|
'activity' => $activity,
|
||||||
'planNodes' => $planNodes,
|
'planNodes' => $planNodes,
|
||||||
'realityNodes' => $realityNodes,
|
'realityNodes' => $realityNodes,
|
||||||
'executedPlanIds' => $executedPlanIds,
|
'executedPlanIds' => $executedPlanIds,
|
||||||
|
'nodeMembers' => $nodeMembers,
|
||||||
'expenseSummary' => Expenses::summaryForActivity($id),
|
'expenseSummary' => Expenses::summaryForActivity($id),
|
||||||
'commentTree' => Comments::tree(Comments::TARGET_ACTIVITY, $id),
|
'commentTree' => Comments::tree(Comments::TARGET_ACTIVITY, $id),
|
||||||
'pageTitle' => $activity['title'],
|
'pageTitle' => $activity['title'],
|
||||||
|
|||||||
@@ -84,6 +84,29 @@ class Members
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一次查完多个节点的参与人明细(头像 / 姓名 / 情绪),供活动页的节点卡片使用。
|
||||||
|
* 一条 IN 查询带回全部节点,避免每张卡片单独查(N+1)。
|
||||||
|
* @return array<int, array<int, array>> node_id => 参与人列表(按 node_members.id ASC)
|
||||||
|
*/
|
||||||
|
public static function forNodes(array $nodeIds): array
|
||||||
|
{
|
||||||
|
if (empty($nodeIds)) return [];
|
||||||
|
$ph = implode(',', array_fill(0, count($nodeIds), '?'));
|
||||||
|
$stmt = Database::get()->prepare('
|
||||||
|
SELECT nm.trip_node_id, nm.user_id, nm.emotion, u.name, u.avatar
|
||||||
|
FROM node_members nm
|
||||||
|
LEFT JOIN users u ON u.id = nm.user_id
|
||||||
|
WHERE nm.trip_node_id IN (' . $ph . ')
|
||||||
|
ORDER BY nm.id ASC');
|
||||||
|
$stmt->execute(array_values($nodeIds));
|
||||||
|
$out = [];
|
||||||
|
foreach ($stmt->fetchAll() as $row) {
|
||||||
|
$out[(int)$row['trip_node_id']][] = $row;
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增或更新一个参与人(同一节点同一人只留一条)
|
* 新增或更新一个参与人(同一节点同一人只留一条)
|
||||||
* @return array{ok: bool, error?: string}
|
* @return array{ok: bool, error?: string}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 节点卡片(计划列 / 执行列共用)
|
* 节点卡片(计划列 / 执行列共用)
|
||||||
* 依赖外部变量:$n(节点行)、$cardCategory(plan|reality)、$executedPlanIds
|
* 依赖外部变量:$n(节点行)、$cardCategory(plan|reality)、$executedPlanIds、
|
||||||
|
* $nodeMembers(node_id => 参与人列表,ActivityController::show 注入;可缺省)
|
||||||
*/
|
*/
|
||||||
$cardCategory = $cardCategory ?? 'plan';
|
$cardCategory = $cardCategory ?? 'plan';
|
||||||
$executedPlanIds = $executedPlanIds ?? [];
|
$executedPlanIds = $executedPlanIds ?? [];
|
||||||
|
$nodeMembersMap = $nodeMembers ?? [];
|
||||||
|
|
||||||
$statusLabels = [
|
$statusLabels = [
|
||||||
'planned' => '待执行',
|
'planned' => '待执行',
|
||||||
@@ -72,6 +74,30 @@ if ($cardCategory === 'plan') {
|
|||||||
<div class="node-cancel-reason">取消原因:<?= e($n['cancel_reason']) ?></div>
|
<div class="node-cancel-reason">取消原因:<?= e($n['cancel_reason']) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php /* 参与人:头像 + 名字(有心情顺带一个小 emoji),点行内任一人进节点参与区 */ ?>
|
||||||
|
<?php $cardMembers = $nodeMembersMap[(int)$n['id']] ?? []; ?>
|
||||||
|
<?php if (!empty($cardMembers)): ?>
|
||||||
|
<div class="node-members">
|
||||||
|
<?php foreach ($cardMembers as $cm):
|
||||||
|
$cmName = (string)($cm['name'] ?? '');
|
||||||
|
if ($cmName === '') $cmName = '已注销用户';
|
||||||
|
?>
|
||||||
|
<a class="node-member-chip" href="<?= url('nodes/show', ['id' => $n['id']]) ?>#members"
|
||||||
|
title="<?= e($cmName) ?>">
|
||||||
|
<?php if (!empty($cm['avatar'])): ?>
|
||||||
|
<img src="<?= e($cm['avatar']) ?>" alt="" loading="lazy">
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="node-member-avatar-fallback"><?= e(mb_substr($cmName, 0, 1)) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="node-member-name"><?= e($cmName) ?></span>
|
||||||
|
<?php if (!empty($cm['emotion'])): ?>
|
||||||
|
<span class="node-member-emotion"><?= e($cm['emotion']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="node-actions">
|
<div class="node-actions">
|
||||||
<span class="status-tag status-<?= e($st) ?>"><?= e($statusLabels[$st]) ?></span>
|
<span class="status-tag status-<?= e($st) ?>"><?= e($statusLabels[$st]) ?></span>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user