Files
TripLog/templates/nodes/show.php
T
laoyang a24099fc53 节点详情页:所有附件统一展示,不再把图片和其它附件分开列
之前照片墙只显示图片,非图片附件只给一句「另有 N 个非图片附件」,要看得再进管理页。
现在全部附件同列在照片与附件区:图片仍是缩略图,视频 / PDF / Word 等按 MIME
显示类型图标(🎬 / 📄 / 📎),和缩略图一样占满 4:3 格子。

点击行为沿用批 7 的灯箱四态:图片 / 视频 / PDF 直接预览(视频可拖进度条、
PDF 内嵌阅读器),预览不了的(Word / ZIP 等)灯箱里给下载按钮。
页头与底部入口文案同步改为「附件 (N) / 管理附件(N)」,
最多展示 12 个,超出给「+N 个」入口进管理页。

已验证:本地全链 11 步全绿(渲染检查 316 项,新增图标格 / 四态标记 /
不再出现「另有」等 8 项断言);远端 e2e 322 项 0 失败(新增详情页统一展示
专项断言 7 项);部署后双 PHP 渲染逐字节一致、60 文件哈希一致。
2026-09-23 17:36:58 +08:00

220 lines
9.6 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
/**
* 节点详情:基础信息 + 参与人 + 花销 + 讨论
* 一个节点一页,把挂在这个节点上的所有记录集中起来
*/
use App\Attachments;
use App\Comments;
$statusLabels = [
'planned' => '待执行',
'ongoing' => '进行中',
'completed' => '已完成',
'cancelled' => '已取消',
];
$st = (string)($node['status'] ?? 'planned');
if (!isset($statusLabels[$st])) $st = 'planned';
$sLoc = json_decode_field($node['start_location'] ?? null);
$eLoc = json_decode_field($node['end_location'] ?? null);
/**
* 交通只记一个号码(航班号 / 车次 / 班次)——
* 航班号本身就隐含航空公司,座位号对多人同行又没法记,所以都不收。
*/
$tripFieldLabels = [
'plane' => '航班号',
'train' => '车次',
'bus' => '班次',
'subway' => '线路',
'ship' => '航次',
'taxi' => '备注',
];
$tripLabel = $tripFieldLabels[(string)($node['travel_mode_type'] ?? '')] ?? '车次 / 班次';
$transportParts = [];
if (!empty($node['trip_number'])) {
$transportParts[] = $tripLabel . ' ' . $node['trip_number'];
}
$durationText = '';
if (!empty($node['duration_minutes'])) {
$mins = (int)$node['duration_minutes'];
$durationText = $mins >= 60 ? intdiv($mins, 60) . ' 小时 ' . ($mins % 60) . ' 分' : $mins . ' 分钟';
}
?>
<div class="page-header">
<div>
<h1><?= e(($node['node_type_icon'] ?? '') . ' ' . ($node['title'] ?: '(无标题节点)')) ?></h1>
<p class="muted">
<a href="<?= url('activities/show', ['id' => $node['activity_id']]) ?>"><?= e($node['activity_title']) ?></a>
· <?= $node['node_category'] === 'plan' ? '计划节点' : '执行节点' ?>
· <span class="status-tag status-<?= e($st) ?>"><?= e($statusLabels[$st]) ?></span>
<?php if ($node['node_category'] === 'reality' && !empty($node['original_plan_node_id'])): ?>
· <span class="tag-default">由计划节点生成</span>
<?php endif; ?>
</p>
</div>
<div class="actions" style="display:flex; gap:8px; flex-wrap:wrap;">
<a href="<?= url('nodes/edit', ['id' => $node['id']]) ?>" class="btn">编辑节点</a>
<a href="<?= url('attachments/node', ['id' => $node['id']]) ?>" class="btn">📎 附件 (<?= (int)$attachmentCount ?>)</a>
<a href="<?= url('activities/show', ['id' => $node['activity_id']]) ?>" class="btn">返回活动</a>
<form method="post" action="<?= url('nodes/destroy') ?>" style="display:inline;"
onsubmit="return confirm('确定删除这个节点?它的附件、参与记录、花销和讨论都会一并删除。');">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="id" value="<?= (int)$node['id'] ?>">
<button type="submit" class="btn btn-danger">删除节点</button>
</form>
</div>
</div>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">时间</span>
<span class="detail-value">
<?= date('Y-m-d H:i', strtotime((string)$node['start_time'])) ?>
→
<?= date('Y-m-d H:i', strtotime((string)$node['end_time'])) ?>
<?php if ($durationText !== ''): ?>
<span class="muted">(<?= e($durationText) ?>)</span>
<?php endif; ?>
</span>
</div>
<div class="detail-item">
<span class="detail-label">类型</span>
<span class="detail-value"><?= e($node['node_type_name'] ?? '未分类') ?></span>
</div>
<?php if (!empty($node['travel_mode_label'])): ?>
<div class="detail-item">
<span class="detail-label">出行方式</span>
<span class="detail-value"><?= e($node['travel_mode_label']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($transportParts)): ?>
<div class="detail-item">
<span class="detail-label">交通</span>
<span class="detail-value"><?= e(implode(' · ', $transportParts)) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($sLoc['name']) || !empty($sLoc['address'])): ?>
<div class="detail-item">
<span class="detail-label">起点</span>
<span class="detail-value">
📍 <?= e($sLoc['name'] ?? '') ?>
<?php if (!empty($sLoc['address'])): ?>
<span class="muted"><?= e($sLoc['address']) ?></span>
<?php endif; ?>
<?php /* 存下来的坐标就是为了一键导航:手机端点开会直接唤起高德 App */ ?>
<?php $sNav = amap_nav_url($sLoc); ?>
<?php if ($sNav !== ''): ?>
<a class="loc-nav-link loc-nav-inline" href="<?= e($sNav) ?>"
target="_blank" rel="noopener">🧭 导航</a>
<?php endif; ?>
</span>
</div>
<?php endif; ?>
<?php if (!empty($eLoc['name']) || !empty($eLoc['address'])): ?>
<div class="detail-item">
<span class="detail-label">终点</span>
<span class="detail-value">
📍 <?= e($eLoc['name'] ?? '') ?>
<?php if (!empty($eLoc['address'])): ?>
<span class="muted"><?= e($eLoc['address']) ?></span>
<?php endif; ?>
<?php $eNav = amap_nav_url($eLoc); ?>
<?php if ($eNav !== ''): ?>
<a class="loc-nav-link loc-nav-inline" href="<?= e($eNav) ?>"
target="_blank" rel="noopener">🧭 导航</a>
<?php endif; ?>
</span>
</div>
<?php endif; ?>
<?php if (!empty($node['description'])): ?>
<div class="detail-item detail-item-wide">
<span class="detail-label">描述</span>
<span class="detail-value"><?= nl2br(e($node['description'])) ?></span>
</div>
<?php endif; ?>
<?php if ($st === 'cancelled' && !empty($node['cancel_reason'])): ?>
<div class="detail-item detail-item-wide">
<span class="detail-label">取消原因</span>
<span class="detail-value node-cancel-reason"><?= e($node['cancel_reason']) ?></span>
</div>
<?php endif; ?>
</div>
<?php
/* 附件墙(批 8 起统一展示):不再把图片和其它附件分开列 ——
图片给缩略图,视频 / PDF / 其它给类型图标;全部走 media/show 代理,
能预览的(图片 / 视频 / PDF)点击进灯箱直接看,
预览不了的(Word / ZIP 等)灯箱里给下载按钮(见 _lightbox.php) */
$showAtts = array_slice(array_values($attachments ?? []), 0, 12);
$attIcon = static function (string $mime): string {
if (strpos($mime, 'video/') === 0) return '🎬';
if ($mime === 'application/pdf') return '📄';
return '📎';
};
?>
<?php if (!empty($attachments)): ?>
<h2 class="section-title" id="photos">照片与附件(<?= count($attachments) ?>)</h2>
<?php if (!empty($showAtts)): ?>
<div class="photo-grid photo-grid-sm">
<?php foreach ($showAtts as $attRow):
$attId = (int)$attRow['id'];
$attName = (string)($attRow['file_name'] ?? '');
$attMime = (string)($attRow['mime_type'] ?? '');
$isImg = Attachments::isImageRow($attRow);
if ($isImg) {
$kind = 'image';
} elseif (strpos($attMime, 'video/') === 0) {
$kind = 'video';
} elseif ($attMime === 'application/pdf') {
$kind = 'pdf';
} else {
$kind = 'file';
}
?>
<?php if ($isImg): ?>
<?php /* js-photo:图片点击在当前页灯箱里看大图 */ ?>
<a class="photo-card js-photo" href="<?= e(media_att($attId)) ?>"
data-cap="<?= e($attName) ?>" title="<?= e($attName) ?>">
<img src="<?= e(media_att($attId)) ?>" alt="<?= e($attName) ?>" loading="lazy">
</a>
<?php else: ?>
<?php /* js-media:非图片走四态灯箱(视频/PDF 可预览,其它给下载) */ ?>
<a class="photo-card photo-card-icon js-media" href="<?= e(media_att($attId)) ?>"
data-kind="<?= $kind ?>" data-cap="<?= e($attName) ?>"
data-dl="<?= e(media_att($attId, true)) ?>" title="<?= e($attName) ?>">
<span class="attachment-icon"><?= $attIcon($attMime) ?></span>
</a>
<?php endif; ?>
<?php endforeach; ?>
<?php if (count($attachments) > count($showAtts)): ?>
<a class="photo-more" href="<?= url('attachments/node', ['id' => (int)$node['id']]) ?>#photos">
+<?= count($attachments) - count($showAtts) ?><span>个</span>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="photo-bar">
<a class="btn btn-sm" href="<?= url('attachments/node', ['id' => (int)$node['id']]) ?>">
📎 管理附件(<?= count($attachments) ?>)
</a>
</div>
<?php endif; ?>
<?php require __DIR__ . '/_members.php'; ?>
<?php require __DIR__ . '/_expenses.php'; ?>
<?php
$commentTargetType = Comments::TARGET_NODE;
$commentTargetId = (int)$node['id'];
require dirname(__DIR__) . '/comments/_block.php';
?>
<?php /* 照片灯箱:当前页弹窗看大图,左右箭头切换 */ ?>
<?php require dirname(__DIR__) . '/_lightbox.php'; ?>