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:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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]`(模板在阅读态才输出),
|
||||
编辑页没有这个元素,因此不会白拉一份脚本。 */ ?>
|
||||
|
||||
@@ -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); ?>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 条:所有"找个人"的地方都要**输入即筛选**。
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; ?>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user