From 142b187911f64f33cbbba812a49efd92a2942705 Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 24 Sep 2026 12:36:01 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=A1=A8=E5=8D=95=EF=BC=9A?= =?UTF-8?q?=E5=8F=82=E4=B8=8E=E4=BA=BA=E7=9B=B4=E6=8E=A5=E5=8B=BE=E9=80=89?= =?UTF-8?q?=20+=20=E8=B5=B7=E7=82=B9=E7=BB=88=E7=82=B9=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E5=9C=B0=E5=9D=80=EF=BC=88=E6=89=B9=2013?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 参与人(用户反馈:不想等全部填完、保存之后才回详情页加人): - 节点表单里新增「参与人(直接勾选)」区块:家庭成员按胶囊复选框列出, 保存即生效;编辑时预勾选已有人。 - Members::syncForNode() 只做增删,**不动留下来的那些人的情绪 / 签到 / 角色标签**(重新勾一遍不该把之前记的心情清掉)。 - 安全性:新增走 Members::save(),逐条校验「用户属于该活动所在家庭」, 夹带别家 user_id 进不来;只有请求带 members_present=1(表单确实渲染了 参与人区块)才同步 —— 否则没这个字段的请求会把已有成员清空。 起点/终点记录详细地址(用户反馈:高德联想只返回简化地名): - 联想项选中后不再是「青岛北站」就完事: ① 有坐标 → 逆地理编码(AMap.Geocoder.getAddress)取该点的 formattedAddress; ② 没坐标 → 按「区县 + 名称」正查,顺带把坐标补回来。 - 详细地址写进 start/end_location 的 address 字段,输入框下方实时显示 「详细地址:…」,节点详情页本来就展示这一项。 - 路线地图补点(只有地名没坐标时)改为**优先用详细地址**查询, 比简化地名定位准。 已验证:本地全链 12 步全绿(渲染检查 350 项、真 AMap 浏览器验证 11 项、 表单交互 75 项);远端 e2e 350 项 0 失败(新增 10 项:勾选落库 / 跨家庭 拒绝 / 取消勾选移出 / 情绪不被清 / 无标记不动参与人);部署静态断言、 双 PHP 渲染、内容断言全过后提交。 --- assets/style.css | 19 +++++ src/Controllers/NodeController.php | 54 +++++++++++- src/Members.php | 42 ++++++++++ templates/activities/show.php | 13 ++- templates/nodes/form.php | 130 ++++++++++++++++++++++++++++- 5 files changed, 254 insertions(+), 4 deletions(-) diff --git a/assets/style.css b/assets/style.css index 47c4106..ee32538 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1303,3 +1303,22 @@ video.video-thumb.video-thumb-mini { .route-map { height: 62vh; } .route-tab { padding: 7px 14px; font-size: 13px; } } + +/* ── 21. 节点表单:参与人勾选 + 位置详细地址(批 13)───────────── */ +.member-pick { display: flex; flex-wrap: wrap; gap: 8px; } +.member-pick-item { + display: inline-flex; align-items: center; gap: 6px; + padding: 7px 12px; border: 1px solid var(--border); + border-radius: 999px; background: var(--surface); cursor: pointer; + font-size: 14px; transition: background .15s, border-color .15s; +} +.member-pick-item:hover { border-color: var(--border-strong); } +.member-pick-item input[type="checkbox"] { margin: 0; accent-color: var(--accent); } +.member-pick-item:has(input:checked) { + border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, #fff); +} +.member-pick-avatar { + width: 22px; height: 22px; border-radius: 50%; object-fit: cover; display: block; +} +/* 联想选中后回填的详细地址(让人看见记下来的是什么) */ +.loc-detail { margin-top: 6px; font-size: 12.5px; word-break: break-all; } diff --git a/src/Controllers/NodeController.php b/src/Controllers/NodeController.php index 6120939..c2704ae 100644 --- a/src/Controllers/NodeController.php +++ b/src/Controllers/NodeController.php @@ -95,8 +95,13 @@ class NodeController $stmt = $db->prepare($sql); $stmt->execute($data); + $nodeId = (int)$db->lastInsertId(); + // 节点必须先有 id 才能挂附件,所以附件在插入之后处理 - $extra = $this->handleNodeUploads((int)$db->lastInsertId()); + $extra = $this->handleNodeUploads($nodeId); + + // 表单里勾的参与人(members_present=1 才认,避免没渲染这个区块的请求误清) + $extra .= $this->syncMembers($nodeId); flash('error', '节点已保存' . $extra); redirect('activities/show', ['id' => (int)$activity['id']]); @@ -148,6 +153,9 @@ class NodeController // 再处理这次新选的照片 $extra = $this->handleNodeUploads((int)$node['id']); + // 参与人在表单里直接勾(编辑时重新勾选不会清掉已记的情绪/签到) + $extra .= $this->syncMembers((int)$node['id']); + $msg = '保存成功'; if ($removed > 0) { $msg .= ",删除了 {$removed} 个附件"; @@ -358,6 +366,18 @@ class NodeController $isEdit = $node !== null; + /* 参与人在表单里直接勾(不必先保存再回详情页加人)。 + 编辑时把已有人带过去预勾选;新建时空着。 */ + $familyUsers = Members::familyUsers((int)$activity['family_id']); + $nodeMemberIds = []; + if ($isEdit) { + $s = $db->prepare('SELECT user_id FROM node_members WHERE trip_node_id = ?'); + $s->execute([(int)$node['id']]); + foreach ($s->fetchAll() as $row) { + $nodeMemberIds[] = (int)$row['user_id']; + } + } + return [ 'modeLabels' => TravelModeController::MODE_LABELS, 'activity' => $activity, @@ -367,6 +387,8 @@ class NodeController 'planNodes' => $planNodes, 'node' => $node, 'isEdit' => $isEdit, + 'familyUsers' => $familyUsers, + 'nodeMemberIds' => $nodeMemberIds, // 编辑时把已有照片带进表单:能看、能勾选删除。新建时节点还没 id, // 附件挂不上去,所以只能是空数组(模板里会提示先保存)。 'attachments' => $isEdit ? Attachments::forTarget(Attachments::TARGET_NODE, (int)$node['id']) : [], @@ -583,6 +605,36 @@ class NodeController * ⚠️ 必须逐条校验归属:attachments 是「target_type + target_id」的软关联, * 表单里的 id 是用户可以随便改的,不校验就等于给了跨家庭删文件的入口。 */ + /** + * 表单里勾的参与人 → 同步成节点成员(只增删,不动已记的情绪/签到)。 + * + * 只在表单确实渲染了参与人区块时(members_present=1)才同步: + * 没有渲染却收到空数组的话,会把节点已有成员全删掉。 + * 跨家庭的 user_id 由 Members::save() 逐条拦掉。 + */ + private function syncMembers(int $nodeId): string + { + if (($_POST['members_present'] ?? '') !== '1') { + return ''; + } + $picked = $_POST['members'] ?? []; + if (!is_array($picked)) { + return ''; + } + $res = Members::syncForNode($nodeId, $picked); + if ($res['added'] === 0 && $res['removed'] === 0) { + return ''; + } + $parts = []; + if ($res['added'] > 0) { + $parts[] = "加入 {$res['added']} 位参与人"; + } + if ($res['removed'] > 0) { + $parts[] = "移出 {$res['removed']} 位参与人"; + } + return ',' . implode(',', $parts); + } + private function deleteCheckedAttachments(int $nodeId): int { $ids = $_POST['delete_attachments'] ?? []; diff --git a/src/Members.php b/src/Members.php index 0a5cf31..677a888 100644 --- a/src/Members.php +++ b/src/Members.php @@ -166,6 +166,48 @@ class Members $stmt->execute([$nodeId, $userId]); } + /** + * 勾选式同步:把节点参与人「对齐」成 $userIds(节点表单里直接勾选用)。 + * + * 只做增删 —— 留下来的人的情绪 / 签到 / 角色标签**一律不动**(编辑节点时 + * 重新勾一遍参与人不该把之前记的心情清掉)。新增走 save(), + * 家庭归属校验与「一键加入」那条链路完全一致。 + * + * @return array{added:int, removed:int} + */ + public static function syncForNode(int $nodeId, array $userIds): array + { + $db = Database::get(); + + $want = []; + foreach ($userIds as $uid) { + $uid = (int)$uid; + if ($uid > 0 && !in_array($uid, $want, true)) { + $want[] = $uid; + } + } + + $stmt = $db->prepare('SELECT user_id FROM node_members WHERE trip_node_id = ?'); + $stmt->execute([$nodeId]); + $have = []; + foreach ($stmt->fetchAll(\PDO::FETCH_COLUMN) as $uid) { + $have[] = (int)$uid; + } + + $added = 0; + $removed = 0; + foreach (array_diff($have, $want) as $uid) { + self::remove($nodeId, (int)$uid); + $removed++; + } + foreach (array_diff($want, $have) as $uid) { + if (self::save($nodeId, (int)$uid, [], '', null)['ok']) { + $added++; + } + } + return ['added' => $added, 'removed' => $removed]; + } + /** 一次把一批人加进节点(用于「把活动成员一键全部加入」) */ public static function addMany(int $nodeId, array $userIds, array $roleTagIds = []): int { diff --git a/templates/activities/show.php b/templates/activities/show.php index d8cab65..34f0c9c 100644 --- a/templates/activities/show.php +++ b/templates/activities/show.php @@ -61,6 +61,8 @@ foreach (($realityNodes ?? []) as $rn) { 'url' => url('nodes/show', ['id' => (int)$rn['id']]), 'sname' => $sname, // 出发地名:没有坐标时交给高德地理编码兜底 'ename' => $ename, // 到达地名:同上 + 'saddr' => trim((string)($s['address'] ?? '')), // 详细地址:地理编码优先用它 + 'eaddr' => trim((string)($e['address'] ?? '')), 'start' => $hasS ? ['lng' => (float)$s['lng'], 'lat' => (float)$s['lat']] : null, 'end' => $hasE ? ['lng' => (float)$e['lng'], 'lat' => (float)$e['lat']] : null, ]; @@ -253,9 +255,16 @@ require dirname(__DIR__) . '/comments/_block.php'; } catch (e) { /* 地理编码服务不可用时保持原状 */ } }); } + /* 没有坐标的点:优先拿**详细地址**去查(比简化地名准得多),没记地址才用地名 */ route.forEach(function (n) { - if (!n.start && n.sname) geocode(n.sname, function (p) { n.start = p; drawRoute(); }); - if (!n.end && n.ename) geocode(n.ename, function (p) { n.end = p; drawRoute(); }); + if (!n.start) { + var qs = n.saddr || n.sname; + if (qs) geocode(qs, function (p) { n.start = p; drawRoute(); }); + } + if (!n.end) { + var qe = n.eaddr || n.ename; + if (qe) geocode(qe, function (p) { n.end = p; drawRoute(); }); + } }); })(); diff --git a/templates/nodes/form.php b/templates/nodes/form.php index 74d276e..a654790 100644 --- a/templates/nodes/form.php +++ b/templates/nodes/form.php @@ -44,6 +44,26 @@ $eLoc = [ $startTimeVal = old_dt('start_time', $n['start_time'] ?? null); $endTimeVal = old_dt('end_time', $n['end_time'] ?? null); +/* 参与人在表单里直接勾(不必先保存再回详情页加人)。 + 回填优先:上次提交失败时,用刚勾的而不是库里的。 */ +$familyUsers = $familyUsers ?? []; +$nodeMemberIds = $nodeMemberIds ?? []; +$oldMembers = isset($_SESSION['_old']) && is_array($_SESSION['_old']) + ? (array)($_SESSION['_old']['members'] ?? []) + : null; +$pickedMembers = static function () use ($oldMembers, $nodeMemberIds): array { + $src = $oldMembers ?? $nodeMemberIds; + $out = []; + foreach ((array)$src as $v) { + $v = (int)$v; + if ($v > 0) { + $out[] = $v; + } + } + return $out; +}; +$picked = $pickedMembers(); + /** * 交通信息只留一个输入框:飞机只要航班号(航空公司是隐含的),火车只要车次。 * 座位号/登机口这类「一趟车多人同行」时根本没法记,所以不再收集。 @@ -185,6 +205,7 @@ $tripField = [ target="_blank" rel="noopener">🧭 导航到这里 保存后节点详情页也会带这个链接 +
@@ -205,6 +226,7 @@ $tripField = [ target="_blank" rel="noopener">🧭 导航到这里 保存后节点详情页也会带这个链接 +
@@ -213,6 +235,30 @@ $tripField = [ + +
+ 参与人(直接勾选) + +

这个家庭还没有成员。

+ + +
+ + + + +
+

勾选后保存即生效;已选的人重新勾选不会清掉之前记的签到 / 心情。

+ +
+