Files
FamilyBook/templates/notes/form.php
T
laoyang 4c63e42091 v61:笔记补齐成熟软件的块(Markdown 引擎加表格与任务清单,命令表扩全)
用户反馈:「笔记应该增加更多的块:段落、清单、图片、代码……去查成熟笔记
软件都分哪些块,我们都应该支持。」

对照 Notion / flomo / 思源 / Logseq 的块清单逐项核对(块类型仍只有
段落/图片两种——v29 用户拍板"清单并入 Markdown"),补齐的是
**Markdown 引擎与编辑器到 GFM 关键子集**:

app/Markdown.php
- GFM 表格:表头+分隔行两行联合识别(正文里有竖线不会误判成表格);
  `:---`/`---:`/`:-:` 左右中对齐;列数向表头看齐(多丢少补,格子不错位);
  单元格走 inline()(先 htmlspecialchars,`<script>` 只会被画成字面文本);
  段落收集遇到"紧跟正文的表格"会断开(GFM 不强制空行)
- GFM 任务清单:`- [ ]`/`- [x]` 渲染成**只读**勾选框(笔记不是任务系统,
  能点却不保存是假交互;要勾就改源码里的 x)
- 摘要清洗:表格分隔行/竖线/任务方括号不进列表卡片

assets/js/note_editor.js
- 命令表补齐:H1/H2/H3、有序列表、待办清单、删除线、行内代码、表格模板、
  分割线(工具条与斜线菜单仍是同一份表)
- 🔴 修真 no-op:v29 的"逐行套"写着逐行实际原样返回,选中三行点列表只给
  第一行挂前缀;现在行首记号型命令逐行套(空行跳过)
- 表格模板光标落在第一行数据的第一格,插完可直接打字

templates/notes/form.php:工具条 8→14 键(高频);全部命令斜线 `/` 可调
assets/css/app.css:.md-table / .md-task 样式(跟主题变量,不写死色)

刻意不做:代码语法高亮(引库=几 MB,用户当年已拍板轻量)、折叠块
(与"逐段编辑"打架,flomo 也没有)、页内子页/数据库(家书没有那个形态)。

验证:_fb_v61_ui.py 真机 30/0(含 XSS 夹具、紧跟正文断段、实时预览同源、
逐段编辑接口、v28 老语法零回退、历史 list 块不丢);
_b61_falsify.py 8 处注入 8/8 全红在预期断言;回归 b29 38/0、v55 30/0、v60 28/0。
2026-10-18 11:22:34 +08:00

455 lines
24 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
/**
* 个人笔记 · 一条笔记的两种面貌(v29 用户 2026-10-08 第 6/7/8/9 条)。
*
* 🔴 用户原话(第 8 条):
* 「笔记点进来,默认是阅读状态,图文混排,附件在最下方,图文混排时,
* 需要可以调整大小。点击编辑按钮,才进入编辑功能。
* 清单功能不要了吧,markdown 里面包含这个功能,把段落做的完善一点就可以了。」
* ⇒ 这个模板现在有**两个态**:
* · **阅读态**(`$editMode = false`):图文按顺序排下来、附件在最底下、
* 右上角一个「编辑」按钮。多数时候用户是来看的。
* · **编辑态**(`$editMode = true`):分块编辑器 + 工具条 + Markdown 补全。
*
* 🔴 用户原话(第 7 条):
* 「添加段落部分,还是不太好,这样就意味着用户还是要记住所有的 markdown 命令,
* 要么有工具条,要么有代码提示,例如插入一个斜线,然后输入代码,例如 code,
* 就能自动补全 code 的 markdown 代码。」
* ⇒ **两样都给**(用户原话就是"要么…要么…",那两样都给最省事):
* · 工具条:粗体 / 斜体 / 标题 / 列表 / 引用 / 代码 / 链接 —— 点一下插到光标处;
* · 斜线提示:行首打 `/` 弹出命令菜单,打 `code` 回车就补上代码块。
*
* ⚠️ 为什么"阅读态"要单独一份渲染(而不是复用编辑器的预览):
* 编辑器里的预览是"每块一个方框、带虚线边框",那是**编辑**的视觉语言;
* 阅读态要的是干净的排版(没有框、没有按钮)。两者混用会让阅读页
* 看着像"还没保存的草稿"。
* ⚠️ 图片"可以调整大小"(第 8 条):用 width 百分比存进图片块的内容里
* (`key|50`),渲染时按它出宽度。见下面 `$imgW` 的解析与注释。
*
* @param array|null $editing 笔记(阅读/编辑都以它为准);新建时 null
* @param bool $editMode true = 编辑态
* @param string $perm 'owner' / 'write' / 'read' / ''
* @param bool $canEdit 能不能改(作者或 write)
* @param array $shares 已共享给谁(作者视角才有内容)
* @param array $tagSuggest 我用过的标签(点一下就加)
*/
$editing = $editing ?? null;
$isEditing = $editing !== null;
$editMode = (bool)($editMode ?? ($isEditing ? false : true));
$canWrite = \App\Auth::canWrite();
$perm = (string)($perm ?? '');
$canEdit = (bool)($canEdit ?? false);
$shares = (array)($shares ?? []);
$isMine = $isEditing && (int)$editing['person_id'] === (int)\App\Auth::personId();
// 把编辑中的数据整理成前端要的形状(块数组 + 标签词)
$blocks = [];
$tagsStr = '';
if ($isEditing) {
foreach ((array)$editing['blocks'] as $b) {
// ⚠️ 历史 list 块在编辑态里**转成 Markdown 文本**再放进编辑器:
// 否则编辑器认不出这种块(v29 已把清单并入 Markdown),
// 会渲染成一个空的方块 —— 用户一保存内容就丢了。
$k = (string)$b['kind'];
$c = (string)$b['content'];
if ($k === 'list') {
$c = \App\Note::blockHtmlOut($c); // 见 Note::blockHtmlOut(返回 Markdown 文本)
$k = 'text';
}
$blocks[] = ['kind' => $k, 'content' => $c];
}
$tagsStr = implode(' ', array_map(static fn($t): string => '#' . $t, (array)$editing['tags']));
}
if (!$blocks) {
// 新建时给一个空段落块打底(否则页面上一片空白,用户不知道从哪写)
$blocks = [['kind' => 'text', 'content' => '']];
}
/** 图片块内容 → [key, 宽度百分比]。内容形如 `abc123` 或 `abc123|50`。 */
$imgParts = static function (string $c): array {
$p = explode('|', $c);
$w = isset($p[1]) ? (int)$p[1] : 0;
if ($w < 20 || $w > 100) { $w = 0; }
return [trim((string)$p[0]), $w];
};
?>
<div class="page-head">
<?php
/* 🔴 v52(用户 2026-10-15 第 2 条):「笔记这个功能模块,点进去一个笔记的详情之后,
上边的标题不是看笔记,而是**用户自己写的这个标题的名称**。」
⇒ 阅读态页头显示笔记自己的标题;没有标题时才退回「看笔记」。
⚠️ 只在阅读态这么做 —— 编辑态仍用「改笔记 / 写笔记」:
编辑时页面标题是"我在干什么",而笔记标题就在下面第一个输入框里,
两处都写同一个名字反而看着像重复。
⚠️ 用 `$noteHeadTitle`(HTTP 里也参与 <title>,见 View::render 的 title 参数)——
由路由传进来,见 index.php 的 notes/form 分支。 */
$noteTitle = trim((string)($editing['title'] ?? ''));
$headText = (!$editMode && $isEditing && $noteTitle !== '') ? $noteTitle
: ($editMode ? ($isEditing ? '改笔记' : '写笔记') : '看笔记');
?>
<h1><?= e($headText) ?></h1>
<span class="sub">
<?php if ($isEditing && !$editMode): ?>
<?= e(substr((string)$editing['updated_at'], 0, 16)) ?>
<?php if (!$isMine): ?>
· 作者 <?= e((string)($editing['author_name'] ?? '')) ?>
· 你的权限:<?= e(\App\Note::permWord($perm)) ?>
<?php endif; ?>
<?php endif; ?>
</span>
<div class="actions">
<?php /* 🔴 v29 第 8 条:阅读态的右上角就是那个「编辑」按钮 —— 用户明确说
"点击编辑按钮,才进入编辑功能"。 */ ?>
<?php if ($isEditing && !$editMode && $canEdit && $canWrite): ?>
<a class="btn btn-sm" href="<?= e(url('notes/form', ['id' => (int)$editing['id'], 'edit' => 1])) ?>">编辑</a>
<?php endif; ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('notes')) ?>">返回列表</a>
</div>
</div>
<?php if (!$isEditing && !$canWrite): ?>
<div class="card card-pad"><p class="empty-hint">只读账号不能写笔记。</p></div>
<?php elseif (!$editMode): ?>
<?php /* ==================== ✅ 阅读态(默认)====================
用户第 8 条要的"图文混排 + 附件在最下方",就在这一段。
⚠️ 图片块用 `<figure>` 包:宽度可调(第 8 条),且要与正文有间距。
⚠️ 附件(`$editing['files']`)**排在正文之后、页面最下方** ——
用户原话「附件在最下方」。 */ ?>
<article class="note-read card card-pad">
<?php /* 🔴 v55(用户 2026-10-17 第 1 条):「标题已经变成这个日笔记的题目了,
那下面那个里面就**不用再显示这个题目**了。」
⇒ 正文区里那个重复的 `<h1 class="note-read-title">` 删掉。
⚠️ 背景:v52 已经把**页头**改成笔记自己的标题(见本文件上面那段),
而正文这里**又写了一遍同一个标题** ⇒ 同一页同一个名字出现两次
(页头一行、正文开头一行),用户看到的就是这个重复。
⚠️ **页头那份保留**(那是"这一页叫什么",也参与 <title> 与浏览器标签);
删的只是正文里的副本。
⚠️ 编辑态不受影响(本段只在阅读态渲染)。 */ ?>
<?php if (!empty($editing['tags'])): ?>
<div class="note-tags note-read-tags">
<?php foreach ((array)$editing['tags'] as $t): ?>
<a class="note-tag" href="<?= e(url('notes', ['tag' => (string)$t])) ?>">#<?= e((string)$t) ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="note-read-body" data-note-id="<?= (int)($editing['id'] ?? 0) ?>">
<?php foreach ((array)$editing['blocks'] as $b): ?>
<?php
$k = (string)$b['kind']; $c = (string)$b['content'];
/* 🔴 v52(用户第 4 条):每一块包一层 `.note-read-item`,带**块主键** ——
详情页的"分段编辑"要靠它定位改哪一块(AJAX 按主键改一行)。
⚠️ 加 `data-block` 的**同时**保留原来的类名(`note-read-fig` / `note-read-block`)——
现有 e2e 与 CSS 都按它们断言,改类名会静默弄坏那些检查。 */
?>
<div class="note-read-item" data-block="<?= (int)($b['id'] ?? 0) ?>"
data-kind="<?= e($k) ?>" data-raw="<?= e($c) ?>">
<?php if ($k === 'image'): ?>
<?php [$key, $w] = $imgParts($c); ?>
<?php if ($key !== ''): ?>
<figure class="note-read-fig">
<a href="<?= e(media_url($key)) ?>" data-prev>
<img src="<?= e(media_url($key)) ?>" alt=""
<?= $w > 0 ? 'style="width:' . (int)$w . '%"' : '' ?>
loading="lazy" decoding="async">
</a>
</figure>
<?php endif; ?>
<?php else: ?>
<div class="note-read-block md-body"><?= (string)($b['html'] ?? '') ?></div>
<?php endif; ?>
<?php /* 鼠标划过才出现的「编辑」——只在能写且这一块真的有主键时给
(`$b['id']` 为 0 的块理论上不该有;给了也改不了,不如不给)。 */ ?>
<?php if ($canEdit && $canWrite && (int)($b['id'] ?? 0) > 0): ?>
<button type="button" class="note-item-edit"
data-item-edit="<?= (int)$b['id'] ?>" title="只改这一段">编辑</button>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (!$editing['blocks']): ?>
<p class="empty-hint">(这条笔记没有正文)</p>
<?php endif; ?>
</div>
<?php /* ---- 附件:永远在最后 ---- */ ?>
<?php if (!empty($editing['files'])): ?>
<section class="note-read-files">
<h2 class="note-read-h2">附件</h2>
<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>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
</article>
<?php /* ==================== 共享(v29 第 9 条:选人 + 权限,同资源)====================
🔴 只在**作者**视角显示:共享范围是作者的决定,拿到只读的人不该看到名单。
交互与资源页完全一致:选人 → 选权限 → 共享;已共享的逐个可「取消共享」。 */ ?>
<?php if ($isMine && $canWrite): ?>
<div class="card card-pad">
<h2 class="cinema-h2">共享
<span class="hint">默认只有自己看得到;共享出去给别人看或改</span></h2>
<form method="post" action="<?= e(url('notes/share')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
<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="note-share-now">
<h3 class="note-read-h2">已共享给</h3>
<?php if ($shares): ?>
<div class="note-files">
<?php foreach ($shares as $s): ?>
<div class="note-file">
<span class="note-file-ico"><?= e(\App\Note::permWord((string)$s['perm'])) ?></span>
<span class="note-file-nm"><?= e((string)$s['name']) ?></span>
<form method="post" action="<?= e(url('notes/unshare')) ?>" class="inline-form">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
<input type="hidden" name="person_id" value="<?= (int)$s['person_id'] ?>">
<button type="submit" class="note-file-del" title="取消共享">取消共享</button>
</form>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="empty-hint">还没有共享给谁。</p>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<?php /* ==================== ✏️ 编辑态 ==================== */ ?>
<form method="post" action="<?= e(url('notes/save')) ?>" id="noteForm" class="note-form">
<?php /* ⚠️ media 地址**不在表单上注入**:`media_url('')` 返回的是默认头像
(见 helpers.php 里 `$key === ''` 那一支),拿它当基址会拼出
`avatar-default.svgabc123` 这种地址。基址统一由 layout 的
`<body data-media-base="media.php?k=">` 提供,脚本读它就行。 */ ?>
<?= 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>
</div>
</div>
<?php /* 🔴 v29 第 7 条:**工具条**(点一下插到光标处)+ 斜线命令提示。
工具条按钮与斜线菜单是**同一份命令表**(见 note_editor.js 的 CMDS),
避免"按钮能插、斜线插不了"这种两套实现。
🔴 v61(用户 2026-10-18):按成熟软件的块清单补齐 ——
标题一/二/三、有序列表、待办清单、删除线、行内代码、表格、分割线。
工具条只摆**高频**的;全部命令(含标题三、行内代码)斜线 `/` 都能调出。 */ ?>
<div class="note-cmdbar" data-cmdbar>
<button type="button" class="cmd" data-cmd="b" title="粗体 **文字**"><b>B</b></button>
<button type="button" class="cmd" data-cmd="i" title="斜体 *文字*"><i>I</i></button>
<button type="button" class="cmd" data-cmd="s" title="删除线 ~~文字~~"><s>S</s></button>
<span class="cmd-sep"></span>
<button type="button" class="cmd" data-cmd="h1" title="标题一 # 文字">H1</button>
<button type="button" class="cmd" data-cmd="h2" title="标题二 ## 文字">H2</button>
<button type="button" class="cmd" data-cmd="h3" title="标题三 ### 文字(全部命令见斜线)">H3</button>
<span class="cmd-sep"></span>
<button type="button" class="cmd" data-cmd="ul" title="无序列表 - 项目">≡</button>
<button type="button" class="cmd" data-cmd="ol" title="有序列表 1. 项目">1.</button>
<button type="button" class="cmd" data-cmd="todo" title="待办清单 - [ ] 项目">☑</button>
<button type="button" class="cmd" data-cmd="quote" title="引用 > 文字">❝</button>
<span class="cmd-sep"></span>
<button type="button" class="cmd" data-cmd="code" title="代码块 ```">&#60;/&#62;</button>
<button type="button" class="cmd" data-cmd="table" title="表格 | 列 | 列 |">▦</button>
<button type="button" class="cmd" data-cmd="hr" title="分割线 ---">—</button>
<button type="button" class="cmd" data-cmd="link" title="链接 [文字](地址)">🔗</button>
<span class="cmd-sep"></span>
<span class="small muted">行首打 <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>
<?php /* ⚠️ v29:原来的「共享给全家」勾选框**删掉**了 ——
共享改成"保存后在阅读态选人 + 选权限"(与资源一致,见第 9 条)。 */ ?>
</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 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; ?>
<?php /* 🔴🔴 v47(用户 2026-10-13):「编辑一个笔记,点击编辑,然后这时候点保存
是没有任何反应的。」——真因就是**这里原来是一个嵌套 <form>**。
HTML 不允许 form 套 form:浏览器解析时会把**内层** `<form>` 标签丢掉,
但**保留**它后面的 `</form>`。于是那个 `</form>` 提前把外层的
`#noteForm`(整页的保存表单)关掉了 ——
结果「保存」按钮落入**没有任何表单**的区域:
· `button.form === null`;
· 点它既不提交、也不报错(**所以"没有任何反应"**)。
⚠️ 页面里其它字段看着都正常,只有"提交"这条链断了 ——
这类 bug 不打开真浏览器量 `btn.form` 是看不出来的。
改法:内层不再用 `<form>`,改成 `<div>`,把"上传所选文件"这个按钮
用 HTML5 的 `formaction` / `formenctype` **指向自己的端点**。
—— 这样仍然只有**一个**表单(保存表单),上传按钮却能提交到
`notes/attach_add` 且是 multipart。既修了 bug,又不牺牲上传。
⚠️ 隐藏域 `id` 与 `_csrf` 与保存表单同名同值(都是这条笔记的 id /
同一个令牌),合进一个表单不会冲突 —— 已在探针里断言过。 */ ?>
<div 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">
<?php /* ⚠️ formaction/formenctype 必须写在**按钮**上:表单主体是"保存",
只有这一个按钮走附件端点。 */ ?>
<button class="btn btn-ghost btn-sm" type="submit"
formaction="<?= e(url('notes/attach_add')) ?>"
formenctype="multipart/form-data">上传所选文件</button>
</div>
</div>
</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>
<?php /* 取消:编辑已有笔记时回它的阅读态;新建时回列表 */ ?>
<a class="btn btn-ghost"
href="<?= e($isEditing ? url('notes/form', ['id' => (int)$editing['id']]) : 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; ?>