v52:旅程 CalDAV 结束时间 / 笔记(标题·段落下加段加图·逐段就地编辑)/ 名册自动匹配与设置页 / 模块设置整合表
用户 2026-10-15 七条。
① 旅程行程经 CalDAV 同步后「结束时间 = 开始时间」
**真根因**(app/Calendar.php 的 exportRows()):对**派生条目**(观影/旅程)写死
'ends_at' => null,而 Ics::vevent() 遇到空结束会退化成「DTEND = DTSTART」
⇒ 手机上每条行程的结束时间都等于开始。
**附带第二个 bug**:分组键含日期(kind-ref_id-date),而 month() 把**跨天条目逐天**
铺出来 ⇒ 跨天节点被拆成 N 条(UID 相同),resources() 按 UID 去重后**只留第一天**,
而第一天那条没有结束时刻 ⇒ 又回到 DTEND=DTSTART。
修法:键改成 `kind-ref_id`,先收集同节点的各天,再用**首末天**拼一条真实跨度
(末天那条才带 end_time)。实测单日 DTEND=12:30、跨天 DTEND=第3天11:00,
每节点恰好 1 条 VEVENT。
⚠️ 副作用(已处理):v31 DAV 去重的反向验证原来靠「跨天节点逐天出、UID 全同」造重复,
本修把那条素材**修没了** ⇒ 给夹具换成「同一份 .ics 导两次」(两条 UID 相同的
自定义日程),守卫仍能被证伪(见 _fb_dav_diag.py 与 pitfalls 的同族条目)。
② 笔记详情页标题是"看笔记"而不是笔记名
阅读态 h1 改用笔记自己的 $noteTitle;**编辑态仍用「改笔记/写笔记」**
(编辑时标题就在下面第一个输入框,两处重名看着像重复)。浏览器 <title> 同步。
③ 加段落/加图片要在**输入框下面**
note_editor.js 的 makeBlock() 里每块下方加一排 .note-block-add(+段落/+图片),
点了插在**这一块后面**;顶部那组**保留**(用户明确"上面和下面都保留")。
⚠️ 挂在 body 里而不是 wrap 上 —— .note-block 是 display:flex 横排,
挂 wrap 会变成"输入框右边的第三列",不是"下面"。
⚠️ 用 data-add-below(不是 data-add):顶部是"追加到末尾"、块内是"插在本块后"。
④ 笔记详情页「逐段就地编辑」(分块多时不用跳编辑页找那一块)
模板给每块包 .note-read-item[data-block][data-kind][data-raw] + 右上角「编辑」按钮;
新增 assets/js/note_inline.js(按需加载):划过出现 → 点编辑就地变 textarea →
保存 POST notes/block → 只换这一块。
服务端 Note::updateBlock():**按块主键改一行**(不是整组替换)——
⚠️ 整组替换会换掉每个块的 id(前端手里的 data-block 当场失效)+ 并发互相覆盖;
⚠️ SQL 必须带 `AND note_id = ?`(块 id 全局自增,少了它就能改别人笔记里的块)。
权限与整条编辑同一把尺(find() + canWrite())。
⑤ 删掉「从已有家里人中挑选」,改成「新增/导入时自动匹配」
新增 Person::matchExisting():手机号(有 phone_hash,一条 SQL 精确命中)→
身份证(无哈希,整表解密比)→ 姓名(**先记下不立刻返回**,等精确命中看完再退到它)。
people/create:校验通过后先 matchExisting,命中就在**同一张表单里**出确认条
(#personConfirm)。⚠️ **不做成单独确认页**:单独页要把整张表(含工作经历/补充联系
方式这些**数组字段**)原样回传,漏一类就静默丢数据;同表单里字段天然全在。
⚠️ 判"是不是第一次提交"用 array_key_exists('confirm_existing')(0=不是、1=是、无=首次)。
导入那条路的缺口(本轮补):ContactImporter 命中已有的人时只补字段、**不挂进导入者的
名册** ⇒ "文件里有他、系统里也有他,但导完我的名册里看不到他"。已在命中分支加
PersonRoster::ensure(..., $meId, ...)。
顺手加 PersonRoster::has()(避免调用方又踩"visibleIds 是值列表不是集合"那个坑)。
⑥ 名册页只剩「添加成员 + 设置」;分组搬到名册设置
templates/people/_settings_menu.php(新):☰ 下拉两行 = 导入联系人 / 名册分组。
templates/people/settings.php(新,路由 people/settings):require 从个人设置**整块搬出来**
的 templates/_person_groups.php。
⚠️ 是**真搬**不是复制(两份一模一样的分组界面,改一处忘一处是必然的);
⚠️ 片段变量一律加 pg 前缀 —— 模板片段与宿主 require 是**共享作用域**,
用 $groups 这种通名会覆盖宿主同名变量(本轮真踩到,已改)。
⚠️ 表单 action 仍指 settings/groups/*(一个字段没改)。
⑦ 个人设置:模块设置整合成一张表
原来是**三张卡三个表单**(首页模块 / 导航模块 / 主题色),同一串模块名出现两遍。
现在一张表五列:顺序 | 模块 | 导航 | 首页 | 颜色,一次保存。
新增 settings/modules_save 路由:**底层存储一个没改**(仍是 nav_modules /
home_modules / module_colors),只是把三个 save 依次调一遍 ⇒ 老数据天然兼容。
⚠️ 权限**没有**因为"合并"被放宽:颜色仍是家庭一份、只有管理员能改
(非管理员那列是只读色块;提交时服务端静默忽略 c[])。
窄屏:表格 min-width:560px + 外层 overflow-x:auto(375px 实测横向溢出 0)。
验证(新增 5 套件 + 更新若干旧断言):
· _fb_v52_ics.py(3)/ _fb_v52_note.py(19)/ _fb_v52_roster.py(12)/ _fb_v52_mods.py(13)
· _b52_falsify.py(**12 注入 / 31 断言全抓**)
· _fb_import_test.py 补 ⑯(命中的人也要回到导入者名册);_fb_dav_diag.py 换去重素材
· e2e 1516/0;_fb_batch_e.py / _fb_e2e.py / familybook_deploy.sh 里随 #6/#7 搬家或改形的
旧断言(分组在个人设置、模块行 li→tr、show_present、RRULE 前缀)意图一字不改
· 全流水线 **PUSH_RC=0**、部署 0 FAIL(含"第52批形态");线上 16 文件 md5 逐字节一致
⚠️ 工程纪律(已写进 pitfalls.md / 工作日志):
· 探针自起的 PHP 服务**一律随机空闲端口**(写死 + "连上就复用" 会静默测到别的目录/库);
· **同一工具链目录 + 同一库名不支持两个会话并发** —— 本轮最后一个多小时的"诡异全红"
全是**另一个会话**在同时跑 _fb_e2e.py、共享 fb_test 与 _fb_site 造成的;
· 并发下**连 git stash 都不能用**(会把别人的半成品一起藏起来),基线对照用 git worktree;
· 修好业务可能让"守着它的反向验证"空转(素材被修没了)⇒ 换一处等价素材,别删守卫;
· 断言盯**形态**而非具体数值(分栏那次写死 50% 在别的批次数据上假红)。
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* 「名册分组」管理卡片 —— 从个人设置**搬出来**的(v52 用户 2026-10-15 第 6 条)。
|
||||
*
|
||||
* 用户原话:「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
|
||||
* 点击后下拉设置,第一个就是导入联系人,第二个就是原来个人设置那里的
|
||||
* **名册分组**,这样**个人设置中就不显示名册分组的内容**了。」
|
||||
*
|
||||
* ⇒ 同一个卡片现在挂在「名册 → 设置 → 名册分组」这一页上(`templates/people/settings.php`)。
|
||||
* ⚠️ 表单 action 仍指向 `settings/groups/*`(**一个字段都没改**)——
|
||||
* 分组管理是"我自己的分组",与它挂在哪一页无关;
|
||||
* 换路由只会白白多一处要维护的东西(而且老书签/脚本会失效)。
|
||||
*
|
||||
* 变量(require 前设好;**入参仍用宿主那边的名字**,片段内部一律 pg* 前缀,见下):
|
||||
* $groups PersonGroup::orderFor(...) 的结果(公共 + 我的)
|
||||
* $groupCounts gid => 人数
|
||||
* $rosterCount 我的名册人数
|
||||
*/
|
||||
/* ⚠️ 变量名**必须带片段前缀**(pg*):模板片段与宿主 `require` 是**共享作用域**,
|
||||
用 `$groups` 这种通名会把宿主模板的同名变量覆盖掉(本项目有静态断言专门查这个)。 */
|
||||
$pgGroups = (array)($groups ?? []);
|
||||
$pgCounts = (array)($groupCounts ?? []);
|
||||
$pgRosterN = (int)($rosterCount ?? 0);
|
||||
$pgTotal = count($pgGroups);
|
||||
?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="section-title">
|
||||
名册分组
|
||||
<span class="hint">公共的 + 我自己加的</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
名册是按「关系分组」分栏显示的。这里有两层:
|
||||
<b>公共分组</b>由管理员在「系统设置」里维护(全家都能用);
|
||||
你要是觉得不够,就在下面<b>加只给自己看的分组</b>,别人看不到、也不受影响。
|
||||
<br>
|
||||
列表里 <b>公共</b> 标的是大家共用的那几个。
|
||||
哪一组排前面也是你自己的习惯,用 ▲ ▼ 调 —— 别人看到的顺序不变。
|
||||
<br>
|
||||
你的名册现在有 <b><?= $pgRosterN ?></b> 人。
|
||||
</p>
|
||||
|
||||
<?php if (!$pgGroups): ?>
|
||||
<p class="small muted" style="margin:0 0 14px">还没有分组。加一个吧。</p>
|
||||
<?php else: ?>
|
||||
<ol class="order-list">
|
||||
<?php foreach ($pgGroups as $i => $g): $pgGid = (int)$g['id']; $pgOwn = (int)$g['owner_id']; ?>
|
||||
<li class="order-row">
|
||||
<span class="order-no"><?= $i + 1 ?></span>
|
||||
<span class="order-name">
|
||||
<?= e((string)$g['name']) ?>
|
||||
<?php if ($pgOwn === 0): ?><span class="badge">公共</span><?php else: ?><span class="badge badge-open">我的</span><?php endif; ?>
|
||||
</span>
|
||||
<span class="order-count"><?= (int)($pgCounts[$pgGid] ?? 0) ?> 人</span>
|
||||
<form method="post" action="<?= e(url('settings/groups/move')) ?>" class="order-move">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="up"
|
||||
title="上移一组" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="down"
|
||||
title="下移一组" <?= $i === $pgTotal - 1 ? 'disabled' : '' ?>>▼</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?= e(url('settings/groups/add')) ?>" class="inline-form" style="margin-top:16px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<label class="sr-only" for="newMyGroup">新分组名</label>
|
||||
<input class="input" id="newMyGroup" name="name" maxlength="20" placeholder="例如:球友、老同学">
|
||||
<button class="btn btn-sm" type="submit">加一个我自己的分组</button>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
// 只有我自己建的分组才给改名 / 删除 —— 公共分组要在系统设置里改(那是管理员的事),
|
||||
// 免得有人在自己设置里把"家人"改成别的、影响全家。
|
||||
$pgMine = array_values(array_filter($pgGroups, fn($g) => (int)$g['owner_id'] > 0));
|
||||
?>
|
||||
<?php if ($pgMine): ?>
|
||||
<ul class="group-manage" style="margin-top:14px">
|
||||
<?php foreach ($pgMine as $g): $pgGid = (int)$g['id']; ?>
|
||||
<li class="group-manage-row">
|
||||
<form method="post" action="<?= e(url('settings/groups/rename')) ?>" class="group-rename">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<label class="sr-only" for="gname<?= $pgGid ?>">分组名</label>
|
||||
<input class="input" id="gname<?= $pgGid ?>" name="name" maxlength="20"
|
||||
value="<?= e((string)$g['name']) ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
|
||||
</form>
|
||||
<span class="group-count muted small"><?= (int)($pgCounts[$pgGid] ?? 0) ?> 人</span>
|
||||
<?php /* 删除做成两步(<details>),不用 JS 也能防手滑 —— 一点就没了太容易出事 */ ?>
|
||||
<details class="del-wrap">
|
||||
<summary class="btn btn-ghost btn-sm">删除</summary>
|
||||
<form method="post" action="<?= e(url('settings/groups/delete')) ?>" class="del-confirm">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $pgGid ?>">
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<span class="small muted">这组的人会变成「未分类」,确定?</span>
|
||||
<button class="btn btn-ghost btn-sm danger" type="submit">确认删除</button>
|
||||
</form>
|
||||
</details>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (\App\Auth::isAdmin()): ?>
|
||||
<p class="small muted" style="margin:16px 0 0">
|
||||
公共分组在「<a href="<?= e(url('settings')) ?>#public-groups">系统设置</a>」里维护。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php unset($pgGroups, $pgCounts, $pgRosterN, $pgTotal, $pgMine, $pgGid, $pgOwn, $g); ?>
|
||||
@@ -378,5 +378,17 @@ window.FB_NOTIFY = {
|
||||
</script>
|
||||
<script src="<?= e(asset('/assets/js/filter.js')) ?>"></script>
|
||||
<script src="<?= e(asset('/assets/js/preview.js')) ?>"></script>
|
||||
<?php /* 🔴 v52(用户第 4 条):笔记**详情页**的分段编辑(就地改一段、不跳整页编辑器)。
|
||||
⚠️ 只在阅读态需要它 —— 判据用 `.note-read-body[data-note-id]`(模板在阅读态才输出),
|
||||
编辑页没有这个元素,因此不会白拉一份脚本。 */ ?>
|
||||
<script>
|
||||
(function () {
|
||||
if (!document.querySelector('.note-read-body[data-note-id]')) { return; }
|
||||
var s = document.createElement('script');
|
||||
s.src = <?= json_encode(asset('/assets/js/note_inline.js')) ?>;
|
||||
s.defer = true;
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+45
-16
@@ -74,7 +74,20 @@ $imgParts = static function (string $c): array {
|
||||
};
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1><?= $editMode ? ($isEditing ? '改笔记' : '写笔记') : '看笔记' ?></h1>
|
||||
<?php
|
||||
/* 🔴 v52(用户 2026-10-15 第 2 条):「笔记这个功能模块,点进去一个笔记的详情之后,
|
||||
上边的标题不是看笔记,而是**用户自己写的这个标题的名称**。」
|
||||
⇒ 阅读态页头显示笔记自己的标题;没有标题时才退回「看笔记」。
|
||||
⚠️ 只在阅读态这么做 —— 编辑态仍用「改笔记 / 写笔记」:
|
||||
编辑时页面标题是"我在干什么",而笔记标题就在下面第一个输入框里,
|
||||
两处都写同一个名字反而看着像重复。
|
||||
⚠️ 用 `$noteHeadTitle`(HTTP 里也参与 <title>,见 View::render 的 title 参数)——
|
||||
由路由传进来,见 index.php 的 notes/form 分支。 */
|
||||
$noteTitle = trim((string)($editing['title'] ?? ''));
|
||||
$headText = (!$editMode && $isEditing && $noteTitle !== '') ? $noteTitle
|
||||
: ($editMode ? ($isEditing ? '改笔记' : '写笔记') : '看笔记');
|
||||
?>
|
||||
<h1><?= e($headText) ?></h1>
|
||||
<span class="sub">
|
||||
<?php if ($isEditing && !$editMode): ?>
|
||||
<?= e(substr((string)$editing['updated_at'], 0, 16)) ?>
|
||||
@@ -116,23 +129,39 @@ $imgParts = static function (string $c): array {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="note-read-body">
|
||||
<div class="note-read-body" data-note-id="<?= (int)($editing['id'] ?? 0) ?>">
|
||||
<?php foreach ((array)$editing['blocks'] as $b): ?>
|
||||
<?php $k = (string)$b['kind']; $c = (string)$b['content']; ?>
|
||||
<?php if ($k === 'image'): ?>
|
||||
<?php [$key, $w] = $imgParts($c); ?>
|
||||
<?php if ($key !== ''): ?>
|
||||
<figure class="note-read-fig">
|
||||
<a href="<?= e(media_url($key)) ?>" data-prev>
|
||||
<img src="<?= e(media_url($key)) ?>" alt=""
|
||||
<?= $w > 0 ? 'style="width:' . (int)$w . '%"' : '' ?>
|
||||
loading="lazy" decoding="async">
|
||||
</a>
|
||||
</figure>
|
||||
<?php
|
||||
$k = (string)$b['kind']; $c = (string)$b['content'];
|
||||
/* 🔴 v52(用户第 4 条):每一块包一层 `.note-read-item`,带**块主键** ——
|
||||
详情页的"分段编辑"要靠它定位改哪一块(AJAX 按主键改一行)。
|
||||
⚠️ 加 `data-block` 的**同时**保留原来的类名(`note-read-fig` / `note-read-block`)——
|
||||
现有 e2e 与 CSS 都按它们断言,改类名会静默弄坏那些检查。 */
|
||||
?>
|
||||
<div class="note-read-item" data-block="<?= (int)($b['id'] ?? 0) ?>"
|
||||
data-kind="<?= e($k) ?>" data-raw="<?= e($c) ?>">
|
||||
<?php if ($k === 'image'): ?>
|
||||
<?php [$key, $w] = $imgParts($c); ?>
|
||||
<?php if ($key !== ''): ?>
|
||||
<figure class="note-read-fig">
|
||||
<a href="<?= e(media_url($key)) ?>" data-prev>
|
||||
<img src="<?= e(media_url($key)) ?>" alt=""
|
||||
<?= $w > 0 ? 'style="width:' . (int)$w . '%"' : '' ?>
|
||||
loading="lazy" decoding="async">
|
||||
</a>
|
||||
</figure>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="note-read-block md-body"><?= (string)($b['html'] ?? '') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="note-read-block md-body"><?= (string)($b['html'] ?? '') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* 鼠标划过才出现的「编辑」——只在能写且这一块真的有主键时给
|
||||
(`$b['id']` 为 0 的块理论上不该有;给了也改不了,不如不给)。 */ ?>
|
||||
<?php if ($canEdit && $canWrite && (int)($b['id'] ?? 0) > 0): ?>
|
||||
<button type="button" class="note-item-edit"
|
||||
data-item-edit="<?= (int)$b['id'] ?>" title="只改这一段">编辑</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (!$editing['blocks']): ?>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* 名册模块的「设置」下拉(v52 用户 2026-10-15 第 6 条)。
|
||||
*
|
||||
* 用户原话:「名册右侧,**只保留一个添加成员按钮**,右侧再加一个设置按钮,
|
||||
* 点击后下拉设置,第一个就是**导入联系人**,第二个就是原来个人设置那里的
|
||||
* **名册分组**,这样个人设置中就不显示名册分组的内容了。」
|
||||
*
|
||||
* ⇒ 名册页右上角从此只有两颗:**添加成员** + **☰ 设置**(设置恒在最右,全站统一)。
|
||||
* 原来散着的那几颗(从已有家里人里挑 / 导入联系人 / …)全部收进这个下拉。
|
||||
*
|
||||
* 两行(顺序是用户点名的):
|
||||
* ① 导入联系人 → `people/import`
|
||||
* ② 名册分组 → `people/settings`(从个人设置搬过来的那一块)
|
||||
*
|
||||
* 用法(require 前 set 好):
|
||||
* $plSetCurrent 当前页 key('import'|'groups')用来高亮;'' 也行
|
||||
*/
|
||||
$plSetCurrent = (string)($plSetCurrent ?? '');
|
||||
$plSetOn = static function (string $key) use ($plSetCurrent): string {
|
||||
return $plSetCurrent === $key ? ' class="on"' : '';
|
||||
};
|
||||
$plSetIcon = static function (string $name): string {
|
||||
$paths = [
|
||||
'import' => '<path d="M8 2.6v7.2M5 6.8l3 3 3-3M3.2 13.4h9.6"/>',
|
||||
'groups' => '<rect x="2" y="2.6" width="5" height="5" rx="1"/><rect x="9" y="2.6" width="5" height="5" rx="1"/>'
|
||||
. '<rect x="2" y="8.6" width="5" height="5" rx="1"/><rect x="9" y="8.6" width="5" height="5" rx="1"/>',
|
||||
];
|
||||
$d = $paths[$name] ?? '';
|
||||
if ($d === '') {
|
||||
return '';
|
||||
}
|
||||
return '<svg class="mi" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"'
|
||||
. ' fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"'
|
||||
. ' stroke-linejoin="round">' . $d . '</svg>';
|
||||
};
|
||||
?>
|
||||
<details class="movie-set menu">
|
||||
<summary class="btn btn-icon" title="名册设置" aria-label="名册设置"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg></summary>
|
||||
<div class="menu-list">
|
||||
<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>
|
||||
</div>
|
||||
</details>
|
||||
<?php unset($plSetCurrent, $plSetOn, $plSetIcon); ?>
|
||||
@@ -69,6 +69,66 @@ $ctHints = [
|
||||
<input type="hidden" name="id" value="<?= (int)$person['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* 🔴🔴 v52(用户 2026-10-15 第 5 条):「是不是这个人?」
|
||||
新增成员时,若按 姓名 / 手机号 / 身份证号 在家里找到匹配的人,
|
||||
**就在这张表里问一句** —— 而不是跳到一个单独的确认页。
|
||||
⚠️ 关键理由:单独一页要把用户刚填的一整张表(含工作经历、补充联系方式
|
||||
这些**数组字段**)原样回传;漏传任何一类都会**静默丢数据**。
|
||||
在同一张表里问,字段天然全在。
|
||||
⚠️ 两个按钮提交的就是**这张表**,靠 `confirm_existing` 的值分流:
|
||||
1 = 就是这个人(不新建,直接加进我的名册);0 = 不是,另建一条。 */ ?>
|
||||
<?php if (!empty($confirmHit)): ?>
|
||||
<?php
|
||||
$cfWhy = [
|
||||
'phone' => '手机号一样',
|
||||
'id_card' => '身份证号一样',
|
||||
'name' => '姓名一样',
|
||||
][(string)($confirmWhy ?? 'name')] ?? '信息很像';
|
||||
$cfPid = (int)($confirmHit['id'] ?? 0);
|
||||
$cfInMine = \App\PersonRoster::has((int)\App\Auth::familyId(), (int)\App\Auth::personId(), $cfPid);
|
||||
?>
|
||||
<div class="card card-pad person-confirm" id="personConfirm">
|
||||
<h2 class="section-title">
|
||||
家里已经有这个人了 —— 是他吗?
|
||||
<span class="hint"><?= e($cfWhy) ?></span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 12px">
|
||||
如果是他,直接加进你的名册就行 —— <b>资料共用同一份</b>,不用再录一遍,
|
||||
以后你们改的也是同一个人。
|
||||
<br>如果不是(只是重名),点「不是,是另一个人」,我就按你填的当新人建一条。
|
||||
</p>
|
||||
|
||||
<div class="person-hit">
|
||||
<span class="person-hit-av">
|
||||
<?php if (!empty($confirmHit['avatar'])): ?>
|
||||
<img src="<?= e(media_url((string)$confirmHit['avatar'])) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<span class="avatar-fallback"><?= e(mb_substr((string)($confirmHit['name'] ?? ''), 0, 1, 'UTF-8')) ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span class="person-hit-info">
|
||||
<b><?= e((string)($confirmHit['name'] ?? '')) ?></b>
|
||||
<?php if (!empty($confirmHit['is_placeholder'])): ?><span class="node-badge">虚拟人物</span><?php endif; ?>
|
||||
<?php if ($cfInMine): ?><span class="node-badge badge-open">已在你的名册</span><?php endif; ?>
|
||||
<span class="small muted">
|
||||
<?php if ((string)($confirmHit['org'] ?? '') !== ''): ?><?= e((string)$confirmHit['org']) ?> · <?php endif; ?>
|
||||
<?php if ((string)($confirmHit['title'] ?? '') !== ''): ?><?= e((string)$confirmHit['title']) ?><?php endif; ?>
|
||||
</span>
|
||||
</span>
|
||||
<span class="person-hit-ops">
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/show', ['id' => $cfPid])) ?>"
|
||||
target="_blank" rel="noopener">先看看他的资料</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="btn-row" style="margin-top:16px">
|
||||
<button class="btn" type="submit" name="confirm_existing" value="1">就是这个人,加进我的名册</button>
|
||||
<button class="btn btn-ghost" type="submit" name="confirm_existing" value="0">不是,是另一个人</button>
|
||||
<input type="hidden" name="existing_id" value="<?= $cfPid ?>">
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card card-pad">
|
||||
<div class="form-grid">
|
||||
|
||||
|
||||
+18
-20
@@ -243,19 +243,20 @@ $renderGrid = function (array $list) use ($meId, $canMove): void {
|
||||
共 <b data-roster-total><?= $total ?></b> 人<?= $tagCloud && $q === '' ? ',有 ' . count($tagCloud) . ' 个标签' : '' ?>
|
||||
</span>
|
||||
<div class="actions">
|
||||
<?php if ($canMove && $pickable > 0): ?>
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/pick')) ?>">
|
||||
从家里已有的人里挑(<?= $pickable ?>)
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($canMove): ?>
|
||||
<?php /* 用户 2026-10-03 第 1 条:名册导入入口。
|
||||
⚠️ 放在「添加成员」**左边**(次要动作在左、主要动作在右)——
|
||||
日常还是一个个加,导入是换手机/换号时的一次性动作。 */ ?>
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/import')) ?>">导入联系人</a>
|
||||
<?php endif; ?>
|
||||
<?php if (\App\Auth::isAdmin()): ?>
|
||||
<?php /* 🔴🔴 v52(用户 2026-10-15 第 6 条):「名册右侧,**只保留一个添加成员按钮**,
|
||||
右侧再加一个设置按钮,点击后下拉设置,第一个就是导入联系人,
|
||||
第二个就是原来个人设置那里的名册分组,这样个人设置中就不显示名册分组的内容了。」
|
||||
⇒ 右上角只剩两颗:**添加成员** + **☰ 设置**(设置恒在最右,全站统一)。
|
||||
⚠️ 原来那两颗去哪了:
|
||||
· 「从家里已有的人里挑」→ **整颗删掉**(用户第 5 条:不再让人手动挑,
|
||||
改由"新增/导入时自动按姓名/手机号/身份证匹配并问一句"来替代);
|
||||
· 「导入联系人」→ 收进 ☰ 设置里(第一行)。 */ ?>
|
||||
<a class="btn btn-sm" href="<?= e(url('people/new')) ?>">添加成员</a>
|
||||
<?php $plSetCurrent = ''; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
<?php elseif (\App\Auth::isAdmin()): ?>
|
||||
<a class="btn btn-sm" href="<?= e(url('people/new')) ?>">添加成员</a>
|
||||
<?php $plSetCurrent = ''; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -285,21 +286,18 @@ $renderGrid = function (array $list) use ($meId, $canMove): void {
|
||||
<?php if ($q !== ''): ?>
|
||||
<b>没有匹配的人</b>
|
||||
换个词试试。
|
||||
<?php elseif ($pickable > 0): ?>
|
||||
<b>你的名册还是空的</b>
|
||||
家里已经有 <?= (int)$pickable ?> 个人了 —— 直接挑过来就行,不用重新录一遍。
|
||||
<div class="btn-row" style="justify-content:center;margin-top:14px">
|
||||
<a class="btn" href="<?= e(url('people/pick')) ?>">从家里已有的人里挑</a>
|
||||
<?php if (\App\Auth::isAdmin()): ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('people/new')) ?>">添加一个新人</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<b>名册还是空的</b>
|
||||
<?php if (\App\Auth::isAdmin()): ?>
|
||||
<?php /* 🔴 v52(用户第 6 条):这里原来挂着「从家里已有的人里挑」按钮 ——
|
||||
那条入口已按用户第 5 条的要求**整颗删掉**(改成"新增/导入时自动匹配")。
|
||||
现在只留"添加成员",以及一句说明:家里已有的人会被自动认出来。 */ ?>
|
||||
把家里人加进来,之后观影、旅行的同行人就可以直接从名册里选。
|
||||
<br>家里已经有的人(别人先录的)不用重录 —— 录入时填上姓名或手机号,
|
||||
系统会认出来并问你要不要直接加进你的名册。
|
||||
<div class="btn-row" style="justify-content:center;margin-top:14px">
|
||||
<a class="btn" href="<?= e(url('people/new')) ?>">添加成员</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('people/import')) ?>">导入联系人</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
等家里人把你加进来,或者让管理员给你开个账号。
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* 名册设置(v52 用户 2026-10-15 第 6 条)。
|
||||
*
|
||||
* 用户原话:「名册右侧,只保留一个添加成员按钮,右侧再加一个设置按钮,
|
||||
* 点击后下拉设置,第一个就是导入联系人,第二个就是**原来个人设置那里的名册分组**,
|
||||
* 这样**个人设置中就不显示名册分组的内容**了。」
|
||||
*
|
||||
* ⇒ 这一页 = 「设置 → 名册分组」进去的那一页,内容就是从个人设置**整块搬**过来的
|
||||
* `_person_groups.php`(表单 action 一个字段没改,仍指向 settings/groups/*)。
|
||||
*
|
||||
* ⚠️ 为什么不做成"个人设置那一块的副本":两份一模一样的分组管理界面,
|
||||
* 改一处忘一处是必然的(本项目在"设置页"上已经踩过这个)。
|
||||
* 所以是**真的搬走** —— 个人设置里那一块删掉了,这里 require 同一个片段。
|
||||
*
|
||||
* 变量:
|
||||
* $groups / $groupCounts / $rosterCount 见 _person_groups.php 的说明
|
||||
*/
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>名册设置</h1>
|
||||
<span class="sub">分组</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a>
|
||||
<?php $plSetCurrent = 'groups'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require __DIR__ . '/../_person_groups.php'; ?>
|
||||
@@ -1,176 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 个人设置里的两块「模块」设置(用户 2026-10-01 第 10、11 条):
|
||||
*
|
||||
* ① **导航条上显示哪些模块、什么顺序** —— 每人一份(App\Module::navFor 读的就是它)。
|
||||
* 勾选 = 出现在导航条上;拖动 / ▲▼ = 排序。
|
||||
* ② **每个模块的主题色** —— **家庭一份**(settings('module_colors')),
|
||||
* 所以只有管理员能改:一个家认同一套配色,不然同一个"观影"在爸爸那屏是紫的、
|
||||
* 妈妈那屏是绿的,反而记不住。非管理员在这里只看得到色板。
|
||||
*
|
||||
* 变量(require 前设好):
|
||||
* $navKeys 我当前导航条上的顺序(只含显示的)
|
||||
* $colors key => '#rrggbb'
|
||||
*/
|
||||
$allKeys = \App\Module::keys();
|
||||
$onKeys = array_map('strval', (array)($navKeys ?? []));
|
||||
// 显示顺序:已勾选的按现有顺序在前,未勾选的按 LIST 顺序在后(这样"关掉再打开"位置不跳)
|
||||
$ordered = array_values(array_filter($allKeys, static fn($k) => in_array($k, $onKeys, true)));
|
||||
foreach ($allKeys as $k) {
|
||||
if (!in_array($k, $ordered, true)) {
|
||||
$ordered[] = $k;
|
||||
}
|
||||
}
|
||||
$isAdmin = \App\Auth::isAdmin();
|
||||
?>
|
||||
<!-- ==================== 导航条上的模块 ==================== -->
|
||||
<div class="card card-pad" id="modules" style="margin-top:20px">
|
||||
<h2 class="section-title">
|
||||
导航条上的模块
|
||||
<span class="hint">勾上才会出现在顶部,拖动可改顺序</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
模块越来越多,但每个人关心的不一样 —— 这里只挑你常用的几个上导航条,其余的仍然能
|
||||
从别处进(比如观影里还能进影院管理)。关掉的模块<b>不会丢数据</b>,只是从顶栏收起来。
|
||||
</p>
|
||||
|
||||
<form method="post" action="<?= e(url('settings/nav_save')) ?>" id="navForm">
|
||||
<?= csrf_field() ?>
|
||||
<ol class="order-list mod-order" id="navOrderList">
|
||||
<?php foreach ($ordered as $i => $k): ?>
|
||||
<li class="order-row mod-row" draggable="true" data-key="<?= e($k) ?>">
|
||||
<span class="drag-handle" title="拖我排序" aria-hidden="true">⋮⋮</span>
|
||||
<?php /* ⚠️ 顺序就藏在 DOM 里:每个 <li> 自带一个 order[] 隐藏域,
|
||||
拖动搬的是整个 <li>,所以提交上来的顺序天然就是屏幕上的顺序。
|
||||
没有 JS 时 ▲▼ 按钮走同一份表单(服务端按 move 参数交换后再存)。 */ ?>
|
||||
<input type="hidden" name="order[]" value="<?= e($k) ?>">
|
||||
<label class="mod-pick">
|
||||
<input type="checkbox" name="on[]" value="<?= e($k) ?>"
|
||||
<?= in_array($k, $onKeys, true) ? 'checked' : '' ?>>
|
||||
<span class="color-preview" style="background: <?= e((string)($colors[$k] ?? '#888')) ?>;"></span>
|
||||
<span class="order-name"><?= e(\App\Module::name($k)) ?></span>
|
||||
</label>
|
||||
<span class="order-move">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="up:<?= e($k) ?>"
|
||||
title="上移" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="down:<?= e($k) ?>"
|
||||
title="下移" <?= $i === count($ordered) - 1 ? 'disabled' : '' ?>>▼</button>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit" name="save" value="1">保存导航条</button>
|
||||
<a class="btn btn-ghost" href="<?= e(url('settings/me')) ?>">重来</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 模块主题色 ==================== -->
|
||||
<div class="card card-pad" style="margin-top:20px">
|
||||
<h2 class="section-title">
|
||||
模块主题色
|
||||
<span class="hint"><?= $isAdmin ? '全家共用一套' : '由管理员设置' ?></span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
进到哪个模块,顶栏高亮与按钮就换成那个模块的颜色 —— 一眼就知道自己在哪儿。
|
||||
<?php if (!$isAdmin): ?>
|
||||
<br>这是<b>全家共用</b>的一套配色(不然同一个「观影」在两个人屏幕上两种颜色,反而记不住),
|
||||
所以只有管理员能改。
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<?php if (!$isAdmin): ?>
|
||||
<div class="mod-swatches">
|
||||
<?php foreach ($allKeys as $k): ?>
|
||||
<span class="mod-swatch">
|
||||
<span class="color-preview" style="background: <?= e((string)($colors[$k] ?? '#888')) ?>;"></span>
|
||||
<?= e(\App\Module::name($k)) ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form method="post" action="<?= e(url('settings/module_colors')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<div class="mod-colors">
|
||||
<?php foreach ($allKeys as $k): ?>
|
||||
<?php $cid = 'mc-' . $k; ?>
|
||||
<div class="mod-color-row">
|
||||
<span class="mod-color-name"><?= e(\App\Module::name($k)) ?></span>
|
||||
<input type="color" class="color-picker" id="<?= e($cid) ?>" name="c[<?= e($k) ?>]"
|
||||
value="<?= e((string)($colors[$k] ?? '#888888')) ?>">
|
||||
<span class="mod-preset">
|
||||
<?php foreach (\App\Module::PRESET_COLORS as $pc): ?>
|
||||
<span class="preset-color" data-color="<?= $pc ?>" data-for="<?= e($cid) ?>"
|
||||
style="background: <?= $pc ?>;" title="<?= $pc ?>"></span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit">保存主题色</button>
|
||||
<button class="btn btn-ghost" type="submit" name="reset" value="1"
|
||||
onclick="return confirm('恢复成默认配色?')">恢复默认</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* ---- 拖动排序(▲▼ 是不用 JS 的兜底,两条路都能用)----
|
||||
⚠️ 遍历**所有** `ol.order-list`,而不是只找 navOrderList:
|
||||
本页现在有两块同构的清单(首页上的模块 / 导航条上的模块),
|
||||
写死 id 的话第二块就没有拖动了 —— 而且看起来"功能好好的",
|
||||
只是那一块拖不动,很难联想到是这里写死了 id。 */
|
||||
function bindSort(list) {
|
||||
if (!list) return;
|
||||
var dragEl = null;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.mod-row'), function (li) {
|
||||
li.addEventListener('dragstart', function (ev) {
|
||||
dragEl = li;
|
||||
li.classList.add('is-dragged');
|
||||
if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; }
|
||||
});
|
||||
li.addEventListener('dragend', function () {
|
||||
li.classList.remove('is-dragged');
|
||||
dragEl = null;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.mod-row'), function (x) {
|
||||
x.classList.remove('is-drop-before');
|
||||
});
|
||||
});
|
||||
li.addEventListener('dragover', function (ev) {
|
||||
if (!dragEl || dragEl === li) return;
|
||||
ev.preventDefault();
|
||||
var box = li.getBoundingClientRect();
|
||||
var before = ev.clientY < box.top + box.height / 2;
|
||||
li.classList.toggle('is-drop-before', before);
|
||||
li.classList.toggle('is-drop-after', !before);
|
||||
});
|
||||
li.addEventListener('dragleave', function () {
|
||||
li.classList.remove('is-drop-before', 'is-drop-after');
|
||||
});
|
||||
li.addEventListener('drop', function (ev) {
|
||||
if (!dragEl || dragEl === li) return;
|
||||
ev.preventDefault();
|
||||
var box = li.getBoundingClientRect();
|
||||
var before = ev.clientY < box.top + box.height / 2;
|
||||
/* 搬的是整个 <li> —— 它自带的 order[] 隐藏域跟着走,
|
||||
所以提交上来的顺序 = 屏幕上的顺序,不用再同步一遍。 */
|
||||
if (before) { list.insertBefore(dragEl, li); } else { list.insertBefore(dragEl, li.nextSibling); }
|
||||
li.classList.remove('is-drop-before', 'is-drop-after');
|
||||
});
|
||||
});
|
||||
}
|
||||
Array.prototype.forEach.call(document.querySelectorAll('ol.order-list'), bindSort);
|
||||
|
||||
/* ---- 色板点一下就换 ---- */
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.preset-color[data-for]'), function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
var target = document.getElementById(el.getAttribute('data-for'));
|
||||
if (target) { target.value = el.getAttribute('data-color'); }
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
+156
-132
@@ -313,149 +313,173 @@ $fbCityN = count(\App\Geo::CITIES);
|
||||
内置了 <b><?= (int)$fbCityN ?></b> 个城市(全国地级市 + 直辖市),一般常用的都在里面。
|
||||
</p>
|
||||
|
||||
<!-- ==================== 我的分组 ==================== -->
|
||||
<?php /* 🔴 v52(用户 2026-10-15 第 6 条):「原来个人设置那里的名册分组……
|
||||
这样**个人设置中就不显示名册分组的内容**了。」
|
||||
⇒ 整块「我的分组」卡片**搬到了**「名册 → 设置 → 名册分组」
|
||||
(`templates/people/settings.php` + `templates/_person_groups.php`)。
|
||||
⚠️ 是**真的搬走**,不是复制一份:两份一模一样的分组管理界面,
|
||||
改一处忘一处是必然的。这里只留一句指路,免得老用户找不到。 */ ?>
|
||||
<div class="card card-pad" style="margin-top:20px">
|
||||
<h2 class="section-title">
|
||||
我的分组
|
||||
<span class="hint">公共的 + 我自己加的</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
名册是按「关系分组」分栏显示的。这里有两层:
|
||||
<b>公共分组</b>由管理员在「系统设置」里维护(全家都能用);
|
||||
你要是觉得不够,就在下面<b>加只给自己看的分组</b>,别人看不到、也不受影响。
|
||||
<br>
|
||||
列表里 <b>{{ 公共 }}</b> 标的是大家共用的那几个。
|
||||
哪一组排前面也是你自己的习惯,用 ▲ ▼ 调 —— 别人看到的顺序不变。
|
||||
<br>
|
||||
你的名册现在有 <b><?= (int)$rosterCount ?></b> 人。
|
||||
<h2 class="section-title">名册分组</h2>
|
||||
<p class="small muted" style="margin:0 0 12px">
|
||||
分组改到**名册页**那边去了 —— 它本来就是"名册怎么分组"的事,与个人设置无关。
|
||||
</p>
|
||||
|
||||
<?php if (!$groups): ?>
|
||||
<p class="small muted" style="margin:0 0 14px">还没有分组。加一个吧。</p>
|
||||
<?php else: ?>
|
||||
<ol class="order-list">
|
||||
<?php foreach ($groups as $i => $g): $gid = (int)$g['id']; $own = (int)$g['owner_id']; ?>
|
||||
<li class="order-row">
|
||||
<span class="order-no"><?= $i + 1 ?></span>
|
||||
<span class="order-name">
|
||||
<?= e((string)$g['name']) ?>
|
||||
<?php if ($own === 0): ?><span class="badge">公共</span><?php else: ?><span class="badge badge-open">我的</span><?php endif; ?>
|
||||
</span>
|
||||
<span class="order-count"><?= (int)($groupCounts[$gid] ?? 0) ?> 人</span>
|
||||
<form method="post" action="<?= e(url('settings/groups/move')) ?>" class="order-move">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $gid ?>">
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="up"
|
||||
title="上移一组" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="down"
|
||||
title="下移一组" <?= $i === $groupTotal - 1 ? 'disabled' : '' ?>>▼</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?= e(url('settings/groups/add')) ?>" class="inline-form" style="margin-top:16px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<label class="sr-only" for="newMyGroup">新分组名</label>
|
||||
<input class="input" id="newMyGroup" name="name" maxlength="20" placeholder="例如:球友、老同学">
|
||||
<button class="btn btn-sm" type="submit">加一个我自己的分组</button>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
// 只有我自己建的分组才给改名 / 删除 —— 公共分组要在系统设置里改(那是管理员的事),
|
||||
// 免得有人在自己设置里把"家人"改成别的、影响全家。
|
||||
$mineGroups = array_values(array_filter($groups, fn($g) => (int)$g['owner_id'] > 0));
|
||||
?>
|
||||
<?php if ($mineGroups): ?>
|
||||
<ul class="group-manage" style="margin-top:14px">
|
||||
<?php foreach ($mineGroups as $g): $gid = (int)$g['id']; ?>
|
||||
<li class="group-manage-row">
|
||||
<form method="post" action="<?= e(url('settings/groups/rename')) ?>" class="group-rename">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $gid ?>">
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<label class="sr-only" for="gname<?= $gid ?>">分组名</label>
|
||||
<input class="input" id="gname<?= $gid ?>" name="name" maxlength="20"
|
||||
value="<?= e((string)$g['name']) ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
|
||||
</form>
|
||||
<span class="group-count muted small"><?= (int)($groupCounts[$gid] ?? 0) ?> 人</span>
|
||||
<?php /* 删除做成两步(<details>),不用 JS 也能防手滑 —— 一点就没了太容易出事 */ ?>
|
||||
<details class="del-wrap">
|
||||
<summary class="btn btn-ghost btn-sm">删除</summary>
|
||||
<form method="post" action="<?= e(url('settings/groups/delete')) ?>" class="del-confirm">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $gid ?>">
|
||||
<input type="hidden" name="scope" value="me">
|
||||
<span class="small muted">这组的人会变成「未分类」,确定?</span>
|
||||
<button class="btn btn-ghost btn-sm danger" type="submit">确认删除</button>
|
||||
</form>
|
||||
</details>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (\App\Auth::isAdmin()): ?>
|
||||
<p class="small muted" style="margin:16px 0 0">
|
||||
公共分组在「<a href="<?= e(url('settings')) ?>#public-groups">系统设置</a>」里维护。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<div class="btn-row">
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/settings')) ?>">去名册设置里改分组</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 首页上的模块(用户 2026-10-03 第 3 条)==================== -->
|
||||
<?php /* 与下面「导航条上的模块」是同一套交互(勾选 + 拖动/▲▼ 排序),
|
||||
只是作用对象换成首页 dashboard 上那几张模块卡。
|
||||
两块都要有:导航条管"能不能进",这里管"首页上先看到什么"。 */ ?>
|
||||
<div class="card card-pad" id="home-mods" style="margin-top:20px">
|
||||
<!-- ==================== 模块:导航 / 首页 / 颜色 / 顺序(v52 用户第 7 条)==================== -->
|
||||
<?php /* 🔴🔴 v52(用户 2026-10-15 第 7 条):「个人设置里面,模块相关的设置太多了,应该**整合**,
|
||||
先列出来所有模块,中间有一列,让他勾选是否在导航显示,再给他一列,让他选择是否在首页显示,
|
||||
再给他一列,让他选择颜色,最后给他一列,让他**调整顺序**,这样就把几个设置,整合到一起了,
|
||||
更加简洁。」
|
||||
|
||||
⇒ 原来是**三张卡、三个表单、三次保存**:
|
||||
① 首页上的模块(勾选 + 排序)
|
||||
② 导航条上的模块(勾选 + 排序)
|
||||
③ 模块主题色(改色)
|
||||
同一串模块名出现两遍、颜色又另起一张 —— 用户看得累、改起来要点三次保存。
|
||||
⇒ 现在合成**一张表**:一行一个模块,四列 = 导航 / 首页 / 颜色 / 顺序。
|
||||
提交走 `settings/modules_save`(它内部把三个 save 依次调一遍,见 index.php 的说明)。
|
||||
|
||||
⚠️ 颜色那一列的权限**没有变**:它仍是家庭一份、只有管理员能改;
|
||||
非管理员那一列显示成只读色块(看得到、改不了)。
|
||||
⚠️ 拖动排序仍搬整行(行自带 order[]),▲▼ 是没 JS 时的兜底 —— 两条路都在。 */
|
||||
$plMAll = \App\Module::keys();
|
||||
$plMNav = array_map('strval', (array)(\App\Module::navFor((int)\App\Auth::personId())));
|
||||
$plMHome = array_map('strval', (array)(\App\HomeMods::forPerson((int)\App\Auth::personId())));
|
||||
$plMColor = \App\Module::colors((int)\App\Auth::familyId());
|
||||
$plMIsAdmin = \App\Auth::isAdmin();
|
||||
// 行的顺序:导航里显示的按导航顺序在前(那是用户最在意的),其余按默认顺序在后
|
||||
$plMOrder = [];
|
||||
foreach ($plMNav as $k) { if (in_array($k, $plMAll, true)) { $plMOrder[] = $k; } }
|
||||
foreach ($plMAll as $k) { if (!in_array($k, $plMOrder, true)) { $plMOrder[] = $k; } }
|
||||
?>
|
||||
<div class="card card-pad" id="modules" style="margin-top:20px">
|
||||
<h2 class="section-title">
|
||||
首页上的模块
|
||||
<span class="hint">勾上才会出现在首页,拖动可改顺序</span>
|
||||
模块设置
|
||||
<span class="hint">导航 / 首页 / 颜色 / 顺序,一页改完</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
首页是"一眼看家里近况"的地方,模块太多会变成一面墙 —— 这里只挑你想每天看到的几个。
|
||||
关掉的模块<b>照样能从顶部导航条进去</b>,只是不在首页上占位置。
|
||||
模块越来越多,但每个人关心的不一样 —— 这里一行一个模块:
|
||||
<b>导航</b>管"顶栏上有没有它",<b>首页</b>管"打开首页看不看得到",
|
||||
<b>颜色</b>是进到那个模块后顶栏高亮的色(<b>全家共用一套</b>),
|
||||
<b>顺序</b>用拖动手柄或 ▲▼ 调。
|
||||
<br>关掉的模块<b>不会丢数据</b>,只是从顶栏 / 首页收起来,别处仍能进。
|
||||
</p>
|
||||
|
||||
<form method="post" action="<?= e(url('settings/home_save')) ?>" id="homeForm">
|
||||
<form method="post" action="<?= e(url('settings/modules_save')) ?>" id="modulesForm">
|
||||
<?= csrf_field() ?>
|
||||
<ol class="order-list mod-order" id="homeOrderList">
|
||||
<?php
|
||||
$hOn = array_map('strval', (array)($homeOn ?? []));
|
||||
$hOrder = (array)($homeOrder ?? []);
|
||||
?>
|
||||
<?php foreach ($hOrder as $i => $k): ?>
|
||||
<li class="order-row mod-row" draggable="true" data-key="<?= e($k) ?>">
|
||||
<span class="drag-handle" title="拖我排序" aria-hidden="true">⋮⋮</span>
|
||||
<input type="hidden" name="order[]" value="<?= e($k) ?>">
|
||||
<label class="mod-pick">
|
||||
<input type="checkbox" name="on[]" value="<?= e($k) ?>"
|
||||
<?= in_array($k, $hOn, true) ? 'checked' : '' ?>>
|
||||
<span class="color-preview"
|
||||
style="background: <?= e(\App\Module::color($k, (int)\App\Auth::familyId())) ?>;"></span>
|
||||
<span class="order-name"><?= e(\App\Module::name($k)) ?></span>
|
||||
</label>
|
||||
<span class="order-move">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="up:<?= e($k) ?>"
|
||||
title="上移" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="down:<?= e($k) ?>"
|
||||
title="下移" <?= $i === count($hOrder) - 1 ? 'disabled' : '' ?>>▼</button>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit" name="save" value="1">保存首页显示</button>
|
||||
<div class="mod-table-wrap">
|
||||
<table class="mod-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mod-th-order">顺序</th>
|
||||
<th class="mod-th-name">模块</th>
|
||||
<th class="mod-th-on">导航</th>
|
||||
<th class="mod-th-on">首页</th>
|
||||
<th class="mod-th-color">颜色</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="modRows">
|
||||
<?php foreach ($plMOrder as $i => $k): ?>
|
||||
<tr class="mod-row" draggable="true" data-key="<?= e($k) ?>">
|
||||
<td class="mod-td-order">
|
||||
<span class="drag-handle" title="拖我排序" aria-hidden="true">⋮⋮</span>
|
||||
<?php /* ⚠️ 顺序就藏在 DOM 里:每行自带 order[] 隐藏域,拖动搬整行 ⇒
|
||||
提交上来的顺序天然就是屏幕上的顺序(与旧的两块同一手法)。 */ ?>
|
||||
<input type="hidden" name="order[]" value="<?= e($k) ?>">
|
||||
<span class="mod-move">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="up:<?= e($k) ?>"
|
||||
title="上移" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="down:<?= e($k) ?>"
|
||||
title="下移" <?= $i === count($plMOrder) - 1 ? 'disabled' : '' ?>>▼</button>
|
||||
</span>
|
||||
</td>
|
||||
<td class="mod-td-name">
|
||||
<span class="color-preview" style="background: <?= e((string)($plMColor[$k] ?? '#888')) ?>;"></span>
|
||||
<?= e(\App\Module::name($k)) ?>
|
||||
</td>
|
||||
<td class="mod-td-on">
|
||||
<label class="mod-chk" title="显示在顶部导航条">
|
||||
<input type="checkbox" name="nav[]" value="<?= e($k) ?>"
|
||||
<?= in_array($k, $plMNav, true) ? 'checked' : '' ?>>
|
||||
</label>
|
||||
</td>
|
||||
<td class="mod-td-on">
|
||||
<label class="mod-chk" title="显示在首页">
|
||||
<input type="checkbox" name="home[]" value="<?= e($k) ?>"
|
||||
<?= in_array($k, $plMHome, true) ? 'checked' : '' ?>>
|
||||
</label>
|
||||
</td>
|
||||
<td class="mod-td-color">
|
||||
<?php if ($plMIsAdmin): ?>
|
||||
<input type="color" class="color-picker" name="c[<?= e($k) ?>]"
|
||||
value="<?= e((string)($plMColor[$k] ?? '#888888')) ?>"
|
||||
aria-label="<?= e(\App\Module::name($k)) ?>的颜色">
|
||||
<?php else: ?>
|
||||
<?php /* 非管理员:看得到颜色、改不了(家庭一份,见本页说明) */ ?>
|
||||
<span class="color-preview mod-color-ro"
|
||||
style="background: <?= e((string)($plMColor[$k] ?? '#888')) ?>;"
|
||||
title="由管理员设置"></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn-row" style="margin-top:14px">
|
||||
<button class="btn" type="submit" name="save" value="1">保存模块设置</button>
|
||||
<?php if ($plMIsAdmin): ?>
|
||||
<span class="small muted">颜色是全家共用的一套,改了对所有人都生效。</span>
|
||||
<?php else: ?>
|
||||
<span class="small muted">颜色由管理员设置,你这里只读。</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 模块:导航条上显示哪些、各自什么主题色 ==================== -->
|
||||
<?php
|
||||
$navKeys = \App\Module::navFor((int)\App\Auth::personId());
|
||||
$colors = \App\Module::colors((int)\App\Auth::familyId());
|
||||
require __DIR__ . '/_modules.php';
|
||||
?>
|
||||
<script>
|
||||
(function () {
|
||||
/* 拖动排序(▲▼ 是不用 JS 的兜底,两条路都能用)。
|
||||
⚠️ 搬的是整行 <tr> —— 它自带的 order[] 隐藏域跟着走,
|
||||
所以提交上来的顺序 = 屏幕上的顺序,不用再同步一遍。 */
|
||||
var list = document.getElementById('modRows');
|
||||
if (!list) { return; }
|
||||
var dragEl = null;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.mod-row'), function (tr) {
|
||||
tr.addEventListener('dragstart', function (ev) {
|
||||
dragEl = tr;
|
||||
tr.classList.add('is-dragged');
|
||||
if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; }
|
||||
});
|
||||
tr.addEventListener('dragend', function () {
|
||||
tr.classList.remove('is-dragged');
|
||||
dragEl = null;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.mod-row'), function (x) {
|
||||
x.classList.remove('is-drop-before');
|
||||
});
|
||||
});
|
||||
tr.addEventListener('dragover', function (ev) {
|
||||
if (!dragEl || dragEl === tr) { return; }
|
||||
ev.preventDefault();
|
||||
var box = tr.getBoundingClientRect();
|
||||
tr.classList.toggle('is-drop-before', ev.clientY < box.top + box.height / 2);
|
||||
});
|
||||
tr.addEventListener('dragleave', function () { tr.classList.remove('is-drop-before'); });
|
||||
tr.addEventListener('drop', function (ev) {
|
||||
if (!dragEl || dragEl === tr) { return; }
|
||||
ev.preventDefault();
|
||||
var box = tr.getBoundingClientRect();
|
||||
if (ev.clientY < box.top + box.height / 2) {
|
||||
list.insertBefore(dragEl, tr);
|
||||
} else {
|
||||
list.insertBefore(dragEl, tr.nextSibling);
|
||||
}
|
||||
tr.classList.remove('is-drop-before');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user