Files
TripLog/templates/activities/_node_card.php
T
laoyang c8515632fb 节点与活动支持导出 ICS 日历(批 27)
- 新增 src/Ics.php:RFC 5545 生成器(CRLF、75 octet 折行且不切断
  UTF-8 字符、TEXT 转义、时间输出 UTC Z、UID 固定为 triplog-node-{id}
  使重复导入按 UID 更新而不堆重复事件)。
- 新增路由 nodes/ics、activities/ics:节点级导出单个事件
  (SUMMARY=活动名·节点标题、DTSTART/DTEND、LOCATION=出发地、
  DESCRIPTION=车次/航班号+备注);活动级导出整个行程(跳过已取消
  节点与已生成执行记录的计划节点,口径与详情页展示一致)。
- 入口:活动详情页「导出日历」、节点详情页「导出日历」、
  节点卡片「日历」链接。
- 修复隐性事故:index.php 漏 require src/Ics.php(无 autoloader 的
  项目里新增类必须同步入口),部署哨兵新增「src/ 每个类文件都被
  index.php require」逐文件对账。
- 验证:php -l 61 文件全过;渲染检查 386 项全过;Ics 单测 42 项
  全过(转义/折行/UTC/拼装);服务器 e2e 391 项全过(含 11c 节
  17 项 ICS 断言)。
2026-09-29 07:58:31 +08:00

163 lines
7.9 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
/* 批 19:交通行图标跟着「出行方式」走(飞机/火车/游船…),不再一律 🚄。
没选出行方式时回退到 🚄,保持旧行为。 */
$modeIcons = $modeIcons ?? [];
$tModeType = (string)($n['travel_mode_type'] ?? '');
$tIcon = $modeIcons[$tModeType] ?? '🚄';
?>
<?php if (!empty($transportParts)): ?>
<div class="node-transport"><?= e($tIcon) ?> <?= 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/ics', ['id' => $n['id']]) ?>" class="node-action-link"
title="下载 .ics 导入到手机/电脑日历">📅 日历</a>
<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>