Compare commits
2 Commits
docs
...
24f1549730
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24f1549730 | ||
| 4471441d6f |
@@ -93,32 +93,13 @@ export default defineConfig({
|
|||||||
items: [
|
items: [
|
||||||
{text: '简介', link: '/zh/guide/introduction'},
|
{text: '简介', link: '/zh/guide/introduction'},
|
||||||
{text: '安装', link: '/zh/guide/installation'},
|
{text: '安装', link: '/zh/guide/installation'},
|
||||||
{text: '快速开始', link: '/zh/guide/getting-started'},
|
{text: '快速开始', link: '/zh/guide/getting-started'}
|
||||||
{text: '界面总览', link: '/zh/guide/ui-overview'},
|
|
||||||
{text: '块语法与结构', link: '/zh/guide/block-syntax'}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '编辑与效率',
|
text: '功能特性',
|
||||||
items: [
|
items: [
|
||||||
{text: '键盘快捷键', link: '/zh/guide/keyboard-shortcuts'},
|
{text: '功能概览', link: '/zh/guide/features'}
|
||||||
{text: '多窗口与标签页', link: '/zh/guide/multiwindow-tabs'},
|
|
||||||
{text: '扩展与插件', link: '/zh/guide/extensions'},
|
|
||||||
{text: 'HTTP 客户端', link: '/zh/guide/http-client'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '个性化与数据',
|
|
||||||
items: [
|
|
||||||
{text: '设置与配置', link: '/zh/guide/settings'},
|
|
||||||
{text: '主题与外观', link: '/zh/guide/themes'},
|
|
||||||
{text: '备份与更新', link: '/zh/guide/backup-update'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '问题处理',
|
|
||||||
items: [
|
|
||||||
{text: '常见问题与故障排查', link: '/zh/guide/troubleshooting'}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,60 +0,0 @@
|
|||||||
# 备份与更新
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为备份设置、推送状态、更新提示的截图。
|
|
||||||
|
|
||||||
## Git 备份
|
|
||||||
`BackupService` 将 `dataPath` 转化为 Git 仓库,并提供自动/手动推送。
|
|
||||||
|
|
||||||
### 初始化
|
|
||||||
1. 在设置 > 备份中开启「启用备份」。
|
|
||||||
2. 填写远程仓库 URL(HTTPS 或 SSH)。
|
|
||||||
3. 选择认证方式:
|
|
||||||
- **Token**:适用于 GitHub/Gitea https 仓库。
|
|
||||||
- **SSH Key**:指定私钥路径和 passphrase。
|
|
||||||
- **用户名/密码**:适合自建 HTTP 仓库。
|
|
||||||
4. 点击“测试连接”(按钮在计划中,可先在终端测试)。
|
|
||||||
|
|
||||||
### 自动备份
|
|
||||||
- 勾选 “自动备份” + 设置 `BackupInterval`(分钟)。
|
|
||||||
- 服务会创建 ticker 定时 `git add -> commit -> push`。
|
|
||||||
- Commit 消息形如 `Auto backup <timestamp>`,包含 `voidraft.db`, `extensions.json`, `config.json`, `voidraft_data.bin`。
|
|
||||||
|
|
||||||
### 手动推送
|
|
||||||
- 打开工具栏消息中心或设置页点击“立即推送”。
|
|
||||||
- `backupStore.pushToRemote` 会显示状态气泡(成功/失败提示 3~5 秒)。
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
| 提示 | 解决 |
|
|
||||||
| --- | --- |
|
|
||||||
| `repository not found` | 检查 Repo URL 与权限,必要时创建空仓库 |
|
|
||||||
| `authentication required` | 选择正确认证方式,确认 token scope(需 repo 权限) |
|
|
||||||
| `auto backup stopped` | 查看日志,可能是网络不通或凭据失效;修改配置后服务会自动重启 |
|
|
||||||
|
|
||||||
## 自动更新
|
|
||||||
`SelfUpdateService` 负责检测、下载、应用新版。
|
|
||||||
|
|
||||||
### 检查更新
|
|
||||||
- 启动时若勾选 “自动更新” 会自动检查。
|
|
||||||
- 也可在设置 > 更新点击 “检查更新” 或在工具栏更新图标处触发。
|
|
||||||
- 服务优先访问 `primarySource`,失败时回退 `backupSource`。
|
|
||||||
|
|
||||||
### 下载与应用
|
|
||||||
1. 检测到更新后,界面提示版本号与变更信息(从 Release Notes 获取)。
|
|
||||||
2. 点击 “下载并安装” 后,后台执行下载,完成后提示“准备重启”。
|
|
||||||
3. 选择 “立即重启” 将调用 `RestartApplication`,自动重新打开上次的文档。
|
|
||||||
4. 若启用了 “更新前备份”,在下载前会触发一次 Git push。
|
|
||||||
|
|
||||||
### 失败处理
|
|
||||||
| 场景 | 建议 |
|
|
||||||
| --- | --- |
|
|
||||||
| 下载失败 | 检查网络/代理,切换至备用源 |
|
|
||||||
| 校验失败 | 删除 `%LOCALAPPDATA%/voidraft/update-cache` 再重试 |
|
|
||||||
| 应用后无法启动 | 从 Git 备份回滚数据,下载旧版本安装包覆盖 |
|
|
||||||
|
|
||||||
## 发布渠道
|
|
||||||
- 官方 GitHub Releases:`https://github.com/landaiqing/voidraft/releases`
|
|
||||||
- 自建 Gitea:`https://git.landaiqing.cn/landaiqing/voidraft`
|
|
||||||
- 可在设置中替换 Owner/Repo/BaseURL,以指向企业私有镜像。
|
|
||||||
|
|
||||||
> 建议将备份仓库设为私有,并在更新前后验证数据完整性。若要接入 S3/OSS 等备份方式,可关注 roadmap 或自行扩展。
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# 块语法与结构
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为展示分隔符(`∞∞∞language`)、块内容、语言标签的截图。
|
|
||||||
|
|
||||||
## 结构定义
|
|
||||||
每个块都由 **分隔符 + 内容** 组成:
|
|
||||||
|
|
||||||
```
|
|
||||||
∞∞∞language[-a]\n
|
|
||||||
<内容>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `language`:`lang-parser/languages.ts` 中的 token,例如 `text`、`javascript`、`python`、`md`、`http`、`math`、`sql`。
|
|
||||||
- `-a`:可选自动检测后缀,表示忽略显式语言,由 `lang-detect/autodetect.ts` 根据内容猜测。
|
|
||||||
- 内容允许空行;块之间无需额外空格。
|
|
||||||
|
|
||||||
`parser.ts` 会将块解析为:
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
language: { name: "javascript", auto: false },
|
|
||||||
delimiter: { from, to },
|
|
||||||
content: { from, to },
|
|
||||||
range: { from, to }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
这些字段供格式化、HTTP 运行、块操作等扩展示例使用。
|
|
||||||
|
|
||||||
## 快捷命令
|
|
||||||
| 命令 | 默认快捷键 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| blockAddAfterCurrent | `Ctrl+Enter` | 插入新块(下方) |
|
|
||||||
| blockAddBeforeCurrent | `Ctrl+Shift+Enter` | 插入新块(上方) |
|
|
||||||
| blockGotoPrevious/Next | `Alt+↑ / Alt+↓` | 在块之间跳转 |
|
|
||||||
| blockSelectAll | `Ctrl+Shift+A` | 选中当前块(含分隔符) |
|
|
||||||
| blockDelete | `Alt+Delete` | 删除整个块 |
|
|
||||||
| blockMoveUp/Down | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | 重排块顺序 |
|
|
||||||
| blockFormat | `Ctrl+Shift+F` | 针对当前块执行 Prettier |
|
|
||||||
|
|
||||||
## 语言与能力矩阵
|
|
||||||
| 语言 | 适配特性 |
|
|
||||||
| --- | --- |
|
|
||||||
| `text`/`note` | 基础文本,没有特殊扩展 |
|
|
||||||
| `md` | Markdown 预览、checkbox、高亮 |
|
|
||||||
| `javascript`/`typescript`/`json` | Prettier 格式化、彩虹括号、折叠、颜色选择 |
|
|
||||||
| `go`/`rust`/`python`/`java` | 高亮、折叠、自动缩进、语法跳转 |
|
|
||||||
| `http` | HTTP DSL、变量、响应插入(详见 [HTTP 客户端](/zh/guide/http-client))|
|
|
||||||
| `math` | `mathBlock` 运行器,支持 `prev` 引用上一次结果 |
|
|
||||||
| `sql`/`yaml`/`toml` | 语法高亮、格式化(由 Prettier/插件支持) |
|
|
||||||
|
|
||||||
> 若需要不在列表中的语言,可先使用 `text` 块输入,再在工具栏搜索语言名;也可以在 `lang-parser/languages.ts` 中添加条目。
|
|
||||||
|
|
||||||
## 自动检测策略
|
|
||||||
- 当分隔符以 `∞∞∞text-a` 写成时,`AUTO_DETECT_SUFFIX` 生效,`lang-detect` 会基于内容统计 + Levenshtein 距离预测语言。
|
|
||||||
- 自动结果会写入块状态,但不会覆盖分隔符原文,因此可通过工具栏明确指定。
|
|
||||||
|
|
||||||
## 特殊块
|
|
||||||
### HTTP 块
|
|
||||||
- 语法位于 `extensions/httpclient/language`,支持 `@var/@json/@form/@multipart` 等指令。
|
|
||||||
- 运行器会在块尾生成 `### Response`,包含状态码、耗时、headers、body。
|
|
||||||
|
|
||||||
### 数学块
|
|
||||||
- 语言设为 `math`,逐行计算。
|
|
||||||
- `prev` 变量表示上一行结果,可完成链式运算。
|
|
||||||
- 结果挂件可点击复制,或显示格式化/定点值。
|
|
||||||
|
|
||||||
### Markdown 块
|
|
||||||
- 工具栏中的 Preview 按钮会调用 `toggleMarkdownPreview`,右侧打开面板。
|
|
||||||
- 预览状态按文档隔离,不会影响其他文档。
|
|
||||||
|
|
||||||
## 分隔符校验
|
|
||||||
- `parser.ts` 暴露 `isValidDelimiter` 方法,格式错误时分隔符会以红色底纹标记。
|
|
||||||
- 复制/剪切操作会自动扩选到整个块,确保分隔符完整。
|
|
||||||
|
|
||||||
## 维护建议
|
|
||||||
- 保持每个逻辑主题占用一个块,并用 `md` 块写标题。
|
|
||||||
- 大量多语言内容时,可用 `text` + 自动检测,待语言确定后再改分隔符。
|
|
||||||
- 若文档出现“无法解析块”提示,可运行 `格式化文档` 或在命令面板触发“重建语法树”。
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 扩展与插件
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为展示扩展设置面板或功能合集(小地图、搜索、翻译)的截图。
|
|
||||||
|
|
||||||
voidraft 的扩展系统由 `internal/models/extensions.go` + 前端 `ExtensionManager` 驱动。扩展配置存储在 `%USERPROFILE%/.voidraft/data/extensions.json`,可在设置页面勾选启用或调整参数。
|
|
||||||
|
|
||||||
## 核心扩展
|
|
||||||
| 扩展 ID | 功能 | 关键文件 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `editor` | 基础 CodeMirror 行为、光标保护、滚轮缩放 | `frontend/src/views/editor/basic/*` |
|
|
||||||
| `codeblock` | 块解析、拖拽、复制、格式化、数学、HTTP DSL | `extensions/codeblock` |
|
|
||||||
| `vscodeSearch` | VSCode 风格搜索替换面板 | `extensions/vscodeSearch` |
|
|
||||||
| `markdownPreview` | Markdown 实时预览 | `extensions/markdownPreview` |
|
|
||||||
|
|
||||||
## 编辑增强
|
|
||||||
- **Rainbow Brackets (`rainbowBrackets`)**:彩虹色括号匹配。
|
|
||||||
- **Fold (`fold`)**:代码折叠/展开,支持 `Ctrl+Alt+[`/`]`。
|
|
||||||
- **Hyperlink (`hyperlink`)**:识别 URL/邮箱,`Ctrl+Click` 打开。
|
|
||||||
- **Color Selector (`colorSelector`)**:悬浮配色器,支持 HEX/RGB/HSL。
|
|
||||||
- **Checkbox (`checkbox`)**:Markdown 任务列表交互式勾选。
|
|
||||||
- **Text Highlight (`textHighlight`)**:`Mod+Shift+H` 快速标记重点,可自定义颜色/透明度。
|
|
||||||
|
|
||||||
## 工具扩展
|
|
||||||
- **Translator (`translator`)**:选区翻译;配置项包括默认翻译器、最短/最长字符数。后端集成 Bing/Google/Youdao/DeepL/TartuNLP。
|
|
||||||
- **Minimap (`minimap`)**:右侧迷你地图,支持悬浮/常驻、显示字符/块,突出当前选区。
|
|
||||||
- **Search (`search`)**:补充 VSCode 风格搜索,暴露命令给快捷键系统。
|
|
||||||
- **HTTP Client (`httpclient`)**:DSL + 运行器,详见 [HTTP 客户端](/zh/guide/http-client)。
|
|
||||||
|
|
||||||
## 未来扩展(欢迎参与)
|
|
||||||
- Vim / Emacs 键位层(正在计划)。
|
|
||||||
- 自定义命令面板(Command Palette)。
|
|
||||||
- 代码片段/模板库扩展。
|
|
||||||
- AI 助手(文生代码/注释)。
|
|
||||||
|
|
||||||
## 开发者指南概述
|
|
||||||
1. **注册扩展**:在 `extensionManager.registerFactory` 中添加自定义扩展工厂。
|
|
||||||
2. **配置项**:在 `extensions.json` 中声明默认配置,并在设置面板暴露 UI(Vue 组件)。
|
|
||||||
3. **热更新**:调用 `manager.updateExtensionImmediate(id, enabled, config)` 实时切换,无需刷新窗口。
|
|
||||||
4. **后端交互**:通过 `ExtensionService.UpdateExtensionState` 将配置写入 SQLite。
|
|
||||||
|
|
||||||
> 如果需要编写自用扩展,可 fork 项目在 `frontend/src/views/editor/extensions` 中添加文件,再通过 PR 贡献给社区。
|
|
||||||
@@ -1,86 +1,163 @@
|
|||||||
# 功能特性
|
# 功能特性
|
||||||
|
|
||||||

|
探索 voidraft 的强大功能,让它成为开发者的优秀工具。
|
||||||
> 替换为展示彩虹括号、小地图、搜索工具条等扩展组合的截图。
|
|
||||||
|
|
||||||
## 1. 编辑体验
|
## 块状编辑
|
||||||
### 块状编辑全流程
|
|
||||||
- `∞∞∞language[-a]` 语法由 `codeblock/lang-parser` 解析,支持自动检测、分隔符校验、块范围缓存。
|
|
||||||
- `blockState` 暴露 API(`getActiveBlock/getFirstBlock/getLastBlock`),供格式化、重排、复制、HTTP 执行等插件共享。
|
|
||||||
- `mathBlock` 可在块尾展示计算结果,点击可复制;`CURRENCIES_LOADED` 注解在汇率更新时刷新缓存。
|
|
||||||
|
|
||||||
### 语言支持
|
voidraft 的核心功能是其块状编辑系统:
|
||||||
- 内建 30+ 语言模版(`lang-parser/languages.ts`),覆盖 JS/TS/HTML/CSS/Go/Rust/Python/SQL/YAML/HTTP/Markdown/Plain/Text/Math。
|
|
||||||
- 语言切换下拉实时更新分隔符;支持自定义别名(例如 `∞∞∞shell`)。
|
|
||||||
|
|
||||||
### 语法高亮与主题
|
- 每个块可以有不同的编程语言
|
||||||
- `rainbowBracket`、`fold`、`hyperlink`、`colorSelector` 等扩展组合提供接近 VSCode 的体验。
|
- 块之间由分隔符分隔(`∞∞∞语言`)
|
||||||
- `ThemeService` 预置 12+ 暗/亮主题,可在设置中克隆、修改 JSON 色板,并立即生效。
|
- 快速在块之间导航
|
||||||
|
- 独立格式化每个块
|
||||||
|
|
||||||
### 文本统计与滚轮缩放
|
## 语法高亮
|
||||||
- `statsExtension` 实时统计行数、字符数和选区,展示在状态栏。
|
|
||||||
- `wheelZoomExtension` 让 `Ctrl + 鼠标滚轮` 调整字体大小,同时同步 `configStore`。
|
|
||||||
|
|
||||||
## 2. 高效工具箱
|
支持 30+ 种语言的专业语法高亮:
|
||||||
### VSCode 式搜索替换
|
|
||||||
- `extensions/vscodeSearch` 提供悬浮面板,支持大小写/整词/正则、向上/向下跳转、批量替换。
|
|
||||||
- 对应快捷键:`Ctrl+F`、`Ctrl+H`、`Alt+Enter`(替换全部)。
|
|
||||||
|
|
||||||
### Markdown 预览
|
- 自动语言检测
|
||||||
- `panelStore` 为每个文档维护预览状态,保证不同文档互不影响。
|
- 可自定义配色方案
|
||||||
- 选中 Markdown 块后点击工具栏预览按钮即可在右侧展开实时渲染面板。
|
- 支持嵌套语言
|
||||||
|
- 代码折叠支持
|
||||||
|
|
||||||
### HTTP 客户端
|
## HTTP 客户端
|
||||||
- Request DSL + 运行器在 [专章](/zh/guide/http-client) 详细说明。
|
|
||||||
- 支持变量、响应插入、多种请求体、定制 header、复制 cURL。
|
|
||||||
|
|
||||||
### 翻译助手
|
用于 API 测试的内置 HTTP 客户端:
|
||||||
- `translator` 扩展监听选区,符合长度阈值后显示按钮;由 `TranslationService` 调用 Bing/Google/Youdao/DeepL/TartuNLP。
|
|
||||||
- 支持语种缓存、复制译文、切换译文方向。
|
|
||||||
|
|
||||||
### 颜色与高亮
|
### 请求类型
|
||||||
- `colorSelector` 识别 `#fff/rgba/hsl`、打开取色器;`textHighlight` 用 `Mod+Shift+H` 标记重要行。
|
- GET、POST、PUT、DELETE、PATCH
|
||||||
|
- 自定义请求头
|
||||||
|
- 多种请求体格式:JSON、FormData、URL 编码、XML、文本
|
||||||
|
|
||||||
## 3. 复杂布局能力
|
### 请求变量
|
||||||
### 多窗口
|
定义和重用变量:
|
||||||
- `WindowService` 允许为任意文档创建独立 WebView,URL 自动携带 `?documentId=`。
|
|
||||||
- `WindowSnapService` 根据主窗口位置吸附子窗口(上下左右+四角),并缓存尺寸、位置。
|
|
||||||
- 支持全局热键(默认 `Alt+X`)一键显示或隐藏所有窗口。
|
|
||||||
|
|
||||||
### 标签页
|
```http
|
||||||
- `tabStore` 通过 `enableTabs` 控制;支持拖拽排序、关闭其他/左侧/右侧标签。
|
@var {
|
||||||
- 与多窗口互斥:当文档被新窗口接管后会从标签栏移除,避免重复。
|
baseUrl: "https://api.example.com",
|
||||||
|
token: "your-api-token"
|
||||||
|
}
|
||||||
|
|
||||||
### 系统托盘与置顶
|
GET "{{baseUrl}}/users" {
|
||||||
- `TrayService` 控制关闭时隐藏到托盘或直接退出。
|
authorization: "Bearer {{token}}"
|
||||||
- 工具栏提供图钉按钮,可即时切换 `AlwaysOnTop`(支持临时置顶和永久置顶)。
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 4. 数据守护
|
### 响应处理
|
||||||
### SQLite + 自动迁移
|
- 查看格式化的 JSON 响应
|
||||||
- `DatabaseService` 启动时执行 PRAGMA + 表结构校验,缺失字段自动 `ALTER TABLE`。
|
- 查看响应时间和大小
|
||||||
- 默认生成 `documents/extensions/key_bindings/themes` 等表,支持软删除与锁定。
|
- 检查响应头
|
||||||
|
- 保存响应以供日后使用
|
||||||
|
|
||||||
### Git 备份
|
## 代码格式化
|
||||||
- `BackupService` 将 `dataPath` 初始化为 Git 仓库,支持 Token/SSHKey/用户名密码三种方式。
|
|
||||||
- 自动任务按分钟运行(`BackupInterval`),包括 add/commit/push;也可从 UI 触发一次性 push。
|
|
||||||
|
|
||||||
### 配置快照
|
集成 Prettier 支持:
|
||||||
- 所有设置存于 `config.json`,包含 `metadata.version/lastUpdated`,方便手工回滚。
|
|
||||||
- `ConfigService.Watch` 为窗口吸附、托盘、热键等服务提供实时响应。
|
|
||||||
|
|
||||||
### 自动更新
|
- 保存时格式化(可选)
|
||||||
- `SelfUpdateService` 先检查主源(Gitea),失败再回退到 GitHub;下载完成后可一键「重启并更新」。
|
- 格式化选区或整个块
|
||||||
- 更新前可选自动触发 Git 备份(`backupBeforeUpdate`)。
|
- 支持 JavaScript、TypeScript、CSS、HTML、JSON 等
|
||||||
|
- 可自定义格式化规则
|
||||||
|
|
||||||
## 5. 自动化与集成
|
## 编辑器扩展
|
||||||
- **启动时动作**:可开启开机自启(`StartupService`)、默认最小化至托盘。
|
|
||||||
- **HTTP 运行挂钩**:`response-inserter` 可在响应块尾部插入 `// @timestamp` 等自定义标记。
|
|
||||||
- **Math/汇率**:`mathBlock` 可引用上一次结果 (`prev`),配合 `CURRENCIES_LOADED` 注解支撑货币换算。
|
|
||||||
- **系统信息**:`SystemService` 暴露内存、GC、Goroutine 数量,可在调试面板查看。
|
|
||||||
|
|
||||||
## 6. 可配置的快捷键
|
### VSCode 风格搜索
|
||||||
- 详见 [键盘快捷键](/zh/guide/keyboard-shortcuts)。默认绑定定义在 `internal/models/key_bindings.go`,前端设置页可逐项修改、禁用。
|
- 查找和替换,支持正则表达式
|
||||||
|
- 区分大小写和全字匹配选项
|
||||||
|
- 跨所有块搜索
|
||||||
|
|
||||||
|
### 小地图
|
||||||
|
- 文档的鸟瞰图
|
||||||
|
- 快速导航
|
||||||
|
- 可自定义大小和位置
|
||||||
|
|
||||||
|
### 彩虹括号
|
||||||
|
- 彩色括号配对
|
||||||
|
- 更容易匹配括号
|
||||||
|
- 可自定义颜色
|
||||||
|
|
||||||
|
### 颜色选择器
|
||||||
|
- 可视化颜色选择
|
||||||
|
- 支持 hex、RGB、HSL
|
||||||
|
- 实时预览
|
||||||
|
|
||||||
|
### 翻译工具
|
||||||
|
- 翻译选定的文本
|
||||||
|
- 支持多种语言
|
||||||
|
- 快速键盘访问
|
||||||
|
|
||||||
|
### 文本高亮
|
||||||
|
- 高亮重要文本
|
||||||
|
- 多种高亮颜色
|
||||||
|
- 持久化高亮
|
||||||
|
|
||||||
|
## 多窗口支持
|
||||||
|
|
||||||
|
高效使用多个窗口:
|
||||||
|
|
||||||
|
- 每个窗口都是独立的
|
||||||
|
- 独立的文档
|
||||||
|
- 同步的设置
|
||||||
|
- 窗口状态持久化
|
||||||
|
|
||||||
|
## 主题自定义
|
||||||
|
|
||||||
|
完全控制编辑器外观:
|
||||||
|
|
||||||
|
### 内置主题
|
||||||
|
- 深色模式
|
||||||
|
- 浅色模式
|
||||||
|
- 根据系统自动切换
|
||||||
|
|
||||||
|
### 自定义主题
|
||||||
|
- 创建你自己的主题
|
||||||
|
- 自定义每种颜色
|
||||||
|
- 保存和分享主题
|
||||||
|
- 导入社区主题
|
||||||
|
|
||||||
|
## 自动更新系统
|
||||||
|
|
||||||
|
通过自动更新保持最新:
|
||||||
|
|
||||||
|
- 后台更新检查
|
||||||
|
- 新版本通知
|
||||||
|
- 一键更新
|
||||||
|
- 更新历史
|
||||||
|
- 支持多个更新源(GitHub、Gitea)
|
||||||
|
|
||||||
|
## 数据备份
|
||||||
|
|
||||||
|
使用基于 Git 的备份保护你的数据:
|
||||||
|
|
||||||
|
- 自动备份
|
||||||
|
- 手动触发备份
|
||||||
|
- 支持 GitHub 和 Gitea
|
||||||
|
- 多种认证方式(SSH、Token、密码)
|
||||||
|
- 可配置备份间隔
|
||||||
|
|
||||||
|
## 键盘快捷键
|
||||||
|
|
||||||
|
广泛的键盘支持:
|
||||||
|
|
||||||
|
- 可自定义快捷键
|
||||||
|
- Vim/Emacs 按键绑定(计划中)
|
||||||
|
- 快速命令面板
|
||||||
|
- 上下文感知快捷键
|
||||||
|
|
||||||
|
## 性能
|
||||||
|
|
||||||
|
专为速度而构建:
|
||||||
|
|
||||||
|
- 快速启动时间
|
||||||
|
- 流畅滚动
|
||||||
|
- 高效内存使用
|
||||||
|
- 支持大文件
|
||||||
|
|
||||||
|
## 隐私与安全
|
||||||
|
|
||||||
|
你的数据是安全的:
|
||||||
|
|
||||||
|
- 本地优先存储
|
||||||
|
- 可选云备份
|
||||||
|
- 无遥测或跟踪
|
||||||
|
- 开源代码库
|
||||||
|
|
||||||
## 7. 文档 & 帮助
|
|
||||||
- 文档站以 VitePress 构建(`frontend/docs`),内置中英双语导航,可一键部署到 GitHub Pages。
|
|
||||||
- `README` 与本文档同步介绍核心功能;建议将常用工作流截图补充到每个「图片占位」中。
|
|
||||||
|
|||||||
@@ -1,82 +1,107 @@
|
|||||||
# 快速开始
|
# 快速开始
|
||||||
|
|
||||||

|
学习使用 voidraft 的基础知识并创建你的第一个文档。
|
||||||
> 替换为展示块分隔符、语言标签、内容的截图,帮助读者直观理解 `∞∞∞language` 结构。
|
|
||||||
|
|
||||||
## 5 分钟上手流程
|
## 编辑器界面
|
||||||
1. **启动应用**:等待加载动画结束,默认会打开 `default` 文档。
|
|
||||||
2. **新建文档**:点击工具栏的文档列表按钮,输入标题后创建;也可在设置里开启标签页,以便同时挂载多个文档。
|
|
||||||
3. **创建首个块**:
|
|
||||||
- 在空白处输入 `∞∞∞javascript` 并回车。
|
|
||||||
- 输入代码或文本,`CodeBlockExtension` 会自动匹配语法高亮。
|
|
||||||
4. **格式化与预览**:
|
|
||||||
- 选中块后点击工具栏的「Format」或使用 `Ctrl+Shift+F`。
|
|
||||||
- 如果块语言是 `md`,可点击「Preview」按钮开启 Markdown 侧栏。
|
|
||||||
5. **运行 HTTP 请求**:创建 `∞∞∞http` 块,填写请求,再点击行号旁的 Run 按钮即可获取响应。
|
|
||||||
6. **打开第二窗口**:在文档列表中右键文档 -> “在新窗口中打开”。`WindowService` 会创建无边框窗口并自动贴靠主窗口。
|
|
||||||
|
|
||||||
## 界面导览
|
当你打开 voidraft 时,你将看到:
|
||||||
- **主编辑区**:CodeMirror 视图,支持鼠标滚轮 + `Ctrl` 缩放(`wheelZoomExtension`)。
|
|
||||||
- **右侧小地图**:`extensions/minimap` 提供鸟瞰和选区同步。
|
|
||||||
- **底部状态**:`editorStore.documentStats` 实时展示行数/字符/选区。
|
|
||||||
- **工具栏**(`Toolbar.vue`):包含文档切换、块语言下拉、窗口置顶、格式化、Markdown 预览、更新提示、进入设置等。
|
|
||||||
|
|
||||||
## 块的基本操作
|
- **主编辑器**:编写和编辑的中心区域
|
||||||
| 操作 | 快捷键 | 说明 |
|
- **工具栏**:快速访问常用操作
|
||||||
| --- | --- | --- |
|
- **状态栏**:显示当前块的语言和其他信息
|
||||||
| 新建块(下方) | `Ctrl+Enter` | 在当前块后插入 `∞∞∞text-a` 分隔符 |
|
|
||||||
| 新建块(上方) | `Ctrl+Shift+Enter` | 在当前块前插入 |
|
|
||||||
| 跳到上/下一个块 | `Alt+Up / Alt+Down` | 通过 `blockGotoPrevious/Next` 命令 |
|
|
||||||
| 删除块 | `Alt+Delete` | 仅删除块内容,不影响其他块 |
|
|
||||||
| 块排序 | `Ctrl+Shift+↑/↓` | `moveLines` 结合块范围移动 |
|
|
||||||
| 复制块 | `Ctrl+C`(光标在块上即可) | `copyPaste.ts` 自动扩展选区至整个块 |
|
|
||||||
|
|
||||||
## 自动语言与格式化
|
## 创建代码块
|
||||||
- 当分隔符写成 `∞∞∞text-a` 时,会触发语言自动检测(`lang-detect/autodetect.ts`),常用于粘贴未知代码。
|
|
||||||
- `formatCode.ts` 调用 Prettier,自动选择 parser;若语言不支持,会提示不可格式化。
|
|
||||||
- 块语言可在工具栏下拉中修改,列表由 `lang-parser/languages.ts` 提供。
|
|
||||||
|
|
||||||
## Markdown / 待办
|
voidraft 使用基于块的编辑系统。每个块可以有不同的语言:
|
||||||
1. 使用 `∞∞∞md` 分隔符。
|
|
||||||
2. 在块内写 Markdown,点击工具栏预览按钮。
|
|
||||||
3. 勾选/取消 Checkbox(`extensions/checkbox`)即可同步更新文本。
|
|
||||||
|
|
||||||
## 翻译与文本标注
|
1. 按 `Ctrl+Enter` 创建新块
|
||||||
- 选中文本后会浮现翻译入口(`translator` 扩展),点击即可在块内查看结果、复制、切换目标语言。
|
2. 输入 `∞∞∞` 后跟语言名称(例如 `∞∞∞javascript`)
|
||||||
- `textHighlight` 扩展提供 `Mod+Shift+H` 高亮当前选区,颜色可在扩展设置中调整。
|
3. 在该块中开始编码
|
||||||
|
|
||||||
|
### 支持的语言
|
||||||
|
|
||||||
|
voidraft 支持 30+ 种编程语言,包括:
|
||||||
|
- JavaScript、TypeScript
|
||||||
|
- Python、Go、Rust
|
||||||
|
- HTML、CSS、Sass
|
||||||
|
- SQL、YAML、JSON
|
||||||
|
- 以及更多...
|
||||||
|
|
||||||
|
## 基本操作
|
||||||
|
|
||||||
|
### 导航
|
||||||
|
|
||||||
|
- `Ctrl+Up/Down`:在块之间移动
|
||||||
|
- `Ctrl+Home/End`:跳转到第一个/最后一个块
|
||||||
|
- `Ctrl+F`:在文档中搜索
|
||||||
|
|
||||||
|
### 编辑
|
||||||
|
|
||||||
|
- `Ctrl+D`:复制当前行
|
||||||
|
- `Ctrl+/`:切换注释
|
||||||
|
- `Alt+Up/Down`:向上/向下移动行
|
||||||
|
- `Ctrl+Shift+F`:格式化代码(如果语言支持 Prettier)
|
||||||
|
|
||||||
|
### 块管理
|
||||||
|
|
||||||
|
- `Ctrl+Enter`:创建新块
|
||||||
|
- `Ctrl+Shift+Enter`:在上方创建块
|
||||||
|
- `Alt+Delete`:删除当前块
|
||||||
|
|
||||||
|
## 使用 HTTP 客户端
|
||||||
|
|
||||||
|
voidraft 包含用于测试 API 的内置 HTTP 客户端:
|
||||||
|
|
||||||
|
1. 创建一个 HTTP 语言的块
|
||||||
|
2. 编写你的 HTTP 请求:
|
||||||
|
|
||||||
## HTTP 客户端概览
|
|
||||||
```http
|
```http
|
||||||
∞∞∞http
|
POST "https://api.example.com/users" {
|
||||||
@var {
|
|
||||||
baseUrl: "https://api.example.com",
|
|
||||||
token: "{{secrets.token}}"
|
|
||||||
}
|
|
||||||
|
|
||||||
POST "{{baseUrl}}/users" {
|
|
||||||
authorization: "Bearer {{token}}"
|
|
||||||
content-type: "application/json"
|
content-type: "application/json"
|
||||||
|
|
||||||
@json {
|
@json {
|
||||||
name: "voidraft",
|
name: "张三",
|
||||||
role: "developer"
|
email: "zhangsan@example.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- `parser/request-parser.ts` 会将变量与请求体解析为结构化对象。
|
|
||||||
- 点击 gutter Run 获取响应,`response-inserter.ts` 会将结果写入 `### Response` 区块。
|
|
||||||
|
|
||||||
## 自动保存与版本安全
|
3. 点击运行按钮执行请求
|
||||||
- `editorStore` 为每个文档维护 `autoSaveTimer`,默认 2000 ms,可在设置 > 编辑 调整。
|
4. 内联查看响应
|
||||||
- `documentStates` 记录每个文档的光标位置,切换文档或重启应用都会恢复。
|
|
||||||
- 若开启 Git 备份,可在工具栏或设置中查看最近一次 `push` 是否成功。
|
|
||||||
|
|
||||||
## 最佳实践
|
## 多窗口支持
|
||||||
- 使用 Markdown 块为每组代码加标题/注释,便于导航。
|
|
||||||
- 重要文档启用“锁定”以避免被删除(文档右键菜单)。
|
同时处理多个文档:
|
||||||
- 多窗口 + 吸附用于常驻参考资料,标签页用于在一个窗口内快速切换。
|
|
||||||
- 善用「窗口置顶」图钉,让 voidraft 叠放在 VSCode/浏览器之上。
|
1. 转到 `文件 > 新建窗口`(或 `Ctrl+Shift+N`)
|
||||||
|
2. 每个窗口都是独立的
|
||||||
|
3. 更改会自动保存
|
||||||
|
|
||||||
|
## 自定义主题
|
||||||
|
|
||||||
|
个性化你的编辑器:
|
||||||
|
|
||||||
|
1. 打开设置(`Ctrl+,`)
|
||||||
|
2. 转到外观
|
||||||
|
3. 选择主题或创建自己的主题
|
||||||
|
4. 根据你的偏好自定义颜色
|
||||||
|
|
||||||
|
## 键盘快捷键
|
||||||
|
|
||||||
|
学习基本快捷键:
|
||||||
|
|
||||||
|
| 操作 | 快捷键 |
|
||||||
|
|-----|--------|
|
||||||
|
| 新建窗口 | `Ctrl+Shift+N` |
|
||||||
|
| 搜索 | `Ctrl+F` |
|
||||||
|
| 替换 | `Ctrl+H` |
|
||||||
|
| 格式化代码 | `Ctrl+Shift+F` |
|
||||||
|
| 切换主题 | `Ctrl+Shift+T` |
|
||||||
|
| 命令面板 | `Ctrl+Shift+P` |
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
现在你已经了解了基础知识:
|
||||||
|
|
||||||
|
- 详细探索[功能特性](/zh/guide/features)
|
||||||
|
|
||||||
接下来:
|
|
||||||
- [界面总览](/zh/guide/ui-overview)
|
|
||||||
- [块语法与结构](/zh/guide/block-syntax)
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# HTTP 客户端
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为 HTTP 块 + 响应卡片的截图。
|
|
||||||
|
|
||||||
voidraft 将 HTTP 测试写成块(`∞∞∞http`),语法与 JetBrains Http Client 类似。解析与执行由 `frontend/src/views/editor/extensions/httpclient` 完成。
|
|
||||||
|
|
||||||
## 基本语法
|
|
||||||
```http
|
|
||||||
∞∞∞http
|
|
||||||
GET "https://api.example.com/users" {
|
|
||||||
accept: "application/json"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 方法 + URL 必须用双引号包裹。
|
|
||||||
- Header 以 `key: "value"` 格式编写。
|
|
||||||
- 请求体使用内联指令(见下文)。
|
|
||||||
|
|
||||||
## 变量与环境
|
|
||||||
```http
|
|
||||||
@var {
|
|
||||||
baseUrl: "https://api.example.com",
|
|
||||||
token: "{{secrets.token}}"
|
|
||||||
}
|
|
||||||
|
|
||||||
GET "{{baseUrl}}/users" {
|
|
||||||
authorization: "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- `@var` 块使用 JSON 语法。
|
|
||||||
- 变量在任意请求中以 `{{name}}` 引用。
|
|
||||||
- `variable-resolver.ts` 支持嵌套、默认值、外部 secrets 映射。
|
|
||||||
|
|
||||||
## 请求体助手
|
|
||||||
| 指令 | 示例 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `@json` | `@json { "name": "voidraft" }` | 自动 `content-type: application/json` 并格式化 |
|
|
||||||
| `@form` | `@form { username: "demo" }` | 转为 `application/x-www-form-urlencoded` |
|
|
||||||
| `@multipart` | `@multipart { file: @"C:\tmp\a.txt" }` | 读取文件、多段表单 |
|
|
||||||
| `@text` | `@text <raw body>` | 自由文本 |
|
|
||||||
|
|
||||||
## 运行与响应
|
|
||||||
1. 将光标置于 HTTP 块内。
|
|
||||||
2. 点击行号左侧的 Run(三角形图标),或按下自定义快捷键。
|
|
||||||
3. 运行结果会插入到块尾的 `### Response` 中,包含:
|
|
||||||
- 状态行 + 响应时间 + 体积。
|
|
||||||
- Headers(可折叠)。
|
|
||||||
- 响应体(自动格式化 JSON / XML / HTML / Text)。
|
|
||||||
- 复制、另存为、再次发送等快捷按钮。
|
|
||||||
|
|
||||||
## 多请求文档
|
|
||||||
- 每个 `∞∞∞http` 块被视为独立请求。
|
|
||||||
- `request-parser.ts` 会解析同一块内的多个请求(以 `###` 分隔)。
|
|
||||||
- 使用 Markdown 块写注释或分组标题。
|
|
||||||
|
|
||||||
## 变量注入顺序
|
|
||||||
1. 块内 `@var`。
|
|
||||||
2. 文档级变量(计划中)。
|
|
||||||
3. 环境变量(`EnvironmentService` 预留)。
|
|
||||||
|
|
||||||
## 调试技巧
|
|
||||||
- 运行器会在控制台打印完整请求信息,可通过 `wails3 dev` 查看。
|
|
||||||
- 如果响应过大,可右键响应块选择“折叠正文”或“导出到文件”。
|
|
||||||
- 网络错误会在响应卡片顶部以红条展示,内容来自 `HttpClientService`。
|
|
||||||
- 需要代理时确保系统代理已设置,voidraft 会自动继承。
|
|
||||||
|
|
||||||
## 与其他功能配合
|
|
||||||
- 运行结果可直接与 Markdown/代码块混排,形成 API 使用手册。
|
|
||||||
- 配合 Git 备份可版本化 API 调试记录。
|
|
||||||
- 可将响应复制到其他块(例如 JSON → Prettier 之后用于 mock)。
|
|
||||||
|
|
||||||
> 欢迎在 Issue 中提交你希望支持的额外 DSL 指令(例如 GraphQL、WebSocket、gRPC)。
|
|
||||||
@@ -1,77 +1,63 @@
|
|||||||
# 安装
|
# 安装
|
||||||
|
|
||||||

|
本指南将帮助你在系统上安装 voidraft。
|
||||||
> 替换为安装向导或设置页截图,展示关键开关(置顶、数据目录、自动更新等)。
|
|
||||||
|
|
||||||
## 系统要求(2025.11)
|
## 系统要求
|
||||||
| 项目 | 最低配置 | 推荐配置 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 操作系统 | Windows 10 19045 / Windows 11 21H2 | Windows 11 23H2(macOS/Linux 版本开发中) |
|
|
||||||
| CPU | x86_64 双核 | 4 核以上 |
|
|
||||||
| 内存 | 4 GB | ≥ 8 GB |
|
|
||||||
| 磁盘空间 | 200 MB(含 SQLite 数据) | 1 GB 以上以保存附件/备份 |
|
|
||||||
| 运行环境 | Go 1.21+, Node.js 18+(仅开发者编译时需要) | 同左 + pnpm 8 用于前端 |
|
|
||||||
|
|
||||||
## 获取发行版
|
- **操作系统**:Windows 10 或更高版本(macOS 和 Linux 支持计划中)
|
||||||
1. 打开 [GitHub Releases](https://github.com/landaiqing/voidraft/releases) 或自建 Gitea 镜像。
|
- **内存**:最低 4GB,推荐 8GB
|
||||||
2. 下载 `voidraft-windows-amd64-installer.exe`(安装版)或 `voidraft-portable.zip`(绿色版)。
|
- **磁盘空间**:200MB 可用空间
|
||||||
3. (可选)验证 SHA256:
|
|
||||||
```powershell
|
|
||||||
Get-FileHash .\voidraft-windows-amd64-installer.exe -Algorithm SHA256
|
|
||||||
```
|
|
||||||
4. 双击安装包,按向导完成安装;或解压绿色版至任意目录并创建快捷方式。
|
|
||||||
|
|
||||||
## 首次启动流程
|
## 下载
|
||||||
1. 启动后将创建数据目录:`%USERPROFILE%\.voidraft\data`(含 `voidraft.db`、`config.json`、`extensions.json`)。
|
|
||||||
2. 默认会生成 `default` 文档和一段示例块 `∞∞∞text-a`。
|
|
||||||
3. 若检测到旧版本数据,`ConfigMigrationService` 会自动迁移字段;`DataMigrationService` 确保表结构一致。
|
|
||||||
4. 首次运行建议立刻打开「设置 > 备份」配置远程 Git 仓库。
|
|
||||||
|
|
||||||
## 开发者手动构建
|
访问[发布页面](https://github.com/landaiqing/voidraft/releases)并下载适合你平台的最新版本:
|
||||||
```bash
|
|
||||||
# 克隆项目
|
|
||||||
git clone https://github.com/landaiqing/voidraft.git
|
|
||||||
cd voidraft
|
|
||||||
|
|
||||||
# 安装前端依赖
|
- **Windows**:`voidraft-windows-amd64-installer.exe`
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# 构建/运行桌面应用
|
## 安装步骤
|
||||||
wails3 dev # 启动调试
|
|
||||||
wails3 package # 生成安装包(输出位于 bin/)
|
|
||||||
```
|
|
||||||
> 若遇到 `wails3` 未找到,请先执行 `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`。
|
|
||||||
|
|
||||||
## 数据目录与可执行文件
|
### Windows
|
||||||
| 类型 | 默认位置 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 安装目录 | `C:\Program Files\voidraft` | 包含主程序与嵌入式前端资源 |
|
|
||||||
| 数据目录 | `C:\Users\<you>\.voidraft\data` | 可在设置 > 通用修改 `dataPath`,修改后需重启 |
|
|
||||||
| 备份仓库 | `dataPath/.git` | `BackupService` 初始化或使用现有仓库 |
|
|
||||||
| 日志 | `%LOCALAPPDATA%/voidraft/logs/*.log` | 通过 Wails `application.Log` 输出 |
|
|
||||||
|
|
||||||
## 常用 CLI 检查
|
1. 从发布页面下载安装程序
|
||||||
```powershell
|
2. 运行 `voidraft-windows-amd64-installer.exe` 文件
|
||||||
# 查看版本
|
3. 按照安装向导操作
|
||||||
& "C:\Program Files\voidraft\voidraft.exe" --version
|
4. 从开始菜单或桌面快捷方式启动 voidraft
|
||||||
|
|
||||||
# 清理缓存(若前端异常)
|
## 首次启动
|
||||||
Remove-Item "$env:APPDATA\voidraft\Cache" -Recurse -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
## 防火墙与代理
|
首次启动 voidraft 时:
|
||||||
- voidraft 仅在使用 HTTP 客户端、更新检测、REST 翻译器时发起网络请求。
|
|
||||||
- 若处于企业代理,请在系统代理中放行 `voidraft.exe` 或设置环境变量 `HTTP(S)_PROXY`,HTTP 客户端会继承系统代理。
|
|
||||||
|
|
||||||
## 常见安装问题
|
1. 应用程序将创建一个数据目录来存储你的文档
|
||||||
| 症状 | 处理方案 |
|
2. 你将看到带有欢迎块的主编辑器界面
|
||||||
| --- | --- |
|
3. 开始输入或创建你的第一个代码块!
|
||||||
| 安装向导被安全策略阻止 | 使用签名哈希进行白名单设置或改用便携版 |
|
|
||||||
| 启动后白屏 | 删除 `%APPDATA%/voidraft/Cache`,确保显卡驱动支持 WebView2 |
|
## 配置
|
||||||
| `wails3 dev` 报错缺少 WebView2 | 安装 [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) |
|
|
||||||
| 便携版无法写入 | 检查解压目录是否具有写权限,或在设置内切换 `dataPath` 至可写分区 |
|
voidraft 将其配置和数据存储在:
|
||||||
|
|
||||||
|
- **Windows**:`%APPDATA%/voidraft/`
|
||||||
|
|
||||||
|
你可以自定义各种设置,包括:
|
||||||
|
- 编辑器主题(深色/浅色模式)
|
||||||
|
- 代码格式化偏好
|
||||||
|
- 备份设置
|
||||||
|
- 键盘快捷键
|
||||||
|
|
||||||
|
## 更新
|
||||||
|
|
||||||
|
voidraft 包含自动更新功能,会在有新版本时通知你。你可以:
|
||||||
|
|
||||||
|
- 从设置中手动检查更新
|
||||||
|
- 启用自动更新
|
||||||
|
- 选择首选的更新源
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
如果在安装过程中遇到任何问题:
|
||||||
|
|
||||||
|
1. 确保你有管理员权限
|
||||||
|
2. 检查杀毒软件是否阻止了安装
|
||||||
|
3. 访问我们的 [GitHub issues](https://github.com/landaiqing/voidraft/issues) 页面寻求帮助
|
||||||
|
|
||||||
|
下一步:[快速开始 →](/zh/guide/getting-started)
|
||||||
|
|
||||||
> 继续阅读:[快速开始](/zh/guide/getting-started)
|
|
||||||
|
|||||||
@@ -1,73 +1,50 @@
|
|||||||
# 简介
|
# 简介
|
||||||
|
|
||||||
> voidraft 是一款面向开发者的「块式工作台」,用 CodeMirror 6 打造 Heynote 风格的体验,并结合 Wails3 + Go 后端提供系统托盘、全局热键、自动备份等桌面级能力。
|
欢迎使用 voidraft —— 一个专为开发者设计的优雅文本片段记录工具。
|
||||||
|
|
||||||

|
## 什么是 voidraft?
|
||||||
> 将 `/img/placeholder-main-ui.png` 替换为真实的应用主界面截图,演示数据面板、工具栏和右侧小地图。
|
|
||||||
|
|
||||||
## 产品定位
|
voidraft 是一个现代化的桌面应用程序,帮助开发者管理文本片段、代码块、API 响应、会议笔记和日常待办事项。它为开发工作流程提供了流畅而优雅的编辑体验和强大的功能。
|
||||||
- **核心诉求**:在一处快速记录代码/配置/API 响应/待办清单,并能随时重排、格式化、运行或搜索。
|
|
||||||
- **目标用户**:需要跨项目管理零碎文本的开发者、DevOps、测试或产品技术写作者。
|
|
||||||
- **设计理念**:所有内容都拆成可重排的块(`∞∞∞language`);每个块拥有独立语言、格式化器与扩展;多窗口/多标签保证同一份数据的不同视角。
|
|
||||||
|
|
||||||
## 面向场景
|
## 核心特性
|
||||||
1. **临时代码/脚本草稿**:支持 30+ 语言高亮、Prettier 格式化、彩虹括号、文本高亮。
|
|
||||||
2. **API 调试台**:HTTP 块内置运行器、变量解析、响应插入;请求和响应始终和文档共存。
|
|
||||||
3. **会议 & 需求记录**:Markdown 块 + Checkbox 扩展 + 颜色标注快速整理想法。
|
|
||||||
4. **翻译与研究**:选中文本即可调 Bing/Google/DeepL/TartuNLP/有道翻译,结果内联呈现。
|
|
||||||
5. **多窗口资料墙**:重要文档可弹出独立无边框窗口,依附(Snap)在主窗口侧边。
|
|
||||||
|
|
||||||
## 核心概念
|
### 块状编辑模式
|
||||||
### 块式编辑器
|
|
||||||
- 解析器位于 `frontend/src/views/editor/extensions/codeblock`,依赖自研 Lezer 语法树确保 `∞∞∞` 分隔符稳定。
|
|
||||||
- 块结构(语言、是否自动检测、正文范围)存入 `blockState`,供格式化、移动、复制、HTTP 执行等扩展复用。
|
|
||||||
- `math` 块使用 `math.js` 运行器,`http` 块调用 request parser + gutter run widget。
|
|
||||||
|
|
||||||
### 扩展驱动
|
voidraft 使用受 Heynote 启发的独特块状编辑系统。你可以将内容分割为独立的代码块,每个块具有:
|
||||||
- 后端通过 `internal/models/extensions.go` 定义扩展 ID/配置,`ExtensionService` 负责持久化。
|
- 不同的编程语言设置
|
||||||
- 前端 `ExtensionManager` 根据扩展配置动态拼装 CodeMirror Extension pipeline(小地图、VSCode Search、Translator、Color Picker 等)。
|
- 语法高亮
|
||||||
- 所有扩展都可在设置页热切换,立即同步到当前与所有已打开的编辑器实例。
|
- 独立格式化
|
||||||
|
- 轻松在块之间导航
|
||||||
|
|
||||||
### 数据与安全
|
### 开发者工具
|
||||||
- SQLite 数据保存在 `%USERPROFILE%/.voidraft/data/voidraft.db`(可在设置中自定义 dataPath)。
|
|
||||||
- `DatabaseService` 自动迁移表结构,`DocumentService` 提供软删除/锁定机制避免误删默认草稿。
|
|
||||||
- `BackupService` 基于 go-git(SSH/Token/用户名密码)把 dataPath git 化,可按分钟全量提交、推送到 GitHub/Gitea 等。
|
|
||||||
- `SelfUpdateService` 同时轮询 GitHub/Gitea Release,支持自动下载 + 一键重启。
|
|
||||||
|
|
||||||
## 系统架构概览
|
- **HTTP 客户端**:直接在编辑器中测试 API
|
||||||
| 层级 | 说明 | 关键路径 |
|
- **代码格式化**:内置 Prettier 支持多种语言
|
||||||
| --- | --- | --- |
|
- **语法高亮**:支持 30+ 种编程语言
|
||||||
| 桌面容器 | Wails3 + Go 1.21,负责窗口、托盘、热键、服务注入 | `main.go`, `internal/services` |
|
- **自动语言检测**:自动识别代码块语言类型
|
||||||
| 后端服务 | Config/Document/Extension/Theme/Backup/Window/Hotkey/Translation 等 | `internal/services/*.go` |
|
|
||||||
| 数据模型 | Document、Theme、KeyBinding、GitBackup、Config | `internal/models` |
|
|
||||||
| 前端应用 | Vue 3 + Vite + Pinia + vue-router | `frontend/src` |
|
|
||||||
| 编辑器内核 | CodeMirror 6 扩展及自研块解析、HTTP DSL、Markdown 预览 | `frontend/src/views/editor` |
|
|
||||||
| 文档站点 | VitePress,多语言导航 | `frontend/docs` |
|
|
||||||
|
|
||||||
## 模块速览
|
### 自定义
|
||||||
- **文档存储**:`DocumentService` 支持创建/重命名/软删除/恢复、多窗口并发打开同一文档。
|
|
||||||
- **编辑器实例管理**:`editorStore` 使用 LRU 缓存 + 自动保存计时器,确保在多文档切换时保留光标位置、未保存内容。
|
|
||||||
- **HTTP 客户端**:`extensions/httpclient` 包括 Lezer 语法、变量解析、响应插入与运行 gutter;支持 JSON/FormData/GraphQL 等多体格式。
|
|
||||||
- **Markdown 预览**:`panelStore` 管理逐文档的预览状态,可随块实时刷新。
|
|
||||||
- **多窗口/吸附**:`WindowService` + `WindowSnapService` 根据主窗口位置智能吸附子窗口、自动记忆尺寸。
|
|
||||||
- **全局热键**:`HotkeyService` 监听系统级组合键,切换窗口显隐(默认 Alt+X,可配置)。
|
|
||||||
- **系统托盘**:`systray.SetupSystemTray` 注入显示/隐藏、退出、开机启动等操作。
|
|
||||||
- **翻译生态**:`TranslationService` 聚合 Bing/Google/Youdao/DeepL/TartuNLP,前端 `translator` 扩展提供 Tooltip + 复制。
|
|
||||||
- **主题与外观**:`ThemeService` 预置 12+ 主题,可重置/克隆;前端 `createThemeExtension` 即时应用。
|
|
||||||
|
|
||||||
## 数据流(从键盘到持久化)
|
- **自定义主题**:创建并保存你自己的编辑器主题
|
||||||
1. 用户按键 -> CodeMirror extensions 更新文档。
|
- **扩展功能**:丰富的编辑器扩展,包括小地图、彩虹括号、颜色选择器等
|
||||||
2. `contentChangeExtension` 记录脏状态并刷新 `documentStats`(行数、字符数、选区字符数)。
|
- **多窗口**:同时处理多个文档
|
||||||
3. 触发自动保存计时器(默认 2s) -> `DocumentService.UpdateDocumentContent` 写入 SQLite。
|
|
||||||
4. 若开启 Git 自动备份,每次 Commit 会序列化数据库 + 附带 `voidraft_data.bin`。
|
|
||||||
5. 配置变更(Pinia store)通过 `ConfigService.Set` 传回 Go,并触发观察者(如 WindowSnap/Hotkey/Backup)。
|
|
||||||
|
|
||||||
## 版本节奏与路线图
|
### 数据管理
|
||||||
- ✅ 当前实现:多窗口、标签页、HTTP 客户端、Markdown Preview、数学块、彩虹括号、翻译、Git 备份、自动更新。
|
|
||||||
- 🚧 进行中:自定义扩展导入、键位模版、Linux/macOS 原生打包。
|
- **Git 备份**:使用 Git 仓库自动备份
|
||||||
- 🗺️ 规划中:剪贴板历史、团队同步、云端模板市场。
|
- **云同步**:跨设备同步你的数据
|
||||||
|
- **自动更新**:及时获取最新功能
|
||||||
|
|
||||||
|
## 为什么选择 voidraft?
|
||||||
|
|
||||||
|
- **专注开发者**:考虑开发者需求而构建
|
||||||
|
- **现代技术栈**:使用前沿技术(Wails3、Vue 3、CodeMirror 6)
|
||||||
|
- **跨平台**:支持 Windows(macOS 和 Linux 支持计划中)
|
||||||
|
- **开源**:MIT 许可证,社区驱动开发
|
||||||
|
|
||||||
|
## 开始使用
|
||||||
|
|
||||||
|
准备好开始了吗?从我们的[发布页面](https://github.com/landaiqing/voidraft/releases)下载最新版本,或继续阅读文档了解更多。
|
||||||
|
|
||||||
|
下一步:[安装 →](/zh/guide/installation)
|
||||||
|
|
||||||
## 下一步
|
|
||||||
- [安装 voidraft](/zh/guide/installation)
|
|
||||||
- [界面总览](/zh/guide/ui-overview)
|
|
||||||
- [快速开始](/zh/guide/getting-started)
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
# 键盘快捷键
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为展示快捷键设置界面或常用快捷键速查表的截图。
|
|
||||||
|
|
||||||
快捷键定义源自 `internal/models/key_bindings.go`,在设置 > 键位 中可以启用/禁用或改写。下表列出常用组合:
|
|
||||||
|
|
||||||
## 块管理
|
|
||||||
| 功能 | 默认快捷键 | 备注 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 新建块(下方) | `Ctrl+Enter` | `blockAddAfterCurrent` |
|
|
||||||
| 新建块(上方) | `Ctrl+Shift+Enter` | `blockAddBeforeCurrent` |
|
|
||||||
| 跳到上/下一个块 | `Alt+↑ / Alt+↓` | `blockGotoPrevious/Next` |
|
|
||||||
| 选择当前块 | `Ctrl+Shift+A` | `blockSelectAll` |
|
|
||||||
| 删除块 | `Alt+Delete` | `blockDelete` |
|
|
||||||
| 块上移/下移 | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | `blockMoveUp/Down` |
|
|
||||||
| 复制块 | `Ctrl+C`(块获得焦点) | `blockCopy` |
|
|
||||||
| 剪切块 | `Ctrl+X` | `blockCut` |
|
|
||||||
| 粘贴块 | `Ctrl+V` | `blockPaste` |
|
|
||||||
|
|
||||||
## 行与文本编辑
|
|
||||||
| 功能 | 快捷键 |
|
|
||||||
| --- | --- |
|
|
||||||
| 行复制(上/下) | `Shift+Alt+↑ / Shift+Alt+↓` |
|
|
||||||
| 行移动(上/下) | `Alt+↑ / Alt+↓`(在块内部) |
|
|
||||||
| 插入空行 | `Ctrl+Enter`(块尾后仍可插入) |
|
|
||||||
| 选择整行 | `Alt+L` |
|
|
||||||
| 语法级跳转 | `Ctrl+Alt+Left / Ctrl+Alt+Right` |
|
|
||||||
| 匹配括号 | `Shift+Ctrl+\` |
|
|
||||||
| 注释/块注释 | `Ctrl+/` / `Shift+Alt+A` |
|
|
||||||
| Tab 缩进/反向缩进 | `Ctrl+]` / `Ctrl+[` |
|
|
||||||
| 删除单词(向前/向后) | `Ctrl+Backspace` / `Ctrl+Delete` |
|
|
||||||
|
|
||||||
## 搜索与替换
|
|
||||||
| 功能 | 快捷键 | 描述 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 打开搜索 | `Ctrl+F` | `showSearch` |
|
|
||||||
| 打开替换 | `Ctrl+H` | `searchShowReplace` |
|
|
||||||
| 切换大小写/整词/正则 | `Alt+C / Alt+W / Alt+R` | `searchToggleCase/Word/Regex` |
|
|
||||||
| 替换全部 | `Alt+Enter` | `searchReplaceAll` |
|
|
||||||
|
|
||||||
## Markdown/预览/格式化
|
|
||||||
| 功能 | 快捷键 |
|
|
||||||
| --- | --- |
|
|
||||||
| 格式化块 | `Ctrl+Shift+F` |
|
|
||||||
| 打开 Markdown 预览 | 工具栏按钮(建议映射到 `Ctrl+Shift+M`) |
|
|
||||||
| 高亮文本 | `Mod+Shift+H` |
|
|
||||||
|
|
||||||
## 窗口与系统
|
|
||||||
| 功能 | 快捷键 |
|
|
||||||
| --- | --- |
|
|
||||||
| 新建窗口 | `Ctrl+Shift+N`(命令面板) |
|
|
||||||
| 全局显示/隐藏所有窗口 | 默认 `Alt+X`(可在设置 > 通用 > 全局热键中修改) |
|
|
||||||
| 打开设置 | `Ctrl+,` |
|
|
||||||
| 切换主题 | `Ctrl+Shift+T`(可自定义) |
|
|
||||||
|
|
||||||
## HTTP 客户端
|
|
||||||
| 功能 | 快捷键 |
|
|
||||||
| --- | --- |
|
|
||||||
| 运行请求 | 点击行号旁 Run 或自定义 `Ctrl+Alt+R` |
|
|
||||||
| 复制响应正文 | `Ctrl+Alt+C`(响应块聚焦时) |
|
|
||||||
|
|
||||||
## 翻译工具
|
|
||||||
| 功能 | 快捷键 |
|
|
||||||
| --- | --- |
|
|
||||||
| 显示翻译浮层 | 选中 ≥ `minSelectionLength` 的文本后按 `Ctrl+'`(可自定义) |
|
|
||||||
| 复制译文 | 在浮层中按 `Ctrl+C` |
|
|
||||||
|
|
||||||
## 自定义与导出
|
|
||||||
1. 打开设置 > 键位,列表会加载来自 `ExtensionService.GetAllKeyBindings()` 的数据。
|
|
||||||
2. 可单独禁用某个绑定或录入新组合;存储在 `%USERPROFILE%/.voidraft/data/key_bindings.json`。
|
|
||||||
3. 需要与系统级快捷键冲突时,可勾选“忽略系统修饰键”。
|
|
||||||
|
|
||||||
> 建议将以上表格打印贴在工作区,或在文档中保留常用组合,方便新同事查阅。
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# 多窗口与标签页
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为展示主窗口 + 侧边浮窗(子窗口)或标签页齐开的截图。
|
|
||||||
|
|
||||||
## 多窗口工作流
|
|
||||||
- `WindowService.OpenDocumentWindow` 会根据文档 ID 创建新 WebView 窗口,URL 自动附加 `?documentId=<id>`。
|
|
||||||
- `windowStore` 通过查询字符串判断当前是否为子窗口(非主窗口)。
|
|
||||||
- 子窗口具备:
|
|
||||||
- 独立的 CodeMirror 实例与扩展栈。
|
|
||||||
- 与主窗口共享的 Document/Config Store,因此编辑内容实时同步(SQLite 数据库为唯一来源)。
|
|
||||||
- `WindowSnapService` 提供吸附:拖动靠近主窗口边缘时自动贴靠;支持上下左右以及四个角。
|
|
||||||
- 关闭时自动注销吸附状态,避免悬挂引用。
|
|
||||||
|
|
||||||
### 操作步骤
|
|
||||||
1. 打开文档列表(工具栏图标或 `Ctrl+Shift+O`)。
|
|
||||||
2. 右键目标文档 → 选择 “在新窗口中打开”。
|
|
||||||
3. 若文档已在标签页打开,会自动从标签栏移除,防止重复。
|
|
||||||
4. 关闭窗口:
|
|
||||||
- 点击自定义标题栏关闭按钮。
|
|
||||||
- 系统托盘菜单选择退出。
|
|
||||||
|
|
||||||
### 使用建议
|
|
||||||
- 将参考资料或检查清单放在子窗口中,配合“窗口置顶”保持常驻。
|
|
||||||
- 通过 Windows Snap + voidraft Snap 组合,可快速排版 2-4 个窗口。
|
|
||||||
- 若想在多窗口之间同步滚动,可尝试启用“共享视图状态”扩展(计划中)。
|
|
||||||
|
|
||||||
## 标签页模式
|
|
||||||
- 在设置 > 通用中开启“启用标签页”(`config.general.enableTabs`)。
|
|
||||||
- `tabStore` 维护 `tabsMap` + `tabOrder`,支持:
|
|
||||||
- 拖拽排序(拖动标签即可)。
|
|
||||||
- 关闭单个/其他/左侧/右侧标签。
|
|
||||||
- 检测当前文档是否已存在标签。
|
|
||||||
- 标签栏位于主窗口顶部,紧贴工具栏下方。
|
|
||||||
|
|
||||||
### 常用操作
|
|
||||||
| 操作 | 方法 |
|
|
||||||
| --- | --- |
|
|
||||||
| 关闭标签 | 点击标签上的叉号或中键 |
|
|
||||||
| 关闭其他 | 右键标签 → “关闭其他标签” |
|
|
||||||
| 关闭右侧/左侧 | 右键标签 → 选择对应菜单 |
|
|
||||||
| 固定标签(计划中) | 将在后续版本中提供 pin 功能 |
|
|
||||||
|
|
||||||
### Tabs vs 窗口
|
|
||||||
| 项目 | 标签页 | 新窗口 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| UI 占用 | 集成在一个窗口中 | 独立操作系统窗口 |
|
|
||||||
| 跨屏 | 不方便 | 可拖到其他显示器 |
|
|
||||||
| 独立置顶 | 不可单独置顶 | 每个窗口可单独置顶 |
|
|
||||||
| 推荐场景 | 同一背景的多个文档 | 跨项目/跨显示器对比 |
|
|
||||||
|
|
||||||
## 系统托盘与热键
|
|
||||||
- 勾选 “启用系统托盘” 后,关闭窗口默认隐藏至托盘。
|
|
||||||
- 全局热键(默认 `Alt+X`)由 `HotkeyService` 控制:
|
|
||||||
- 若 main window 可见 → 隐藏所有 voidraft 窗口。
|
|
||||||
- 若 main window 隐藏 → Show + Restore + Focus。
|
|
||||||
- `TrayService` 还提供“最小化到托盘”“显示主窗口”等菜单项。
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
| 问题 | 原因 | 解决 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 新窗口无法打开 | 文档 ID 不存在或被锁定 | 在文档列表确认状态,必要时解锁 |
|
|
||||||
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
|
|
||||||
| 关闭窗口直接退出应用 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
|
|
||||||
| 标签页切换慢 | 同时开启标签 + 多窗口导致资源占用 | 关闭暂不需要的窗口或减少标签 |
|
|
||||||
|
|
||||||
> 如果需要更复杂的布局(如平铺窗口、快捷布局),欢迎在 Issue 中提出建议。
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# 设置与配置
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为设置页截图,突出通用/编辑/外观/更新/备份等分栏。
|
|
||||||
|
|
||||||
所有设置都映射到 `internal/models/config.go`,持久化文件位于 `%USERPROFILE%/.voidraft/data/config.json`。前端 `configStore` 负责与后端 `ConfigService` 同步。
|
|
||||||
|
|
||||||
## 通用(General)
|
|
||||||
| 选项 | 说明 | 后端键 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 窗口置顶 (`alwaysOnTop`) | 永久置顶主窗口 | `general.alwaysOnTop` |
|
|
||||||
| 数据目录 (`dataPath`) | SQLite + 备份所在目录,修改后需重启 | `general.dataPath` |
|
|
||||||
| 系统托盘 (`enableSystemTray`) | 关闭窗口后隐藏到托盘而非退出 | `general.enableSystemTray` |
|
|
||||||
| 开机自启 (`startAtLogin`) | 调用 `StartupService` 注册 | `general.startAtLogin` |
|
|
||||||
| 窗口吸附 (`enableWindowSnap`) | `WindowSnapService` 是否启用 | `general.enableWindowSnap` |
|
|
||||||
| 全局热键 (`enableGlobalHotkey` + `globalHotkey`) | 默认 Alt+X,控制显隐 | `general.globalHotkey` |
|
|
||||||
| 标签页 (`enableTabs`) | 启用多标签界面 | `general.enableTabs` |
|
|
||||||
| 加载动画 (`enableLoadingAnimation`) | 切换文档时显示动画 | `general.enableLoadingAnimation` |
|
|
||||||
|
|
||||||
## 编辑(Editing)
|
|
||||||
| 选项 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| Font Size/Family/Weight/Line Height | 立即作用于所有编辑器实例 |
|
|
||||||
| Tab Size/Tab Type/Enable Tab Indent | 映射 `tabExtension` 行为 |
|
|
||||||
| Auto Save Delay | ms,影响 `editorStore` 自动保存周期 |
|
|
||||||
|
|
||||||
## 外观(Appearance)
|
|
||||||
| 选项 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| Language | UI 语言(`zh-CN`/`en-US`) |
|
|
||||||
| System Theme | 深色/浅色/跟随系统 |
|
|
||||||
| Current Theme | 选择预设或自定义主题(详见 [主题与外观](/zh/guide/themes)) |
|
|
||||||
|
|
||||||
## 更新(Updates)
|
|
||||||
| 选项 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| Auto Update | 启动时自动检查更新 |
|
|
||||||
| Primary/Backup Source | `github` 或 `gitea`,对应 `UpdatesConfig` |
|
|
||||||
| Backup Before Update | 下载更新前执行 Git 备份 |
|
|
||||||
| Update Timeout | HTTP 请求超时 |
|
|
||||||
| GitHub/Gitea 仓库 | owner/repo/baseURL,可指向自建镜像 |
|
|
||||||
|
|
||||||
## 备份(Backup)
|
|
||||||
| 选项 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| Enabled | 开关 Git 备份 |
|
|
||||||
| Repo URL | 远程仓库地址(HTTPS 或 SSH) |
|
|
||||||
| Auth Method | `token` / `ssh_key` / `user_pass` |
|
|
||||||
| Username/Password/Token/SSH Key Path | 根据认证方式填写 |
|
|
||||||
| Backup Interval | 自动备份间隔(分钟) |
|
|
||||||
| Auto Backup | 是否按间隔自动推送 |
|
|
||||||
|
|
||||||
## 键位(Key Bindings)
|
|
||||||
- 列表由 `ExtensionService.GetAllKeyBindings()` 提供。可搜索命令 ID 或组合。
|
|
||||||
- 允许将命令禁用(关闭开关)或录入新组合。
|
|
||||||
- 更改立即影响所有编辑器实例。
|
|
||||||
|
|
||||||
## 扩展(Extensions)
|
|
||||||
- 显示 `ExtensionSettings` 中的所有扩展。
|
|
||||||
- 每项可开关并展示 JSON 配置(背景色、最小选区、最小化提示等)。
|
|
||||||
- 修改后调用 `ExtensionService.UpdateExtensionState` 并通知 `ExtensionManager` 热更新。
|
|
||||||
|
|
||||||
## 配置文件备份
|
|
||||||
- 每次修改配置都会更新 `metadata.lastUpdated`,可用 Git 备份追踪历史。
|
|
||||||
- 若出现配置损坏,可删除 `config.json`,应用会写入 `NewDefaultAppConfig`。
|
|
||||||
|
|
||||||
## 导入/导出(建议)
|
|
||||||
- 目前可手动复制 `config.json`/`extensions.json`/`key_bindings.json`。
|
|
||||||
- 计划提供 UI 层面的导入导出按钮,便于跨设备同步。
|
|
||||||
|
|
||||||
> 修改高级选项(如 dataPath)后建议重启,以确保后台服务(数据库、备份、窗口吸附等)读取到最新配置。
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# 主题与外观
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为主题切换界面或自定义主题编辑器的截图。
|
|
||||||
|
|
||||||
voidraft 的主题由后端 `ThemeService` 管理,存储在 `themes` 表。前端通过 `themeStore` + `createThemeExtension` 应用色板。
|
|
||||||
|
|
||||||
## 预设主题
|
|
||||||
| 名称 | 类型 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| default-dark | Dark | 默认暗色,适合低光环境 |
|
|
||||||
| default-light | Light | 默认亮色 |
|
|
||||||
| dracula | Dark | 高对比度紫色系 |
|
|
||||||
| aura | Dark | 柔和霓虹风 |
|
|
||||||
| github-dark / github-light | Dark/Light | 与 GitHub 主题接近 |
|
|
||||||
| material-dark / light | Dark/Light | Material Design 色板 |
|
|
||||||
| one-dark | Dark | VSCode 经典主题 |
|
|
||||||
| solarized-dark / light | Dark/Light | Solarized 配色 |
|
|
||||||
| tokyo-night / storm / day | Dark/Light | Tokyo Night 三件套 |
|
|
||||||
|
|
||||||
## 自定义主题
|
|
||||||
1. 打开设置 > 外观,选择「创建主题」。
|
|
||||||
2. 颜色字段对应 `ThemeColorConfig`,包含 `editor.background`, `editor.foreground`, `gutter`, `selection`, `bracket`, `keyword`, `string`, `comment`, `accent` 等。
|
|
||||||
3. 保存后立即写入数据库,可通过 `Reset` 按钮恢复为预设值。
|
|
||||||
4. 前端 `themeExtension` 会向 CodeMirror 注入新的 `EditorView.theme`。
|
|
||||||
|
|
||||||
## 动态切换
|
|
||||||
- 切换主题会立即影响所有已打开的编辑器实例;`updateEditorTheme` 逐个更新 `EditorView`。
|
|
||||||
- `SystemTheme` 设为 `auto` 时,voidraft 会监听操作系统深浅模式并自动切换到 `default-dark` 或 `default-light`。
|
|
||||||
|
|
||||||
## 字体与行高
|
|
||||||
- 字体配置来自设置 > 编辑,`createFontExtensionFromBackend` 会同步 `fontFamily/fontSize/fontWeight/lineHeight`。
|
|
||||||
- 可在通用设置中的“滚轮缩放”手势下临时调整字号。
|
|
||||||
|
|
||||||
## 小地图/装饰色
|
|
||||||
- `minimap` 扩展读取主题中的 `accent` 颜色,用于高亮当前视区。
|
|
||||||
- `textHighlight` 扩展的默认背景色可在扩展设置中配置。
|
|
||||||
|
|
||||||
## 截图建议
|
|
||||||
- 展示暗/亮主题对比。
|
|
||||||
- 展示主题编辑对话框,标出关键字段。
|
|
||||||
- 展示自定义主题应用后的编辑器界面。
|
|
||||||
|
|
||||||
> 如果希望导入 VSCode `.json` 主题,可将颜色映射到 `ThemeColorConfig` 后写入数据库,或等待官方导入工具上线。
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# 常见问题与故障排查
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为错误提示或日志查看界面的截图。
|
|
||||||
|
|
||||||
## 安装与启动
|
|
||||||
| 问题 | 可能原因 | 解决步骤 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 启动白屏 | WebView2 缺失或缓存损坏 | 安装 WebView2 Runtime;删除 `%APPDATA%/voidraft/Cache` 后重启 |
|
|
||||||
| 双击无反应 | 被安全策略拦截 | 以管理员运行或使用便携版;验证 SHA256 后加入白名单 |
|
|
||||||
| `wails3 dev` 报错 | Go/Node 版本不符或缺少 WebView2 | 确保 Go 1.21+、Node 18+,安装 WebView2 |
|
|
||||||
|
|
||||||
## 编辑器
|
|
||||||
| 问题 | 原因 | 解决 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 格式化按钮灰色 | 当前块语言无对应 Prettier parser | 更换语言或安装支持的语言扩展(未来版本) |
|
|
||||||
| 块解析错误 | 分隔符格式不正确 | 确认 `∞∞∞language` 后跟换行;可用自动重建语法树命令 |
|
|
||||||
| 翻译浮层不出现 | 选区过短或超过最大长度 | 在设置 > 扩展 > translator 中调整阈值 |
|
|
||||||
| 小地图不同步 | 编辑器实例未刷新 | 切换文档或重开应用,检查扩展是否被禁用 |
|
|
||||||
|
|
||||||
## 窗口与多实例
|
|
||||||
| 问题 | 原因 | 解决 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
|
|
||||||
| 全局热键无效 | 与系统/其他软件冲突 | 在设置 > 通用改用非系统占用组合,比如 `Ctrl+Alt+Space` |
|
|
||||||
| 关闭窗口直接退出 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
|
|
||||||
|
|
||||||
## HTTP 客户端
|
|
||||||
| 问题 | 可能原因 | 解决 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 发送失败,提示 `proxy` | 系统代理配置异常 | 在系统代理或环境变量中设置 HTTP(S)_PROXY,或关闭代理再试 |
|
|
||||||
| 响应乱码 | 服务器未声明编码 | 手动在请求头中加 `accept-charset: utf-8`,或在响应视图切换编码(计划) |
|
|
||||||
| 变量未替换 | 变量名拼写或作用域错误 | 确认 `@var` 定义位置,使用 `{{name}}` 语法 |
|
|
||||||
|
|
||||||
## 数据与备份
|
|
||||||
| 问题 | 可能原因 | 解决 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 自动备份停在 “未初始化” | Repo URL/认证缺失 | 补全备份配置或关闭自动备份 |
|
|
||||||
| Push 失败 | Token 权限不足或网络问题 | 为 Token 开启 `repo` scope;检查代理;稍后再试 |
|
|
||||||
| 数据目录迁移后文件缺失 | 未重启或权限不足 | 修改 `dataPath` 后重启应用;确保目标目录可写 |
|
|
||||||
|
|
||||||
## 更新
|
|
||||||
| 问题 | 原因 | 解决 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 检查更新超时 | 主源不可达 | 切换到备用源或关闭代理重试 |
|
|
||||||
| 下载完成但未重启 | 权限或文件被占用 | 以管理员运行,关闭杀毒软件后重试 |
|
|
||||||
|
|
||||||
## 收集日志
|
|
||||||
- Wails 日志:`%LOCALAPPDATA%/voidraft/logs/*.log`。
|
|
||||||
- 终端调试:运行 `wails3 dev` 并观察控制台输出。
|
|
||||||
- 若提交 Issue,请附上:系统版本、voidraft 版本、日志片段、复现步骤。
|
|
||||||
|
|
||||||
> 仍未解决?请到 GitHub Issues 提交反馈,并尽可能附上截图和日志。
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 界面总览
|
|
||||||
|
|
||||||

|
|
||||||
> 替换为包含顶部工具栏、块区域、右侧小地图、底部状态栏的完整截图。
|
|
||||||
|
|
||||||
voidraft 的主窗口由四个区域组成:
|
|
||||||
|
|
||||||
| 区域 | 位置 | 作用 | 相关代码 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 工具栏 | 顶部浮层 | 文档切换、块语言选择、格式化、Markdown 预览、窗口置顶、更新提示、进入设置 | `frontend/src/components/toolbar/Toolbar.vue` |
|
|
||||||
| 编辑器主体 | 中央 | CodeMirror 6 视图,承载块编辑、HTTP 运行器、翻译按钮等 | `frontend/src/views/editor/Editor.vue` + `extensions` |
|
|
||||||
| 导航辅助 | 右侧 | 小地图、滚动条、块徽标、HTTP 运行按钮 | `extensions/minimap`, `codeblock/decorations.ts` |
|
|
||||||
| 底部状态 | 左下角 | 行数、字符数、选区统计、文档脏状态 | `editorStore.documentStats` |
|
|
||||||
|
|
||||||
## 工具栏详解
|
|
||||||
| 项 | 说明 | 快捷入口 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 文档切换器 | 展开后列出全部文档,支持搜索、创建、在新窗口打开 | 同步 `DocumentService.ListAllDocumentsMeta` |
|
|
||||||
| 块语言下拉 | 当前块语言,列表取自 `lang-parser/languages.ts`,支持搜索 | 鼠标选择或输入语言 token |
|
|
||||||
| Pin(窗口置顶) | 临时 / 永久置顶切换,调用 `SystemService.SetWindowOnTop` 与 `config.general.alwaysOnTop` | Alt+Space(自定义) |
|
|
||||||
| Format / Preview | 对当前块执行 Prettier 或打开 Markdown 预览 | `Ctrl+Shift+F` / 工具栏按钮 |
|
|
||||||
| 更新提示 | 轮询 `SelfUpdateService`,有更新时显示小点,可直接“检查/下载/重启” | 设置 > 更新 |
|
|
||||||
| 设置入口 | 跳转到 Vue Router 的 `/settings` 页面 | `Ctrl+,` |
|
|
||||||
|
|
||||||
## 多文档视图
|
|
||||||
- **标签页(可选)**:在设置 > 通用中启用“标签页模式”,`tabStore` 将当前文档加入 tab bar,支持拖拽、批量关闭。
|
|
||||||
- **多窗口**:以文档列表右键「在新窗口中打开」或命令面板为入口。`WindowService` 会根据文档 ID 命名窗口,`WindowSnapService` 自动吸附。
|
|
||||||
- **系统托盘**:关闭窗口时默认最小化到托盘,可在托盘图标中重新唤醒或彻底退出。
|
|
||||||
|
|
||||||
## 面板与浮层
|
|
||||||
- **Markdown 预览**:针对选中的 Markdown 块,面板会贴在右侧,支持实时滚动同步、关闭动画。
|
|
||||||
- **HTTP 响应**:运行后在块底部自动插入 `### Response`,可展开查看头部/体/耗时。
|
|
||||||
- **翻译浮层**:选中文本后自动出现按钮,点击后显示结果卡片,附带复制、语种切换。
|
|
||||||
|
|
||||||
## 快捷状态
|
|
||||||
- **底部统计**:
|
|
||||||
- `Ln`:当前块内行号。
|
|
||||||
- `Ch`:字符数。
|
|
||||||
- `Sel`:选区字符数。
|
|
||||||
- **右上角加载动画**:当编辑器实例加载或切换文档时显示,遵循 `enableLoadingAnimation` 设置。
|
|
||||||
|
|
||||||
## 建议截图
|
|
||||||
1. 默认深色主题 + 多块示例。
|
|
||||||
2. 打开 Markdown 预览 + 小地图。
|
|
||||||
3. 展示 HTTP 块运行按钮与响应卡片。
|
|
||||||
4. 展示标签页或多窗口。
|
|
||||||
@@ -14,6 +14,7 @@ import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtensi
|
|||||||
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
|
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
|
||||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||||
|
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
||||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||||
import {
|
import {
|
||||||
createDynamicExtensions,
|
createDynamicExtensions,
|
||||||
@@ -242,6 +243,11 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
fontWeight: configStore.config.editing.fontWeight
|
fontWeight: configStore.config.editing.fontWeight
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const wheelZoomExtension = createWheelZoomExtension(
|
||||||
|
() => configStore.increaseFontSize(),
|
||||||
|
() => configStore.decreaseFontSize()
|
||||||
|
);
|
||||||
|
|
||||||
// 统计扩展
|
// 统计扩展
|
||||||
const statsExtension = createStatsUpdateExtension(updateDocumentStats);
|
const statsExtension = createStatsUpdateExtension(updateDocumentStats);
|
||||||
|
|
||||||
@@ -287,6 +293,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
themeExtension,
|
themeExtension,
|
||||||
...tabExtensions,
|
...tabExtensions,
|
||||||
fontExtension,
|
fontExtension,
|
||||||
|
wheelZoomExtension,
|
||||||
statsExtension,
|
statsExtension,
|
||||||
contentChangeExtension,
|
contentChangeExtension,
|
||||||
codeBlockExtension,
|
codeBlockExtension,
|
||||||
@@ -707,12 +714,15 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 更新前端编辑器扩展 - 应用于所有实例
|
// 更新前端编辑器扩展 - 应用于所有实例
|
||||||
const manager = getExtensionManager();
|
const manager = getExtensionManager();
|
||||||
if (manager) {
|
if (manager) {
|
||||||
// 使用立即更新模式,跳过防抖
|
// 直接更新前端扩展至所有视图
|
||||||
manager.updateExtensionImmediate(id, enabled, config || {});
|
manager.updateExtension(id, enabled, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载扩展配置
|
// 重新加载扩展配置
|
||||||
await extensionStore.loadExtensions();
|
await extensionStore.loadExtensions();
|
||||||
|
if (manager) {
|
||||||
|
manager.initExtensions(extensionStore.extensions);
|
||||||
|
}
|
||||||
|
|
||||||
await applyKeymapSettings();
|
await applyKeymapSettings();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
|
|||||||
import {useEditorStore} from '@/stores/editorStore';
|
import {useEditorStore} from '@/stores/editorStore';
|
||||||
import {useDocumentStore} from '@/stores/documentStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
|
|
||||||
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
||||||
import {useWindowStore} from "@/stores/windowStore";
|
import {useWindowStore} from "@/stores/windowStore";
|
||||||
import LoadingScreen from '@/components/loading/LoadingScreen.vue';
|
import LoadingScreen from '@/components/loading/LoadingScreen.vue';
|
||||||
@@ -19,12 +18,6 @@ const editorElement = ref<HTMLElement | null>(null);
|
|||||||
|
|
||||||
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
|
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
|
||||||
|
|
||||||
// 创建滚轮缩放处理器
|
|
||||||
const wheelHandler = createWheelZoomHandler(
|
|
||||||
configStore.increaseFontSize,
|
|
||||||
configStore.decreaseFontSize
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!editorElement.value) return;
|
if (!editorElement.value) return;
|
||||||
|
|
||||||
@@ -38,16 +31,9 @@ onMounted(async () => {
|
|||||||
editorStore.setEditorContainer(editorElement.value);
|
editorStore.setEditorContainer(editorElement.value);
|
||||||
|
|
||||||
await tabStore.initializeTab();
|
await tabStore.initializeTab();
|
||||||
|
|
||||||
// 添加滚轮事件监听
|
|
||||||
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
// 移除滚轮事件监听
|
|
||||||
if (editorElement.value) {
|
|
||||||
editorElement.value.removeEventListener('wheel', wheelHandler);
|
|
||||||
}
|
|
||||||
editorStore.clearAllEditors();
|
editorStore.clearAllEditors();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -88,7 +74,6 @@ onBeforeUnmount(() => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载动画过渡效果
|
|
||||||
.loading-fade-enter-active,
|
.loading-fade-enter-active,
|
||||||
.loading-fade-leave-active {
|
.loading-fade-leave-active {
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
@@ -99,3 +84,4 @@ onBeforeUnmount(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
// 处理滚轮缩放字体的事件处理函数
|
import {EditorView} from '@codemirror/view';
|
||||||
export const createWheelZoomHandler = (
|
import type {Extension} from '@codemirror/state';
|
||||||
increaseFontSize: () => void,
|
|
||||||
decreaseFontSize: () => void
|
type FontAdjuster = () => Promise<void> | void;
|
||||||
) => {
|
|
||||||
return (event: WheelEvent) => {
|
const runAdjuster = (adjuster: FontAdjuster) => {
|
||||||
// 检查是否按住了Ctrl键
|
try {
|
||||||
if (event.ctrlKey) {
|
const result = adjuster();
|
||||||
// 阻止默认行为(防止页面缩放)
|
if (result && typeof (result as Promise<void>).then === 'function') {
|
||||||
|
(result as Promise<void>).catch((error) => {
|
||||||
|
console.error('Failed to adjust font size:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to adjust font size:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createWheelZoomExtension = (
|
||||||
|
increaseFontSize: FontAdjuster,
|
||||||
|
decreaseFontSize: FontAdjuster
|
||||||
|
): Extension => {
|
||||||
|
return EditorView.domEventHandlers({
|
||||||
|
wheel(event) {
|
||||||
|
if (!event.ctrlKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// 根据滚轮方向增大或减小字体
|
|
||||||
if (event.deltaY < 0) {
|
if (event.deltaY < 0) {
|
||||||
// 向上滚动,增大字体
|
runAdjuster(increaseFontSize);
|
||||||
increaseFontSize();
|
} else if (event.deltaY > 0) {
|
||||||
} else {
|
runAdjuster(decreaseFontSize);
|
||||||
// 向下滚动,减小字体
|
|
||||||
decreaseFontSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Extension } from '@codemirror/state';
|
import { Extension } from '@codemirror/state';
|
||||||
import { useKeybindingStore } from '@/stores/keybindingStore';
|
import { useKeybindingStore } from '@/stores/keybindingStore';
|
||||||
import { useExtensionStore } from '@/stores/extensionStore';
|
import { useExtensionStore } from '@/stores/extensionStore';
|
||||||
import { KeymapManager } from './keymapManager';
|
import { Manager } from './manager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步创建快捷键扩展
|
* 异步创建快捷键扩展
|
||||||
@@ -23,7 +23,7 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
|
|||||||
// 获取启用的扩展ID列表
|
// 获取启用的扩展ID列表
|
||||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
||||||
|
|
||||||
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
|
return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,10 +37,10 @@ export const updateKeymapExtension = (view: any): void => {
|
|||||||
// 获取启用的扩展ID列表
|
// 获取启用的扩展ID列表
|
||||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
||||||
|
|
||||||
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
|
Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出相关模块
|
// 导出相关模块
|
||||||
export { KeymapManager } from './keymapManager';
|
export { Manager } from './manager';
|
||||||
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
|
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
|
||||||
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';
|
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';
|
||||||
@@ -8,7 +8,7 @@ import {getCommandHandler, isCommandRegistered} from './commands';
|
|||||||
* 快捷键管理器
|
* 快捷键管理器
|
||||||
* 负责将后端配置转换为CodeMirror快捷键扩展
|
* 负责将后端配置转换为CodeMirror快捷键扩展
|
||||||
*/
|
*/
|
||||||
export class KeymapManager {
|
export class Manager {
|
||||||
private static compartment = new Compartment();
|
private static compartment = new Compartment();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import {Compartment, Extension} from '@codemirror/state';
|
|
||||||
import {EditorView} from '@codemirror/view';
|
|
||||||
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
|
||||||
import {ExtensionState, EditorViewInfo, ExtensionFactory} from './types'
|
|
||||||
import {createDebounce} from '@/common/utils/debounce';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扩展管理器
|
|
||||||
* 负责管理所有动态扩展的注册、启用、禁用和配置更新
|
|
||||||
* 采用统一配置,多视图同步的设计模式
|
|
||||||
*/
|
|
||||||
export class ExtensionManager {
|
|
||||||
// 统一的扩展状态存储
|
|
||||||
private extensionStates = new Map<ExtensionID, ExtensionState>();
|
|
||||||
|
|
||||||
// 编辑器视图管理
|
|
||||||
private viewsMap = new Map<number, EditorViewInfo>();
|
|
||||||
private activeViewId: number | null = null;
|
|
||||||
|
|
||||||
// 注册的扩展工厂
|
|
||||||
private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
|
|
||||||
|
|
||||||
// 防抖处理
|
|
||||||
private debouncedUpdateFunctions = new Map<ExtensionID, {
|
|
||||||
debouncedFn: (enabled: boolean, config: any) => void;
|
|
||||||
cancel: () => void;
|
|
||||||
flush: () => void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册扩展工厂
|
|
||||||
* @param id 扩展ID
|
|
||||||
* @param factory 扩展工厂
|
|
||||||
*/
|
|
||||||
registerExtension(id: ExtensionID, factory: ExtensionFactory): void {
|
|
||||||
this.extensionFactories.set(id, factory);
|
|
||||||
|
|
||||||
// 创建初始状态
|
|
||||||
if (!this.extensionStates.has(id)) {
|
|
||||||
const compartment = new Compartment();
|
|
||||||
const defaultConfig = factory.getDefaultConfig();
|
|
||||||
|
|
||||||
this.extensionStates.set(id, {
|
|
||||||
id,
|
|
||||||
factory,
|
|
||||||
config: defaultConfig,
|
|
||||||
enabled: false,
|
|
||||||
compartment,
|
|
||||||
extension: [] // 默认为空扩展(禁用状态)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为每个扩展创建防抖函数
|
|
||||||
if (!this.debouncedUpdateFunctions.has(id)) {
|
|
||||||
const { debouncedFn, cancel, flush } = createDebounce(
|
|
||||||
(enabled: boolean, config: any) => {
|
|
||||||
this.updateExtensionImmediate(id, enabled, config);
|
|
||||||
},
|
|
||||||
{ delay: 300 }
|
|
||||||
);
|
|
||||||
|
|
||||||
this.debouncedUpdateFunctions.set(id, {
|
|
||||||
debouncedFn,
|
|
||||||
cancel,
|
|
||||||
flush
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有注册的扩展ID列表
|
|
||||||
*/
|
|
||||||
getRegisteredExtensions(): ExtensionID[] {
|
|
||||||
return Array.from(this.extensionFactories.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查扩展是否已注册
|
|
||||||
* @param id 扩展ID
|
|
||||||
*/
|
|
||||||
isExtensionRegistered(id: ExtensionID): boolean {
|
|
||||||
return this.extensionFactories.has(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从后端配置初始化扩展状态
|
|
||||||
* @param extensionConfigs 后端扩展配置列表
|
|
||||||
*/
|
|
||||||
initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void {
|
|
||||||
for (const config of extensionConfigs) {
|
|
||||||
const factory = this.extensionFactories.get(config.id);
|
|
||||||
if (!factory) continue;
|
|
||||||
|
|
||||||
// 验证配置
|
|
||||||
if (factory.validateConfig && !factory.validateConfig(config.config)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建扩展实例
|
|
||||||
const extension = config.enabled ? factory.create(config.config) : [];
|
|
||||||
|
|
||||||
// 如果状态已存在则更新,否则创建新状态
|
|
||||||
if (this.extensionStates.has(config.id)) {
|
|
||||||
const state = this.extensionStates.get(config.id)!;
|
|
||||||
state.config = config.config;
|
|
||||||
state.enabled = config.enabled;
|
|
||||||
state.extension = extension;
|
|
||||||
} else {
|
|
||||||
const compartment = new Compartment();
|
|
||||||
this.extensionStates.set(config.id, {
|
|
||||||
id: config.id,
|
|
||||||
factory,
|
|
||||||
config: config.config,
|
|
||||||
enabled: config.enabled,
|
|
||||||
compartment,
|
|
||||||
extension
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to initialize extension ${config.id}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取初始扩展配置数组(用于创建编辑器)
|
|
||||||
* @returns CodeMirror扩展数组
|
|
||||||
*/
|
|
||||||
getInitialExtensions(): Extension[] {
|
|
||||||
const extensions: Extension[] = [];
|
|
||||||
|
|
||||||
// 为每个注册的扩展添加compartment
|
|
||||||
for (const state of this.extensionStates.values()) {
|
|
||||||
extensions.push(state.compartment.of(state.extension));
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置编辑器视图
|
|
||||||
* @param view 编辑器视图实例
|
|
||||||
* @param documentId 文档ID
|
|
||||||
*/
|
|
||||||
setView(view: EditorView, documentId: number): void {
|
|
||||||
// 保存视图信息
|
|
||||||
this.viewsMap.set(documentId, {
|
|
||||||
view,
|
|
||||||
documentId,
|
|
||||||
registered: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置当前活动视图
|
|
||||||
this.activeViewId = documentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前活动视图
|
|
||||||
*/
|
|
||||||
private getActiveView(): EditorView | null {
|
|
||||||
if (this.activeViewId === null) return null;
|
|
||||||
const viewInfo = this.viewsMap.get(this.activeViewId);
|
|
||||||
return viewInfo ? viewInfo.view : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新单个扩展配置并应用到所有视图(带防抖功能)
|
|
||||||
* @param id 扩展ID
|
|
||||||
* @param enabled 是否启用
|
|
||||||
* @param config 扩展配置
|
|
||||||
*/
|
|
||||||
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
|
||||||
const debouncedUpdate = this.debouncedUpdateFunctions.get(id);
|
|
||||||
if (debouncedUpdate) {
|
|
||||||
debouncedUpdate.debouncedFn(enabled, config);
|
|
||||||
} else {
|
|
||||||
// 如果没有防抖函数,直接执行
|
|
||||||
this.updateExtensionImmediate(id, enabled, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 立即更新扩展(无防抖)
|
|
||||||
* @param id 扩展ID
|
|
||||||
* @param enabled 是否启用
|
|
||||||
* @param config 扩展配置
|
|
||||||
*/
|
|
||||||
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
|
||||||
// 获取扩展状态
|
|
||||||
const state = this.extensionStates.get(id);
|
|
||||||
if (!state) return;
|
|
||||||
|
|
||||||
// 获取工厂
|
|
||||||
const factory = state.factory;
|
|
||||||
|
|
||||||
// 验证配置
|
|
||||||
if (factory.validateConfig && !factory.validateConfig(config)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建新的扩展实例
|
|
||||||
const extension = enabled ? factory.create(config) : [];
|
|
||||||
|
|
||||||
// 更新内部状态
|
|
||||||
state.config = config;
|
|
||||||
state.enabled = enabled;
|
|
||||||
state.extension = extension;
|
|
||||||
|
|
||||||
// 应用到所有视图
|
|
||||||
this.applyExtensionToAllViews(id);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to update extension ${id}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将指定扩展的当前状态应用到所有视图
|
|
||||||
* @param id 扩展ID
|
|
||||||
*/
|
|
||||||
private applyExtensionToAllViews(id: ExtensionID): void {
|
|
||||||
const state = this.extensionStates.get(id);
|
|
||||||
if (!state) return;
|
|
||||||
|
|
||||||
// 遍历所有视图并应用更改
|
|
||||||
for (const viewInfo of this.viewsMap.values()) {
|
|
||||||
try {
|
|
||||||
if (!viewInfo.registered) continue;
|
|
||||||
|
|
||||||
viewInfo.view.dispatch({
|
|
||||||
effects: state.compartment.reconfigure(state.extension)
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取扩展当前状态
|
|
||||||
* @param id 扩展ID
|
|
||||||
*/
|
|
||||||
getExtensionState(id: ExtensionID): {
|
|
||||||
enabled: boolean
|
|
||||||
config: any
|
|
||||||
} | null {
|
|
||||||
const state = this.extensionStates.get(id);
|
|
||||||
if (!state) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: state.enabled,
|
|
||||||
config: state.config
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置扩展到默认配置
|
|
||||||
* @param id 扩展ID
|
|
||||||
*/
|
|
||||||
resetExtensionToDefault(id: ExtensionID): void {
|
|
||||||
const state = this.extensionStates.get(id);
|
|
||||||
if (!state) return;
|
|
||||||
|
|
||||||
const defaultConfig = state.factory.getDefaultConfig();
|
|
||||||
this.updateExtension(id, true, defaultConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从管理器中移除视图
|
|
||||||
* @param documentId 文档ID
|
|
||||||
*/
|
|
||||||
removeView(documentId: number): void {
|
|
||||||
if (this.activeViewId === documentId) {
|
|
||||||
this.activeViewId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.viewsMap.delete(documentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁管理器
|
|
||||||
*/
|
|
||||||
destroy(): void {
|
|
||||||
// 清除所有防抖函数
|
|
||||||
for (const { cancel } of this.debouncedUpdateFunctions.values()) {
|
|
||||||
cancel();
|
|
||||||
}
|
|
||||||
this.debouncedUpdateFunctions.clear();
|
|
||||||
|
|
||||||
this.viewsMap.clear();
|
|
||||||
this.activeViewId = null;
|
|
||||||
this.extensionFactories.clear();
|
|
||||||
this.extensionStates.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,301 +1,152 @@
|
|||||||
import {ExtensionManager} from './extensionManager';
|
import {Manager} from './manager';
|
||||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import {ExtensionFactory} from './types'
|
import {ExtensionDefinition} from './types';
|
||||||
|
|
||||||
// 导入现有扩展的创建函数
|
|
||||||
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
|
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
|
||||||
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
|
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
|
||||||
|
|
||||||
import {color} from '../extensions/colorSelector';
|
import {color} from '../extensions/colorSelector';
|
||||||
import {hyperLink} from '../extensions/hyperlink';
|
import {hyperLink} from '../extensions/hyperlink';
|
||||||
import {minimap} from '../extensions/minimap';
|
import {minimap} from '../extensions/minimap';
|
||||||
import {vscodeSearch} from '../extensions/vscodeSearch';
|
import {vscodeSearch} from '../extensions/vscodeSearch';
|
||||||
import {createCheckboxExtension} from '../extensions/checkbox';
|
import {createCheckboxExtension} from '../extensions/checkbox';
|
||||||
import {createTranslatorExtension} from '../extensions/translator';
|
import {createTranslatorExtension} from '../extensions/translator';
|
||||||
|
|
||||||
import {foldingOnIndent} from '../extensions/fold/foldExtension';
|
import {foldingOnIndent} from '../extensions/fold/foldExtension';
|
||||||
|
|
||||||
/**
|
type ExtensionEntry = {
|
||||||
* 彩虹括号扩展工厂
|
definition: ExtensionDefinition
|
||||||
*/
|
displayNameKey: string
|
||||||
export const rainbowBracketsFactory: ExtensionFactory = {
|
descriptionKey: string
|
||||||
create(_config: any) {
|
|
||||||
return rainbowBracketsExtension();
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
|
||||||
* 文本高亮扩展工厂
|
|
||||||
*/
|
|
||||||
export const textHighlightFactory: ExtensionFactory = {
|
|
||||||
create(config: any) {
|
|
||||||
return createTextHighlighter({
|
|
||||||
backgroundColor: config.backgroundColor || '#FFD700',
|
|
||||||
opacity: config.opacity || 0.3
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {
|
|
||||||
backgroundColor: '#FFD700', // 金黄色
|
|
||||||
opacity: 0.3 // 透明度
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object' &&
|
|
||||||
(!config.backgroundColor || typeof config.backgroundColor === 'string') &&
|
|
||||||
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
|
||||||
* 小地图扩展工厂
|
create,
|
||||||
*/
|
defaultConfig
|
||||||
export const minimapFactory: ExtensionFactory = {
|
});
|
||||||
create(config: any) {
|
|
||||||
const options = {
|
|
||||||
displayText: config.displayText || 'characters',
|
|
||||||
showOverlay: config.showOverlay || 'always',
|
|
||||||
autohide: config.autohide || false
|
|
||||||
};
|
|
||||||
return minimap(options);
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {
|
|
||||||
displayText: 'characters',
|
|
||||||
showOverlay: 'always',
|
|
||||||
autohide: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object' &&
|
|
||||||
(!config.displayText || typeof config.displayText === 'string') &&
|
|
||||||
(!config.showOverlay || typeof config.showOverlay === 'string') &&
|
|
||||||
(!config.autohide || typeof config.autohide === 'boolean');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
|
||||||
* 超链接扩展工厂
|
|
||||||
*/
|
|
||||||
export const hyperlinkFactory: ExtensionFactory = {
|
|
||||||
create(_config: any) {
|
|
||||||
return hyperLink;
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 颜色选择器扩展工厂
|
|
||||||
*/
|
|
||||||
export const colorSelectorFactory: ExtensionFactory = {
|
|
||||||
create(_config: any) {
|
|
||||||
return color;
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索扩展工厂
|
|
||||||
*/
|
|
||||||
export const searchFactory: ExtensionFactory = {
|
|
||||||
create(_config: any) {
|
|
||||||
return vscodeSearch;
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const foldFactory: ExtensionFactory = {
|
|
||||||
create(_config: any) {
|
|
||||||
return foldingOnIndent;
|
|
||||||
},
|
|
||||||
getDefaultConfig(): any {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
validateConfig(config: any): boolean {
|
|
||||||
return typeof config === 'object';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选择框扩展工厂
|
|
||||||
*/
|
|
||||||
export const checkboxFactory: ExtensionFactory = {
|
|
||||||
create(_config: any) {
|
|
||||||
return createCheckboxExtension();
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻译扩展工厂
|
|
||||||
*/
|
|
||||||
export const translatorFactory: ExtensionFactory = {
|
|
||||||
create(config: any) {
|
|
||||||
return createTranslatorExtension({
|
|
||||||
minSelectionLength: config.minSelectionLength || 2,
|
|
||||||
maxTranslationLength: config.maxTranslationLength || 5000,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {
|
|
||||||
minSelectionLength: 2,
|
|
||||||
maxTranslationLength: 5000,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所有扩展的统一配置
|
|
||||||
* 排除$zero值以避免TypeScript类型错误
|
|
||||||
*/
|
|
||||||
const EXTENSION_CONFIGS = {
|
|
||||||
|
|
||||||
// 编辑增强扩展
|
|
||||||
[ExtensionID.ExtensionRainbowBrackets]: {
|
[ExtensionID.ExtensionRainbowBrackets]: {
|
||||||
factory: rainbowBracketsFactory,
|
definition: defineExtension(() => rainbowBracketsExtension()),
|
||||||
displayNameKey: 'extensions.rainbowBrackets.name',
|
displayNameKey: 'extensions.rainbowBrackets.name',
|
||||||
descriptionKey: 'extensions.rainbowBrackets.description'
|
descriptionKey: 'extensions.rainbowBrackets.description'
|
||||||
},
|
},
|
||||||
[ExtensionID.ExtensionHyperlink]: {
|
[ExtensionID.ExtensionHyperlink]: {
|
||||||
factory: hyperlinkFactory,
|
definition: defineExtension(() => hyperLink),
|
||||||
displayNameKey: 'extensions.hyperlink.name',
|
displayNameKey: 'extensions.hyperlink.name',
|
||||||
descriptionKey: 'extensions.hyperlink.description'
|
descriptionKey: 'extensions.hyperlink.description'
|
||||||
},
|
},
|
||||||
[ExtensionID.ExtensionColorSelector]: {
|
[ExtensionID.ExtensionColorSelector]: {
|
||||||
factory: colorSelectorFactory,
|
definition: defineExtension(() => color),
|
||||||
displayNameKey: 'extensions.colorSelector.name',
|
displayNameKey: 'extensions.colorSelector.name',
|
||||||
descriptionKey: 'extensions.colorSelector.description'
|
descriptionKey: 'extensions.colorSelector.description'
|
||||||
},
|
},
|
||||||
[ExtensionID.ExtensionTranslator]: {
|
[ExtensionID.ExtensionTranslator]: {
|
||||||
factory: translatorFactory,
|
definition: defineExtension((config: any) => createTranslatorExtension({
|
||||||
|
minSelectionLength: config?.minSelectionLength ?? 2,
|
||||||
|
maxTranslationLength: config?.maxTranslationLength ?? 5000
|
||||||
|
}), {
|
||||||
|
minSelectionLength: 2,
|
||||||
|
maxTranslationLength: 5000
|
||||||
|
}),
|
||||||
displayNameKey: 'extensions.translator.name',
|
displayNameKey: 'extensions.translator.name',
|
||||||
descriptionKey: 'extensions.translator.description'
|
descriptionKey: 'extensions.translator.description'
|
||||||
},
|
},
|
||||||
|
|
||||||
// UI增强扩展
|
|
||||||
[ExtensionID.ExtensionMinimap]: {
|
[ExtensionID.ExtensionMinimap]: {
|
||||||
factory: minimapFactory,
|
definition: defineExtension((config: any) => minimap({
|
||||||
|
displayText: config?.displayText ?? 'characters',
|
||||||
|
showOverlay: config?.showOverlay ?? 'always',
|
||||||
|
autohide: config?.autohide ?? false
|
||||||
|
}), {
|
||||||
|
displayText: 'characters',
|
||||||
|
showOverlay: 'always',
|
||||||
|
autohide: false
|
||||||
|
}),
|
||||||
displayNameKey: 'extensions.minimap.name',
|
displayNameKey: 'extensions.minimap.name',
|
||||||
descriptionKey: 'extensions.minimap.description'
|
descriptionKey: 'extensions.minimap.description'
|
||||||
},
|
},
|
||||||
|
|
||||||
// 工具扩展
|
|
||||||
[ExtensionID.ExtensionSearch]: {
|
[ExtensionID.ExtensionSearch]: {
|
||||||
factory: searchFactory,
|
definition: defineExtension(() => vscodeSearch),
|
||||||
displayNameKey: 'extensions.search.name',
|
displayNameKey: 'extensions.search.name',
|
||||||
descriptionKey: 'extensions.search.description'
|
descriptionKey: 'extensions.search.description'
|
||||||
},
|
},
|
||||||
|
|
||||||
[ExtensionID.ExtensionFold]: {
|
[ExtensionID.ExtensionFold]: {
|
||||||
factory: foldFactory,
|
definition: defineExtension(() => foldingOnIndent),
|
||||||
displayNameKey: 'extensions.fold.name',
|
displayNameKey: 'extensions.fold.name',
|
||||||
descriptionKey: 'extensions.fold.description'
|
descriptionKey: 'extensions.fold.description'
|
||||||
},
|
},
|
||||||
[ExtensionID.ExtensionTextHighlight]: {
|
[ExtensionID.ExtensionTextHighlight]: {
|
||||||
factory: textHighlightFactory,
|
definition: defineExtension((config: any) => createTextHighlighter({
|
||||||
|
backgroundColor: config?.backgroundColor ?? '#FFD700',
|
||||||
|
opacity: config?.opacity ?? 0.3
|
||||||
|
}), {
|
||||||
|
backgroundColor: '#FFD700',
|
||||||
|
opacity: 0.3
|
||||||
|
}),
|
||||||
displayNameKey: 'extensions.textHighlight.name',
|
displayNameKey: 'extensions.textHighlight.name',
|
||||||
descriptionKey: 'extensions.textHighlight.description'
|
descriptionKey: 'extensions.textHighlight.description'
|
||||||
},
|
},
|
||||||
[ExtensionID.ExtensionCheckbox]: {
|
[ExtensionID.ExtensionCheckbox]: {
|
||||||
factory: checkboxFactory,
|
definition: defineExtension(() => createCheckboxExtension()),
|
||||||
displayNameKey: 'extensions.checkbox.name',
|
displayNameKey: 'extensions.checkbox.name',
|
||||||
descriptionKey: 'extensions.checkbox.description'
|
descriptionKey: 'extensions.checkbox.description'
|
||||||
}
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID =>
|
||||||
|
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id);
|
||||||
|
|
||||||
|
const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => {
|
||||||
|
if (!isRegisteredExtension(id)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return EXTENSION_REGISTRY[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export function registerAllExtensions(manager: Manager): void {
|
||||||
* 注册所有扩展工厂到管理器
|
(Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
|
||||||
* @param manager 扩展管理器实例
|
manager.registerExtension(id, entry.definition);
|
||||||
*/
|
|
||||||
export function registerAllExtensions(manager: ExtensionManager): void {
|
|
||||||
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
|
|
||||||
manager.registerExtension(id as ExtensionID, config.factory);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取扩展工厂的显示名称
|
|
||||||
* @param id 扩展ID
|
|
||||||
* @returns 显示名称
|
|
||||||
*/
|
|
||||||
export function getExtensionDisplayName(id: ExtensionID): string {
|
export function getExtensionDisplayName(id: ExtensionID): string {
|
||||||
const config = EXTENSION_CONFIGS[id as ExtensionID];
|
const entry = getRegistryEntry(id);
|
||||||
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id;
|
return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取扩展工厂的描述
|
|
||||||
* @param id 扩展ID
|
|
||||||
* @returns 描述
|
|
||||||
*/
|
|
||||||
export function getExtensionDescription(id: ExtensionID): string {
|
export function getExtensionDescription(id: ExtensionID): string {
|
||||||
const config = EXTENSION_CONFIGS[id as ExtensionID];
|
const entry = getRegistryEntry(id);
|
||||||
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : '';
|
return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
|
||||||
* 获取扩展工厂实例
|
return getRegistryEntry(id)?.definition;
|
||||||
* @param id 扩展ID
|
|
||||||
* @returns 扩展工厂实例
|
|
||||||
*/
|
|
||||||
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
|
|
||||||
return EXTENSION_CONFIGS[id as ExtensionID]?.factory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取扩展的默认配置
|
|
||||||
* @param id 扩展ID
|
|
||||||
* @returns 默认配置对象
|
|
||||||
*/
|
|
||||||
export function getExtensionDefaultConfig(id: ExtensionID): any {
|
export function getExtensionDefaultConfig(id: ExtensionID): any {
|
||||||
const factory = getExtensionFactory(id);
|
const definition = getExtensionDefinition(id);
|
||||||
return factory?.getDefaultConfig() || {};
|
if (!definition) return {};
|
||||||
|
return cloneConfig(definition.defaultConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查扩展是否有配置项
|
|
||||||
* @param id 扩展ID
|
|
||||||
* @returns 是否有配置项
|
|
||||||
*/
|
|
||||||
export function hasExtensionConfig(id: ExtensionID): boolean {
|
export function hasExtensionConfig(id: ExtensionID): boolean {
|
||||||
const defaultConfig = getExtensionDefaultConfig(id);
|
return Object.keys(getExtensionDefaultConfig(id)).length > 0;
|
||||||
return Object.keys(defaultConfig).length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有可用扩展的ID列表
|
|
||||||
* @returns 扩展ID数组
|
|
||||||
*/
|
|
||||||
export function getAllExtensionIds(): ExtensionID[] {
|
export function getAllExtensionIds(): ExtensionID[] {
|
||||||
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[];
|
return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cloneConfig = (config: any) => {
|
||||||
|
if (Array.isArray(config)) {
|
||||||
|
return config.map(cloneConfig);
|
||||||
|
}
|
||||||
|
if (config && typeof config === 'object') {
|
||||||
|
return Object.keys(config).reduce((acc, key) => {
|
||||||
|
acc[key] = cloneConfig(config[key]);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any>);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {Extension} from '@codemirror/state';
|
import {Extension} from '@codemirror/state';
|
||||||
import {EditorView} from '@codemirror/view';
|
import {EditorView} from '@codemirror/view';
|
||||||
import {useExtensionStore} from '@/stores/extensionStore';
|
import {useExtensionStore} from '@/stores/extensionStore';
|
||||||
import {ExtensionManager} from './extensionManager';
|
import {Manager} from './manager';
|
||||||
import {registerAllExtensions} from './extensions';
|
import {registerAllExtensions} from './extensions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局扩展管理器实例
|
* 全局扩展管理器实例
|
||||||
*/
|
*/
|
||||||
const extensionManager = new ExtensionManager();
|
const extensionManager = new Manager();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步创建动态扩展
|
* 异步创建动态扩展
|
||||||
@@ -26,7 +26,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化扩展管理器配置
|
// 初始化扩展管理器配置
|
||||||
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions);
|
extensionManager.initExtensions(extensionStore.extensions);
|
||||||
|
|
||||||
// 获取初始扩展配置
|
// 获取初始扩展配置
|
||||||
return extensionManager.getInitialExtensions();
|
return extensionManager.getInitialExtensions();
|
||||||
@@ -36,7 +36,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
|
|||||||
* 获取扩展管理器实例
|
* 获取扩展管理器实例
|
||||||
* @returns 扩展管理器
|
* @returns 扩展管理器
|
||||||
*/
|
*/
|
||||||
export const getExtensionManager = (): ExtensionManager => {
|
export const getExtensionManager = (): Manager => {
|
||||||
return extensionManager;
|
return extensionManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,5 +58,5 @@ export const removeExtensionManagerView = (documentId: number): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 导出相关模块
|
// 导出相关模块
|
||||||
export {ExtensionManager} from './extensionManager';
|
export {Manager} from './manager';
|
||||||
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';
|
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';
|
||||||
135
frontend/src/views/editor/manager/manager.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import {Compartment, Extension} from '@codemirror/state';
|
||||||
|
import {EditorView} from '@codemirror/view';
|
||||||
|
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
import {ExtensionDefinition, ExtensionState} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展管理器
|
||||||
|
* 负责注册、初始化与同步所有动态扩展
|
||||||
|
*/
|
||||||
|
export class Manager {
|
||||||
|
private extensionStates = new Map<ExtensionID, ExtensionState>();
|
||||||
|
private views = new Map<number, EditorView>();
|
||||||
|
|
||||||
|
registerExtension(id: ExtensionID, definition: ExtensionDefinition): void {
|
||||||
|
const existingState = this.extensionStates.get(id);
|
||||||
|
if (existingState) {
|
||||||
|
existingState.definition = definition;
|
||||||
|
if (existingState.config === undefined) {
|
||||||
|
existingState.config = this.cloneConfig(definition.defaultConfig ?? {});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const compartment = new Compartment();
|
||||||
|
const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {});
|
||||||
|
this.extensionStates.set(id, {
|
||||||
|
id,
|
||||||
|
definition,
|
||||||
|
config: defaultConfig,
|
||||||
|
enabled: false,
|
||||||
|
compartment,
|
||||||
|
extension: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initExtensions(extensionConfigs: ExtensionConfig[]): void {
|
||||||
|
for (const config of extensionConfigs) {
|
||||||
|
const state = this.extensionStates.get(config.id);
|
||||||
|
if (!state) continue;
|
||||||
|
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
|
||||||
|
this.commitExtensionState(state, config.enabled, resolvedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getInitialExtensions(): Extension[] {
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
for (const state of this.extensionStates.values()) {
|
||||||
|
extensions.push(state.compartment.of(state.extension));
|
||||||
|
}
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
setView(view: EditorView, documentId: number): void {
|
||||||
|
this.views.set(documentId, view);
|
||||||
|
this.applyAllExtensionsToView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateExtension(id: ExtensionID, enabled: boolean, config?: any): void {
|
||||||
|
const state = this.extensionStates.get(id);
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
const resolvedConfig = this.resolveConfig(state, config);
|
||||||
|
this.commitExtensionState(state, enabled, resolvedConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeView(documentId: number): void {
|
||||||
|
this.views.delete(documentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.views.clear();
|
||||||
|
this.extensionStates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveConfig(state: ExtensionState, config?: any): any {
|
||||||
|
if (config !== undefined) {
|
||||||
|
return this.cloneConfig(config);
|
||||||
|
}
|
||||||
|
if (state.config !== undefined) {
|
||||||
|
return this.cloneConfig(state.config);
|
||||||
|
}
|
||||||
|
return this.cloneConfig(state.definition.defaultConfig ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private commitExtensionState(state: ExtensionState, enabled: boolean, config: any): void {
|
||||||
|
try {
|
||||||
|
const runtimeExtension = enabled ? state.definition.create(config) : [];
|
||||||
|
state.enabled = enabled;
|
||||||
|
state.config = config;
|
||||||
|
state.extension = runtimeExtension;
|
||||||
|
this.applyExtensionToAllViews(state.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to update extension ${state.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyExtensionToAllViews(id: ExtensionID): void {
|
||||||
|
const state = this.extensionStates.get(id);
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
for (const [documentId, view] of this.views.entries()) {
|
||||||
|
try {
|
||||||
|
view.dispatch({effects: state.compartment.reconfigure(state.extension)});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to apply extension ${id} to document ${documentId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyAllExtensionsToView(view: EditorView): void {
|
||||||
|
const effects: any[] = [];
|
||||||
|
for (const state of this.extensionStates.values()) {
|
||||||
|
effects.push(state.compartment.reconfigure(state.extension));
|
||||||
|
}
|
||||||
|
if (effects.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
view.dispatch({effects});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to register extensions on view:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cloneConfig<T>(config: T): T {
|
||||||
|
if (Array.isArray(config)) {
|
||||||
|
return config.map(item => this.cloneConfig(item)) as unknown as T;
|
||||||
|
}
|
||||||
|
if (config && typeof config === 'object') {
|
||||||
|
return Object.keys(config as Record<string, any>).reduce((acc, key) => {
|
||||||
|
(acc as any)[key] = this.cloneConfig((config as Record<string, any>)[key]);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any>) as T;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1 @@
|
|||||||
import {Compartment, Extension} from '@codemirror/state';
|
import {Compartment, Extension} from '@codemirror/state';
|
||||||
import {EditorView} from '@codemirror/view';
|
|
||||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
|
||||||
/**
|
|
||||||
* 扩展工厂接口
|
|
||||||
* 每个扩展需要实现此接口来创建和配置扩展
|
|
||||||
*/
|
|
||||||
export interface ExtensionFactory {
|
|
||||||
/**
|
|
||||||
* 创建扩展实例
|
|
||||||
* @param config 扩展配置
|
|
||||||
* @returns CodeMirror扩展
|
|
||||||
*/
|
|
||||||
create(config: any): Extension
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取默认配置
|
|
||||||
* @returns 默认配置对象
|
|
||||||
*/
|
|
||||||
getDefaultConfig(): any
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证配置
|
|
||||||
* @param config 配置对象
|
|
||||||
* @returns 是否有效
|
|
||||||
*/
|
|
||||||
validateConfig?(config: any): boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扩展状态
|
|
||||||
*/
|
|
||||||
export interface ExtensionState {
|
|
||||||
id: ExtensionID
|
|
||||||
factory: ExtensionFactory
|
|
||||||
config: any
|
|
||||||
enabled: boolean
|
|
||||||
compartment: Compartment
|
|
||||||
extension: Extension
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 视图信息
|
|
||||||
*/
|
|
||||||
export interface EditorViewInfo {
|
|
||||||
view: EditorView
|
|
||||||
documentId: number
|
|
||||||
registered: boolean
|
|
||||||
}
|
|
||||||
@@ -66,10 +66,12 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
|
|||||||
if (!extension) return;
|
if (!extension) return;
|
||||||
|
|
||||||
// 更新配置
|
// 更新配置
|
||||||
const updatedConfig = {...extension.config, [configKey]: value};
|
const updatedConfig = {...extension.config};
|
||||||
|
if (value === undefined) {
|
||||||
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`);
|
delete updatedConfig[configKey];
|
||||||
|
} else {
|
||||||
|
updatedConfig[configKey] = value;
|
||||||
|
}
|
||||||
// 使用editorStore的updateExtension方法更新,确保应用到所有编辑器实例
|
// 使用editorStore的updateExtension方法更新,确保应用到所有编辑器实例
|
||||||
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
|
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
|
|||||||
// 重置扩展到默认配置
|
// 重置扩展到默认配置
|
||||||
const resetExtension = async (extensionId: ExtensionID) => {
|
const resetExtension = async (extensionId: ExtensionID) => {
|
||||||
try {
|
try {
|
||||||
// 重置到默认配置(后端)
|
// 重置到默认配置
|
||||||
await ExtensionService.ResetExtensionToDefault(extensionId);
|
await ExtensionService.ResetExtensionToDefault(extensionId);
|
||||||
|
|
||||||
// 重新加载扩展状态以获取最新配置
|
// 重新加载扩展状态以获取最新配置
|
||||||
@@ -92,63 +94,65 @@ const resetExtension = async (extensionId: ExtensionID) => {
|
|||||||
if (extension) {
|
if (extension) {
|
||||||
// 通过editorStore更新,确保所有视图都能同步
|
// 通过editorStore更新,确保所有视图都能同步
|
||||||
await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
|
await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
|
||||||
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reset extension:', error);
|
console.error('Failed to reset extension:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 配置项类型定义
|
const getConfigValue = (
|
||||||
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select'
|
config: Record<string, any> | undefined,
|
||||||
|
configKey: string,
|
||||||
interface SelectOption {
|
defaultValue: any
|
||||||
value: any
|
) => {
|
||||||
label: string
|
if (config && Object.prototype.hasOwnProperty.call(config, configKey)) {
|
||||||
}
|
return config[configKey];
|
||||||
|
|
||||||
interface ConfigItemMeta {
|
|
||||||
type: ConfigItemType
|
|
||||||
options?: SelectOption[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只保留 select 类型的配置项元数据
|
|
||||||
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
|
|
||||||
[ExtensionID.ExtensionMinimap]: {
|
|
||||||
displayText: {
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{value: 'characters', label: 'Characters'},
|
|
||||||
{value: 'blocks', label: 'Blocks'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
showOverlay: {
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{value: 'always', label: 'Always'},
|
|
||||||
{value: 'mouse-over', label: 'Mouse Over'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return defaultValue;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取配置项类型
|
|
||||||
const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => {
|
const formatConfigValue = (value: any): string => {
|
||||||
const meta = extensionConfigMeta[extensionId]?.[configKey];
|
if (value === undefined) return '';
|
||||||
if (meta?.type) {
|
try {
|
||||||
return meta.type;
|
const serialized = JSON.stringify(value);
|
||||||
|
return serialized ?? '';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to stringify config value', error);
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据默认值类型自动推断
|
|
||||||
if (typeof defaultValue === 'boolean') return 'toggle';
|
|
||||||
if (typeof defaultValue === 'number') return 'number';
|
|
||||||
return 'text';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取选择框的选项列表
|
|
||||||
const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => {
|
const handleConfigInput = async (
|
||||||
return extensionConfigMeta[extensionId]?.[configKey]?.options || [];
|
extensionId: ExtensionID,
|
||||||
|
configKey: string,
|
||||||
|
defaultValue: any,
|
||||||
|
event: Event
|
||||||
|
) => {
|
||||||
|
const target = event.target as HTMLInputElement | null;
|
||||||
|
if (!target) return;
|
||||||
|
const rawValue = target.value;
|
||||||
|
const trimmedValue = rawValue.trim();
|
||||||
|
if (!trimmedValue.length) {
|
||||||
|
await updateExtensionConfig(extensionId, configKey, undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(trimmedValue);
|
||||||
|
await updateExtensionConfig(extensionId, configKey, parsedValue);
|
||||||
|
} catch (_error) {
|
||||||
|
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||||
|
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
|
||||||
|
target.value = formatConfigValue(fallbackValue);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -204,58 +208,28 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="config-table-wrapper">
|
||||||
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
|
<table class="config-table">
|
||||||
:key="configKey"
|
<tbody>
|
||||||
class="config-item"
|
<tr
|
||||||
>
|
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
|
||||||
<SettingItem
|
:key="configKey"
|
||||||
:title="configKey"
|
|
||||||
>
|
|
||||||
<!-- 布尔值切换开关 -->
|
|
||||||
<ToggleSwitch
|
|
||||||
v-if="getConfigItemType(extension.id, configKey, configValue) === 'toggle'"
|
|
||||||
:model-value="extension.config[configKey] ?? configValue"
|
|
||||||
@update:model-value="updateExtensionConfig(extension.id, configKey, $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 数字输入框 -->
|
|
||||||
<input
|
|
||||||
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'number'"
|
|
||||||
type="number"
|
|
||||||
class="config-input"
|
|
||||||
:value="extension.config[configKey] ?? configValue"
|
|
||||||
:min="configKey === 'opacity' ? 0 : undefined"
|
|
||||||
:max="configKey === 'opacity' ? 1 : undefined"
|
|
||||||
:step="configKey === 'opacity' ? 0.1 : 1"
|
|
||||||
@input="updateExtensionConfig(extension.id, configKey, parseFloat(($event.target as HTMLInputElement).value))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 选择框 -->
|
|
||||||
<select
|
|
||||||
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'select'"
|
|
||||||
class="config-select"
|
|
||||||
:value="extension.config[configKey] ?? configValue"
|
|
||||||
@change="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLSelectElement).value)"
|
|
||||||
>
|
>
|
||||||
<option
|
<th scope="row" class="config-table-key">
|
||||||
v-for="option in getSelectOptions(extension.id, configKey)"
|
{{ configKey }}
|
||||||
:key="option.value"
|
</th>
|
||||||
:value="option.value"
|
<td class="config-table-value">
|
||||||
>
|
<input
|
||||||
{{ option.label }}
|
class="config-value-input"
|
||||||
</option>
|
type="text"
|
||||||
</select>
|
:value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
|
||||||
|
@change="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||||
<!-- 文本输入框 -->
|
@keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||||
<input
|
/>
|
||||||
v-else
|
</td>
|
||||||
type="text"
|
</tr>
|
||||||
class="config-input"
|
</tbody>
|
||||||
:value="extension.config[configKey] ?? configValue"
|
</table>
|
||||||
@input="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLInputElement).value)"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -361,37 +335,65 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-item {
|
.config-table-wrapper {
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 配置项标题和描述字体大小 */
|
|
||||||
:deep(.setting-item-title) {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.setting-item-description) {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-input, .config-select {
|
|
||||||
min-width: 120px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border: 1px solid var(--settings-input-border);
|
border: 1px solid var(--settings-input-border);
|
||||||
border-radius: 3px;
|
border-radius: 6px;
|
||||||
background-color: var(--settings-input-bg);
|
margin-top: 8px;
|
||||||
color: var(--settings-text);
|
overflow: hidden;
|
||||||
font-size: 11px;
|
background-color: var(--settings-panel, var(--settings-input-bg));
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--settings-accent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-select {
|
.config-table {
|
||||||
cursor: pointer;
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table tr + tr {
|
||||||
|
border-top: 1px solid var(--settings-input-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table th,
|
||||||
|
.config-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table-key {
|
||||||
|
width: 36%;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--settings-text-secondary);
|
||||||
|
border-right: 1px solid var(--settings-input-border);
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table-value {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--settings-text);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value-input:hover {
|
||||||
|
border-color: var(--settings-hover-border, var(--settings-input-border));
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--settings-accent);
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
2
go.mod
@@ -77,7 +77,7 @@ require (
|
|||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.44.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/image v0.33.0 // indirect
|
golang.org/x/image v0.33.0 // indirect
|
||||||
golang.org/x/oauth2 v0.33.0 // indirect
|
golang.org/x/oauth2 v0.33.0 // indirect
|
||||||
|
|||||||
4
go.sum
@@ -176,8 +176,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||||
|
|||||||