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:
2026-10-07 15:31:26 +08:00
parent a5e559b2b3
commit c8839fa728
15 changed files with 964 additions and 93 deletions
+135 -4
View File
@@ -175,16 +175,34 @@ $sc = static fn($k, $d = '') => (string)($navSet[$k] ?? $d);
<?php if ($canWrite): ?>
<div class="card card-pad">
<h2 class="cinema-h2"><?= $editSite ? '编辑站点' : '新增站点' ?></h2>
<form method="post" action="<?= e(url('nav/save')) ?>">
<?php /* 🔴 v24 第 26 批 用户第 4 条:「输入 url 之后,如果其他用户添加了,
他可以直接复用」。做法是**输入框失焦时问一次**服务端,
把别人加过的同款站点列出来让用户挑一个 —— 挑中就带上 reuse_key 提交。
⚠️ 复用只在**新增**时给(编辑已有站点时再抄别人语义很乱):
所以这段只在 `!$editSite` 时输出,并带 `is_new=1` 让服务端也认这个前提。 */ ?>
<?php if (!$editSite): ?>
<div class="nav-reuse" id="navReuse" hidden>
<div class="nav-reuse-h">有人加过这个站点,可以直接复用它的图标与名称:</div>
<div class="nav-reuse-list" id="navReuseList"></div>
<div class="nav-reuse-f" id="navReuseFoot"></div>
</div>
<?php endif; ?>
<form method="post" action="<?= e(url('nav/save')) ?>" id="navSiteForm">
<?= csrf_field() ?>
<input type="hidden" name="act" value="site_save">
<?php if ($editSite): ?>
<input type="hidden" name="id" value="<?= (int)$editSite['id'] ?>">
<?php endif; ?>
<?php if (!$editSite): ?>
<?php /* 服务端凭这个标记判断"这是新建、可以复用"(见 index.php 的 site_save 段) */ ?>
<input type="hidden" name="is_new" value="1">
<?php endif; ?>
<?php /* 用户选定的可复用条目(点一下列表里的卡片就填进来) */ ?>
<input type="hidden" name="reuse_key" id="navReuseKey" value="">
<div class="form-grid">
<div class="field">
<label>网址 *</label>
<input class="input mono" name="url" required
<input class="input mono" name="url" id="navSiteUrl" required
value="<?= e((string)($editSite['url'] ?? '')) ?>" placeholder="https://example.com">
</div>
<div class="field">
@@ -200,12 +218,13 @@ $sc = static fn($k, $d = '') => (string)($navSet[$k] ?? $d);
</div>
<div class="field">
<label>显示名</label>
<input class="input" name="name" value="<?= e((string)($editSite['name'] ?? '')) ?>"
<input class="input" name="name" id="navSiteName" value="<?= e((string)($editSite['name'] ?? '')) ?>"
placeholder="留空会自动抓">
</div>
<div class="field">
<label>简称</label>
<input class="input" name="short_name" value="<?= e((string)($editSite['short_name'] ?? '')) ?>"
<input class="input" name="short_name" id="navSiteShort"
value="<?= e((string)($editSite['short_name'] ?? '')) ?>"
placeholder="页面上显示的字(优先于显示名)">
</div>
<div class="field full">
@@ -405,6 +424,24 @@ $sc = static fn($k, $d = '') => (string)($navSet[$k] ?? $d);
<div class="card card-pad">
<h2 class="cinema-h2">搜索引擎列表</h2>
<?php /* 🔴 v24 第 26 批 用户第 4 条:「可以内置几个,让用户选择」。
默认已经内置 6 个(必应/百度/360 启用,谷歌/搜狗/DuckGo 未启用 —— 后三个
在国内网络下不可用,见 nav_suggest.php 头部的线上实测)。
⚠️ 但**老用户**库里只有当年那几条(ensureSeed 只在"一条都没有"时才灌),
所以要给一个"补齐内置"的入口,否则"内置几个"对他们等于 0 个。
⚠️ 用独立表单(不是 data-nav-* 那种 AJAX 按钮):它是一次性动作、
执行完要整页刷新看结果,走普通 POST 最省事也最不容易出错。 */ ?>
<p class="small muted" style="margin-top:0">
内置了必应 / 百度 / 360 三档(国内可用)。谷歌 / 搜狗 / DuckGo 也列在下面,
但它们的联想或搜索在国内网络下不可用,所以默认<strong>未启用</b>。
展示页顶部那排标签,就是这里<strong>已启用</strong>的引擎。
</p>
<form method="post" action="<?= e(url('nav/save')) ?>" class="inline-form" style="margin:0 0 10px">
<?= csrf_field() ?>
<input type="hidden" name="act" value="engine_seed_add">
<button class="btn btn-sm" type="submit">补齐内置搜索引擎</button>
<span class="small muted">已有的不会被改动(你改过的地址、颜色都保留)。</span>
</form>
<ul class="nav-sortlist" data-sort-list data-reorder-url="<?= e(url('nav/save')) ?>">
<?php foreach ($engines as $en): ?>
<li class="nav-sortrow<?= empty($en['is_active']) ? ' is-off' : '' ?>" data-id="<?= (int)$en['id'] ?>">
@@ -541,5 +578,99 @@ $sc = static fn($k, $d = '') => (string)($navSet[$k] ?? $d);
.then(function () { window.location.reload(); });
}
});
/* ==================================================================
跨用户复用站点(v24 第 26 批 用户第 4 条)
用户原话:「输入 url 之后,如果其他用户添加了,他可以直接复用,
复用的时候,自定义图标和简称,他可以自己再设置,
设置好以后就固定了。」
交互设计(三拍,对应他这句话的三段):
① **输入 URL 之后** → 输入框失焦(或粘贴后 600ms)自动问一次服务端;
② 有人加过 → 列出可复用的卡片(图标 + 显示名),**点一下即选中**;
③ 选中后 → 把它的 名字/图标 展示出来,并把 reuse_key 填进隐藏域;
用户可以在上面的「显示名 / 简称」里改成自己喜欢的("自己再设置"),
提交时服务端会把图标**复制一份并标成自定义**("设置好以后就固定了")。
⚠️ 输入框再变 → 立刻清空已选(URL 换了,之前选的复用目标就不成立了)。
这一条最容易漏:不清的话用户改了网址、却还带着上一个站点的图标提交。
⚠️ 全程不阻塞:查询失败(离线/超时)就当"没人加过",不弹错、不挡提交。
================================================================== */
var urlIn = document.getElementById('navSiteUrl');
var reuse = document.getElementById('navReuse');
var reuseL = document.getElementById('navReuseList');
var reuseF = document.getElementById('navReuseFoot');
var reuseK = document.getElementById('navReuseKey');
var nameIn = document.getElementById('navSiteName');
var shrtIn = document.getElementById('navSiteShort');
if (urlIn && reuse && reuseL && reuseK) {
var lookupTimer = null;
var lastLookup = '';
function clearReuse() {
reuseK.value = '';
reuse.hidden = true;
reuseL.innerHTML = '';
if (reuseF) { reuseF.textContent = ''; }
}
function doLookup() {
var u = (urlIn.value || '').trim();
if (u === '' || !/^https?:\/\//i.test(u)) { clearReuse(); return; }
if (u === lastLookup) { return; } // 同一个地址不重复问
lastLookup = u;
post({ act: 'site_lookup', url: u }).then(function (d) {
if (!d || !d.items || !d.items.length) { clearReuse(); return; }
reuseL.innerHTML = '';
d.items.forEach(function (it) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'nav-reuse-item';
b.setAttribute('data-key', it.key);
b.setAttribute('data-name', it.name || '');
b.setAttribute('data-short', it.short_name || '');
var img = document.createElement('img');
img.className = 'nav-ico';
img.alt = '';
if (it.icon) { img.src = it.icon; }
var sp = document.createElement('span');
sp.className = 'nav-reuse-nm';
sp.textContent = it.label || '';
b.appendChild(img);
b.appendChild(sp);
b.addEventListener('click', function () {
Array.prototype.forEach.call(reuseL.querySelectorAll('.nav-reuse-item'),
function (x) { x.classList.remove('is-on'); });
b.classList.add('is-on');
reuseK.value = it.key;
/* "他可以自己再设置":把别人的名字/简称**预填**进输入框,
用户想改就直接改(不填也不会错 —— 服务端会用别人的补上)。
⚠️ 只填**空的**字段:用户已经手打过名字时不要覆盖他(那是抢用户的输入)。 */
if (nameIn && nameIn.value.trim() === '' && it.name) { nameIn.value = it.name; }
if (shrtIn && shrtIn.value.trim() === '' && it.short_name) { shrtIn.value = it.short_name; }
if (reuseF) {
reuseF.textContent = '已选「' + (it.label || '') + '」:'
+ '图标会复制一份到你名下并固定(以后「全部更新」不会改它)。'
+ '上面两项可自行修改。';
}
});
reuseL.appendChild(b);
});
reuse.hidden = false;
}).catch(function () { clearReuse(); });
}
// 失焦后问(不用 input 事件:边打边问会把半截域名也拿去查)
urlIn.addEventListener('blur', function () { setTimeout(doLookup, 120); });
// 粘贴之后通常不会失焦,补一拍
urlIn.addEventListener('paste', function () {
clearTimeout(lookupTimer);
lookupTimer = setTimeout(doLookup, 600);
});
// URL 一变 → 之前选的复用目标失效,必须清掉
urlIn.addEventListener('input', function () {
if (reuseK.value !== '') { clearReuse(); }
lastLookup = '';
});
}
})();
</script>