diff --git a/Vue3插件库精选.md b/Vue3插件库精选.md new file mode 100644 index 0000000..3f5960b --- /dev/null +++ b/Vue3插件库精选.md @@ -0,0 +1,285 @@ +# Vue3 开发效率翻倍!2026 年必备 20+ 实用 npm 插件库精选 + +Original 尔嵘 尔嵘 + +--- + +在 Vue3 + Vite 生态下,优质的 npm 插件能大幅简化开发流程、解决常见痛点、提升项目性能与交互体验。本文整理了前端开发、Vue3 专属、工具类、UI 增强、网络请求、表单/表格、可视化、工程化八大场景的实用插件,全部支持 Vue3 + TypeScript,开箱即用,附核心用途 + 快速上手代码,新手也能直接用。 + +--- + +## 一、Vue3 核心生态插件(必装) + +### 1. Pinia - Vue3 官方状态管理库 + +替代 Vuex,Vue3 官方推荐的状态管理,语法简洁、支持 TS、无需 mutations,轻量化易上手。 + +``` +npm install pinia +``` + +核心价值:全局状态共享,告别繁琐的 Vuex 语法,支持模块化、热更新。 + +### 2. Vue Router 4 - Vue3 官方路由 + +Vue3 专属路由管理器,支持动态路由、路由守卫、懒加载。 + +``` +npm install vue-router@4 +``` + +核心价值:单页应用路由跳转,项目必备基础插件。 + +### 3. @vueuse/core - Vue3 组合式工具集 + +Vue3 最强工具库,封装了上百个实用的 Composition API 工具函数,无需重复造轮子。 + +``` +npm install @vueuse/core +``` + +常用功能:监听窗口大小、本地存储、防抖节流、鼠标跟踪、暗黑模式切换。 + +```html + +``` + +--- + +## 二、UI 组件库(高效搭建页面) + +### 4. Element Plus - Vue3 主流 UI 库 + +最成熟的 Vue3 中后台 UI 组件库,组件齐全、文档完善、支持国际化。 + +``` +npm install element-plus +``` + +适用场景:管理系统、企业级后台、常规业务页面。 + +### 5. Ant Design Vue 3.x - 阿里出品 UI 库 + +阿里开源的 Vue3 版本,设计规范,适合高端企业级项目。 + +``` +npm install ant-design-vue +``` + +### 6. Naive UI - Vue3 高颜值 UI 库 + +TS 原生支持,主题定制极强,组件动画流畅,适合追求美观的项目。 + +``` +npm install naive-ui +``` + +--- + +## 三、网络请求 & 数据处理 + +### 7. Axios - 前端请求神器 + +Vue 生态最常用的 HTTP 客户端,支持拦截器、取消请求、请求封装。 + +``` +npm install axios +``` + +核心价值:统一管理接口请求、封装请求/响应拦截器(token 校验、错误处理)。 + +### 8. qs - 参数序列化工具 + +配合 Axios 使用,解决表单提交、GET 请求参数格式化问题。 + +``` +npm install qs +``` + +```js +import qs from 'qs' +// qs.stringify / qs.parse +``` + +### 9. dayjs - 轻量级时间处理库 + +替代 Moment.js(体积更小,API 一致),格式化时间、计算时间差。 + +``` +npm install dayjs +``` + +```js +import dayjs from 'dayjs' +dayjs().format('YYYY-MM-DD HH:mm:ss') +``` + +--- + +## 四、表单 & 表格(业务开发神器) + +### 10. Vxe Table - Vue3 高性能表格 + +支持虚拟滚动、合并单元格、导出 Excel、树形表格,大数据表格必备。 + +``` +npm install vxe-table +``` + +### 11. Vue Use Form - 轻量级表单钩子 + +轻量化表单验证、状态管理,无 UI 依赖,搭配任何 UI 库都能用。 + +``` +npm install @vueuse/form +``` + +### 12. vee-validate - Vue3 表单验证库 + +强大的表单验证工具,支持自定义规则、错误提示,适合复杂表单场景。 + +``` +npm install vee-validate yup +``` + +--- + +## 五、交互 & 动画增强 + +### 13. animate.css - 开箱即用动画库 + +一行代码实现页面入场、交互动画,无需手写 CSS 动画。 + +``` +npm install animate.css +``` + +### 14. @vueuse/motion - Vue3 动效工具 + +基于 Composition API 的流畅动画,支持滚动动画、过渡效果。 + +``` +npm install @vueuse/motion +``` + +### 15. better-scroll - 移动端滚动优化 + +解决移动端滚动卡顿、下拉刷新、上拉加载问题,适配 Vue3。 + +``` +npm install better-scroll +``` + +--- + +## 六、可视化 & 图表 + +### 16. ECharts - 百度开源图表库 + +支持折线图、柱状图、地图、仪表盘等全场景图表,Vue3 完美适配。 + +``` +npm install echarts +``` + +### 17. vue-echarts - ECharts Vue3 封装 + +简化 ECharts 在 Vue3 中的使用,无需手动操作 DOM。 + +``` +npm install vue-echarts +``` + +--- + +## 七、工具类 & 实用函数 + +### 18. lodash-es - 模块化工具函数库 + +提供防抖、深拷贝、数组/对象处理等百种实用函数,ES 模块版体积更小。 + +``` +npm install lodash-es +``` + +```js +import { debounce, cloneDeep } from 'lodash-es' +``` + +### 19. js-cookie - Cookie 操作工具 + +简化浏览器 Cookie 的增删改查,适配登录态存储。 + +``` +npm install js-cookie +``` + +### 20. nprogress - 页面加载进度条 + +路由切换、请求加载时显示顶部进度条,提升用户体验。 + +``` +npm install nprogress +``` + +--- + +## 八、工程化 & 开发效率 + +### 21. unplugin-auto-import - 自动导入 API + +Vue3、Pinia、VueUse 等 API 无需手动 import,自动导入,告别重复代码。 + +``` +npm install unplugin-auto-import -D +``` + +### 22. unplugin-vue-components - 组件自动导入 + +UI 库组件、自定义组件无需手动引入,直接使用。 + +``` +npm install unplugin-vue-components -D +``` + +### 23. vite-plugin-compression - 打包压缩 + +Vite 项目打包时自动生成 gzip/brotli 压缩包,提升页面加载速度。 + +``` +npm install vite-plugin-compression -D +``` + +--- + +## 九、插件使用建议 + +1. **基础必备**:Pinia + Vue Router 4 + Axios + @vueuse/core + dayjs + +2. **UI 选型**:中后台选 Element Plus,移动端选 Vant 4,高颜值选 Naive UI + +3. **性能优化**:vite-plugin-compression + 虚拟滚动表格(Vxe Table) + +4. **开发提效**:自动导入插件(unplugin 系列)+ lodash-es + +--- + +## 总结 + +这篇插件库覆盖了 Vue3 开发从基础搭建到业务开发、性能优化的全流程,所有插件均稳定支持 Vue3 + TypeScript,无兼容坑点。 + +- 新手优先安装核心生态 + 工具类插件,快速上手开发; +- 中大型项目可补充表单表格、可视化、工程化插件,提升团队效率; +- 所有插件均提供官方安装命令,直接复制即可使用。 diff --git a/part6/Cargo.lock b/part6/Cargo.lock new file mode 100644 index 0000000..d29d25e --- /dev/null +++ b/part6/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "part6" +version = "0.1.0" diff --git a/part6/Cargo.toml b/part6/Cargo.toml new file mode 100644 index 0000000..1eeeb32 --- /dev/null +++ b/part6/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "part6" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/part6/src/main.rs b/part6/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/part6/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/part6/包和模块练习题.md b/part6/包和模块练习题.md new file mode 100644 index 0000000..24c6f9a --- /dev/null +++ b/part6/包和模块练习题.md @@ -0,0 +1,1202 @@ +# Rust Package、Crate、Module 练习题 + +> 建议先手动写出每道题的答案,再运行代码验证。部分题目涉及多文件组织,请实际创建对应目录和文件来练习。 + +## 目录 + +- [一、概念辨析题](#一概念辨析题) +- [二、填空题:补充代码](#二填空题补充代码) +- [三、找出并修复错误](#三找出并修复错误) +- [四、实战题:组织代码](#四实战题组织代码) +- [五、综合思考题](#五综合思考题) +- [参考答案](#参考答案) + +--- + +## 一、概念辨析题 + +判断以下说法是否正确,并简要说明原因。 + +### 题目 1-1 + +> 一个 Package 只能包含一个 Crate。 + +### 题目 1-2 + +> `main.rs` 和 `lib.rs` 分别对应二进制 Crate 和库 Crate 的根文件。 + +### 题目 1-3 + +> 在模块树中,父模块默认可以访问子模块中的私有项。 + +### 题目 1-4 + +> 使用 `use` 关键字将模块或项引入作用域后,其子模块也会自动被引入。 + +### 题目 1-5 + +> `cargo new my_project` 默认会创建一个二进制 Crate。 + +### 题目 1-6 + +> 同一个文件中可以定义多个 `mod` 模块。 + +### 题目 1-7 + +> `pub use` 的作用完全等同于 `use`,只是书写风格不同。 + +### 题目 1-8 + +> 在一个 Package 中,`src/bin/` 目录下的每个 `.rs` 文件都会被编译为一个独立的二进制 Crate。 + +--- + +## 二、填空题:补充代码 + +补全下列代码使其能通过编译并达到期望输出。 + +### 题目 2-1:定义模块 + +```rust +// 填空:定义一个名为 network 的模块,内部包含一个名为 connect 的函数 +________ network { + fn connect() { + println!("已连接"); + } +} + +fn main() { + // 填空:调用 network 模块中的 connect 函数 + network::________(); +} +// 期望输出:已连接 +``` + +### 题目 2-2:pub 可见性 + +```rust +mod garden { + // 填空:使 plant 函数能够被模块外部访问 + ________ fn plant() -> &'static str { + "玫瑰花" + } + + fn water() { + println!("浇水..."); + } +} + +fn main() { + let flower = garden::plant(); + println!("种了{}", flower); + + // 下面这行取消注释会怎样? + // garden::water(); +} +// 期望输出:种了玫瑰花 +``` + +### 题目 2-3:use 关键字 + +```rust +mod front_of_house { + pub mod hosting { + pub fn add_to_waitlist() { + println!("已加入等候列表"); + } + + pub fn seat_at_table() { + println!("已入座"); + } + } +} + +fn main() { + // 填空:使用 use 将 hosting 模块引入当前作用域 + ________ front_of_house::hosting; + + hosting::add_to_waitlist(); + hosting::seat_at_table(); +} +// 期望输出: +// 已加入等候列表 +// 已入座 +``` + +### 题目 2-4:super 关键字 + +```rust +fn serve_order() { + println!("上菜!"); +} + +mod back_of_house { + fn cook() { + println!("烹饪中..."); + } + + // 填空:使用 super 调用父模块中的 serve_order 函数 + pub fn deliver() { + cook(); + ________::serve_order(); + } +} + +fn main() { + // 填空:正确调用 deliver 函数 + ________(); +} +// 期望输出: +// 烹饪中... +// 上菜! +``` + +### 题目 2-5:as 别名 + +```rust +use std::fmt::Result as FmtResult; +// 填空:将 std::io::Result 以别名 IoResult 引入 +use std::io::Result ________ ________; + +fn main() { + // 直接使用别名调用 Ok 构造函数 + let _r1: FmtResult = Ok(()); + let _r2: IoResult<()> = Ok(()); + println!("两种 Result 都能正常使用!"); +} +``` + +### 题目 2-6:嵌套路径导入 + +```rust +// 下面的导入使用了 3 行 use 语句 +// use std::cmp::Ordering; +// use std::io; +// use std::io::Write; + +// 填空:替换为一行嵌套路径的 use 语句 +use std::________; +use std::________::{self, Write}; + +fn main() { + println!("模块导入成功!"); +} +``` + +### 题目 2-7:pub use 重导出 + +```rust +mod inner { + pub fn secret() -> &'static str { + "内部机密数据" + } +} + +// 填空:将 inner::secret 重导出为公开接口,使外部可以通过 crate::config 访问 +________ inner::secret as config; + +fn main() { + // 填空:通过新的路径名调用 + println!("{}", ________()); +} +// 期望输出:内部机密数据 +``` + +### 题目 2-8:模块拆分到文件 + +假设 `src/main.rs` 中有以下代码,需要将 `garden` 模块拆分到单独的文件中。 + +```rust +// ===== src/main.rs ===== +mod garden; + +fn main() { + garden::grow(); +} +// 期望输出:植物正在生长... + +// ===== 填空:写出 src/garden.rs 的内容 ===== +________ fn grow() { + println!("植物正在生长..."); +} +``` + +### 题目 2-9:模块拆分到目录 + +假设有以下模块结构,需要将 `restaurant` 模块拆分到 `src/restaurant/` 目录中。 + +```rust +// ===== src/main.rs ===== +mod restaurant; + +fn main() { + restaurant::front::serve(); +} +// 期望输出:上菜中... + +// ===== 填空:写出需要的文件及其内容 ===== +// 文件 1: src/restaurant/________ +// 文件 2: src/restaurant/________ + +// restaurant/________ 内容: +pub mod front; + +// restaurant/________ 内容: +pub fn serve() { + println!("上菜中..."); +} +``` + +--- + +## 三、找出并修复错误 + +以下每段代码都有编译错误,请指出错误并写出修正后的代码。 + +### 题目 3-1 + +```rust +mod house { + fn open_door() { + println!("门开了"); + } +} + +fn main() { + house::open_door(); +} +``` + +### 题目 3-2 + +```rust +mod front { + pub fn greet() { + println!("欢迎光临!"); + } +} + +fn main() { + use front; + greet(); +} +``` + +### 题目 3-3 + +```rust +mod math { + pub mod operations { + pub fn add(a: i32, b: i32) -> i32 { + a + b + } + } +} + +fn main() { + use math::operations; + println!("{}", add(3, 5)); +} +``` + +### 题目 3-4 + +```rust +mod a { + pub mod b { + pub fn hello() { + println!("hello from b"); + } + } +} + +fn main() { + use a::b; + // 想要同时使用模块 b 和 c(c 不存在) + use a::c; + b::hello(); +} +``` + +### 题目 3-5 + +```rust +mod outer { + mod inner { + pub fn secret_data() -> &'static str { + "机密" + } + } + + pub fn reveal() { + inner::secret_data(); + } +} + +fn main() { + outer::reveal(); + // 下面这行是否可以? + // outer::inner::secret_data(); +} +``` + +### 题目 3-6 + +```rust +mod food { + pub struct Breakfast { + pub toast: String, + seasonal_fruit: String, + } + + impl Breakfast { + pub fn summer(toast: &str) -> Breakfast { + Breakfast { + toast: String::from(toast), + seasonal_fruit: String::from("桃子"), + } + } + } +} + +fn main() { + let mut meal = food::Breakfast::summer("黑麦面包"); + meal.toast = String::from("全麦面包"); + println!("我要{}吐司", meal.toast); + + // 下面这行取消注释会怎样? + // meal.seasonal_fruit = String::from("蓝莓"); +} +``` + +### 题目 3-7 + +```rust +// ===== Cargo.toml ===== +// [package] +// name = "my_project" +// version = "0.1.0" +// edition = "2021" + +// ===== src/main.rs ===== +use rand::Rng; + +fn main() { + let mut rng = rand::thread_rng(); + let n: i32 = rng.gen_range(1..100); + println!("随机数: {}", n); +} +// 运行 cargo build 时报错:can't find crate for `rand` +``` + +### 题目 3-8 + +```rust +use std::collections::HashMap; +use std::collections::HashSet; + +fn main() { + let mut map: HashMap<&str, i32> = HashMap::new(); + map.insert("one", 1); + + let mut set: HashSet = HashSet::new(); + set.insert(1); + + println!("HashMap: {:?}", map); + println!("HashSet: {:?}", set); +} +// 虽然编译通过,但如何将两个 use 语句合并为一行? +``` + +--- + +## 四、实战题:组织代码 + +### 题目 4-1:餐厅管理系统 + +请按以下要求组织一个餐厅管理系统的模块结构,并实现全部代码。 + +**模块结构要求:** + +``` +restaurant (库 Crate) +├── front_of_house/ +│ ├── mod.rs → pub mod hosting; pub mod serving; +│ ├── hosting.rs → pub fn add_to_waitlist() { ... } +│ │ pub fn seat_at_table() { ... } +│ └── serving.rs → pub fn take_order() { ... } +│ pub fn serve_order() { ... } +│ pub fn take_payment() { ... } +├── back_of_house.rs → pub fn cook() { ... } +│ fn wash_dishes() { ... } ← 私有函数 +│ pub fn clean_up() { ... } 调用 wash_dishes() +├── menu.rs → pub enum Dish { ... } +│ impl Dish { fn price(&self) -> f64 } +└── lib.rs → pub mod front_of_house; + pub mod back_of_house; + pub mod menu; + pub fn eat_at_restaurant() { 演示调用各模块 } +``` + +**要求:** + +1. 在 `menu.rs` 中定义 `Dish` 枚举,包含以下变体和价格: + - `Steak` → 168.0 + - `Salad` → 38.0 + - `Pasta` → 58.0 + - `Water` → 0.0(免费) + +2. 为 `Dish` 实现 `price(&self) -> f64` 方法 + +3. 实现 `eat_at_restaurant()` 函数,演示完整的用餐流程: + - 加入等候列表 + - 入座 + - 点菜(点一个牛排和一份沙拉) + - 上菜 + - 结账(打印总价) + - 清理 + +4. 创建对应的目录结构,写出所有文件的完整代码 + +### 题目 4-2:数学工具库 + +创建一个名为 `math_tools` 的库 Crate,按以下层级组织代码。 + +**模块结构:** + +``` +math_tools (库 Crate) +├── lib.rs +├── basic/ +│ ├── mod.rs → pub mod arithmetic; pub mod compare; +│ ├── arithmetic.rs → pub fn add, sub, mul, div +│ └── compare.rs → pub fn min, max, is_even +├── advanced/ +│ ├── mod.rs → pub mod stats; pub mod geometry; +│ ├── stats.rs → pub fn mean, median, variance +│ └── geometry.rs → pub fn circle_area, rect_area, triangle_area +└── utils.rs → pub fn is_prime(n: u32) -> bool + pub fn factorial(n: u32) -> u64 +``` + +**要求:** + +1. 实现全部函数 +2. `lib.rs` 中使用 `pub use` 将以下函数重导出为库的顶层接口: + - `add`、`mean`、`is_prime`、`circle_area` +3. 在 `lib.rs` 中写测试(`#[cfg(test)] mod tests`)验证以下场景: + - `add(2, 3) == 5` + - `mean(&[1.0, 2.0, 3.0, 4.0, 5.0]) == 3.0` + - `is_prime(17)` 和 `is_prime(18)` + - `circle_area(1.0)` 约等于 π + +### 题目 4-3:多二进制 Crate 的 Package + +创建一个 Package,包含一个库 Crate 和两个二进制 Crate。 + +**结构:** + +``` +my_app/ +├── Cargo.toml → package name = "my_app" +├── src/ +│ ├── lib.rs → 库 Crate:定义 Config 结构体,包含 host, port, debug 字段 +│ │ 和 parse_args 函数,解析命令行参数 +│ └── bin/ +│ ├── server.rs → 二进制 Crate:启动一个模拟服务器,打印配置信息 +│ └── client.rs → 二进制 Crate:模拟连接到服务器,打印连接信息 +``` + +**要求:** + +1. 库中定义 `Config` 结构体: + ```rust + pub struct Config { + pub host: String, + pub port: u16, + pub debug: bool, + } + ``` +2. 库中实现 `Config` 的 `new` 关联函数(使用默认值) +3. `server.rs` 调用库的 `Config::new()`,打印 `"服务器启动于 {host}:{port}"`,如果 debug 为 true 再打印调试信息 +4. `client.rs` 调用库的 `Config::new()`,打印 `"客户端连接到 {host}:{port}"` +5. 创建完整的目录结构,写出所有文件内容 + +--- + +## 五、综合思考题 + +### 题目 5-1 + +分析以下模块结构。假设当前在 `crate::outer::middle::inner` 模块中,有哪些方式可以访问 `crate::outer::top_level` 中的 `value` 函数?请写出至少三种不同的路径写法。 + +```rust +// lib.rs +pub mod outer { + pub fn top_level() {} + + pub mod middle { + pub mod inner { + pub fn deep() { + // 在此处调用 outer::top_level() + } + } + } +} + +pub mod another { + pub fn value() { + println!("another value"); + } +} +``` + +### 题目 5-2 + +对比以下三种 `use` 导入方式的优缺点和使用场景: + +```rust +// 方式 A:导入模块本身 +use std::collections; +// 使用: collections::HashMap::new() + +// 方式 B:导入具体类型 +use std::collections::HashMap; +// 使用: HashMap::new() + +// 方式 C:通配符导入 +use std::collections::*; +// 使用: HashMap::new(), HashSet::new(), BTreeMap::new() +``` + +### 题目 5-3 + +Rust 的可见性规则中,`pub` 的"公开"是相对于**模块路径**而言的,而不是公开给所有人。请解释以下代码中为什么 `outer::inner::secret` 虽然标记为 `pub`,但在 `main` 中仍然无法直接访问。 + +```rust +mod outer { + mod inner { // inner 模块本身是私有的 + pub fn secret() { + println!("秘密"); + } + } + + pub fn reveal() { + inner::secret(); // 可以,因为 outer 是 inner 的父模块 + } +} + +fn main() { + outer::reveal(); // 可以 + // outer::inner::secret(); // 取消注释会怎样? +} +``` + +### 题目 5-4 + +阅读以下代码,回答: + +```rust +mod parent { + pub fn parent_func() { + println!("父函数"); + } + + pub mod child { + pub fn child_func() { + // 如何在 child 模块中调用 parent_func()? + } + } +} + +mod sibling { + pub fn sibling_func() { + // 如何在此处调用 parent::parent_func()? + } +} +``` + +1. 在 `child` 模块中调用 `parent_func()` 应该使用什么路径?(写出具体写法) +2. 在 `sibling` 模块中调用 `parent_func()` 应该使用什么路径? +3. `super` 和 `crate` 分别指代什么?各自适用于什么场景? + +### 题目 5-5 + +`pub mod` 和 `pub use` 有什么区别?以下两种写法分别适用于什么场景? + +```rust +// 写法 A:嵌套模块 + 重导出 +mod internal { + pub fn helper() {} +} +pub use internal::helper; + +// 写法 B:先定义公开模块,再导入使用 +pub mod api { + pub fn helper() {} +} +``` + +--- + +## 参考答案 + +> 请独立完成再查看答案。 + +
+点击展开答案 + +### 一、概念辨析题 + +**1-1**:❌ 错误。一个 Package 最多包含一个库 Crate,但可以包含任意多个二进制 Crate。例如 `src/main.rs` 是一个二进制 Crate,`src/bin/` 下的每个文件也是独立的二进制 Crate,同时还可以有一个 `src/lib.rs` 作为库 Crate。 + +**1-2**:✅ 正确。`src/main.rs` 是二进制 Crate 的根文件(crate root),`src/lib.rs` 是库 Crate 的根文件。Cargo 会将根文件传递给 `rustc` 来构建 crate。 + +**1-3**:❌ 错误。在模块树中,**子模块可以访问父模块中的所有项(包括私有项)**,但反过来不成立——父模块不能访问子模块中的私有项。要将子模块的项暴露给外部,必须用 `pub` 标记。 + +**1-4**:❌ 错误。`use` 只引入指定的路径,不会自动引入子模块。例如 `use std::collections;` 只引入 `collections` 模块本身,`collections` 下的 `HashMap` 等仍需要通过 `collections::HashMap` 访问。 + +**1-5**:✅ 正确。`cargo new my_project` 创建的模板包含 `src/main.rs`,是一个二进制 Crate。`cargo new my_lib --lib` 创建的是库 Crate(`src/lib.rs`)。 + +**1-6**:✅ 正确。同一个文件中可以定义任意多个 `mod` 块,每个块成为一个独立的模块。 + +**1-7**:❌ 错误。`use` 将路径引入当前作用域供自己使用,外部代码无法感知。`pub use` 是**重导出(re-exporting)**,不仅引入当前作用域,还将其作为当前模块的公开 API 暴露给外部使用者。 + +**1-8**:✅ 正确。`src/bin/` 目录下的每个 `.rs` 文件会被 Cargo 自动编译为独立的二进制 Crate,名称等于文件名(不含扩展名)。 + +### 二、填空题 + +**2-1**: +```rust +mod network { + fn connect() { + println!("已连接"); + } +} + +fn main() { + network::connect(); +} +``` + +**2-2**: +```rust +mod garden { + pub fn plant() -> &'static str { + "玫瑰花" + } + fn water() { println!("浇水..."); } +} + +fn main() { + let flower = garden::plant(); + println!("种了{}", flower); + // garden::water(); 取消注释会报错:water 是私有函数 +} +``` + +**2-3**: +```rust +use front_of_house::hosting; +``` + +**2-4**: +```rust +pub fn deliver() { + cook(); + super::serve_order(); +} + +fn main() { + back_of_house::deliver(); +} +``` + +**2-5**: +```rust +use std::io::Result as IoResult; +``` + +**2-6**: +```rust +use std::{cmp::Ordering, io::{self, Write}}; +``` + +**2-7**: +```rust +pub use inner::secret as config; + +fn main() { + println!("{}", config()); +} +``` + +**2-8**: +``` +文件: src/garden.rs +``` +```rust +pub fn grow() { + println!("植物正在生长..."); +} +``` + +**2-9**: +``` +文件 1: src/restaurant/mod.rs +内容: pub mod front; + +文件 2: src/restaurant/front.rs +内容: pub fn serve() { println!("上菜中..."); } +``` + +### 三、修复错误 + +**3-1**:`open_door` 是私有函数,模块外部无法访问。修复:加 `pub`。 +```rust +mod house { + pub fn open_door() { println!("门开了"); } +} +``` + +**3-2**:`use front;` 只引入了模块,没有引入函数。函数名本身不在作用域中。修复: +```rust +use front::greet; +// 或 use front; 后调用 front::greet(); +``` + +**3-3**:`use math::operations;` 引入的是模块,`add` 需要通过 `operations::add()` 调用。修复: +```rust +use math::operations::add; +println!("{}", add(3, 5)); +``` + +**3-4**:模块 `a::c` 不存在。如果 `c` 不需要,直接删除该行。如果想引入两个模块,应等模块存在。此题故意制造了一个引入不存在的模块的错误。 + +**3-5**:主函数中 `outer::inner::secret_data()` **不能**使用。虽然 `secret_data` 标记了 `pub`,但 `inner` 模块本身是私有的(没有 `pub mod`),外部无法通过私有模块访问其内部的任何项。修复:将 `mod inner` 改为 `pub mod inner`。 + +**3-6**:取消注释会报错,因为 `seasonal_fruit` 字段是私有的,即便结构体实例是 `mut`,也不能修改私有字段。只有直接拥有该结构体的模块才能访问私有字段。但如果改为: +```rust +meal.seasonal_fruit = String::from("蓝莓"); +``` +编译错误:`seasonal_fruit` is private。外模块只能修改 `pub` 字段。 + +**3-7**:`Cargo.toml` 中未添加 `rand` 依赖。修复:在 `[dependencies]` 下添加: +```toml +[dependencies] +rand = "0.8" +``` +然后运行 `cargo build`,Cargo 会自动下载并编译 `rand`。 + +**3-8**:合并为: +```rust +use std::collections::{HashMap, HashSet}; +``` + +### 四、实战题 + +**4-1 餐厅管理系统:** + +文件结构: +``` +src/ +├── lib.rs +├── front_of_house/ +│ ├── mod.rs +│ ├── hosting.rs +│ └── serving.rs +├── back_of_house.rs +└── menu.rs +``` + +```rust +// src/lib.rs +pub mod front_of_house; +pub mod back_of_house; +pub mod menu; + +use menu::Dish; + +pub fn eat_at_restaurant() { + front_of_house::hosting::add_to_waitlist(); + front_of_house::hosting::seat_at_table(); + + let order = vec![Dish::Steak, Dish::Salad]; + front_of_house::serving::take_order(order.clone()); + front_of_house::serving::serve_order(); + + let total: f64 = order.iter().map(|d| d.price()).sum(); + println!("总消费: {:.2} 元", total); + + front_of_house::serving::take_payment(); + back_of_house::clean_up(); +} +``` + +```rust +// src/front_of_house/mod.rs +pub mod hosting; +pub mod serving; +``` + +```rust +// src/front_of_house/hosting.rs +pub fn add_to_waitlist() { + println!("已加入等候列表"); +} + +pub fn seat_at_table() { + println!("已入座"); +} +``` + +```rust +// src/front_of_house/serving.rs +use crate::menu::Dish; + +pub fn take_order(dishes: Vec) { + print!("点菜: "); + for (i, dish) in dishes.iter().enumerate() { + if i > 0 { print!(", "); } + print!("{:?}", dish); + } + println!(); +} + +pub fn serve_order() { + println!("上菜完成!"); +} + +pub fn take_payment() { + println!("结账完成!"); +} +``` + +```rust +// src/back_of_house.rs +pub fn cook() { + println!("烹饪中..."); +} + +fn wash_dishes() { + println!("洗碗中..."); +} + +pub fn clean_up() { + cook(); + println!("厨房已清理"); + wash_dishes(); +} +``` + +```rust +// src/menu.rs +#[derive(Debug, Clone)] +pub enum Dish { + Steak, + Salad, + Pasta, + Water, +} + +impl Dish { + pub fn price(&self) -> f64 { + match self { + Dish::Steak => 168.0, + Dish::Salad => 38.0, + Dish::Pasta => 58.0, + Dish::Water => 0.0, + } + } +} +``` + +**4-2 数学工具库:** + +文件结构: +``` +src/ +├── lib.rs +├── basic/ +│ ├── mod.rs +│ ├── arithmetic.rs +│ └── compare.rs +├── advanced/ +│ ├── mod.rs +│ ├── stats.rs +│ └── geometry.rs +└── utils.rs +``` + +```rust +// src/lib.rs +pub mod basic; +pub mod advanced; +pub mod utils; + +pub use basic::arithmetic::add; +pub use advanced::stats::mean; +pub use advanced::geometry::circle_area; +pub use utils::is_prime; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + assert_eq!(add(2, 3), 5); + } + + #[test] + fn test_mean() { + assert_eq!(mean(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0); + } + + #[test] + fn test_is_prime() { + assert!(is_prime(17)); + assert!(!is_prime(18)); + } + + #[test] + fn test_circle_area() { + let area = circle_area(1.0); + assert!((area - std::f64::consts::PI).abs() < 1e-10); + } +} +``` + +```rust +// src/basic/mod.rs +pub mod arithmetic; +pub mod compare; +``` + +```rust +// src/basic/arithmetic.rs +pub fn add(a: i32, b: i32) -> i32 { a + b } +pub fn sub(a: i32, b: i32) -> i32 { a - b } +pub fn mul(a: i32, b: i32) -> i32 { a * b } +pub fn div(a: i32, b: i32) -> Option { + if b == 0 { None } else { Some(a / b) } +} +``` + +```rust +// src/basic/compare.rs +pub fn min(a: i32, b: i32) -> i32 { if a < b { a } else { b } } +pub fn max(a: i32, b: i32) -> i32 { if a > b { a } else { b } } +pub fn is_even(n: i32) -> bool { n % 2 == 0 } +``` + +```rust +// src/advanced/mod.rs +pub mod stats; +pub mod geometry; +``` + +```rust +// src/advanced/stats.rs +pub fn mean(data: &[f64]) -> f64 { + let sum: f64 = data.iter().sum(); + sum / data.len() as f64 +} + +pub fn median(data: &mut [f64]) -> f64 { + data.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let mid = data.len() / 2; + if data.len() % 2 == 0 { + (data[mid - 1] + data[mid]) / 2.0 + } else { + data[mid] + } +} + +pub fn variance(data: &[f64]) -> f64 { + let m = mean(data); + data.iter().map(|x| (x - m).powi(2)).sum::() / data.len() as f64 +} +``` + +```rust +// src/advanced/geometry.rs +use std::f64::consts::PI; + +pub fn circle_area(radius: f64) -> f64 { PI * radius * radius } +pub fn rect_area(width: f64, height: f64) -> f64 { width * height } +pub fn triangle_area(base: f64, height: f64) -> f64 { base * height / 2.0 } +``` + +```rust +// src/utils.rs +pub fn is_prime(n: u32) -> bool { + if n < 2 { return false; } + for i in 2..=((n as f64).sqrt() as u32) { + if n % i == 0 { return false; } + } + true +} + +pub fn factorial(n: u32) -> u64 { + (1..=n).fold(1, |acc, x| acc * x as u64) +} +``` + +**4-3 多二进制 Crate Package:** + +文件结构: +``` +my_app/ +├── Cargo.toml +└── src/ + ├── lib.rs + └── bin/ + ├── server.rs + └── client.rs +``` + +```toml +# Cargo.toml +[package] +name = "my_app" +version = "0.1.0" +edition = "2021" +``` + +```rust +// src/lib.rs +pub struct Config { + pub host: String, + pub port: u16, + pub debug: bool, +} + +impl Config { + pub fn new() -> Config { + Config { + host: String::from("127.0.0.1"), + port: 8080, + debug: false, + } + } +} +``` + +```rust +// src/bin/server.rs +use my_app::Config; + +fn main() { + let config = Config::new(); + println!("服务器启动于 {}:{}", config.host, config.port); + if config.debug { + println!("[调试模式] 详细日志已启用"); + } +} +``` + +```rust +// src/bin/client.rs +use my_app::Config; + +fn main() { + let config = Config::new(); + println!("客户端连接到 {}:{}", config.host, config.port); +} +``` + +运行方式: +``` +cargo run --bin server # 启动服务器 +cargo run --bin client # 启动客户端 +``` + +### 五、综合思考题 + +**5-1**:在 `inner::deep()` 中访问 `outer::top_level()` 的三种方式: + +```rust +pub fn deep() { + // 方式 1:super(回到父模块 middle,再回到 outer) + super::super::top_level(); + + // 方式 2:crate 绝对路径 + crate::outer::top_level(); + + // 方式 3:self + 多级 super + // self::super::super::top_level(); // 等效于 super::super +} +``` + +若要访问 `another::value()`: +```rust +super::super::super::another::value(); // 不够优雅 +crate::another::value(); // 推荐:使用 crate 根路径 +``` + +**5-2**: + +| 维度 | 方式 A(导入模块) | 方式 B(导入类型) | 方式 C(通配符) | +|------|------------------|------------------|----------------| +| 清晰度 | 中等(知道来自哪个模块) | 低(看不出来自哪个模块) | 最低(完全不清楚来源) | +| 简洁度 | 低(写路径长) | 高(直接使用类型名) | 高 | +| 名称冲突风险 | 低 | 中等(可能与其他同名类型冲突) | 高(极易引入未知冲突) | +| 适用场景 | 需要明确模块来源时;模块下类型少时 | 高频使用的具体类型 | 测试模块、prelude 模式;生产代码不推荐 | + +最佳实践: +- 常规代码:使用方式 A(模块导入),调用时写 `collections::HashMap` +- 高频类型:使用方式 B +- 避免方式 C(除测试模块和 prelude 设计) + +**5-3**: + +虽然 `secret` 标记为 `pub`,但 `inner` 模块本身是**私有的**(`mod inner` 没有 `pub`)。Rust 的可见性是分层级的:父模块定义子模块的可见范围。因为 `inner` 模块对外不可见,即使它内部的项全是 `pub`,外部也无法访问——路径在 `inner` 这一层就已经堵死了。 + +类比:一个没有挂牌的办公楼(私有模块),即使一楼大堂对所有访客开放(内部函数是 `pub`),路人也找不到入口。 + +**5-4**: + +1. 在 `child` 中调用 `parent_func()`: + ```rust + super::parent_func(); + ``` + +2. 在 `sibling` 中调用 `parent_func()`: + ```rust + crate::parent::parent_func(); + ``` + +3. `super` 和 `crate` 的区别: + - `super`:指向**当前模块的父模块**,适用于在同级模块间或访问父模块内容时使用。 + - `crate`:指向**crate 根目录**(即 `lib.rs` 或 `main.rs` 顶层),适用于跨模块树访问时使用绝对路径。 + - 场景:父子关系简单、层级少时用 `super`;跨多层级或需要稳定路径时用 `crate`。 + +**5-5**: + +**写法 A:内部模块 + 重导出** +```rust +mod internal { + pub fn helper() {} +} +pub use internal::helper; +``` +- `internal` 模块是私有的,外部不知道它的存在 +- 通过 `pub use` 将 `helper` 提升到外层模块的公开 API 中 +- 适用于:**内部组织代码但对外隐藏实现细节**。外部调用者只看到 `helper`,不知道它来自 `internal` 模块。后续可以自由重构 `internal` 而不会破坏 API。 + +**写法 B:公开模块** +```rust +pub mod api { + pub fn helper() {} +} +``` +- `api` 模块整体暴露在 API 中 +- 外部使用时需要 `api::helper()`,模块结构成为 API 的一部分 +- 适用于:**模块结构本身就是设计意图的一部分**,希望用户理解模块层级。但将模块布局暴露后,修改结构可能破坏下游代码。 + +总结: +- 写法 A(`pub use` 重导出):灵活、可卸载、隐藏内部结构;库设计常用模式 +- 写法 B(`pub mod`):简单直接、但模块结构成为契约的一部分,重构成本高 + +
diff --git a/tauri-app/CHANGELOG.md b/tauri-app/CHANGELOG.md new file mode 100644 index 0000000..35bec2d --- /dev/null +++ b/tauri-app/CHANGELOG.md @@ -0,0 +1,57 @@ +# WriteFlow Changelog + +## Phase 1 – MVP 基础编辑器 + +### 迭代 1.1 – 项目骨架与编辑器内核 (2026-07-15) + +#### 新增 +- 配置 `vite.config.ts` 与 `tsconfig.json` 的 `@` 别名,指向 `src` 目录 +- 集成 Milkdown 编辑器:`@milkdown/kit` + `@milkdown/vue`,配置 CommonMark 预设 +- 创建 `Editor.vue` 编辑器组件,支持 v-model 双向绑定、历史记录 (history)、剪贴板 (clipboard) +- 实现纯键盘驱动所见即所得编辑(无工具栏) +- 自定义标题栏:含 WriteFlow Logo、主题切换按钮、窗口控制(最小化/最大化/关闭) + - 使用 `@vicons/ionicons5` 图标库,通过 Naive UI `n-icon` 组件渲染(`CreateOutline`、`SunnyOutline`、`MoonOutline`、`RemoveOutline`、`ExpandOutline`、`ContractOutline`、`CloseOutline`) +- 亮色 / 暗色主题切换:CSS 变量驱动,标题栏、编辑器、滚动条同步变化 +- 集成 Naive UI 组件库,使用 `n-config-provider` 管理全局主题 +- 编辑器样式覆盖:标题、列表、引用、代码块、表格、链接等 Markdown 元素样式完整 +- Markdown 内容变化通过 `listener` 插件实时同步到 v-model + +#### 技术要点 +- Milkdown v7 编辑器通过 `useEditor` composable 创建实例 +- 自定义标题栏使用 Tauri `data-tauri-drag-region` 实现窗口拖拽 +- 主题通过 `html.dark` CSS 类切换,Naive UI 同步切换 `darkTheme` +- 编辑器 `ProseMirror` 输出 markdown 通过 `listenerCtx.markdownUpdated` 实时获取 + +#### 修复 +- 修复页面空白:`MilkdownProvider` 需作为 `useEditor` 的祖先组件提供上下文,将其从 Editor.vue 提升至 App.vue +- 修复窗口拖拽权限:补充 `core:window:allow-is-maximized`、`allow-maximize`、`allow-unmaximize` +- 修复编辑器无法滚动:`#app` 改用 `padding-top: 40px` 替代 `.app-body` 的 `margin-top: 40px`,避免 `overflow: hidden` 裁掉底部内容导致高度计算错误 +- 移除 `milkdown.css` 中 `min-height: 100%` 避免干扰 flex 布局高度约束 +- 导入 ProseMirror 和 Tables 基础 CSS(`@milkdown/kit/prose/view/style/prosemirror.css`、`tables.css`) + +### 迭代 1.2 – 文件系统集成 (2026-07-15) + +#### 新增 +- **Rust 后端**:添加 `tauri-plugin-fs` + `tauri-plugin-dialog` 插件,实现文件系统 Tauri commands + - `read_directory` — 递归读取目录,返回文件树结构(仅 `.md` 文件和非空目录) + - `read_file_content` / `write_file_content` — 读写文件内容 + - `create_file` / `rename_file` / `delete_file` / `file_exists` — 文件增删改查 +- **前端插件**:安装 `@tauri-apps/plugin-fs` + `@tauri-apps/plugin-dialog` +- **文件树侧边栏** (`Sidebar.vue` + `FileTree.vue`):使用 Naive UI `n-tree` 渲染目录结构 + - 支持展开/折叠文件夹,点击 `.md` 文件在编辑器中打开 + - 顶部工具栏:打开工作区、刷新、最近文件切换 +- **新建/打开/保存文件** + - `Ctrl+N` 新建空白文件,`Ctrl+O` 打开文件对话框 + - `Ctrl+S` 保存当前文件,`Ctrl+Shift+S` 另存为 + - 首次启动自动弹出工作区目录选择 +- **工作区管理**:工作区路径持久化到 `localStorage`,后续启动自动加载 +- **最近文件列表**:记录最近 10 个打开的文件,存储在 `localStorage`,重启保留 +- **标题栏增强**:显示当前文件名 + 未保存标记(●) +- **Editor 组件增强**:支持外部内容替换(`replaceAll`),用于打开文件时更新编辑器 + +#### 技术要点 +- 文件操作通过 Rust `std::fs` 实现,前端通过 `invoke` 调用 Tauri commands +- 目录遍历过滤隐藏文件和 `node_modules`/`target` 目录,仅展示 `.md` 文件 +- 编辑器使用 `@milkdown/kit/utils` 的 `replaceAll` 实现外部内容替换 +- 使用 `@tauri-apps/plugin-dialog` 的 `open`/`save` 实现原生文件对话框 +- 快捷键通过全局 `keydown` 监听,区分 `Ctrl`/`Cmd` + 组合键 diff --git a/tauri-app/README.md b/tauri-app/README.md index 12920b6..c1b0f86 100644 --- a/tauri-app/README.md +++ b/tauri-app/README.md @@ -1,7 +1,143 @@ -# Tauri + Vue + TypeScript +# WriteFlow - 所见即所得的 Markdown 编辑器 -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` - - - diff --git a/tauri-app/src/components/Editor.vue b/tauri-app/src/components/Editor.vue new file mode 100644 index 0000000..689fb5c --- /dev/null +++ b/tauri-app/src/components/Editor.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/tauri-app/src/components/FileTree.vue b/tauri-app/src/components/FileTree.vue new file mode 100644 index 0000000..f138ac4 --- /dev/null +++ b/tauri-app/src/components/FileTree.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/tauri-app/src/components/Sidebar.vue b/tauri-app/src/components/Sidebar.vue new file mode 100644 index 0000000..4e6e132 --- /dev/null +++ b/tauri-app/src/components/Sidebar.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/tauri-app/src/components/TitleBar.vue b/tauri-app/src/components/TitleBar.vue index 00cef2c..da35044 100644 --- a/tauri-app/src/components/TitleBar.vue +++ b/tauri-app/src/components/TitleBar.vue @@ -1,62 +1,78 @@ diff --git a/tauri-app/src/main.ts b/tauri-app/src/main.ts index 7d82ca2..ec9f329 100644 --- a/tauri-app/src/main.ts +++ b/tauri-app/src/main.ts @@ -1,8 +1,12 @@ -import { createApp } from "vue"; -import Antd from "ant-design-vue"; -import App from "./App.vue"; -import "ant-design-vue/dist/reset.css"; +import { createApp } from 'vue' +import { create } from 'naive-ui' +import App from './App.vue' -const app = createApp(App); -app.use(Antd); -app.mount("#app"); +import '@milkdown/kit/prose/view/style/prosemirror.css' +import '@milkdown/kit/prose/tables/style/tables.css' + +const naive = create() +const app = createApp(App) + +app.use(naive) +app.mount('#app') diff --git a/tauri-app/src/stores/recentFiles.ts b/tauri-app/src/stores/recentFiles.ts new file mode 100644 index 0000000..d37eedb --- /dev/null +++ b/tauri-app/src/stores/recentFiles.ts @@ -0,0 +1,26 @@ +const STORAGE_KEY = 'writeflow-recent-files' +const MAX_RECENT = 10 + +export function getRecentFiles(): string[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + return JSON.parse(raw) as string[] + } catch { + return [] + } +} + +export function addRecentFile(path: string): void { + const files = getRecentFiles().filter((f) => f !== path) + files.unshift(path) + if (files.length > MAX_RECENT) { + files.length = MAX_RECENT + } + localStorage.setItem(STORAGE_KEY, JSON.stringify(files)) +} + +export function removeRecentFile(path: string): void { + const files = getRecentFiles().filter((f) => f !== path) + localStorage.setItem(STORAGE_KEY, JSON.stringify(files)) +} diff --git a/tauri-app/src/stores/workspace.ts b/tauri-app/src/stores/workspace.ts new file mode 100644 index 0000000..7d76038 --- /dev/null +++ b/tauri-app/src/stores/workspace.ts @@ -0,0 +1,13 @@ +const STORAGE_KEY = 'writeflow-workspace' + +export function getWorkspacePath(): string | null { + return localStorage.getItem(STORAGE_KEY) +} + +export function setWorkspacePath(path: string): void { + localStorage.setItem(STORAGE_KEY, path) +} + +export function clearWorkspacePath(): void { + localStorage.removeItem(STORAGE_KEY) +} diff --git a/tauri-app/src/styles/global.css b/tauri-app/src/styles/global.css new file mode 100644 index 0000000..81c026e --- /dev/null +++ b/tauri-app/src/styles/global.css @@ -0,0 +1,58 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, body, #app { + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; + background: var(--color-bg); + color: var(--color-text); +} + +body { + font-family: 'Inter', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, sans-serif; + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +#app { + display: flex; + flex-direction: column; + padding-top: 40px; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: var(--color-scrollbar-track); +} + +::-webkit-scrollbar-thumb { + background: var(--color-scrollbar-thumb); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-text-secondary); +} + +a { + color: var(--color-accent); + text-decoration: none; +} + +a:hover { + color: var(--color-accent-hover); +} diff --git a/tauri-app/src/styles/milkdown.css b/tauri-app/src/styles/milkdown.css new file mode 100644 index 0000000..64ce951 --- /dev/null +++ b/tauri-app/src/styles/milkdown.css @@ -0,0 +1,129 @@ +/* ========== Milkdown 编辑器全局样式覆盖 ========== */ + +.editor-wrapper { + background: var(--color-bg); + color: var(--color-text); + font-family: 'Inter', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 16px; + line-height: 1.75; +} + +.editor-wrapper .milkdown-editor { + max-width: 820px; + margin: 0 auto; + padding: 40px 60px 200px; + outline: none; +} + +.editor-wrapper .milkdown-editor .ProseMirror { + outline: none; +} +.editor-wrapper .milkdown-editor .ProseMirror > * + * { + margin-top: 0.75em; +} + +/* 标题 */ +.editor-wrapper h1 { font-size: 2em; font-weight: 700; margin: 1em 0 0.5em; line-height: 1.2; } +.editor-wrapper h2 { font-size: 1.5em; font-weight: 600; margin: 1em 0 0.5em; line-height: 1.3; } +.editor-wrapper h3 { font-size: 1.25em; font-weight: 600; margin: 0.8em 0 0.4em; } +.editor-wrapper h4 { font-size: 1.1em; font-weight: 600; } +.editor-wrapper h5 { font-size: 1em; font-weight: 600; } +.editor-wrapper h6 { font-size: 0.9em; font-weight: 600; color: var(--color-text-secondary); } + +/* 段落 */ +.editor-wrapper p { margin: 0.5em 0; } + +/* 加粗斜体 */ +.editor-wrapper strong { font-weight: 700; } +.editor-wrapper em { font-style: italic; } + +/* 列表 */ +.editor-wrapper ul, .editor-wrapper ol { + padding-left: 1.5em; + margin: 0.5em 0; +} +.editor-wrapper li { margin: 0.25em 0; } + +/* 引用 */ +.editor-wrapper blockquote { + margin: 0.75em 0; + padding: 0.5em 1em; + border-left: 4px solid var(--color-blockquote-border); + background: var(--color-blockquote-bg); + border-radius: 0 4px 4px 0; +} + +/* 代码 */ +.editor-wrapper code { + font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace; + font-size: 0.9em; + background: var(--color-code-bg); + padding: 0.1em 0.3em; + border-radius: 3px; +} + +.editor-wrapper pre { + background: var(--color-code-bg); + padding: 1em; + border-radius: 8px; + overflow-x: auto; + margin: 0.75em 0; +} + +.editor-wrapper pre code { + background: none; + padding: 0; +} + +/* 分割线 */ +.editor-wrapper hr { + border: none; + border-top: 2px solid var(--color-border); + margin: 1.5em 0; +} + +/* 链接 */ +.editor-wrapper a { + color: var(--color-accent); +} + +.editor-wrapper a:hover { + color: var(--color-accent-hover); +} + +/* 表格 */ +.editor-wrapper table { + border-collapse: collapse; + width: 100%; + margin: 0.75em 0; +} + +.editor-wrapper th, .editor-wrapper td { + border: 1px solid var(--color-table-border); + padding: 8px 12px; + text-align: left; +} + +.editor-wrapper th { + background: var(--color-table-stripe); + font-weight: 600; +} + +.editor-wrapper tr:nth-child(even) td { + background: var(--color-table-stripe); +} + +/* 图片 */ +.editor-wrapper img { + max-width: 100%; + border-radius: 6px; +} + +/* 选中文本高亮 */ +.editor-wrapper ::selection { + background-color: rgba(79, 140, 255, 0.3); +} + +html.dark .editor-wrapper ::selection { + background-color: rgba(137, 180, 250, 0.3); +} diff --git a/tauri-app/src/styles/theme.css b/tauri-app/src/styles/theme.css new file mode 100644 index 0000000..9fcce59 --- /dev/null +++ b/tauri-app/src/styles/theme.css @@ -0,0 +1,50 @@ +:root { + /* 编辑器基础色 */ + --color-bg: #ffffff; + --color-bg-secondary: #f8f9fa; + --color-bg-tertiary: #f0f0f0; + --color-text: #1a1a2e; + --color-text-secondary: #6b7280; + --color-border: #e5e7eb; + --color-accent: #4f8cff; + --color-accent-hover: #3a75e8; + --color-blockquote-bg: #f3f4f6; + --color-blockquote-border: #d1d5db; + --color-code-bg: #f3f4f6; + --color-table-border: #d1d5db; + --color-table-stripe: #f9fafb; + + /* 滚动条 */ + --color-scrollbar-thumb: #c1c1c1; + --color-scrollbar-track: transparent; + + /* 标题栏 */ + --titlebar-bg: #fafafa; + --titlebar-fg: rgba(0, 0, 0, 0.88); + --titlebar-btn-hover: rgba(0, 0, 0, 0.06); + --titlebar-btn-active: rgba(0, 0, 0, 0.12); +} + +html.dark { + --color-bg: #1e1e2e; + --color-bg-secondary: #252536; + --color-bg-tertiary: #2d2d44; + --color-text: #cdd6f4; + --color-text-secondary: #a6adc8; + --color-border: #3d3d5c; + --color-accent: #89b4fa; + --color-accent-hover: #74a8f2; + --color-blockquote-bg: #252536; + --color-blockquote-border: #45475a; + --color-code-bg: #181825; + --color-table-border: #45475a; + --color-table-stripe: #252536; + + --color-scrollbar-thumb: #585b70; + --color-scrollbar-track: transparent; + + --titlebar-bg: #141414; + --titlebar-fg: rgba(255, 255, 255, 0.85); + --titlebar-btn-hover: rgba(255, 255, 255, 0.08); + --titlebar-btn-active: rgba(255, 255, 255, 0.14); +} diff --git a/tauri-app/tsconfig.json b/tauri-app/tsconfig.json index f82888f..8f05e62 100644 --- a/tauri-app/tsconfig.json +++ b/tauri-app/tsconfig.json @@ -14,6 +14,11 @@ "noEmit": true, "jsx": "preserve", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + /* Linting */ "strict": true, "noUnusedLocals": true, diff --git a/tauri-app/vite.config.ts b/tauri-app/vite.config.ts index 812e61c..a571b87 100644 --- a/tauri-app/vite.config.ts +++ b/tauri-app/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; +import { resolve } from "path"; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; @@ -8,6 +9,12 @@ const host = process.env.TAURI_DEV_HOST; export default defineConfig(async () => ({ plugins: [vue()], + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors diff --git a/tauri-app/产品策划开发手册.md b/tauri-app/产品策划开发手册.md new file mode 100644 index 0000000..000dc51 --- /dev/null +++ b/tauri-app/产品策划开发手册.md @@ -0,0 +1,671 @@ +# WriteFlow 产品策划开发手册 + +> 版本:v1.0 | 作者:产品团队 | 最后更新:2026-07 + +--- + +## 目录 + +1. [产品概述](#1-产品概述) +2. [市场分析与竞品调研](#2-市场分析与竞品调研) +3. [目标用户与用户故事](#3-目标用户与用户故事) +4. [功能规划](#4-功能规划) +5. [技术架构](#5-技术架构) +6. [UI/UX 设计规范](#6-uiux-设计规范) +7. [数据结构设计](#7-数据结构设计) +8. [同步引擎设计](#8-同步引擎设计) +9. [开发排期与里程碑](#9-开发排期与里程碑) +10. [质量保障与测试策略](#10-质量保障与测试策略) +11. [发布与运营策略](#11-发布与运营策略) +12. [风险评估与应对](#12-风险评估与应对) + +--- + +## 1. 产品概述 + +### 1.1 产品定位 + +WriteFlow 是一款 **本地优先、所见即所得、支持多设备云同步** 的 Markdown 笔记编辑器。定位为 Typora 的现代化替代品,补齐 Typora 缺乏云同步能力、不再积极更新的短板。 + +### 1.2 产品愿景 + +> 让每一个人在任何设备上都能获得沉浸、流畅的 Markdown 写作体验,所有内容安全可控、随处可及。 + +### 1.3 核心价值主张 + +| 维度 | 价值 | +|------|------| +| 编辑体验 | 所见即所得,打字即渲染,无干扰沉浸 | +| 数据主权 | 本地 .md 文件存储,不锁定数据格式 | +| 多设备 | S3/WebDAV 协议打通,你自己的云存储 | +| 性能 | Tauri 桌面端原生性能,秒启动、低资源 | +| 可扩展 | 插件系统、多导出格式、自定义主题 | + +### 1.4 产品边界 + +- **做什么**:Markdown 编辑、本地文件管理、多设备云同步 +- **不做什么**:协作编辑(那是 Google Docs 的事)、在线 Web 版(专注桌面端)、知识图谱(那是 Obsidian 的事) + +--- + +## 2. 市场分析与竞品调研 + +### 2.1 竞品矩阵 + +| 产品 | 编辑模式 | 同步 | 价格 | 优势 | 劣势 | +|------|---------|------|------|------|------| +| **Typora** | WYSIWYG | 无内置 | $14.99 买断 | 编辑体验最好 | 停更、无同步、无插件 | +| **Obsidian** | 源码+预览 | 付费 Obsidian Sync $5/月 | 免费 | 插件生态强、双链 | 学习曲线陡、非 WYSIWYG、同步收费 | +| **Notion** | Block 编辑器 | 内置 | 免费+付费 | 全能协作 | 非 Markdown 原生态、云端依赖、慢 | +| **VS Code** | 源码+预览 | Git | 免费 | 全能编辑器 | Markdown 非核心、体验不够专注 | +| **Bear** | WYSIWYG | iCloud | 免费+订阅 | 体验精美 | 仅 Apple 生态 | +| **Joplin** | 双栏 | S3/WebDAV/NextCloud 等 | 免费开源 | 多协议同步 | 非 WYSIWYG、界面偏工程化 | +| **思源笔记** | WYSIWYG | 付费 S3/WebDAV | 免费+订阅 | 国产、功能全 | 数据格式私有 | + +### 2.2 WriteFlow 的差异化机会 + +1. **Typora 级别的编辑体验 + 内置同步** — 目前市场上没有产品同时做到这两点 +2. **本地文件格式(.md)—** 不锁定用户数据,可用任意编辑器打开 +3. **自带同步引擎 —** 用你自己的 S3/WebDAV,无需额外付费 +4. **轻量高性能 —** Tauri 比 Electron 内存占用少 60%+ +5. **跨平台 —** Windows / macOS / Linux 全支持 + +--- + +## 3. 目标用户与用户故事 + +### 3.1 核心用户画像 + +| 画像 | 描述 | 核心需求 | +|------|------|---------| +| **技术写作者** | 写技术博客、文档的开发者 | 代码高亮、数学公式、导出、Git 同步 | +| **知识管理者** | 做笔记、写日记的学生/研究者 | 多设备同步、全文搜索、文件管理 | +| **效率追求者** | 用 Markdown 做一切记录的极客 | 快捷键、最小干扰、自定义主题 | +| **隐私敏感者** | 不希望数据在第三方服务器 | 自建 S3/MinIO 同步、纯本地可用 | + +### 3.2 用户故事(关键场景) + +``` +作为一名技术博客作者, +我想要在笔记本上写完文章后,自动同步到台式机继续编辑, +以便无缝切换设备而不需要手动拷贝文件。 +``` + +``` +作为一个研究生, +我希望能将上课拍的板书照片拖入笔记中, +图片能自动上传到我的 S3 图床并在笔记里用相对路径引用, +以便在任何设备上都能看到完整笔记。 +``` + +``` +作为一个隐私敏感用户, +我希望能使用自己搭建的 MinIO 服务器作为同步后端, +笔记和图片都不会经过任何第三方服务器。 +``` + +``` +作为一名程序员, +我希望笔记仓库能通过 Git 协议同步, +以便利用 Git 的版本历史随时回溯笔记变更。 +``` + +--- + +## 4. 功能规划 + +### 4.1 MVP(Phase 1)— 基础编辑器 + +**目标:可用的本地 Markdown 编辑器,核心编辑体验对齐 Typora。** + +| 模块 | 功能点 | 优先级 | +|------|--------|--------| +| 编辑器 | 所见即所得渲染 | P0 | +| 编辑器 | 标题(H1-H6) | P0 | +| 编辑器 | 加粗、斜体、删除线、行内代码 | P0 | +| 编辑器 | 有序/无序列表、嵌套列表 | P0 | +| 编辑器 | 引用块 | P0 | +| 编辑器 | 分隔线 | P0 | +| 编辑器 | 链接与图片 | P0 | +| 编辑器 | 代码块(有/无语言标注) | P0 | +| 编辑器 | 表格(增删行列、对齐) | P1 | +| 文件管理 | 新建/打开/保存 .md 文件 | P0 | +| 文件管理 | 文件树侧边栏(浏览目录) | P0 | +| 文件管理 | 最近打开的文件列表 | P1 | +| 窗口 | 自定义标题栏(无系统边框) | P0 | +| 主题 | 亮色/暗色主题切换 | P0 | +| 导出 | 导出为 HTML | P1 | + +### 4.2 Phase 2 — 同步引擎 v1 + +**目标:S3 协议同步上线,实现多设备笔记互通。** + +| 模块 | 功能点 | 优先级 | +|------|--------|--------| +| 同步核心 | S3 协议连接配置(Endpoint / Bucket / AK / SK) | P0 | +| 同步核心 | 增量同步(仅上传/下载变更文件) | P0 | +| 同步核心 | 冲突检测(同名文件被两台设备同时修改) | P0 | +| 同步核心 | 手动触发同步 + 定时自动同步 | P0 | +| 同步核心 | 同步状态指示(同步中/已同步/冲突/错误) | P0 | +| 图片同步 | 图片上传到 S3,自动替换为 S3 URL | P1 | +| 图片同步 | 图片相对路径引用方案 | P1 | +| 图片同步 | 粘贴图片自动上传 + 生成引用 | P1 | +| 体验 | 首次配置向导 | P1 | + +### 4.3 Phase 3 — 高级编辑 + +**目标:编辑能力对齐 Typora,满足专业写作需求。** + +| 模块 | 功能点 | 优先级 | +|------|--------|--------| +| 代码 | 语法高亮(100+ 语言) | P0 | +| 数学 | KaTeX/LaTeX 公式渲染 | P0 | +| 图表 | Mermaid 流程图、甘特图、序列图 | P1 | +| 图表 | PlantUML 可选支持 | P2 | +| 导航 | 文档大纲(根据标题自动生成) | P0 | +| 导航 | 大纲点击跳转到对应段落 | P0 | +| 表格 | 可视化表格编辑器(拖拽调整列宽) | P1 | +| 表格 | 表格内公式计算 | P2 | +| 导出 | 导出 PDF(带样式) | P0 | +| 导出 | 导出 Word (.docx) | P1 | +| 导出 | 导出图片 (.png) | P2 | +| 脚注 | 脚注/尾注 | P2 | +| 任务 | 任务列表(checkboxes) | P1 | +| 目录 | TOC 自动生成 | P1 | + +### 4.4 Phase 4 — 多协议同步 + +**目标:扩展同步协议,覆盖更多使用场景。** + +| 协议 | 功能说明 | 优先级 | +|------|---------|--------| +| WebDAV | 支持坚果云、NextCloud、自建 WebDAV 服务器 | P0 | +| Git | 同步到 GitHub/GitLab/Gitee 仓库,支持版本历史浏览 | P1 | +| WebRTC | 局域网点对点直连同步(无需中央服务器,极低延迟) | P1 | +| SMB/NFS | 局域网共享文件夹模式,检测文件变更自动刷新 | P2 | +| OneDrive | 利用系统 OneDrive 目录的自动同步能力 | P2 | +| iCloud | macOS 上利用 iCloud Drive 目录 | P2 | + +### 4.5 Phase 5 — 体验打磨与生态 + +**目标:完整的桌面应用体验,建立插件生态。** + +| 模块 | 功能点 | 优先级 | +|------|--------|--------| +| 标签页 | 多标签页管理文件 | P0 | +| 标签页 | 标签页拖拽排序、分离窗口 | P1 | +| 搜索 | 全文搜索(文件名+文件内容) | P0 | +| 搜索 | 正则搜索 | P1 | +| 搜索 | 全局搜索与替换 | P1 | +| 快捷键 | 完整快捷键体系(可自定义) | P0 | +| 插件 | 插件系统(JS/TS 编写) | P1 | +| 插件 | 插件市场 | P2 | +| 主题 | 自定义 CSS 主题 | P0 | +| 主题 | 主题市场 / 社区分享 | P2 | +| 国际化 | 中英文界面 | P0 | +| 国际化 | 社区贡献更多语言 | P2 | +| 无障碍 | 屏幕阅读器支持、键盘导航 | P2 | + +--- + +## 5. 技术架构 + +### 5.1 整体架构图 + +``` +┌──────────────────────────────────────────────────┐ +│ 前端 (Vue 3) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ +│ │ 编辑器组件 │ │ 文件树 │ │ 设置/同步管理 │ │ +│ │ ProseMirror│ │ 组件 │ │ 组件 │ │ +│ └─────┬─────┘ └─────┬─────┘ └────────┬─────────┘ │ +│ │ │ │ │ +├────────┼─────────────┼───────────────┼─────────────┤ +│ │ Tauri IPC Bridge (invoke) │ │ +├────────┼─────────────┼───────────────┼─────────────┤ +│ 后端 (Rust) │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Command Layer │ │ +│ │ read_file / write_file / list_dir / ... │ │ +│ ├──────────────┬───────────────┬───────────────┤ │ +│ │ 文件管理模块 │ 同步引擎 │ 导出模块 │ │ +│ │ - 文件IO │ - S3 Client │ - PDF │ │ +│ │ - 目录监听 │ - Diff 引擎 │ - HTML │ │ +│ │ - 图片处理 │ - 冲突解决 │ - DOCX │ │ +│ ├──────────────┴───────────────┴───────────────┤ │ +│ │ Platform Abstraction │ │ +│ │ (文件系统 / 网络 / 系统托盘 / 快捷键) │ │ +│ └──────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────┘ +``` + +### 5.2 前端技术选型 + +| 组件 | 技术选型 | 选型理由 | +|------|---------|---------| +| 框架 | Vue 3 + Composition API | 与 Tauri 生态结合好,轻量灵活 | +| 语言 | TypeScript | 类型安全,大型项目必备 | +| UI 库 | Naive UI | Vue3 高颜值 UI 库,TypeScript 原生支持 | +| Markdown 编辑器 | ProseMirror / Milkdown | 插件化架构,WYSIWYG 能力强 | +| 代码高亮 | Shiki / Prism.js | 服务端级语法高亮 | +| 数学渲染 | KaTeX | 比 MathJax 更快 | +| 图表 | Mermaid.js | 标准 Markdown 图表方案 | +| 打包 | Vite 6 | 极速 HMR,生态完善 | + +### 5.3 Rust 后端技术选型 + +| 模块 | 选型 | 说明 | +|------|------|------| +| 框架 | Tauri 2 | 桌面框架,Rust 后端 | +| S3 客户端 | rust-s3 / aws-sdk-s3 | S3 协议实现 | +| WebDAV 客户端 | reqwest + 自实现 | HTTP 协议扩展 | +| Git | git2-rs | libgit2 绑定 | +| 文件监听 | notify | 跨平台文件变更监听 | +| Markdown 解析 | pulldown-cmark | 高性能 Markdown 解析器 | +| PDF 生成 | printpdf / genpdf | Rust 原生 PDF 生成 | +| 序列化 | serde + serde_json | 配置与 IPC 通信 | +| 加密 | ring / aes-gcm | 远程凭据加密存储 | +| 压缩 | flate2 | gzip 压缩(WebDAV) | + +### 5.4 前端与后端通信设计 + +```rust +// Rust 侧 - 定义 Tauri Command +#[tauri::command] +async fn read_file(path: String) -> Result { + std::fs::read_to_string(&path).map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn sync_to_s3( + config: S3Config, + files: Vec, +) -> Result { + // 同步逻辑 +} +``` + +```typescript +// 前端侧 - 调用 Tauri Command +import { invoke } from '@tauri-apps/api/core'; + +const content = await invoke('read_file', { path: '/path/to/file.md' }); +const result = await invoke('sync_to_s3', { config, files }); +``` + +### 5.5 Tauri Capabilities 权限设计 + +```json +{ + "identifier": "default", + "windows": ["main"], + "permissions": [ + "core:default", + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "fs:allow-read-dir", + "fs:allow-exists", + "dialog:allow-open", + "dialog:allow-save", + "http:default" + ] +} +``` + +--- + +## 6. UI/UX 设计规范 + +### 6.1 设计原则 + +1. **沉浸优先** — 编辑器占据视觉重心,Chrome 尽可能少 +2. **所见即所得** — 光标所在行的 Markdown 语法标记即时隐藏,只展示渲染结果 +3. **无干扰** — 默认隐藏工具栏,聚焦模式更进一步隐藏侧边栏 +4. **反馈及时** — 同步状态、保存状态始终可见,不超过 200ms 延迟给出反馈 +5. **渐进披露** — 高级功能隐藏在二级菜单,不干扰日常使用 + +### 6.2 布局方案 + +``` +┌─────────────────────────────────────────┐ +│ 自定义标题栏 (拖拽区 + 窗口控制 + Logo) │ +├────────┬────────────────────────────────┤ +│ 侧边栏 │ │ +│ │ │ +│ 文件树 │ 编辑器主区域 │ +│ │ (所见即所得 Markdown) │ +│ 大纲 │ │ +│ │ │ +│ 同步状态│ │ +│ │ │ +├────────┴────────────────────────────────┤ +│ 状态栏 (文件路径 | 字数 | 同步状态 | 光标位置) │ +└─────────────────────────────────────────┘ +``` + +### 6.3 三种视图模式 + +| 模式 | 描述 | 使用场景 | +|------|------|---------| +| **编辑模式(默认)** | 所见即所得,打字时隐藏 Markdown 标记 | 日常写作 | +| **源码模式** | 显示原始 Markdown 文本 | 精确控制格式、调试 | +| **阅读模式** | 纯渲染,不可编辑 | 阅读长文、演示 | + +### 6.4 色彩方案 + +- **亮色主题**:背景 #FFFFFF,文字 #333333,强调色 #2080F0 +- **暗色主题**:背景 #1E1E1E,文字 #D4D4D4,强调色 #4FC1FF +- **Sepia 主题**:背景 #FBF0D9,文字 #5F4B32(类 Kindle) + +--- + +## 7. 数据结构设计 + +### 7.1 笔记本(Workspace)配置 + +```json +{ + "workspaces": [ + { + "id": "ws-001", + "name": "我的笔记", + "local_path": "~/Documents/WriteFlow/notes", + "sync": { + "enabled": true, + "protocol": "s3", + "config": { + "endpoint": "https://s3.amazonaws.com", + "bucket": "my-notes-bucket", + "region": "us-east-1", + "prefix": "writeflow/" + }, + "auto_sync_interval_secs": 300, + "conflict_strategy": "keep_both" + } + } + ] +} +``` + +### 7.2 同步元数据 + +```json +{ + "file_sync_meta": { + "notes/getting-started.md": { + "last_local_modified": "2026-07-15T10:30:00Z", + "local_hash": "sha256:abc123...", + "last_remote_modified": "2026-07-15T09:00:00Z", + "remote_hash": "sha256:abc123...", + "sync_status": "synced" + }, + "notes/draft.md": { + "last_local_modified": "2026-07-15T10:35:00Z", + "local_hash": "sha256:def456...", + "last_remote_modified": "2026-07-15T09:00:00Z", + "remote_hash": "sha256:old789...", + "sync_status": "local_newer" + } + } +} +``` + +### 7.3 图片存储方案 + +``` +笔记目录/ +├── notes/ +│ ├── article.md +│ └── images/ +│ └── article/ +│ ├── screenshot-01.png +│ └── diagram-02.png +``` + +Markdown 中引用方式: + +```markdown +![截图](images/article/screenshot-01.png) +``` + +同步时策略: +- **S3 模式**:图片上传到 `s3://bucket/prefix/notes/images/article/screenshot-01.png`,本地保留原始文件 +- **图片引用不做 URL 替换**,保持相对路径,确保本地和远程均可渲染 + +--- + +## 8. 同步引擎设计 + +### 8.1 同步流程 + +``` +用户操作 / 定时触发 + │ + ▼ + ┌─ 计算本地文件 Hash ─┐ + │ │ + ▼ ▼ +┌──────────┐ ┌──────────────┐ +│ 本地变更 │ │ 获取远程文件列表 │ +│ 文件列表 │ │ (S3 ListObjects)│ +└────┬─────┘ └──────┬───────┘ + │ │ + └───────┬───────────┘ + ▼ + ┌──────────────┐ + │ Diff 对比 │ + │ 三元对比: │ + │ 本地 / 远程 │ + │ / 上次同步基线│ + └──────┬───────┘ + │ + ┌───────┼───────┐ + ▼ ▼ ▼ + 仅本地上传 仅远程下载 冲突 +(Upload) (Download) (Conflict) + │ │ │ + └───────┼───────┘ + ▼ + ┌──────────────┐ + │ 更新同步基线 │ + │ (保存到本地) │ + └──────────────┘ +``` + +### 8.2 冲突处理策略 + +| 策略 | 描述 | 适用场景 | +|------|------|---------| +| `keep_local` | 以本地版本为准,覆盖远程 | 确定本地是最新 | +| `keep_remote` | 以远程版本为准,覆盖本地 | 刚换了新设备 | +| `keep_both` | 保留冲突副本 `filename_conflict_20260715.md` | 不确定优先级的默认策略 | +| `manual_merge` | 弹出对比界面,用户手动合并 | 重要文件 | + +### 8.3 安全性 + +- **凭据加密**:S3 的 AccessKey/SecretKey 使用 AES-256-GCM 加密存储在本机 Keychain(macOS Keychain / Windows Credential Manager / Linux Secret Service) +- **传输加密**:HTTPS/TLS 加密传输(S3 SDK 默认开启) +- **本地文件不动**:同步失败不会删除或修改本地文件 + +### 8.4 同步协议抽象设计 + +```rust +// 统一的同步接口 +#[async_trait] +pub trait SyncProtocol { + async fn connect(&self, config: SyncConfig) -> Result<(), SyncError>; + async fn list_files(&self, prefix: &str) -> Result, SyncError>; + async fn upload(&self, local_path: &Path, remote_key: &str) -> Result<(), SyncError>; + async fn download(&self, remote_key: &str, local_path: &Path) -> Result<(), SyncError>; + async fn delete(&self, remote_key: &str) -> Result<(), SyncError>; + async fn get_metadata(&self, remote_key: &str) -> Result; +} + +// 具体实现 +pub struct S3SyncEngine { /* ... */ } +pub struct WebDAVSyncEngine { /* ... */ } +pub struct GitSyncEngine { /* ... */ } +pub struct WebRTCSyncEngine { /* ... */ } +``` + +--- + +## 9. 开发排期与里程碑 + +### 9.1 整体时间线 + +``` +2026 Q3 2026 Q4 2027 Q1 2027 Q2 +├──────┼──────────┼───────┼────────┼───────┼────────┼────── +│ │ │ │ │ │ │ +Phase 1 Phase 2 Phase 3 Phase 4 +(MVP) (S3同步) (高级编辑) (多协议同步) +8 周 6 周 8 周 6 周 +``` + +### 9.2 Phase 1 MVP 详细排期(8 周) + +| 周次 | 里程碑 | 交付物 | +|------|--------|--------| +| W1 | 编辑器基础 | Milkdown 集成,基础 Markdown 实时渲染 | +| W2 | 编辑器进阶 | 代码块、表格、链接、图片、任务列表 | +| W3 | 文件系统 | 文件树侧边栏、新建/打开/保存文件 | +| W4 | 文件管理 | 最近文件、自动保存、文件监听刷新 | +| W5 | 主题系统 | 亮色/暗色主题、CSS 变量体系 | +| W6 | 导出功能 | HTML 导出、PDF 导出 | +| W7 | 打磨与测试 | 快捷键、Bug 修复、性能优化 | +| W8 | 发布 MVP | 打包、签名、发布 GitHub Release | + +### 9.3 Phase 2 S3 同步详细排期(6 周) + +| 周次 | 里程碑 | 交付物 | +|------|--------|--------| +| W9-W10 | S3 连接层 | S3 Client 实现、连接配置 UI | +| W11 | 同步核心 | Hash 计算、Diff 引擎、上传/下载 | +| W12 | 冲突处理 | 冲突检测、三种冲突策略、状态 UI | +| W13 | 图片同步 | 图片上传 + 引用管理 | +| W14 | 打磨发布 | 首次配置向导、同步日志、发布 v0.2 | + +### 9.4 关键指标 + +| 指标 | 目标值 | +|------|--------| +| 冷启动时间 | < 2 秒 | +| 空闲内存占用 | < 100 MB | +| 打开 1000 行 .md 文件 | < 500ms | +| S3 同步 100 个文件 | < 30 秒(首次)/< 5 秒(增量) | +| 冲突检测准确率 | 100%(不应有静默覆盖) | + +--- + +## 10. 质量保障与测试策略 + +### 10.1 测试金字塔 + +``` + ┌───────┐ + │ E2E │ 端到端测试 (Playwright) + │ 10% │ + ┌┴───────┴┐ + │ 集成测试 │ 前后端联调 + 同步集成 + │ 30% │ + ┌┴─────────┴┐ + │ 单元测试 │ Rust + Vitest + │ 60% │ + └────────────┘ +``` + +### 10.2 各层测试策略 + +| 层级 | 工具 | 覆盖重点 | +|------|------|---------| +| Rust 单元测试 | `cargo test` | 文件操作、Hash 计算、同步协议逻辑 | +| Rust 集成测试 | `cargo test --test` | S3 客户端连接、同步引擎完整流程 | +| 前端单元测试 | Vitest + Vue Test Utils | 组件渲染、编辑器 API、状态管理 | +| E2E 测试 | Playwright | 核心用户路径(新建-编辑-保存-同步-导出) | + +### 10.3 关键测试用例 + +- [ ] 创建新文件 → 编辑内容 → Ctrl+S 保存 → 文件系统验证内容正确 +- [ ] 粘贴图片 → 图片保存到相对路径 → Markdown 引用正确 +- [ ] 配置 S3 → 点击同步 → 文件出现在 S3 Bucket +- [ ] 两台设备修改同一文件 → 同步 → 检测到冲突 → 生成冲突副本 +- [ ] 导出 PDF → 样式正确、图片完整、中文正常显示 +- [ ] 暗色主题切换 → 编辑器/侧边栏/状态栏全部切换 + +--- + +## 11. 发布与运营策略 + +### 11.1 发布平台 + +| 平台 | 渠道 | 说明 | +|------|------|------| +| GitHub | Releases | 主要发布渠道,附带 Release Notes | +| macOS | Homebrew Cask | `brew install writeflow` | +| Windows | Winget / Chocolatey | 包管理器分发 | +| Linux | AppImage / Snap / Flatpak | 主流格式覆盖 | +| 官网 | writeflow.app | 下载页 + 文档 | + +### 11.2 版本策略 + +- 采用语义化版本 `MAJOR.MINOR.PATCH` +- 偶数次版本号为稳定版,奇数次为开发版 +- 每 4-6 周发布一个 MINOR 版本 + +### 11.3 开源策略 + +- **License**:MIT(最大化社区采用) +- **贡献指南**:CONTRIBUTING.md + Issue Template + PR Template +- **文档站点**:VitePress 搭建,托管于 GitHub Pages +- **社区**:GitHub Discussions + Discord Server + +### 11.4 商业化(长期可选) + +| 方式 | 说明 | 时机 | +|------|------|------| +| 完全免费开源 | 核心功能永久免费 | 从 Phase 1 起 | +| 赞助 | GitHub Sponsors / Open Collective | Phase 3 后 | +| 增值服务 | 官方托管同步服务(WriteFlow Sync) | Phase 5 后 | +| 企业版 | SAML/SSO、审计日志、集中管理 | Phase 5 后 | + +--- + +## 12. 风险评估与应对 + +| 风险 | 影响 | 概率 | 应对措施 | +|------|------|------|---------| +| ProseMirror/Milkdown 集成复杂度超出预期 | 延期 2-4 周 | 中 | 备选方案:使用 markdown-it 双栏模式先上线 | +| S3 SDK 某些平台兼容性问题 | 同步功能不可用 | 低 | 多测试主力 S3 服务商,抽象接口可快速替换 | +| 文件系统同步冲突数据丢失 | 用户数据丢失 | 低 | 默认 `keep_both` 策略,绝不静默删除文件 | +| Tauri 2.x 大版本 API 变更 | 编译失败 | 中 | 锁定 Tauri 版本,定期评估升级风险 | +| 竞品(Typora 复活/Obsidian 推出 WYSIWYG) | 差异化被削弱 | 中 | 主打同步能力作为核心壁垒 | +| Linux 桌面碎片化导致兼容问题 | 部分 Linux 用户不可用 | 高 | AppImage 一份构建覆盖主流发行版 | + +--- + +## 附录 A:参考资源 + +- [Tauri v2 官方文档](https://v2.tauri.app/) +- [Milkdown - WYSIWYG Markdown Editor](https://milkdown.dev/) +- [ProseMirror 官方指南](https://prosemirror.net/docs/guide/) +- [Typora 产品设计参考](https://typora.io/) +- [S3 API 规范](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) +- [WebDAV RFC 4918](https://www.rfc-editor.org/rfc/rfc4918) +- [Naive UI](https://www.naiveui.com/) + +## 附录 B:命名备选 + +| 名称 | 含义 | 可用性 | +|------|------|--------| +| WriteFlow | 写作流动 | 暂未注册 | +| MarkNote | Markdown + Note | 暂未注册 | +| InkWell | 墨池 | 暂未注册 | +| Scripta | 拉丁文"写作" | GitHub 同名项目存在 | +| TypeStone | 刻字石 | 暂未注册 | +| **WriteFlow** ✅ | 最终选择 | — | + +--- + +> 文档维护:产品团队 +> 下次评审时间:Phase 1 MVP 完成后 diff --git a/tauri-app/开发迭代计划.md b/tauri-app/开发迭代计划.md new file mode 100644 index 0000000..7c88efe --- /dev/null +++ b/tauri-app/开发迭代计划.md @@ -0,0 +1,390 @@ +# WriteFlow 开发提示词(迭代式开发指南) + +> 本指南基于 [README.md](./README.md) 与 [产品策划开发手册](./产品策划开发手册.md) 制定,采用**增量迭代**方式推进。每个阶段均有明确的交付物、技术要点、验收标准,确保代码质量、功能完整性和用户体验一致性。 +> **UI 开发必须遵循 `/ui-ux-pro-max` 规范**(即符合产品设计原则、使用 Naive UI 组件库、响应式布局、暗色/亮色主题无缝切换)。 + +--- + +## 总体开发原则 + +1. **本地优先,数据安全**:所有笔记以 `.md` 文件存储于本地,同步仅作为附加能力,不得破坏本地数据。 +2. **所见即所得为核心体验**:编辑器必须达到 Typora 级别的流畅度和渲染效果。 +3. **渐进增强**:先保证核心编辑与文件管理可用,再叠加同步、高级功能。 +4. **跨平台一致性**:在 Windows/macOS/Linux 上行为一致,UI 适配各平台习惯(如菜单栏、快捷键)。 +5. **代码规范**: + - 前端:TypeScript + ESLint + Prettier(Vue 官方风格指南) + - 后端:Rust 1.70+,遵循 Rustfmt + Clippy,模块化设计,错误处理清晰。 +6. **测试覆盖**:关键路径(文件读写、同步、冲突处理)必须有单元测试和集成测试。 +7. **性能目标**:冷启动 < 2s,打开 1000 行文件 < 500ms,空闲内存 < 100MB。 + +--- + +## Phase 1 – MVP 基础编辑器(8 周) + +**目标**:提供可用的本地 Markdown 所见即所得编辑器,包含文件管理、基础主题、导出 HTML/PDF。 +**UI/UX 依据**:设计规范第 6 章(沉浸优先、无干扰、反馈及时),使用 Naive UI 组件(布局、按钮、弹窗、消息提示)。 + +--- + +### 迭代 1.1 – 项目骨架与编辑器内核(W1) + +#### 任务清单 +- [ ] **初始化 Tauri + Vue 3 + TypeScript 项目** + - 使用 `create-tauri-app` 选择 Vue 3 + TypeScript。 + - 配置 `vite.config.ts`,启用别名 `@` 指向 `src`。 + - 安装依赖:`naive-ui`、`milkdown`(含 `@milkdown/vue`)、`@milkdown/theme-nord`(或其他主题)、`@milkdown/preset-commonmark`。 +- [ ] **集成 Milkdown 编辑器组件** + - 在 `App.vue` 或独立 `Editor.vue` 中挂载 Milkdown 编辑器。 + - 配置基本 CommonMark 预设,实现标题、加粗、斜体、列表、引用、代码块、链接、图片、表格(基础)的实时渲染。 + - 关闭 Milkdown 的工具栏,实现**纯键盘驱动**的所见即所得(光标所在行自动隐藏标记符号)。 +- [ ] **实现自定义标题栏(无系统边框)** + - 修改 `tauri.conf.json`:`"windows"` 配置 `"decorations": false`。 + - 在 Vue 中绘制标题栏,包含应用 Logo、标题(当前文件名)、窗口控制按钮(最小化、最大化、关闭)。 + - 实现窗口拖拽区域:使用 Tauri `data-tauri-drag-region` 属性。 +- [ ] **基础的亮色/暗色主题** + - 定义 CSS 变量(色值参考设计规范 6.4)。 + - 提供顶部菜单或设置项切换主题,利用 Naive UI 的 `n-config-provider` 配合自定义类。 + +#### 关键技术点 +- Milkdown 编辑器的 `editor` 实例管理(通过 `ref` 获取,用于后续内容读写)。 +- Tauri 的 `window` API 控制窗口大小和位置。 +- 使用 `@tauri-apps/api/event` 监听系统主题变更(可选)。 + +#### 验收标准 +- [ ] 启动后编辑器可输入,Markdown 语法正确渲染。 +- [ ] 标题栏可拖拽移动窗口,控制按钮功能正常。 +- [ ] 主题切换后编辑器背景、文字、边框颜色同步变化。 +- [ ] 无控制台报错,内存占用 < 50MB(空文档)。 + +--- + +### 迭代 1.2 – 文件系统集成(W2–W3) + +#### 任务清单 +- [ ] **实现文件树侧边栏** + - 使用 Naive UI 的 `n-tree` 组件,绑定目录结构数据。 + - 通过 Tauri `fs` 模块(`read_dir`)递归读取工作区目录。 + - 支持展开/折叠文件夹,点击 `.md` 文件在编辑器中打开。 +- [ ] **新建/打开/保存文件** + - 新建:生成 `untitled.md`,弹窗让用户重命名(或自动生成时间戳文件名)。 + - 打开:使用 `dialog.open()` 选择 `.md` 文件,读取内容并渲染。 + - 保存:使用 `fs.writeFile()`,支持 `Ctrl+S` 快捷键,同时自动保存到当前文件。 +- [ ] **最近打开文件列表** + - 使用 `localStorage` 或 Tauri `store` 插件存储最近 10 个文件路径。 + - 在菜单或侧边栏显示快速入口。 +- [ ] **工作区管理(单工作区 MVP)** + - 首次启动引导用户选择笔记目录(`dialog.open({ directory: true })`)。 + - 将路径持久化(`store` 或配置文件),后续启动自动加载该目录。 + +#### 技术要点 +- 文件读写使用 Tauri 的 `tauri::fs` 和 `tauri::dialog` 模块,注意权限配置(`capabilities`)。 +- 文件树数据使用 `ref` 响应式,结合 `watch` 监听目录变更(后续可升级为 `notify` 后端监听)。 +- 编辑器内容与文件路径双向绑定:打开文件 → 设置 `editor` 内容,保存时读取 `editor` 内容写入文件。 + +#### 验收标准 +- [ ] 文件树正确显示目录及 `.md` 文件,点击文件立即在编辑器中打开。 +- [ ] 新建文件后保存,文件出现在文件树中。 +- [ ] 最近文件列表正确记录,重启后依然存在。 +- [ ] 打开非文本文件或非 MD 文件时,给出友好提示。 + +--- + +### 迭代 1.3 – 编辑体验增强(W4) + +#### 任务清单 +- [ ] **自动保存与手动保存状态** + - 每隔 30 秒自动保存当前文件(若有更改)。 + - 状态栏显示“已保存”或“正在保存...”图标(Naive UI `n-icon`)。 +- [ ] **快捷键体系(核心)** + - `Ctrl+N` 新建,`Ctrl+O` 打开,`Ctrl+S` 保存,`Ctrl+Shift+S` 另存为。 + - `Ctrl+Z`/`Ctrl+Y` 撤销/重做(Milkdown 内置)。 + - `Ctrl+F` 查找(后续迭代)。 + - 使用 Tauri `globalShortcut` 或前端 `@vueuse/core` 的 `useMagicKeys`。 +- [ ] **字数统计与文档信息** + - 状态栏显示当前文档字数(中英文混合计数)、行数、光标位置。 + - 使用 `@milkdown/utils` 的 `prose` 或自行解析 AST 统计。 +- [ ] **焦点模式(可选)** + - 快捷键 `F11` 或菜单切换:隐藏侧边栏,编辑器全屏。 + - 使用 CSS 动画过渡,保持平滑。 + +#### UI/UX 规范检查 +- 状态栏位于底部,信息左对齐,同步状态(尚无)预留位置。 +- 快捷键提示可悬浮显示(使用 `n-tooltip`)。 + +#### 验收标准 +- [ ] 自动保存生效,关闭窗口或切换文件时不丢失编辑内容。 +- [ ] 核心快捷键全平台(Win/Cmd 适配)正常。 +- [ ] 字数统计准确(英文单词、中文字符、总字符)。 +- [ ] 焦点模式切换流畅,无闪烁。 + +--- + +### 迭代 1.4 – 主题与导出(W5–W6) + +#### 任务清单 +- [ ] **完整的亮色/暗色主题切换** + - 提供至少三种配色:默认亮色、默认暗色、Sepia(暖色)。 + - 使用 Naive UI 的 `n-config-provider` 全局管理主题,同时自定义编辑器样式(Milkdown 的样式覆盖)。 +- [ ] **导出 HTML** + - 将当前 Markdown 内容转换为 HTML(可借助 Milkdown 的 `getMarkdown` + `markdown-it` 或直接使用 Milkdown 渲染结果)。 + - 打包为一个自包含的 `.html` 文件(含样式),通过 Tauri `dialog.save()` 选择保存位置。 +- [ ] **导出 PDF** + - 方案一:使用 `window.print()` 配合 `@media print` 样式实现打印为 PDF(跨平台简单)。 + - 方案二:使用 Rust 后端(`printpdf`)生成 PDF,更专业但复杂度高。MVP 采用方案一,后续替换。 + - 导出前确保样式一致(保留主题)。 +- [ ] **设置存储** + - 使用 Tauri `store` 插件(或 `confy` crate)保存用户偏好:主题、最近工作区、自动保存间隔等。 + +#### 技术要点 +- 导出 HTML 时,需内联主题 CSS 和 Milkdown 渲染样式,确保外部打开显示正常。 +- PDF 导出使用 Tauri 的 `WebviewWindow` 的 `print` 方法或调用系统打印对话框。 + +#### 验收标准 +- [ ] 三种主题切换后,编辑器、侧边栏、状态栏颜色同步。 +- [ ] 导出 HTML 文件,在浏览器中打开与编辑器内显示一致。 +- [ ] 导出 PDF 内容完整,中文不乱码,图片(若有)正常显示。 +- [ ] 用户设置(主题、最近工作区)在重启后保留。 + +--- + +### 迭代 1.5 – 错误处理与基础测试(W7–W8) + +#### 任务清单 +- [ ] **全局错误处理** + - 前端:Vue 全局错误处理器(`app.config.errorHandler`)捕获并显示友好提示(`n-notification`)。 + - Rust:使用 `anyhow` 或自定义错误类型,Tauri Command 返回 `Result`,错误信息通过 `invoke` 传递到前端。 +- [ ] **文件操作异常场景** + - 文件被外部修改(只读、删除)时的提示。 + - 磁盘空间不足、权限错误等提示。 +- [ ] **单元测试(Rust)** + - 文件读写函数、Hash 计算、路径处理等工具函数。 + - 使用 `cargo test`,覆盖率 > 70%。 +- [ ] **前端单元测试(Vitest)** + - 测试文件树组件、编辑器组件(通过 Mock 编辑器 API)。 +- [ ] **E2E 测试(Playwright)** + - 核心路径:启动 → 新建文件 → 输入内容 → 保存 → 重新打开 → 内容一致。 + - 导出 HTML 和 PDF 可验证文件生成。 + +#### 验收标准 +- [ ] 所有错误场景有对应的用户提示,不崩溃。 +- [ ] 单元测试全部通过。 +- [ ] E2E 测试通过。 +- [ ] 打包后的应用在 Windows/macOS/Linux 上可运行(基础功能)。 + +--- + +### Phase 1 交付物 +- 可安装的桌面应用(`.msi`/`.dmg`/`.AppImage`)。 +- 源码仓库,包含完整的开发文档(README、CONTRIBUTING)。 +- 项目网站(VitePress)上线,包含下载链接和用户手册。 + +--- + +## Phase 2 – S3 同步引擎(6 周) + +**目标**:实现基于 S3 协议的云端同步,支持手动/自动同步、冲突检测、图片上传。 + +--- + +### 迭代 2.1 – S3 连接层(W9–W10) + +#### 任务清单 +- [ ] **添加 Rust S3 客户端** + - 依赖 `aws-sdk-s3` 或 `rust-s3`,实现 `SyncProtocol` trait(参考设计文档 8.4)。 + - 实现 `connect`、`list_files`、`upload`、`download`、`delete`、`get_metadata` 方法。 +- [ ] **凭据安全存储** + - 使用 `keyring` crate(macOS/Windows/Linux 统一)或 Tauri 的 `store` 加密存储 AccessKey/SecretKey。 + - 前端提供配置表单(Endpoint、Bucket、Region、AccessKey、SecretKey),通过 Tauri Command 保存。 +- [ ] **连接测试** + - 提供“测试连接”按钮,验证凭据是否正确,Bucket 是否可访问。 + +#### 技术要点 +- 使用 `async`/`await`,超时控制(`tokio::time::timeout`)。 +- 处理 S3 的路径前缀(`prefix`)作为远程目录。 + +#### 验收标准 +- [ ] 配置正确的 S3 信息后,测试连接成功。 +- [ ] 凭据加密存储,无法在明文配置文件中读取。 +- [ ] 列出远程文件(含前缀)正常。 + +--- + +### 迭代 2.2 – 同步核心(W11) + +#### 任务清单 +- [ ] **计算文件 Hash** + - 使用 `sha2` 或 `blake3` 计算文件内容的哈希值。 + - 存储本地元数据:`last_modified`、`hash`、`sync_status`。 +- [ ] **Diff 引擎** + - 对比本地文件、远程文件、上次同步基线(存储在本地 `.writeflow/sync_meta.json`)。 + - 分类:`local_newer`、`remote_newer`、`both_changed`(冲突)、`identical`。 +- [ ] **上传/下载执行** + - 实现批量上传(并发控制,避免过多连接)。 + - 实现增量下载(仅下载变更文件)。 + - 进度反馈:通过 Tauri 事件向前端发送进度信息(`emit`)。 + +#### 技术要点 +- 使用 `tokio` 并发,控制并发数(如 5 个)。 +- 前端显示同步进度条(`n-progress`)。 + +#### 验收标准 +- [ ] 本地新增文件 → 同步后出现在远程。 +- [ ] 远程新增文件 → 同步后下载到本地。 +- [ ] 本地文件修改 → 同步后远程更新。 +- [ ] 远程文件修改 → 同步后本地更新。 + +--- + +### 迭代 2.3 – 冲突处理与状态 UI(W12) + +#### 任务清单 +- [ ] **冲突检测与策略** + - 实现三种策略:`keep_local`、`keep_remote`、`keep_both`(默认)。 + - `keep_both` 生成 `filename_conflict_YYYYMMDDHHMMSS.md`。 +- [ ] **同步状态 UI** + - 状态栏显示同步状态(空闲、同步中、冲突、错误)。 + - 文件树中每个文件显示同步图标(绿勾、橙色感叹号、红色叉)。 + - 点击状态可查看详细信息(使用 `n-drawer` 或 `n-modal`)。 +- [ ] **手动/自动同步** + - 提供“立即同步”按钮。 + - 定时同步(可配置间隔,默认 5 分钟)。 + +#### 验收标准 +- [ ] 两台设备修改同一文件,同步后生成冲突副本,原文件保留不丢失。 +- [ ] 状态指示器实时更新,冲突有明确提示。 +- [ ] 自动同步按设定间隔执行。 + +--- + +### 迭代 2.4 – 图片同步与首次配置向导(W13–W14) + +#### 任务清单 +- [ ] **图片自动上传** + - 监听编辑器中的图片插入(拖拽或粘贴),使用 Tauri `fs` 读取图片文件并上传至 S3。 + - 替换 Markdown 中的图片路径为相对路径(如 `images/xxx.png`),同时上传到远程对应路径。 +- [ ] **图片引用管理** + - 确保本地和远程图片目录结构一致。 +- [ ] **首次配置向导** + - 启动时检测是否已配置同步,若无则弹出向导(`n-steps`)。 + - 引导选择工作区、配置 S3 信息、测试连接。 +- [ ] **同步日志** + - 提供查看同步日志的界面(`n-log` 或 `n-textarea`),便于调试。 + +#### 验收标准 +- [ ] 粘贴图片到编辑器,自动上传到 S3,并在本地生成相对路径引用。 +- [ ] 在另一设备同步后,图片正常显示。 +- [ ] 向导流程完整,可跳过或稍后配置。 +- [ ] 同步日志记录关键操作。 + +--- + +### Phase 2 交付物 +- 完整的 S3 同步功能,支持公开云(AWS、阿里云等)和私有部署(MinIO)。 +- 冲突处理机制完善,数据安全有保障。 + +--- + +## Phase 3 – 高级编辑功能(8 周) + +**目标**:补齐 Typora 的高级能力,包括代码高亮、数学公式、图表、目录大纲、多种导出格式。 + +> 由于篇幅,此处仅列出关键迭代,细节可参考产品手册 4.3。 + +--- + +### 迭代 3.1 – 代码高亮与数学公式 +- 引入 `Shiki`(Rust 端可预高亮)或前端 `prism.js` 实现语法高亮。 +- 使用 `KaTeX` 渲染行内和块级公式。 +- 配置 Milkdown 插件:`@milkdown/plugin-math`(需等待适配)或自定义。 + +### 迭代 3.2 – Mermaid 图表与表格编辑器 +- 集成 `Mermaid.js`,在渲染阶段将代码块 `mermaid` 转换为 SVG。 +- 使用 Naive UI 的 `n-table` 构建可视化表格编辑器(弹窗形式),支持增删行列、对齐。 + +### 迭代 3.3 – 文档大纲与导航 +- 解析 Markdown 标题,在侧边栏或独立面板生成大纲树,点击跳转。 +- 使用 `@milkdown/plugin-toc` 或自行解析。 + +### 迭代 3.4 – 导出增强(PDF、Word、图片) +- 替换 `window.print()` 为 Rust `genpdf` 或 `html-to-pdf` 引擎,提升 PDF 质量。 +- 集成 `docx` crate 或调用 Pandoc(需用户安装)导出 Word。 +- 导出为 PNG(截图)。 + +--- + +## Phase 4 – 多协议同步(6 周) + +**目标**:支持 WebDAV、Git、WebRTC 等协议,扩展同步场景。 + +> 每个协议作为一个独立迭代,实现相同的 `SyncProtocol` trait,复用核心引擎。 + +--- + +### 迭代 4.1 – WebDAV +- 使用 `reqwest` 实现 PROPFIND、PUT、GET、DELETE。 +- 支持摘要认证和 OAuth2(如坚果云)。 + +### 迭代 4.2 – Git +- 集成 `git2-rs`,实现 clone、pull、push、commit。 +- 提供版本历史查看(类似 Git 日志)。 + +### 迭代 4.3 – WebRTC +- 使用 `webrtc-rs` 或通过 Tauri 调用前端 WebRTC,实现 P2P 直连同步。 +- 需要信令服务器(简单实现)。 + +--- + +## Phase 5 – 体验打磨与生态(持续) + +**目标**:多标签页、全文搜索、快捷键自定义、主题市场、插件系统。 + +--- + +### 迭代 5.1 – 多标签页 +- 使用 `n-tabs` 管理多个打开的文件,支持拖拽排序。 +- 每个标签页独立编辑器实例(或共享实例切换内容)。 + +### 迭代 5.2 – 全文搜索 +- 使用 `tauri` 后端遍历文件内容,结合 `tantivy` 或 `ripgrep` 实现高速搜索。 +- 前端结果列表,点击跳转并高亮匹配项。 + +### 迭代 5.3 – 快捷键自定义 +- 提供设置界面,使用 `n-dynamic-tags` 编辑快捷键映射。 +- 存储为 JSON,动态注册/注销。 + +### 迭代 5.4 – 插件系统 +- 基于 `deno` 或 `quickjs` 嵌入 JavaScript 引擎,允许用户编写插件扩展。 +- 提供插件 API(如 `onSave`、`onRender`)。 + +--- + +## 开发流程与质量保障 + +### 代码审查 +- 每次 PR 必须通过 CI(检查 lint、test、build)。 +- 至少一名 Reviewer 批准。 + +### 持续集成 +- GitHub Actions: + - 前端 lint + test + - Rust clippy + test + - 构建 Tauri 应用(仅 PR 检查,不发布) + +### 文档同步 +- 所有 API 变更更新 TSDoc/Rustdoc。 +- 用户手册同步更新(VitePress)。 + +--- + +## 附录:UI 开发规范(/ui-ux-pro-max) + +1. **使用 Naive UI 组件**:优先使用 `n-*` 组件,避免自定义基础组件(除非必要)。 +2. **响应式适配**:窗口缩放时编辑器宽度自适应,侧边栏可折叠。 +3. **暗色/亮色完全支持**:所有颜色使用 CSS 变量,随主题切换。 +4. **交互反馈**:操作成功/失败使用 `n-message` 或 `n-notification`,加载状态使用 `n-spin`。 +5. **可访问性**:使用语义化标签,键盘导航(Tab 顺序合理)。 +6. **布局**:遵循设计规范第 6 章,标题栏高度 32px,状态栏 28px,编辑器填充适当留白。 + +--- + +**开始开发**:请从 **Phase 1 – 迭代 1.1** 入手,完成后再推进后续。每个迭代完成后进行自测,并更新 CHANGELOG。祝开发顺利! \ No newline at end of file