v14: 观影/家谱/日程/旅程/资源/任务六模块统一,首页改 dashboard
用户 14 条需求一次交付(1-13 功能 + 14 工作方式)。 观影(1-4) - 返回文案全模块统一为「返回观影」(不再有「返回列表」「返回观影记录」三种叫法) - 观影人一律用彩色人物标签 person_chip:底色由头像主色算出来(上传时存库), 瀑布流卡片也按人显示;五处观影模板全部改用它 - 人员显示统一走 person_name()(shown → 我给他起的称呼 → 姓名) - 影院管理页与年度报告页的上下两块用 .card + .card 统一给间距(原来紧贴成一整块) 家谱(5) - 树根从「我」换成**辈分最高的祖先**:库画的是"树根的祖先链+后代+树根的兄弟姐妹", 根=我 时伯父/堂兄/嫂子/侄子一个都不画(12 人夹具只画出 8 人) - focusMap 按**代数**圈定三代(不再按步数),normalizeLinks 把子女对称挂到父母双方 —— 修掉"母亲登录看不到孩子" - 新增「看全部 / 回到我」两个按钮,放在画布外(画布整块被库接管了手势) 统一事件原语(6-9) - 旅程:TripLog 全量迁入(3 趟旅程 / 21 段行程 / 72 张照片 / 5 个人)。 复用 events(kind='trip') + trip_details + trip_nodes(计划/执行双轨保留) - 日程:每人可建日历本(自定义色)+ 共享只读/可写;观影与旅行是每人默认两本, 内容是**从各模块实时派生**的(不复制,所以改了观影日期日历立刻跟着变); 没有具体时分 = 全天;月视图参照群晖 Calendar - 资源:挂在某人名下的附属信息(标题+正文+多附件),可共享给多人并设只读/读写, 默认折叠、可搜标题与正文 - 任务:看板三列 + 两级子任务 + 评论@人 + 共享 + 循环(完成时按原截止日推下一期) - 统一附件表:四个模块的附件共用一张,下载地址不落库、一律由 media.php 现算 模块化(10-11) - App\Module 是模块的唯一来源(加一行 = 导航条/主题色/个人设置三处自动出现) - 个人设置里可勾选要显示哪些模块、拖动排序(关掉的模块**不会**被兜底补回来) - 每个模块一套主题色(家庭级),只覆盖 --brand 三兄弟,不碰纸墨底色 其他(12-13) - 新增成员表单可选所属分组(编辑表单没有:分组是"我和他的关系",只归我自己) - 首页改 dashboard:接下来的日子 / 未来七天 / 手头在做 / 往年今日。 家人卡片从首页撤走(看人去名册) 验证(全部通过) - e2e 542 条(新增 4 模块端到端 60+ 条) - 桌面几何 368 / 移动端几何 520 / 家谱 12 / 交互 97 / 老库升级 123 - SQL 列名静态校验 0 处可疑(对着真实表结构) - 反向验证 18 条新注入全部抓住 顺带修掉的既有问题 - 老库升级回填 SQL 按实际存在的列动态拼(缺列不再 1054 炸断升级) - 全新安装时 Schema::ensure 跑在"人还没建"之前 → 补建日历本三处齐备 - 窄屏导航横向溢出(模块从 4 个加到 8 个后顶栏撑破视口)
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<?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 的兜底,两条路都能用)---- */
|
||||
var list = document.getElementById('navOrderList');
|
||||
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('.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>
|
||||
Reference in New Issue
Block a user