v62:纪念日列表修正 / 分类图标与设置菜单 / 首页卡片体系
- 纪念日列表:display:flex 从 td 移到内层 div(写在 td 上会破坏表格列算法, 表现为关联人列与其它列高度不一致);行内列序按表头对齐; 改完再点「新建」不再带出上次修改的数据;文案改为「与我相关」「来自名册」; 别人记的、把我挂成关联人的条目一并显示(split() 返回 hiddenIds, 路由层据此过滤「接下来的日子」,与两张表同一套可见性) - 分类图标:新增 app/AnnivIcon.php(17 个行内 SVG 图标 + 同 key 的 emoji 供日历纯文本处使用,内网无 CDN 不用 FontAwesome);去掉「看日历」按钮; 设置按钮改为菜单,第一项进 days/settings 独立页;分类可自选图标, 列表徽标与日历条目都按分类显示 - 首页卡片:新增 app/HomeCards.php 与 templates/home_cards.php; 8 格宽 × 高 2/4/6/8;首页拆成 templates/home/_card_*.php 共 8 个片段; home/cards 可改尺寸、调顺序、开关
This commit is contained in:
@@ -29,6 +29,7 @@ use App\Expense;
|
||||
use App\Family;
|
||||
use App\Geo;
|
||||
use App\Home;
|
||||
use App\HomeCards;
|
||||
use App\HomeMods;
|
||||
use App\Holiday;
|
||||
use App\Anniversary;
|
||||
@@ -847,6 +848,16 @@ switch ($r) {
|
||||
$homeMods = HomeMods::forPerson($me);
|
||||
$moduleCards = Home::moduleCards($familyId, $me, $soon, $tasks);
|
||||
|
||||
/* 🔴 v62(用户 2026-10-18 第 3 条):「今天,我们正式引入卡片的概念。」
|
||||
⇒ 首页从"三块写死的区块"改成**卡片网格**:
|
||||
· `$homeCards` 是这个人要摆哪几张、各自多宽多高(user_settings.home_cards);
|
||||
· `$cardData` 只给那几张需要额外查询的卡片取数(纪念日/观影/读书/联系人)。
|
||||
⚠️ 缺的卡片不查库 —— 关掉的卡片不该白跑一次 SQL。
|
||||
⚠️ 日程/任务/往年的今天/概述这四张用的还是下面那几个变量(同一份数据),
|
||||
不要在模板里再查第二遍。 */
|
||||
$homeCards = HomeCards::layout($me);
|
||||
$cardData = HomeCards::data($homeCards, $familyId, $me);
|
||||
|
||||
// 统计信息块:随机问候 + 今日天气 + 近七天的数字
|
||||
$weekEnd = date('Y-m-d', strtotime('+7 days'));
|
||||
$st7 = Database::get()->prepare(
|
||||
@@ -936,6 +947,9 @@ switch ($r) {
|
||||
'today' => $today,
|
||||
'homeModules'=> $homeMods,
|
||||
'moduleCards'=> $moduleCards,
|
||||
// v62 卡片体系
|
||||
'homeCards' => $homeCards,
|
||||
'cardData' => $cardData,
|
||||
// v30 第 5 条:通知块(放在「往年的今天」之前,见 templates/home.php)
|
||||
// ⚠️ 首页只给**最新的几条**(10 条)就够 —— 它是"瞥一眼"的地方,
|
||||
// 真正的消息中心在 ?r=notifications。
|
||||
@@ -944,6 +958,84 @@ switch ($r) {
|
||||
], 'home');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 首页卡片布局设置(v62 用户 2026-10-18 第 3 条)。
|
||||
*
|
||||
* 用户原话:「卡片的规格,我们可以定义一下,例如,把宽度定义为 8,高度可以是
|
||||
* 2、4、6、8……你可以先规划一些,实现出来,我们再讨论。」
|
||||
*
|
||||
* ⇒ 这是**每人一份**的设置(首页怎么摆是很个人的事),存 user_settings。
|
||||
* ⚠️ 与「模块设置 → 导航」那套完全分开:导航管顶栏,这里只管首页的卡片。
|
||||
*/
|
||||
case 'home/cards':
|
||||
Auth::requireLogin();
|
||||
$me = (int)Auth::personId();
|
||||
View::render('home_cards', [
|
||||
'title' => '首页卡片',
|
||||
'rows' => HomeCards::orderedForPicker($me),
|
||||
], 'home');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 保存首页卡片布局。
|
||||
*
|
||||
* 表单形状:每行一个 `order[]`(屏幕顺序)+ `on[<key>]` 勾选 + `w[<key>]` / `h[<key>]`
|
||||
* + `n[<key>]`(只有"联系人"用得上)。
|
||||
* ⚠️ 顺序**按 order[] 取**,不按 POST 里 on 的顺序 —— 浏览器提交复选框的顺序
|
||||
* 并不保证与 DOM 顺序一致(有些浏览器按 name 分组提交)。
|
||||
*/
|
||||
case 'home/cards_save':
|
||||
Auth::requireLogin();
|
||||
csrf_check();
|
||||
$me = (int)Auth::personId();
|
||||
$order = array_map('strval', (array)($_POST['order'] ?? []));
|
||||
/* ⚠️ `on[<key>]` 是**按卡片 key 作数组下标**提交的(`on[brief]=1`),
|
||||
所以判"勾没勾"必须 `isset($on[$k])` ——
|
||||
写成 `in_array($k, $on)` 是错的:那比的是**值**(全是 "1"),
|
||||
结果一个都匹配不上,保存出来是**空布局**(首页变成"卡片都关掉了")。 */
|
||||
$on = (array)($_POST['on'] ?? []);
|
||||
$ws = (array)($_POST['w'] ?? []);
|
||||
$hs = (array)($_POST['h'] ?? []);
|
||||
$ns = (array)($_POST['n'] ?? []);
|
||||
/* ▲▼ 是**没有 JS 时的兜底**(页面里还有拖动排序,两条路都要能走):
|
||||
先把屏幕顺序里这两个换位,再照新顺序建卡片列表 —— 于是"点了上移"
|
||||
与"改完再点保存"得到的是同一个结果。 */
|
||||
$move = (string)($_POST['move'] ?? '');
|
||||
if ($move !== '' && strpos($move, ':') !== false) {
|
||||
[$dir, $mkey] = explode(':', $move, 2);
|
||||
$mi = array_search($mkey, $order, true);
|
||||
if ($mi !== false) {
|
||||
$mj = $dir === 'up' ? $mi - 1 : $mi + 1;
|
||||
if ($mj >= 0 && $mj < count($order)) {
|
||||
[$order[$mi], $order[$mj]] = [$order[$mj], $order[$mi]];
|
||||
}
|
||||
}
|
||||
}
|
||||
$items = [];
|
||||
foreach ($order as $k) {
|
||||
if (!isset($on[$k])) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'k' => $k,
|
||||
'w' => (int)($ws[$k] ?? 0),
|
||||
'h' => (int)($hs[$k] ?? 0),
|
||||
'n' => (int)($ns[$k] ?? 0),
|
||||
];
|
||||
}
|
||||
// 一个都没勾 = 空首页。这是用户的选择(不是错误),照存。
|
||||
HomeCards::save($me, $items);
|
||||
/* ⚠️ ▲▼ 只是"编辑中的一个动作",**不能跳去首页**:
|
||||
跳走等于把编辑器关掉,用户想再挪一格就得重新进来(实测就是这样,
|
||||
连点两下只会生效一次)。只有真正"保存"才回首页。 */
|
||||
if ($move !== '') {
|
||||
flash('ok', '顺序已调整。');
|
||||
redirect('home/cards');
|
||||
}
|
||||
flash('ok', '首页卡片已保存。');
|
||||
redirect('home');
|
||||
break;
|
||||
|
||||
// ==================== 家谱(以我为中心) ====================
|
||||
case 'kin':
|
||||
$familyId = (int)Auth::familyId();
|
||||
@@ -1760,7 +1852,12 @@ switch ($r) {
|
||||
$navKeys = Module::DEFAULT_NAV; // 一个都不勾 = 导航条会空掉,退回默认
|
||||
}
|
||||
Module::saveNav($me, $navKeys);
|
||||
HomeMods::save($me, $homeKeys);
|
||||
/* ⚠️ v62:模块设置里**已经没有「首页」那一列**(首页改由「首页卡片」摆,
|
||||
见 HomeCards)。所以只有表单真的带了 `home[]` 才写 —— 否则每次保存导航
|
||||
都会把 HomeMods 清成空数组("关了还在"的反面:"没动也丢")。 */
|
||||
if (array_key_exists('home', $_POST)) {
|
||||
HomeMods::save($me, $homeKeys);
|
||||
}
|
||||
|
||||
// 主题色:家庭一份,只有管理员能改(非管理员提交时**静默忽略**,不报错)
|
||||
if (Auth::isAdmin() && isset($_POST['c']) && is_array($_POST['c'])) {
|
||||
@@ -4638,12 +4735,26 @@ $want = array_keys($want);
|
||||
foreach ($people as $pp) {
|
||||
$pmap[(int)$pp['id']] = $pp;
|
||||
}
|
||||
/* 「接下来的日子」必须和下面两张表**用同一套可见性**:
|
||||
否则别人记的、与你无关的那条会孤零零出现在最上面,
|
||||
而下面「与我相关」「来自名册」里都找不到它(v62 实测踩到)。
|
||||
⚠️ 只筛这个页面;日历是全家共享视图,照旧全显示。 */
|
||||
$upcoming = Anniversary::upcoming($familyId, $me, 60);
|
||||
if (!empty($sp['hiddenIds'])) {
|
||||
$hid = array_flip(array_map('intval', $sp['hiddenIds']));
|
||||
$upcoming = array_values(array_filter($upcoming, static function ($u) use ($hid) {
|
||||
return !isset($hid[(int)($u['id'] ?? 0)]);
|
||||
}));
|
||||
}
|
||||
View::render('days/index', [
|
||||
'title' => '纪念日',
|
||||
'custom' => $sp['custom'],
|
||||
'derived' => $sp['derived'],
|
||||
'upcoming' => Anniversary::upcoming($familyId, $me, 60),
|
||||
// v62:别人记的、与我无关的条数(只在页面上给一句小字)
|
||||
'hidden' => $sp['hidden'],
|
||||
'upcoming' => $upcoming,
|
||||
'kinds' => Anniversary::kinds($familyId),
|
||||
'kindsFull' => Anniversary::kindsFull($familyId),
|
||||
'kindCounts' => Anniversary::kindCounts($familyId),
|
||||
'people' => $people,
|
||||
'peopleMap' => $pmap,
|
||||
@@ -4651,12 +4762,102 @@ $want = array_keys($want);
|
||||
break;
|
||||
|
||||
/**
|
||||
* 纪念日分类设置(v24 用户 2026-10-03 第 9 条)。
|
||||
* 纪念日分类设置页(v62 用户 2026-10-18 第 2 条)。
|
||||
*
|
||||
* 用户原话:「在新建纪念日和右侧,也增加设置按钮,可以设置纪念日的分类。」
|
||||
* 用户原话:「设置按钮,点击后弹菜单,第一个菜单是分类设置,进去之后可以设置分类,
|
||||
* 每一类纪念日都有不同的图标……如果用户添加了自己的分类,就让用户自己选。」
|
||||
*
|
||||
* 分类清单是**家庭级**的(全家共用一套,不然「结婚纪念日」在两个人屏幕上
|
||||
* 是两套叫法,筛选/看的时候对不上)。所以放在 settings 而不是 user_settings。
|
||||
* ⇒ 与日程/观影/任务三个模块同一套形态:☰ 下拉 → 点条目 → 独立页面。
|
||||
*
|
||||
* 表单是**一张**(改图标 / 改名 / 排序 / 删 / 加 全在里面),靠按钮的 name 区分动作:
|
||||
* save / del / move / add。为什么不用"每行一个小表单":那样改了 A 行图标再点
|
||||
* B 行删除,A 行那次修改会被浏览器丢掉(只提交被点的那张表单)——
|
||||
* "改了一半"是这类设置页最常见、也最难 debug 的投诉。
|
||||
*/
|
||||
case 'days/settings':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$names = (array)($_POST['k_name'] ?? []);
|
||||
$icons = (array)($_POST['k_icon'] ?? []);
|
||||
// 用**原始下标**当键:空名字被跳过时下标也不能重排,
|
||||
// 否则「删第 3 行」会删到第 3 个非空行(两者不是一回事)。
|
||||
$items = [];
|
||||
$tooLong = 0;
|
||||
foreach ($names as $i => $nm) {
|
||||
$nm = trim((string)$nm);
|
||||
if ($nm === '') {
|
||||
continue;
|
||||
}
|
||||
if (mb_strlen($nm, 'UTF-8') > 20) {
|
||||
$tooLong++;
|
||||
continue;
|
||||
}
|
||||
$items[(string)$i] = [
|
||||
'name' => $nm,
|
||||
'icon' => (string)($icons[$i] ?? ''),
|
||||
];
|
||||
}
|
||||
$notice = '分类已保存。';
|
||||
if (isset($_POST['del'])) {
|
||||
unset($items[(string)(int)$_POST['del']]);
|
||||
$notice = '分类已删除(已用它的纪念日不变,只是新建时不再出现)。';
|
||||
} elseif (isset($_POST['move'])) {
|
||||
$mv = explode(':', (string)$_POST['move'], 2);
|
||||
$idx = (string)(int)($mv[0] ?? -1);
|
||||
$keys = array_keys($items);
|
||||
$pos = array_search($idx, $keys, true);
|
||||
if ($pos !== false) {
|
||||
$to = ($mv[1] ?? 'up') === 'up' ? $pos - 1 : $pos + 1;
|
||||
if ($to >= 0 && $to < count($keys)) {
|
||||
$tmp = $keys[$pos];
|
||||
$keys[$pos] = $keys[$to];
|
||||
$keys[$to] = $tmp;
|
||||
$reordered = [];
|
||||
foreach ($keys as $k) {
|
||||
$reordered[$k] = $items[$k];
|
||||
}
|
||||
$items = $reordered;
|
||||
}
|
||||
}
|
||||
$notice = '顺序已调整。';
|
||||
} elseif (isset($_POST['add'])) {
|
||||
$nn = trim((string)($_POST['new_name'] ?? ''));
|
||||
if ($nn === '') {
|
||||
flash('err', '新分类的名字是空的。');
|
||||
redirect('days/settings');
|
||||
}
|
||||
if (mb_strlen($nn, 'UTF-8') > 20) {
|
||||
flash('err', '分类名最多 20 个字。');
|
||||
redirect('days/settings');
|
||||
}
|
||||
$items[] = ['name' => $nn, 'icon' => (string)($_POST['new_icon'] ?? '')];
|
||||
$notice = '分类已添加。';
|
||||
}
|
||||
Anniversary::saveKinds($familyId, array_values($items));
|
||||
if ($tooLong > 0) {
|
||||
flash('err', '有 ' . $tooLong . ' 个分类名超过 20 个字,已跳过。');
|
||||
}
|
||||
flash('ok', $notice);
|
||||
redirect('days/settings');
|
||||
}
|
||||
View::render('days/settings', [
|
||||
'title' => '纪念日分类',
|
||||
'kindsFull' => Anniversary::kindsFull($familyId),
|
||||
'kindCounts' => Anniversary::kindCounts($familyId),
|
||||
'meId' => $me,
|
||||
], 'days');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 纪念日分类设置(v24 用户 2026-10-03 第 9 条)—— **老入口,保留兼容**。
|
||||
*
|
||||
* v62 起前端的入口已经改成 `days/settings` 那一页(带图标)。这个 POST 端点留着:
|
||||
* ① 老页面/书签里还有提交到它的表单;
|
||||
* ② 反向验证脚本要能单独打它(证明"删一个分类"这条链路没断)。
|
||||
* ⚠️ 它只处理**名字**(add / del / 整份 kinds)——新格式的图标由 saveKinds 兜底补。
|
||||
*/
|
||||
case 'days/kinds':
|
||||
$familyId = (int)Auth::familyId();
|
||||
@@ -4724,6 +4925,8 @@ $want = array_keys($want);
|
||||
$anArgs = [
|
||||
'editing' => $editing,
|
||||
'kinds' => Anniversary::kinds($familyId),
|
||||
// v62:带图标的清单(下拉里用 emoji 打头,见 form.php)
|
||||
'kindsFull' => Anniversary::kindsFull($familyId),
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
/* 🔴 v41(用户第 10 条):「新建纪念日,采用弹框形式。」
|
||||
直达 `days/new` 时也渲染成**打开着的弹框** ——
|
||||
|
||||
Reference in New Issue
Block a user