<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>React on Vibe Coding 笔记</title><link>https://8-152-7-248.sslip.io/tags/react/</link><description>Recent content in React on Vibe Coding 笔记</description><generator>Hugo -- gohugo.io</generator><language>zh_CN</language><lastBuildDate>Sun, 01 Nov 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://8-152-7-248.sslip.io/tags/react/index.xml" rel="self" type="application/rss+xml"/><item><title>一个 3.4 MB 的 exe，做到了 Trello 10 美元档的事</title><link>https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/</link><pubDate>Sun, 01 Nov 2026 00:00:00 +0800</pubDate><guid>https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/</guid><description>&lt;img src="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/cover.png" alt="Featured image of post 一个 3.4 MB 的 exe，做到了 Trello 10 美元档的事" /&gt;&lt;p&gt;把一份 PRD 变成能排期的任务，手里的工具多半已经换过好几轮。Trello 的&lt;a class="link" href="https://trello.com/pricing" target="_blank" rel="noopener"&#10; &gt;定价页&lt;/a&gt;上，Custom Fields 和 Saved searches 排在 Standard 档，每人每月 5 美元；Calendar、Timeline、Table、Dashboard、Map 这些视图排在 Premium 档，10 美元。Notion 的&lt;a class="link" href="https://www.notion.com/pricing" target="_blank" rel="noopener"&#10; &gt;免费版&lt;/a&gt;对个人近乎不设限，代价是数据在别人服务器上。&lt;/p&gt;&#10;&lt;p&gt;我给自己写了个 Windows 桌面看板：MIT 许可，安装包 3.4 MB，不要账号，不联网，全部数据就是硬盘上一个 &lt;code&gt;.db&lt;/code&gt; 文件。最近两个版本刚把自定义字段和保存的筛选视图加上去——正是 Trello 要收 5 美元的那两项。&lt;/p&gt;&#10;&lt;p&gt;它叫潮线，仓库名 tideline。GitHub 描述栏就一句话：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&amp;ldquo;A local-first project management desktop app with Board, Table, Gantt and Calendar views, built with Tauri 2 + Rust + React.&amp;rdquo;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;本地优先的项目管理桌面应用，带看板、表格、甘特、日历四种视图，用 Tauri 2 + Rust + React 写的。&lt;/p&gt;&#10;&lt;h2 id="一份-prd怎么变成一棵任务树"&gt;一份 PRD，怎么变成一棵任务树&#10;&lt;/h2&gt;&lt;p&gt;需求文档粘进去，任务树自己就出来了，全程不需要 AI 读懂任何一个字。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="需求拆解" class="gallery-image" data-flex-basis="294px" data-flex-grow="122" height="880" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/01-requirement-split.png" srcset="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/01-requirement-split_hu_f6058d8c225b12f7.png 800w, https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/01-requirement-split.png 1080w" width="1080"&gt;&lt;/p&gt;&#10;&lt;p&gt;它靠的是 Markdown 自己写出来的结构。&lt;code&gt;#&lt;/code&gt; 到 &lt;code&gt;######&lt;/code&gt; 六个级别的标题就是六个层级，&lt;code&gt;1.&lt;/code&gt; &lt;code&gt;1.1.&lt;/code&gt; &lt;code&gt;1.1.1.&lt;/code&gt; 这样的编号按编号层级嵌套，&lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; 这种无序列表按缩进嵌套，剩下的普通段落归到离它最近的那个节点，原样保留成任务描述。代码块整块留着。&lt;/p&gt;&#10;&lt;p&gt;拆解深度能选 1 到 4 层。选浅了不要紧，超出深度的内容会折叠进任务描述里，不会丢掉。拆法有两种：一种是一个任务带着一串子任务，另一种是按章节拆成一堆平级任务。文档里只有一个一级标题的时候，那个标题会被当成文档标题上提一层，让下面的章节直接变成顶层任务。&lt;/p&gt;&#10;&lt;p&gt;目标看板里已经有同名任务时，解析结果会提前标出来，默认跳过，不会造出一堆重复条目。&lt;/p&gt;&#10;&lt;p&gt;这部分代码是 &lt;code&gt;src/lib/requirementParser.ts&lt;/code&gt; 里的一个纯函数，文件头第一句就写着：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;纯前端、纯函数：把 Markdown / 纯文本需求文档解析成层级树，不调用任何 AI 服务，也不触碰数据库。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;它的测试用例是这样的：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;按编号层级解析有序列表&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parseRequirementDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1. 用户登录&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1.1. 输入手机号&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1.2. 输入验证码&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;2. 退出登录&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;用户登录&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;退出登录&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1.1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1.2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;四行输入，两条顶层任务，编号和层级都保住了。&lt;/p&gt;&#10;&lt;p&gt;不调 AI 是有道理的。AI 拆需求是理解之后再翻译，它会给你补出文档里根本没有的条目，改一轮的时间比自己拆还长。解析 Markdown 是照着结构走，文档里没写的东西不会凭空长出来。&lt;/p&gt;&#10;&lt;h2 id="同一批任务五种看法"&gt;同一批任务，五种看法&#10;&lt;/h2&gt;&lt;p&gt;拆完的任务落到看板上，就是一条卡片：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="看板视图" class="gallery-image" data-flex-basis="354px" data-flex-grow="147" height="731" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/02-board.png" srcset="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/02-board_hu_45428df900149fd4.png 800w, https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/02-board.png 1080w" width="1080"&gt;&lt;/p&gt;&#10;&lt;p&gt;同一批数据还能换三种看法。表格视图按列表分组，任务和子任务是三层缩进，子任务能直接勾掉；甘特视图有周和日两档缩放、今日线、周末底色，任务条上带进度；日历是 6×7 的月历，按截止日期归格，拖到别的日期就改期。第五个入口是筛选器，关键词、完成状态、优先级、标签可以叠加着用。&lt;/p&gt;&#10;&lt;h2 id="甘特图上那条线是有类型的"&gt;甘特图上那条线，是有类型的&#10;&lt;/h2&gt;&lt;p&gt;&lt;img alt="甘特视图" class="gallery-image" data-flex-basis="321px" data-flex-grow="133" height="806" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/03-gantt.png" srcset="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/03-gantt_hu_8b37a67299c38061.png 800w, https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/03-gantt.png 1080w" width="1080"&gt;&lt;/p&gt;&#10;&lt;p&gt;任务之间的关系分了三种：前置是阻塞关系，前置没完成后面的就算被挡住，而且要查循环依赖；相关只表示这两件事有关系，不产生阻塞；重复是同一件事记了两遍。&lt;/p&gt;&#10;&lt;p&gt;三种关系画在图上不一样：前置是阻塞虚线箭头，相关是细点线，重复是点划线。关系还能跨看板建立，A 看板的交付物卡住 B 看板的任务，这种事真实存在。仪表盘上的「被阻塞」只认前置那一种。&lt;/p&gt;&#10;&lt;h2 id="自己定义字段把常用的看法存下来"&gt;自己定义字段，把常用的看法存下来&#10;&lt;/h2&gt;&lt;p&gt;看板可以定义自己的字段结构，按看板隔离，只有两种类型：&lt;/p&gt;&#10;&lt;p&gt;单选下拉和数值。改一个选项的名字不会让已有任务的值失效，因为选项有稳定的 id，改的只是显示名。删除字段的时候，它挂在任务上的取值会一起清掉，不留孤儿数据。字段名在同一个看板里不能重名，重了会明确提示。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="仪表盘" class="gallery-image" data-flex-basis="271px" data-flex-grow="112" height="956" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/04-dashboard.png" srcset="https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/04-dashboard_hu_b00ddb10c262f502.png 800w, https://8-152-7-248.sslip.io/p/tideline-local-first-kanban/images/04-dashboard.png 1080w" width="1080"&gt;&lt;/p&gt;&#10;&lt;p&gt;0.13.1 加的保存视图是另一个方向。你把常用的观察角度存下来，视图会连同筛选条件、排序方式和看板视图类型一起记住，切回来不会排序错乱。内置的「全部 / 我的任务 / 最近更新」三个视图不能改名也不能删除，免得用户把自己锁死，自己建的可以随时改名或者删掉。&lt;/p&gt;&#10;&lt;p&gt;筛选条里还多了一个「字段」入口，单选字段可以多选，数值字段可以填上下限区间，跟关键词、状态、优先级、标签叠着用。字段或者选项被删掉的时候，引用它们的视图会降级成忽略这个条件，而不是弹一个错误出来。&lt;/p&gt;&#10;&lt;h2 id="全部数据就一个文件"&gt;全部数据就一个文件&#10;&lt;/h2&gt;&lt;p&gt;后端是 Rust + rusqlite，数据库开在 WAL 模式，文件默认落在 &lt;code&gt;%APPDATA%\com.lemostic.tideline\tideline.db&lt;/code&gt;。数据目录和安装目录是分开的，升级、卸载都不会动你的库；备份就是把这个文件复制走，恢复就是复制回来：&lt;/p&gt;&#10;&lt;p&gt;安装包 3.4 MB，NSIS 简体中文向导，默认装到 &lt;code&gt;C:\Program Files\Tideline&lt;/code&gt;。前端 React 18 + TypeScript + Vite，前后端通过 77 个命令通信，MIT 许可。&lt;/p&gt;&#10;&lt;p&gt;整个应用对外只有一次网络请求，是检查 GitHub 上有没有新版本，未签名的安装包不会静默下载安装。&lt;/p&gt;&#10;&lt;p&gt;说缺点也得说，三条：&lt;/p&gt;&#10;&lt;p&gt;目前只在 Windows 上构建验证。看板和列表是硬删除，只有任务进回收站。提醒开关属于设备偏好，不跟着 JSON 备份走。&lt;/p&gt;&#10;&lt;p&gt;仓库是 10 月 29 日建起来的，到今天 65 次提交、46 个版本，还是个 0 star 的新项目。想试试的话在 &lt;a class="link" href="https://github.com/Lemostic/tideline" target="_blank" rel="noopener"&#10; &gt;github.com/Lemostic/tideline&lt;/a&gt;，Releases 页里有 Windows 安装包；想先看它怎么拆需求的，从 &lt;code&gt;src/lib/requirementParser.ts&lt;/code&gt; 开始读，那个文件只有几百行。&lt;/p&gt;&#10;</description></item></channel></rss>