v27 个人笔记模块:分块(段落/图片/清单)+ 单层多标签 + 默认私有可共享
参考 flomo(时间流卡片 + 标签即筛)、Obsidian(图文混排的直觉)、
思源(「块」的粒度),但刻意只取轻的那一层:
- 分块只做 text / image / list 三种,可增删、可上下移;不做块引用 / 块 ID 双链
- 标签单层、多对多,只存字符串(**不建标签表**)——改名/合并这类重型能力一律不做
- 不做富文本工具栏、不做 Markdown 预览、不做拖拽排序(只有 ↑↓ 按钮)
- 可见性默认**私有**,单条可勾「共享给全家」;共享出去只给别人看,不给别人改
落地:
- 新增 app/Note.php(块清洗、标签归一、可见性、认领草稿附件)
- 新增 3 张表 notes / note_blocks / note_tags;Schema VERSION 26→27
- 🔴 attachments.target_type 是 ENUM:加 'note' 必须同时改建表串 +
新增 widenAttachTargetEnum()(老库不做这一步,传笔记图片会被严格模式拒掉)
- 路由 5 条:notes / notes/form / notes/save / notes/destroy / notes/upload
- 模板 templates/notes/index.php(列表)+ form.php(分块编辑)
- JS assets/js/note_editor.js(按需加载,同 person_picker 那套)
- 编辑器状态放 DOM 而非 JS 数组:移动块时用户已输入的内容一字不丢
This commit is contained in:
+84
-2
@@ -97,6 +97,10 @@ final class Schema
|
||||
* 会**另建一张空表** —— 用户已配好的 CalDAV 口令看起来就"消失"了
|
||||
* (所有设备上的同步立刻 401)。而且必须先判旧表在不在:
|
||||
* 全新安装时没有旧表,直接 RENAME 会报错把整段升级打断。
|
||||
* 27 → **个人笔记(用户 2026-10-07)**:`notes`(卡片)+ `note_blocks`(卡内的块)
|
||||
* + `note_tags`(单层标签)。参考 flomo / Obsidian / 思源,但刻意**只取轻的**:
|
||||
* 块只做 text/image/list 三种、不做块引用;标签只存字符串、不建标签表;
|
||||
* 权限默认私有、单条可一键共享给全家。详见 App\Note 头部。
|
||||
*/
|
||||
/**
|
||||
* 结构版本(**人读**用:日志、设置页上显示"库结构是哪一版")。
|
||||
@@ -104,7 +108,7 @@ final class Schema
|
||||
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
|
||||
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
|
||||
*/
|
||||
public const VERSION = '26';
|
||||
public const VERSION = '27';
|
||||
|
||||
/**
|
||||
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
|
||||
@@ -448,7 +452,7 @@ SQL,
|
||||
CREATE TABLE IF NOT EXISTS `attachments` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`family_id` INT UNSIGNED NOT NULL,
|
||||
`target_type` ENUM('trip','trip_node','resource','task','task_comment') NOT NULL COMMENT '挂在谁身上',
|
||||
`target_type` ENUM('trip','trip_node','resource','task','task_comment','note') NOT NULL COMMENT '挂在谁身上',
|
||||
`target_id` INT UNSIGNED NOT NULL COMMENT '宿主的 id',
|
||||
`key` VARCHAR(255) NOT NULL COMMENT '文件(存储相对键)',
|
||||
`name` VARCHAR(200) DEFAULT NULL COMMENT '原始文件名(下载时回给它)',
|
||||
@@ -915,6 +919,57 @@ CREATE TABLE IF NOT EXISTS `nav_settings` (
|
||||
PRIMARY KEY (`person_id`, `k`),
|
||||
CONSTRAINT `fk_navset_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导航设置(每人一份)'
|
||||
SQL,
|
||||
// ---- 个人笔记(v27,用户 2026-10-07)----
|
||||
// 三张表都很薄。为什么这样切(详见 App\Note 头部):
|
||||
// · notes —— 卡片(标题 + 可见性 + 归属人)
|
||||
// · note_blocks—— 卡内的块(text/image/list + 排序)「分块」的落点
|
||||
// · note_tags —— 标签(单层,存字符串,**不建标签表**)
|
||||
// ⚠️ 外键一律 `int(10) unsigned` 与 persons.id / families.id 严格同型 ——
|
||||
// 类型不一致(比如写成 INT 而非 INT UNSIGNED)会 errno 150 建表失败,
|
||||
// 而报错信息只说"外键约束不对",很难一眼看出是类型问题(本仓踩过)。
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `notes` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`family_id` INT UNSIGNED NOT NULL,
|
||||
`person_id` INT UNSIGNED NOT NULL COMMENT '作者(笔记属于人,不属于家)',
|
||||
`title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '标题(可空,纯正文笔记也允许)',
|
||||
`is_shared` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '共享给全家?0=私有(默认) 1=全家可见',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '改过就刷新(列表按它倒序)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_note_owner` (`family_id`, `person_id`, `updated_at`),
|
||||
KEY `idx_note_shared` (`family_id`, `is_shared`, `updated_at`),
|
||||
CONSTRAINT `fk_note_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_note_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='个人笔记:一条 = 一张卡片(v27 用户 2026-10-07)'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `note_blocks` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`note_id` INT UNSIGNED NOT NULL,
|
||||
`kind` VARCHAR(16) NOT NULL DEFAULT 'text'
|
||||
COMMENT '块类型:text=段落 / image=图片 / list=清单',
|
||||
`content` TEXT NOT NULL COMMENT 'text/list=正文;image=附件 key(走 media.php)',
|
||||
`position` INT NOT NULL DEFAULT 10 COMMENT '卡内顺序(步长 10,留插入空间)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_nb_note` (`note_id`, `position`),
|
||||
CONSTRAINT `fk_nb_note` FOREIGN KEY (`note_id`) REFERENCES `notes` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='笔记的块:卡片内的一段/一张图/一个清单(v27「分块」)'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `note_tags` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`note_id` INT UNSIGNED NOT NULL,
|
||||
`tag` VARCHAR(20) NOT NULL COMMENT '单层标签词(不含 #;同一笔记内不重复)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_nt` (`note_id`, `tag`),
|
||||
KEY `idx_nt_tag` (`tag`),
|
||||
CONSTRAINT `fk_nt_note` FOREIGN KEY (`note_id`) REFERENCES `notes` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='笔记标签(单层、多对多;标签词本身不建表,见 App\\Note)'
|
||||
SQL,
|
||||
];
|
||||
}
|
||||
@@ -1331,6 +1386,11 @@ SQL);
|
||||
// 建表语句的 IF NOT EXISTS 对**已存在的表**不会更新枚举 —— 老库必须显式改,
|
||||
// 否则插入 kind='trip' 会被严格模式整条拒掉(v7 就踩过同一类坑)。
|
||||
self::widenEventKindEnum($db);
|
||||
// v27:attachments.target_type 要能挂笔记的图片('note')。
|
||||
// ⚠️ 与上面 widenEventKindEnum 同一类坑:建表语句里的 ENUM 对**已存在的表**
|
||||
// 不会更新 —— 老库必须显式 MODIFY,否则上传笔记图片时插入
|
||||
// target_type='note' 会被严格模式整条拒掉(表现为"图传不上去")。
|
||||
self::widenAttachTargetEnum($db);
|
||||
// 每人的两本默认日历(观影 / 旅行):新装的由 Calendar::seedDefaults 在
|
||||
// 建账号时建,老库在这里补(幂等:按 (owner_id, kind, name) 唯一键查过再插)。
|
||||
self::tryStep('补建每人默认的观影/旅行日历本', static function () use ($db) {
|
||||
@@ -2118,6 +2178,28 @@ SQL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* attachments.target_type 加 'note'(v27 个人笔记的图片块)。
|
||||
* ⚠️ 判据用 strpos 找 'note' 要小心:ENUM 串里 'trip_node'、'task_comment' 都
|
||||
* 不含 note,所以直接找 `'note'`(带引号)最稳,不必担心误判。
|
||||
*/
|
||||
private static function widenAttachTargetEnum(\PDO $db): void
|
||||
{
|
||||
$st = $db->prepare(
|
||||
"SELECT COLUMN_TYPE FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'attachments' AND COLUMN_NAME = 'target_type'"
|
||||
);
|
||||
$st->execute();
|
||||
$type = (string)$st->fetchColumn();
|
||||
if ($type !== '' && strpos($type, "'note'") === false) {
|
||||
$db->exec(
|
||||
"ALTER TABLE `attachments` MODIFY COLUMN `target_type`
|
||||
ENUM('trip','trip_node','resource','task','task_comment','note') NOT NULL
|
||||
COMMENT '挂在谁身上'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function hasIndex(\PDO $db, string $table, string $index): bool
|
||||
{
|
||||
$st = $db->prepare(
|
||||
|
||||
Reference in New Issue
Block a user