v54:首页任务标题截断(真根因=两列网格被撑破)/ 名册分组→标签且候选读词表 / 旅程地图视图 / 旅程设置按钮 / 显示方式默认列表或地图

用户 2026-10-16 傍晚五条。

① 首页「近期任务」标题太长会挤成两行
   用户原话:「首页**近期任务**那个地方要注意截断:如果任务标题太长,
   会把这个任务**挤成两行**。」
   🔴🔴 **真根因是两层,第一版只修了表面**:
     · 表面:`.dash-what` 只有 `flex:1 1 auto; min-width:0`,**缺** nowrap/hidden/ellipsis
       ⇒ 收缩后文字在框内换行;
     · 真身:`.home-duo` 是**两列 grid**,grid 子项默认 `min-width: auto`
       ("我最小也得装下我的内容")⇒ 一条长标题把那一列顶到 **1763px**
       (父 `home-duo` 才 1032px)⇒ **整个两列布局被撑破**、页面横向溢出到 **2089px**
       (视口 1440)。⇒ 行宽 1721px、`scrollWidth === clientWidth`,
       **`text-overflow: ellipsis` 看起来"没生效"** —— 其实容器自己先爆了。
   修法:`.home-duo > * { min-width: 0; }`,且 **整条 flex 链**都补
     (`.hd-pane` → `.hd-pane-head` → `.hd-pane-body` → `.dash-list/.tk-list` → `.tk-row`)
     —— 链上任何一层漏了,收缩就断在那一层、下面的 nowrap 全白写。
   配套:任务标题加 `title=` 悬停看全。
   ⚠️ 这是本项目**第二次**踩 flex/grid 的 `min-width:auto`(v53 的 `.mv-grid` 是同类)。
      **以后"文字截断不生效"先量容器有没有被撑破**。

② 名册「分组」→「标签」;打标签的候选要**从设置里读**
   用户原话:「现在名册里面给人打标签的时候,**这个标签数据是从哪读出来的**呀?
   在这个名册的设置页面,有很多分组,现在还叫分组。但是打标签的时候,
   **却不是从这些分组里读出来的**。……应该叫「名册标签」,是不是?
   **应该从这里面把所有标签都读出来**吧?……以后就没有分组的概念了,
   **全部都是标签的概念**。」
   🔴 **用户观察得完全正确,这是两套数据源**:
     · 设置页显示的词表来自 `person_groups`(**分组表**);
     · 打标签的候选来自 `PersonTag::cloud()`(**你已经打过的词**,现算的);
     ⇒ 两边根本不是一个数据源 ⇒ "设置里列着一堆、打标签时却只有几个"。
   修法:**`PersonTag::choices()` = 设置页词表 ∪ 我用过的词**(并集,词表在前)。
     · 词表里的词**还没用过**也能一点就打上(这正是词表的意义);
     · 用过的词补在后面(历史不丢);
     · 名册列表 / 新增 / 编辑 / 挑人 / 导入 **五处同一个函数**。
   界面口径全面改"标签"(**表名仍是 `person_groups`**:改名要背着 `PersonGroup` 类、
   `person_roster.group_id`、以及**另一个会话正在写的 `app/ApiHandler.php`**
   (它引用了 `PersonGroup::orderFor`)一起动,风险远大于收益):
     · `_person_groups.php` 标题「名册标签」、按钮「加一个我自己的标签」;
     · 删除文案改成"只是从候选里去掉这个词,已打在别人身上的不受影响"
       (**语义真的变了**:标签是各自身上的字符串,不是引用);
     · `people/_settings_menu.php`、`people/settings.php`、`settings/me.php`、
       `settings.php`(「公共分组」→「公共标签」)全部同步。
   顺带按"全部都是标签"改另两条入口:
     · `people/pick.php`「加进哪一组」下拉 → **打标签**;
     · `people/import.php` 同上;`ContactImporter::run()` 加 `$tags` 参数。
   🔴 新增 `PersonTag::addOne()`(**追加**一个标签)—— 这两处**绝不能用 `setFor()`**
      (它是"先 DELETE 再 INSERT"的整组替换)⇒ 会把人**原有**的标签清光
      (症状是"用一次导入,别人原有的标签全没了",极难联想)。
   ⚠️ `group_id` 在 pick/import 的**服务端仍收着**(老书签/脚本兼容),只是界面不再展示。

③ 旅程模块加**地图视图**(读 TripLog 移植)
   用户原话:「你去读一下 TripLog 的代码,在旅程这个模块,**可以在地图上显示旅程数据**。
   1. 先读一下它之前那个代码是怎么实现的,**在我们的旅程模块也实现一下**。」
   读 TripLog(`templates/activities/index.php`)得到的要点:
     · 用**高德 AMap**(合规白名单内,绝不是境外瓦片);
     · `position` 必须是 **[lng,lat] 数组**(传 `{lng,lat}` 裸对象会让 AMap 抛错、
       整个绘制循环当场中断 —— 它注释里写着"批 11 的光秃秃地图就是它");
     · `setFitView()` 后**上限 11 级**(只有一趟时会被放大到街道级,那是导航视角);
     · `.amap-marker` 整层 `pointer-events:none`(否则包装层挡住别家卡片的点击);
     · **容器必须有固定高度**(0 高度 → 空白地图)。
     它那套"同城环形扇开"是**几百行几何**,本仓数据量没压出这个问题 ⇒ **有意不做**。
   本仓数据更规整:`trip_nodes` 早有 `from_lng/from_lat/to_lng/to_lat`(v17 批)
   + `from_city/to_city` ⇒ 新增 `Trip::attachSpots()`:
     ① 有坐标取**重心**(不是端点 —— 端点会让"青岛→伊春"偏到某一头);
     ② 没坐标退到**城市重心**(`Geo::lookup` 离线城市表,**不发网络请求**:
        列表页几十趟若逐条打高德逆地理,一次开页就是几十个外部请求);
     ③ 都没有就不上图(列表里照常有,模板会说清"有几趟没上图")。
     🔴 城市那里**必须也取重心**:`cities()` 的顺序是**按时间**(出发地在最前),
        取"第一个城市"会让**所有旅程都叠在家上**、地图毫无信息量。
   布局:两个视图用**标签页**(`?view=map` 是**链接**不是纯 JS 按钮 ——
     书签/前进后退/无 JS 都正确;也**有意不**把地图藏在 `display:none` 里,
     高德在 0 尺寸容器里会渲染空白)。
   高德 SDK 加载器重构:原来 `fbLoadAddr()` 顺带拉 `addr_suggest.js`,
   而地图只用 SDK 自带的 `AMap.Map/Marker` ⇒ 抽出 `window.fbAmapSdk()`(只拉 SDK)、
   新增 `fb-amap` meta(不带 plugin),`fb-addr` 那份共用同一加载器。

④ 旅程列表页「新建旅程」**后面**加「设置」按钮
   用户原话:「2. 在旅程的「新建旅程」后面也加一个「设置」按钮。」
   ⇒ 补 `_settings_menu.php`(设置恒排最右),菜单**第一条**加「显示方式」。

⑤ 显示方式:默认列表还是地图(用户自配)
   用户原话:「3. 里面的设置让用户自己配置:**到底默认显示旅程列表,还是用地图显示**。」
   ⇒ 新增 `trips/display`(设置页 + `?set=` 快速设置)与 `trips/display_save`,
     存 **`user_settings.trip_default_view`**(每人一份,**不是**全家设置)。
   ⚠️ 访客也能改(只影响自己看到的默认,不是数据变更)。

验证(新增 2 套件 + 更新 e2e/deploy 断言):
 · _fb_v54_ui.py(**33 条**真机断言)
 · _b54_falsify.py(**11 注入 / 11 全抓**,基线 33/0)
 · e2e **1518/0**;familybook_deploy.sh 加「第54批形态」段(**24 条**)
 · 部署 **0 FAIL**;线上 **20 个文件 md5 逐字节一致**;
   线上实测:词表 24 条、`choices()` 24 条、**`cloud` 15 条全部 ⊆ `choices`**、
   4 趟旅程全部可上图(样例落在济南·青岛重心);诊断页已删复验 404

⚠️ 本轮踩坑(已写进 pitfalls.md):
 · 🔴🔴 **"文字截断不生效"要先量容器有没有被撑破**(第一嫌疑是 flex/grid 子项
   `min-width:auto`,不是 `text-overflow` 写错);
 · **反向断言("不该再出现 X")必须用 `_code_has`(剥注释)** ——
   需求原话正当留在注释里,裸 `grep -F` 会命中自己的说明(本轮部署假红一次);
 · **冗余防线会让反向验证打不红**(两处 `min-width:0` 只去掉一条 ⇒ 探针照旧全绿,
   注入必须一起去掉);
 · **界面多加一个字符就可能打歪既有断言**(我在词表管理页给标签名加了 `#`,
   把 e2e 的 `my_group_order()` 读成 `#家人`)。
This commit is contained in:
2026-10-17 00:15:06 +08:00
parent 26026c7250
commit 8d79857f1a
20 changed files with 1023 additions and 125 deletions
+53 -21
View File
@@ -1,18 +1,35 @@
<?php
/**
* 「名册分组」管理卡片 —— 从个人设置**搬出来**的(v52 用户 2026-10-15 第 6 条)。
* 「名册标签」词表管理卡片 —— 从个人设置**搬出来**的(v52 用户 2026-10-15 第 6 条),
* v54(用户 2026-10-16 第 2 条)**从"分组"改口径为"标签词表"**。
*
* 用户原话:「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
* 用户原话(v52):「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
* 点击后下拉设置,第一个就是导入联系人,第二个就是原来个人设置那里的
* **名册分组**,这样**个人设置中就不显示名册分组的内容**了。」
*
* ⇒ 同一个卡片现在挂在「名册 → 设置 → 名册分组」这一页上(`templates/people/settings.php`)。
* ⚠️ 表单 action 仍指向 `settings/groups/*`(**一个字段都没改**)——
* 分组管理是"我自己的分组",与它挂在哪一页无关;
* 换路由只会白白多一处要维护的东西(而且老书签/脚本会失效)。
* 用户原话(v54,本次):「现在名册里面给人打标签的时候,这个标签数据是从哪读出来的呀?
* 在这个名册的设置页面,有很多分组,现在还叫分组。但是打标签的时候,
* 却不是从这些分组里读出来的。所以现在应该在这个设置页面,不应该再叫
* 「名册分组」了吧?应该叫「名册标签」,是不是?**应该从这里面把所有标签都读出来**吧?」
*
* 🔴 澄清一个容易混的点(也是用户困惑的根源):
* 这张卡背后的表仍然是 `person_groups`(**表名没改**,因为改名要背着
* `PersonGroup` 类、`person_roster.group_id`、其它会话正在写的 App-API 一起动,
* 风险远大于收益)—— 但它的**语义**从 v28 起就已经是"**标签词表**":
* 这里配的每个词,现在会出现在名册里打标签时的**候选列表**里
* (见 `App\PersonTag::choices()`:**这张表 ∪ 我用过的词**)。
*
* 所以:
* · 界面上**一律叫"标签"**,不再出现"分组"二字;
* · 这里加的词 = "预定义标签",还没用过也能一点就打上;
* · 某个人身上真有哪几个标签,仍然在名册卡片上那颗标签按钮里改。
*
* ⚠️ 表单 action 仍指向 `settings/groups/*`(**一个字段都没改**)——
* 它是"我自己的词表",与它挂在哪一页、叫什么名字无关;
* 换路由只会白白多一处要维护的东西(而且老书签/脚本会失效)。
*
* 变量(require 前设好;**入参仍用宿主那边的名字**,片段内部一律 pg* 前缀,见下):
* $groups PersonGroup::orderFor(...) 的结果(公共 + 我的)
* $groups PersonGroup::orderFor(...) 的结果(公共同我的)
* $groupCounts gid => 人数
* $rosterCount 我的名册人数
*/
@@ -25,28 +42,38 @@ $pgTotal = count($pgGroups);
?>
<div class="card card-pad">
<h2 class="section-title">
名册分组
名册标签
<span class="hint">公共的 + 我自己加的</span>
</h2>
<p class="small muted" style="margin:0 0 16px">
名册是按「关系分组」分栏显示的。这里有两层:
<b>公共分组</b>由管理员在「系统设置」里维护(全家都能用);
你要是觉得不够,就在下面<b>加只给自己看的分组</b>,别人看不到、也不受影响。
这里配的是**给名册成员打标签时可以一点就选的词**。
<b>公共标签</b>由管理员在「系统设置」里维护(全家都能用);
你要是觉得不够,就在下面<b>加只给自己看的标签</b>,别人看不到、也不受影响。
<br>
一个人可以有多个标签(他既是「亲戚」又是「球友」),所以标签不分组、不分栏。
谁身上真有哪个标签,在名册里点他那张卡片上的标签按钮就能改。
<br>
列表里 <b>公共</b> 标的是大家共用的那几个。
哪一组排前面也是你自己的习惯,用 ▲ ▼ 调 —— 别人看到的顺序不变。
哪个排前面也是你自己的习惯,用 ▲ ▼ 调 —— 别人看到的顺序不变。
<br>
你的名册现在有 <b><?= $pgRosterN ?></b> 人。
</p>
<?php if (!$pgGroups): ?>
<p class="small muted" style="margin:0 0 14px">还没有分组。加一个吧。</p>
<p class="small muted" style="margin:0 0 14px">还没有标签。加一个吧。</p>
<?php else: ?>
<ol class="order-list">
<?php foreach ($pgGroups as $i => $g): $pgGid = (int)$g['id']; $pgOwn = (int)$g['owner_id']; ?>
<li class="order-row">
<span class="order-no"><?= $i + 1 ?></span>
<span class="order-name">
<?php /* ⚠️ 这里**不加 `#`** —— 词表里存的是**词本身**(`家人`),
`#` 只是名册里打标签时的显示习惯(`#家人`)。
在词表管理页加上 `#`,会让人以为"输入时要带 #",
而实际 `PersonTag::clean()` 会把 `#` 剥掉 —— 两处显示不一致。
🔴 v54:e2e 的顺序断言 `my_group_order()` 抓的是 `<ol class="order-list">`
里 `order-name` 的**第一个子串**,多出来的 `#` 会让它读成 `#家人`
⇒ 断言红(这也是"界面多加一个字符就可能打歪既有断言"的又一例)。 */ ?>
<?= e((string)$g['name']) ?>
<?php if ($pgOwn === 0): ?><span class="badge">公共</span><?php else: ?><span class="badge badge-open">我的</span><?php endif; ?>
</span>
@@ -56,9 +83,9 @@ $pgTotal = count($pgGroups);
<input type="hidden" name="id" value="<?= $pgGid ?>">
<input type="hidden" name="scope" value="me">
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="up"
title="上移一组" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
title="上移一个" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="down"
title="下移一组" <?= $i === $pgTotal - 1 ? 'disabled' : '' ?>>▼</button>
title="下移一个" <?= $i === $pgTotal - 1 ? 'disabled' : '' ?>>▼</button>
</form>
</li>
<?php endforeach; ?>
@@ -68,13 +95,13 @@ $pgTotal = count($pgGroups);
<form method="post" action="<?= e(url('settings/groups/add')) ?>" class="inline-form" style="margin-top:16px">
<?= csrf_field() ?>
<input type="hidden" name="scope" value="me">
<label class="sr-only" for="newMyGroup">新分组名</label>
<label class="sr-only" for="newMyGroup">新标签名</label>
<input class="input" id="newMyGroup" name="name" maxlength="20" placeholder="例如:球友、老同学">
<button class="btn btn-sm" type="submit">加一个我自己的分组</button>
<button class="btn btn-sm" type="submit">加一个我自己的标签</button>
</form>
<?php
// 只有我自己建的分组才给改名 / 删除 —— 公共分组要在系统设置里改(那是管理员的事),
// 只有我自己建的标签才给改名 / 删除 —— 公共标签要在系统设置里改(那是管理员的事),
// 免得有人在自己设置里把"家人"改成别的、影响全家。
$pgMine = array_values(array_filter($pgGroups, fn($g) => (int)$g['owner_id'] > 0));
?>
@@ -86,7 +113,7 @@ $pgTotal = count($pgGroups);
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $pgGid ?>">
<input type="hidden" name="scope" value="me">
<label class="sr-only" for="gname<?= $pgGid ?>">分组名</label>
<label class="sr-only" for="gname<?= $pgGid ?>">标签名</label>
<input class="input" id="gname<?= $pgGid ?>" name="name" maxlength="20"
value="<?= e((string)$g['name']) ?>">
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
@@ -99,7 +126,12 @@ $pgTotal = count($pgGroups);
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $pgGid ?>">
<input type="hidden" name="scope" value="me">
<span class="small muted">这组的人会变成「未分类」,确定?</span>
<?php /* 🔴 v54:删词表的**成员语义变了** ——
原来它删的是"分组",挂在这一组的人会变成「未分类」;
现在它只是**从可选词里去掉这个词**,已经打在别人身上的标签**不受影响**
(标签是各自身上的字符串,不是引用)。文案必须说清,
否则用户以为"删了=把人身上的标签也摘了"。 */ ?>
<span class="small muted">只是从候选里去掉这个词,已经打在别人身上的不受影响,确定?</span>
<button class="btn btn-ghost btn-sm danger" type="submit">确认删除</button>
</form>
</details>
@@ -110,7 +142,7 @@ $pgTotal = count($pgGroups);
<?php if (\App\Auth::isAdmin()): ?>
<p class="small muted" style="margin:16px 0 0">
公共分组在「<a href="<?= e(url('settings')) ?>#public-groups">系统设置</a>」里维护。
公共标签在「<a href="<?= e(url('settings')) ?>#public-groups">系统设置</a>」里维护。
</p>
<?php endif; ?>
</div>
+4
View File
@@ -473,6 +473,10 @@ unset($evRef);
行里还有复选框(勾一下完成)与其它链接,
整行可点会让"勾选"和"打开"互相抢。 */ ?>
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
<?php /* 🔴 v54(用户 2026-10-16 第 1 条):标题被 CSS 截成一行 + 省略号,
所以这里给 `title` —— 鼠标停上去能看全名。
(点开还有详情浮层,但"只看一眼全名"不该逼人点进去。) */ ?>
title="<?= e((string)$t['title']) ?>"
data-fb-entry='<?= fb_entry_json([
'title' => (string)$t['title'],
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
+82 -25
View File
@@ -77,7 +77,19 @@ $isLoggedIn = \App\Auth::check();
$_amapJs = \App\Geo::jsConfig();
$_needAddr = ($_amapJs['key'] !== '');
?>
<?php if ($_needAddr): ?>
<?php /* 🔴 v54(用户 2026-10-16 第 3 条):**高德地图 SDK** 的独立登记项。
旅程模块的"地图视图"要用 `AMap.Map` / `AMap.Marker`(不需要地址联想),
所以 SDK 的加载器必须能从**不依赖 `[data-addr]`** 的入口触发。
做法:把 SDK 地址与密钥单独挂一个 `fb-amap` meta,`fb-addr` 那份**共用**
同一个加载器(见下面脚本里 `window.fbAmapSdk()`)。
⚠️ 仍然只在**配了 key** 时输出(凭据不该出现在用不到高德的页面)。
⚠️ 插件按需:地图本身**不需要** AutoComplete 插件,只有地址联想要 ——
所以两份 meta 各带自己的 `plugin`(`fb-amap` 不带插件)。 */
if ($_needAddr): ?>
<meta name="fb-amap" content="<?= e(json_encode([
'sdk' => 'https://webapi.amap.com/maps?v=2.0&key=' . rawurlencode($_amapJs['key']),
'security' => $_amapJs['security'],
], JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>">
<meta name="fb-addr" content="<?= e(json_encode([
'js' => asset('/assets/js/addr_suggest.js'),
'sdk' => 'https://webapi.amap.com/maps?v=2.0&key=' . rawurlencode($_amapJs['key'])
@@ -304,33 +316,32 @@ window.FB_NOTIFY = {
与 person_picker 的 `fbLoadPicker()` 完全同一套路。 */ ?>
<script>
(function () {
var meta = document.querySelector('meta[name="fb-addr"]');
if (!meta) { return; } // 没配高德 key(或本页不需要)→ 什么都不做
/* 🔴 v54:配置源改成 `fb-amap`(**只要有高德 key 就输出**,与本页是否需要联想无关)——
因为"地图 SDK"与"地址联想脚本"现在是两件独立的事(见 meta 那段的说明)。
⚠️ 兼容旧路径:万一哪天有页面只带了 `fb-addr`(老缓存 HTML),这里也能兜住。 */
var metaAmap = document.querySelector('meta[name="fb-amap"]');
var metaAddr = document.querySelector('meta[name="fb-addr"]');
var meta = metaAmap || metaAddr;
if (!meta) { return; } // 没配高德 key → 什么都不做
var conf;
try { conf = JSON.parse(meta.getAttribute('content')); } catch (e) { return; }
if (!conf || !conf.js || !conf.sdk) { return; }
if (!conf || !conf.sdk) { return; }
var sdkReady = null;
function loadSdk() {
if (window.__AMAP_LOADED) { return Promise.resolve(true); }
if (sdkReady) { return sdkReady; }
sdkReady = new Promise(function (res) {
var s = document.createElement('script');
s.src = conf.sdk;
s.setAttribute('data-fb-amap', '1');
s.onload = function () { window.__AMAP_LOADED = true; res(true); };
s.onerror = function () { sdkReady = null; res(false); }; // 失败不锁死,下次可重试
document.head.appendChild(s);
});
return sdkReady;
}
function loadOurs() {
if (window.fbAddr) { return Promise.resolve(true); }
// 联想脚本只在 `fb-addr` 那份配置里(地图页不需要它)
var jsUrl = '';
if (metaAddr) {
try {
jsUrl = (JSON.parse(metaAddr.getAttribute('content') || '{}') || {}).js || '';
} catch (e) { jsUrl = ''; }
}
if (!jsUrl) { return Promise.resolve(false); }
return new Promise(function (res) {
var s = document.createElement('script');
s.src = conf.js;
s.src = jsUrl;
s.defer = true;
s.onload = function () { res(true); };
s.onerror = function () { res(false); };
@@ -338,15 +349,47 @@ window.FB_NOTIFY = {
});
}
/* 安全密钥必须在 SDK **之前**设好(高德要求),否则 AutoComplete 会被拒。 */
function boot() {
/* ⚠️ 安全密钥必须在 SDK **之前**设好(高德硬要求),否则 AutoComplete 会被拒。
放在这里而不是页面顶部:没用到地址联想的页面不该出现这两个凭据。 */
/**
* 🔴 v54(用户 2026-10-16 第 3 条):把"**只拉高德地图 SDK**"这一步单独暴露出来。
*
* 为什么不能只留 `fbLoadAddr()`:那个函数**顺带**会去拉 `addr_suggest.js`
* (地址联想那一整块),而旅程地图只用 SDK 自带的 `AMap.Map` / `AMap.Marker`,
* 根本不需要联想脚本 —— 让地图页白拉一个用不上的脚本是浪费。
*
* ⚠️ 安全密钥 `_AMapSecurityConfig` 必须在 SDK **之前**设好(高德硬要求,
* 否则 SDK 内的服务调用会被拒)—— 所以密钥设置放在这里、由两个入口共用。
* ⚠️ 失败**不锁死** `sdkReady`:下次调用可以重试(网络抖动不该让地图永久白屏)。
* @return Promise<boolean> SDK 是否可用(AMap 全局对象就位)
*/
window.fbAmapSdk = function () {
if (window.__AMAP_LOADED && typeof window.AMap !== 'undefined') {
return Promise.resolve(true);
}
if (sdkReady) { return sdkReady; }
/* 密钥必须在此刻(SDK 之前)设好,见上面的说明 */
if (!window._AMapSecurityConfig) {
window._AMapSecurityConfig = { securityJsCode: conf.security || '' };
}
return loadSdk().then(function () {
if (!window.__AMAP_LOADED || typeof window.AMap === 'undefined') { return false; }
sdkReady = new Promise(function (res) {
var s = document.createElement('script');
s.src = conf.sdk;
s.setAttribute('data-fb-amap', '1');
s.onload = function () {
window.__AMAP_LOADED = true;
res(typeof window.AMap !== 'undefined');
};
s.onerror = function () { sdkReady = null; res(false); }; // 失败不锁死,下次可重试
document.head.appendChild(s);
});
return sdkReady;
};
/* 安全密钥必须在 SDK **之前**设好(高德要求),否则 AutoComplete 会被拒。 */
function boot() {
/* ⚠️ 密钥的设置已经收进 `fbAmapSdk()`(地图与联想两个入口共用一处),
这里只需要"先 SDK、再我们的脚本"这个顺序。 */
return window.fbAmapSdk().then(function (ok) {
if (!ok) { return false; }
return loadOurs();
});
}
@@ -378,6 +421,20 @@ window.FB_NOTIFY = {
</script>
<script src="<?= e(asset('/assets/js/filter.js')) ?>"></script>
<script src="<?= e(asset('/assets/js/preview.js')) ?>"></script>
<?php /* 🔴 v54(用户 2026-10-16 第 3 条):旅程**地图**脚本,按需加载。
判据用 `#tripMapCanvas`(地图容器真实 id)—— 只有旅程列表的"地图视图"会输出它,
别的页面不该白拉这一个脚本(与 note_editor / addr_suggest 同一套规矩)。
⚠️ 它内部再按需去要**高德 SDK**(`window.fbAmapSdk()`,见上面 fb-amap 那段)——
所以这一条只管"要不要引我们自己的脚本",SDK 由脚本自己拉。 */ ?>
<script>
(function () {
if (!document.getElementById('tripMapCanvas')) { return; }
var s = document.createElement('script');
s.src = <?= json_encode(asset('/assets/js/trip_map.js')) ?>;
s.defer = true;
document.head.appendChild(s);
})();
</script>
<?php /* 🔴 v52(用户第 4 条):笔记**详情页**的分段编辑(就地改一段、不跳整页编辑器)。
⚠️ 只在阅读态需要它 —— 判据用 `.note-read-body[data-note-id]`(模板在阅读态才输出),
编辑页没有这个元素,因此不会白拉一份脚本。 */ ?>
+7 -2
View File
@@ -9,9 +9,14 @@
* ⇒ 名册页右上角从此只有两颗:**添加成员** + **☰ 设置**(设置恒在最右,全站统一)。
* 原来散着的那几颗(从已有家里人里挑 / 导入联系人 / …)全部收进这个下拉。
*
* 🔴 v54(用户 2026-10-16 第 2 条):第二行**从「名册分组」改叫「名册标签」**。
* 用户原话:「现在应该在这个设置页面,不应该再叫「名册分组」了吧?
* 应该叫「名册标签」,是不是?应该从这里面把所有标签都读出来吧?」
* ⇒ 文案改掉;指向的仍是同一个页面(`people/settings`)与同一个词表。
*
* 两行(顺序是用户点名的):
* ① 导入联系人 → `people/import`
* ② 名册分组 → `people/settings`(从个人设置搬过来的那一块)
* ② 名册标签 → `people/settings`(从个人设置搬过来的那一块)
*
* 用法(require 前 set 好):
* $plSetCurrent 当前页 key('import'|'groups')用来高亮;'' 也行
@@ -39,7 +44,7 @@ $plSetIcon = static function (string $name): string {
<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('people/import')) ?>"<?= $plSetOn('import') ?>><?= $plSetIcon('import') ?><span>导入联系人</span></a>
<a href="<?= e(url('people/settings')) ?>"<?= $plSetOn('groups') ?>><?= $plSetIcon('groups') ?><span>名册分组</span></a>
<a href="<?= e(url('people/settings')) ?>"<?= $plSetOn('groups') ?>><?= $plSetIcon('groups') ?><span>名册标签</span></a>
</div>
</details>
<?php unset($plSetCurrent, $plSetOn, $plSetIcon); ?>
+25 -8
View File
@@ -10,6 +10,10 @@
*/
$report = $report ?? null;
$canMove = \App\Auth::canWrite();
/* 🔴 v54(用户 2026-10-16 第 2 条):打标签的候选 —— 设置页配的词表 ∪ 我用过的词。
⚠️ 显式给默认值(模板可能被别处 require、未必总带上这个键)。 */
$tagChoices = $tagChoices ?? [];
$groups = $groups ?? [];
?>
<div class="page-head">
<h1>导入联系人</h1>
@@ -43,15 +47,28 @@ $canMove = \App\Auth::canWrite();
$sdMultiple = false; $sdRequired = true; $sdStage = false;
require __DIR__ . '/../_stage_drop.php'; ?>
</div>
<?php /* 🔴 v54(用户 2026-10-16 第 2 条):原来这里是「加进哪一组」的下拉。
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
⇒ 换成**打标签**(与「添加成员」页同一个契约:`name=tags`)。
导入进来的新人会自动带上这串标签(owner=我)。 */ ?>
<div class="field">
<label for="imp-group">加进哪一组</label>
<select class="select" id="imp-group" name="group_id">
<option value="0">先不分类</option>
<?php foreach ($groups as $g): ?>
<option value="<?= (int)$g['id'] ?>"><?= e((string)$g['name']) ?></option>
<?php endforeach; ?>
</select>
<span class="tip">只影响<b>新建</b>的那些人;已有的人保持他原来的分组。</span>
<label for="imp-tags">给导入的人打标签(可留空)</label>
<?php if (!empty($tagChoices)): ?>
<div class="pt-picks pf-picks" style="margin-bottom:8px">
<?php foreach ($tagChoices as $tg): ?>
<label class="pt-pick">
<input type="checkbox" value="<?= e((string)$tg) ?>">
<span>#<?= e((string)$tg) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php /* 回显刚填的标签(导入失败重渲时不该让人重打一遍)——
`clean` 归一后再拼,输入什么分隔符都能正确回显。 */ ?>
<input class="input" id="imp-tags" name="tags" maxlength="300"
value="<?= e(implode(' ', \App\PersonTag::clean((string)($_POST['tags'] ?? '')))) ?>"
placeholder="如:同事 老同学(用空格分开)" autocomplete="off">
<span class="tip">只影响<b>新建</b>的那些人;已有的人身上原来的标签不动。</span>
</div>
</div>
<div class="btn-row">
+15 -5
View File
@@ -24,7 +24,11 @@
*/
$canMove = \App\Auth::canWrite();
$pickable = $pickable ?? 0;
// ⚠️ v54(用户 2026-10-16 第 2 条):两个变量**不是一个东西**,别合并:
// · $tagCloud —— **真有人的**标签(筛选条用;人的数为 0 的词不上筛选条)
// · $tagChoices —— **打标签的候选**(词表 ∪ 用过的词;浮层里的 checkbox 用它)
$tagCloud = $tagCloud ?? [];
$tagChoices = $tagChoices ?? [];
$tagFilter = $tagFilter ?? '';
// v28:平铺列表,人数直接数行数(不再按分组累加)
$total = count($people ?? []);
@@ -417,13 +421,19 @@ $renderGrid = function (array $list) use ($meId, $canMove): void {
<?= csrf_field() ?>
<input type="hidden" name="person_id" value="">
<div class="rm-body">
<?php /* 已有的标签(我名下全部用过的):点一下勾选/取消 */ ?>
<?php if ($tagCloud): ?>
<?php /* 🔴 v54(用户 2026-10-16 第 2 条):候选**不再是"我用过的词"**,
而是 `$tagChoices` = **设置页配的词表 ∪ 我用过的词**。
用户原话:「打标签的时候,**却不是从这些分组里读出来的**……
应该从这里面把所有标签都读出来吧?但是现在标签里面却只有几个,
也不知道是从哪儿读出来的。」
⇒ 词表里的词**还没用过**也能一点就打上 —— 这正是词表的意义
(否则每个新标签都得手打一遍,词表就等于没有)。 */ ?>
<?php if ($tagChoices): ?>
<div class="pt-picks" id="ptPicks">
<?php foreach ($tagCloud as $t): ?>
<?php foreach ($tagChoices as $tg): ?>
<label class="pt-pick">
<input type="checkbox" value="<?= e((string)$t['tag']) ?>">
<span>#<?= e((string)$t['tag']) ?></span>
<input type="checkbox" value="<?= e((string)$tg) ?>">
<span>#<?= e((string)$tg) ?></span>
</label>
<?php endforeach; ?>
</div>
+28 -8
View File
@@ -12,6 +12,12 @@
* 那么他可以直接复用这个人的信息,就不需要再重新录入。」
*/
?>
<?php
/* 🔴 v54(用户 2026-10-16 第 2 条):打标签的候选 —— 设置页配的词表 ∪ 我用过的词。
⚠️ 显式给默认值(模板可能被别处 require、未必总带上这个键)。 */
$tagChoices = $tagChoices ?? [];
$groups = $groups ?? [];
?>
<div class="page-head">
<h1>从家里已有的人里挑</h1>
<span class="sub">选中的会进你的名册,资料共用一份,不会重复录入</span>
@@ -34,14 +40,28 @@
<b>资料是共用的那一份</b>,你以后改的也是同一份。
</p>
<div class="field" style="max-width:280px;margin-bottom:16px">
<label for="pickGroup">加进哪一组</label>
<select class="select" id="pickGroup" name="group_id">
<option value="">先不分组(之后拖到那一栏也行)</option>
<?php foreach ($groups as $g): $gid = (int)$g['id']; ?>
<option value="<?= $gid ?>"><?= e((string)$g['name']) ?></option>
<?php endforeach; ?>
</select>
<?php /* 🔴 v54(用户 2026-10-16 第 2 条):原来这里是「加进哪一组」的下拉(group_id)。
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
⇒ 换成**打标签**(与「添加成员」页同一个契约:`name=tags` + `#ptBlock` 那套)。
候选来自 `PersonTag::choices()`(设置页配的词表 ∪ 我用过的词),
服务端 people/pick 会把这一串标签逐个落进 person_tags(owner=我)。
⚠️ 勾了多个人时,这串标签会**给每个人各打一遍** —— 一次挑进来的人
往往是同一类("小学同学"),所以这正是想要的默认。 */ ?>
<div class="field" style="margin-bottom:16px">
<label for="pickTags">给他们打标签(可留空,之后在卡片上也能改)</label>
<?php if (!empty($tagChoices)): ?>
<div class="pt-picks pf-picks" style="margin-bottom:8px">
<?php foreach ($tagChoices as $tg): ?>
<label class="pt-pick">
<input type="checkbox" value="<?= e((string)$tg) ?>">
<span>#<?= e((string)$tg) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
<input class="input" id="pickTags" name="tags" maxlength="300"
placeholder="如:小学同学 球友(用空格分开)" autocomplete="off">
<span class="tip">一个人可以有多个标签;打上的标签只在你自己的名册里可见。</span>
</div>
<?php /* 用户 2026-10-03 第 2 条:所有"找个人"的地方都要**输入即筛选**。
+11 -6
View File
@@ -1,15 +1,20 @@
<?php
/**
* 名册设置(v52 用户 2026-10-15 第 6 条)。
* 名册设置(v52 用户 2026-10-15 第 6 条;v54 用户 2026-10-16 第 2 条改口径)。
*
* 用户原话:「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
* 用户原话(v52):「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
* 点击后下拉设置,第一个就是导入联系人,第二个就是**原来个人设置那里的名册分组**,
* 这样**个人设置中就不显示名册分组的内容**了。」
*
* ⇒ 这一页 = 「设置 → 名册分组」进去的那一页,内容就是从个人设置**整块搬**过来的
* `_person_groups.php`(表单 action 一个字段没改,仍指向 settings/groups/*)。
* 用户原话(v54):「现在应该在这个设置页面,**不应该再叫「名册分组」了吧?
* 应该叫「名册标签」**,是不是?应该从这里面把所有标签都读出来吧?」
*
* ⚠️ 为什么不做成"个人设置那一块的副本":两份一模一样的分组管理界面,
* ⇒ 这一页 = 「设置 → 名册标签」进去的那一页,内容就是从个人设置**整块搬**过来的
* `_person_groups.php`(表单 action 一个字段没改,仍指向 settings/groups/*)。
* v54 起界面文案**全面改成"标签"**:这是"预定义标签词表",
* 名册里打标签时的候选就从它读(见 `App\PersonTag::choices()`)。
*
* ⚠️ 为什么不做成"个人设置那一块的副本":两份一模一样的标签管理界面,
* 改一处忘一处是必然的(本项目在"设置页"上已经踩过这个)。
* 所以是**真的搬走** —— 个人设置里那一块删掉了,这里 require 同一个片段。
*
@@ -19,7 +24,7 @@
?>
<div class="page-head">
<h1>名册设置</h1>
<span class="sub">分组</span>
<span class="sub">标签</span>
<div class="actions">
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a>
<?php $plSetCurrent = 'groups'; require __DIR__ . '/_settings_menu.php'; ?>
+18 -13
View File
@@ -45,27 +45,32 @@
</div>
<?php endif; ?>
<!-- ==================== 公共分组 ==================== -->
<!-- ==================== 公共标签 ==================== -->
<?php
/* 🔴 v54(用户 2026-10-16 第 2 条):这块从「公共分组」改口径为「公共标签」。
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
⇒ 这里维护的是**公共标签词表**:名册里给成员打标签时,这些词会出现在候选里
(见 `App\PersonTag::choices()`)。表名仍叫 person_groups(见 _person_groups.php
的说明:改名要背着 PersonGroup 类与其它会话正在改的 App-API 一起动,不划算),
但界面上一律说"标签"。 */
$pubGroups = $pubGroups ?? [];
$pubCounts = $pubCounts ?? [];
$pubTotal = count($pubGroups);
?>
<div class="card card-pad" style="margin-top:20px" id="public-groups">
<h2 class="section-title">
公共分组
公共标签
<span class="hint">全家人共用的一套</span>
</h2>
<p class="small muted" style="margin:0 0 16px">
名册按「关系分组」分栏显示。这里维护的是<b>公共的那一套</b> ——
建一次,全家每个人都能拿来用。
这里维护的是<b>公共标签</b> —— 建一次,全家每个人给名册成员打标签时都能拿来用。
<br>
每个人还可以在「个人设置 → 我的分组」里<b>再加只给自己看的分组</b>,
每个人还可以在「名册 → 设置 → 名册标签」里<b>再加只给自己看的标签</b>,
那部分不在这儿、也不会影响别人。挪顺序用 ▲ ▼(▲▼ 调的是"全家的默认顺序",
各人仍可在个人设置里改成自己习惯的顺序)。
各人仍可在名册设置里改成自己习惯的顺序)。
<br>
删掉一个分组<b>不会删掉人</b>:名册里归到那一组的人会变成「未分类」,
各自重新归类即可。
删掉一个标签<b>只是从候选里去掉这个词</b>:已经打在别人身上的标签不受影响
(标签是各人身上记着的词,不是引用)。
</p>
<?php if ($pubGroups): ?>
@@ -80,9 +85,9 @@ $pubTotal = count($pubGroups);
<input type="hidden" name="id" value="<?= $gid ?>">
<input type="hidden" name="scope" value="public">
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="up"
title="上移一组" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
title="上移一个" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="down"
title="下移一组" <?= $i === $pubTotal - 1 ? 'disabled' : '' ?>>▼</button>
title="下移一个" <?= $i === $pubTotal - 1 ? 'disabled' : '' ?>>▼</button>
</form>
</li>
<?php endforeach; ?>
@@ -92,9 +97,9 @@ $pubTotal = count($pubGroups);
<form method="post" action="<?= e(url('settings/groups/add')) ?>" class="inline-form" style="margin-top:16px">
<?= csrf_field() ?>
<input type="hidden" name="scope" value="public">
<label class="sr-only" for="newPubGroup">新分组名</label>
<label class="sr-only" for="newPubGroup">新标签名</label>
<input class="input" id="newPubGroup" name="name" maxlength="20" placeholder="例如:邻居、亲家、老同事">
<button class="btn btn-sm" type="submit">加一个公共分组</button>
<button class="btn btn-sm" type="submit">加一个公共标签</button>
</form>
<?php if ($pubGroups): ?>
@@ -105,7 +110,7 @@ $pubTotal = count($pubGroups);
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $gid ?>">
<input type="hidden" name="scope" value="public">
<label class="sr-only" for="pgname<?= $gid ?>">分组名</label>
<label class="sr-only" for="pgname<?= $gid ?>">标签名</label>
<input class="input" id="pgname<?= $gid ?>" name="name" maxlength="20"
value="<?= e((string)$g['name']) ?>">
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
+8 -6
View File
@@ -315,17 +315,19 @@ $fbCityN = count(\App\Geo::CITIES);
<?php /* 🔴 v52(用户 2026-10-15 第 6 条):「原来个人设置那里的名册分组……
这样**个人设置中就不显示名册分组的内容**了。」
⇒ 整块「我的分组」卡片**搬到了**「名册 → 设置 → 名册分组」
⇒ 整块卡片**搬到了**「名册 → 设置 → 名册标签」
(`templates/people/settings.php` + `templates/_person_groups.php`)。
⚠️ 是**真的搬走**,不是复制一份:两份一模一样的分组管理界面,
改一处忘一处是必然的。这里只留一句指路,免得老用户找不到。 */ ?>
⚠️ 是**真的搬走**,不是复制一份:两份一模一样的管理界面,
改一处忘一处是必然的。这里只留一句指路,免得老用户找不到。
🔴 v54(用户 2026-10-16 第 2 条):口径**从"分组"改成"标签"** ——
用户原话:「应该叫「名册标签」,是不是?」⇒ 这块指路文案也跟着改。 */ ?>
<div class="card card-pad" style="margin-top:20px">
<h2 class="section-title">名册分组</h2>
<h2 class="section-title">名册标签</h2>
<p class="small muted" style="margin:0 0 12px">
分组改到**名册页**那边去了 —— 它本来就是"名册怎么分组"的事,与个人设置无关。
标签改到**名册页**那边去了 —— 它本来就是"给名册里的人打什么标签"的事,与个人设置无关。
</p>
<div class="btn-row">
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/settings')) ?>">去名册设置里改分组</a>
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/settings')) ?>">去名册设置里改标签</a>
</div>
</div>
+73
View File
@@ -0,0 +1,73 @@
<?php
/**
* 旅程**地图视图**的主体(v54 用户 2026-10-16 第 3 条)。
*
* 用户原话:「你去读一下 TripLog 的代码,在旅程这个模块,**可以在地图上显示旅程数据**。
* 1. 先读一下它之前那个代码是怎么实现的,**在我们的旅程模块也实现一下**。」
*
* ── TripLog 那套是怎么做的(读了它的 `templates/activities/index.php`)──
* · 用**高德 JS API**(`AMap.Map` + `AMap.Marker`);
* · 每个活动一枚标记;标记的 `content` 是自绘的"小卡片"(封面图 + 标题 + 地点),
* 同城/邻近的卡片会**环形扇开**+画引线,避免叠在一起;
* · `map.setFitView()` 自动框住所有点,但**上限 11 级**(不然只有一趟时会被
* 一路放大到街道级,那是导航视角不是"足迹"视角)。
*
* ── 本仓怎么落地(同一套思路,但数据源更规整)──
* · 坐标来源:`Trip::attachSpots()` —— 旅程下所有节点的 from/to 坐标**取重心**;
* 没有坐标就退到城市(内置离线城市表,**不发网络请求**,见那里的说明)。
* · 每趟旅程一枚标记,点它进 `trips/show`。
* · 卡片只放"标题 + 时间 + 城市",不放封面图 —— 本仓列表已经是图片卡片,
* 地图上再来一层图片会喧宾夺主(地图的作用是"看空间分布")。
* · 视野上限同样 11 级、贴合全部标记。
*
* ⚠️ **合规**:地图用高德(腾讯/高德/百度/天地图是合规白名单内),
* 绝不用 Google/OSM 等境外瓦片。key 走本仓既有配置(`App\Geo::jsConfig()`),
* 由 `layout.php` 的 `fb-amap` 按需加载 —— 本页不自己拼 SDK 地址。
* ⚠️ 容器必须有**固定高度**(这里是 `.trip-map` 的 CSS 高度):
* 高德地图在"高度为 0 的容器"里渲染出来是空白,这是最常见的坑。
* ⚠️ 没有坐标的旅程**不画**(数据里说了 `trip_spot=false`);
* 若**一个点都没有**,不渲染地图容器,直接给一句说明(免得用户对着白框发呆)。
*
* 变量:$rows(Trip::all + attachDerived + attachSpots 之后)、$total
*/
$mapPoints = [];
foreach ((array)($rows ?? []) as $row) {
if (empty($row['trip_spot'])) {
continue;
}
$mapPoints[] = [
'id' => (int)$row['id'],
'title' => (string)$row['title'],
'lng' => (float)$row['trip_lng'],
'lat' => (float)$row['trip_lat'],
'when' => \App\Trip::activitySpan($row['starts_at'] ?? null, $row['ends_at'] ?? null),
'city' => (string)($row['city'] ?? ''),
'url' => url('trips/show', ['id' => (int)$row['id']]),
];
}
?>
<?php if (!$mapPoints): ?>
<?php /* 一个坐标都没有 —— 如实说清楚"为什么没有",而不是给个空地图。
常见原因:行程里没填出发/到达地,或填了但(当时没配高德)没解析出坐标。 */ ?>
<div class="card card-pad empty" id="tripMapEmpty">
<b>还没有能在地图上显示的旅程</b>
地图要靠行程里的**出发地 / 到达地**(有坐标或能认出城市)。
去某趟旅程里加一段行程、把出发到达填上,它就会出现在这里。
</div>
<?php else: ?>
<?php /* 🔴 高度写在内联 style 上不够稳(有的主题会覆盖),
所以主要靠 `.trip-map` 的 CSS;这里只在**无 JS** 时兜一个提示。 */ ?>
<div class="trip-map-wrap">
<div class="trip-map" id="tripMapCanvas"
data-points="<?= e(json_encode($mapPoints, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>">
<noscript><p class="trip-map-fallback">地图需要 JavaScript;用左侧的「列表」看旅程。</p></noscript>
</div>
<?php if (count($mapPoints) < (int)($total ?? 0)): ?>
<?php /* 有旅程不在图上 —— 说清楚,免得用户以为"少了几趟"。 */ ?>
<p class="small muted trip-map-hint">
有 <?= (int)($total ?? 0) - count($mapPoints) ?> 趟旅程没有可用位置(行程里没填出发/到达,或认不出城市),
没有画在地图上 —— 它们在「列表」里照常显示。
</p>
<?php endif; ?>
</div>
<?php endif; ?>
+13 -3
View File
@@ -1,8 +1,11 @@
<?php
/**
* 旅程模块的「设置」下拉(v24 第 16 批 用户第 8 条)。
* 旅程模块的「设置」下拉(v24 第 16 批 用户第 8 条;v54 用户第 4 条加一项)。
*
* 用户原话:「右上角的按钮,需要增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。」
* 用户原话(v24):「右上角的按钮,需要增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。」
*
* 用户原话(v54):「2. 在旅程的「新建旅程」**后面也加一个「设置」按钮**。
* 3. 里面的设置让用户自己配置:**到底默认显示旅程列表,还是用地图显示**。」
*
* 与其它模块同一套规矩(用户 2026-10-01 定的四条):
* ① 按钮**没有文字**、只有图标,图标是**三条横线**;
@@ -11,7 +14,7 @@
* ④ 除设置外其它按钮都有颜色。
*
* 用法(require 前 set 好):
* $trSetCurrent 当前页 key('vehicles'|'modes'|'types'|'')
* $trSetCurrent 当前页 key('vehicles'|'modes'|'types'|'display'|'')
*/
$trSetCurrent = (string)($trSetCurrent ?? '');
$trSetOn = static function (string $key) use ($trSetCurrent): string {
@@ -30,6 +33,9 @@ $trSetIcon = static function (string $name): string {
. '<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"/>',
// 🔴 v54:显示方式 —— 列表 + 地图图钉两个小图形
'display' => '<path d="M2.5 4h11M2.5 8h11M2.5 12h6"/>'
. '<path d="M12.6 10.2a2.1 2.1 0 1 0-4.2 0c0 1.5 2.1 3.4 2.1 3.4s2.1-1.9 2.1-3.4z"/>',
];
$d = $paths[$name] ?? '';
if ($d === '') {
@@ -43,6 +49,10 @@ $trSetIcon = static function (string $name): string {
<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">
<?php /* 🔴 v54(第 4/5 条):显示方式**排在第一条** ——
它是这一页自己最常用的设置("我进来先看列表还是先看地图"),
下面三条(车/方式/类型)是"配一次就不动"的。 */ ?>
<a href="<?= e(url('trips/display')) ?>"<?= $trSetOn('display') ?>><?= $trSetIcon('display') ?><span>显示方式</span></a>
<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>
+72
View File
@@ -0,0 +1,72 @@
<?php
/**
* 旅程「显示方式」设置(v54 用户 2026-10-16 第 5 条)。
*
* 用户原话:「3. 里面的设置让用户自己配置:**到底默认显示旅程列表,还是用地图显示**。」
*
* ⇒ 就两项:**列表** / **地图**。选哪个,以后进「旅程」模块就默认先显示哪个。
* ⚠️ 这是**我自己的**偏好,存在 `user_settings.trip_default_view`(每人一份)——
* 与"看板颜色""天气地点"同一层,改它绝不影响家人。
* ⚠️ 名单页上的「列表 / 地图」标签页**任何时候都能手动切换**,
* 这个设置只决定"**刚进来时**看哪个"(用户说的"默认"就是这个意思)。
*
* 变量:$current('list'|'map')
*/
$current = (string)($current ?? 'list');
if (!in_array($current, ['list', 'map'], true)) {
$current = 'list';
}
?>
<div class="page-head">
<h1>显示方式</h1>
<span class="sub">打开「旅程」时默认先看哪个</span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('trips')) ?>">返回旅程</a>
<?php $trSetCurrent = 'display'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<div class="card card-pad">
<h2 class="cinema-h2">
默认视图
<span class="hint">只影响你自己</span>
</h2>
<p class="small muted" style="margin:0 0 16px">
进「旅程」时先显示<b>列表</b>还是<b>地图</b>。这只是"默认看哪个"——
进去之后随时能用页面上的「列表 / 地图」切换,不受这里限制。
</p>
<form method="post" action="<?= e(url('trips/display_save')) ?>">
<?= csrf_field() ?>
<div class="view-choice">
<?php
/* 两个单选项做成"卡片样":图标 + 名称 + 一句说明。
⚠️ 用原生 radio(不是 JS 假按钮)—— 无 JS 也能提交,与全站口径一致。 */
$opts = [
'list' => ['列表', '一行一趟旅程,带封面与参与人。找某一趟、看详情走这里。'],
'map' => ['地图', '把去过的旅程钉在地图上看空间分布。看"都去过哪儿"走这里。'],
];
foreach ($opts as $k => $o): ?>
<label class="view-choice-item<?= $current === $k ? ' is-on' : '' ?>">
<input type="radio" name="view" value="<?= e($k) ?>" <?= $current === $k ? 'checked' : '' ?>>
<span class="vci-body">
<b><?= e($o[0]) ?></b>
<span class="small muted"><?= e($o[1]) ?></span>
</span>
</label>
<?php endforeach; ?>
</div>
<div class="btn-row">
<button class="btn" type="submit">保存</button>
</div>
</form>
</div>
<div class="card card-pad" style="margin-top:16px">
<h2 class="cinema-h2">地图上的点是哪来的</h2>
<p class="small muted" style="margin-top:0">
地图上每趟旅程一枚标记,位置按该旅程**各段行程的出发/到达地点**取一个代表点;
没有坐标时退到**城市中心**(用内置的城市表,不联网)。
所以想让某趟旅程出现在地图上,得在它里面把行程的<b>出发地 / 到达地</b>填上。
</p>
</div>
+71 -4
View File
@@ -1,6 +1,7 @@
<?php
/**
* 旅程列表(v12 建立;v24 第 16 批 第 9 条把术语**固定下来**)。
* 旅程列表(v12 建立;v24 第 16 批 第 9 条把术语**固定下来**;
* v54 用户 2026-10-16 第 3/4/5 条加了**地图视图**与显示方式设置)。
*
* ⚠️ 术语(用户第 9 条原话):「活动都改名吧,改成旅程,新建旅程,旅程包含行程,
* 这个名字就固定下来。」
@@ -9,8 +10,36 @@
*
* 时间与城市都是从各段**行程**派生出来的(第 15 批第 2 条)——
* 列表里读的是 Trip::attachDerived() 挂上来的值,不是旅程自己的字段。
*
* 🔴 v54(用户 2026-10-16 第 3 条):「你去读一下 TripLog 的代码,在旅程这个模块,
* 可以在地图上显示旅程数据。1. 先读一下它之前那个代码是怎么实现的,
* 在我们的旅程模块也实现一下。」
* ⇒ 这一页现在有**两个视图**:「列表」(原有)与「地图」(新,见 _map_body.php)。
* 两者是**同一份 $rows** 的两种看法,用标签页切换、不刷新整页。
* ⚠️ 地图坐标由 `Trip::attachSpots()` 挂在 $rows 上(见路由)。
* 🔴 v54(第 4/5 条):「2. 在旅程的「新建旅程」后面也加一个「设置」按钮。
* 3. 里面的设置让用户自己配置:到底**默认显示旅程列表,还是用地图显示**。」
* ⇒ 右上角加了 `_settings_menu.php`(含新的「显示方式」设置页);
* 默认看哪个由 `$tripDefaultView`(我的 user_settings)决定。
*/
$canWrite = \App\Auth::canWrite();
/* 默认视图:'list' | 'map'(来自 user_settings.trip_default_view,见路由)。
⚠️ 给个兜底常量而不是空串 —— 万一调用方没传,页面也要能正常显示列表。 */
$tripDefaultView = (string)($tripDefaultView ?? 'list');
if (!in_array($tripDefaultView, ['list', 'map'], true)) {
$tripDefaultView = 'list';
}
/* URL 上的 ?view=map 优先于个人默认("我就想现在看一眼地图"不该被设置卡住)。
⚠️ 值不合法时落回默认,避免 ?view=xxx 让页面空白。 */
$tripView = (string)($_GET['view'] ?? '');
if (!in_array($tripView, ['list', 'map'], true)) {
$tripView = $tripDefaultView;
}
$tripHasSpot = false;
foreach ((array)($rows ?? []) as $__r) {
if (!empty($__r['trip_spot'])) { $tripHasSpot = true; break; }
}
unset($__r);
?>
<div class="page-head">
<h1>旅程</h1>
@@ -29,14 +58,52 @@ $canWrite = \App\Auth::canWrite();
<?php else: ?>
<span class="link-quiet">只读账号</span>
<?php endif; ?>
<?php /* 🔴 v54(第 4 条):「在旅程的「新建旅程」**后面**也加一个「设置」按钮。」
与其它旅程页同一份菜单(`_settings_menu.php`),设置恒排最右。 */ ?>
<?php $trSetCurrent = ''; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<?php /* ---- 列表主体:包一层 id,供"新建成功后**动态刷新**"换掉(用户第 2 条)---- */ ?>
<div id="tripListHost">
<?php $tripListBody = true; require __DIR__ . '/_list_body.php'; ?>
<?php /* ---- 视图切换(列表 / 地图)----
⚠️ 用**链接**而不是纯 JS 按钮:`?view=map` 能被书签、前进后退、无 JS 环境正确处理;
点链接会整页重载一次(比"两个 DOM 都渲染、JS 藏一个"更省内存,
地图那种案例尤其不该把它藏在 display:none 里 —— 高德在 0 尺寸容器里会渲染空白)。
⚠️ 只在**真有可显示的点**时才给「地图」这颗;否则点进去是一片空地图,
不如老实告诉用户"还没有能上图的位置"(他自己去填出发到达)。 */ ?>
<div class="trip-views" role="tablist">
<a class="tv-btn<?= $tripView === 'list' ? ' is-on' : '' ?>" role="tab"
aria-selected="<?= $tripView === 'list' ? 'true' : 'false' ?>"
href="<?= e(url('trips', ['view' => 'list'] + ($keyword !== '' ? ['keyword' => $keyword] : []))) ?>">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M2.5 4h11M2.5 8h11M2.5 12h11"/></svg>
列表
</a>
<a class="tv-btn<?= $tripView === 'map' ? ' is-on' : '' ?>" role="tab"
aria-selected="<?= $tripView === 'map' ? 'true' : 'false' ?>"
href="<?= e(url('trips', ['view' => 'map'] + ($keyword !== '' ? ['keyword' => $keyword] : []))) ?>">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 3.5l3.5-1.2 4 1.4 3.5-1.2v10.5l-3.5 1.2-4-1.4-3.5 1.2z"/><path d="M6 2.3v10.4M10 3.7v10.4"/></svg>
地图
<?php if (!$tripHasSpot): ?><span class="tv-dim" title="还没有能上图的位置">(0)</span><?php endif; ?>
</a>
<?php /* "把当前这个设成默认" —— 放在标签页同一行右侧。
⚠️ 只在**当前视图不是默认**时才给,否则会出现两颗功能重复的入口。 */ ?>
<?php if ($tripView !== $tripDefaultView): ?>
<a class="tv-default" href="<?= e(url('trips/display', ['set' => $tripView])) ?>"
title="以后打开旅程模块默认看这个">把「<?= $tripView === 'map' ? '地图' : '列表' ?>」设为默认</a>
<?php else: ?>
<span class="tv-default">默认看:<?= $tripView === 'map' ? '地图' : '列表' ?></span>
<?php endif; ?>
</div>
<?php if ($tripView === 'map'): ?>
<?php /* ---- 地图视图 ---- */ ?>
<?php require __DIR__ . '/_map_body.php'; ?>
<?php else: ?>
<?php /* ---- 列表视图:包一层 id,供"新建成功后**动态刷新**"换掉(用户第 2 条)---- */ ?>
<div id="tripListHost">
<?php $tripListBody = true; require __DIR__ . '/_list_body.php'; ?>
</div>
<?php endif; ?>
<?php if ($canWrite): ?>
<?php /* ---- 新建旅程弹框(内嵌同一份 form.php;初始关闭)---- */ ?>
<?php