用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:
- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
(实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
金额一律以「分」存,不用浮点。
验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
187 lines
8.2 KiB
PHP
187 lines
8.2 KiB
PHP
<?php
|
|
/**
|
|
* 旅程详情(v12):封面 + 小结 + 同行的人 + 照片 + 行程节点(计划 / 执行双轨)。
|
|
*
|
|
* ⚠️ 双轨的意义:出发前排的是「打算怎么走」,走完记的是「实际怎么走的」。
|
|
* 两轨并存才能回看"本来想去哪、后来去了哪"—— 这是 TripLog 最有价值的一点,搬过来时保留了。
|
|
*/
|
|
$canWrite = \App\Auth::canWrite();
|
|
$plan = [];
|
|
$reality = [];
|
|
foreach ((array)($nodes ?? []) as $n) {
|
|
if (($n['category'] ?? 'plan') === 'reality') {
|
|
$reality[] = $n;
|
|
} else {
|
|
$plan[] = $n;
|
|
}
|
|
}
|
|
$memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'id'));
|
|
?>
|
|
<div class="page-head">
|
|
<h1><?= e((string)$t['title']) ?></h1>
|
|
<span class="sub"><?= e(\App\Trip::formatSpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
|
<div class="actions">
|
|
<?php if ($canWrite): ?>
|
|
<a class="btn btn-ghost" href="<?= e(url('trips/edit', ['id' => (int)$t['id']])) ?>">编辑</a>
|
|
<form method="post" action="<?= e(url('trips/destroy')) ?>"
|
|
onsubmit="return confirm('确定删除「<?= e((string)$t['title']) ?>」吗?照片与行程节点会一起删掉,不可恢复。')">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
|
<button class="btn btn-danger" type="submit">删除</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<a class="btn btn-ghost" href="<?= e(url('trips')) ?>">返回旅程</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-pad trip-hero">
|
|
<div class="trip-hero-cover">
|
|
<?php if (!empty($t['cover_key'])): ?>
|
|
<img src="<?= e(media_url((string)$t['cover_key'])) ?>" alt="">
|
|
<?php else: ?>
|
|
<span class="trip-cover-empty">🧳</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="trip-hero-body">
|
|
<?php if (!empty($t['location'])): ?>
|
|
<p class="trip-loc">📍 <?= e((string)$t['location']) ?></p>
|
|
<?php endif; ?>
|
|
<?php if (!empty($t['summary'])): ?>
|
|
<p class="info-text"><?= nl2br(e((string)$t['summary'])) ?></p>
|
|
<?php endif; ?>
|
|
<?php if (!empty($t['attendees'])): ?>
|
|
<div class="viewers-list">
|
|
<?php foreach ($t['attendees'] as $a): ?>
|
|
<?= person_chip($a, url('people/show', ['id' => (int)$a['id']])) ?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($t['note'])): ?>
|
|
<p class="remark-text"><?= nl2br(e((string)$t['note'])) ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php /* ---- 照片 ---- */ ?>
|
|
<div class="card card-pad">
|
|
<h2 class="cinema-h2">照片</h2>
|
|
<?php if (!$photos): ?>
|
|
<p class="empty-hint">还没有照片。</p>
|
|
<?php else: ?>
|
|
<div class="photo-grid">
|
|
<?php foreach ($photos as $ph): ?>
|
|
<figure class="photo-cell">
|
|
<?php /* v24 第 3 条:照片也在**本页**预览(原来 target=_blank 是另开一个标签页
|
|
只显示一张图,既没有下载按钮也不好回来看) */ ?>
|
|
<a href="<?= e(media_url((string)$ph['key'])) ?>"
|
|
data-prev="image"
|
|
data-src="<?= e(media_url((string)$ph['key'])) ?>"
|
|
data-dl="<?= e(media_url((string)$ph['key'], ['dl' => 1])) ?>"
|
|
data-name="<?= e((string)($ph['name'] ?? '照片')) ?>">
|
|
<img src="<?= e(media_url((string)$ph['key'])) ?>" alt="<?= e((string)($ph['name'] ?? '')) ?>">
|
|
</a>
|
|
<?php if ($canWrite): ?>
|
|
<form method="post" action="<?= e(url('trips/photo_del')) ?>" class="photo-del">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
|
<input type="hidden" name="photo_id" value="<?= (int)$ph['id'] ?>">
|
|
<button class="btn btn-ghost btn-sm" type="submit"
|
|
onclick="return confirm('删掉这张照片?')">删除</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</figure>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if ($canWrite): ?>
|
|
<form class="inline-form" method="post" enctype="multipart/form-data" action="<?= e(url('trips/photo_add')) ?>">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
|
<input type="file" name="photo" accept="image/*" multiple required>
|
|
<button class="btn btn-sm" type="submit">上传照片</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php /* ---- 行程节点(双轨)---- */ ?>
|
|
<?php foreach ([['plan', '计划行程', $plan], ['reality', '实际走过', $reality]] as [$cat, $catName, $list]): ?>
|
|
<div class="card card-pad">
|
|
<h2 class="cinema-h2"><?= e($catName) ?> <span class="group-n"><?= count($list) ?> 段</span></h2>
|
|
|
|
<?php if (!$list): ?>
|
|
<p class="empty-hint"><?= $cat === 'plan' ? '还没有排计划。' : '还没有记录实际行程。' ?></p>
|
|
<?php else: ?>
|
|
<ol class="node-list">
|
|
<?php foreach ($list as $n): ?>
|
|
<?php /* v24:日历里点「行程节点」是跳 #node-<id> 过来的,这里给个锚点好定位。 */ ?>
|
|
<li class="node-row status-<?= e((string)$n['status']) ?>" id="node-<?= (int)$n['id'] ?>">
|
|
<span class="node-ico" title="<?= e(\App\Trip::typeWord((string)$n['node_type'])) ?>">
|
|
<?= e(\App\Trip::typeIcon((string)$n['node_type'])) ?>
|
|
</span>
|
|
<div class="node-body">
|
|
<div class="node-head">
|
|
<b><?= e((string)$n['title']) ?></b>
|
|
<span class="node-badge"><?= e(\App\Trip::statusWord((string)$n['status'])) ?></span>
|
|
<?php if (!empty($n['place'])): ?>
|
|
<span class="node-place">📍 <?= e((string)$n['place']) ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php $span = \App\Trip::formatSpan($n['starts_at'] ?? null, $n['ends_at'] ?? null); ?>
|
|
<?php if ($span !== '日期待补'): ?>
|
|
<div class="node-time"><?= e($span) ?></div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($n['detail'])): ?>
|
|
<p class="node-detail"><?= nl2br(e((string)$n['detail'])) ?></p>
|
|
<?php endif; ?>
|
|
<?php if (!empty($n['members'])): ?>
|
|
<div class="trip-people">
|
|
<?php foreach ($n['members'] as $mm): ?><?= person_chip($mm) ?><?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($n['attachs'])): ?>
|
|
<?php
|
|
$attRows = $n['attachs'];
|
|
$attCanEdit = false; // 节点附件不单独给删除入口(防越权:得先判整趟旅程)
|
|
$attDelUrl = '';
|
|
require __DIR__ . '/../_attachments.php';
|
|
?>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php if ($canWrite): ?>
|
|
<div class="node-ops">
|
|
<details class="node-edit">
|
|
<summary class="link-quiet">编辑</summary>
|
|
<?php
|
|
$nfNode = $n; $nfTripId = (int)$t['id']; $nfCategory = $cat;
|
|
$nfPeople = $people ?? [];
|
|
$nfMembers = array_map('intval', array_column((array)($n['members'] ?? []), 'id'));
|
|
require __DIR__ . '/_node_form.php';
|
|
?>
|
|
</details>
|
|
<form method="post" action="<?= e(url('trips/node_del')) ?>"
|
|
onsubmit="return confirm('删掉这一段?')">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
|
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
|
|
<button class="btn btn-ghost btn-sm" type="submit">删除</button>
|
|
</form>
|
|
</div>
|
|
<?php endif; ?>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ol>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($canWrite): ?>
|
|
<details class="node-add">
|
|
<summary class="link-quiet">+ 加一段<?= e($catName) ?></summary>
|
|
<?php
|
|
$nfNode = null; $nfTripId = (int)$t['id']; $nfCategory = $cat;
|
|
$nfPeople = $people ?? []; $nfMembers = $memberIds;
|
|
require __DIR__ . '/_node_form.php';
|
|
?>
|
|
</details>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|