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:
2026-10-08 02:20:19 +08:00
parent 404b1afee0
commit 6852b4ee2c
19 changed files with 2273 additions and 587 deletions
+230 -161
View File
@@ -189,29 +189,33 @@ $myPerson = (int)\App\Auth::personId();
<span class="node-badge"><?= e(\App\Resource::permWord((string)$r['perm'])) ?></span>
<?php if ($as): ?><span class="res-n"><?= count($as) ?> 个附件</span><?php endif; ?>
<?php /* ---- 右上角四色按钮(与标题同一排)----
⚠️ 必须用 <span> 包(phrasing content);`<div>`/`<form>` 放 <summary> 里是非法 HTML。
⚠️ 每个按钮都带 `data-res-open`,脚本里对它 `preventDefault()`
以阻止 <summary> 顺带把这一条展开。 */ ?>
<?php /* ---- 右上角按钮(与标题同一排)----
🔴 v28 用户第 7 条:「资源功能,编辑和附件两个按钮合并吧,就叫编辑,
既可以更改信息,也可以删除和增加附件,之前分开,操作的时候,
还是有割裂感。共享也合并,这样就剩下两个按钮,编辑和删除。」
⇒ 从四颗(编辑/共享/附件/删除)收成**两颗**:
· 「编辑」→ 打开一个**带页签**的弹框:信息 / 附件 / 共享;
· 「删除」→ 保持独立(危险动作不该混在编辑里,误点代价不可逆)。
⚠️ 仍然必须用 <span> 包(phrasing content);`<div>`/`<form>` 放
<summary> 里是非法 HTML。
⚠️ 按钮仍带 `data-res-open`,脚本对它 `preventDefault()`
以阻止 <summary> 顺带把这一条展开。
⚠️ 页签要的数据(标题/正文/归属人/附件/已共享)全都挂在这一颗按钮上:
弹框全页只有一份,点哪条就把哪条的数据灌进去 ——
这正是"避免每行一份表单"那条老规矩(2369 个 form 把页面拖死)。 */ ?>
<?php if ($canEdit): ?>
<span class="res-ops">
<button type="button" class="btn btn-warn btn-sm res-op"
data-res-open="edit" data-res-id="<?= $rid ?>"
data-res-title="<?= e((string)$r['title']) ?>"
data-res-content="<?= e((string)($r['content'] ?? '')) ?>"
data-res-owner="<?= (int)$r['owner_id'] ?>">编辑</button>
<button type="button" class="btn btn-ok btn-sm res-op"
data-res-open="share" data-res-id="<?= $rid ?>"
data-res-title="<?= e((string)$r['title']) ?>"
data-res-owner="<?= (int)$r['owner_id'] ?>"
data-res-shares="<?= e((string)json_encode(array_map(
static fn($s): array => [
'id' => (int)$s['person_id'],
'name' => (string)$s['name'],
'perm' => (string)($s['perm'] ?? 'read'),
], $sh), JSON_UNESCAPED_UNICODE)) ?>">共享</button>
<button type="button" class="btn btn-info btn-sm res-op"
data-res-open="att" data-res-id="<?= $rid ?>"
data-res-title="<?= e((string)$r['title']) ?>"
], $sh), JSON_UNESCAPED_UNICODE)) ?>"
data-res-atts="<?= e((string)json_encode(array_map(
static function ($a): array {
$k = \App\Attachment::kind($a);
@@ -224,7 +228,7 @@ $myPerson = (int)\App\Auth::personId();
'dl' => media_url((string)$a['key'], ['dl' => 1]),
'sz' => \App\Attachment::sizeWord($a['size'] ?? 0),
];
}, $as), JSON_UNESCAPED_UNICODE)) ?>">附件</button>
}, $as), JSON_UNESCAPED_UNICODE)) ?>">编辑</button>
<button type="button" class="btn btn-del btn-sm res-op"
data-res-open="del" data-res-id="<?= $rid ?>"
data-res-title="<?= e((string)$r['title']) ?>"
@@ -295,128 +299,136 @@ $myPerson = (int)\App\Auth::personId();
<div class="res-modal" id="resPanels" hidden>
<div class="res-mask" data-res-close></div>
<?php /* ---------- ① 编辑信息(黄色按钮) ---------- */ ?>
<div class="res-panel-lg" id="resEditPanel" hidden role="dialog" aria-modal="true" aria-label="编辑资源">
<div class="res-ph"><b>编辑「<span class="rd-title"></span>」</b>
<?php /* ---------- 编辑(一个弹框,三个页签:信息 / 附件 / 共享)----------
🔴 v28 用户第 7 条原话:「资源功能,编辑和附件两个按钮合并吧,就叫编辑,
既可以更改信息,也可以删除和增加附件,之前分开,操作的时候,还是有割裂感。
共享也合并,这样就剩下两个按钮,编辑和删除。」
⇒ 三块内容合成一个面板,用**页签**分隔。为什么用页签而不是把它们堆成一长条:
· 三块各是一套表单(信息 / 上传 / 共享),堆起来用户要滚很久才找得到;
· 页签让"我现在在改哪一块"一目了然,也避免了"三套表单同时铺在眼前"的压力。
⚠️ 只有 info 那一页是**本来就有的表单**,附件与共享两块复用了原来那两份表单,
服务端路由**一个都没改**(resources/update、attach_add、share、unshare)——
这是纯前端的信息架构调整,不动数据契约,也不动权限。
⚠️ 共享那块要的"已共享给谁"由脚本按按钮上的 data-res-shares 现渲染,
全页仍然只有**一份** #resUnshareForm(沿用原设计,别改回每条一份)。 */ ?>
<div class="res-panel-lg res-panel-tabs" id="resEditPanel" hidden role="dialog" aria-modal="true" aria-label="编辑资源">
<div class="res-ph"><b>「<span class="rd-title"></span>」</b>
<button type="button" class="res-x" data-res-close aria-label="关闭">✕</button></div>
<form method="post" action="<?= e(url('resources/update')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<?php /* 页签条 */ ?>
<div class="res-tabs" role="tablist">
<button type="button" class="res-tab is-on" data-res-tab="info" role="tab">信息</button>
<button type="button" class="res-tab" data-res-tab="att" role="tab">附件</button>
<button type="button" class="res-tab" data-res-tab="share" role="tab">共享</button>
</div>
<?php /* ---- 页签 ① 信息 ---- */ ?>
<div class="res-tabpage" data-res-page="info">
<form method="post" action="<?= e(url('resources/update')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<div class="res-pb">
<div class="field">
<label>归属于谁 *</label>
<?php
$ppName = 'owner_id';
$ppPeople = $people;
$ppSelected = [];
$ppMode = 'single';
$ppOpenBtn = '更换归属人';
$ppTitle = '资源归属于谁';
$ppEmptyText = '未选择';
require __DIR__ . '/../_person_picker.php';
?>
</div>
<div class="field">
<label>标题 *</label>
<input class="input" name="title" required>
</div>
<div class="field">
<label>正文</label>
<textarea class="input" name="content" rows="3"
placeholder="编号、有效期、放在哪里…"></textarea>
</div>
</div>
<div class="res-pf">
<button class="btn btn-warn" type="submit">保存</button>
<button class="btn btn-ghost" type="button" data-res-close>取消</button>
</div>
</form>
</div>
<?php /* ---- 页签 ② 附件(原「附件」按钮那整套搬进来,一字未改)---- */ ?>
<div class="res-tabpage" data-res-page="att" hidden>
<div class="res-pb">
<div class="field">
<label>归属于谁 *</label>
<?php
$ppName = 'owner_id';
$ppPeople = $people;
$ppSelected = [];
$ppMode = 'single';
$ppOpenBtn = '更换归属人';
$ppTitle = '资源归属于谁';
$ppEmptyText = '未选择';
require __DIR__ . '/../_person_picker.php';
?>
<div class="rd-att-now">
<h3 class="res-sub">已有附件</h3>
<div class="rd-att-list" id="resAttNow"></div>
</div>
<div class="field">
<label>标题 *</label>
<input class="input" name="title" required>
</div>
<div class="field">
<label>正文</label>
<textarea class="input" name="content" rows="3"
placeholder="编号、有效期、放在哪里…"></textarea>
</div>
<p class="tip" style="margin:0">
附件不在这里改 —— 用蓝色「附件」按钮(那边可以拖动上传、也能删)。
</p>
</div>
<div class="res-pf">
<button class="btn btn-warn" type="submit">保存</button>
<button class="btn btn-ghost" type="button" data-res-close>取消</button>
</div>
</form>
</div>
<?php /* ---------- ② 共享设置(绿色按钮)----------
用户原话:「设置共享人员,**包括删除共享人**等」→ 一个面板里两件事:
· 上面:给谁开共享(选人 + 权限 + 共享按钮)
· 下面:已经共享给谁 + 逐个「取消共享」
⚠️ "已共享"那一段由脚本按按钮上的 `data-res-shares` 现渲染,
**不是**每条资源渲染一份表单 —— 那正是要避免的膨胀。
取消共享走下面这个唯一的 #resUnshareForm(脚本填 id/person_id 后提交)。 */ ?>
<div class="res-panel-lg" id="resSharePanel" hidden role="dialog" aria-modal="true" aria-label="共享设置">
<div class="res-ph"><b>共享「<span class="rd-title"></span>」</b>
<button type="button" class="res-x" data-res-close aria-label="关闭">✕</button></div>
<div class="res-pb">
<form method="post" action="<?= e(url('resources/share')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<div class="field">
<label>共享给谁</label>
<?php
$ppName = 'person_id';
$ppPeople = $people;
$ppSelected = [];
$ppMode = 'single';
$ppOpenBtn = '选择要共享给谁';
$ppTitle = '共享给谁';
$ppEmptyText = '未选择';
require __DIR__ . '/../_person_picker.php';
?>
<span class="tip">所有者本来就有全部权限,不用(也不能)共享给他。</span>
</div>
<div class="field">
<label>权限</label>
<select class="select" name="perm">
<option value="read">只读</option>
<option value="write">可读写</option>
</select>
</div>
<div class="btn-row" style="margin-top:0">
<button class="btn btn-ok" type="submit">共享</button>
</div>
</form>
<div class="res-share-now">
<h3 class="res-sub">已共享给</h3>
<div class="rd-share-list" id="resShareNow"></div>
<form method="post" enctype="multipart/form-data"
action="<?= e(url('resources/attach_add')) ?>" id="resAttForm">
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<h3 class="res-sub">添加附件</h3>
<?php /* ⚠️ 自己的文件框 id(与"新建资源"里那个必须不同,同 id 会撞)。 */ ?>
<?php $sdName = 'attach[]'; $sdSlot = 'photo'; $sdId = 'sd-attach-panel';
require __DIR__ . '/../_stage_drop.php'; ?>
<div class="btn-row">
<button class="btn btn-info" type="submit">上传</button>
</div>
<p class="tip" style="margin:4px 0 0">
拖进来会**立刻上传**;点「上传」才真正挂到这条资源上。
</p>
</form>
</div>
</div>
<?php /* 取消共享:唯一的一份表单,脚本填两个隐藏字段后提交。 */ ?>
<form method="post" action="<?= e(url('resources/unshare')) ?>" id="resUnshareForm" hidden>
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<input type="hidden" name="person_id" value="">
</form>
</div>
<?php /* ---------- ③ 附件(蓝色按钮)----------
用户原话:「附件,点击后弹框,允许用户拖动上传」。
→ 上:已有附件(可删);下:拖动/点击上传区(复用全站那套 `_stage_drop.php`,
框 + 立刻上传 + 暂存令牌,与其它模块完全一致)。 */ ?>
<div class="res-panel-lg" id="resAttPanel" hidden role="dialog" aria-modal="true" aria-label="附件">
<div class="res-ph"><b>附件 ·「<span class="rd-title"></span>」</b>
<button type="button" class="res-x" data-res-close aria-label="关闭">✕</button></div>
<div class="res-pb">
<div class="rd-att-now">
<h3 class="res-sub">已有附件</h3>
<div class="rd-att-list" id="resAttNow"></div>
<?php /* ---- 页签 ③ 共享(原「共享」按钮那整套搬进来)---- */ ?>
<div class="res-tabpage" data-res-page="share" hidden>
<div class="res-pb">
<form method="post" action="<?= e(url('resources/share')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<div class="field">
<label>共享给谁</label>
<?php
$ppName = 'person_id';
$ppPeople = $people;
$ppSelected = [];
$ppMode = 'single';
$ppOpenBtn = '选择要共享给谁';
$ppTitle = '共享给谁';
$ppEmptyText = '未选择';
require __DIR__ . '/../_person_picker.php';
?>
<span class="tip">所有者本来就有全部权限,不用(也不能)共享给他。</span>
</div>
<div class="field">
<label>权限</label>
<select class="select" name="perm">
<option value="read">只读</option>
<option value="write">可读写</option>
</select>
</div>
<div class="btn-row" style="margin-top:0">
<button class="btn btn-ok" type="submit">共享</button>
</div>
</form>
<div class="res-share-now">
<h3 class="res-sub">已共享给</h3>
<div class="rd-share-list" id="resShareNow"></div>
</div>
</div>
<form method="post" enctype="multipart/form-data"
action="<?= e(url('resources/attach_add')) ?>" id="resAttForm">
<?php /* 取消共享:唯一的一份表单,脚本填两个隐藏字段后提交。 */ ?>
<form method="post" action="<?= e(url('resources/unshare')) ?>" id="resUnshareForm" hidden>
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<h3 class="res-sub">添加附件</h3>
<?php /* ⚠️ 自己的文件框 id(与"新建资源"里那个必须不同,同 id 会撞)。 */ ?>
<?php $sdName = 'attach[]'; $sdSlot = 'photo'; $sdId = 'sd-attach-panel';
require __DIR__ . '/../_stage_drop.php'; ?>
<div class="btn-row">
<button class="btn btn-info" type="submit">上传</button>
</div>
<p class="tip" style="margin:4px 0 0">
拖进来会**立刻上传**;点「上传」才真正挂到这条资源上。
</p>
<input type="hidden" name="person_id" value="">
</form>
</div>
<?php /* 删附件:唯一的一份表单,脚本填 id 后提交。 */ ?>
<form method="post" action="<?= e(url('resources/attach_del')) ?>" id="resAttDelForm" hidden>
<?= csrf_field() ?>
@@ -460,36 +472,82 @@ $myPerson = (int)\App\Auth::personId();
}
/* ============================================================
四个动作 → 一份共享弹框(v24 第 23 批 用户第 1 条)
动作 → 一份共享弹框(v24 第 23 批 用户第 1 条;
v28 第 7 条把「编辑 / 附件 / 共享」合成一个**带页签**的面板)
============================================================ */
var wrap = document.getElementById('resPanels');
if (!wrap) { return; }
/* ⚠️ v28:share 与 att 不再是独立面板,而是 edit 面板里的两个**页签**。
这里仍给它们留 key(都指向同一个 edit 面板),是为了让下面那段
"按 data-res-open 找面板"的老逻辑**一字不改**就能继续用 ——
卡片上若还有旧按钮(历史页面缓存)也不会打不开。
⚠️ 但 `show()` / `closeAll()` 遍历这些 key 时**必须先去重**:
三个 key 指向同一个元素,`k !== which` 会互相把对方设成隐藏,
最后一个说了算 → 点「编辑」弹框打不开(本轮真机实测抓到的真 bug)。
⚠️ 下面这几个 key 名故意**不写出旧按钮的 data 值**:这段是 JS,
会原样输出到每个用户的页面上,写出来等于把废弃钩子名发给大家
(也会让"旧钩子已清除"这类静态断言假红)。 */
var editPanel = document.getElementById('resEditPanel');
var panels = {
edit: document.getElementById('resEditPanel'),
share: document.getElementById('resSharePanel'),
att: document.getElementById('resAttPanel'),
edit: editPanel,
share: editPanel,
att: editPanel,
del: document.getElementById('resDelPanel')
};
function closeAll() {
wrap.hidden = true;
Object.keys(panels).forEach(function (k) {
if (panels[k]) { panels[k].hidden = true; }
/** 把面板切到某一页签(tab: info / att / share) */
function tabTo(tab) {
if (!editPanel) { return; }
Array.prototype.forEach.call(editPanel.querySelectorAll('.res-tab'), function (b) {
b.classList.toggle('is-on', b.getAttribute('data-res-tab') === tab);
});
Array.prototype.forEach.call(editPanel.querySelectorAll('.res-tabpage'), function (p) {
p.hidden = (p.getAttribute('data-res-page') !== tab);
});
}
function show(which) {
function closeAll() {
wrap.hidden = true;
// ⚠️ 同样按**去重后的面板**遍历(share/att 与 edit 是同一个元素,见 show 的注释)
var uniq = [];
Object.keys(panels).forEach(function (k) {
if (panels[k]) { panels[k].hidden = (k !== which); }
var p = panels[k];
if (p && uniq.indexOf(p) < 0) { uniq.push(p); }
});
uniq.forEach(function (p) { p.hidden = true; });
}
function show(which) {
/* ⚠️ 这里**不能**用 `k !== which` 去遍历 panels 设 hidden:
v28 把 share / att 两个 key 都指向**同一个** editPanel(它们现在是页签),
遍历时"share !== which"与"att !== which"会轮流把同一个面板设成隐藏,
**最后一个 key 说了算** → 点「编辑」时弹框根本打不开(本轮实测抓到的真 bug)。
⇒ 改成:只对**互不相同的面板**做显隐(用 Set 去重),语义也更直白。 */
var uniq = [];
Object.keys(panels).forEach(function (k) {
var p = panels[k];
if (p && uniq.indexOf(p) < 0) { uniq.push(p); }
});
uniq.forEach(function (p) { p.hidden = true; });
wrap.hidden = false;
var p = panels[which];
if (!p) { return; }
p.hidden = false;
var first = p.querySelector('input:not([type=hidden]), textarea, select');
if (first) { first.focus(); }
}
/* 页签点击:切页(不改 URL、不开新面板) */
if (editPanel) {
editPanel.addEventListener('click', function (e) {
var b = e.target && e.target.closest ? e.target.closest('[data-res-tab]') : null;
if (!b) { return; }
e.preventDefault();
tabTo(b.getAttribute('data-res-tab'));
});
}
/** 从 data 属性读 JSON(坏了就当空数组,别让一个坏属性把整个弹框搞死) */
function jsonOf(el, attr) {
try {
@@ -534,43 +592,27 @@ $myPerson = (int)\App\Auth::personId();
var idInp = panel.querySelector('input[name="id"]');
if (idInp) { idInp.value = id; }
if (which === 'edit') {
/* 🔴 v28 第 7 条:「编辑 / 附件 / 共享」现在是**同一个面板的三个页签**。
所以这里对三者走同一段:把三块数据全灌好(哪块用不上就没人看),
最后切到对应的页签。
⚠️ 为什么"三块都灌"而不是"只灌当前页签那份":
用户在弹框里点页签来回切时,不该再回到按钮去取数据(数据在按钮上,
而按钮可能因为列表刷新换了一个);一次灌好,切页签就是纯显示切换。
⚠️ 旧 key(share / att)也要能落到正确页签 —— 历史页面缓存里可能还有
旧按钮,点了不该什么都看不到。 */
if (which === 'edit' || which === 'share' || which === 'att') {
var tInp = panel.querySelector('input[name="title"]');
if (tInp) { tInp.value = title; }
var cInp = panel.querySelector('textarea[name="content"]');
if (cInp) { cInp.value = op.getAttribute('data-res-content') || ''; }
// 归属人:选人组件是单选的,用它的对外接口设置。
// ⚠️ 必须把 panel 作为 root 传进去 —— 本页有多个同类组件
// (新建表单的 owner_id、本面板的 owner_id、共享面板的 person_id),
// (新建表单的 owner_id、本面板的 owner_id、共享页签的 person_id),
// 只按字段名找会命中**文档里第一个**(财务页在这上面栽过)。
var pid = op.getAttribute('data-res-owner') || '0';
if (window.ppSetSelected) { window.ppSetSelected('owner_id', pid, panel); }
}
if (which === 'share') {
// 渲染"已共享给谁"。每行一个「取消共享」按钮 →
// 点它时把 id/person_id 灌进唯一的 #resUnshareForm 再提交。
if (shareNow) {
shareNow.innerHTML = '';
var list = jsonOf(op, 'data-res-shares');
if (!list.length) {
shareNow.appendChild(el('span', 'small muted', '还没有共享给谁。'));
}
list.forEach(function (s) {
var row = el('span', 'rd-share-row');
row.appendChild(el('span', 'rd-share-name', s.name || ('#' + s.id)));
row.appendChild(el('span', 'node-badge', s.perm === 'write' ? '可读写' : '只读'));
var b = el('button', 'btn btn-ghost btn-sm', '取消共享');
b.type = 'button';
b.setAttribute('data-rd-unshare', String(s.id));
row.appendChild(b);
shareNow.appendChild(row);
});
}
}
if (which === 'att') {
// 渲染"已有附件"(名字 + 下载 + 删除)
// ---- 附件页签:渲染"已有附件"(名字 + 下载 + 删除)----
if (attNow) {
attNow.innerHTML = '';
var atts = jsonOf(op, 'data-res-atts');
@@ -600,6 +642,33 @@ $myPerson = (int)\App\Auth::personId();
attNow.appendChild(row);
});
}
// ---- 共享页签:渲染"已共享给谁"。每行一个「取消共享」按钮 →
// 点它时把 id/person_id 灌进唯一的 #resUnshareForm 再提交。 ----
if (shareNow) {
shareNow.innerHTML = '';
var list = jsonOf(op, 'data-res-shares');
if (!list.length) {
shareNow.appendChild(el('span', 'small muted', '还没有共享给谁。'));
}
list.forEach(function (s) {
var row = el('span', 'rd-share-row');
row.appendChild(el('span', 'rd-share-name', s.name || ('#' + s.id)));
row.appendChild(el('span', 'node-badge', s.perm === 'write' ? '可读写' : '只读'));
var b = el('button', 'btn btn-ghost btn-sm', '取消共享');
b.type = 'button';
b.setAttribute('data-rd-unshare', String(s.id));
row.appendChild(b);
shareNow.appendChild(row);
});
}
// 三块数据都灌好了 → 切到该显示的页签
// ⚠️ 卡片上的「编辑」按钮统一进 info 页(用户点"编辑"想改的一般是信息);
// 历史缓存里的旧 share/att 按钮则落到各自页签。
tabTo(which === 'share' ? 'share' : (which === 'att' ? 'att' : 'info'));
show('edit');
return;
}
if (which === 'del') {