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
+301
View File
@@ -4571,3 +4571,304 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
/* 长英文/URL 不要把卡片撑出横向滚动(全站反复栽在这条上) */
.note-text, .note-list, .note-title { overflow-wrap: anywhere; }
}
/* ============================================================
v28 用户第 2 / 3 条:首页两块右上角的「更多日程 / 全部任务」按钮
要**用所属模块的主题色做背景**(原来一律 .btn-ghost 的白底灰字,
与"日程=橙、任务=砖红"的分区色完全看不出关系)。
做法:模板把模块色注入 `--hd-c`(见 home.php 的 style 属性),
这里只负责"拿这个色当底 + 白字 + 悬停加深"三件事 ——
不为日程/任务各写一套(那是两份迟早漂移的样式)。
⚠️ 用 color-mix 压暗悬停色而不是写死两个色:模块色在「个人设置」里可改,
写死就会出现"用户换了主题色、按钮悬停还是原来的颜色"。
============================================================ */
.hd-more {
background: var(--hd-c, var(--brand));
border-color: var(--hd-c, var(--brand));
color: #fff;
}
.hd-more:hover {
background: color-mix(in srgb, var(--hd-c, var(--brand)) 82%, #000);
border-color: color-mix(in srgb, var(--hd-c, var(--brand)) 82%, #000);
color: #fff;
}
/* 老浏览器不认 color-mix 时的兜底:至少不要变成透明/消失 */
@supports not (background: color-mix(in srgb, red 50%, blue)) {
.hd-more:hover { background: var(--hd-c, var(--brand)); filter: brightness(.88); }
}
/* ============================================================
v28 用户第 5 / 6 条:名册「分组」改「标签」+ 卡片按钮要有底色
============================================================ */
/* ---- 顶部标签筛选条(替代原来的分栏)---- */
.roster-tagbar {
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
margin: 0 0 12px; padding: 10px 12px;
border: 1px solid var(--line); border-radius: var(--radius-s);
background: var(--surface);
}
.roster-tagbar-k { font-size: 12.5px; color: var(--faint); margin-right: 2px; }
.roster-tagbar-clear { margin-left: auto; font-size: 12.5px; }
/* ---- 平铺列表:卡片单列铺开,不再有"栏"的分隔线 ----
⚠️ `.people-grid` 原来在多栏里是 grid;平铺后让它 2 列自适应(宽屏两列、
窄屏一列),既不像单列那样长得没边,也不至于把姓名行挤断。
用 auto-fill + minmax 而不是写死列数:名册人数从 3 到 1000+ 都用同一套。 */
.roster.is-flat .people-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 8px;
}
@media (max-width: 720px) {
.roster.is-flat .people-grid { grid-template-columns: 1fr; }
}
/* ---- 🔴 用户第 6 条:「名册,每个人后面的按钮,也是要有背景颜色的。」
原来三颗按钮全是 .btn-ghost(白底 + 极浅描边),在纸白卡片上几乎看不见边界。
给它们各自一个**语义底色**(编辑=品牌淡、标签=中性灰、删除=危险淡),
而不是统一刷成一种 —— 一眼能分清哪个是危险动作。 ---- */
.person-edit,
.person-tag-btn,
.person-more-btn {
background: var(--line-2);
border-color: var(--line);
color: var(--ink);
}
/* 编辑 = 品牌色淡底(主要动作用它) */
.person-edit {
background: var(--brand-soft, #FDF3EC);
border-color: color-mix(in srgb, var(--brand) 28%, transparent);
color: var(--brand-ink);
}
.person-edit:hover { background: color-mix(in srgb, var(--brand) 16%, #fff); }
/* 标签 = 中性灰底;有标签时稍深一点,一眼能看出"这个人是有标记的" */
.person-tag-btn { background: var(--line-2); color: var(--muted); }
.person-tag-btn:hover { background: var(--line); color: var(--ink); }
.person-tag-btn.is-none { color: var(--faint); }
/* ⋯ = 中性 */
.person-more-btn { background: var(--line-2); color: var(--muted); }
.person-more-btn:hover { background: var(--line); color: var(--brand); }
/* 删除已经是 .btn-danger(自带红底),不动 */
/* ---- 标签编辑浮层里的候选标签 ---- */
.pt-picks { display: flex; flex-wrap: wrap; gap: 6px; }
.pt-pick {
display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px;
background: #fff; font-size: 12.5px; color: var(--muted);
}
.pt-pick:hover { border-color: var(--brand-2); color: var(--brand-ink); }
/* 勾上时整颗变实心(不是只动那个小方框)—— 点一下的反馈要一眼看得见 */
.pt-pick:has(input:checked) {
border-color: var(--brand); background: var(--brand-soft, #FDF3EC);
color: var(--brand-ink);
}
.pt-pick input { accent-color: var(--brand); }
/* ============================================================
v28 用户第 7 条:资源的「编辑」弹框改成带页签(信息 / 附件 / 共享)
============================================================ */
.res-tabs {
display: flex; gap: 4px; padding: 0 16px 10px;
border-bottom: 1px solid var(--line); margin-bottom: 12px;
}
.res-tab {
padding: 5px 14px; border: 1px solid transparent; border-radius: var(--radius-s);
background: transparent; color: var(--muted); cursor: pointer; font-size: 13.5px;
}
.res-tab:hover { background: var(--line-2); color: var(--ink); }
/* 当前页签:品牌淡底 + 品牌字色(与全站分段控件同一套观感) */
.res-tab.is-on {
background: var(--brand-soft, #FDF3EC);
border-color: color-mix(in srgb, var(--brand) 30%, transparent);
color: var(--brand-ink); font-weight: 500;
}
/* ============================================================
v28 用户第 10 条:内置联想接口库(点一下填入表单)
============================================================ */
.sug-lib { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
.sug-lib-item {
display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px;
padding: 6px 12px; border: 1px solid var(--line); border-radius: var(--radius-s);
background: #fff; cursor: pointer; text-align: left;
}
.sug-lib-item b { font-size: 13px; font-weight: 500; color: var(--ink); }
.sug-lib-item span { font-size: 11.5px; color: var(--faint); }
.sug-lib-item:hover { border-color: var(--brand-2); }
/* 刚被点选的那一个:品牌底 + 品牌边,明确"我选了它" */
.sug-lib-item.is-picked {
border-color: var(--brand); background: var(--brand-soft, #FDF3EC);
}
.sug-lib-item.is-picked b { color: var(--brand-ink); }
/* ============================================================
v28 用户第 12 条:笔记 —— 富文本双模式 / 附件 / 多级清单 / 定尺寸卡片网格
============================================================ */
/* ---- 列表页:顶部条(搜索 + 标签),去掉左侧标签栏 ---- */
.note-bar { margin-bottom: 12px; }
.note-search { margin: 0 0 8px; }
.note-tagbar { margin-top: 2px; }
/* ---- 🔴 定尺寸卡片网格(用户:"一行几个块,但是块大小要确定")----
高度**固定**:内容装不下就靠摘要 + 溢出裁切,卡片之间永远整齐。
用 auto-fill + minmax 让"一行几个"随宽度自适应,而不是写死列数。 */
.note-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 12px;
}
.note-card {
display: flex; flex-direction: column;
/* 固定高度 = 用户要的"块大小确定"。240px 大约放得下:一行头 + 标题 + 3~4 行摘要 + 缩略图 */
height: 240px; overflow: hidden;
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 12px 14px;
box-shadow: var(--shadow);
}
.note-card:hover { border-color: var(--brand-2); }
.note-head { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.note-meta { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1 1 auto; }
.note-time { font-size: 11.5px; color: var(--faint); white-space: nowrap; }
.note-ops { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.note-ops .inline-form { display: inline; }
.note-op {
font-size: 12px; padding: 1px 7px; border-radius: 6px; cursor: pointer;
border: 1px solid var(--line); background: var(--line-2); color: var(--muted);
text-decoration: none; line-height: 1.6;
}
.note-op:hover { background: var(--line); color: var(--ink); text-decoration: none; }
.note-op-del:hover { border-color: var(--red); color: var(--red); background: #FDF2F0; }
/* 点卡片进编辑(整块可点,但不用 <a> 包全文 —— 里面还有表单与链接) */
.note-hit { display: block; flex: 1 1 auto; min-height: 0; overflow: hidden; }
.note-hit:hover { text-decoration: none; }
.note-title {
margin: 8px 0 5px; font-size: 14.5px; font-weight: 500; color: var(--ink);
/* 标题最多两行,超出省略 —— 固定高度下不能让它无限长 */
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.note-ex {
margin: 6px 0 0; font-size: 13px; line-height: 1.6; color: var(--muted);
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
}
.note-ex-empty { color: var(--faint); font-style: italic; }
/* 图片:等比缩放到卡片宽,放在摘要下方(用户:"图片缩放后,放在下面") */
.note-thumb { margin-top: 8px; flex: 0 0 auto; overflow: hidden; border-radius: var(--radius-s); }
.note-thumb img {
width: 100%; height: 88px; object-fit: cover; display: block;
border: 1px solid var(--line); border-radius: var(--radius-s);
}
.note-card .note-tags-inline { margin-top: auto; padding-top: 8px; flex: 0 0 auto; }
.note-card .note-tag { font-size: 11.5px; padding: 2px 7px; }
/* ---- 编辑页:模式提示条 ---- */
.note-modebar {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--line-2);
}
.note-modebar code {
font-size: 11.5px; background: var(--line-2); padding: 1px 5px;
border-radius: 4px; color: var(--ink);
}
.note-live { margin-left: auto; display: inline-flex; align-items: center; gap: 5px;
font-size: 12.5px; color: var(--muted); cursor: pointer; }
.note-live input { accent-color: var(--brand); }
/* ---- 段落的 Markdown 实时预览块 ---- */
.note-block-pv {
margin-top: 6px; padding: 0 2px; font-size: 14px; line-height: 1.7; color: var(--ink);
}
.note-block-pv:not(.is-on) { display: none; }
/* 预览区左边一条细线,与输入区在视觉上分开(不然分不清哪块是输入、哪块是结果) */
.note-block-pv.is-on {
border-left: 3px solid var(--brand-soft, #F3D9CB);
padding-left: 10px;
}
/* ---- Markdown 渲染出来的元素(列表页/预览共用同一套)---- */
.md-p { margin: 0 0 8px; }
.md-p:last-child { margin-bottom: 0; }
.md-h { margin: 10px 0 6px; font-weight: 500; color: var(--ink); }
h3.md-h { font-size: 16px; }
h4.md-h { font-size: 15px; }
h5.md-h, h6.md-h { font-size: 14px; }
.md-list { margin: 4px 0 8px; padding-left: 22px; }
.md-li { margin: 2px 0; }
/* 多级清单:子层用空心点,层级一眼可分(用户第 12 条"清单可以多级") */
.md-list .md-list { list-style-type: circle; margin: 2px 0 2px; }
.md-list .md-list .md-list { list-style-type: square; }
.md-quote {
margin: 6px 0; padding: 4px 12px; border-left: 3px solid var(--line);
color: var(--muted); background: var(--line-2); border-radius: 0 6px 6px 0;
}
.md-code {
background: #F6F4F0; border: 1px solid var(--line); border-radius: 6px;
padding: 8px 10px; overflow-x: auto; font-size: 12.5px; line-height: 1.6;
font-family: ui-monospace, Consolas, monospace; color: var(--ink);
}
.md-icode {
background: var(--line-2); padding: 1px 5px; border-radius: 4px;
font-size: 12.5px; font-family: ui-monospace, Consolas, monospace;
}
.md-link { color: var(--brand); text-decoration: underline; }
.md-hr { border: 0; border-top: 1px solid var(--line); margin: 10px 0; }
/* ---- 编辑页底部附件区 ---- */
.note-files { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.note-file {
display: flex; align-items: center; gap: 8px;
padding: 6px 10px; border: 1px solid var(--line); border-radius: var(--radius-s);
background: #FDFCFA;
}
.note-file-ico {
font-size: 11px; padding: 1px 6px; border-radius: 4px;
background: var(--line-2); color: var(--muted); flex: 0 0 auto;
}
.note-file-nm { flex: 1 1 auto; min-width: 0; font-size: 13.5px; color: var(--ink);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.note-file-del {
flex: 0 0 auto; border: 1px solid var(--line); background: #fff; border-radius: 6px;
color: var(--muted); cursor: pointer; padding: 2px 8px; font-size: 12px;
}
.note-file-del:hover { border-color: var(--red); color: var(--red); }
/* 拖放区:虚线框,与全站其它模块的上传区同一观感 */
.note-att-drop {
border: 1px dashed var(--line); border-radius: var(--radius-s);
padding: 14px; text-align: center; background: #FDFCFA;
}
.note-att-drop.is-over { border-color: var(--brand); background: var(--brand-soft, #FDF3EC); }
.note-att-drop input[type=file] { display: none; }
.note-att-tip { font-size: 13px; color: var(--muted); }
.note-att-tip label { color: var(--brand); cursor: pointer; text-decoration: underline; }
/* ---- 窄屏:网格收成一列(固定高度在小屏上会显得很挤)---- */
@media (max-width: 640px) {
.note-grid { grid-template-columns: 1fr; }
.note-card { height: auto; min-height: 150px; }
}
/* ---- v28 修:名册卡片在窄屏被"标签按钮"撑破(真机几何探针抓到)----
根因:卡片右侧的动作区是一条 flex,里面那颗「标签」按钮的文字是
**所有标签拼起来**(`#小学同学 #初中同学 …`),且 `white-space: nowrap` ——
标签一多它就无限变宽,把整行、整张卡、整张网格一起顶出视口
(实测 360px 下 scrollW=2090,越界元素就是那一串 <a>/<button>)。
⇒ 三件事一起做,缺一不可:
① 动作区**允许换行**(标签多了折到第二行,而不是往外顶);
② 标签按钮**限最大宽度 + 溢出截断**(再长的标签也不会撑破);
③ 卡片主体 `min-width: 0`(flex/grid 子项默认 min-width:auto,不肯收缩)。 */
.person-actions { flex-wrap: wrap; justify-content: flex-end; }
.person-tag-btn {
max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.person { min-width: 0; }
.person-main { min-width: 0; }
.person .pb { min-width: 0; }
/* 窄屏更严:动作区整体不参与撑宽,标签按钮再收一档 */
@media (max-width: 720px) {
.person-actions { max-width: 100%; }
.person-tag-btn { max-width: 110px; }
/* 姓名那一栏允许收缩并截断,否则长名字也会顶破 */
.pname { overflow-wrap: anywhere; }
}
+19 -10
View File
@@ -824,8 +824,16 @@
function each(list, fn) { Array.prototype.forEach.call(list, fn); }
/* 🔴 v28 用户第 5 条:名册从「分栏」改成**平铺**(一个人可挂多个标签,
分栏会让他在多栏里重复出现)。拖动因此只剩"排序"这一件事。
⇒ 把整个 `.roster` 当成**唯一一栏**返回:下游的 placeAt / save / refreshCounts
全都按"栏"来写,只要这里返回一个元素,它们不用改就能继续工作
(比重写三条逻辑安全得多,也不会漏掉其中一条)。
⚠️ 返回的是 `[roster]` 而不是 `.people-grid`:placeAt 里会 `block.querySelector('.people-grid')`,
所以仍要有一个带 `.people-grid` 的容器;模板里 roster 直接包含 .people-grid。 */
function blocks() {
return roster.querySelectorAll('.group-block');
var flat = roster.querySelector('.people-grid');
return flat ? [roster] : [];
}
/** 从事件目标往上找那张可拖的卡片(点在卡片里的链接/表单上时也算这张卡) */
@@ -848,19 +856,16 @@
* 留着一个没人调用的函数只会让下一个人以为还有第二条拖拽路径。
*/
/** 每栏人数与"空栏"标记跟着 DOM 走 —— 不然拖完之后标题上的「N 人」还是旧的 */
/** 每栏人数与"空栏"标记跟着 DOM 走 —— 不然拖完之后标题上的「N 人」还是旧的。
* ⚠️ v28:平铺之后只剩"一栏"(就是 .roster 本身),它**没有 .group-n 也没有
* data-group**,所以下面这些查询都会拿到 null —— 全部做了存在性判断,
* 并且**绝不能**再把 `b.hidden` 设成"没人就隐藏":
* 那会把整张名册(roster 自己)藏起来。平铺下没有"空栏"这个概念。 */
function refreshCounts() {
each(blocks(), function (b) {
var n = b.querySelectorAll('.people-grid .person').length;
var nEl = b.querySelector('.group-n .n');
if (nEl) { nEl.textContent = String(n); }
b.classList.toggle('is-empty', n === 0);
// 🔴 empty 的栏被 filter.js 标了 hidden(筛完没人剩就整栏收起)。
// 拖了一张卡进来之后必须**把 hidden 也清掉** —— 只去掉 is-empty 没用:
// 作者样式里 .group-block 没有写 display,于是浏览器默认的
// [hidden]{display:none} 照样生效,那一栏连同刚放进去的人一起看不见
// (症状就是"拖到后面的分组里,松手什么都没有")。
b.hidden = (n === 0);
});
}
@@ -887,7 +892,11 @@
var pid = parseInt(c.getAttribute('data-person'), 10);
if (pid > 0) { ids.push(pid); }
});
cols.push({ g: parseInt(b.getAttribute('data-group'), 10) || 0, ids: ids });
/* ⚠️ v28:平铺之后**不报 `g`** —— 见 PersonRoster::applyLayout 的注释:
报了组号(哪怕是 0)就会覆盖 group_id,拖一次就把所有人的旧分组清空,
而那是"旧分组→同名标签"迁移的数据源、也是 vCard 的读点。
不报 `g` = 只更新顺序,这正是"拖动只改先后"的新语义。 */
cols.push({ ids: ids });
});
var body = new URLSearchParams();
+108 -4
View File
@@ -73,15 +73,73 @@
ta.rows = kind === 'list' ? 4 : 3;
ta.value = content || '';
ta.placeholder = kind === 'list'
? '一行一项,回车换行'
: (kind === 'text' ? '写点什么…' : '');
// 清单块自动在每行前加「· 」的视觉提示(内容本身不加,保持纯文本)
// v28 第 12 条要求"清单可以多级":缩进即层级(两个空格一层)
? '一行一项;行首缩进两个空格 = 子项(可多级)'
: '写点什么…支持 Markdown:**粗体** *斜体* # 标题 > 引用 `代码`';
body.appendChild(ta);
/* 🔴 v28 第 12 条:段落支持 Markdown,且要**所见即所得**。
做法:在 textarea 下方跟一个预览块,输入时(防抖)请服务端渲染。
⚠️ 为什么预览请求服务端而不是前端自己渲染:两套渲染器必然漂移
(预览是粗体、保存后不是),而用户看到的就是预览。
清单块**不给**预览:清单是"一行一项"的结构化输入,预览意义不大,
反而会占掉一半高度(这里是"轻量"的取舍,不是漏了)。 */
/* 🔴 v28 第 12 条:段落与**清单**都支持 Markdown 渲染(清单的多级就靠它)。
段落:原文直接渲染。
清单:把每一行包成 `- 行内容` 再渲染 —— 行首的缩进原样保留,
于是"缩进两个空格 = 子项"这条规则自然生效(多级清单)。
这样做还有个好处:**渲染规则只有一份**(都在服务端 Markdown 里),
不必为清单再写一套层级解析(两套必然漂移)。 */
if (kind === 'text' || kind === 'list') {
var pv = document.createElement('div');
pv.className = 'note-block-pv';
pv.setAttribute('data-pv', '1');
body.appendChild(pv);
}
}
wrap.appendChild(body);
return wrap;
}
/* ---- Markdown 实时预览(服务端渲染,防抖 350ms)---- */
var pvTimer = 0;
function renderPreview(block) {
var pv = block.querySelector('[data-pv]');
var ta = block.querySelector('textarea');
if (!pv || !ta) { return; }
var box = document.getElementById('noteLivePreview');
if (box && !box.checked) { pv.innerHTML = ''; pv.classList.remove('is-on'); return; }
var md = ta.value;
if (md.trim() === '') { pv.innerHTML = ''; pv.classList.remove('is-on'); return; }
// 清单块:每行包成 `- xxx`(缩进原样),交给服务端同一套 Markdown 渲染 → 天然多级
var block = pv.closest ? pv.closest('.note-block') : null;
if (block && block.getAttribute('data-kind') === 'list') {
md = md.split(/\r?\n/).map(function (line) {
return line.trim() === '' ? '' : ('- ' + line.replace(/^(\s*)[-*+]\s+/, '$1'));
}).join('\n');
}
var form = document.getElementById('noteForm');
var tok = form ? form.querySelector('input[name="_csrf"]') : null;
var fd = new FormData();
fd.append('md', md);
if (tok) { fd.append('_csrf', tok.value); }
fetch('index.php?r=notes/preview', {
method: 'POST', body: fd, credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
}).then(function (r) { return r.json().catch(function () { return null; }); })
.then(function (d) {
if (!d || !d.ok) { return; }
// ⚠️ 响应回来时用户可能已经又改了:只在内容仍一致时才贴上去
// (否则会把"旧的渲染结果"盖在"新的输入"下面,看着像卡住了)
if (ta.value !== md) { return; }
pv.innerHTML = d.html;
pv.classList.add('is-on');
}).catch(function () { /* 预览失败不打扰用户,正文照旧可保存 */ });
}
function schedulePreview(block) {
window.clearTimeout(pvTimer);
pvTimer = window.setTimeout(function () { renderPreview(block); }, 350);
}
// ---------------------------------------------------------------------
// 序列化:从 DOM 读回块数组,写进隐藏域
// ---------------------------------------------------------------------
@@ -210,7 +268,48 @@
});
// textarea 输入时同步(这样即便用户直接点提交,内容也是最新的)
list.addEventListener('input', serialize);
// + v28:段落块顺手刷新 Markdown 实时预览
list.addEventListener('input', function (e) {
serialize();
var ta = e.target;
if (ta && ta.tagName === 'TEXTAREA') {
var block = ta.closest ? ta.closest('.note-block') : null;
if (block && block.getAttribute('data-kind') === 'text') {
schedulePreview(block);
}
}
});
/* 实时预览开关(用户第 12 条:"允许所见即所得编辑,也允许 markdown 编辑")——
关掉之后预览块收起,textarea 能占满整块(专心改 Markdown 源码时更清爽)。 */
var liveBox = document.getElementById('noteLivePreview');
if (liveBox) {
liveBox.addEventListener('change', function () {
Array.prototype.forEach.call(list.querySelectorAll('.note-block[data-kind="text"]'),
function (b) {
if (liveBox.checked) { renderPreview(b); }
else {
var pv = b.querySelector('[data-pv]');
if (pv) { pv.innerHTML = ''; pv.classList.remove('is-on'); }
}
});
});
}
/* ---------------- 底部附件:删除(v28 第 12 条)----------------
⚠️ 用全页唯一的那一份 #noteFileDelForm(与项目其它模块同一做法):
每个附件一份 form 会在附件多时把页面撑爆。 */
var fileDelForm = document.getElementById('noteFileDelForm');
if (fileDelForm) {
document.addEventListener('click', function (e) {
var b = e.target && e.target.closest ? e.target.closest('[data-note-file-del]') : null;
if (!b) { return; }
if (!confirm('删掉这个附件?')) { return; }
var inp = fileDelForm.querySelector('input[name="id"]');
if (inp) { inp.value = b.getAttribute('data-note-file-del'); }
fileDelForm.submit();
});
}
// 提交前再序列化一次(双保险)
form.addEventListener('submit', serialize);
@@ -226,6 +325,11 @@
initial.forEach(function (b) { addBlock(b.kind, b.content); });
serialize();
/* v28:编辑已有笔记时,让段落**一进来就看到渲染结果**(否则用户要动一下
键盘才第一次看到"所见即所得",会以为这功能没生效)。 */
Array.prototype.forEach.call(list.querySelectorAll('.note-block[data-kind="text"]'),
function (b) { renderPreview(b); });
// ---------------------------------------------------------------------
// 标签联想:点一下把 #标签 追加进输入框
// ---------------------------------------------------------------------