Files
FamilyBook/templates/notes/index.php
T
laoyang 404b1afee0 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 数组:移动块时用户已输入的内容一字不丢
2026-10-07 18:22:55 +08:00

168 lines
7.2 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)。
*
* 形态参考 **flomo**:一条一张卡片、按时间倒序的流;顶部一个搜索框;
* 侧边一列标签云(点一下只看这个标签)。
* 「图文混排」在**卡片里就看得见**(图以缩略图排在文字之下),
* 不必点进去 —— 笔记是"回看"的东西,回看时应该一眼看到内容。
*
* 分块只在**编辑页**体现(卡内可加段/图/清单并上下移);列表页只做**只读渲染**,
* 这是刻意的取舍:列表页可编辑就得为每张卡片配一套 JS 状态,与"轻量级"冲突。
*
* @param array $rows 我能看到的笔记(带 blocks / tags / is_mine)
* @param array $tagCloud 标签 + 条数
* @param string $keyword 搜索词
* @param string $tag 当前筛的标签
* @param int $totalAll 一共几条
* @param bool $showAll 是否已展开全部
* @param int $limit 列表默认条数
*/
$canWrite = \App\Auth::canWrite();
$myPerson = (int)\App\Auth::personId();
?>
<div class="page-head">
<h1>笔记</h1>
<span class="sub">共 <?= (int)$totalAll ?> 条</span>
<div class="actions">
<?php if ($canWrite): ?>
<a class="btn" href="<?= e(url('notes/form')) ?>">写笔记</a>
<?php else: ?>
<span class="link-quiet">只读账号</span>
<?php endif; ?>
</div>
</div>
<div class="note-layout">
<?php /* ---------------- 左侧:标签云 ---------------- */ ?>
<aside class="note-side">
<div class="card card-pad">
<h2 class="cinema-h2">标签</h2>
<?php if (!$tagCloud): ?>
<p class="empty-hint">还没有标签。写笔记时加上 <code>#标签</code> 就会出现在这里。</p>
<?php else: ?>
<div class="note-tags">
<a class="note-tag<?= $tag === '' ? ' is-on' : '' ?>" href="<?= e(url('notes')) ?>">
全部 <b><?= (int)$totalAll ?></b>
</a>
<?php foreach ($tagCloud as $t): ?>
<a class="note-tag<?= $tag === (string)$t['tag'] ? ' is-on' : '' ?>"
href="<?= e(url('notes', ['tag' => (string)$t['tag']])) ?>">
#<?= e((string)$t['tag']) ?> <b><?= (int)$t['n'] ?></b>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</aside>
<?php /* ---------------- 右侧:卡片流 ---------------- */ ?>
<div class="note-main">
<div class="card card-pad note-search-card">
<form class="search" method="get" action="index.php">
<input type="hidden" name="r" value="notes">
<?php if ($tag !== ''): ?>
<input type="hidden" name="tag" value="<?= e($tag) ?>">
<?php endif; ?>
<label class="sr-only" for="noteKw">搜索笔记</label>
<input class="input" id="noteKw" name="keyword" value="<?= e($keyword) ?>"
placeholder="搜标题 / 正文 / 标签,如「读书」" autocomplete="off">
<button class="btn btn-ghost btn-sm" type="submit">搜索</button>
<?php if ($keyword !== '' || $tag !== ''): ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('notes')) ?>">清除</a>
<?php endif; ?>
</form>
</div>
<?php if (!$rows): ?>
<div class="card card-pad">
<p class="empty-hint">
<?php if ($keyword !== '' || $tag !== ''): ?>
没有匹配的笔记。
<?php elseif ($canWrite): ?>
还没有笔记。点右上角「写笔记」随手记一条吧。
<?php else: ?>
还没有笔记。
<?php endif; ?>
</p>
</div>
<?php else: ?>
<?php foreach ($rows as $n): ?>
<?php $mine = !empty($n['is_mine']); ?>
<article class="card card-pad note-card">
<header class="note-head">
<div class="note-meta">
<?php /* 头像 + 作者:共享笔记要一眼看出是谁写的 */ ?>
<img class="note-ava" src="<?= e(avatar_url($n['avatar'] ?? null)) ?>" alt="">
<span class="note-who"><?= e((string)$n['author_name']) ?></span>
<?php if (!empty($n['is_shared'])): ?>
<span class="note-share" title="全家可见">共享</span>
<?php else: ?>
<span class="note-private" title="只有自己可见">私有</span>
<?php endif; ?>
<?php /* 只显示到分钟、且把 T 换成空格后的形态(全站惯例:不引第三方时间库) */ ?>
<span class="note-time"><?= e(substr((string)$n['updated_at'], 0, 16)) ?></span>
</div>
<?php if ($mine && $canWrite): ?>
<div class="note-ops">
<a class="btn btn-ghost btn-sm" href="<?= e(url('notes/form', ['id' => (int)$n['id']])) ?>">编辑</a>
<form method="post" action="<?= e(url('notes/destroy')) ?>" class="inline-form"
onsubmit="return confirm('删掉这条笔记?');">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit">删除</button>
</form>
</div>
<?php endif; ?>
</header>
<?php if (trim((string)$n['title']) !== ''): ?>
<h3 class="note-title"><?= e((string)$n['title']) ?></h3>
<?php endif; ?>
<?php /* 块渲染:文字段 / 图片 / 清单 —— 「图文混排」就在这儿 */ ?>
<div class="note-body">
<?php foreach ((array)$n['blocks'] as $b): ?>
<?php $kind = (string)$b['kind']; $c = (string)$b['content']; ?>
<?php if ($kind === 'image'): ?>
<a class="note-img" href="<?= e(media_url($c)) ?>"
data-prev="image" data-prev-src="<?= e(media_url($c)) ?>">
<img src="<?= e(media_url($c)) ?>" alt="笔记图片" loading="lazy">
</a>
<?php elseif ($kind === 'list'): ?>
<ul class="note-list">
<?php foreach (preg_split('/\R/u', $c) ?: [] as $line): ?>
<?php $line = trim((string)$line); ?>
<?php if ($line !== ''): ?>
<li><?= e($line) ?></li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="note-text"><?= nl2br(e($c)) ?></p>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php if (!empty($n['tags'])): ?>
<div class="note-tags note-tags-inline">
<?php foreach ((array)$n['tags'] as $t): ?>
<a class="note-tag" href="<?= e(url('notes', ['tag' => (string)$t])) ?>">
#<?= e((string)$t) ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?php if (!$showAll && (int)$totalAll > count($rows)): ?>
<div class="note-more">
<a class="btn btn-ghost" href="<?= e(url('notes', ['all' => 1, 'keyword' => $keyword, 'tag' => $tag])) ?>">
显示全部 <?= (int)$totalAll ?> 条
</a>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>