v24 第 16 批:旅程 / 行程两级(术语固定 · 题图16:9 · 五项统计 · 独立行程页 · 设置三页)
用户十条,逐条落地:
1. 「题图应该按照 16:9 显示,因为添加的时候是按这个比例裁剪的,现在的显示比例不对。」
→ 详情页题图容器从 4:3 改成 16:9,与 ImageSlot 的 cover 槽位同一个数字
(部署断言比对这两处,避免哪天改一处漏一处)。
2. 「题图的右侧,显示的是参与人都有谁、去了几个城市、一共去了几天、一共几段行程,
一共花了多少钱,这些信息,都从该活动下的所有行程统计总结,简介那一段话保留。」
→ 新增 Trip::summary():参与人(各段并集)/ 城市(去重)/ 天数 / 段数 / 总花销,
全部读时算(与"时间城市派生"同一条原则,不存冗余)。
天数按**日历天**含首尾(8/1→8/3 = 3 天,不是 24 小时整除的 2 天)。
城市与时间从标题旁挪进信息区,简介保留。
3. 「活动页面,不需要上传图片,图片视频,都是针对一次具体的行程的。」
→ 删掉旅程页的照片上传板块与照片墙;照片一律挂在**行程**上
(行程表单里有上传,附件 target_type='trip_node')。
4. 「计划行程、未成行的行程,都不要显示……只要是没有就是 0 条,那个类型就不显示了,
现在显示的是 0 条,这样没意义。」
→ 空分组直接跳过不渲染;全空时给一句引导(否则新建旅程页面一片空白)。
有内容的分组照常显示(计划 / 实际 / 未成行三种)。
5. 「行程的编辑功能,点击后会把页面撑破。编辑行程,打开新的页面进行编辑。」
→ 行程的新增与编辑改成**独立页面**(trips/node_new / trips/node_edit)。
原来内联在详情页:N 份表单互相撞元素 id、密码管理器逐个扫描变卡,
正是"把页面撑破"的成因。
6. 「添加计划行程和实际行程的按钮,都放在右上角。点击后打开新的页面进行添加。」
→ 详情页右上角两个按钮(+ 计划行程 / + 实际行程),指向独立新增页。
顺带:标题旁边不再放时间(时间移到题图右侧信息区)。
7. 「添加和编辑行程的内容,你去找 triplog 里面的添加节点功能,跟那个一样。」
→ 字段全量照搬 TripLog 的 nodes/form.php:
类型 / 标题 / 起止时间 / 出行方式 + 车辆 / 线路名 / 承运方 / 车次航班号 /
座位 / 登机口 / 出发地 / 到达地 / 内容 / 参与人(含角色)/ 照片 / 花销。
交通字段不做条件列(先填高铁车次、后来改成自驾,填过的东西不该丢)。
8. 「右上角的按钮,需要增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。
注意,我们在财务中,已经把车辆添加进去了,所以这里不用重复添加那里面的车辆,
直接读取就可以,但是可以添加其他的车辆,比如租的车,借别人的车。」
→ 新增旅程设置菜单(三页):车辆管理 / 出行方式 / 节点类型。
车辆**两个来源刻意不合并**:财务里 kind='car' 的账目(那是资产,没有车牌车型)
由 Vehicle::fromLedger() 只读列出,选车下拉里标「来自财务」;
自家 / 租的 / 借的车录进 vehicles 表。不复制财务的车(避免同一台车两处维护)。
节点类型做成**可自定义**的表(TripLog 同款),内置 5 种不可删;
行程里存类型**名字**,所以改类型名会级联更新已用它的行程。
9. 「活动都改名吧,改成旅程,新建旅程,旅程包含行程,这个名字就固定下来。」
→ 术语固定:**旅程**(容器)包含**行程**(单元)。
顶层按钮「新建旅程」、详情页三组叫「计划的行程 / 实际行程 / 未成行」、
列表「N 段行程」、设置菜单「旅程设置」。上一批的"活动"叫法全部换掉。
10. 「新建旅程里面,怎么还有备注啊,同行的人啊,这些都不要了,这些都是从行程里面
总结出来的。在新建旅程和编辑旅程的时候,都不要了。」
→ 旅程表单只剩:标题 → 题图 → 简介 + 公开开关。
备注与同行的人全部移除(参与人 = 各段行程参与人的合集)。
⚠️ 但**老数据里活动级那份参与人名单不能被清空**(那时还没有行程级参与人),
所以路由"没传这个键就不动",并加了断言钉住这一点。
数据结构(Schema v21,老库自动升级):
· 新表 vehicles(自家/租的/借的)、node_types(可自定义)、trip_expenses(行程级花销)
· trip_nodes 补 9 列:出行方式 / 车辆 / 线路名 / 承运方 / 车次 / 座位 / 登机口 / 起终点
· trip_node_members 补 role_tag(司机 / 摄影担当这类角色)
· 花销金额存**分**,解析走字符串拆小数点(128.55*100 在浮点下会差一分)
验证:e2e 891/0(+46 条本批断言)· 升级 134/0 · 桌面几何 428/0 · 交互 92/0 ·
窄屏溢出 30/0(扩到行程表单 + 设置三页 + 详情页)· 题图点击 4/0 · 静态体检全绿。
**反向验证 6/6**:题图退回 4:3、标题旁加回时间、空分组照旧渲染、编辑退回非独立页、
车辆不读财务、总花销不汇总 —— 六种坏法全部被断言抓住。
部署静态断言新增第 16 批一组(含"空分组不许渲染""行程页不许有照片上传"
"旅程表单不许有备注与同行的人""题图必须 16:9"等反退化防线)。
部署后服务器自检 19/19,六个关键文件 md5 与线上逐一核对一致。
This commit is contained in:
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 一段旅程的表单(新增 / 编辑共用;被 show.php 循环 require)。
|
||||
*
|
||||
* ⚠️ 术语(用户第 15 批 第 1 条):这是**一段旅程**;「计划 / 实际」是它的阶段。
|
||||
*
|
||||
* v24 第 13 批:旅程重构后,**不再分两条轨**,而是**一条记录 + 状态机**。
|
||||
* 用户原话:「这个实际的行程和计划行程,是一条记录,不是两条」。
|
||||
* 所以这里有「这是哪一类」的下拉:计划 / 实际
|
||||
* (新建时由用户选;编辑时是当前状态)。
|
||||
* 「修正」「标记为实际」「完成」「标记未成行」是**另外几个按钮**(在 show.php 上),
|
||||
* 不走这张表单 —— 它们的语义是"对既有旅程的操作",不是"填一遍字段"。
|
||||
*
|
||||
* 变量(require 前设好):
|
||||
* $nfNode 被编辑的节点数组,或 null(新增)
|
||||
* $nfTripId 活动 id(events.id)
|
||||
* $nfCategory 【兼容保留】'plan' | 'reality'(新版只用来定默认值)
|
||||
* $nfPeople 可选人名册(选参与人用),为空就不显示成员多选
|
||||
* $nfMembers 已选成员 id 列表
|
||||
*/
|
||||
$nfIsEdit = !empty($nfNode);
|
||||
$nfPrefix = $nfIsEdit ? 'ne' . (int)$nfNode['id'] : 'nn';
|
||||
// 默认状态:编辑时用它的 stage;新建时按调用方给的 $nfCategory 映射(兼容旧调用)
|
||||
$nfStage = $nfIsEdit
|
||||
? (string)($nfNode['stage'] ?? (($nfNode['category'] ?? 'plan') === 'reality' ? 'actual' : 'plan'))
|
||||
: ($nfCategory === 'reality' ? 'actual' : 'plan');
|
||||
?>
|
||||
<form class="node-form" method="post" action="<?= e(url($nfIsEdit ? 'trips/node_update' : 'trips/node_add')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$nfTripId ?>">
|
||||
<?php /* v24 第 13 批:新建时选「计划 / 实际」——用户原话第 2 条明确要求。
|
||||
一旦创建后要改,走行程上那三个按钮(修正/完成/标记未成行),不在这张表里。 */ ?>
|
||||
<input type="hidden" name="stage" value="<?= e($nfStage) ?>">
|
||||
<?php if ($nfIsEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$nfNode['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="nf-grid">
|
||||
<div class="field">
|
||||
<label for="<?= e($nfPrefix) ?>-title">这段旅程叫什么 *</label>
|
||||
<input class="input" id="<?= e($nfPrefix) ?>-title" name="title" required
|
||||
value="<?= e($nfNode['title'] ?? '') ?>" placeholder="如:栈桥看海">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="<?= e($nfPrefix) ?>-type">类型</label>
|
||||
<select class="select" id="<?= e($nfPrefix) ?>-type" name="node_type">
|
||||
<?php foreach (\App\Trip::NODE_TYPES as $tk => $tv): ?>
|
||||
<option value="<?= e($tk) ?>"<?= ($nfNode['node_type'] ?? 'other') === $tk ? ' selected' : '' ?>>
|
||||
<?= e($tv[1] . ' ' . $tv[0]) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if (!$nfIsEdit): ?>
|
||||
<div class="field">
|
||||
<label for="<?= e($nfPrefix) ?>-stage">阶段 *</label>
|
||||
<select class="select" id="<?= e($nfPrefix) ?>-stage" name="stage_pick">
|
||||
<option value="plan"<?= $nfStage === 'plan' ? ' selected' : '' ?>>计划(还没走)</option>
|
||||
<option value="actual"<?= $nfStage === 'actual' ? ' selected' : '' ?>>实际(已经走过)</option>
|
||||
</select>
|
||||
<span class="tip">计划走完后可用「修正 / 完成」把它转成实际。</span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="field">
|
||||
<label>当前状态</label>
|
||||
<div class="node-stage-ro"><?= e(\App\Trip::stageWord($nfStage)) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="field">
|
||||
<label for="<?= e($nfPrefix) ?>-place">地点</label>
|
||||
<input class="input" id="<?= e($nfPrefix) ?>-place" name="place"
|
||||
value="<?= e($nfNode['place'] ?? '') ?>" placeholder="选填">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="<?= e($nfPrefix) ?>-status">状态</label>
|
||||
<select class="select" id="<?= e($nfPrefix) ?>-status" name="status">
|
||||
<?php foreach (\App\Trip::NODE_STATUS as $sk => $sv): ?>
|
||||
<option value="<?= e($sk) ?>"<?= ($nfNode['status'] ?? 'planned') === $sk ? ' selected' : '' ?>>
|
||||
<?= e($sv) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php /* 用户 2026-10-04 第 7 条:节点的起止也是**一个范围**,一次选完 */ ?>
|
||||
<div class="field dt-range">
|
||||
<label>开始 / 结束</label>
|
||||
<?php $dtName = 'starts_at'; $dtValue = $nfNode['starts_at'] ?? '';
|
||||
$dtId = $nfPrefix . '-s'; $dtSeconds = true; $dtRange = 'start';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php $dtName = 'ends_at'; $dtValue = $nfNode['ends_at'] ?? '';
|
||||
$dtId = $nfPrefix . '-e'; $dtSeconds = true; $dtRange = 'end';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<span class="tip">结束时间可不填。</span>
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="<?= e($nfPrefix) ?>-detail">记录</label>
|
||||
<textarea class="input" id="<?= e($nfPrefix) ?>-detail" name="detail" rows="2"
|
||||
placeholder="这段旅程发生了什么"><?= e($nfNode['detail'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<?php if (!empty($nfPeople)): ?>
|
||||
<div class="field full">
|
||||
<label>这段旅程里有谁(不勾 = 整个活动同行的人)</label>
|
||||
<div class="nf-members">
|
||||
<?php foreach ($nfPeople as $p): ?>
|
||||
<label class="check-item">
|
||||
<input type="checkbox" name="members[]" value="<?= (int)$p['id'] ?>"
|
||||
<?= in_array((int)$p['id'], $nfMembers, true) ? 'checked' : '' ?>>
|
||||
<span><?= e(person_name($p)) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-sm" type="submit"><?= $nfIsEdit ? '保存' : '加这段旅程' ?></button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* 旅程模块的「设置」下拉(v24 第 16 批 用户第 8 条)。
|
||||
*
|
||||
* 用户原话:「右上角的按钮,需要增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。」
|
||||
*
|
||||
* 与其它模块同一套规矩(用户 2026-10-01 定的四条):
|
||||
* ① 按钮**没有文字**、只有图标,图标是**三条横线**;
|
||||
* ② 排在动作区**最右**(靠 .movie-set 的 margin-left:auto);
|
||||
* ③ 条目文字前都有图标;
|
||||
* ④ 除设置外其它按钮都有颜色。
|
||||
*
|
||||
* 用法(require 前 set 好):
|
||||
* $trSetCurrent 当前页 key('vehicles'|'modes'|'types'|'')
|
||||
*/
|
||||
$trSetCurrent = (string)($trSetCurrent ?? '');
|
||||
$trSetOn = static function (string $key) use ($trSetCurrent): string {
|
||||
return $trSetCurrent === $key ? ' class="on"' : '';
|
||||
};
|
||||
$trSetIcon = static function (string $name): string {
|
||||
$paths = [
|
||||
// 车辆:车头 + 两个轮
|
||||
'car' => '<path d="M2.5 9.5l1.2-3.2A1.4 1.4 0 0 1 5 5.4h6a1.4 1.4 0 0 1 1.3.9l1.2 3.2"/>'
|
||||
. '<path d="M1.8 9.5h12.4v3.2H1.8z"/><circle cx="4.4" cy="12.9" r="1.1"/>'
|
||||
. '<circle cx="11.6" cy="12.9" r="1.1"/>',
|
||||
// 出行方式:一个方向箭头
|
||||
'modes' => '<path d="M2.5 8h9"/><path d="M8.5 5l3 3-3 3"/>',
|
||||
// 节点类型:一排小方块
|
||||
'types' => '<rect x="2" y="2" width="5" height="5" rx="1"/>'
|
||||
. '<rect x="9" y="2" width="5" height="5" rx="1"/>'
|
||||
. '<rect x="2" y="9" width="5" height="5" rx="1"/>'
|
||||
. '<rect x="9" y="9" width="5" height="5" rx="1"/>',
|
||||
];
|
||||
$d = $paths[$name] ?? '';
|
||||
if ($d === '') {
|
||||
return '';
|
||||
}
|
||||
return '<svg class="mi" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"'
|
||||
. ' fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"'
|
||||
. ' stroke-linejoin="round">' . $d . '</svg>';
|
||||
};
|
||||
?>
|
||||
<details class="movie-set menu">
|
||||
<summary class="btn btn-icon" title="旅程设置" aria-label="旅程设置"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg></summary>
|
||||
<div class="menu-list">
|
||||
<a href="<?= e(url('trips/vehicles')) ?>"<?= $trSetOn('vehicles') ?>><?= $trSetIcon('car') ?><span>车辆管理</span></a>
|
||||
<a href="<?= e(url('trips/modes')) ?>"<?= $trSetOn('modes') ?>><?= $trSetIcon('modes') ?><span>出行方式</span></a>
|
||||
<a href="<?= e(url('trips/types')) ?>"<?= $trSetOn('types') ?>><?= $trSetIcon('types') ?><span>节点类型</span></a>
|
||||
</div>
|
||||
</details>
|
||||
<?php
|
||||
// ⚠️ 只 unset **本片段自己**定义的变量(前缀 trSet*),宿主模板的变量一个都不能碰
|
||||
unset($trSetCurrent, $trSetOn, $trSetIcon);
|
||||
+48
-105
@@ -1,48 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* 记一次活动 / 编辑活动(v24 第 15 批重排)。
|
||||
* 新建 / 编辑旅程(v24 第 16 批精简)。
|
||||
*
|
||||
* ⚠️ 用户原话(2026-10-05 第 15 批):
|
||||
* 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
|
||||
* 「活动不需要开始和结束时间,是根据活动中的旅程…计算出来的。目的地也不需要…
|
||||
* 去的城市,也是根据旅程,得到的。」
|
||||
* 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
|
||||
* ⚠️ 术语(用户第 9 条):这一层是**旅程**(容器),里面一段一段的是**行程**。
|
||||
*
|
||||
* 所以表单**主区就三样**(顺序就是用户说的顺序):
|
||||
* ① 标题 ② 题图(上传 + 16:9 裁剪)
|
||||
* ③ 简介
|
||||
* 时间 / 城市**不在表单里** —— 它们是"旅程算出来的",不是"这里填进去的"。
|
||||
* 页面上只做一句说明,让人知道这两个东西从哪来(不然会去找"怎么没地方填")。
|
||||
* 用户第 10 条原话:「新建旅程里面,怎么还有备注啊,同行的人啊,这些都不要了,
|
||||
* 这些都是从行程里面总结出来的。在新建旅程和编辑旅程的时候,都不要了。」
|
||||
* 上一批(第 4 条)已经定过主区就三项:标题 → 题图 → 简介。
|
||||
* 所以现在这张表单**只剩这三项 + 公开开关**:
|
||||
* · 同行的人 —— 删掉(参与人 = 各段行程参与人的合集,在详情页算出来)
|
||||
* · 备注 —— 删掉(用户明确"都不要了")
|
||||
* · 时间 / 城市 —— 本来就不是输入项(从行程派生)
|
||||
* 「公开」保留:它是"这个旅程要不要出现在列表与日历上"的**隐私开关**,
|
||||
* 不是从行程里能总结出来的信息。
|
||||
*
|
||||
* 「备注 / 公开 / 同行的人」功能没丢,收进「更多设置」折叠区:
|
||||
* 用户说的是"就可以了"(主流程够短),不是"把这些删掉"。
|
||||
* ⚠️ 但它们在同一个 <form> 里、只是视觉上折叠 —— 少了哪个字段都会静默丢数据。
|
||||
*
|
||||
* ⚠️ 同行的人:活动上登记的这份名单**仍然有用**(参与人 = 活动登记 ∪ 各段旅程成员)。
|
||||
* 但既然活动现在"没有时间也没有地点",多数人会直接在每段旅程里填成员;
|
||||
* 所以它退到折叠区,默认不占地方。
|
||||
* @param array|null $t 编辑时的旅程;null = 新建
|
||||
* @param array $errors 校验错误
|
||||
*/
|
||||
$isEdit = !empty($t);
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$selIds = [];
|
||||
if ($isEdit) {
|
||||
foreach ((array)($t['attendees'] ?? []) as $a) {
|
||||
$selIds[] = (int)$a['id'];
|
||||
}
|
||||
} else {
|
||||
$selIds = array_map('intval', (array)($presetAttendees ?? []));
|
||||
}
|
||||
$cvSlot = \App\ImageSlot::get('cover') ?: ['ratio' => '16:9', 'w' => 1600, 'h' => 900,
|
||||
'label' => '题图', 'hint' => ''];
|
||||
$hasCover = $isEdit && !empty($t['cover_key']);
|
||||
// 折叠区里"有没有填过东西" —— 有就默认展开,别让改过的人还要再点一次
|
||||
$moreOpen = $isEdit && (trim((string)($t['note'] ?? '')) !== ''
|
||||
|| (string)($t['is_public'] ?? '1') === '0'
|
||||
|| $selIds);
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1><?= $isEdit ? '编辑活动' : '记一次活动' ?></h1>
|
||||
<span class="sub">活动是容器;里面一段一段的旅程才是日程</span>
|
||||
<h1><?= $isEdit ? '编辑旅程' : '新建旅程' ?></h1>
|
||||
<span class="sub">旅程里装行程;参与人、城市、天数、花销都从行程里汇总</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost"
|
||||
href="<?= e($isEdit ? url('trips/show', ['id' => (int)$t['id']]) : url('trips')) ?>">返回</a>
|
||||
@@ -59,16 +42,15 @@ $moreOpen = $isEdit && (trim((string)($t['note'] ?? '')) !== ''
|
||||
|
||||
<?php /* ==================== ① 标题 ==================== */ ?>
|
||||
<div class="field full">
|
||||
<label for="t-title">这次活动叫什么 *</label>
|
||||
<label for="t-title">这个旅程叫什么 *</label>
|
||||
<input class="input" id="t-title" name="title" required autofocus
|
||||
value="<?= e(old('title', $t['title'] ?? '')) ?>" placeholder="如:青岛之行 · 2026 暑假">
|
||||
<?php if (!empty($errors['title'])): ?><span class="err"><?= e($errors['title']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ==================== ② 题图(上传 + 裁剪)====================
|
||||
⚠️ 用声明式绑定:`data-pick-input="#t-cover"` 让 app.js 的通用逻辑接管
|
||||
"点图 = 打开文件选择框"。上一批这里**没有**这段绑定(JS 只写在
|
||||
people/form.php 里),于是点图毫无反应 —— 用户当轮就报了这个问题。 */ ?>
|
||||
<?php /* ==================== ② 题图(上传 + 16:9 裁剪)====================
|
||||
⚠️ data-pick-input 让 app.js 的通用逻辑接管"点图 = 开文件框" ——
|
||||
少了它会"看着能点、点了没反应"(第 15 批用户报过的那个 bug)。 */ ?>
|
||||
<div class="field full">
|
||||
<label>题图</label>
|
||||
<div class="cover-picker">
|
||||
@@ -88,8 +70,7 @@ $moreOpen = $isEdit && (trim((string)($t['note'] ?? '')) !== ''
|
||||
<input type="hidden" name="cover_crop" value="">
|
||||
<span class="avatar-hint small muted">
|
||||
点左边那块选题图,选完弹裁剪框。<br>
|
||||
题图是 16:9 的宽幅横图,多大都能传,保存时自动缩成
|
||||
<?= (int)$cvSlot['w'] ?>×<?= (int)$cvSlot['h'] ?>。
|
||||
题图是 16:9 的宽幅横图,详情页也按 16:9 显示。
|
||||
<?php if ($hasCover): ?>不换图就不用管这里。<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
@@ -102,79 +83,41 @@ $moreOpen = $isEdit && (trim((string)($t['note'] ?? '')) !== ''
|
||||
placeholder="这趟玩得怎么样、为什么想记下来"><?= e(old('summary', $t['summary'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php if (!$isEdit): ?>
|
||||
<?php /* 时间与城市**不是输入项** —— 说清楚它们从哪来,
|
||||
否则用户会满页找"怎么没地方填时间"。 */ ?>
|
||||
<p class="form-note">
|
||||
时间与去过的城市不用填:加进一段段旅程之后,会自动取<b>最早的一段</b>、
|
||||
<b>最晚的一段</b>和<b>各段去的地方</b>算出来。
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<?php /* ---- 公开开关(隐私开关,不是"从行程总结"出来的信息)----
|
||||
⚠️ 先放 hidden=0:不勾选时提交的是"明确的不公开",而不是"这个字段没出现"
|
||||
(后者会被当成"没传"而默认公开,于是"取消勾选"看起来毫无作用)。 */ ?>
|
||||
<?php $isPub = old('is_public', (string)($t['is_public'] ?? 1)) !== '0'; ?>
|
||||
<div class="field full">
|
||||
<label>公开</label>
|
||||
<label class="chk-inline">
|
||||
<input type="hidden" name="is_public" value="0">
|
||||
<input type="checkbox" name="is_public" value="1" <?= $isPub ? 'checked' : '' ?>>
|
||||
<span>在旅程列表与日历里显示这个旅程</span>
|
||||
</label>
|
||||
<span class="tip">不勾 = 只留着自己看:不进列表、不上日历,但有链接仍能打开。</span>
|
||||
</div>
|
||||
|
||||
<?php if ($isEdit): ?>
|
||||
<?php
|
||||
/* 编辑时把"已经算出来什么"直接摆出来:
|
||||
不显示的话,改完标题保存会以为"我把时间弄丢了"。 */
|
||||
$tSpan = \App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null);
|
||||
$tCity = (string)($t['city'] ?? '');
|
||||
/* 编辑时把"已经算出来什么"摆出来:不显示的话,改完标题保存会以为
|
||||
"我把参与人/时间弄丢了"。 */
|
||||
$tSpan = \App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null);
|
||||
$tCity = (string)($t['city'] ?? '');
|
||||
$tN = count((array)($t['attendees'] ?? []));
|
||||
?>
|
||||
<p class="form-note">
|
||||
当前时间:<b><?= e($tSpan) ?></b>
|
||||
<?php if ($tCity !== ''): ?>· 去过:<b><?= e($tCity) ?></b><?php endif; ?>
|
||||
<br>这两样都是从下面各段旅程算出来的,改这里没用;要改就去改对应的那段旅程。
|
||||
当前:<b><?= e($tSpan) ?></b><?php if ($tCity !== ''): ?> · 去过 <b><?= e($tCity) ?></b><?php endif; ?>
|
||||
<?php if ($tN > 0): ?> · <b><?= $tN ?></b> 人参与<?php endif; ?>
|
||||
<br>这些都是从下面各段行程算出来的,这里改不了 —— 要改就去改对应的那段行程。
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="form-note">
|
||||
参与人、城市、天数、花销都不用填:加进一段段<b>行程</b>之后会自动汇总。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ==================== 更多设置(折叠)====================
|
||||
功能一个都没删,只是主流程只留用户点名的那三项。 */ ?>
|
||||
<details class="more-set"<?= $moreOpen ? ' open' : '' ?>>
|
||||
<summary>更多设置(备注 / 公开 / 同行的人)</summary>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field full">
|
||||
<label for="t-note">备注</label>
|
||||
<textarea class="input" id="t-note" name="note" rows="3"
|
||||
placeholder="选填。不显示在列表里,只在这趟活动的页面上"><?= e(old('note', $t['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php /* 用户 2026-10-03 第 4 条:不公开的活动只留着自己看。
|
||||
⚠️ 先放 hidden=0:不勾选时提交的是"明确的不公开",
|
||||
而不是"这个字段没出现"(后者会被当成"没传"而默认公开)。 */ ?>
|
||||
<?php $isPub = old('is_public', (string)($t['is_public'] ?? 1)) !== '0'; ?>
|
||||
<div class="field full">
|
||||
<label>公开</label>
|
||||
<label class="chk-inline">
|
||||
<input type="hidden" name="is_public" value="0">
|
||||
<input type="checkbox" name="is_public" value="1" <?= $isPub ? 'checked' : '' ?>>
|
||||
<span>在活动列表与日历里显示这次活动</span>
|
||||
</label>
|
||||
<span class="tip">不勾 = 只留着自己看:不进列表、不上日历,但有链接仍能打开。</span>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label>同行的人</label>
|
||||
<?php if (!$people): ?>
|
||||
<p class="empty-hint">名册里还没有人。</p>
|
||||
<?php else: ?>
|
||||
<span class="tip">
|
||||
这里登记的是"整趟活动都在的人"。只在某一段旅程里的人,
|
||||
到那段旅程里勾就行 —— 活动页上的名单是两边合起来算的。
|
||||
</span>
|
||||
<?php
|
||||
$ppName = 'attendees[]';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = $selIds;
|
||||
$ppMode = 'multi';
|
||||
$ppOpenBtn = '+ 添加同行人';
|
||||
$ppTitle = '选择同行的人';
|
||||
$ppTip = '';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit"><?= $isEdit ? '保存' : '记下来' ?></button>
|
||||
<button class="btn" type="submit"><?= $isEdit ? '保存' : '建好,去加行程' ?></button>
|
||||
<a class="btn btn-ghost"
|
||||
href="<?= e($isEdit ? url('trips/show', ['id' => (int)$t['id']]) : url('trips')) ?>">取消</a>
|
||||
</div>
|
||||
|
||||
+15
-15
@@ -1,26 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* 活动列表(v12 建立,v24 第 15 批把术语理清)。
|
||||
* 旅程列表(v12 建立;v24 第 16 批 第 9 条把术语**固定下来**)。
|
||||
*
|
||||
* ⚠️ 术语(用户 2026-10-05 第 15 批 第 1 条原话):
|
||||
* 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
|
||||
* 所以:这一页是**活动**的列表,一条活动里头才是若干段**旅程**。
|
||||
* 页面上的"趟"一律改成"次"(一次活动);"段行程"改成"段旅程"。
|
||||
* ⚠️ 术语(用户第 9 条原话):「活动都改名吧,改成旅程,新建旅程,旅程包含行程,
|
||||
* 这个名字就固定下来。」
|
||||
* → **旅程** = 这一页列的东西(容器);**行程** = 旅程里面的一段一段。
|
||||
* (第 15 批曾把容器叫"活动",本次按用户要求改成"旅程"。)
|
||||
*
|
||||
* 时间与城市都是从各段旅程**派生**的(用户第 2 条)——
|
||||
* 列表里读的是 Trip::attachDerived() 挂上来的值,不是活动自己的字段。
|
||||
* 时间与城市都是从各段**行程**派生出来的(第 15 批第 2 条)——
|
||||
* 列表里读的是 Trip::attachDerived() 挂上来的值,不是旅程自己的字段。
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>活动</h1>
|
||||
<h1>旅程</h1>
|
||||
<?php /* 导航条上这个模块仍叫「旅程」(用户就是这么称呼它的),
|
||||
但这一页列的顶层单位是**活动** —— 一句副标题把两级关系说清楚,
|
||||
否则"导航写旅程、页面写活动"会让人以为点错了地方。 */ ?>
|
||||
<span class="sub">共 <?= (int)$total ?> 次 · 每次活动里可以有若干段旅程</span>
|
||||
<span class="sub">共 <?= (int)$total ?> 个 · 每个旅程里可以有若干段行程</span>
|
||||
<div class="actions">
|
||||
<?php if ($canWrite): ?>
|
||||
<a class="btn" href="<?= e(url('trips/new')) ?>">记一次活动</a>
|
||||
<a class="btn" href="<?= e(url('trips/new')) ?>">新建旅程</a>
|
||||
<?php else: ?>
|
||||
<span class="link-quiet">只读账号</span>
|
||||
<?php endif; ?>
|
||||
@@ -29,7 +29,7 @@ $canWrite = \App\Auth::canWrite();
|
||||
|
||||
<form class="search" method="get" action="index.php">
|
||||
<input type="hidden" name="r" value="trips">
|
||||
<input class="input" type="text" name="keyword" value="<?= e($keyword) ?>" placeholder="搜活动名 / 去过的城市 / 备注">
|
||||
<input class="input" type="text" name="keyword" value="<?= e($keyword) ?>" placeholder="搜旅程名 / 去过的城市">
|
||||
<button class="btn" type="submit">搜索</button>
|
||||
<?php if ($keyword !== ''): ?>
|
||||
<a class="search-clear" href="<?= e(url('trips')) ?>">清除</a>
|
||||
@@ -38,10 +38,10 @@ $canWrite = \App\Auth::canWrite();
|
||||
|
||||
<?php if (!$rows): ?>
|
||||
<div class="card card-pad empty">
|
||||
<b><?= $keyword !== '' ? '没有匹配的活动' : '还没有活动记录' ?></b>
|
||||
<b><?= $keyword !== '' ? '没有匹配的旅程' : '还没有旅程' ?></b>
|
||||
<?= $keyword !== '' ? '换个关键词试试,或清除搜索看全部。'
|
||||
: ($canWrite ? '点右上角「记一次活动」,把一次出行记下来;进去之后再加一段段旅程。'
|
||||
: '家人还没有记录活动。') ?>
|
||||
: ($canWrite ? '点右上角「新建旅程」,把一次出行记下来;进去之后再加一段段行程。'
|
||||
: '家人还没有记录旅程。') ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="trip-list">
|
||||
@@ -62,7 +62,7 @@ $canWrite = \App\Auth::canWrite();
|
||||
<?php if (!empty($t['city'])): ?>
|
||||
<span>📍 <?= e((string)$t['city']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span><?= (int)($t['node_count'] ?? 0) ?> 段旅程</span>
|
||||
<span><?= (int)($t['node_count'] ?? 0) ?> 段行程</span>
|
||||
</div>
|
||||
<?php if (!empty($t['summary'])): ?>
|
||||
<p class="trip-sum"><?= e(mb_substr((string)$t['summary'], 0, 80, 'UTF-8')) ?></p>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* 出行方式设置(v24 第 16 批 用户第 8 条)。
|
||||
*
|
||||
* ⚠️ 这里**不是**"增删交通方式"(那是固定的物理事实,写死在 Trip::TRAVEL_MODES),
|
||||
* 而是"每种方式下我常填什么"的**记忆**:
|
||||
* 比如常坐的车次、常用航空公司、车牌默认选哪台。
|
||||
* 用户原话要的是"设置出行方式",实际需求是"别每次重新填一遍"。
|
||||
*
|
||||
* @param array $modes Trip::TRAVEL_MODES
|
||||
* @param array $memo 已记住的默认值(key => 值)
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$modes = $modes ?? [];
|
||||
$memo = $memo ?? [];
|
||||
$back = url('trips');
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>出行方式</h1>
|
||||
<span class="sub">交通方式是固定的;这里设的是"常填什么",省得每次重打</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e($back) ?>">返回旅程</a>
|
||||
<?php $trSetCurrent = 'modes'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$canWrite): ?>
|
||||
<div class="card card-pad"><p class="empty-hint">只读账号不能改设置。</p></div>
|
||||
<?php else: ?>
|
||||
<form class="card card-pad" method="post" action="<?= e(url('trips/modes_save')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<h2 class="cinema-h2">
|
||||
常用信息
|
||||
<span class="hint">填了以后,新建行程选到这个方式时会自动带上</span>
|
||||
</h2>
|
||||
|
||||
<div class="mode-grid">
|
||||
<?php foreach ($modes as $mk => $mv): ?>
|
||||
<div class="mode-row">
|
||||
<span class="mode-ico"><?= e($mv[1]) ?></span>
|
||||
<span class="mode-name"><?= e($mv[0]) ?></span>
|
||||
<?php /* 只给"有可记的东西"的方式一个输入框:走路的"常用信息"没有意义 */ ?>
|
||||
<?php if (in_array($mk, ['train', 'plane', 'ship', 'bus'], true)): ?>
|
||||
<input class="input" name="memo[<?= e($mk) ?>]" maxlength="60"
|
||||
value="<?= e((string)($memo[$mk] ?? '')) ?>"
|
||||
placeholder="<?= $mk === 'plane' ? '常坐的航空公司,如 国航' : '常坐的承运方 / 车次前缀' ?>">
|
||||
<?php elseif ($mk === 'drive'): ?>
|
||||
<input class="input" name="memo[<?= e($mk) ?>]" maxlength="60"
|
||||
value="<?= e((string)($memo[$mk] ?? '')) ?>"
|
||||
placeholder="默认车辆名(空 = 用车辆库里那台「默认」)">
|
||||
<?php else: ?>
|
||||
<span class="mode-none small muted">无需设置</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">这些方式在哪用</h2>
|
||||
<p class="small muted" style="margin-top:0">
|
||||
在「编辑行程 / 添加行程」里选出行方式。
|
||||
选了「自驾」还会多出一个车辆下拉 —— 里面既有<b>财务里登记过的车</b>,
|
||||
也有「车辆管理」里自己加的车(租的、借的)。
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
/**
|
||||
* 一段行程的新增 / 编辑 —— **独立页面**(v24 第 16 批 用户第 5 / 6 / 7 条)。
|
||||
*
|
||||
* 用户原话:
|
||||
* · 第 5 条:「行程的编辑功能,点击后会把页面撑破。编辑行程,打开新的页面进行编辑。」
|
||||
* · 第 6 条:「添加计划行程和实际行程的按钮,都放在右上角。点击后打开新的页面进行添加。」
|
||||
* · 第 7 条:「添加和编辑行程的内容,你去找 triplog 里面的添加节点功能,跟那个一样。」
|
||||
*
|
||||
* ⚠️ 为什么必须独立成页(而不是继续用 `<details>` 内联):
|
||||
* TripLog 的节点表单有十几组字段(类型/时间/出行方式/交通细节/起终点/内容/
|
||||
* 参与人/照片/状态),内联在主页面里会把整个详情页撑得极长;
|
||||
* 更要命的是**编辑**时每个行程内联一份表单 → 页面上 N 份表单互相干扰
|
||||
* (元素 id 撞车、密码管理器逐个扫描变卡)。这正是用户说的"把页面撑破"。
|
||||
*
|
||||
* 字段顺序照 TripLog 的 nodes/form.php:
|
||||
* 类型 → 起止时间 → 标题 → 出行方式(+车辆+交通细节)→ 起点 → 终点
|
||||
* → 内容 → 参与人 → 照片 →(编辑时)状态与花销。
|
||||
*
|
||||
* 变量(require 前设好):
|
||||
* $ev 所属旅程(events 行,带 title)
|
||||
* $n 被编辑的行程;null = 新增
|
||||
* $stage 'plan'|'actual'(新增时由按钮决定;编辑时取行程自己的)
|
||||
* $people 名册
|
||||
* $types 节点类型(NodeType::all)
|
||||
* $vehicles 可挑的车(Vehicle::pickable,含"来自财务"的只读候选)
|
||||
* $expenses 已有花销(编辑时)
|
||||
* $errors 校验错误
|
||||
*/
|
||||
$isEdit = !empty($n);
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$fid = (int)\App\Auth::familyId();
|
||||
$people = $people ?? [];
|
||||
$types = $types ?? [];
|
||||
$vehicles = $vehicles ?? [];
|
||||
$expenses = $expenses ?? [];
|
||||
$errors = $errors ?? [];
|
||||
|
||||
$stage = (string)($stage ?? ($n['stage'] ?? 'plan'));
|
||||
if (!isset(\App\Trip::NODE_STAGES[$stage])) { $stage = 'plan'; }
|
||||
$curType = (string)($n['node_type'] ?? '');
|
||||
$curMode = (string)($n['travel_mode'] ?? '');
|
||||
$picked = array_map('intval', array_column((array)($n['members'] ?? []), 'id'));
|
||||
|
||||
// 参与人角色:person_id => role
|
||||
$roles = [];
|
||||
foreach ((array)($n['members'] ?? []) as $m) {
|
||||
if (!empty($m['role_tag'])) {
|
||||
$roles[(int)$m['id']] = (string)$m['role_tag'];
|
||||
}
|
||||
}
|
||||
$back = url('trips/show', ['id' => (int)$ev['id']]);
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>
|
||||
<?= $isEdit ? '编辑行程' : ('添加' . ($stage === 'actual' ? '实际' : '计划') . '行程') ?>
|
||||
</h1>
|
||||
<span class="sub">
|
||||
<?= e((string)$ev['title']) ?>
|
||||
<?php if ($isEdit): ?> · <?= e(\App\Trip::stageWord((string)($n['stage'] ?? $stage))) ?><?php endif; ?>
|
||||
</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e($back) ?>">返回旅程</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<form method="post" enctype="multipart/form-data"
|
||||
action="<?= e(url($isEdit ? 'trips/node_update' : 'trips/node_add')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$ev['id'] ?>">
|
||||
<?php /* 新增时阶段由按钮决定;编辑时是"当前状态"(只读展示,改状态走详情页那几个动作) */ ?>
|
||||
<input type="hidden" name="stage" value="<?= e($stage) ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid">
|
||||
<?php /* ---- 类型(TripLog 的第一个字段)---- */ ?>
|
||||
<div class="field">
|
||||
<label for="nf-type">类型 *</label>
|
||||
<select class="select" id="nf-type" name="node_type">
|
||||
<?php foreach ($types as $t): ?>
|
||||
<?php $tn = (string)$t['name']; ?>
|
||||
<option value="<?= e($tn) ?>"<?= $curType === $tn ? ' selected' : '' ?>>
|
||||
<?= e(($t['icon'] ?? '') . ' ' . $tn) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php /* 老数据的英文 key 不在类型表里 → 补一个当前值,否则一保存就被改成别的 */ ?>
|
||||
<?php if ($curType !== '' && !in_array($curType, array_column($types, 'name'), true)): ?>
|
||||
<option value="<?= e($curType) ?>" selected>
|
||||
<?= e(\App\Trip::typeIcon($curType, $fid) . ' ' . \App\Trip::typeWord($curType, $fid)) ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<span class="tip">在右上角「设置 → 节点类型」里可以增删自定义。</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>当前状态</label>
|
||||
<div class="node-stage-ro"><?= e(\App\Trip::stageWord($stage)) ?></div>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 标题 ---- */ ?>
|
||||
<div class="field full">
|
||||
<label for="nf-title">标题 *</label>
|
||||
<input class="input" id="nf-title" name="title" required
|
||||
value="<?= e(old('title', $n['title'] ?? '')) ?>" placeholder="例如:出发去栈桥">
|
||||
<?php if (!empty($errors['title'])): ?><span class="err"><?= e($errors['title']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 起止时间:一个范围控件 ---- */ ?>
|
||||
<div class="field dt-range full">
|
||||
<label>开始 / 结束</label>
|
||||
<?php $dtName = 'starts_at'; $dtValue = $n['starts_at'] ?? '';
|
||||
$dtId = 'nf-s'; $dtSeconds = true; $dtRange = 'start';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php $dtName = 'ends_at'; $dtValue = $n['ends_at'] ?? '';
|
||||
$dtId = 'nf-e'; $dtSeconds = true; $dtRange = 'end';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<span class="tip">结束时间可不填。</span>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 出行方式 + 车辆(TripLog 的 travel_modes)----
|
||||
用户第 8 条:车辆从财务里**读**,也可以加自己的(租的 / 借的)。 */ ?>
|
||||
<div class="field">
|
||||
<label for="nf-mode">出行方式</label>
|
||||
<select class="select" id="nf-mode" name="travel_mode" data-mode-select>
|
||||
<option value="">— 不填 —</option>
|
||||
<?php foreach (\App\Trip::TRAVEL_MODES as $mk => $mv): ?>
|
||||
<option value="<?= e($mk) ?>"<?= $curMode === $mk ? ' selected' : '' ?>>
|
||||
<?= e($mv[1] . ' ' . $mv[0]) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field" data-mode-vehicle hidden>
|
||||
<label for="nf-veh">车辆</label>
|
||||
<select class="select" id="nf-veh" name="vehicle_id">
|
||||
<option value="">— 不指定 —</option>
|
||||
<?php
|
||||
$curVeh = (int)($n['vehicle_id'] ?? 0);
|
||||
$ledgerShown = false;
|
||||
foreach ($vehicles as $v):
|
||||
$isLedger = (($v['from'] ?? '') === 'ledger');
|
||||
if ($isLedger && !$ledgerShown) {
|
||||
echo '<optgroup label="财务里已登记的车">';
|
||||
$ledgerShown = true;
|
||||
}
|
||||
?>
|
||||
<option value="<?= $isLedger ? (int)$v['id'] : (int)$v['id'] ?>"
|
||||
<?= $curVeh === (int)$v['id'] ? ' selected' : '' ?>>
|
||||
<?= e(\App\Vehicle::label($v) . ($isLedger ? '(来自财务)' : '')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($ledgerShown): ?></optgroup><?php endif; ?>
|
||||
</select>
|
||||
<span class="tip">
|
||||
财务里登记过的车子会直接列在这里;租的车、借别人的车到
|
||||
「设置 → 车辆管理」加一台。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 交通细节(照 TripLog:线路名 / 车次 / 承运方 / 座位 / 登机口)---- */ ?>
|
||||
<div class="field">
|
||||
<label for="nf-line">线路名</label>
|
||||
<input class="input" id="nf-line" name="line_name"
|
||||
value="<?= e(old('line_name', $n['line_name'] ?? '')) ?>" placeholder="如:京沪高铁、地铁 3 号线">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="nf-tripno">车次 / 航班号</label>
|
||||
<input class="input" id="nf-tripno" name="trip_no"
|
||||
value="<?= e(old('trip_no', $n['trip_no'] ?? '')) ?>" placeholder="如:G1234、CA1234">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="nf-carrier">承运方</label>
|
||||
<input class="input" id="nf-carrier" name="carrier"
|
||||
value="<?= e(old('carrier', $n['carrier'] ?? '')) ?>" placeholder="如:中国铁路、滴滴">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="nf-seat">座位 / 车厢</label>
|
||||
<input class="input" id="nf-seat" name="seat_info"
|
||||
value="<?= e(old('seat_info', $n['seat_info'] ?? '')) ?>" placeholder="如:7 车 12F">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="nf-gate">登机口 / 站台</label>
|
||||
<input class="input" id="nf-gate" name="gate"
|
||||
value="<?= e(old('gate', $n['gate'] ?? '')) ?>" placeholder="如:T3 登机口 B12">
|
||||
</div>
|
||||
|
||||
<?php /* ---- 起点 / 终点(TripLog 的 start/end_location)---- */ ?>
|
||||
<div class="field">
|
||||
<label for="nf-from">出发地</label>
|
||||
<input class="input" id="nf-from" name="from_place"
|
||||
value="<?= e(old('from_place', $n['from_place'] ?? '')) ?>" placeholder="如:家里">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="nf-to">到达地</label>
|
||||
<input class="input" id="nf-to" name="to_place"
|
||||
value="<?= e(old('to_place', $n['to_place'] ?? '')) ?>" placeholder="如:青岛北站">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="nf-place">地点</label>
|
||||
<input class="input" id="nf-place" name="place"
|
||||
value="<?= e(old('place', $n['place'] ?? '')) ?>" placeholder="这一段主要在哪(用于统计城市)">
|
||||
<span class="tip">旅程页上的"去了几个城市"就是按这里统计的。</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="nf-status">执行状态</label>
|
||||
<select class="select" id="nf-status" name="status">
|
||||
<?php foreach (\App\Trip::NODE_STATUS as $sk => $sv): ?>
|
||||
<option value="<?= e($sk) ?>"<?= ($n['status'] ?? 'planned') === $sk ? ' selected' : '' ?>>
|
||||
<?= e($sv) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="nf-detail">记录</label>
|
||||
<textarea class="input" id="nf-detail" name="detail" rows="4"
|
||||
placeholder="这一段发生了什么"><?= e(old('detail', $n['detail'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 参与人(+角色,TripLog 的 role_tag)---- */ ?>
|
||||
<div class="field full">
|
||||
<label>参与的人</label>
|
||||
<?php if (!$people): ?>
|
||||
<p class="empty-hint">名册里还没有人。</p>
|
||||
<?php else: ?>
|
||||
<div class="nf-members">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php $pid = (int)$p['id']; ?>
|
||||
<label class="check-item">
|
||||
<input type="checkbox" name="members[]" value="<?= $pid ?>"
|
||||
<?= in_array($pid, $picked, true) ? 'checked' : '' ?>>
|
||||
<span><?= e(person_name($p)) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="role-set">
|
||||
<summary class="link-quiet">给参与的人标个角色(可选)</summary>
|
||||
<div class="role-grid">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php $pid = (int)$p['id']; ?>
|
||||
<div class="role-row">
|
||||
<span class="role-who"><?= e(person_name($p)) ?></span>
|
||||
<input class="input" name="roles[<?= $pid ?>]" maxlength="30"
|
||||
value="<?= e($roles[$pid] ?? '') ?>" placeholder="如:司机 / 摄影">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<span class="tip">不填也没关系;填了会显示在那个人名字旁边。</span>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 照片(用户第 3 条:图片视频都针对一次具体行程)---- */ ?>
|
||||
<div class="field full">
|
||||
<label for="nf-photo">这一段行程的照片</label>
|
||||
<?php if ($isEdit && !empty($n['attachs'])): ?>
|
||||
<div class="photo-grid">
|
||||
<?php foreach ($n['attachs'] as $ph): ?>
|
||||
<figure class="photo-cell">
|
||||
<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="">
|
||||
</a>
|
||||
<form method="post" action="<?= e(url('trips/photo_del')) ?>" class="photo-del">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$ev['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>
|
||||
</figure>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input class="input" type="file" id="nf-photo" name="photo[]" accept="image/*" multiple>
|
||||
<span class="tip">可多选;图片、视频都会存到这段行程下面。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit"><?= $isEdit ? '保存' : '加这段行程' ?></button>
|
||||
<a class="btn btn-ghost" href="<?= e($back) ?>">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php /* ==================== 花销(只在编辑时出现)====================
|
||||
用户第 2 条要"一共花了多少钱"、本次确认「行程级花销记录」。
|
||||
⚠️ 新增时不给花销面板:那时行程 id 还不存在,花销挂不上(先存行程再加花销,
|
||||
少一种"提交了但没存上"的状态)。 */ ?>
|
||||
<?php if ($isEdit): ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
花销(<?= count($expenses) ?> 笔)
|
||||
<span class="hint">这一段花了多少钱;旅程页的"总花销"是各段之和</span>
|
||||
</h2>
|
||||
|
||||
<?php if (!$expenses): ?>
|
||||
<p class="empty-hint">还没记花销。</p>
|
||||
<?php else: ?>
|
||||
<ul class="exp-list">
|
||||
<?php foreach ($expenses as $ex): ?>
|
||||
<li class="exp-row">
|
||||
<span class="exp-cat"><?= e(\App\Expense::CATS[(string)$ex['category']] ?? '其他') ?></span>
|
||||
<span class="exp-title"><?= e((string)($ex['title'] ?? '')) ?></span>
|
||||
<?php if (!empty($ex['person_name'])): ?>
|
||||
<span class="exp-who small muted"><?= e((string)$ex['person_name']) ?> 付</span>
|
||||
<?php endif; ?>
|
||||
<span class="exp-amt"><?= e(\App\Expense::money((int)$ex['amount_cents'])) ?></span>
|
||||
<form method="post" action="<?= e(url('trips/expense_del')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="node_id" value="<?= (int)$n['id'] ?>">
|
||||
<input type="hidden" name="expense_id" value="<?= (int)$ex['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这笔花销?')">删</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="exp-total">合计 <b><?= e(\App\Expense::money(\App\Expense::sumOfNode((int)$n['id']))) ?></b></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?= e(url('trips/expense_add')) ?>" class="exp-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="node_id" value="<?= (int)$n['id'] ?>">
|
||||
<div class="exp-form-row">
|
||||
<input class="input" name="amount" required placeholder="金额,如 128.50" inputmode="decimal">
|
||||
<select class="select" name="category" aria-label="类别">
|
||||
<?php foreach (\App\Expense::CATS as $ck => $cv): ?>
|
||||
<option value="<?= e($ck) ?>"><?= e($cv) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php /* ⚠️ 「谁付的」也是选人 —— 必须走**全局选人组件**(v24 第 8 条:
|
||||
全站的选人一律弹窗 + 头像 + 拼音联想,不许再写裸 <select>)。
|
||||
裸下拉的判据在部署断言里,写完空跑会直接红。 */ ?>
|
||||
<div class="exp-who">
|
||||
<?php
|
||||
$ppName = 'person_id';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = [];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '谁付的(可不选)';
|
||||
$ppTitle = '这笔是谁付的';
|
||||
$ppEmptyText = '不指定';
|
||||
$ppAllowEmpty = true;
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
</div>
|
||||
<input class="input" name="title" placeholder="花在哪(可空)">
|
||||
<button class="btn btn-sm" type="submit">加一笔</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
+198
-161
@@ -1,53 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* 活动详情(v12 建立,v24 第 15 批整理术语与派生值)。
|
||||
* 旅程详情(v24 第 16 批重做)。
|
||||
*
|
||||
* 页面结构:封面 + 简介 + 同行的人 + 照片 + 下面的若干段**旅程**。
|
||||
* ⚠️ 术语(用户第 9 条:「活动都改名吧,改成旅程,新建旅程,旅程包含行程,
|
||||
* 这个名字就固定下来」):
|
||||
* · **旅程** = 这一页(原来叫"活动")
|
||||
* · **行程** = 里面的一段一段(原来叫"旅程")
|
||||
*
|
||||
* ⚠️ 术语(用户 2026-10-05 第 15 批 第 1 条):这里是**一次活动**,
|
||||
* 里头一段一段的才是**旅程**。所以「+ 加一段计划行程」这类按钮
|
||||
* 统一改成「+ 加一段旅程」(阶段用「计划 / 实际」修饰)。
|
||||
* 页面结构(用户第 1/2/3/6 条):
|
||||
* ① 页头:标题 + 右上角按钮(计划行程 / 实际行程 / 设置 / 编辑 / 删除)
|
||||
* —— 标题旁边**不放时间**(第 6 条:时间移到内容里、题图右侧)
|
||||
* ② 封面行:左 16:9 题图 + 右侧信息区
|
||||
* 信息区 = 参与人 / 城市数 / 天数 / 行程段数 / 总花销 + 简介那一段话
|
||||
* ③ 行程分组:**只显示非空的分组**(第 4 条:"只要是没有就是 0 条,
|
||||
* 那个类型就不显示了,现在显示的是 0 条,这样没意义")
|
||||
* ④ **没有照片上传板块**(第 3 条:图片视频都针对一次具体行程)
|
||||
*
|
||||
* ⚠️ 时间与城市都来自 $t 上挂的**派生值**(Trip::derived(),用户第 2 条)——
|
||||
* 页面不再读 events.starts_at / location。
|
||||
* @param array $t 旅程(events + trip_details + 派生值)
|
||||
* @param array $nodes 全部行程(已带上成员/附件/花销)
|
||||
* @param array $sm 汇总信息(Trip::summary())
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
// v24 第 13 批:不再按 category 双轨分,按**状态机 stage** 分三组展示。
|
||||
// 计划行程 / 实际行程 / 未成行(未成行的也要显示,用户明确要求"只不过要标记上")。
|
||||
$fid = (int)\App\Auth::familyId();
|
||||
$sm = $sm ?? [];
|
||||
$people = $sm['people'] ?? [];
|
||||
$cities = $sm['cities'] ?? [];
|
||||
$days = (int)($sm['days'] ?? 0);
|
||||
$nodeN = (int)($sm['nodes'] ?? 0);
|
||||
$cents = (int)($sm['cents'] ?? 0);
|
||||
|
||||
// 三组:计划 / 实际 / 未成行
|
||||
$bysStage = ['plan' => [], 'actual' => [], 'skipped' => []];
|
||||
foreach ((array)($nodes ?? []) as $n) {
|
||||
$sg = (string)($n['stage'] ?? (($n['category'] ?? 'plan') === 'reality' ? 'actual' : 'plan'));
|
||||
$sg = (string)($n['stage'] ?? 'plan');
|
||||
if (!isset($bysStage[$sg])) { $sg = 'plan'; }
|
||||
$bysStage[$sg][] = $n;
|
||||
}
|
||||
// 每条行程带上它的修正历史(模板里画"原计划 → 实际"的对比)
|
||||
foreach ($bysStage as $sg => &$lst) {
|
||||
foreach ($lst as &$nd) {
|
||||
$nd['revisions'] = \App\Trip::revisions((int)$nd['id']);
|
||||
}
|
||||
unset($nd);
|
||||
}
|
||||
unset($lst);
|
||||
$memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'id'));
|
||||
$groupMeta = [
|
||||
'plan' => ['计划的行程', '还没走 —— 可以修正 / 完成 / 标记未成行'],
|
||||
'actual' => ['实际行程', '已走过 —— 也出现在日历与首页「往年今日」'],
|
||||
'skipped' => ['未成行', '这次没去成;不上日程,但在这里留着可查'],
|
||||
];
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1><?= e((string)$t['title']) ?></h1>
|
||||
<?php /* 时间/城市都是派生的:一段旅程都没加时显示「还没有旅程」,不是「日期待补」 */ ?>
|
||||
<span class="sub"><?= e(\App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
||||
<?php /* ⚠️ 用户第 6 条:标题旁边**不放**时间。时间在下面题图右侧的信息区里。 */ ?>
|
||||
<div class="actions">
|
||||
<?php if ($canWrite): ?>
|
||||
<?php /* 用户第 6 条:添加计划行程 / 实际行程的按钮都放**右上角**,
|
||||
点击打开**新页面**(第 5 条:不再内联,内联会把页面撑破)。 */ ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('trips/node_new', ['id' => (int)$t['id'], 'stage' => 'plan'])) ?>">
|
||||
+ 计划行程
|
||||
</a>
|
||||
<a class="btn" href="<?= e(url('trips/node_new', ['id' => (int)$t['id'], 'stage' => 'actual'])) ?>">
|
||||
+ 实际行程
|
||||
</a>
|
||||
<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']) ?>」吗?里面的旅程、照片会一起删掉,不可恢复。')">
|
||||
<?php /* 用户第 8 条:右上角增加一个**设置**按钮(车辆管理 / 出行方式 / 节点类型)。
|
||||
按全站规矩:设置按钮只有图标(三条横线)、无文字、贴最右。 */ ?>
|
||||
<?php $trSetCurrent = ''; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
<form method="post" action="<?= e(url('trips/destroy')) ?>" class="res-inline"
|
||||
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>
|
||||
<button class="btn btn-ghost btn-icon" type="submit" title="删除这个旅程" aria-label="删除">🗑</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="link-quiet">只读账号</span>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('trips')) ?>">返回活动</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /* ==================== 封面行:题图 + 信息区 ====================
|
||||
⚠️ 第 1 条:「题图应该按照 16:9 显示,因为添加的时候,是按照这个比例裁剪的,
|
||||
现在的显示比例不对」—— 之前 CSS 写的是 4:3,把 16:9 的图裁掉两边。
|
||||
16:9 这个数字与 ImageSlot 里 cover 槽位的比例**必须一致**,
|
||||
部署断言里有一条专门比对它。 */ ?>
|
||||
<div class="card card-pad trip-hero">
|
||||
<div class="trip-hero-cover">
|
||||
<?php if (!empty($t['cover_key'])): ?>
|
||||
@@ -56,145 +84,173 @@ $memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'i
|
||||
<span class="trip-cover-empty">🧳</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="trip-hero-body">
|
||||
<?php /* 地名可能是好几个城市 → 用派生出来的列表逐颗画(用户第 2 条) */ ?>
|
||||
<?php if (!empty($t['cities'])): ?>
|
||||
<p class="trip-loc">
|
||||
<?php foreach ((array)$t['cities'] as $i => $city): ?>
|
||||
<?= $i > 0 ? '<span class="city-sep">·</span>' : '📍' ?>
|
||||
<span class="city-chip"><?= e((string)$city) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</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'])): ?>
|
||||
<?php /* v24 第 14 批 用户第 4 条:「一个活动的参与人,应该是这个活动下的
|
||||
所有旅程的参与人的合集」—— 这上面的名单是**每段行程参与人的并集**,
|
||||
在行程里加了人,这里自动出现,不用再到活动上登记一遍。 */ ?>
|
||||
<?php /* ---- 时间与城市:从行程算出来的(第 15 批第 2 条);
|
||||
第 6 条要求放在这里(题图右侧)而不是标题旁 ---- */ ?>
|
||||
<div class="trip-when">
|
||||
<span class="trip-when-date"><?= e(\App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
||||
<?php if ($cities): ?>
|
||||
<span class="trip-when-cities">
|
||||
<?php foreach ($cities as $i => $c): ?>
|
||||
<?= $i > 0 ? '<span class="city-sep">·</span>' : '' ?><span class="city-chip"><?= e((string)$c) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 五项统计(用户第 2 条):参与人 / 城市 / 天数 / 段数 / 花销 ---- */ ?>
|
||||
<div class="trip-stats">
|
||||
<div class="ts-item">
|
||||
<span class="ts-n"><?= count($people) ?></span>
|
||||
<span class="ts-l">参与人</span>
|
||||
</div>
|
||||
<div class="ts-item">
|
||||
<span class="ts-n"><?= count($cities) ?></span>
|
||||
<span class="ts-l">个城市</span>
|
||||
</div>
|
||||
<div class="ts-item">
|
||||
<span class="ts-n"><?= $days > 0 ? $days : '—' ?></span>
|
||||
<span class="ts-l">天</span>
|
||||
</div>
|
||||
<div class="ts-item">
|
||||
<span class="ts-n"><?= $nodeN ?></span>
|
||||
<span class="ts-l">段行程</span>
|
||||
</div>
|
||||
<div class="ts-item">
|
||||
<span class="ts-n"><?= e(\App\Expense::money($cents)) ?></span>
|
||||
<span class="ts-l">总花销</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /* 参与人是谁:用户点名要("显示的是参与人都有谁") */ ?>
|
||||
<?php if ($people): ?>
|
||||
<div class="trip-people-block">
|
||||
<span class="small muted">参与的人(各段行程的合集)</span>
|
||||
<div class="viewers-list">
|
||||
<?php foreach ($t['attendees'] as $a): ?>
|
||||
<?php foreach ($people as $a): ?>
|
||||
<?= person_chip($a, url('people/show', ['id' => (int)$a['id']])) ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ---- 简介:用户第 2 条明确"简介那一段话保留" ---- */ ?>
|
||||
<?php if (!empty($t['summary'])): ?>
|
||||
<p class="info-text trip-sum"><?= nl2br(e((string)$t['summary'])) ?></p>
|
||||
<?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 /* ==================== 这个活动里的旅程(v24 第 13 批重构)====================
|
||||
⚠️ 术语(用户第 15 批 第 1 条):这一块里每一条都是**一段旅程**,
|
||||
不是"行程";分三组是按它的**阶段**(计划 / 实际 / 未成行)。
|
||||
|
||||
用户 2026-10-05(第 13 批)原话要点:
|
||||
· 刚开始添加时选「计划 / 实际」
|
||||
· 计划的三段有三个按钮:删除(**标记未成行,不真删**)、修正(记实际执行,可无限次)、
|
||||
完成(实际=计划,直接变实际)
|
||||
· 未成行的仍显示、但带标记;不上日程、不进往年今日
|
||||
· 实际的会同时上日程 + 往年今日
|
||||
分三组渲染(未成行放最后,用灰调)。 */ ?>
|
||||
<?php /* ==================== 行程分组 ====================
|
||||
用户第 4 条:「只要是没有就是 0 条,那个类型就不显示了,现在显示的是 0 条,这样没意义。」
|
||||
⚠️ 所以这里**跳过空分组**。但要给"一段都还没有"的情况一句引导 ——
|
||||
否则新建的旅程页面上会一片空白,人不知道该干什么。 */ ?>
|
||||
<?php
|
||||
$stageMeta = [
|
||||
'plan' => ['计划的旅程', '还没走。每段下面的「修正 / 完成 / 标记未成行」都能用'],
|
||||
'actual' => ['走过的旅程', '已走过 —— 会出现在日历与首页「往年今日」'],
|
||||
'skipped' => ['没成行的', '这次没去成;不再出现在日程与「往年今日」,但在这里保留可查'],
|
||||
];
|
||||
foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
$list = $bysStage[$sg];
|
||||
$shown = 0;
|
||||
foreach ($bysStage as $lst) { $shown += count($lst); }
|
||||
?>
|
||||
<div class="card card-pad trip-stage-card stage-<?= e($sg) ?>">
|
||||
<h2 class="cinema-h2">
|
||||
<?= e($stageMeta[$sg][0]) ?>
|
||||
<span class="group-n"><?= count($list) ?> 段</span>
|
||||
</h2>
|
||||
<p class="stage-hint"><?= e($stageMeta[$sg][1]) ?></p>
|
||||
|
||||
<?php if (!$list): ?>
|
||||
<p class="empty-hint">
|
||||
<?= $sg === 'plan' ? '还没有排计划。点下面的「+ 加一段旅程」,把打算走的地方排进来。'
|
||||
: ($sg === 'actual' ? '还没有走过的旅程。' : '没有未成行的。') ?>
|
||||
</p>
|
||||
<?php if ($shown === 0): ?>
|
||||
<div class="card card-pad empty">
|
||||
<b>还没有行程</b>
|
||||
<?php if ($canWrite): ?>
|
||||
点右上角「+ 计划行程」把打算走的地方排进来,或者「+ 实际行程」直接记已经走过的。
|
||||
参与人、城市、天数、花销都会从行程里自动汇总到这里。
|
||||
<?php else: ?>
|
||||
家人还没有记录行程。
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach (['plan', 'actual', 'skipped'] as $sg): ?>
|
||||
<?php $list = $bysStage[$sg]; if (!$list) { continue; } /* 空分组不显示 */ ?>
|
||||
<div class="card card-pad trip-stage-card stage-<?= e($sg) ?>">
|
||||
<h2 class="cinema-h2">
|
||||
<?= e($groupMeta[$sg][0]) ?>
|
||||
<span class="group-n"><?= count($list) ?> 段</span>
|
||||
</h2>
|
||||
<p class="stage-hint"><?= e($groupMeta[$sg][1]) ?></p>
|
||||
|
||||
<ol class="node-list">
|
||||
<?php foreach ($list as $n): ?>
|
||||
<?php
|
||||
$nid = (int)$n['id'];
|
||||
$span = \App\Trip::formatSpan($n['starts_at'] ?? null, $n['ends_at'] ?? null);
|
||||
$mode = (string)($n['travel_mode'] ?? '');
|
||||
$spent = (int)($n['spent_cents'] ?? 0);
|
||||
?>
|
||||
<li class="node-row status-<?= e((string)$n['status']) ?> stage-<?= e($sg) ?>"
|
||||
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'])) ?>
|
||||
id="node-<?= $nid ?>">
|
||||
<span class="node-ico" title="<?= e(\App\Trip::typeWord((string)$n['node_type'], $fid)) ?>">
|
||||
<?= e(\App\Trip::typeIcon((string)$n['node_type'], $fid)) ?>
|
||||
</span>
|
||||
<div class="node-body">
|
||||
<div class="node-head">
|
||||
<b><?= e((string)$n['title']) ?></b>
|
||||
<span class="node-badge nb-<?= e($sg) ?>"><?= e(\App\Trip::stageWord($sg)) ?></span>
|
||||
<span class="node-badge nb-<?= e($sg) ?>"><?= e(\App\Trip::typeWord((string)$n['node_type'], $fid)) ?></span>
|
||||
<?php if ((int)($n['revision_n'] ?? 0) > 0): ?>
|
||||
<span class="rev-badge" title="已修正过多次,点下方「看修正记录」查看">已修正 <?= (int)$n['revision_n'] ?> 次</span>
|
||||
<span class="rev-badge" title="已修正过多次">已修正 <?= (int)$n['revision_n'] ?> 次</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($n['place'])): ?>
|
||||
<span class="node-place">📍 <?= e((string)$n['place']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($mode !== ''): ?>
|
||||
<span class="node-mode"><?= e(\App\Trip::modeIcon($mode)) ?> <?= e(\App\Trip::modeWord($mode)) ?></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) ?>
|
||||
<?php if ($sg === 'actual'): ?><span class="actual-tag">实际</span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* 交通细节(TripLog 同款):只显示**填过的**那几个,没填就不占地方 */ ?>
|
||||
<?php
|
||||
$traffic = [];
|
||||
if (!empty($n['line_name'])) { $traffic[] = (string)$n['line_name']; }
|
||||
if (!empty($n['trip_no'])) { $traffic[] = (string)$n['trip_no']; }
|
||||
if (!empty($n['carrier'])) { $traffic[] = (string)$n['carrier']; }
|
||||
if (!empty($n['seat_info'])) { $traffic[] = '座位 ' . (string)$n['seat_info']; }
|
||||
if (!empty($n['gate'])) { $traffic[] = '登机口/站台 ' . (string)$n['gate']; }
|
||||
?>
|
||||
<?php if ($traffic): ?>
|
||||
<div class="node-traffic">
|
||||
<?php foreach ($traffic as $tf): ?><span class="ntag"><?= e($tf) ?></span><?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($n['from_place']) || !empty($n['to_place'])): ?>
|
||||
<div class="node-route-line">
|
||||
<?= e((string)($n['from_place'] ?? '—')) ?> <span class="arrow">→</span> <?= e((string)($n['to_place'] ?? '—')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($n['detail'])): ?>
|
||||
<p class="node-detail"><?= nl2br(e((string)$n['detail'])) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($spent !== 0): ?>
|
||||
<div class="node-spent">💰 这一段花了 <?= e(\App\Expense::money($spent)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($n['members'])): ?>
|
||||
<div class="trip-people">
|
||||
<?php foreach ($n['members'] as $mm): ?><?= person_chip($mm) ?><?php endforeach; ?>
|
||||
<?php foreach ($n['members'] as $mm): ?>
|
||||
<span class="node-member">
|
||||
<?= person_chip($mm) ?>
|
||||
<?php if (!empty($mm['role_tag'])): ?>
|
||||
<span class="node-role"><?= e((string)$mm['role_tag']) ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($n['attachs'])): ?>
|
||||
<?php
|
||||
$attRows = $n['attachs'];
|
||||
@@ -204,7 +260,7 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ---- 修正记录:原计划 → 每次修正的对比 ---- */ ?>
|
||||
<?php /* ---- 修正记录对比(第 13 批) ---- */ ?>
|
||||
<?php if (!empty($n['revisions'])): ?>
|
||||
<details class="rev-hist">
|
||||
<summary class="link-quiet">看修正记录(<?= count($n['revisions']) ?> 条)</summary>
|
||||
@@ -229,7 +285,10 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<div class="node-ops">
|
||||
<?php /* ---- 计划行程:三个动作(用户点名的三个按钮)---- */ ?>
|
||||
<?php /* 用户第 5 条:**编辑行程打开新页面**(内联的编辑面板会把页面撑破) */ ?>
|
||||
<a class="btn btn-ghost btn-sm"
|
||||
href="<?= e(url('trips/node_edit', ['id' => $nid])) ?>">编辑</a>
|
||||
|
||||
<?php if ($sg === 'plan'): ?>
|
||||
<details class="rev-open">
|
||||
<summary class="btn btn-ghost btn-sm">修正</summary>
|
||||
@@ -237,7 +296,7 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
<p class="rev-tip">
|
||||
记录实际执行情况。这仍与上面那条计划是<b>同一条记录</b>,
|
||||
原来的计划数据会保留在「修正记录」里,方便日后对比。
|
||||
点完这里<b>这段旅程仍是计划</b>;确认无误后再点「标记为实际」。
|
||||
点完这里<b>这段行程仍是计划</b>;确认无误后再点「标记为实际」。
|
||||
</p>
|
||||
<?php
|
||||
$rvNode = $n; $rvTripId = (int)$t['id'];
|
||||
@@ -247,71 +306,49 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
?>
|
||||
</div>
|
||||
</details>
|
||||
<form method="post" action="<?= e(url('trips/node_done')) ?>"
|
||||
<form method="post" action="<?= e(url('trips/node_done')) ?>" class="res-inline"
|
||||
onsubmit="return confirm('实际执行与计划完全相同,直接转为实际?')">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<input type="hidden" name="id" value="<?= $nid ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
|
||||
<button class="btn btn-sm" type="submit">完成</button>
|
||||
</form>
|
||||
<form method="post" action="<?= e(url('trips/node_skip')) ?>"
|
||||
onsubmit="return confirm('标记为「未成行」?它不会再出现在日程与往年今日,但在这个活动里仍然看得到。')">
|
||||
<form method="post" action="<?= e(url('trips/node_skip')) ?>" class="res-inline"
|
||||
onsubmit="return confirm('标记为「未成行」?它不会再出现在日程与往年今日,但在这个旅程里仍然看得到。')">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<input type="hidden" name="id" value="<?= $nid ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">标记未成行</button>
|
||||
</form>
|
||||
<?php if ((int)($n['revision_n'] ?? 0) > 0): ?>
|
||||
<form method="post" action="<?= e(url('trips/node_actual')) ?>">
|
||||
<form method="post" action="<?= e(url('trips/node_actual')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<input type="hidden" name="id" value="<?= $nid ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
|
||||
<button class="btn btn-sm btn-primary" type="submit">标记为实际</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($sg === 'skipped'): ?>
|
||||
<form method="post" action="<?= e(url('trips/node_restore')) ?>">
|
||||
<form method="post" action="<?= e(url('trips/node_restore')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<input type="hidden" name="id" value="<?= $nid ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">恢复为计划</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<details class="node-edit">
|
||||
<summary class="link-quiet">编辑</summary>
|
||||
<?php
|
||||
$nfNode = $n; $nfTripId = (int)$t['id']; $nfCategory = $sg;
|
||||
$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('彻底删除这一段旅程?连同它的修正记录一起消失,不可恢复。\n\n如果你只是想表达「这次没去成」,请用「标记未成行」。')">
|
||||
<form method="post" action="<?= e(url('trips/node_del')) ?>" class="res-inline"
|
||||
onsubmit="return confirm('彻底删除这段行程?连同它的修正记录一起消失,不可恢复。\n\n如果你只是想表达「这次没去成」,请用「标记未成行」。')">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<input type="hidden" name="id" value="<?= $nid ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">彻底删除</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($canWrite && $sg !== 'skipped'): ?>
|
||||
<details class="node-add">
|
||||
<?php /* 术语:加的是"一段旅程",阶段只是它的属性 */ ?>
|
||||
<summary class="link-quiet">+ 加一段旅程(<?= $sg === 'actual' ? '实际' : '计划' ?>)</summary>
|
||||
<?php
|
||||
$nfNode = null; $nfTripId = (int)$t['id']; $nfCategory = $sg;
|
||||
$nfPeople = $people ?? []; $nfMembers = $memberIds;
|
||||
require __DIR__ . '/_node_form.php';
|
||||
?>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* 节点类型设置(v24 第 16 批 用户第 8 条)。
|
||||
*
|
||||
* 用户第 7 条要求照 TripLog 的添加节点功能,TripLog 的节点类型是**可自定义的**:
|
||||
* 内置几种(游玩/用餐/赶路/住宿/其他)+ 用户自己加。
|
||||
*
|
||||
* ⚠️ 内置的不许删(删了会被自动预置回来,看起来像"删不掉");
|
||||
* 还在被行程使用的也不许删(行程里存的是名字,删了那些行程就显示成"其他")。
|
||||
*
|
||||
* @param array $types NodeType::all()(带 used 计数)
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$types = $types ?? [];
|
||||
$back = url('trips');
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>节点类型</h1>
|
||||
<span class="sub">行程分几类,你自己定</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e($back) ?>">返回旅程</a>
|
||||
<?php $trSetCurrent = 'types'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$canWrite): ?>
|
||||
<div class="card card-pad"><p class="empty-hint">只读账号不能改设置。</p></div>
|
||||
<?php else: ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">现有类型</h2>
|
||||
<ul class="type-list">
|
||||
<?php foreach ($types as $t): ?>
|
||||
<li class="type-row">
|
||||
<form class="inline-form type-form" method="post" action="<?= e(url('trips/types_save')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
||||
<input class="input type-ico" name="icon" maxlength="4"
|
||||
value="<?= e((string)($t['icon'] ?? '')) ?>" aria-label="图标">
|
||||
<input class="input" name="name" value="<?= e((string)$t['name']) ?>"
|
||||
required aria-label="类型名">
|
||||
<?php if ((int)$t['is_system'] === 1): ?>
|
||||
<span class="type-tag">内置</span>
|
||||
<?php else: ?>
|
||||
<span class="type-tag type-tag-custom">自定义</span>
|
||||
<?php endif; ?>
|
||||
<span class="type-used small muted">
|
||||
<?= (int)($t['used'] ?? 0) ?> 段在用
|
||||
</span>
|
||||
<button class="btn btn-sm" type="submit">保存</button>
|
||||
</form>
|
||||
<?php if ((int)$t['is_system'] === 0): ?>
|
||||
<form class="inline-form" method="post" action="<?= e(url('trips/types_del')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉类型「<?= e((string)$t['name']) ?>」?')">删</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="small muted">
|
||||
内置的几种不能删(可以改名)。已经有行程在用的类型也不能删 ——
|
||||
先到那些行程里把它们改成别的类型。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">加一个类型</h2>
|
||||
<form class="inline-form type-form type-add" method="post" action="<?= e(url('trips/types_save')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input class="input type-ico" name="icon" maxlength="4" placeholder="🎣" aria-label="图标">
|
||||
<input class="input" name="name" placeholder="类型名,如「赶海」" required aria-label="类型名">
|
||||
<button class="btn btn-sm" type="submit">加进类型库</button>
|
||||
</form>
|
||||
<p class="tip">图标填一个表情符号就行(如 🎣 🏖 🍜)。</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* 车辆管理(v24 第 16 批 用户第 8 条)。
|
||||
*
|
||||
* 用户原话:「注意,我们在财务中,已经把车辆添加进去了,所以这里不用重复添加
|
||||
* 那里面的车辆,直接读取就可以,但是可以添加其他的车辆,
|
||||
* 比如租的车,借别人的车。」
|
||||
*
|
||||
* 所以这一页有两块:
|
||||
* ① **财务里已登记的车**(只读)—— 只列出来,不给改不给删(那是财务的数据);
|
||||
* ② **出行用的车**(本表)—— 自家 / 租的 / 借的,可增删改。
|
||||
*
|
||||
* @param array $ledger Vehicle::fromLedger() 的结果(只读)
|
||||
* @param array $mine Vehicle::all() 的结果
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$ledger = $ledger ?? [];
|
||||
$mine = $mine ?? [];
|
||||
$back = url('trips');
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>车辆管理</h1>
|
||||
<span class="sub">出行时能挑的车:自家的、租的、借别人的</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e($back) ?>">返回旅程</a>
|
||||
<?php $trSetCurrent = 'vehicles'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /* ---- ① 来自财务的车(只读引用)---- */ ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
财务里已登记的车
|
||||
<span class="hint">只读 —— 要改请到财务模块</span>
|
||||
</h2>
|
||||
<?php if (!$ledger): ?>
|
||||
<p class="empty-hint">
|
||||
财务里还没有登记车子。到「财务 → 记一笔」里把车子作为一项资产记下来,
|
||||
这里就会自动出现(<b>不必在这里重复添加</b>)。
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<ul class="veh-list">
|
||||
<?php foreach ($ledger as $v): ?>
|
||||
<li class="veh-row veh-ro">
|
||||
<span class="veh-ico">🚗</span>
|
||||
<span class="veh-name"><?= e((string)$v['name']) ?></span>
|
||||
<span class="veh-tag">来自财务</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="small muted">
|
||||
这些是财务里的**资产记录**(有一台车值多少钱),不一定有车牌与车型。
|
||||
出行要选车时它们也会出现在下拉里,标着「来自财务」。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ---- ② 出行用的车(本表)---- */ ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
出行用的车
|
||||
<span class="hint">自家车可以录一台方便选;租的车、借的车录在这里</span>
|
||||
</h2>
|
||||
|
||||
<?php if ($mine): ?>
|
||||
<ul class="veh-list">
|
||||
<?php foreach ($mine as $v): ?>
|
||||
<li class="veh-row">
|
||||
<span class="veh-ico"><?= ($v['vtype'] ?? 'own') === 'rental' ? '🔑' : (($v['vtype'] ?? 'own') === 'borrowed' ? '🤝' : '🚗') ?></span>
|
||||
<?php if ($canWrite): ?>
|
||||
<form class="inline-form veh-form" method="post" action="<?= e(url('trips/vehicles_save')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$v['id'] ?>">
|
||||
<input class="input" name="plate" value="<?= e((string)$v['plate']) ?>"
|
||||
placeholder="车牌" aria-label="车牌">
|
||||
<input class="input" name="name" value="<?= e((string)$v['name']) ?>"
|
||||
placeholder="车型名" required aria-label="车型名">
|
||||
<select class="select" name="vtype" aria-label="来源">
|
||||
<?php foreach (\App\Vehicle::TYPES as $vk => $vv): ?>
|
||||
<option value="<?= e($vk) ?>"<?= (string)$v['vtype'] === $vk ? ' selected' : '' ?>>
|
||||
<?= e($vv) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input class="input" name="vendor" value="<?= e((string)($v['vendor'] ?? '')) ?>"
|
||||
placeholder="租车公司 / 车主" aria-label="租车公司或车主">
|
||||
<label class="veh-def">
|
||||
<input type="checkbox" name="is_default" value="1"<?= (int)$v['is_default'] === 1 ? ' checked' : '' ?>>
|
||||
<span>默认</span>
|
||||
</label>
|
||||
<button class="btn btn-sm" type="submit">保存</button>
|
||||
</form>
|
||||
<form class="inline-form" method="post" action="<?= e(url('trips/vehicles_del')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$v['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这辆车?')">删</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="veh-name"><?= e(\App\Vehicle::label($v)) ?></span>
|
||||
<span class="veh-tag"><?= e(\App\Vehicle::TYPES[(string)$v['vtype']] ?? '') ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="empty-hint">还没有。自家车录一台,出行时能直接挑。</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<h3 class="set-sub">加一辆车</h3>
|
||||
<form class="inline-form veh-form veh-add" method="post" action="<?= e(url('trips/vehicles_save')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input class="input" name="plate" placeholder="车牌(借的车可不填)" aria-label="车牌">
|
||||
<input class="input" name="name" placeholder="车型名,如「大众朗逸」" required aria-label="车型名">
|
||||
<select class="select" name="vtype" aria-label="来源">
|
||||
<?php foreach (\App\Vehicle::TYPES as $vk => $vv): ?>
|
||||
<option value="<?= e($vk) ?>"><?= e($vv) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input class="input" name="vendor" placeholder="租车公司 / 车主" aria-label="租车公司或车主">
|
||||
<input class="input" name="order_no" placeholder="租车订单号" aria-label="租车订单号">
|
||||
<label class="veh-def">
|
||||
<input type="checkbox" name="is_default" value="1">
|
||||
<span>设为默认</span>
|
||||
</label>
|
||||
<button class="btn btn-sm" type="submit">加进车辆库</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
Reference in New Issue
Block a user