Files
FamilyBook/templates/notes/form.php
T
laoyang 6852b4ee2c 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**),
    实时预览走服务端(前后端只一份规则);底部附件上传;清单多级;
    列表改**定尺寸卡片网格**(溢出显示摘要、图片缩放下方);按用户要求**去掉标签侧栏**。
2026-10-08 02:20:19 +08:00

225 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 个人笔记 · 写 / 改(v27 用户 2026-10-07)。新建与编辑**同一个模板**。
*
* 「分块」的落点就在这一页:正文区由若干**块**组成,每块是
* 段落 / 图片 / 清单 三者之一,可增、可删、可上下移。
*
* 三条刻意的轻量取舍(写清楚,免得后人"顺手升级"成重型编辑器):
* ① **不做富文本工具栏**(粗体/斜体/颜色…)。本项目全站的文本都是"纯文本 +
* 换行",只有这里破例就会出现两套文本口径(导出、搜索、复制都会不一致)。
* 需要强调就写符号,需要排版就用清单块。
* ② **不做 Markdown 预览**。可见即所得:段落块就是段落,清单块就是清单 ——
* 用户不必再学"我打的东西会变成什么"。这也是"轻量"最实在的一层。
* ③ **不做拖拽排序**,只有 ↑ ↓ 两个按钮。鼠标拖拽要引入一整套 pointer 事件
* 与触摸兼容(本仓名册拖拽就为它写了上千行),而笔记的块通常只有几块,
* 两个按钮足够,代码少一个数量级。
*
* ⚠️ 提交形状:所有块打包成一个 JSON 放进隐藏域 `blocks`(见页尾脚本)。
* 为什么不是 `kind[]` / `content[]` 并行数组:服务端要按下标对齐,
* 一旦前端少发一个(比如空块被过滤)整个错位,而且很难在页面上看出来。
* JSON 一把梭,服务端 `json_decode` 之后逐块清洗,简单且不会错位。
* ⚠️ **无 JS 兜底**:`<noscript>` 里给一个普通 textarea(`body_plain`),
* 服务端在 `blocks` 为空时会退回读它(见 index.php 的 notes/save)。
*
* @param array|null $editing 编辑时的笔记(带 blocks/tags/is_shared);新建为 null
* @param array $tagSuggest 我用过的标签(点一下就加)
*/
$editing = $editing ?? null;
$isEditing = $editing !== null;
$canWrite = \App\Auth::canWrite();
// 把编辑中的数据整理成前端要的形状(块数组 + 标签词)
$blocks = [];
$tagsStr = '';
if ($isEditing) {
foreach ((array)$editing['blocks'] as $b) {
$blocks[] = ['kind' => (string)$b['kind'], 'content' => (string)$b['content']];
}
$tagsStr = implode(' ', array_map(static fn($t): string => '#' . $t, (array)$editing['tags']));
}
if (!$blocks) {
// 新建时给一个空段落块打底(否则页面上一片空白,用户不知道从哪写)
$blocks = [['kind' => 'text', 'content' => '']];
}
?>
<div class="page-head">
<h1><?= $isEditing ? '改笔记' : '写笔记' ?></h1>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('notes')) ?>">返回列表</a>
</div>
</div>
<?php if (!$canWrite): ?>
<div class="card card-pad"><p class="empty-hint">只读账号不能写笔记。</p></div>
<?php else: ?>
<form method="post" action="<?= e(url('notes/save')) ?>" id="noteForm" class="note-form">
<?= csrf_field() ?>
<?php if ($isEditing): ?>
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
<?php endif; ?>
<?php /* 块的真实载荷(由页尾脚本维护) */ ?>
<input type="hidden" name="blocks" id="noteBlocks" value="">
<div class="card card-pad">
<div class="field">
<label for="noteTitle">标题</label>
<input class="input" id="noteTitle" name="title" maxlength="200"
value="<?= e((string)($editing['title'] ?? '')) ?>"
placeholder="一句话概括(也可以不写,只写正文)" autocomplete="off">
</div>
</div>
<?php /* ---------------- 分块编辑区 ---------------- */ ?>
<div class="card card-pad">
<div class="note-block-head">
<h2 class="cinema-h2">正文</h2>
<div class="note-add">
<button class="btn btn-ghost btn-sm" type="button" data-add="text">+ 段落</button>
<button class="btn btn-ghost btn-sm" type="button" data-add="image">+ 图片</button>
<button class="btn btn-ghost btn-sm" type="button" data-add="list">+ 清单</button>
</div>
</div>
<?php /* 🔴 v28 用户第 12 条:「段落是需要支持富文本的,可以使用 markdown,
但是需要使用富文本的方式进行编辑,同时可以使用 markdown 源码观看编辑。
就是说,允许所见即所得编辑,也允许 markdown 编辑。」
⇒ 两件事:
① **每个段落块下方实时显示渲染结果**("所见即所得")——
写 Markdown 的同时就能看到它长什么样,不必切来切去;
② 一个**全局"源码 / 预览"开关**:想专心改 Markdown 源码时,
把实时预览关掉(长文档下预览会占很多地方,也会让人分心)。
⚠️ 为什么不做 contenteditable 那种"直接在渲染结果上编辑":
那要引入一套 HTML↔Markdown 的双向转换与清洗(正是"引大库"的那条路),
而且富文本内容再存回 Markdown 时几乎必然丢格式或引入噪音。
这里选择"**输入 Markdown + 实时预览**"—— 两个诉求都满足,
数据始终是干净的 Markdown 纯文本(可导出、可搜索、可版本对比)。 */ ?>
<div class="note-modebar">
<span class="small muted">段落支持 Markdown:<code>**粗体**</code>
<code>*斜体*</code> <code># 标题</code> <code>- 清单</code> <code>&gt; 引用</code>
<code>`代码`</code></span>
<label class="note-live">
<input type="checkbox" id="noteLivePreview" checked>
<span>实时预览</span>
</label>
</div>
<div id="noteBlockList" class="note-blocks"></div>
<?php /* 无 JS 兜底:脚本没跑时用户仍能写一条纯文字笔记 */ ?>
<noscript>
<div class="field" style="margin-top:10px">
<label for="notePlain">正文(纯文本)</label>
<textarea class="input" id="notePlain" name="body_plain" rows="8"
placeholder="没有启用脚本时,可以在这里写纯文字正文"><?php
echo e(implode("\n\n", array_map(
static fn($b): string => (string)$b['content'], $blocks)));
?></textarea>
</div>
</noscript>
<?php /* 图片上传用的隐藏 file 框(点"+ 图片"时程序化触发) */ ?>
<input type="file" id="noteImgInput" accept="image/*" hidden>
</div>
<?php /* ---------------- 标签 + 共享 ---------------- */ ?>
<div class="card card-pad">
<div class="field">
<label for="noteTags">标签</label>
<input class="input" id="noteTags" name="tags" maxlength="300"
value="<?= e($tagsStr) ?>"
placeholder="用空格分开,如:读书 灵感 待办(可写 # 也可不写)" autocomplete="off">
<?php if ($tagSuggest): ?>
<div class="note-tags note-tags-suggest">
<span class="small muted">用过的:</span>
<?php foreach ($tagSuggest as $t): ?>
<button type="button" class="note-tag" data-tag="<?= e((string)$t) ?>">
#<?= e((string)$t) ?>
</button>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<label class="note-share-toggle">
<input type="checkbox" name="is_shared" value="1"
<?= !empty($editing['is_shared']) ? 'checked' : '' ?>>
<span>共享给全家(默认只有自己看得到)</span>
</label>
</div>
<?php /* 🔴 v28 用户第 12 条:「再加上附件上传功能,附件,都放在笔记最下面就行了。」
⇒ 附件区放在**整张表单的最下面**(标签与共享之后),与用户说的一致。
⚠️ 附件要挂在**已存在的笔记**上(attachments.target_id 要真实 id),
所以新建时这一区是**禁用并说明**的 —— 保存之后进入编辑态就能传了。
不采用"草稿附件 + 保存时认领"(图片块那套):附件不像图片那样内嵌在正文里,
草稿态文件在用户放弃保存后会变成谁看不见的垃圾;宁可让他保存完再传。 */ ?>
<?php if ($isEditing): ?>
<div class="card card-pad" id="noteFilesCard">
<h2 class="cinema-h2">附件 <span class="hint">放最后,与正文分开</span></h2>
<?php /* 已有附件:可下载、可删 */ ?>
<?php if (!empty($editing['files'])): ?>
<div class="note-files">
<?php foreach ((array)$editing['files'] as $f): ?>
<?php $fk = \App\Attachment::kind($f); ?>
<div class="note-file">
<span class="note-file-ico"><?= e(\App\Attachment::kindWord($fk)) ?></span>
<a class="note-file-nm" href="<?= e(media_url((string)$f['key'], ['dl' => 1])) ?>">
<?= e((string)($f['name'] ?? '附件')) ?>
</a>
<span class="small muted"><?= e(\App\Attachment::sizeWord($f['size'] ?? 0)) ?></span>
<button type="button" class="note-file-del" data-note-file-del="<?= (int)$f['id'] ?>"
title="删掉这个附件">✕</button>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="empty-hint" id="noteFilesEmpty">还没有附件。</p>
<?php endif; ?>
<form method="post" enctype="multipart/form-data"
action="<?= e(url('notes/attach_add')) ?>" id="noteAttForm">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
<div class="note-att-drop" id="noteAttDrop">
<input type="file" name="file[]" id="noteAttInput" multiple
accept="image/*,.pdf,.txt,.md,.zip,.doc,.docx,.xls,.xlsx,.ppt,.pptx">
<span class="note-att-tip">把文件拖到这里,或<label for="noteAttInput">点这里选文件</label></span>
</div>
<div class="btn-row">
<button class="btn btn-ghost btn-sm" type="submit">上传所选文件</button>
</div>
</form>
</div>
<?php else: ?>
<div class="card card-pad">
<h2 class="cinema-h2">附件</h2>
<p class="empty-hint">先「记下来」,保存之后回到这一页就能传附件了。</p>
</div>
<?php endif; ?>
<div class="note-actions">
<button class="btn" type="submit"><?= $isEditing ? '保存' : '记下来' ?></button>
<a class="btn btn-ghost" href="<?= e(url('notes')) ?>">取消</a>
</div>
</form>
<?php if ($isEditing): ?>
<?php /* 删附件:全页唯一的一份表单,脚本填 id 后提交(沿用项目既有做法,
避免"每行一份 form"那种页面膨胀)。 */ ?>
<form method="post" action="<?= e(url('notes/attach_del')) ?>" id="noteFileDelForm" hidden>
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
</form>
<?php endif; ?>
<?php /* 初始块数据:JSON 塞进 <script type="application/json"> 而不是 data-* ——
属性里的引号/换行会被 HTML 实体化,读出来还得反转义一次;JSON 块原样读。 */ ?>
<script type="application/json" id="noteSeed"><?= json_encode(
$blocks,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
) ?></script>
<?php endif; ?>