Files
FamilyBook/templates/settings/_modules.php
T
laoyang 02f5d072a4 家谱卡片重做 / 首页控制台 / 名册改列表 / 重复日程与旅程公开
用户 2026-10-03 提的四条:

① 家谱卡片重做
   · 去掉「⇄ 姻亲」按钮,改成**每个人**卡片右下角都有「以 TA 为中心」
     (点了以他重画一张家谱,涵盖原来的姻亲场景)。
   · 头像外框改**长方形**(96×72,宽度贴合头像),object-fit: contain 不裁切,
     姓名/称呼在头像下方。
   · 修「点右下角按钮误进成员页」:卡片不再整张是 <a>,
     只有姓名是链接,右下角是独立的 <button>(物理上分开,不抢点击)。

② 家谱文案与按钮清理
   · 去掉副标题与下方图例说明;整块去掉「看全部 / 回到我 / 回到我的家谱」
     (顺带消掉"出现两个回到我的家谱")。镜头由库自动 fit,拖拽/滚轮即可。

③ 首页控制台
   · 标题下显示个人 slogan;去掉右侧三个统计;「我的资料/我的家谱」换成图标按钮;
     去掉与导航条重复的「名册」按钮。
   · 去掉「接下来的日子」(生日归到"未来七天"里,不再重复)。
   · 新增统计信息块:按时段随机问候 + 今日天气(Open-Meteo,免 key,可留空不显示)
     + 近七天日程/待办/逾期数。
   · 新增各模块小结卡(任务/日程/纪念日/观影/旅程/财务/名册/家谱/资源)。
   · 个人设置新增「首页上的模块」:可勾选与拖动排序(每人一份,存 user_settings)。
   · 保留「往年的今天」。

④ 名册改列表
   · 卡片网格 → **竖向列表**:左头像、中间姓名+与我的关系+手机号+生日。
   · 修拖动「只能往后不能往前」:落点改为显式记录"插到谁前面/后面",
     并排除被拖元素自身。
   · 去掉那句拖动提示(能力保留)。
   · 搜索新增**姓名首字母**检索(y / yy / yyq 都命中「杨永全」):
     新 app/Pinyin.php,intl 优先 + 内置常用字表兜底(本地无 intl 也能测);
     纯字母/数字查询词不走 SQL LIKE(拼音与加密手机号在库里都比不了)。

附带上两轮的收尾:
   · 菜单点外部自动关闭(全局事件委托,覆盖所有 details.menu)。
   · 旅程 is_public:列表与日历只显示公开的;详情页不拦(不丢数据)。
   · 重复日程:新 app/Repeat.php(RRULE 解析/生成/展开,含"工作日"),
     新表 calendar_event_skips(RFC 的 EXDATE);日历按规则展开;
     编辑支持三档「全部 / 这一次及以后 / 仅此一次」。
   · 结束时间可添加/去掉,并随开始时间联动。

验证:端到端 772 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 26、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
2026-10-03 11:30:48 +08:00

177 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>