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:
+18
-1
@@ -28,13 +28,22 @@ final class ContactImporter
|
|||||||
/**
|
/**
|
||||||
* @return array{added:int,updated:int,skipped:int,people:array<int,array{name:string,action:string,added:array}>}
|
* @return array{added:int,updated:int,skipped:int,people:array<int,array{name:string,action:string,added:array}>}
|
||||||
*/
|
*/
|
||||||
public static function run(int $familyId, int $meId, array $records, int $groupId = 0): array
|
public static function run(int $familyId, int $meId, array $records, int $groupId = 0, array $tags = []): array
|
||||||
{
|
{
|
||||||
$added = 0;
|
$added = 0;
|
||||||
$updated = 0;
|
$updated = 0;
|
||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
$log = [];
|
$log = [];
|
||||||
|
|
||||||
|
/* 🔴 v54(用户 2026-10-16 第 2 条):导入时可以**顺手打一串标签**。
|
||||||
|
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
|
||||||
|
⚠️ 用 `PersonTag::addOne()`(**追加**)而不是 `setFor()`(整组替换)——
|
||||||
|
命中已有的人时,`setFor` 会把他身上**原有**的标签一起清掉
|
||||||
|
("用一次导入,别人原有的标签全没了")。语义上这里就是"多加一个"。
|
||||||
|
⚠️ 老参数 `$groupId` 保留:不为别的,只为**不改既有调用契约**;
|
||||||
|
新界面不再传它(`templates/people/import.php` 已换成标签输入)。 */
|
||||||
|
$tags = PersonTag::clean($tags);
|
||||||
|
|
||||||
// 现有的人一次取出来做索引(几十~几百人,比逐条查库快得多,也不会漏判)
|
// 现有的人一次取出来做索引(几十~几百人,比逐条查库快得多,也不会漏判)
|
||||||
// 索引键:phone_hash(精确)+ 姓名(小写)
|
// 索引键:phone_hash(精确)+ 姓名(小写)
|
||||||
$byPhone = [];
|
$byPhone = [];
|
||||||
@@ -87,6 +96,10 @@ final class ContactImporter
|
|||||||
⚠️ `ensure` 是幂等的(已在名册里就返回 false),所以重复导入不会出问题。 */
|
⚠️ `ensure` 是幂等的(已在名册里就返回 false),所以重复导入不会出问题。 */
|
||||||
PersonRoster::ensure($familyId, $meId, (int)$hit['id'],
|
PersonRoster::ensure($familyId, $meId, (int)$hit['id'],
|
||||||
$groupId > 0 ? $groupId : null, 'self');
|
$groupId > 0 ? $groupId : null, 'self');
|
||||||
|
// 🔴 v54:命中已有的人时**追加**标签(不动他原有的 —— 见方法头的说明)
|
||||||
|
foreach ($tags as $tg) {
|
||||||
|
PersonTag::addOne($familyId, $meId, (int)$hit['id'], $tg);
|
||||||
|
}
|
||||||
if ($filled) {
|
if ($filled) {
|
||||||
$updated++;
|
$updated++;
|
||||||
$log[] = ['name' => (string)$hit['name'], 'action' => 'updated', 'added' => $filled];
|
$log[] = ['name' => (string)$hit['name'], 'action' => 'updated', 'added' => $filled];
|
||||||
@@ -159,6 +172,10 @@ final class ContactImporter
|
|||||||
|
|
||||||
// 进导入者的名册(谁导入的,就先出现在谁的名册里)
|
// 进导入者的名册(谁导入的,就先出现在谁的名册里)
|
||||||
PersonRoster::ensure($familyId, $meId, $pid, $groupId > 0 ? $groupId : null, 'self');
|
PersonRoster::ensure($familyId, $meId, $pid, $groupId > 0 ? $groupId : null, 'self');
|
||||||
|
// 🔴 v54:新建的人也打上这串标签
|
||||||
|
foreach ($tags as $tg) {
|
||||||
|
PersonTag::addOne($familyId, $meId, $pid, $tg);
|
||||||
|
}
|
||||||
|
|
||||||
// 建完把索引补上,避免同一个文件里出现两条同名时建出两个人
|
// 建完把索引补上,避免同一个文件里出现两条同名时建出两个人
|
||||||
$byName[mb_strtolower($d['name'], 'UTF-8')] = ['id' => $pid, 'name' => $d['name']];
|
$byName[mb_strtolower($d['name'], 'UTF-8')] = ['id' => $pid, 'name' => $d['name']];
|
||||||
|
|||||||
+74
-6
@@ -95,17 +95,57 @@ final class PersonTag
|
|||||||
return $st->fetchAll();
|
return $st->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 建议用的标签词(写标签时的联想;按人气排序) */
|
/**
|
||||||
public static function suggest(int $familyId, int $ownerId, int $limit = 30): array
|
* 打标签时的**候选词**(名册卡片浮层 / 编辑页 / 新增页都用它)。
|
||||||
|
*
|
||||||
|
* 🔴 v54(用户 2026-10-16 第 2 条)—— 这条是本次改动的核心。
|
||||||
|
* 用户原话:「现在名册里面给人打标签的时候,**这个标签数据是从哪读出来的**呀?
|
||||||
|
* 在这个名册的设置页面,有很多分组,现在还叫分组。但是打标签的时候,
|
||||||
|
* **却不是从这些分组里读出来的**。所以现在应该在这个设置页面,不应该再叫
|
||||||
|
* 「名册分组」了吧?应该叫「名册标签」,是不是?**应该从这里面把所有标签都读出来**吧?
|
||||||
|
* 但是现在标签里面却只有几个,也不知道是从哪儿读出来的。」
|
||||||
|
*
|
||||||
|
* **真根因(用户观察得完全正确)**:
|
||||||
|
* · 设置页显示的词表来自 `person_groups`(**分组表**,管理员/我维护的词);
|
||||||
|
* · 打标签的候选来自 `cloud()`(**你已经打过的词**,现算的);
|
||||||
|
* ⇒ 两边**根本不是一个数据源** —— 所以"设置里列着一堆、打标签时却只有几个"。
|
||||||
|
*
|
||||||
|
* ⇒ 合并两者,**并集**:
|
||||||
|
* · 词表(`PersonGroup::orderFor` = 公共同我的)在前 —— 这是用户说的
|
||||||
|
* "应该从这里面把所有标签都读出来";**还没用过**的词也要能一点就打上,
|
||||||
|
* 这正是"词表"相对于"用过的词"的价值(否则新标签永远得手打)。
|
||||||
|
* · 用过的词补在后面(词表里没收录、但历史打过的,不能凭空消失)。
|
||||||
|
*
|
||||||
|
* ⚠️ 只返回**词**(`array_column(...,'tag')` 那种扁平字符串列表),
|
||||||
|
* 与既有 `tagChoices` 的消费方(模板 `foreach` 出 checkbox)契约一致 ——
|
||||||
|
* 不返回 cloud 那种 `[['tag'=>..,'n'=>..]]`,否则调用点全要改。
|
||||||
|
* ⚠️ 那个 `suggest()` 已被它取代(原名容易让人以为是"按人气排序的联想词",
|
||||||
|
* 而实际需求是"设置里配的 + 用过的"并集)—— 保留一个没用处的旧口径
|
||||||
|
* 只会让下一个人继续从 `cloud()` 读、把用户这条需求又绕过一遍。
|
||||||
|
*
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
public static function choices(int $familyId, int $ownerId, int $limit = 60): array
|
||||||
{
|
{
|
||||||
|
if ($familyId <= 0 || $ownerId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
$out = [];
|
$out = [];
|
||||||
|
// ① 词表(设置页「名册标签」里配的那些:公共同我的,带顺序)
|
||||||
|
foreach (PersonGroup::orderFor($familyId, $ownerId) as $g) {
|
||||||
|
$nm = trim((string)($g['name'] ?? ''));
|
||||||
|
if ($nm !== '' && !in_array($nm, $out, true)) {
|
||||||
|
$out[] = $nm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ② 我已经打过的词(词表里没收录的补上;按人气降序)
|
||||||
foreach (self::cloud($familyId, $ownerId) as $r) {
|
foreach (self::cloud($familyId, $ownerId) as $r) {
|
||||||
$out[] = (string)$r['tag'];
|
$t = (string)($r['tag'] ?? '');
|
||||||
if (count($out) >= $limit) {
|
if ($t !== '' && !in_array($t, $out, true)) {
|
||||||
break;
|
$out[] = $t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $out;
|
return array_slice($out, 0, max(1, $limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,6 +191,34 @@ final class PersonTag
|
|||||||
Database::get()->prepare('DELETE FROM person_tags WHERE person_id = ?')->execute([$personId]);
|
Database::get()->prepare('DELETE FROM person_tags WHERE person_id = ?')->execute([$personId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给某人**追加**一个标签(已存在则不重复)。
|
||||||
|
*
|
||||||
|
* 🔴 v54(用户 2026-10-16 第 2 条):挑人 / 导入联系人时要用它。
|
||||||
|
* 为什么必须是"追加"而不是 `setFor()`(整组替换):
|
||||||
|
* `setFor` 会**先 DELETE 再 INSERT** —— 用在这里会把那些人
|
||||||
|
* **原有**的标签一起清掉。而这两个入口的语义是"顺手给新人打一个标签",
|
||||||
|
* 不是"把他们的标签改成这一个"。这种"用整组替换去做追加"的错,
|
||||||
|
* 症状是"用一次导入,别人身上原有的标签全没了",而且很难联想到原因。
|
||||||
|
*
|
||||||
|
* ⚠️ 靠 `uk_pt`(owner_id, person_id, tag 三列唯一)保证幂等:
|
||||||
|
* 重复打同一个词会 `duplicate`,这里用 INSERT IGNORE 吞掉,不报错。
|
||||||
|
* @return bool 这次真的插进去了吗(已存在 = false)
|
||||||
|
*/
|
||||||
|
public static function addOne(int $familyId, int $ownerId, int $personId, string $tag): bool
|
||||||
|
{
|
||||||
|
$one = self::clean($tag);
|
||||||
|
if ($familyId <= 0 || $ownerId <= 0 || $personId <= 0 || !$one) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$st = Database::get()->prepare(
|
||||||
|
'INSERT IGNORE INTO person_tags (family_id, owner_id, person_id, tag, position)
|
||||||
|
VALUES (?,?,?,?,?)'
|
||||||
|
);
|
||||||
|
$st->execute([$familyId, $ownerId, $personId, $one[0], 100]);
|
||||||
|
return $st->rowCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标签归一:`#小学同学` / `小学同学 ` / `小学,同学` 都要变成干净的词。
|
* 标签归一:`#小学同学` / `小学同学 ` / `小学,同学` 都要变成干净的词。
|
||||||
*
|
*
|
||||||
|
|||||||
+109
@@ -245,6 +245,115 @@ final class Trip
|
|||||||
return implode(' · ', $cities);
|
return implode(' · ', $cities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔴 v54(用户 2026-10-16 第 3 条):给**一批**旅程补上「代表坐标」
|
||||||
|
* —— 旅程列表的**地图视图**用它把每趟旅程钉在地图上的一个点。
|
||||||
|
*
|
||||||
|
* 用户原话:「你去读一下 TripLog 的代码,在旅程这个模块,可以在地图上显示旅程数据。
|
||||||
|
* 1. 先读一下它之前那个代码是怎么实现的,**在我们的旅程模块也实现一下**。」
|
||||||
|
*
|
||||||
|
* 做法(移植 TripLog `ActivityController::index` 的思路,但适配本仓的数据):
|
||||||
|
* TripLog 是「把该活动下所有节点的经纬度取**重心**」——
|
||||||
|
* 本仓的数据更规整(`trip_nodes` 已有 from_lng/from_lat/to_lng/to_lat 四列,
|
||||||
|
* 见 Schema 的 v17 段),所以照同一思路来:
|
||||||
|
*
|
||||||
|
* ① 优先用**真实坐标**:把该旅程所有节点的 from/to 坐标收集起来取平均
|
||||||
|
* (= 重心)。为什么取重心而不是"第一段/最后一段":
|
||||||
|
* 几段行程串起来的一条线,重心最能代表"这趟旅程大概在哪儿";
|
||||||
|
* 取端点会让"青岛→伊春"这种整条北上的旅程偏到某一头。
|
||||||
|
* ② 没有坐标就**退到城市**:拿 `cities()` 里第一个城市,
|
||||||
|
* 用本仓内置的 `Geo::lookup()`(离线城市表)换中心坐标。
|
||||||
|
* ⚠️ 这是离线表、**不发网络请求** —— 列表页要为几十趟旅程算坐标,
|
||||||
|
* 若逐条打高德逆地理,一次开页面就是几十个外部请求(配额与延迟都受不了)。
|
||||||
|
* ③ 两者都没有 ⇒ 该旅程**不在地图上出现**(`lng/lat` 为 null),
|
||||||
|
* 由模板跳过;列表视图里它照常显示。
|
||||||
|
*
|
||||||
|
* ⚠️ skipped(未成行)的节点**不算**:计划去但没去成的地方不该把标记拽过去
|
||||||
|
* (与 `cities()` 同一口径)。
|
||||||
|
* ⚠️ 一次查完(IN 批查 + 内存分组),不做 N+1 —— 与 `attachDerived` 同一理由。
|
||||||
|
* ⚠️ 只**读**,不改 `$rows` 里既有的键之外的东西;算完挂 `trip_lng`/`trip_lat`/`trip_spot`
|
||||||
|
* (前缀 `trip_` 避免与节点的 `lng/lat` 撞名)。
|
||||||
|
*/
|
||||||
|
public static function attachSpots(array &$rows): void
|
||||||
|
{
|
||||||
|
$ids = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$id = (int)($r['id'] ?? 0);
|
||||||
|
if ($id > 0) {
|
||||||
|
$ids[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$ids) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||||
|
$st = Database::get()->prepare(
|
||||||
|
"SELECT event_id, from_lng, from_lat, to_lng, to_lat
|
||||||
|
FROM trip_nodes
|
||||||
|
WHERE event_id IN ($in) AND stage IN ('plan','actual')"
|
||||||
|
);
|
||||||
|
$st->execute($ids);
|
||||||
|
$acc = []; // event_id => [sumLng, sumLat, n]
|
||||||
|
foreach ($st->fetchAll() as $n) {
|
||||||
|
$eid = (int)$n['event_id'];
|
||||||
|
foreach ([['from_lng', 'from_lat'], ['to_lng', 'to_lat']] as $pair) {
|
||||||
|
$lng = $n[$pair[0]] ?? null;
|
||||||
|
$lat = $n[$pair[1]] ?? null;
|
||||||
|
if ($lng === null || $lat === null || $lng === '' || $lat === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$lng = (float)$lng;
|
||||||
|
$lat = (float)$lat;
|
||||||
|
// 0,0 是"没坐标"的常见脏值(高德没返回时容易被写成 0),跳过
|
||||||
|
if ($lng === 0.0 && $lat === 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($acc[$eid])) {
|
||||||
|
$acc[$eid] = [0.0, 0.0, 0];
|
||||||
|
}
|
||||||
|
$acc[$eid][0] += $lng;
|
||||||
|
$acc[$eid][1] += $lat;
|
||||||
|
$acc[$eid][2]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($rows as &$r) {
|
||||||
|
$eid = (int)($r['id'] ?? 0);
|
||||||
|
$lng = null;
|
||||||
|
$lat = null;
|
||||||
|
if (isset($acc[$eid]) && $acc[$eid][2] > 0) {
|
||||||
|
$lng = $acc[$eid][0] / $acc[$eid][2];
|
||||||
|
$lat = $acc[$eid][1] / $acc[$eid][2];
|
||||||
|
} else {
|
||||||
|
/* 退到城市(离线表,不发网络请求;见方法头的说明)。
|
||||||
|
🔴 这里取**该旅程所有能认出的城市的中心点的平均值**(重心),
|
||||||
|
而不是"第一个城市" —— 与上面"有坐标时取重心"**同一口径**。
|
||||||
|
为什么不取第一个:`cities()` 的顺序是**按时间**(出发地在前),
|
||||||
|
而出发地往往是同一个家(青岛)⇒ 所有旅程的标记会**全叠在家上**,
|
||||||
|
地图就没有信息量了。取重心则"青岛→哈尔滨"落在两地之间,
|
||||||
|
与"有坐标"那条路的表现一致(写成两套口径迟早对不上)。 */
|
||||||
|
$sumLng = 0.0;
|
||||||
|
$sumLat = 0.0;
|
||||||
|
$nCity = 0;
|
||||||
|
foreach ((array)($r['cities'] ?? []) as $c) {
|
||||||
|
$hit = \App\Geo::lookup((string)$c);
|
||||||
|
if ($hit) {
|
||||||
|
$sumLng += (float)$hit['lon'];
|
||||||
|
$sumLat += (float)$hit['lat'];
|
||||||
|
$nCity++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($nCity > 0) {
|
||||||
|
$lng = $sumLng / $nCity;
|
||||||
|
$lat = $sumLat / $nCity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$r['trip_lng'] = $lng;
|
||||||
|
$r['trip_lat'] = $lat;
|
||||||
|
$r['trip_spot'] = ($lng !== null && $lat !== null);
|
||||||
|
}
|
||||||
|
unset($r);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 给**一批**活动补上派生时间与城市(列表页用;N+1 会让几十趟活动明显变慢)。
|
* 给**一批**活动补上派生时间与城市(列表页用;N+1 会让几十趟活动明显变慢)。
|
||||||
*
|
*
|
||||||
|
|||||||
+139
-6
@@ -2950,7 +2950,20 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
|||||||
.dash-row:first-child { border-top: 0; }
|
.dash-row:first-child { border-top: 0; }
|
||||||
.dash-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
|
.dash-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
|
||||||
.dash-when { flex: 0 0 96px; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
.dash-when { flex: 0 0 96px; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||||
.dash-what { flex: 1 1 auto; min-width: 0; font-size: 14px; }
|
/* 🔴 v54(用户 2026-10-16 第 1 条):「首页**近期任务**那个地方要注意截断:
|
||||||
|
如果任务标题太长,会把这个任务**挤成两行**。」
|
||||||
|
真根因:这一条只有 `flex:1 1 auto` + `min-width:0` —— 它能**收缩**,
|
||||||
|
但行内文字**没有禁止换行** ⇒ 收缩之后标题在框内绕成第二行,把这一行撑高、
|
||||||
|
把旁边的状态/看板徽标挤到下一行。缺的正是这三句。
|
||||||
|
⚠️ 三句缺一不可:`nowrap` 让它不绕行、`hidden` 让溢出被裁、
|
||||||
|
`ellipsis` 给出"后面还有"的信号(不然会像被截断的错字)。
|
||||||
|
⚠️ 全名走 `title=` 悬停(模板里加的),点开也会弹详情浮层 —— 短的看不全不是问题。
|
||||||
|
⚠️ 这条**同时**管「往年的今天」那一块(同一个 class):那边的标题也可能长,
|
||||||
|
一并对齐成"一行 + 省略号"比一处换行一处截断更整齐。 */
|
||||||
|
.dash-what {
|
||||||
|
flex: 1 1 auto; min-width: 0; font-size: 14px;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
.dash-tag { flex: 0 0 auto; font-size: 11px; color: var(--faint); }
|
.dash-tag { flex: 0 0 auto; font-size: 11px; color: var(--faint); }
|
||||||
/* ---- v24 第 11 批 #4:影院下拉(带色点)+ 新建影院弹框 ----
|
/* ---- v24 第 11 批 #4:影院下拉(带色点)+ 新建影院弹框 ----
|
||||||
原生 <option> 画不了色块,所以自己做一个"按钮 + 弹出列表":
|
原生 <option> 画不了色块,所以自己做一个"按钮 + 弹出列表":
|
||||||
@@ -4417,11 +4430,30 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
|
|||||||
对齐又失效(这是"固定高度 + 内部滚动"这套写法的经典坑)。
|
对齐又失效(这是"固定高度 + 内部滚动"这套写法的经典坑)。
|
||||||
这样两块的**外框高度恒等**,与各自内容多少无关 —— 对齐不再依赖数据。 */
|
这样两块的**外框高度恒等**,与各自内容多少无关 —— 对齐不再依赖数据。 */
|
||||||
.home-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: stretch; }
|
.home-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: stretch; }
|
||||||
.hd-pane { display: flex; flex-direction: column; height: 440px; min-height: 0; }
|
/* 🔴🔴 v54(用户 2026-10-16 第 1 条)—— **这条才是"标题太长挤成两行"的真根因**。
|
||||||
.hd-pane-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; flex: 0 0 auto; }
|
症状两层,第一层在这里:
|
||||||
.hd-pane-head .cinema-h2 { margin: 0; flex: 1 1 auto; }
|
`.home-duo` 是**两列网格**(`1fr 1fr`),而 grid/flex 子项的默认
|
||||||
|
`min-width: auto` 意思是"我最小也得装下我的内容" ⇒ 只要右栏有一条**长标题**,
|
||||||
|
那一列就被顶到 1700+px(实测:`home-duo` 1032px,里面 `hd-pane` 1763px),
|
||||||
|
整个两列布局被撑破、横向溢出到 2089px。
|
||||||
|
⇒ 于是"截断"根本没机会发生:**容器自己先被撑爆了**,行宽 1721px,
|
||||||
|
`scrollWidth === clientWidth`,`text-overflow: ellipsis` 看起来"没生效"。
|
||||||
|
⚠️ `min-width: 0` 是让 grid/flex 子项**允许收缩到内容以下**的唯一开关;
|
||||||
|
缺它时,在子项里怎么写 nowrap/ellipsis 都没用(本项目第二次踩这个:
|
||||||
|
v53 的 `.mv-grid` 是同类,那次是 `grid-column`,这次是 `min-width`)。
|
||||||
|
⚠️ 加在**两个**网格项上(两个 pane 都要能收缩,不只是有任务的那个)。 */
|
||||||
|
.home-duo > * { min-width: 0; }
|
||||||
|
.hd-pane { display: flex; flex-direction: column; height: 440px; min-height: 0; min-width: 0; }
|
||||||
|
.hd-pane-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; flex: 0 0 auto; min-width: 0; }
|
||||||
|
.hd-pane-head .cinema-h2 { margin: 0; flex: 1 1 auto; min-width: 0; }
|
||||||
|
/* ⚠️ v54:列表链上每一层都要 `min-width:0`(中间任何一层没给,
|
||||||
|
收缩就断在那一层、下面的 nowrap 又白写)—— `.dash-list`/`.tk-list` 是 ul,
|
||||||
|
`.tk-row` 是 li,都是 flex 项。 */
|
||||||
|
.dash-list { min-width: 0; }
|
||||||
|
.tk-list { min-width: 0; }
|
||||||
|
.tk-row { min-width: 0; }
|
||||||
.hd-pane-body {
|
.hd-pane-body {
|
||||||
flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
|
flex: 1 1 auto; min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* 旧 Edge / IE */
|
-ms-overflow-style: none; /* 旧 Edge / IE */
|
||||||
}
|
}
|
||||||
@@ -4485,7 +4517,12 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
|
|||||||
|
|
||||||
/* ---- 任务行:标题 + 状态 + **看板名** + 截止 ---- */
|
/* ---- 任务行:标题 + 状态 + **看板名** + 截止 ---- */
|
||||||
.tk-list { display: flex; flex-direction: column; }
|
.tk-list { display: flex; flex-direction: column; }
|
||||||
.tk-row { align-items: center; }
|
/* 🔴 v54(用户 2026-10-16 第 1 条):标题太长会把这行**挤成两行**。
|
||||||
|
与 `.dash-what` 那三句(nowrap/hidden/ellipsis)配套:行本身**不许换行**,
|
||||||
|
于是标题只能去收缩(它有 min-width:0,能一直缩到 0),徽标保持原样。
|
||||||
|
⚠️ 不复用 `.dash-row` 的 `flex-wrap:wrap` —— 那是"往年的今天"那一块要的
|
||||||
|
(窄屏时日期徽标该绕到下一行);任务行收紧成 nowrap 才有截断可言。 */
|
||||||
|
.tk-row { align-items: center; flex-wrap: nowrap; }
|
||||||
.tk-board {
|
.tk-board {
|
||||||
flex: 0 0 auto; font-size: 11.5px; color: var(--muted);
|
flex: 0 0 auto; font-size: 11.5px; color: var(--muted);
|
||||||
background: var(--line-2); border-radius: 999px; padding: 1px 8px;
|
background: var(--line-2); border-radius: 999px; padding: 1px 8px;
|
||||||
@@ -5954,3 +5991,99 @@ h5.md-h, h6.md-h { font-size: 14px; }
|
|||||||
.mod-table .mod-row.is-dragged { opacity: .5; }
|
.mod-table .mod-row.is-dragged { opacity: .5; }
|
||||||
.mod-table .mod-row.is-drop-before td { border-top: 2px solid var(--brand-2); }
|
.mod-table .mod-row.is-drop-before td { border-top: 2px solid var(--brand-2); }
|
||||||
.mod-table .color-picker { width: 34px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 5px; background: #fff; cursor: pointer; }
|
.mod-table .color-picker { width: 34px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 5px; background: #fff; cursor: pointer; }
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
🔴 v54(用户 2026-10-16 第 3/4/5 条):旅程 —— **列表 / 地图**两个视图
|
||||||
|
==========================================================================
|
||||||
|
用户原话:「在旅程这个模块,**可以在地图上显示旅程数据**」(移植 TripLog);
|
||||||
|
「在旅程的「新建旅程」后面也加一个「设置」按钮」;
|
||||||
|
「里面的设置让用户自己配置:**到底默认显示旅程列表,还是用地图显示**。」
|
||||||
|
|
||||||
|
两个视图用**标签页**切换(不是两页)—— 因为"看列表"与"看地图"是同一份数据的
|
||||||
|
两种看法,来回切不该刷新整页;也让"默认显示哪个"这个设置有意义
|
||||||
|
(设置决定**进来时**先显示哪一个)。
|
||||||
|
⚠️ 标签页样式沿用本仓既有的"胶囊按钮 + on 态"口径(与日历视图切换一致)。 */
|
||||||
|
|
||||||
|
/* ---- 视图切换条 ---- */
|
||||||
|
.trip-views {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
.trip-views .tv-btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
padding: 5px 14px; border-radius: 999px;
|
||||||
|
border: 1px solid var(--line); background: var(--surface);
|
||||||
|
color: var(--muted); font-size: 13.5px; cursor: pointer;
|
||||||
|
text-decoration: none; line-height: 1.5;
|
||||||
|
}
|
||||||
|
.trip-views .tv-btn:hover { color: var(--ink); border-color: var(--line-3, var(--line)); }
|
||||||
|
.trip-views .tv-btn.is-on {
|
||||||
|
background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600;
|
||||||
|
}
|
||||||
|
.trip-views .tv-btn svg { display: block; }
|
||||||
|
/* 右上角那颗「设为默认」小链接:与标签页同一行、靠右 */
|
||||||
|
.trip-views .tv-default {
|
||||||
|
margin-left: auto; font-size: 12.5px; color: var(--faint);
|
||||||
|
text-decoration: none; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.trip-views .tv-default:hover { color: var(--brand); text-decoration: none; }
|
||||||
|
/* 「地图(0)」里那个小计数:还没位置时提示一下,别让人点进去看空地图 */
|
||||||
|
.trip-views .tv-dim { font-size: 11.5px; opacity: .8; margin-left: 2px; }
|
||||||
|
|
||||||
|
/* ---- 地图容器 ----
|
||||||
|
⚠️ **必须有固定高度**:高德地图在"高度为 0 的容器"里渲染出来是空白
|
||||||
|
(这是高德最常见的坑,`.trip-map` 的 height 就是为它写的)。
|
||||||
|
⚠️ 高度跟视口走(屏幕越大看得越远),但给下限免得笔记本上太扁。 */
|
||||||
|
.trip-map-wrap { margin-top: 2px; }
|
||||||
|
.trip-map {
|
||||||
|
width: 100%; height: clamp(380px, 62vh, 720px);
|
||||||
|
border: 1px solid var(--line); border-radius: var(--radius);
|
||||||
|
overflow: hidden; background: #EEF1F4; /* 瓦片到位前不闪白 */
|
||||||
|
}
|
||||||
|
.trip-map-fallback {
|
||||||
|
margin: 0; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||||
|
color: var(--muted); font-size: 13.5px; padding: 0 24px; text-align: center;
|
||||||
|
}
|
||||||
|
.trip-map-hint { margin: 8px 0 0; }
|
||||||
|
|
||||||
|
/* ---- 地图上的标记:小卡片(左文右无图,比 TripLog 那版更轻)----
|
||||||
|
⚠️ 卡片是**绝对定位在画布中心**(JS 里 left/top = C+dx / C+dy):
|
||||||
|
画布固定 260×260,卡片偏移不影响标记锚点(AMap 按画布中心对齐坐标)。 */
|
||||||
|
.trip-pin-stage { position: relative; pointer-events: none; }
|
||||||
|
.trip-pin {
|
||||||
|
position: absolute; transform: translate(-50%, -50%);
|
||||||
|
pointer-events: auto; cursor: pointer;
|
||||||
|
}
|
||||||
|
.trip-pin-card {
|
||||||
|
background: var(--surface); border: 1px solid var(--line);
|
||||||
|
border-left: 3px solid var(--brand);
|
||||||
|
border-radius: 8px; padding: 6px 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,.10);
|
||||||
|
max-width: 190px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.trip-pin-title {
|
||||||
|
font-size: 13px; font-weight: 600; color: var(--ink);
|
||||||
|
overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.trip-pin-sub { font-size: 11.5px; color: var(--muted); margin-top: 1px; }
|
||||||
|
.trip-pin:hover .trip-pin-card { border-color: var(--brand); box-shadow: 0 4px 14px rgba(0,0,0,.16); }
|
||||||
|
/* ⚠️ AMap 的标记包装层尺寸跟着 content(260×260)走、默认可命中,
|
||||||
|
会把旁边卡片的点击整层挡住。整层禁掉命中、只留卡片本身可点
|
||||||
|
(TripLog 也踩过这个,见其 `.amap-marker` 那条注释)。 */
|
||||||
|
#tripMapCanvas .amap-marker { pointer-events: none !important; }
|
||||||
|
#tripMapCanvas .amap-marker .trip-pin { pointer-events: auto !important; }
|
||||||
|
|
||||||
|
/* ---- 显示方式设置页的单选卡片(v54 第 5 条)----
|
||||||
|
原生 radio + label 包一层卡片外观;`.is-on` 由服务端渲染时给
|
||||||
|
—— 不依赖 JS 同步选中态(无 JS 时至少"当前项"是对的)。 */
|
||||||
|
.view-choice { display: flex; flex-direction: column; gap: 10px; max-width: 480px; }
|
||||||
|
.view-choice-item {
|
||||||
|
display: flex; align-items: flex-start; gap: 10px;
|
||||||
|
padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius-s);
|
||||||
|
cursor: pointer; background: var(--surface);
|
||||||
|
}
|
||||||
|
.view-choice-item:hover { border-color: var(--brand-2); }
|
||||||
|
.view-choice-item.is-on { border-color: var(--brand); background: var(--brand-soft, #f2f7f3); }
|
||||||
|
.view-choice-item input { margin-top: 3px; flex: 0 0 auto; }
|
||||||
|
.view-choice-item .vci-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||||
|
.view-choice-item .vci-body b { font-size: 14px; }
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 旅程地图(v54 用户 2026-10-16 第 3 条)。
|
||||||
|
*
|
||||||
|
* 用户原话:「你去读一下 TripLog 的代码,在旅程这个模块,可以在地图上显示旅程数据。
|
||||||
|
* 1. 先读一下它之前那个代码是怎么实现的,在我们的旅程模块也实现一下。」
|
||||||
|
*
|
||||||
|
* ── 移植自 TripLog `templates/activities/index.php` 的那套(要点)──
|
||||||
|
* · 高德 JS API 2.0;`AMap.Map` 建图 + `AMap.Marker` 打点;
|
||||||
|
* · 每枚标记的 `position` 必须是 **[lng, lat] 数组**(传 `{lng,lat}` 裸对象
|
||||||
|
* 会让 AMap 抛错、整个绘制循环当场中断 —— TripLog 踩过这个,注释里写着
|
||||||
|
* "批 11 的光秃秃地图就是它");
|
||||||
|
* · `setFitView()` 自动框住所有点,但**上限 11 级**:只有一趟旅程时
|
||||||
|
* setFitView 会一路放大到街道级,那是导航视角、不是"足迹"视角;
|
||||||
|
* · 卡片(自绘 content)用**方形画布 + 居中定位**,这样卡片可以偏离真实坐标
|
||||||
|
* 而不影响标记锚点(TripLog 用它做"同城扇开")。
|
||||||
|
*
|
||||||
|
* ── 本版**不**做"同城扇开"(有意取舍)──
|
||||||
|
* TripLog 那个扇开是为了"同一座城市的多次旅行挤在一起"。
|
||||||
|
* 本仓的旅程列表通常没有那么多趟、且每枚卡片更窄(只有标题+时间),
|
||||||
|
* 先做"同一城市多点重合就按到达顺序错开一点"这种**轻量去重叠**即可 ——
|
||||||
|
* 完整扇开(环形均匀分布 + 引线 + 与容器边界求解)是几百行几何代码,
|
||||||
|
* 在没有真实数据压出这个问题之前不值得(TripLog 也是撞到了才写的)。
|
||||||
|
*
|
||||||
|
* ⚠️ 合规:地图是高德(白名单内),SDK 由 layout.php 的 `fb-amap` 按需提供,
|
||||||
|
* 本文件**不自己拼 SDK 地址、不碰 key**。
|
||||||
|
* ⚠️ SDK 未就位(没配 key / 加载失败)时降级成一句说明,不留白框。
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var el = document.getElementById('tripMapCanvas');
|
||||||
|
if (!el) { return; }
|
||||||
|
|
||||||
|
var points;
|
||||||
|
try {
|
||||||
|
points = JSON.parse(el.getAttribute('data-points') || '[]');
|
||||||
|
} catch (e) {
|
||||||
|
points = [];
|
||||||
|
}
|
||||||
|
if (!points || !points.length) { return; }
|
||||||
|
|
||||||
|
function fallback(msg) {
|
||||||
|
el.innerHTML = '<p class="trip-map-fallback">' + msg + '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s === null || s === undefined ? '' : s)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视野上限:别放到街道级(见文件头的说明) */
|
||||||
|
var MAX_ZOOM = 11;
|
||||||
|
|
||||||
|
/* 卡片画布:内容绝对居中,卡片可以在这块画布里自由偏移(本版只用于微错开) */
|
||||||
|
var CANVAS = 260;
|
||||||
|
var C = CANVAS / 2;
|
||||||
|
|
||||||
|
function boot() {
|
||||||
|
var map = new AMap.Map(el, { viewMode: '2D', zoom: 5, resizeEnable: true });
|
||||||
|
|
||||||
|
/* 轻量去重叠:同一位置的多个点(坐标差 < 极小阈值)沿一个方向的螺旋微移,
|
||||||
|
免得完全重合、只能点到一个。位移**只改卡片绘制位置**,
|
||||||
|
若做不明显,至少保证每枚都能被点到(content 是各自的 DOM)。 */
|
||||||
|
var seen = {};
|
||||||
|
var markers = [];
|
||||||
|
points.forEach(function (p, i) {
|
||||||
|
var key = p.lng.toFixed(3) + ',' + p.lat.toFixed(3);
|
||||||
|
var n = seen[key] = (seen[key] || 0);
|
||||||
|
seen[key]++;
|
||||||
|
var dx = 0, dy = 0;
|
||||||
|
if (n > 0) {
|
||||||
|
// 螺旋微移:每多重一个,就往外挪一点(角度按 n 递增)
|
||||||
|
var ang = n * (Math.PI / 3);
|
||||||
|
var rad = 26 + n * 6;
|
||||||
|
dx = Math.cos(ang) * rad;
|
||||||
|
dy = Math.sin(ang) * rad;
|
||||||
|
}
|
||||||
|
var html = '<div class="trip-pin" style="left:' + (C + dx) + 'px;top:' + (C + dy) + 'px;">'
|
||||||
|
+ '<div class="trip-pin-card">'
|
||||||
|
+ '<div class="trip-pin-title">' + esc(p.title) + '</div>'
|
||||||
|
+ (p.when ? '<div class="trip-pin-sub">' + esc(p.when) + '</div>' : '')
|
||||||
|
+ (p.city ? '<div class="trip-pin-sub">📍 ' + esc(p.city) + '</div>' : '')
|
||||||
|
+ '</div></div>';
|
||||||
|
var mk = new AMap.Marker({
|
||||||
|
// ⚠️ 必须是 [lng, lat] 数组(见文件头的说明)
|
||||||
|
position: [p.lng, p.lat],
|
||||||
|
content: '<div class="trip-pin-stage" style="width:' + CANVAS + 'px;height:' + CANVAS + 'px;">'
|
||||||
|
+ html + '</div>',
|
||||||
|
anchor: 'center',
|
||||||
|
map: map,
|
||||||
|
});
|
||||||
|
mk.on('click', function () { window.location.href = p.url; });
|
||||||
|
markers.push(mk);
|
||||||
|
});
|
||||||
|
|
||||||
|
map.setFitView(null, false, [40, 40, 40, 40]);
|
||||||
|
if (map.getZoom() > MAX_ZOOM) { map.setZoom(MAX_ZOOM); }
|
||||||
|
el.setAttribute('data-map-ready', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SDK 由 layout 的按需加载器提供(`window.fbAmapSdk()`)。
|
||||||
|
⚠️ 没配高德 key 时那个函数不存在 —— 直接降级。 */
|
||||||
|
if (typeof window.fbAmapSdk !== 'function') {
|
||||||
|
fallback('地图没启用(还没在配置里填高德 key);用左侧的「列表」看旅程。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.fbAmapSdk().then(function (ok) {
|
||||||
|
if (!ok || typeof window.AMap === 'undefined') {
|
||||||
|
fallback('地图脚本没加载成功,暂时看不了地图;用左侧的「列表」看旅程。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boot();
|
||||||
|
} catch (e) {
|
||||||
|
fallback('地图初始化失败:' + (e && e.message ? e.message : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -1070,7 +1070,12 @@ switch ($r) {
|
|||||||
// 不再传它,模板也不再读(多传一个变量迟早有人用它,把分栏又带回来)。
|
// 不再传它,模板也不再读(多传一个变量迟早有人用它,把分栏又带回来)。
|
||||||
'groups' => PersonGroup::orderFor($familyId, $me),
|
'groups' => PersonGroup::orderFor($familyId, $me),
|
||||||
// v28 用户第 5 条:标签(筛选条 + 卡片上的标签 + 编辑标签时的候选)
|
// v28 用户第 5 条:标签(筛选条 + 卡片上的标签 + 编辑标签时的候选)
|
||||||
|
// 🔴 v54(第 2 条):筛选条与"打标签候选"是**两件事**,别混:
|
||||||
|
// · 筛选条只该列**真有人的**标签(cloud:JOIN 了名册,点进去不会空)——
|
||||||
|
// 把没人的词也列上去,会出现"写着 5 个、点进去 0 个";
|
||||||
|
// · 打标签的候选要**词表 ∪ 用过的词**(模板里的弹层用 tagChoices)。
|
||||||
'tagCloud' => PersonTag::cloud($familyId, $me),
|
'tagCloud' => PersonTag::cloud($familyId, $me),
|
||||||
|
'tagChoices' => PersonTag::choices($familyId, $me),
|
||||||
'tagFilter' => $tagFilter,
|
'tagFilter' => $tagFilter,
|
||||||
'q' => $q,
|
'q' => $q,
|
||||||
'meId' => $me,
|
'meId' => $me,
|
||||||
@@ -1299,7 +1304,9 @@ switch ($r) {
|
|||||||
if ($method !== 'POST') {
|
if ($method !== 'POST') {
|
||||||
View::render('people/import', [
|
View::render('people/import', [
|
||||||
'title' => '导入联系人',
|
'title' => '导入联系人',
|
||||||
|
// ⚠️ v54:`groups` 仍传(兼容),新界面用 `tagChoices`(打标签候选)
|
||||||
'groups' => PersonGroup::orderFor($familyId, $me),
|
'groups' => PersonGroup::orderFor($familyId, $me),
|
||||||
|
'tagChoices' => PersonTag::choices($familyId, $me),
|
||||||
'report' => null,
|
'report' => null,
|
||||||
], 'people');
|
], 'people');
|
||||||
break;
|
break;
|
||||||
@@ -1325,7 +1332,9 @@ switch ($r) {
|
|||||||
redirect('people/import');
|
redirect('people/import');
|
||||||
}
|
}
|
||||||
$groupId = (int)($_POST['group_id'] ?? 0);
|
$groupId = (int)($_POST['group_id'] ?? 0);
|
||||||
$res = ContactImporter::run($familyId, $me, $records, $groupId);
|
// 🔴 v54(第 2 条):导入时可以顺手打一串标签(原来只给一个分组)
|
||||||
|
$impTags = PersonTag::clean((string)($_POST['tags'] ?? ''));
|
||||||
|
$res = ContactImporter::run($familyId, $me, $records, $groupId, $impTags);
|
||||||
|
|
||||||
$msg = '导入完成:新增 ' . $res['added'] . ' 人,补充信息 ' . $res['updated'] . ' 人';
|
$msg = '导入完成:新增 ' . $res['added'] . ' 人,补充信息 ' . $res['updated'] . ' 人';
|
||||||
if ($res['skipped'] > 0) {
|
if ($res['skipped'] > 0) {
|
||||||
@@ -1335,6 +1344,7 @@ switch ($r) {
|
|||||||
View::render('people/import', [
|
View::render('people/import', [
|
||||||
'title' => '导入联系人',
|
'title' => '导入联系人',
|
||||||
'groups' => PersonGroup::orderFor($familyId, $me),
|
'groups' => PersonGroup::orderFor($familyId, $me),
|
||||||
|
'tagChoices' => PersonTag::choices($familyId, $me),
|
||||||
'report' => $res,
|
'report' => $res,
|
||||||
], 'people');
|
], 'people');
|
||||||
break;
|
break;
|
||||||
@@ -1358,6 +1368,11 @@ switch ($r) {
|
|||||||
csrf_check();
|
csrf_check();
|
||||||
$ids = array_map('intval', (array)($_POST['ids'] ?? []));
|
$ids = array_map('intval', (array)($_POST['ids'] ?? []));
|
||||||
$gid = (int)($_POST['group_id'] ?? 0);
|
$gid = (int)($_POST['group_id'] ?? 0);
|
||||||
|
/* 🔴 v54(用户 2026-10-16 第 2 条):挑人时改成**打标签**。
|
||||||
|
⚠️ `group_id` 仍然收着(老书签/脚本、以及别的调用方可能还在传),
|
||||||
|
只是新界面**不再展示**下拉 —— 有值就照旧写进 roster.group_id,
|
||||||
|
没值(新界面的常态)就 NULL,与"未分类"同义。 */
|
||||||
|
$pickTags = PersonTag::clean((string)($_POST['tags'] ?? ''));
|
||||||
// 只认「确实可挑」的那些 id —— 表单可以改,不能凭它把别人家的拉进来
|
// 只认「确实可挑」的那些 id —— 表单可以改,不能凭它把别人家的拉进来
|
||||||
$ok = array_column($familyPick, 'id');
|
$ok = array_column($familyPick, 'id');
|
||||||
$ok = array_map('intval', $ok);
|
$ok = array_map('intval', $ok);
|
||||||
@@ -1368,6 +1383,10 @@ switch ($r) {
|
|||||||
}
|
}
|
||||||
if (PersonRoster::ensure($familyId, $me, $pid, $gid > 0 ? $gid : null, 'self')) {
|
if (PersonRoster::ensure($familyId, $me, $pid, $gid > 0 ? $gid : null, 'self')) {
|
||||||
$n++;
|
$n++;
|
||||||
|
// 这串标签给**每个**新进名册的人各打一遍(见模板里的说明)
|
||||||
|
foreach ($pickTags as $tg) {
|
||||||
|
PersonTag::addOne($familyId, $me, $pid, $tg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flash($n > 0 ? 'ok' : 'err', $n > 0
|
flash($n > 0 ? 'ok' : 'err', $n > 0
|
||||||
@@ -1378,7 +1397,9 @@ switch ($r) {
|
|||||||
View::render('people/pick', [
|
View::render('people/pick', [
|
||||||
'title' => '从家里已有的人里挑',
|
'title' => '从家里已有的人里挑',
|
||||||
'people' => $familyPick,
|
'people' => $familyPick,
|
||||||
|
// ⚠️ v54:`groups` 仍传(老模板/兼容),但新界面用 `tagChoices` 打标签
|
||||||
'groups' => PersonGroup::orderFor($familyId, $me),
|
'groups' => PersonGroup::orderFor($familyId, $me),
|
||||||
|
'tagChoices' => PersonTag::choices($familyId, $me),
|
||||||
'meId' => $me,
|
'meId' => $me,
|
||||||
], 'people');
|
], 'people');
|
||||||
break;
|
break;
|
||||||
@@ -1942,7 +1963,7 @@ switch ($r) {
|
|||||||
// 🔴 v53(第 4/5 条):新增页也要能打标签 + 填「我对他的称呼 / 备注」。
|
// 🔴 v53(第 4/5 条):新增页也要能打标签 + 填「我对他的称呼 / 备注」。
|
||||||
// 标签候选与编辑页同一个来源(我名下用过的全部标签)。
|
// 标签候选与编辑页同一个来源(我名下用过的全部标签)。
|
||||||
'myTags' => [],
|
'myTags' => [],
|
||||||
'tagChoices' => array_column(PersonTag::cloud((int)Auth::familyId(), (int)Auth::personId()), 'tag'),
|
'tagChoices' => PersonTag::choices((int)Auth::familyId(), (int)Auth::personId()),
|
||||||
'myNick' => '',
|
'myNick' => '',
|
||||||
], 'people');
|
], 'people');
|
||||||
break;
|
break;
|
||||||
@@ -2020,7 +2041,7 @@ switch ($r) {
|
|||||||
// 🔴 v53:新增页新加的这三样也要**原样回显**,否则一撞名/一报错
|
// 🔴 v53:新增页新加的这三样也要**原样回显**,否则一撞名/一报错
|
||||||
// 用户刚打的标签、刚写的称呼与备注就全没了(重填一遍)。
|
// 用户刚打的标签、刚写的称呼与备注就全没了(重填一遍)。
|
||||||
'myTags' => PersonTag::clean((string)($_POST['tags'] ?? '')),
|
'myTags' => PersonTag::clean((string)($_POST['tags'] ?? '')),
|
||||||
'tagChoices' => array_column(PersonTag::cloud((int)Auth::familyId(), (int)Auth::personId()), 'tag'),
|
'tagChoices' => PersonTag::choices((int)Auth::familyId(), (int)Auth::personId()),
|
||||||
'myNick' => trim((string)($_POST['my_nick'] ?? '')),
|
'myNick' => trim((string)($_POST['my_nick'] ?? '')),
|
||||||
'confirmHit' => $hit,
|
'confirmHit' => $hit,
|
||||||
'confirmWhy' => (string)($hit['_match'] ?? 'name'),
|
'confirmWhy' => (string)($hit['_match'] ?? 'name'),
|
||||||
@@ -2074,7 +2095,7 @@ switch ($r) {
|
|||||||
'jobRows' => (array)($d['jobs'] ?? []),
|
'jobRows' => (array)($d['jobs'] ?? []),
|
||||||
'myNote' => trim((string)($_POST['my_note'] ?? '')),
|
'myNote' => trim((string)($_POST['my_note'] ?? '')),
|
||||||
'myTags' => PersonTag::clean((string)($_POST['tags'] ?? '')),
|
'myTags' => PersonTag::clean((string)($_POST['tags'] ?? '')),
|
||||||
'tagChoices' => array_column(PersonTag::cloud($familyId, (int)Auth::personId()), 'tag'),
|
'tagChoices' => PersonTag::choices($familyId, (int)Auth::personId()),
|
||||||
'myNick' => trim((string)($_POST['my_nick'] ?? '')),
|
'myNick' => trim((string)($_POST['my_nick'] ?? '')),
|
||||||
], 'people');
|
], 'people');
|
||||||
exit;
|
exit;
|
||||||
@@ -2104,7 +2125,7 @@ switch ($r) {
|
|||||||
'jobRows' => (array)($d['jobs'] ?? []),
|
'jobRows' => (array)($d['jobs'] ?? []),
|
||||||
'myNote' => trim((string)($_POST['my_note'] ?? '')),
|
'myNote' => trim((string)($_POST['my_note'] ?? '')),
|
||||||
'myTags' => PersonTag::clean((string)($_POST['tags'] ?? '')),
|
'myTags' => PersonTag::clean((string)($_POST['tags'] ?? '')),
|
||||||
'tagChoices' => array_column(PersonTag::cloud($familyId, (int)Auth::personId()), 'tag'),
|
'tagChoices' => PersonTag::choices($familyId, (int)Auth::personId()),
|
||||||
'myNick' => trim((string)($_POST['my_nick'] ?? '')),
|
'myNick' => trim((string)($_POST['my_nick'] ?? '')),
|
||||||
], 'people');
|
], 'people');
|
||||||
exit;
|
exit;
|
||||||
@@ -2191,12 +2212,14 @@ switch ($r) {
|
|||||||
⇒ 这一页补上「标签」编辑(与名册列表上那颗标签按钮同一份数据源)。
|
⇒ 这一页补上「标签」编辑(与名册列表上那颗标签按钮同一份数据源)。
|
||||||
⚠️ 标签是**每人一份**的(owner = 我),所以只有"我"名册里的标签有意义:
|
⚠️ 标签是**每人一份**的(owner = 我),所以只有"我"名册里的标签有意义:
|
||||||
· `myTags` 这个人当前在我名下的标签 → 回填输入框;
|
· `myTags` 这个人当前在我名下的标签 → 回填输入框;
|
||||||
· `tagChoices` 我名下**用过**的全部标签 → 给可点选的候选(与列表页同一个
|
🔴 v54(用户 2026-10-16 第 2 条):候选改成 `PersonTag::choices()` ——
|
||||||
`PersonTag::cloud()`,两处口径一致)。
|
**设置页配的词表 ∪ 我用过的词**(原来只读 `cloud()` = 用过的词,
|
||||||
|
于是"设置里列着一堆、打标签时却只有几个")。
|
||||||
|
列表页、新增页、编辑页**三处同一个函数**,口径必须一致。
|
||||||
⚠️ 改自己($isSelf)不给标签:标签是"我给别人打的",给自己打没有意义
|
⚠️ 改自己($isSelf)不给标签:标签是"我给别人打的",给自己打没有意义
|
||||||
(名册页那颗标签按钮也只在 `$canMove` 时出现)。 */
|
(名册页那颗标签按钮也只在 `$canMove` 时出现)。 */
|
||||||
'myTags' => $isSelf ? [] : PersonTag::of((int)Auth::personId(), $id),
|
'myTags' => $isSelf ? [] : PersonTag::of((int)Auth::personId(), $id),
|
||||||
'tagChoices' => $isSelf ? [] : array_column(PersonTag::cloud((int)Auth::familyId(), (int)Auth::personId()), 'tag'),
|
'tagChoices' => $isSelf ? [] : PersonTag::choices((int)Auth::familyId(), (int)Auth::personId()),
|
||||||
// 🔴 v53(第 5 条):编辑页也要回显「我对他的称呼」(与新增页同一个字段名),
|
// 🔴 v53(第 5 条):编辑页也要回显「我对他的称呼」(与新增页同一个字段名),
|
||||||
// 否则在新增页填了称呼、到编辑页却看不见(会以为没存上)。
|
// 否则在新增页填了称呼、到编辑页却看不见(会以为没存上)。
|
||||||
'myNick' => $isSelf ? '' : Nickname::get((int)Auth::personId(), $id),
|
'myNick' => $isSelf ? '' : Nickname::get((int)Auth::personId(), $id),
|
||||||
@@ -2276,7 +2299,7 @@ switch ($r) {
|
|||||||
⚠️ 取自 `$_POST['tags']`(用户刚提交的那份),**不是**库里的旧值 ——
|
⚠️ 取自 `$_POST['tags']`(用户刚提交的那份),**不是**库里的旧值 ——
|
||||||
这才叫"保留用户填的东西",`clear_old`/`old()` 那一套同理。 */
|
这才叫"保留用户填的东西",`clear_old`/`old()` 那一套同理。 */
|
||||||
'myTags' => $isSelf ? [] : PersonTag::clean((string)($_POST['tags'] ?? '')),
|
'myTags' => $isSelf ? [] : PersonTag::clean((string)($_POST['tags'] ?? '')),
|
||||||
'tagChoices' => $isSelf ? [] : array_column(PersonTag::cloud($familyId, (int)Auth::personId()), 'tag'),
|
'tagChoices' => $isSelf ? [] : PersonTag::choices($familyId, (int)Auth::personId()),
|
||||||
], 'people');
|
], 'people');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -2308,7 +2331,7 @@ switch ($r) {
|
|||||||
'contacts' => $contacts,
|
'contacts' => $contacts,
|
||||||
// v51:同上面那条 —— 重画时保留用户刚提交的标签
|
// v51:同上面那条 —— 重画时保留用户刚提交的标签
|
||||||
'myTags' => $isSelf ? [] : PersonTag::clean((string)($_POST['tags'] ?? '')),
|
'myTags' => $isSelf ? [] : PersonTag::clean((string)($_POST['tags'] ?? '')),
|
||||||
'tagChoices' => $isSelf ? [] : array_column(PersonTag::cloud($familyId, (int)Auth::personId()), 'tag'),
|
'tagChoices' => $isSelf ? [] : PersonTag::choices($familyId, (int)Auth::personId()),
|
||||||
], 'people');
|
], 'people');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -3672,11 +3695,20 @@ switch ($r) {
|
|||||||
$keyword = trim((string)($_GET['keyword'] ?? ''));
|
$keyword = trim((string)($_GET['keyword'] ?? ''));
|
||||||
$rows = Trip::all($familyId, $keyword);
|
$rows = Trip::all($familyId, $keyword);
|
||||||
Trip::attachAttendees($rows, (int)Auth::personId());
|
Trip::attachAttendees($rows, (int)Auth::personId());
|
||||||
|
/* 🔴 v54(用户 2026-10-16 第 3 条):「在我们的旅程模块也实现一下」地图显示。
|
||||||
|
`attachSpots` 给每趟旅程算一个**代表坐标**(各段行程 from/to 的重心;
|
||||||
|
没有坐标就退到城市中心,离线表、不发网络请求)—— 地图视图用它打点。
|
||||||
|
⚠️ `Trip::all()` 内部**已经**调过 `attachDerived()`(挂 `cities`),
|
||||||
|
所以这里**不能再调一次** —— 重复跑一遍那几条聚合查询是白费。
|
||||||
|
⚠️ 它对**列表视图**也无害(只是多挂几个键,模板不读就不显示)。 */
|
||||||
|
Trip::attachSpots($rows);
|
||||||
View::render('trips/index', [
|
View::render('trips/index', [
|
||||||
'title' => '旅程',
|
'title' => '旅程',
|
||||||
'rows' => $rows,
|
'rows' => $rows,
|
||||||
'total' => count($rows),
|
'total' => count($rows),
|
||||||
'keyword' => $keyword,
|
'keyword' => $keyword,
|
||||||
|
/* 🔴 v54(第 5 条):默认看列表还是地图(每人一份,见 trips/display)。 */
|
||||||
|
'tripDefaultView' => (string)UserSetting::get((int)Auth::personId(), 'trip_default_view', 'list'),
|
||||||
], 'trips');
|
], 'trips');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -3980,6 +4012,47 @@ switch ($r) {
|
|||||||
|
|
||||||
// ---------------- 旅程设置:车辆 / 出行方式 / 节点类型(用户第 8 条)----------------
|
// ---------------- 旅程设置:车辆 / 出行方式 / 节点类型(用户第 8 条)----------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔴 v54(用户 2026-10-16 第 5 条):「里面的设置让用户自己配置:
|
||||||
|
* **到底默认显示旅程列表,还是用地图显示**。」
|
||||||
|
* ⇒ 旅程「显示方式」设置页。
|
||||||
|
*
|
||||||
|
* 存在 `user_settings.trip_default_view`(**每人一份**,与看板色/天气地点同一层)
|
||||||
|
* —— 这是"我进来先看哪个",不是全家的偏好。
|
||||||
|
*
|
||||||
|
* `?set=list|map` 是**列表页上那颗"设为默认"小链接**用的 —— 它直接改设置再回列表,
|
||||||
|
* 省去"点进设置页 → 选 → 保存 → 再回来"四步(那正是用户抱怨过的那种绕)。
|
||||||
|
* ⚠️ 用 GET 传值只到"点一下就设默认"这种**幂等的个人偏好**为止:
|
||||||
|
* 没有副作用、不影响别人;真正的表单(页面里那两个 radio)仍走 POST + csrf。
|
||||||
|
* ⚠️ 访客(canWrite=false)也能改 —— 它只影响自己看到的默认,不是数据变更。
|
||||||
|
*/
|
||||||
|
case 'trips/display':
|
||||||
|
$me = (int)Auth::personId();
|
||||||
|
$set = (string)($_GET['set'] ?? '');
|
||||||
|
if (in_array($set, ['list', 'map'], true)) {
|
||||||
|
UserSetting::set($me, 'trip_default_view', $set);
|
||||||
|
flash('ok', '已把「' . ($set === 'map' ? '地图' : '列表') . '」设为打开旅程时的默认视图。');
|
||||||
|
redirect('trips');
|
||||||
|
}
|
||||||
|
View::render('trips/display', [
|
||||||
|
'title' => '显示方式',
|
||||||
|
'current' => (string)UserSetting::get($me, 'trip_default_view', 'list'),
|
||||||
|
], 'trips');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'trips/display_save':
|
||||||
|
Auth::requireLogin();
|
||||||
|
csrf_check();
|
||||||
|
$me = (int)Auth::personId();
|
||||||
|
$val = (string)($_POST['view'] ?? '');
|
||||||
|
if (!in_array($val, ['list', 'map'], true)) {
|
||||||
|
flash('err', '没看懂要设成哪种显示方式。');
|
||||||
|
redirect('trips/display');
|
||||||
|
}
|
||||||
|
UserSetting::set($me, 'trip_default_view', $val);
|
||||||
|
flash('ok', '已保存:以后打开旅程默认看「' . ($val === 'map' ? '地图' : '列表') . '」。');
|
||||||
|
redirect('trips/display');
|
||||||
|
|
||||||
case 'trips/vehicles':
|
case 'trips/vehicles':
|
||||||
$familyId = (int)Auth::familyId();
|
$familyId = (int)Auth::familyId();
|
||||||
View::render('trips/vehicles', [
|
View::render('trips/vehicles', [
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 「名册分组」管理卡片 —— 从个人设置**搬出来**的(v52 用户 2026-10-15 第 6 条)。
|
* 「名册标签」词表管理卡片 —— 从个人设置**搬出来**的(v52 用户 2026-10-15 第 6 条),
|
||||||
|
* v54(用户 2026-10-16 第 2 条)**从"分组"改口径为"标签词表"**。
|
||||||
*
|
*
|
||||||
* 用户原话:「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
|
* 用户原话(v52):「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
|
||||||
* 点击后下拉设置,第一个就是导入联系人,第二个就是原来个人设置那里的
|
* 点击后下拉设置,第一个就是导入联系人,第二个就是原来个人设置那里的
|
||||||
* **名册分组**,这样**个人设置中就不显示名册分组的内容**了。」
|
* **名册分组**,这样**个人设置中就不显示名册分组的内容**了。」
|
||||||
*
|
*
|
||||||
* ⇒ 同一个卡片现在挂在「名册 → 设置 → 名册分组」这一页上(`templates/people/settings.php`)。
|
* 用户原话(v54,本次):「现在名册里面给人打标签的时候,这个标签数据是从哪读出来的呀?
|
||||||
|
* 在这个名册的设置页面,有很多分组,现在还叫分组。但是打标签的时候,
|
||||||
|
* 却不是从这些分组里读出来的。所以现在应该在这个设置页面,不应该再叫
|
||||||
|
* 「名册分组」了吧?应该叫「名册标签」,是不是?**应该从这里面把所有标签都读出来**吧?」
|
||||||
|
*
|
||||||
|
* 🔴 澄清一个容易混的点(也是用户困惑的根源):
|
||||||
|
* 这张卡背后的表仍然是 `person_groups`(**表名没改**,因为改名要背着
|
||||||
|
* `PersonGroup` 类、`person_roster.group_id`、其它会话正在写的 App-API 一起动,
|
||||||
|
* 风险远大于收益)—— 但它的**语义**从 v28 起就已经是"**标签词表**":
|
||||||
|
* 这里配的每个词,现在会出现在名册里打标签时的**候选列表**里
|
||||||
|
* (见 `App\PersonTag::choices()`:**这张表 ∪ 我用过的词**)。
|
||||||
|
*
|
||||||
|
* 所以:
|
||||||
|
* · 界面上**一律叫"标签"**,不再出现"分组"二字;
|
||||||
|
* · 这里加的词 = "预定义标签",还没用过也能一点就打上;
|
||||||
|
* · 某个人身上真有哪几个标签,仍然在名册卡片上那颗标签按钮里改。
|
||||||
|
*
|
||||||
* ⚠️ 表单 action 仍指向 `settings/groups/*`(**一个字段都没改**)——
|
* ⚠️ 表单 action 仍指向 `settings/groups/*`(**一个字段都没改**)——
|
||||||
* 分组管理是"我自己的分组",与它挂在哪一页无关;
|
* 它是"我自己的词表",与它挂在哪一页、叫什么名字无关;
|
||||||
* 换路由只会白白多一处要维护的东西(而且老书签/脚本会失效)。
|
* 换路由只会白白多一处要维护的东西(而且老书签/脚本会失效)。
|
||||||
*
|
*
|
||||||
* 变量(require 前设好;**入参仍用宿主那边的名字**,片段内部一律 pg* 前缀,见下):
|
* 变量(require 前设好;**入参仍用宿主那边的名字**,片段内部一律 pg* 前缀,见下):
|
||||||
* $groups PersonGroup::orderFor(...) 的结果(公共 + 我的)
|
* $groups PersonGroup::orderFor(...) 的结果(公共同我的)
|
||||||
* $groupCounts gid => 人数
|
* $groupCounts gid => 人数
|
||||||
* $rosterCount 我的名册人数
|
* $rosterCount 我的名册人数
|
||||||
*/
|
*/
|
||||||
@@ -25,28 +42,38 @@ $pgTotal = count($pgGroups);
|
|||||||
?>
|
?>
|
||||||
<div class="card card-pad">
|
<div class="card card-pad">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
名册分组
|
名册标签
|
||||||
<span class="hint">公共的 + 我自己加的</span>
|
<span class="hint">公共的 + 我自己加的</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="small muted" style="margin:0 0 16px">
|
<p class="small muted" style="margin:0 0 16px">
|
||||||
名册是按「关系分组」分栏显示的。这里有两层:
|
这里配的是**给名册成员打标签时可以一点就选的词**。
|
||||||
<b>公共分组</b>由管理员在「系统设置」里维护(全家都能用);
|
<b>公共标签</b>由管理员在「系统设置」里维护(全家都能用);
|
||||||
你要是觉得不够,就在下面<b>加只给自己看的分组</b>,别人看不到、也不受影响。
|
你要是觉得不够,就在下面<b>加只给自己看的标签</b>,别人看不到、也不受影响。
|
||||||
|
<br>
|
||||||
|
一个人可以有多个标签(他既是「亲戚」又是「球友」),所以标签不分组、不分栏。
|
||||||
|
谁身上真有哪个标签,在名册里点他那张卡片上的标签按钮就能改。
|
||||||
<br>
|
<br>
|
||||||
列表里 <b>公共</b> 标的是大家共用的那几个。
|
列表里 <b>公共</b> 标的是大家共用的那几个。
|
||||||
哪一组排前面也是你自己的习惯,用 ▲ ▼ 调 —— 别人看到的顺序不变。
|
哪个排前面也是你自己的习惯,用 ▲ ▼ 调 —— 别人看到的顺序不变。
|
||||||
<br>
|
<br>
|
||||||
你的名册现在有 <b><?= $pgRosterN ?></b> 人。
|
你的名册现在有 <b><?= $pgRosterN ?></b> 人。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<?php if (!$pgGroups): ?>
|
<?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: ?>
|
<?php else: ?>
|
||||||
<ol class="order-list">
|
<ol class="order-list">
|
||||||
<?php foreach ($pgGroups as $i => $g): $pgGid = (int)$g['id']; $pgOwn = (int)$g['owner_id']; ?>
|
<?php foreach ($pgGroups as $i => $g): $pgGid = (int)$g['id']; $pgOwn = (int)$g['owner_id']; ?>
|
||||||
<li class="order-row">
|
<li class="order-row">
|
||||||
<span class="order-no"><?= $i + 1 ?></span>
|
<span class="order-no"><?= $i + 1 ?></span>
|
||||||
<span class="order-name">
|
<span class="order-name">
|
||||||
|
<?php /* ⚠️ 这里**不加 `#`** —— 词表里存的是**词本身**(`家人`),
|
||||||
|
`#` 只是名册里打标签时的显示习惯(`#家人`)。
|
||||||
|
在词表管理页加上 `#`,会让人以为"输入时要带 #",
|
||||||
|
而实际 `PersonTag::clean()` 会把 `#` 剥掉 —— 两处显示不一致。
|
||||||
|
🔴 v54:e2e 的顺序断言 `my_group_order()` 抓的是 `<ol class="order-list">`
|
||||||
|
里 `order-name` 的**第一个子串**,多出来的 `#` 会让它读成 `#家人`
|
||||||
|
⇒ 断言红(这也是"界面多加一个字符就可能打歪既有断言"的又一例)。 */ ?>
|
||||||
<?= e((string)$g['name']) ?>
|
<?= e((string)$g['name']) ?>
|
||||||
<?php if ($pgOwn === 0): ?><span class="badge">公共</span><?php else: ?><span class="badge badge-open">我的</span><?php endif; ?>
|
<?php if ($pgOwn === 0): ?><span class="badge">公共</span><?php else: ?><span class="badge badge-open">我的</span><?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
@@ -56,9 +83,9 @@ $pgTotal = count($pgGroups);
|
|||||||
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
||||||
<input type="hidden" name="scope" value="me">
|
<input type="hidden" name="scope" value="me">
|
||||||
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="up"
|
<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"
|
<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>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?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">
|
<form method="post" action="<?= e(url('settings/groups/add')) ?>" class="inline-form" style="margin-top:16px">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="scope" value="me">
|
<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="例如:球友、老同学">
|
<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>
|
</form>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// 只有我自己建的分组才给改名 / 删除 —— 公共分组要在系统设置里改(那是管理员的事),
|
// 只有我自己建的标签才给改名 / 删除 —— 公共标签要在系统设置里改(那是管理员的事),
|
||||||
// 免得有人在自己设置里把"家人"改成别的、影响全家。
|
// 免得有人在自己设置里把"家人"改成别的、影响全家。
|
||||||
$pgMine = array_values(array_filter($pgGroups, fn($g) => (int)$g['owner_id'] > 0));
|
$pgMine = array_values(array_filter($pgGroups, fn($g) => (int)$g['owner_id'] > 0));
|
||||||
?>
|
?>
|
||||||
@@ -86,7 +113,7 @@ $pgTotal = count($pgGroups);
|
|||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
||||||
<input type="hidden" name="scope" value="me">
|
<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"
|
<input class="input" id="gname<?= $pgGid ?>" name="name" maxlength="20"
|
||||||
value="<?= e((string)$g['name']) ?>">
|
value="<?= e((string)$g['name']) ?>">
|
||||||
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
|
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
|
||||||
@@ -99,7 +126,12 @@ $pgTotal = count($pgGroups);
|
|||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
||||||
<input type="hidden" name="scope" value="me">
|
<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>
|
<button class="btn btn-ghost btn-sm danger" type="submit">确认删除</button>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
@@ -110,7 +142,7 @@ $pgTotal = count($pgGroups);
|
|||||||
|
|
||||||
<?php if (\App\Auth::isAdmin()): ?>
|
<?php if (\App\Auth::isAdmin()): ?>
|
||||||
<p class="small muted" style="margin:16px 0 0">
|
<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>
|
</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -473,6 +473,10 @@ unset($evRef);
|
|||||||
行里还有复选框(勾一下完成)与其它链接,
|
行里还有复选框(勾一下完成)与其它链接,
|
||||||
整行可点会让"勾选"和"打开"互相抢。 */ ?>
|
整行可点会让"勾选"和"打开"互相抢。 */ ?>
|
||||||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
<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([
|
data-fb-entry='<?= fb_entry_json([
|
||||||
'title' => (string)$t['title'],
|
'title' => (string)$t['title'],
|
||||||
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
|
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
|
||||||
|
|||||||
+82
-25
@@ -77,7 +77,19 @@ $isLoggedIn = \App\Auth::check();
|
|||||||
$_amapJs = \App\Geo::jsConfig();
|
$_amapJs = \App\Geo::jsConfig();
|
||||||
$_needAddr = ($_amapJs['key'] !== '');
|
$_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([
|
<meta name="fb-addr" content="<?= e(json_encode([
|
||||||
'js' => asset('/assets/js/addr_suggest.js'),
|
'js' => asset('/assets/js/addr_suggest.js'),
|
||||||
'sdk' => 'https://webapi.amap.com/maps?v=2.0&key=' . rawurlencode($_amapJs['key'])
|
'sdk' => 'https://webapi.amap.com/maps?v=2.0&key=' . rawurlencode($_amapJs['key'])
|
||||||
@@ -304,33 +316,32 @@ window.FB_NOTIFY = {
|
|||||||
与 person_picker 的 `fbLoadPicker()` 完全同一套路。 */ ?>
|
与 person_picker 的 `fbLoadPicker()` 完全同一套路。 */ ?>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var meta = document.querySelector('meta[name="fb-addr"]');
|
/* 🔴 v54:配置源改成 `fb-amap`(**只要有高德 key 就输出**,与本页是否需要联想无关)——
|
||||||
if (!meta) { return; } // 没配高德 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;
|
var conf;
|
||||||
try { conf = JSON.parse(meta.getAttribute('content')); } catch (e) { return; }
|
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;
|
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() {
|
function loadOurs() {
|
||||||
if (window.fbAddr) { return Promise.resolve(true); }
|
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) {
|
return new Promise(function (res) {
|
||||||
var s = document.createElement('script');
|
var s = document.createElement('script');
|
||||||
s.src = conf.js;
|
s.src = jsUrl;
|
||||||
s.defer = true;
|
s.defer = true;
|
||||||
s.onload = function () { res(true); };
|
s.onload = function () { res(true); };
|
||||||
s.onerror = function () { res(false); };
|
s.onerror = function () { res(false); };
|
||||||
@@ -338,15 +349,47 @@ window.FB_NOTIFY = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 安全密钥必须在 SDK **之前**设好(高德要求),否则 AutoComplete 会被拒。 */
|
/**
|
||||||
function boot() {
|
* 🔴 v54(用户 2026-10-16 第 3 条):把"**只拉高德地图 SDK**"这一步单独暴露出来。
|
||||||
/* ⚠️ 安全密钥必须在 SDK **之前**设好(高德硬要求),否则 AutoComplete 会被拒。
|
*
|
||||||
放在这里而不是页面顶部:没用到地址联想的页面不该出现这两个凭据。 */
|
* 为什么不能只留 `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) {
|
if (!window._AMapSecurityConfig) {
|
||||||
window._AMapSecurityConfig = { securityJsCode: conf.security || '' };
|
window._AMapSecurityConfig = { securityJsCode: conf.security || '' };
|
||||||
}
|
}
|
||||||
return loadSdk().then(function () {
|
sdkReady = new Promise(function (res) {
|
||||||
if (!window.__AMAP_LOADED || typeof window.AMap === 'undefined') { return false; }
|
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();
|
return loadOurs();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -378,6 +421,20 @@ window.FB_NOTIFY = {
|
|||||||
</script>
|
</script>
|
||||||
<script src="<?= e(asset('/assets/js/filter.js')) ?>"></script>
|
<script src="<?= e(asset('/assets/js/filter.js')) ?>"></script>
|
||||||
<script src="<?= e(asset('/assets/js/preview.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 条):笔记**详情页**的分段编辑(就地改一段、不跳整页编辑器)。
|
<?php /* 🔴 v52(用户第 4 条):笔记**详情页**的分段编辑(就地改一段、不跳整页编辑器)。
|
||||||
⚠️ 只在阅读态需要它 —— 判据用 `.note-read-body[data-note-id]`(模板在阅读态才输出),
|
⚠️ 只在阅读态需要它 —— 判据用 `.note-read-body[data-note-id]`(模板在阅读态才输出),
|
||||||
编辑页没有这个元素,因此不会白拉一份脚本。 */ ?>
|
编辑页没有这个元素,因此不会白拉一份脚本。 */ ?>
|
||||||
|
|||||||
@@ -9,9 +9,14 @@
|
|||||||
* ⇒ 名册页右上角从此只有两颗:**添加成员** + **☰ 设置**(设置恒在最右,全站统一)。
|
* ⇒ 名册页右上角从此只有两颗:**添加成员** + **☰ 设置**(设置恒在最右,全站统一)。
|
||||||
* 原来散着的那几颗(从已有家里人里挑 / 导入联系人 / …)全部收进这个下拉。
|
* 原来散着的那几颗(从已有家里人里挑 / 导入联系人 / …)全部收进这个下拉。
|
||||||
*
|
*
|
||||||
|
* 🔴 v54(用户 2026-10-16 第 2 条):第二行**从「名册分组」改叫「名册标签」**。
|
||||||
|
* 用户原话:「现在应该在这个设置页面,不应该再叫「名册分组」了吧?
|
||||||
|
* 应该叫「名册标签」,是不是?应该从这里面把所有标签都读出来吧?」
|
||||||
|
* ⇒ 文案改掉;指向的仍是同一个页面(`people/settings`)与同一个词表。
|
||||||
|
*
|
||||||
* 两行(顺序是用户点名的):
|
* 两行(顺序是用户点名的):
|
||||||
* ① 导入联系人 → `people/import`
|
* ① 导入联系人 → `people/import`
|
||||||
* ② 名册分组 → `people/settings`(从个人设置搬过来的那一块)
|
* ② 名册标签 → `people/settings`(从个人设置搬过来的那一块)
|
||||||
*
|
*
|
||||||
* 用法(require 前 set 好):
|
* 用法(require 前 set 好):
|
||||||
* $plSetCurrent 当前页 key('import'|'groups')用来高亮;'' 也行
|
* $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>
|
<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">
|
<div class="menu-list">
|
||||||
<a href="<?= e(url('people/import')) ?>"<?= $plSetOn('import') ?>><?= $plSetIcon('import') ?><span>导入联系人</span></a>
|
<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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<?php unset($plSetCurrent, $plSetOn, $plSetIcon); ?>
|
<?php unset($plSetCurrent, $plSetOn, $plSetIcon); ?>
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
*/
|
*/
|
||||||
$report = $report ?? null;
|
$report = $report ?? null;
|
||||||
$canMove = \App\Auth::canWrite();
|
$canMove = \App\Auth::canWrite();
|
||||||
|
/* 🔴 v54(用户 2026-10-16 第 2 条):打标签的候选 —— 设置页配的词表 ∪ 我用过的词。
|
||||||
|
⚠️ 显式给默认值(模板可能被别处 require、未必总带上这个键)。 */
|
||||||
|
$tagChoices = $tagChoices ?? [];
|
||||||
|
$groups = $groups ?? [];
|
||||||
?>
|
?>
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>导入联系人</h1>
|
<h1>导入联系人</h1>
|
||||||
@@ -43,15 +47,28 @@ $canMove = \App\Auth::canWrite();
|
|||||||
$sdMultiple = false; $sdRequired = true; $sdStage = false;
|
$sdMultiple = false; $sdRequired = true; $sdStage = false;
|
||||||
require __DIR__ . '/../_stage_drop.php'; ?>
|
require __DIR__ . '/../_stage_drop.php'; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php /* 🔴 v54(用户 2026-10-16 第 2 条):原来这里是「加进哪一组」的下拉。
|
||||||
|
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
|
||||||
|
⇒ 换成**打标签**(与「添加成员」页同一个契约:`name=tags`)。
|
||||||
|
导入进来的新人会自动带上这串标签(owner=我)。 */ ?>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="imp-group">加进哪一组</label>
|
<label for="imp-tags">给导入的人打标签(可留空)</label>
|
||||||
<select class="select" id="imp-group" name="group_id">
|
<?php if (!empty($tagChoices)): ?>
|
||||||
<option value="0">先不分类</option>
|
<div class="pt-picks pf-picks" style="margin-bottom:8px">
|
||||||
<?php foreach ($groups as $g): ?>
|
<?php foreach ($tagChoices as $tg): ?>
|
||||||
<option value="<?= (int)$g['id'] ?>"><?= e((string)$g['name']) ?></option>
|
<label class="pt-pick">
|
||||||
|
<input type="checkbox" value="<?= e((string)$tg) ?>">
|
||||||
|
<span>#<?= e((string)$tg) ?></span>
|
||||||
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</div>
|
||||||
<span class="tip">只影响<b>新建</b>的那些人;已有的人保持他原来的分组。</span>
|
<?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>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
|
|||||||
@@ -24,7 +24,11 @@
|
|||||||
*/
|
*/
|
||||||
$canMove = \App\Auth::canWrite();
|
$canMove = \App\Auth::canWrite();
|
||||||
$pickable = $pickable ?? 0;
|
$pickable = $pickable ?? 0;
|
||||||
|
// ⚠️ v54(用户 2026-10-16 第 2 条):两个变量**不是一个东西**,别合并:
|
||||||
|
// · $tagCloud —— **真有人的**标签(筛选条用;人的数为 0 的词不上筛选条)
|
||||||
|
// · $tagChoices —— **打标签的候选**(词表 ∪ 用过的词;浮层里的 checkbox 用它)
|
||||||
$tagCloud = $tagCloud ?? [];
|
$tagCloud = $tagCloud ?? [];
|
||||||
|
$tagChoices = $tagChoices ?? [];
|
||||||
$tagFilter = $tagFilter ?? '';
|
$tagFilter = $tagFilter ?? '';
|
||||||
// v28:平铺列表,人数直接数行数(不再按分组累加)
|
// v28:平铺列表,人数直接数行数(不再按分组累加)
|
||||||
$total = count($people ?? []);
|
$total = count($people ?? []);
|
||||||
@@ -417,13 +421,19 @@ $renderGrid = function (array $list) use ($meId, $canMove): void {
|
|||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="person_id" value="">
|
<input type="hidden" name="person_id" value="">
|
||||||
<div class="rm-body">
|
<div class="rm-body">
|
||||||
<?php /* 已有的标签(我名下全部用过的):点一下勾选/取消 */ ?>
|
<?php /* 🔴 v54(用户 2026-10-16 第 2 条):候选**不再是"我用过的词"**,
|
||||||
<?php if ($tagCloud): ?>
|
而是 `$tagChoices` = **设置页配的词表 ∪ 我用过的词**。
|
||||||
|
用户原话:「打标签的时候,**却不是从这些分组里读出来的**……
|
||||||
|
应该从这里面把所有标签都读出来吧?但是现在标签里面却只有几个,
|
||||||
|
也不知道是从哪儿读出来的。」
|
||||||
|
⇒ 词表里的词**还没用过**也能一点就打上 —— 这正是词表的意义
|
||||||
|
(否则每个新标签都得手打一遍,词表就等于没有)。 */ ?>
|
||||||
|
<?php if ($tagChoices): ?>
|
||||||
<div class="pt-picks" id="ptPicks">
|
<div class="pt-picks" id="ptPicks">
|
||||||
<?php foreach ($tagCloud as $t): ?>
|
<?php foreach ($tagChoices as $tg): ?>
|
||||||
<label class="pt-pick">
|
<label class="pt-pick">
|
||||||
<input type="checkbox" value="<?= e((string)$t['tag']) ?>">
|
<input type="checkbox" value="<?= e((string)$tg) ?>">
|
||||||
<span>#<?= e((string)$t['tag']) ?></span>
|
<span>#<?= e((string)$tg) ?></span>
|
||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
* 那么他可以直接复用这个人的信息,就不需要再重新录入。」
|
* 那么他可以直接复用这个人的信息,就不需要再重新录入。」
|
||||||
*/
|
*/
|
||||||
?>
|
?>
|
||||||
|
<?php
|
||||||
|
/* 🔴 v54(用户 2026-10-16 第 2 条):打标签的候选 —— 设置页配的词表 ∪ 我用过的词。
|
||||||
|
⚠️ 显式给默认值(模板可能被别处 require、未必总带上这个键)。 */
|
||||||
|
$tagChoices = $tagChoices ?? [];
|
||||||
|
$groups = $groups ?? [];
|
||||||
|
?>
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>从家里已有的人里挑</h1>
|
<h1>从家里已有的人里挑</h1>
|
||||||
<span class="sub">选中的会进你的名册,资料共用一份,不会重复录入</span>
|
<span class="sub">选中的会进你的名册,资料共用一份,不会重复录入</span>
|
||||||
@@ -34,14 +40,28 @@
|
|||||||
<b>资料是共用的那一份</b>,你以后改的也是同一份。
|
<b>资料是共用的那一份</b>,你以后改的也是同一份。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="field" style="max-width:280px;margin-bottom:16px">
|
<?php /* 🔴 v54(用户 2026-10-16 第 2 条):原来这里是「加进哪一组」的下拉(group_id)。
|
||||||
<label for="pickGroup">加进哪一组</label>
|
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
|
||||||
<select class="select" id="pickGroup" name="group_id">
|
⇒ 换成**打标签**(与「添加成员」页同一个契约:`name=tags` + `#ptBlock` 那套)。
|
||||||
<option value="">先不分组(之后拖到那一栏也行)</option>
|
候选来自 `PersonTag::choices()`(设置页配的词表 ∪ 我用过的词),
|
||||||
<?php foreach ($groups as $g): $gid = (int)$g['id']; ?>
|
服务端 people/pick 会把这一串标签逐个落进 person_tags(owner=我)。
|
||||||
<option value="<?= $gid ?>"><?= e((string)$g['name']) ?></option>
|
⚠️ 勾了多个人时,这串标签会**给每个人各打一遍** —— 一次挑进来的人
|
||||||
|
往往是同一类("小学同学"),所以这正是想要的默认。 */ ?>
|
||||||
|
<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; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<input class="input" id="pickTags" name="tags" maxlength="300"
|
||||||
|
placeholder="如:小学同学 球友(用空格分开)" autocomplete="off">
|
||||||
|
<span class="tip">一个人可以有多个标签;打上的标签只在你自己的名册里可见。</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php /* 用户 2026-10-03 第 2 条:所有"找个人"的地方都要**输入即筛选**。
|
<?php /* 用户 2026-10-03 第 2 条:所有"找个人"的地方都要**输入即筛选**。
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 名册设置(v52 用户 2026-10-15 第 6 条)。
|
* 名册设置(v52 用户 2026-10-15 第 6 条;v54 用户 2026-10-16 第 2 条改口径)。
|
||||||
*
|
*
|
||||||
* 用户原话:「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
|
* 用户原话(v52):「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
|
||||||
* 点击后下拉设置,第一个就是导入联系人,第二个就是**原来个人设置那里的名册分组**,
|
* 点击后下拉设置,第一个就是导入联系人,第二个就是**原来个人设置那里的名册分组**,
|
||||||
* 这样**个人设置中就不显示名册分组的内容**了。」
|
* 这样**个人设置中就不显示名册分组的内容**了。」
|
||||||
*
|
*
|
||||||
* ⇒ 这一页 = 「设置 → 名册分组」进去的那一页,内容就是从个人设置**整块搬**过来的
|
* 用户原话(v54):「现在应该在这个设置页面,**不应该再叫「名册分组」了吧?
|
||||||
* `_person_groups.php`(表单 action 一个字段没改,仍指向 settings/groups/*)。
|
* 应该叫「名册标签」**,是不是?应该从这里面把所有标签都读出来吧?」
|
||||||
*
|
*
|
||||||
* ⚠️ 为什么不做成"个人设置那一块的副本":两份一模一样的分组管理界面,
|
* ⇒ 这一页 = 「设置 → 名册标签」进去的那一页,内容就是从个人设置**整块搬**过来的
|
||||||
|
* `_person_groups.php`(表单 action 一个字段没改,仍指向 settings/groups/*)。
|
||||||
|
* v54 起界面文案**全面改成"标签"**:这是"预定义标签词表",
|
||||||
|
* 名册里打标签时的候选就从它读(见 `App\PersonTag::choices()`)。
|
||||||
|
*
|
||||||
|
* ⚠️ 为什么不做成"个人设置那一块的副本":两份一模一样的标签管理界面,
|
||||||
* 改一处忘一处是必然的(本项目在"设置页"上已经踩过这个)。
|
* 改一处忘一处是必然的(本项目在"设置页"上已经踩过这个)。
|
||||||
* 所以是**真的搬走** —— 个人设置里那一块删掉了,这里 require 同一个片段。
|
* 所以是**真的搬走** —— 个人设置里那一块删掉了,这里 require 同一个片段。
|
||||||
*
|
*
|
||||||
@@ -19,7 +24,7 @@
|
|||||||
?>
|
?>
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>名册设置</h1>
|
<h1>名册设置</h1>
|
||||||
<span class="sub">分组</span>
|
<span class="sub">标签</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a>
|
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a>
|
||||||
<?php $plSetCurrent = 'groups'; require __DIR__ . '/_settings_menu.php'; ?>
|
<?php $plSetCurrent = 'groups'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
|
|||||||
+18
-13
@@ -45,27 +45,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- ==================== 公共分组 ==================== -->
|
<!-- ==================== 公共标签 ==================== -->
|
||||||
<?php
|
<?php
|
||||||
|
/* 🔴 v54(用户 2026-10-16 第 2 条):这块从「公共分组」改口径为「公共标签」。
|
||||||
|
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
|
||||||
|
⇒ 这里维护的是**公共标签词表**:名册里给成员打标签时,这些词会出现在候选里
|
||||||
|
(见 `App\PersonTag::choices()`)。表名仍叫 person_groups(见 _person_groups.php
|
||||||
|
的说明:改名要背着 PersonGroup 类与其它会话正在改的 App-API 一起动,不划算),
|
||||||
|
但界面上一律说"标签"。 */
|
||||||
$pubGroups = $pubGroups ?? [];
|
$pubGroups = $pubGroups ?? [];
|
||||||
$pubCounts = $pubCounts ?? [];
|
$pubCounts = $pubCounts ?? [];
|
||||||
$pubTotal = count($pubGroups);
|
$pubTotal = count($pubGroups);
|
||||||
?>
|
?>
|
||||||
<div class="card card-pad" style="margin-top:20px" id="public-groups">
|
<div class="card card-pad" style="margin-top:20px" id="public-groups">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
公共分组
|
公共标签
|
||||||
<span class="hint">全家人共用的一套</span>
|
<span class="hint">全家人共用的一套</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="small muted" style="margin:0 0 16px">
|
<p class="small muted" style="margin:0 0 16px">
|
||||||
名册按「关系分组」分栏显示。这里维护的是<b>公共的那一套</b> ——
|
这里维护的是<b>公共标签</b> —— 建一次,全家每个人给名册成员打标签时都能拿来用。
|
||||||
建一次,全家每个人都能拿来用。
|
|
||||||
<br>
|
<br>
|
||||||
每个人还可以在「个人设置 → 我的分组」里<b>再加只给自己看的分组</b>,
|
每个人还可以在「名册 → 设置 → 名册标签」里<b>再加只给自己看的标签</b>,
|
||||||
那部分不在这儿、也不会影响别人。挪顺序用 ▲ ▼(▲▼ 调的是"全家的默认顺序",
|
那部分不在这儿、也不会影响别人。挪顺序用 ▲ ▼(▲▼ 调的是"全家的默认顺序",
|
||||||
各人仍可在个人设置里改成自己习惯的顺序)。
|
各人仍可在名册设置里改成自己习惯的顺序)。
|
||||||
<br>
|
<br>
|
||||||
删掉一个分组<b>不会删掉人</b>:名册里归到那一组的人会变成「未分类」,
|
删掉一个标签<b>只是从候选里去掉这个词</b>:已经打在别人身上的标签不受影响
|
||||||
各自重新归类即可。
|
(标签是各人身上记着的词,不是引用)。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<?php if ($pubGroups): ?>
|
<?php if ($pubGroups): ?>
|
||||||
@@ -80,9 +85,9 @@ $pubTotal = count($pubGroups);
|
|||||||
<input type="hidden" name="id" value="<?= $gid ?>">
|
<input type="hidden" name="id" value="<?= $gid ?>">
|
||||||
<input type="hidden" name="scope" value="public">
|
<input type="hidden" name="scope" value="public">
|
||||||
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="up"
|
<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"
|
<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>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?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">
|
<form method="post" action="<?= e(url('settings/groups/add')) ?>" class="inline-form" style="margin-top:16px">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="scope" value="public">
|
<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="例如:邻居、亲家、老同事">
|
<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>
|
</form>
|
||||||
|
|
||||||
<?php if ($pubGroups): ?>
|
<?php if ($pubGroups): ?>
|
||||||
@@ -105,7 +110,7 @@ $pubTotal = count($pubGroups);
|
|||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="id" value="<?= $gid ?>">
|
<input type="hidden" name="id" value="<?= $gid ?>">
|
||||||
<input type="hidden" name="scope" value="public">
|
<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"
|
<input class="input" id="pgname<?= $gid ?>" name="name" maxlength="20"
|
||||||
value="<?= e((string)$g['name']) ?>">
|
value="<?= e((string)$g['name']) ?>">
|
||||||
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
|
<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 条):「原来个人设置那里的名册分组……
|
<?php /* 🔴 v52(用户 2026-10-15 第 6 条):「原来个人设置那里的名册分组……
|
||||||
这样**个人设置中就不显示名册分组的内容**了。」
|
这样**个人设置中就不显示名册分组的内容**了。」
|
||||||
⇒ 整块「我的分组」卡片**搬到了**「名册 → 设置 → 名册分组」
|
⇒ 整块卡片**搬到了**「名册 → 设置 → 名册标签」
|
||||||
(`templates/people/settings.php` + `templates/_person_groups.php`)。
|
(`templates/people/settings.php` + `templates/_person_groups.php`)。
|
||||||
⚠️ 是**真的搬走**,不是复制一份:两份一模一样的分组管理界面,
|
⚠️ 是**真的搬走**,不是复制一份:两份一模一样的管理界面,
|
||||||
改一处忘一处是必然的。这里只留一句指路,免得老用户找不到。 */ ?>
|
改一处忘一处是必然的。这里只留一句指路,免得老用户找不到。
|
||||||
|
🔴 v54(用户 2026-10-16 第 2 条):口径**从"分组"改成"标签"** ——
|
||||||
|
用户原话:「应该叫「名册标签」,是不是?」⇒ 这块指路文案也跟着改。 */ ?>
|
||||||
<div class="card card-pad" style="margin-top:20px">
|
<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 class="small muted" style="margin:0 0 12px">
|
||||||
分组改到**名册页**那边去了 —— 它本来就是"名册怎么分组"的事,与个人设置无关。
|
标签改到**名册页**那边去了 —— 它本来就是"给名册里的人打什么标签"的事,与个人设置无关。
|
||||||
</p>
|
</p>
|
||||||
<div class="btn-row">
|
<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>
|
||||||
</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
|
<?php
|
||||||
/**
|
/**
|
||||||
* 旅程模块的「设置」下拉(v24 第 16 批 用户第 8 条)。
|
* 旅程模块的「设置」下拉(v24 第 16 批 用户第 8 条;v54 用户第 4 条加一项)。
|
||||||
*
|
*
|
||||||
* 用户原话:「右上角的按钮,需要增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。」
|
* 用户原话(v24):「右上角的按钮,需要增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。」
|
||||||
|
*
|
||||||
|
* 用户原话(v54):「2. 在旅程的「新建旅程」**后面也加一个「设置」按钮**。
|
||||||
|
* 3. 里面的设置让用户自己配置:**到底默认显示旅程列表,还是用地图显示**。」
|
||||||
*
|
*
|
||||||
* 与其它模块同一套规矩(用户 2026-10-01 定的四条):
|
* 与其它模块同一套规矩(用户 2026-10-01 定的四条):
|
||||||
* ① 按钮**没有文字**、只有图标,图标是**三条横线**;
|
* ① 按钮**没有文字**、只有图标,图标是**三条横线**;
|
||||||
@@ -11,7 +14,7 @@
|
|||||||
* ④ 除设置外其它按钮都有颜色。
|
* ④ 除设置外其它按钮都有颜色。
|
||||||
*
|
*
|
||||||
* 用法(require 前 set 好):
|
* 用法(require 前 set 好):
|
||||||
* $trSetCurrent 当前页 key('vehicles'|'modes'|'types'|'')
|
* $trSetCurrent 当前页 key('vehicles'|'modes'|'types'|'display'|'')
|
||||||
*/
|
*/
|
||||||
$trSetCurrent = (string)($trSetCurrent ?? '');
|
$trSetCurrent = (string)($trSetCurrent ?? '');
|
||||||
$trSetOn = static function (string $key) use ($trSetCurrent): string {
|
$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="9" y="2" width="5" height="5" rx="1"/>'
|
||||||
. '<rect x="2" y="9" 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"/>',
|
. '<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] ?? '';
|
$d = $paths[$name] ?? '';
|
||||||
if ($d === '') {
|
if ($d === '') {
|
||||||
@@ -43,6 +49,10 @@ $trSetIcon = static function (string $name): string {
|
|||||||
<details class="movie-set menu">
|
<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>
|
<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">
|
<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/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/modes')) ?>"<?= $trSetOn('modes') ?>><?= $trSetIcon('modes') ?><span>出行方式</span></a>
|
||||||
<a href="<?= e(url('trips/types')) ?>"<?= $trSetOn('types') ?>><?= $trSetIcon('types') ?><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
|
<?php
|
||||||
/**
|
/**
|
||||||
* 旅程列表(v12 建立;v24 第 16 批 第 9 条把术语**固定下来**)。
|
* 旅程列表(v12 建立;v24 第 16 批 第 9 条把术语**固定下来**;
|
||||||
|
* v54 用户 2026-10-16 第 3/4/5 条加了**地图视图**与显示方式设置)。
|
||||||
*
|
*
|
||||||
* ⚠️ 术语(用户第 9 条原话):「活动都改名吧,改成旅程,新建旅程,旅程包含行程,
|
* ⚠️ 术语(用户第 9 条原话):「活动都改名吧,改成旅程,新建旅程,旅程包含行程,
|
||||||
* 这个名字就固定下来。」
|
* 这个名字就固定下来。」
|
||||||
@@ -9,8 +10,36 @@
|
|||||||
*
|
*
|
||||||
* 时间与城市都是从各段**行程**派生出来的(第 15 批第 2 条)——
|
* 时间与城市都是从各段**行程**派生出来的(第 15 批第 2 条)——
|
||||||
* 列表里读的是 Trip::attachDerived() 挂上来的值,不是旅程自己的字段。
|
* 列表里读的是 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();
|
$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">
|
<div class="page-head">
|
||||||
<h1>旅程</h1>
|
<h1>旅程</h1>
|
||||||
@@ -29,13 +58,51 @@ $canWrite = \App\Auth::canWrite();
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="link-quiet">只读账号</span>
|
<span class="link-quiet">只读账号</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php /* 🔴 v54(第 4 条):「在旅程的「新建旅程」**后面**也加一个「设置」按钮。」
|
||||||
|
与其它旅程页同一份菜单(`_settings_menu.php`),设置恒排最右。 */ ?>
|
||||||
|
<?php $trSetCurrent = ''; require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php /* ---- 列表主体:包一层 id,供"新建成功后**动态刷新**"换掉(用户第 2 条)---- */ ?>
|
<?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">
|
<div id="tripListHost">
|
||||||
<?php $tripListBody = true; require __DIR__ . '/_list_body.php'; ?>
|
<?php $tripListBody = true; require __DIR__ . '/_list_body.php'; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($canWrite): ?>
|
<?php if ($canWrite): ?>
|
||||||
<?php /* ---- 新建旅程弹框(内嵌同一份 form.php;初始关闭)---- */ ?>
|
<?php /* ---- 新建旅程弹框(内嵌同一份 form.php;初始关闭)---- */ ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user