行程:#47~#51 打包 —— 双轨状态、动态类型、动作按钮、去提示 (#51)

#47 编辑浮层居中、类型换行、去掉手动状态选择、按钮改胶囊
#48 计划/实际双轨:完成即转实际行程,取消≠删除,记真实开始/结束时间
#50 类型点击无反应(@Builder 值传参不建立依赖)
#51 节点类型动态加载(服务端9种,原端上硬编码5种)+ 地址结构化回调

API 新增:GET /trips/<id>/node-types、PATCH /trips/<id>/nodes/action
DB 新增:trip_nodes.actual_started_at / actual_ended_at
This commit is contained in:
2026-11-05 12:40:57 +08:00
parent a1140437a1
commit f58a128816
9 changed files with 1361 additions and 46 deletions
+110
View File
@@ -4521,6 +4521,116 @@ switch ($r) {
redirect('trips/node_edit', ['id' => $nodeId]);
break;
/* 🔴 #47(2026-11-05):**行程状态切换**(网页端)。
用户:「在计划行程里面点击开始执行,它的状态自然变成进行中…
如果它没去,取消了,那就自然变成已取消。」
⇒ 状态由动作驱动,不再由编辑表单手工选(表单那边已删掉「执行状态」)。
⚠️ 返回 **JSON**(不是 redirect):前端 `app.js` 拿到后**只重绘那一段**
的动作条,不整页刷新 —— 否则用户刚点完就被弹回顶部。
⚠️ 返回的 `html` 是该节点动作条的新 HTML,前端用 outerHTML 就地替换。 */
/* 🔴 #48(2026-11-05):**行程动作**(开始 / 完成 / 取消)。
用户:「计划行程右上角…一个是开始,一个是完成,一个是编辑,一个是删除。
在删除前面还应该有一个按钮,这个按钮是取消。」
⇒ 返回 JSON,前端据此决定是否重载(动作会改变分组)。 */
case 'node_act':
$familyId = (int)Auth::familyId();
Auth::requireWrite();
csrf_check();
$nodeId = (int)($_POST['node_id'] ?? 0);
$act = (string)($_POST['act'] ?? '');
header('Content-Type: application/json; charset=utf-8');
if ($nodeId <= 0) {
echo json_encode(['ok' => false, 'error' => '缺 node_id'], JSON_UNESCAPED_UNICODE);
exit;
}
/* 校验归属(nodes → events 查一遍) */
$stA = Database::get()->prepare(
'SELECT n.event_id FROM trip_nodes n JOIN events e ON e.id = n.event_id
WHERE n.id = ? AND n.family_id = ? AND e.family_id = ? AND n.deleted_at IS NULL'
);
$stA->execute([$nodeId, $familyId, $familyId]);
$evId = $stA->fetchColumn();
if ($evId === false) {
echo json_encode(['ok' => false, 'error' => '找不到这段行程'], JSON_UNESCAPED_UNICODE);
exit;
}
$resA = \App\Trip::nodeAction($familyId, (int)$evId, $nodeId, $act);
if (empty($resA['ok'])) {
echo json_encode(['ok' => false, 'error' => (string)($resA['error'] ?? '没成功')], JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode(['ok' => true, 'stage' => $resA['stage'] ?? '', 'status' => $resA['status'] ?? ''], JSON_UNESCAPED_UNICODE);
exit;
/* 🔴 #48:**删除**(软删,与"取消"完全不同)。
用户:「所谓的删除,指的是我直接把这个计划给删掉了。」
⇒ `deleted_at` 置位,彻底不显示。 */
case 'node_del':
$familyId = (int)Auth::familyId();
Auth::requireWrite();
csrf_check();
$nodeId = (int)($_POST['node_id'] ?? 0);
header('Content-Type: application/json; charset=utf-8');
if ($nodeId <= 0) {
echo json_encode(['ok' => false, 'error' => '缺 node_id'], JSON_UNESCAPED_UNICODE);
exit;
}
$stD = Database::get()->prepare(
'SELECT n.id FROM trip_nodes n JOIN events e ON e.id = n.event_id
WHERE n.id = ? AND n.family_id = ? AND e.family_id = ? AND n.deleted_at IS NULL'
);
$stD->execute([$nodeId, $familyId, $familyId]);
if ($stD->fetchColumn() === false) {
echo json_encode(['ok' => false, 'error' => '找不到这段行程'], JSON_UNESCAPED_UNICODE);
exit;
}
$upD = Database::get()->prepare(
'UPDATE trip_nodes SET deleted_at = NOW(), updated_at = NOW() WHERE id = ?'
);
$upD->execute([$nodeId]);
echo json_encode(['ok' => true], JSON_UNESCAPED_UNICODE);
exit;
case 'node_status':
$familyId = (int)Auth::familyId();
Auth::requireWrite();
csrf_check();
$nodeId = (int)($_POST['node_id'] ?? 0);
$newSt = (string)($_POST['status'] ?? '');
if ($nodeId <= 0) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => '缺 node_id'], JSON_UNESCAPED_UNICODE);
exit;
}
if (!array_key_exists($newSt, \App\Trip::NODE_STATUS)) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => '状态值不对'], JSON_UNESCAPED_UNICODE);
exit;
}
/* 校验归属(顺着 nodes → events 查一遍,防越权) */
$stN = Database::get()->prepare(
'SELECT n.id FROM trip_nodes n JOIN events e ON e.id = n.event_id
WHERE n.id = ? AND n.family_id = ? AND e.family_id = ? AND n.deleted_at IS NULL'
);
$stN->execute([$nodeId, $familyId, $familyId]);
if ($stN->fetchColumn() === false) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => '找不到这段行程'], JSON_UNESCAPED_UNICODE);
exit;
}
$upN = Database::get()->prepare(
'UPDATE trip_nodes SET status = ?, updated_at = NOW() WHERE id = ?'
);
$upN->execute([$newSt, $nodeId]);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => true,
'status' => $newSt,
'status_word' => \App\Trip::NODE_STATUS[$newSt],
], JSON_UNESCAPED_UNICODE);
exit;
case 'trips/expense_del':
$familyId = (int)Auth::familyId();
Auth::requireWrite();