v28 第 28 批(12 条):名册分组改标签 / 资源合并为两按钮 / 首页定位与主题色 / 笔记富文本双模式
其中 4 条是**上批遗漏的真 bug**(1/4/8/9),根因已逐条定位:
1. 首页日程没定位到红线 —— 真根因:刻度区间**不含当前时刻** → $showNow=false
→ 红线**整个不渲染**,而定位脚本用同一个越界比例算目标,被 clamp 到底部。
修:把当前小时并进区间端点(现在必然落在区间内)。
2. 「看整月」→「更多日程」+ 日程主题色;3. 「手头在做」→「近期任务」、
「任务看板」→「全部任务」+ 任务主题色(注入 --hd-c,不再写死白底)。
4. 任务复选框提示还在 —— 真根因:**JS 在点击后**给状态徽标挂 title
(`badge.setAttribute('title',…)`),上批只去了静态渲染,静态断言永远抓不到。
5. 名册「分组」→「标签」(用户:一个人可能既是小学同学又是初中同学,没法分组):
新增 person_tags(每人一份、一人多标签),名册改**平铺 + 顶部标签筛选**;
🔴 迁移用**一次性哨兵**(第一版用 NOT EXISTS,实测被"用户清空标签"绕过 → 旧分组名被灌回)。
6. 名册卡片按钮加语义底色。
7. 资源四按钮 → **编辑 / 删除**两颗:三块内容进一个带页签的弹框(信息/附件/共享)。
8. 财务多选数字不变 —— 真根因:前端发 `kinds=a,b`(逗号串),服务端 `(array)` 收成
单元素 → 求交为空 → **静默回落整本账**。修:两种形状都收。
9. 微信读书偶发很慢 —— 真根因:syncNow 扫 60 本 × 每本 1~3 次网关调用(单次超时 20s),
全同步阻塞在列表页请求里。修:总时长预算 + 单次超时 8s + 自动/手动分批。
10. 新增全局可见的联想接口库(6 个实测可用来源)+ 三种新解析类型。
11. 导航右上角「设置」:默认进"添加网址 + 全部网址",四类设置收进 ?set=1 视图。
12. 笔记:段落/清单支持 Markdown(自研渲染器,**不允许原始 HTML**),
实时预览走服务端(前后端只一份规则);底部附件上传;清单多级;
列表改**定尺寸卡片网格**(溢出显示摘要、图片缩放下方);按用户要求**去掉标签侧栏**。
This commit is contained in:
@@ -35,6 +35,7 @@ use App\Ics;
|
||||
use App\Kinship;
|
||||
use App\Ledger;
|
||||
use App\Lunar;
|
||||
use App\Markdown;
|
||||
use App\Movie;
|
||||
use App\MovieReview;
|
||||
use App\Module;
|
||||
@@ -50,6 +51,7 @@ use App\PersonJob;
|
||||
use App\PersonNote;
|
||||
use App\PersonQuery;
|
||||
use App\PersonRoster;
|
||||
use App\PersonTag;
|
||||
use App\Pinyin;
|
||||
use App\Relations;
|
||||
use App\Repeat;
|
||||
@@ -917,12 +919,26 @@ switch ($r) {
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// 按关系分组分栏。顺序用**当前用户自己的**(没自定义过就是默认顺序);
|
||||
// 归到键 0 的那一堆是还没分类的人,模板单独放在最后一栏。
|
||||
$grouped = [];
|
||||
foreach ($rows as $row) {
|
||||
$grouped[(int)($row['group_id'] ?? 0)][] = $row;
|
||||
// 🔴🔴 v28 用户第 5 条:「名册的分组,用下来不太好,我感觉应该改成打标签,
|
||||
// 就是说,一个人,他可能并不是只在一个分组,例如,他既是我的小学同学,
|
||||
// 又是我的初中同学,就没法分组,改成标签,似乎更合理。」
|
||||
// ⇒ 不再**按分组分栏**(`$grouped`),改成:
|
||||
// · 名册**平铺**成一张列表(带拖动排序,用户明确要求保留);
|
||||
// · 顶部一条**标签筛选**(`?tag=xxx`),点哪个看哪个;
|
||||
// · 每张卡片上挂着他自己的那几个标签(一个人可以有好几个)。
|
||||
// ⚠️ 仍然传 `groups`:这就是"标签的候选来源"(用户可以在卡片上选已有的标签),
|
||||
// 它本身没废 —— 只是从"分栏依据"降级成"可选的标签词表"。
|
||||
$tagFilter = trim((string)($_GET['tag'] ?? ''));
|
||||
$tagsOf = PersonTag::mapFor($familyId, $me);
|
||||
// 按标签筛选:留在这个标签里的人(`?tag=` 为空 = 全部)
|
||||
if ($tagFilter !== '') {
|
||||
$rows = array_values(array_filter($rows,
|
||||
static fn(array $r): bool => in_array($tagFilter, $tagsOf[(int)$r['id']] ?? [], true)));
|
||||
}
|
||||
foreach ($rows as &$row) {
|
||||
$row['tags'] = $tagsOf[(int)$row['id']] ?? [];
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// v24 第 9 批 #5:千人名册的 HTML 有 3~4 MB。本机 php -S 与部分虚拟机
|
||||
// 都不会替 PHP 输出开 gzip,原样吐 3.8 MB 又慢又费流量;
|
||||
@@ -936,8 +952,12 @@ switch ($r) {
|
||||
View::render('people/index', [
|
||||
'title' => '家庭名册',
|
||||
'people' => $rows,
|
||||
'grouped'=> $grouped,
|
||||
// v28:`$grouped`(按分栏分堆)已不再需要 —— 名册平铺(见上面注释)。
|
||||
// 不再传它,模板也不再读(多传一个变量迟早有人用它,把分栏又带回来)。
|
||||
'groups' => PersonGroup::orderFor($familyId, $me),
|
||||
// v28 用户第 5 条:标签(筛选条 + 卡片上的标签 + 编辑标签时的候选)
|
||||
'tagCloud' => PersonTag::cloud($familyId, $me),
|
||||
'tagFilter' => $tagFilter,
|
||||
'q' => $q,
|
||||
'meId' => $me,
|
||||
// 家里已经有人、但还不在我名册里 → 给一个「直接挑」的入口,
|
||||
@@ -1022,12 +1042,81 @@ switch ($r) {
|
||||
break;
|
||||
|
||||
/**
|
||||
* 名册拖动排序 / 拖动换栏 —— 整版提交。
|
||||
* 名册卡片上的「标签」(v28 用户第 5 条:分组 → 标签)。
|
||||
*
|
||||
* 前端把每一栏(含「未分类」)的有序 id 列表发过来,服务端一次写完。
|
||||
* 与 people/group 同一套做法(AJAX 回 JSON 就地更新 / 普通提交 302 兜底),
|
||||
* 只是把"选一个分组"换成"整组替换他的标签"。
|
||||
*
|
||||
* ⚠️ 标签是**每人一份**的(owner_id = 我),改它只影响我自己的名册 ——
|
||||
* 我给他打「球友」不该让全家的名册都多出这个词。
|
||||
*/
|
||||
case 'people/tags':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
$isAjax = (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest');
|
||||
if (!Auth::canWrite()) {
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json; charset=utf-8', true, 403);
|
||||
echo json_encode(['ok' => false, 'msg' => '你的身份是访客,不能改名册标签。'],
|
||||
JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
flash('err', '你的身份是访客,不能改名册标签。');
|
||||
redirect('people');
|
||||
}
|
||||
csrf_check();
|
||||
$pid = (int)($_POST['person_id'] ?? 0);
|
||||
$p = Person::find($pid);
|
||||
if (!$p || (int)$p['family_id'] !== $familyId) {
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json; charset=utf-8', true, 404);
|
||||
echo json_encode(['ok' => false, 'msg' => '找不到这个人。'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
flash('err', '找不到这个人。');
|
||||
redirect('people');
|
||||
}
|
||||
// ⚠️ 只允许给**在我名册里的人**打标签 ——
|
||||
// 否则会给一份"看不见的名册条目"写上标签,用户永远看不到、也删不掉。
|
||||
if (!isset(PersonRoster::visibleIds($familyId, $me)[$pid])) {
|
||||
$msg = '这个人不在你的名册里。';
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json; charset=utf-8', true, 200);
|
||||
echo json_encode(['ok' => false, 'msg' => $msg], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
flash('err', $msg);
|
||||
redirect('people');
|
||||
}
|
||||
$saved = PersonTag::setFor($familyId, $me, $pid, (string)($_POST['tags'] ?? ''));
|
||||
$msg = $saved
|
||||
? '标签已改成:' . implode('、', array_map(static fn($t): string => '#' . $t, $saved)) . '。'
|
||||
: '标签已清空。';
|
||||
if ($isAjax) {
|
||||
// ⚠️ 前端要用这个数组去改卡片上那颗按钮的文字(空数组 = 显示「标签」)
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'pid' => $pid,
|
||||
'tags' => $saved,
|
||||
'msg' => $msg,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
flash('ok', $msg);
|
||||
redirect('people');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 名册拖动排序 —— 整版提交。
|
||||
*
|
||||
* 🔴 v28:名册已**平铺**(用户第 5 条:分组→标签),所以这里只剩"排序"这一件事,
|
||||
* 不再有"换栏"。前端发上来的是一个不含 `g` 的栏(见 app.js 的 save),
|
||||
* 服务端 `applyLayout` 据此只更新 sort_order、不碰 group_id。
|
||||
*
|
||||
* 前端把有序 id 列表发过来,服务端一次写完。
|
||||
* 为什么不"移动一条算一次":拖拽的结果是一组**相对顺序**,
|
||||
* 逐条提交会在中途留下自相矛盾的中间态(两个人暂时同号),刷新一下就变样。
|
||||
* 没有 JS 也能用:卡片上的「调整分组」表单还在(只是没有拖拽那么顺手)。
|
||||
*/
|
||||
case 'people/reorder':
|
||||
Auth::requireLogin();
|
||||
@@ -3694,7 +3783,33 @@ switch ($r) {
|
||||
echo json_encode(['ok' => false, 'msg' => '账本不存在。'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$want = array_values(array_filter(array_map('strval', (array)($_GET['kinds'] ?? []))));
|
||||
/* 🔴🔴 v28 用户第 8 条:「财务,多选分类的时候,净资产 · 金额变化这个图表,
|
||||
里面的数字还是不改变。」
|
||||
根因:**前后端的参数形状对不上** ——
|
||||
· 前端(templates/ledger/index.php)发的是 `&kinds=deposit%2Chouse`
|
||||
(把选中集合拼成**一个逗号串**,因为 URLSearchParams 里一个 key 只能一个值);
|
||||
· 服务端这里写的是 `(array)($_GET['kinds'] ?? [])` —— 对字符串来说是
|
||||
`['deposit,house']`(**一个元素**,值是带逗号的整串),
|
||||
拿去和真实类别白名单求交 → 空 → 下面的 `if (!$kinds)` 分支把它当成"空集合"
|
||||
→ **回落成"整本账"** → 图与数字当然不变。
|
||||
⇒ 修法:把逗号串**也切开**(两种形状都收:`kinds=a,b` 与 `kinds[]=a&kinds[]=b`)。
|
||||
只收一种形状就会在下次换前端写法时再断一次 —— 收两种是一行代码的事。
|
||||
⚠️ 这与 `Ledger::timeline()` 的入参口径一致:它本来就同时接受字符串与数组。 */
|
||||
$_rawKinds = $_GET['kinds'] ?? [];
|
||||
if (!is_array($_rawKinds)) {
|
||||
$_rawKinds = [$_rawKinds];
|
||||
}
|
||||
$want = [];
|
||||
foreach ($_rawKinds as $_rk) {
|
||||
// 每个值再按逗号切一次 —— `a,b` 与 `a` 都能吃
|
||||
foreach (preg_split('/[,\s]+/', (string)$_rk) ?: [] as $_one) {
|
||||
$_one = trim($_one);
|
||||
if ($_one !== '') {
|
||||
$want[$_one] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$want = array_keys($want);
|
||||
// 白名单:只留下这本账里真实存在的类别
|
||||
$present = array_map('strval', array_keys((array)Ledger::summary(Ledger::entries($lid))['by_kind']));
|
||||
$kinds = array_values(array_intersect($want, $present));
|
||||
@@ -6324,6 +6439,71 @@ switch ($r) {
|
||||
redirect('notes');
|
||||
break;
|
||||
|
||||
/* 笔记底部附件(v28 用户第 12 条:「再加上附件上传功能,附件,都放在笔记最下面」)。
|
||||
⚠️ 只允许给**我自己的**笔记加附件:附件是挂在笔记上的东西,
|
||||
共享出去的笔记别人只能看,不该能往里塞文件。
|
||||
⚠️ 复用全站统一的 `attach_save_files`(它处理多文件、类型判定、失败清单),
|
||||
不自己遍历 $_FILES —— 各模块各写一遍必然有人漏掉"哪些字段是数组"。 */
|
||||
case 'notes/attach_add':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$n0 = Note::find($familyId, $id, $me);
|
||||
if (!$n0 || empty($n0['is_mine'])) {
|
||||
flash('err', '这条笔记不能改。');
|
||||
redirect('notes');
|
||||
}
|
||||
$warns = attach_save_files($familyId, Note::ATTACH, $id, 'file', $me);
|
||||
flash($warns ? 'err' : 'ok',
|
||||
$warns ? '有附件没有保存:' . implode(';', $warns) : '附件已上传。');
|
||||
redirect('notes/form', ['id' => $id]);
|
||||
break;
|
||||
|
||||
case 'notes/attach_del':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$aid = (int)($_POST['id'] ?? 0);
|
||||
// ⚠️ 先按附件 id 找到它挂在**哪条笔记**上,再判那条笔记是不是我的 ——
|
||||
// 只信传进来的附件 id,等于谁都能构造请求删别人家的附件(资源页踩过)。
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT target_id FROM attachments WHERE id = ? AND target_type = ?'
|
||||
);
|
||||
$st->execute([$aid, Note::ATTACH]);
|
||||
$nid = (int)$st->fetchColumn();
|
||||
$n0 = $nid > 0 ? Note::find($familyId, $nid, $me) : null;
|
||||
if (!$n0 || empty($n0['is_mine'])) {
|
||||
flash('err', '这个附件不能删。');
|
||||
redirect('notes');
|
||||
}
|
||||
\App\Attachment::remove($aid);
|
||||
flash('ok', '附件已删除。');
|
||||
redirect('notes/form', ['id' => $nid]);
|
||||
break;
|
||||
|
||||
/* 段落 Markdown 的**实时预览**(v28 用户第 12 条:"允许所见即所得编辑")。
|
||||
⚠️ 为什么预览走**服务端**而不是在前端再写一份渲染器:
|
||||
两套渲染器必然漂移(前端显示成粗体、保存后却不是),而用户看到的
|
||||
就是"预览",一旦不一致,他会以为保存坏了。
|
||||
服务端只有一份 `App\Markdown`,预览与最终渲染保证同源。
|
||||
⚠️ 所以这段必须够轻:只做一段文本的行内+块级渲染,不碰数据库、不查权限
|
||||
(不涉及任何数据,纯函数),前端再配合防抖调用。
|
||||
⚠️ 它仍然要求登录 + 写权限:不是安全所需(没有数据泄露),而是不给
|
||||
"未登录也能拿这个当 Markdown 渲染器用"的口子。 */
|
||||
case 'notes/preview':
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$src = (string)($_POST['md'] ?? '');
|
||||
// 限长:预览是交互用的,不该被拿来转整本书
|
||||
$src = mb_substr($src, 0, Note::MAX_TEXT, 'UTF-8');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok' => true, 'html' => Markdown::render($src)],
|
||||
JSON_UNESCAPED_UNICODE);
|
||||
break;
|
||||
|
||||
/* 图片块上传:走统一附件表(target_type='note')。
|
||||
⚠️ 为什么复用 attachments 而不在 notes 里另起一套:
|
||||
图片的"存哪、怎么压缩、怎么通过 media.php 出去"这套逻辑全站只有一份,
|
||||
|
||||
Reference in New Issue
Block a user