364 lines
17 KiB
PHP
364 lines
17 KiB
PHP
<?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; ?>
|