Files
TripLog/templates/activities/_node_card.php
T
laoyang c50bc7de2b 活动页:每个节点卡片底部渲染参与人(头像 + 名字)
之前节点卡片上只有「👥 N」计数,要看是谁参加得点进节点详情。
现在卡片底部直接列出参与人:头像(缺头像用首字母兜底)+ 名字,
有心情顺带一个小 emoji;点任一人跳到该节点的参与区。
数据侧 Members::forNodes() 一条 IN 查询带回全部节点的参与人
(与 attachCounts 同样的防 N+1 思路),计划列与执行列共用。
无参与人的节点不渲染这一行。

已验证:本地全链 11 步全绿(渲染检查 292 项、表单交互 73 项);
远端 e2e 299 项 0 失败(新增活动页卡片参与人行专项断言);
部署后双 PHP 渲染逐字节一致、60 文件哈希一致。
2026-09-23 15:18:25 +08:00

154 lines
7.4 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
/**
* 节点卡片(计划列 / 执行列共用)
* 依赖外部变量:$n(节点行)、$cardCategory(plan|reality)、$executedPlanIds、
* $nodeMembers(node_id => 参与人列表,ActivityController::show 注入;可缺省)
*/
$cardCategory = $cardCategory ?? 'plan';
$executedPlanIds = $executedPlanIds ?? [];
$nodeMembersMap = $nodeMembers ?? [];
$statusLabels = [
'planned' => '待执行',
'ongoing' => '进行中',
'completed' => '已完成',
'cancelled' => '已取消',
];
$st = (string)($n['status'] ?? ($cardCategory === 'plan' ? 'planned' : 'completed'));
if (!isset($statusLabels[$st])) $st = $cardCategory === 'plan' ? 'planned' : 'completed';
$isExecuted = $cardCategory === 'plan' && in_array((int)$n['id'], $executedPlanIds, true);
$sLoc = json_decode_field($n['start_location'] ?? null);
$eLoc = json_decode_field($n['end_location'] ?? null);
// 交通只记一个号码(航班号 / 车次 / 班次)—— 航班号本身就隐含航空公司,
// 座位号对「多人同乘一趟」没有意义,所以这些字段都已不再收集
$transportParts = [];
if (!empty($n['trip_number'])) $transportParts[] = $n['trip_number'];
// 卡片上的状态快捷切换:计划节点在"待执行 / 已完成"之间来回,取消后可以恢复
$quickStatus = null;
$quickLabel = '';
if ($cardCategory === 'plan') {
if ($st === 'planned') { $quickStatus = 'completed'; $quickLabel = '✓ 标记完成'; }
elseif ($st === 'completed') { $quickStatus = 'planned'; $quickLabel = '↺ 撤销完成'; }
elseif ($st === 'cancelled') { $quickStatus = 'planned'; $quickLabel = '↺ 恢复'; }
} elseif ($st === 'cancelled') {
$quickStatus = 'completed';
$quickLabel = '↺ 恢复';
}
?>
<div class="node-card node-<?= e($cardCategory) ?><?= $st === 'cancelled' ? ' node-cancelled' : '' ?>">
<div class="node-head">
<span class="node-icon"><?= e($n['node_type_icon']) ?></span>
<span class="node-type"><?= e($n['node_type_name']) ?></span>
<?php if ($cardCategory === 'reality' && empty($n['original_plan_node_id'])): ?>
<span class="tag-temp">临时</span>
<?php endif; ?>
</div>
<div class="node-time">
<?= date('m-d H:i', strtotime($n['start_time'])) ?> - <?= date('H:i', strtotime($n['end_time'])) ?>
</div>
<div class="node-title"><?= e($n['title'] ?: '(无标题)') ?></div>
<?php if (!empty($sLoc['name']) || !empty($eLoc['name'])): ?>
<div class="node-loc">
<?php if (!empty($sLoc['name'])): ?>
<span class="loc-start">📍 <?= e($sLoc['name']) ?></span>
<?php endif; ?>
<?php if (!empty($sLoc['name']) && !empty($eLoc['name'])): ?>
<span class="loc-arrow"> → </span>
<?php endif; ?>
<?php if (!empty($eLoc['name'])): ?>
<span class="loc-end">📍 <?= e($eLoc['name']) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($transportParts)): ?>
<div class="node-transport">🚄 <?= e(implode(' · ', $transportParts)) ?></div>
<?php endif; ?>
<?php if ($st === 'cancelled' && !empty($n['cancel_reason'])): ?>
<div class="node-cancel-reason">取消原因:<?= e($n['cancel_reason']) ?></div>
<?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">
<span class="status-tag status-<?= e($st) ?>"><?= e($statusLabels[$st]) ?></span>
<a href="<?= url('nodes/show', ['id' => $n['id']]) ?>" class="node-action-link node-action-strong">详情</a>
<a href="<?= url('attachments/node', ['id' => $n['id']]) ?>" class="node-action-link">
📷 照片 (<?= (int)($n['attachment_count'] ?? 0) ?>)
</a>
<?php if ((int)($n['member_count'] ?? 0) > 0): ?>
<a href="<?= url('nodes/show', ['id' => $n['id']]) ?>#members" class="node-action-link">
👥 <?= (int)$n['member_count'] ?>
</a>
<?php endif; ?>
<?php if ((int)($n['comment_count'] ?? 0) > 0): ?>
<a href="<?= url('nodes/show', ['id' => $n['id']]) ?>#comments" class="node-action-link">
💬 <?= (int)$n['comment_count'] ?>
</a>
<?php endif; ?>
<?php if ((int)($n['expense_count'] ?? 0) > 0): ?>
<a href="<?= url('nodes/show', ['id' => $n['id']]) ?>#expenses" class="node-action-link">
💰 <?= (int)$n['expense_count'] ?>
</a>
<?php endif; ?>
<a href="<?= url('nodes/edit', ['id' => $n['id']]) ?>" class="node-action-link">编辑</a>
<?php if ($quickStatus !== null): ?>
<form method="post" action="<?= url('nodes/status') ?>" class="node-action-form">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<input type="hidden" name="status" value="<?= e($quickStatus) ?>">
<button type="submit" class="node-action-btn"><?= e($quickLabel) ?></button>
</form>
<?php endif; ?>
<?php if ($cardCategory === 'plan'): ?>
<?php if ($isExecuted): ?>
<span class="node-action-note">✓ 已成行</span>
<?php else: ?>
<form method="post" action="<?= url('nodes/fromPlan') ?>" class="node-action-form">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="plan_node_id" value="<?= (int)$n['id'] ?>">
<button type="submit" class="node-action-btn">→ 生成执行记录</button>
</form>
<?php endif; ?>
<?php endif; ?>
<form method="post" action="<?= url('nodes/destroy') ?>" class="node-action-form"
onsubmit="return confirm('确定删除这个节点?该节点的附件也会一并删除。');">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<button type="submit" class="node-action-btn danger">删除</button>
</form>
</div>
</div>