v26 第 26 批(5 条):财务多选按集合取图 / 首页全天置顶与红线定位 / 任务复选框去提示与看板颜色 / 导航搜索引擎切换与站点复用 / 详情页去复选框
- 财务:勾选多个标签时上面那张「总金额」图按**选中集合**统计(新增 ledger/total_chart 路由 + 单一容器 data-ck-total-host,按集合现取并缓存; Ledger::timeline 第二参数放宽为 string|array) - 首页日程:全天日程移出滚动容器固定置顶;进场把当前时刻红线居中 (scrollTop 计算,不用 scrollIntoView),触发条件不绑 $showNow - 任务:复选框去除 title 提示;看板可设颜色(Task::boardColor/updateBoard), 首页任务行用该颜色显示所属看板 - 导航:展示页补回 id=engine-tabs(切换搜索引擎无效的根因);补内置引擎; 新增站点输 URL 可复用他人已加过的站点(sharedByUrl/copyIconFile/adoptSite) - 任务详情页页头去掉复选框(已有「标记完成」按钮)
This commit is contained in:
@@ -3661,6 +3661,70 @@ switch ($r) {
|
||||
], 'ledger');
|
||||
break;
|
||||
|
||||
/* 🔴 v24 第 26 批 用户第 1 条:**多选类别时,上面那张「总金额」图要按选中集合统计**。
|
||||
*
|
||||
* 用户原话:「财务模块,多个标签勾选后,第一个图,总额那里,勾选一个会变化,
|
||||
* 勾选多个后,变成总数了。」
|
||||
*
|
||||
* 上一批(第 25 批)把标签改成多选时,上图只保留了「恰好选一类」才切图的分支:
|
||||
* 选 1 类 → 显示该类曲线;
|
||||
* 选 2 类以上 → `sole` 为空 → **回落成"整本账"**(这正是用户看到的"变成总数了")。
|
||||
*
|
||||
* 为什么不能在页面里把几条已渲染的曲线**加起来**:图是服务端画的 **SVG**,
|
||||
* 里面是一堆 `<path>` 与坐标 —— 前端没法把两条折线"相加"成一条。
|
||||
* (金额能加,但曲线形状加不出来:各自的时间刻度、各自的纵轴比例都不一样。)
|
||||
*
|
||||
* 所以做法是**按需现画**:前端把选中集合 POST 过来,这里用
|
||||
* `Ledger::timeline($lid, $kinds)`(它已支持集合,见其 @param)现算一条曲线,
|
||||
* 用与页面**同一个** `chart_line()` 画成 SVG 回给前端替换。
|
||||
*
|
||||
* ⚠️ 这里是 **GET**(只读、可缓存、可分享),所以不写 csrf ——
|
||||
* 它不改变任何状态。但仍要 **Auth::requireLogin()**:账本金额是家庭隐私。
|
||||
* ⚠️ 只认**本账本真的有的类别**(拿 summary 的白名单过滤),
|
||||
* 否则 `?kinds=xxx` 可以随便塞字符串(虽然只会算不出东西,但没必要留口子)。 */
|
||||
case 'ledger/total_chart':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
$lid = (int)($_GET['book'] ?? 0);
|
||||
$book = Ledger::find($familyId, $lid, $me);
|
||||
if (!$book) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok' => false, 'msg' => '账本不存在。'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$want = array_values(array_filter(array_map('strval', (array)($_GET['kinds'] ?? []))));
|
||||
// 白名单:只留下这本账里真实存在的类别
|
||||
$present = array_map('strval', array_keys((array)Ledger::summary(Ledger::entries($lid))['by_kind']));
|
||||
$kinds = array_values(array_intersect($want, $present));
|
||||
|
||||
if (!$kinds) {
|
||||
// 空集合 = 整本账(与前端 `picked.length === 0` 的语义一致)
|
||||
$tl = Ledger::timeline($lid);
|
||||
$label = '净资产';
|
||||
$tkind = 'asset';
|
||||
} else {
|
||||
$tl = Ledger::timeline($lid, $kinds);
|
||||
// 全选中的都是负债类 → 用绿线(与单类别那套配色一致)
|
||||
$allDebt = true;
|
||||
foreach ($kinds as $k) {
|
||||
if (!Ledger::isDebt($k)) { $allDebt = false; break; }
|
||||
}
|
||||
$label = count($kinds) === 1
|
||||
? Ledger::kindWord($kinds[0])
|
||||
: count($kinds) . ' 类合计';
|
||||
$tkind = $allDebt ? 'debt' : 'asset';
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: private, max-age=30');
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'svg' => $tl ? chart_line($tl, $label, $tkind, 900) : '',
|
||||
'label' => $label,
|
||||
'n' => count($tl),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
break;
|
||||
|
||||
case 'ledger/book_add':
|
||||
case 'ledger/book_rename':
|
||||
case 'ledger/book_del':
|
||||
@@ -4917,8 +4981,11 @@ switch ($r) {
|
||||
if ($nm === '') {
|
||||
flash('err', '看板名不能为空。');
|
||||
} else {
|
||||
Task::renameBoard($familyId, $id, $nm);
|
||||
flash('ok', '看板已改名。');
|
||||
// 🔴 v24 第 26 批 用户第 3 条:这一行表单现在同时提交**名字 + 颜色**。
|
||||
// 颜色合法性在 Task::updateBoard 里收口(空 = 清除自定义、回到按名派生)。
|
||||
// 这里不自己判断颜色格式 —— 两处各判一次必然走样。
|
||||
Task::updateBoard($familyId, $id, $nm, (string)($_POST['color'] ?? ''));
|
||||
flash('ok', '看板已保存(名字与颜色)。');
|
||||
}
|
||||
} elseif ($act === 'tasks/board_del') {
|
||||
Task::deleteBoard($familyId, $id);
|
||||
@@ -5113,8 +5180,18 @@ switch ($r) {
|
||||
exit;
|
||||
}
|
||||
flash('ok', $msg);
|
||||
redirect((string)($_POST['back'] ?? '') === 'show' ? 'tasks/show' : 'tasks',
|
||||
(string)($_POST['back'] ?? '') === 'show' ? ['id' => $id] : []);
|
||||
// 无 JS 时的兜底回跳。三处复选框各自带自己的来源:
|
||||
// show=任务详情 / home=首页 / tasks(默认,看板)。
|
||||
// ⚠️ v24 第 26 批 用户第 3 条顺带修:原来只认 'show',于是**首页**那颗
|
||||
// 在没 JS 的浏览器上提交完会跳到任务看板(人在首页,却被扔去了另一个模块)。
|
||||
// 首页现在传 back=home,这里认它。
|
||||
$back = (string)($_POST['back'] ?? '');
|
||||
if ($back === 'show') {
|
||||
redirect('tasks/show', ['id' => $id]);
|
||||
} elseif ($back === 'home') {
|
||||
redirect('home');
|
||||
}
|
||||
redirect('tasks');
|
||||
break;
|
||||
|
||||
case 'tasks/comment_add':
|
||||
@@ -5839,8 +5916,11 @@ switch ($r) {
|
||||
'navSet' => $settings,
|
||||
'phrase' => $phrase,
|
||||
// 展示页地址(短语为空时模板里会提示去设置)
|
||||
// ⚠️ 用 `Nav::abs()` 而不是自己拼 `base_url`(v24 第 26 批顺带统一):
|
||||
// `abs()` 以**当前请求的 host** 为准,所以从 IP / 测试域名进来时
|
||||
// 给出的地址也是那个入口的(写死 base_url 会把人指到另一个域上)。
|
||||
'showUrl' => $phrase !== ''
|
||||
? rtrim((string)fb_config('base_url', ''), '/') . '/nav/?q=' . rawurlencode($phrase)
|
||||
? Nav::abs('/nav/') . '?q=' . rawurlencode($phrase)
|
||||
: '',
|
||||
// 我自己的头像 —— 展示页右上角要用(用户原话「头像就变成用户自己的头像」)
|
||||
'meAvatar' => avatar_url(Person::find($me)['avatar'] ?? null),
|
||||
@@ -5918,8 +5998,42 @@ switch ($r) {
|
||||
if ($id <= 0) {
|
||||
$bad('站点地址和所属板块都不能空。');
|
||||
}
|
||||
/* 🔴 v24 第 26 批 用户第 4 条:**复用别人加过的站点**。
|
||||
*
|
||||
* 用户原话:「输入 url 之后,如果其他用户添加了,他可以直接复用,
|
||||
* 复用的时候,自定义图标和简称,他可以自己再设置,
|
||||
* 设置好以后就固定了。」
|
||||
*
|
||||
* 做法:表单里带一个 `reuse_key`(= 显示名|图标文件名)。
|
||||
* ⚠️ 这里**重新查一遍**别人那条(不信任前端回传的内容)——
|
||||
* 前端只被允许"指认是哪一条",绝不被允许"提供那条的内容"。
|
||||
* 否则任何人都能凭一个伪造的 key 把任意的 name/icon 写进自己库里
|
||||
* (比如塞一个 name 做 XSS 载荷 —— 展示页那份是 `e()` 转义过的,
|
||||
* 但"不要相信前端给的展示数据"这条规则不该有例外)。
|
||||
* ⚠️ 复用只发生在**新建**(`$editSite` 为空)时:编辑一条已有站点时
|
||||
* 又去抄别人,语义会很乱(是覆盖我刚改的简称吗?)。
|
||||
* 编辑时 `reuse_key` 一律忽略。 */
|
||||
$reuseKey = trim((string)($_POST['reuse_key'] ?? ''));
|
||||
$didReuse = false;
|
||||
if ($reuseKey !== '' && !empty($_POST['is_new'])) {
|
||||
foreach (Nav::sharedByUrl($me, $url, 20) as $cand) {
|
||||
$k = Nav::siteLabel($cand) . '|' . (string)($cand['icon'] ?? '');
|
||||
if ($k !== $reuseKey) {
|
||||
continue;
|
||||
}
|
||||
// 用户在前端填的 简称/显示名 优先("他可以自己再设置");
|
||||
// 没填的字段由 adoptSite 用别人的补上。
|
||||
$didReuse = Nav::adoptSite($me, $id, $cand,
|
||||
(string)($_POST['name'] ?? ''),
|
||||
(string)($_POST['short_name'] ?? ''));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 抓元信息(名称 + 图标):用户勾了"顺便抓取"才做(联网、慢)
|
||||
if (!empty($_POST['do_fetch'])) {
|
||||
// ⚠️ 复用过的那条**跳过抓取**:用户刚说"就用这个图标、以后固定"
|
||||
// (adoptSite 已把它标成 icon_custom=1),紧接着又按本站规则重抓一遍,
|
||||
// 等于当场把他的话推翻。(这也是"设置好以后就固定了"的一部分。)
|
||||
if (!$didReuse && !empty($_POST['do_fetch'])) {
|
||||
$meta = NavFetch::meta($url);
|
||||
$db = Database::get();
|
||||
$newName = trim((string)($meta['name'] ?? ''));
|
||||
@@ -5984,6 +6098,42 @@ switch ($r) {
|
||||
$ok('已切换启用状态。');
|
||||
break;
|
||||
|
||||
/* 🔴 v24 第 26 批 用户第 4 条:「可以内置几个,让用户选择」。
|
||||
老用户库里只有当年那几条引擎(`ensureSeed` 只在"一条都没有"时才灌),
|
||||
所以给他们一个**一键补齐内置引擎**的入口。
|
||||
⚠️ 只补缺的,已有的(含用户改过的)一律不动 —— 见 Nav::addMissingEngines 的说明。 */
|
||||
case 'engine_seed_add':
|
||||
$n = Nav::addMissingEngines($me);
|
||||
$ok($n > 0
|
||||
? ('已补上 ' . $n . ' 个内置搜索引擎。')
|
||||
: '内置搜索引擎都已经在列表里了,没有可补的。');
|
||||
break;
|
||||
|
||||
/* 🔴 v24 第 26 批 用户第 4 条(后半):新增站点时**按 URL 查可复用**。
|
||||
⚠️ 这是 AJAX 接口(前端在 URL 输入框失焦时问一次)。
|
||||
做成 POST + csrf 而不是 GET:它带的是"我现在想加哪个 URL",
|
||||
虽然是只读,但走同一个表单动作链更省事,且防了被当探测接口刷。
|
||||
⚠️ 回的是**展示信息**(名字/简称/图标 URL),**不含是谁加的**。
|
||||
隐私边界见 Nav::sharedByUrl() 的注释。 */
|
||||
case 'site_lookup':
|
||||
$u = (string)($_POST['url'] ?? '');
|
||||
$rows = Nav::sharedByUrl($me, $u);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[] = [
|
||||
'name' => (string)($r['name'] ?? ''),
|
||||
'short_name' => (string)($r['short_name'] ?? ''),
|
||||
'label' => Nav::siteLabel($r),
|
||||
'icon' => Nav::iconUrl((string)($r['icon'] ?? '')),
|
||||
// 复用时给前端一个"可点选"的标识(用显示名+图标即可,
|
||||
// ⚠️ 不能回 id —— 那是别人的行 id,暴露它没有必要)
|
||||
'key' => Nav::siteLabel($r) . '|' . (string)($r['icon'] ?? ''),
|
||||
];
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok' => true, 'items' => $out], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
case 'engine_reorder':
|
||||
if (!is_array($_POST['ids'] ?? null)) {
|
||||
$bad('排序数据格式不对(应是一组 id)。');
|
||||
|
||||
Reference in New Issue
Block a user