feat: add Blueprint feature to ProjectCard and commands
- Introduced BlueprintModal in ProjectCard for project blueprint visualization. - Added state management for displaying the BlueprintModal. - Implemented Blueprint-related interfaces and functions in commands.ts for handling blueprint data.
This commit is contained in:
parent
be7db96d48
commit
0cdaa3678c
163
.blueprint/CONVENTIONS.md
Normal file
163
.blueprint/CONVENTIONS.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# 蓝图更新规则
|
||||||
|
|
||||||
|
本文件定义了 `.blueprint/` 目录的维护规范,供 Claude 在对话中遵循。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.blueprint/
|
||||||
|
├── manifest.yaml ← 模块列表、关系、布局坐标
|
||||||
|
├── CONVENTIONS.md ← 本文件(Claude 行为规则)
|
||||||
|
└── modules/
|
||||||
|
└── <module-id>.md ← 每个模块的详情 + 任务卡
|
||||||
|
```
|
||||||
|
|
||||||
|
## 何时更新蓝图
|
||||||
|
|
||||||
|
| 事件 | 操作 |
|
||||||
|
|------|------|
|
||||||
|
| 用户确认了新需求 | 在 manifest.yaml 新增模块,创建 modules/<id>.md |
|
||||||
|
| 完成了功能实现 | 更新模块 status → done,标记任务卡 ✅ |
|
||||||
|
| 拆分了大需求为子任务 | 在 modules/<id>.md 中添加任务卡 |
|
||||||
|
| 调整了架构方向 | 更新 edges 关系,可能调整 area |
|
||||||
|
| 放弃了某个方向 | 删除对应模块和文件,或标记 status: abandoned |
|
||||||
|
|
||||||
|
## 模块状态定义
|
||||||
|
|
||||||
|
| 状态 | 含义 | 色标 |
|
||||||
|
|------|------|------|
|
||||||
|
| `done` | 已实现并验证 | 🟢 绿色 |
|
||||||
|
| `in_progress` | 正在开发 | 🔵 蓝色 |
|
||||||
|
| `planned` | 已规划,需求明确 | 🟡 黄色 |
|
||||||
|
| `concept` | 构思中,信息不完整 | ⚪ 灰色 |
|
||||||
|
|
||||||
|
## 任务卡格式
|
||||||
|
|
||||||
|
modules/<id>.md 中的每个二级标题(`###`)是一张任务卡:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### 📋 任务标题
|
||||||
|
- status: todo
|
||||||
|
- complexity: M
|
||||||
|
- files: src/path/to/file.tsx, src-tauri/src/commands/xxx.rs
|
||||||
|
- depends: other-module-id
|
||||||
|
- acceptance: 简要描述完成标准
|
||||||
|
|
||||||
|
详细说明(可选)。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态前缀
|
||||||
|
|
||||||
|
| 前缀 | status | 含义 |
|
||||||
|
|------|--------|------|
|
||||||
|
| ✅ | done | 已完成 |
|
||||||
|
| 🔵 | in_progress | 进行中 |
|
||||||
|
| 📋 | todo | 可派发(信息充足) |
|
||||||
|
| 💭 | concept | 构思中 |
|
||||||
|
|
||||||
|
### 可派发标准(📋)
|
||||||
|
|
||||||
|
任务卡同时满足以下条件时标记为 📋:
|
||||||
|
1. status 为 `todo`
|
||||||
|
2. 有明确的 `files`(涉及哪些文件)
|
||||||
|
3. 有明确的 `acceptance`(验收标准)
|
||||||
|
4. complexity 为 `S` 或 `M`
|
||||||
|
|
||||||
|
不满足条件的 todo 任务保持无前缀,说明还需补充信息。
|
||||||
|
|
||||||
|
## manifest.yaml 规范
|
||||||
|
|
||||||
|
### 模块字段
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: kebab-case-id # 唯一标识,同时是 modules/ 下的文件名
|
||||||
|
name: 显示名称
|
||||||
|
area: frontend # 所属领域 ID
|
||||||
|
status: done # done / in_progress / planned / concept
|
||||||
|
progress: 100 # 0~100,done 自动视为 100
|
||||||
|
position: [x, y] # React Flow 画布坐标(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 边字段
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- from: module-a
|
||||||
|
to: module-b
|
||||||
|
type: dependency # dependency(实线)/ related(虚线)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 领域字段
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: frontend
|
||||||
|
name: 前端
|
||||||
|
color: "#3B82F6" # 节点边框/分组背景色
|
||||||
|
```
|
||||||
|
|
||||||
|
## 进度计算
|
||||||
|
|
||||||
|
模块的 progress 由其任务卡的完成比例推算:
|
||||||
|
- 无任务卡的模块:手动设置 progress
|
||||||
|
- 有任务卡的模块:`完成数 / 总数 × 100`(取整)
|
||||||
|
|
||||||
|
## 决策记录格式
|
||||||
|
|
||||||
|
模块文件中可添加 `## 决策记录` 段落,记录架构决策的 WHY:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 决策记录
|
||||||
|
|
||||||
|
- 选择阿里云 OSS 而非 GitHub Releases 作为更新源,因为国内用户访问 GitHub 不稳定
|
||||||
|
- 使用 document-driven 蓝图而非数据库存储,因为 Claude 天然支持文件读写且可版本控制
|
||||||
|
```
|
||||||
|
|
||||||
|
桌面 App 会以琥珀色卡片展示在模块详情面板中。
|
||||||
|
|
||||||
|
## 飞轮工作流
|
||||||
|
|
||||||
|
Claude 在每次对话中应遵循以下流程:
|
||||||
|
|
||||||
|
### 阶段 1:进入对话 — 读取蓝图
|
||||||
|
```
|
||||||
|
读取 .blueprint/manifest.yaml → 了解项目全景和当前状态
|
||||||
|
```
|
||||||
|
- 识别哪些模块 in_progress,哪些有可派发任务
|
||||||
|
- 用简短话语告诉用户当前项目进展(如 "18/22 模块已完成,2 个进行中")
|
||||||
|
|
||||||
|
### 阶段 2:讨论需求 — 同步更新蓝图
|
||||||
|
| 用户说的 | Claude 应做的 |
|
||||||
|
|----------|--------------|
|
||||||
|
| "我想加个 XX 功能" | 确认需求后:manifest 加模块 (status: concept),创建 modules/<id>.md |
|
||||||
|
| "XX 功能需要 A、B、C 三步" | 在模块文件中拆分为 3 张任务卡 |
|
||||||
|
| "这个方案确定了" | 模块 status → planned,补充任务卡的 files/acceptance |
|
||||||
|
| "开始做 XX" | 模块 status → in_progress |
|
||||||
|
| "为什么选这个方案" | 记录到模块的 `## 决策记录` |
|
||||||
|
|
||||||
|
### 阶段 3:实现功能 — 标记完成
|
||||||
|
```
|
||||||
|
完成代码 → 更新任务卡前缀为 ✅ → status: done
|
||||||
|
所有任务卡完成 → 模块 status: done
|
||||||
|
更新 manifest.yaml 的 updated 日期
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 4:回顾 — 用户在桌面 App 查看
|
||||||
|
用户打开蓝图可视化:
|
||||||
|
- 看到全景进度
|
||||||
|
- 在「下一步」面板查看可派发任务
|
||||||
|
- 点击「复制派发」→ 粘贴给新 Claude 会话
|
||||||
|
|
||||||
|
### 飞轮的反馈信号
|
||||||
|
Claude 应关注这些信号来决定是否更新蓝图:
|
||||||
|
- 用户说"这个做完了" → 标记 done
|
||||||
|
- 用户说"这个不做了" → 标记 abandoned 或删除
|
||||||
|
- 用户说"这个要拆一下" → 拆分任务卡
|
||||||
|
- 用户说"记住这个决策" → 写入决策记录
|
||||||
|
- 用户没提蓝图但在讨论功能 → 主动提议更新蓝图
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 不要删除已完成的任务卡,它们是项目历史的一部分
|
||||||
|
- 模块 id 一旦确定尽量不要修改(会影响 edges 引用)
|
||||||
|
- 每次更新后修改 manifest.yaml 顶部的 `updated` 日期
|
||||||
|
- progress 由桌面 App 自动从任务卡完成比例计算,不需要手动维护
|
||||||
|
- 当用户的操作隐含了蓝图变更但没有明说时,Claude 应主动提议更新(而非沉默)
|
||||||
223
.blueprint/manifest.yaml
Normal file
223
.blueprint/manifest.yaml
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
version: 1
|
||||||
|
name: Dev Manager Tauri
|
||||||
|
description: Git 多空间开发管理器,集成项目管理、Git 操作、CI/CD、开发工具链管理
|
||||||
|
updated: 2026-04-02
|
||||||
|
|
||||||
|
areas:
|
||||||
|
- id: frontend
|
||||||
|
name: 前端
|
||||||
|
color: "#3B82F6"
|
||||||
|
- id: backend
|
||||||
|
name: 后端
|
||||||
|
color: "#10B981"
|
||||||
|
- id: infra
|
||||||
|
name: 基础设施
|
||||||
|
color: "#F59E0B"
|
||||||
|
|
||||||
|
modules:
|
||||||
|
# ── 前端 ────────────────────────────────────────
|
||||||
|
- id: workspace-mgmt
|
||||||
|
name: 空间管理
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [0, 0]
|
||||||
|
|
||||||
|
- id: project-mgmt
|
||||||
|
name: 项目管理
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [250, 0]
|
||||||
|
|
||||||
|
- id: project-dashboard
|
||||||
|
name: 项目看板
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [500, 0]
|
||||||
|
|
||||||
|
- id: repo-registry
|
||||||
|
name: Git 仓库注册
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [250, 150]
|
||||||
|
|
||||||
|
- id: project-tools
|
||||||
|
name: 项目工具链
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [500, 150]
|
||||||
|
|
||||||
|
- id: project-templates
|
||||||
|
name: 项目模板
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [0, 150]
|
||||||
|
|
||||||
|
- id: tags-groups
|
||||||
|
name: 标签 & 分组
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [750, 0]
|
||||||
|
|
||||||
|
- id: settings-page
|
||||||
|
name: 设置页
|
||||||
|
area: frontend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [750, 150]
|
||||||
|
|
||||||
|
- id: blueprint-view
|
||||||
|
name: 蓝图可视化
|
||||||
|
area: frontend
|
||||||
|
status: in_progress
|
||||||
|
progress: 10
|
||||||
|
position: [1000, 0]
|
||||||
|
|
||||||
|
# ── 后端 ────────────────────────────────────────
|
||||||
|
- id: git-ops
|
||||||
|
name: Git 操作
|
||||||
|
area: backend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [0, 350]
|
||||||
|
|
||||||
|
- id: github-auth
|
||||||
|
name: GitHub OAuth
|
||||||
|
area: backend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [250, 350]
|
||||||
|
|
||||||
|
- id: github-publish
|
||||||
|
name: GitHub 发布
|
||||||
|
area: backend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [500, 350]
|
||||||
|
|
||||||
|
- id: devtools-scan
|
||||||
|
name: 开发工具扫描
|
||||||
|
area: backend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [750, 350]
|
||||||
|
|
||||||
|
- id: project-env-scan
|
||||||
|
name: 项目环境扫描
|
||||||
|
area: backend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [750, 500]
|
||||||
|
|
||||||
|
- id: shell-launch
|
||||||
|
name: 编辑器 & 终端启动
|
||||||
|
area: backend
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [500, 500]
|
||||||
|
|
||||||
|
- id: blueprint-reader
|
||||||
|
name: 蓝图文件读取
|
||||||
|
area: backend
|
||||||
|
status: planned
|
||||||
|
progress: 0
|
||||||
|
position: [1000, 350]
|
||||||
|
|
||||||
|
# ── 基础设施 ────────────────────────────────────
|
||||||
|
- id: cicd-workflow
|
||||||
|
name: CI/CD 工作流
|
||||||
|
area: infra
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [0, 650]
|
||||||
|
|
||||||
|
- id: auto-update
|
||||||
|
name: 客户端自动更新
|
||||||
|
area: infra
|
||||||
|
status: done
|
||||||
|
progress: 90
|
||||||
|
position: [250, 650]
|
||||||
|
|
||||||
|
- id: wsl-proxy
|
||||||
|
name: WSL 代理同步
|
||||||
|
area: infra
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [500, 650]
|
||||||
|
|
||||||
|
- id: v2rayn-import
|
||||||
|
name: v2rayN 节点读取
|
||||||
|
area: infra
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [500, 800]
|
||||||
|
|
||||||
|
- id: sqlite-db
|
||||||
|
name: SQLite 数据层
|
||||||
|
area: infra
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
position: [750, 650]
|
||||||
|
|
||||||
|
edges:
|
||||||
|
# 前端依赖
|
||||||
|
- from: workspace-mgmt
|
||||||
|
to: project-mgmt
|
||||||
|
type: dependency
|
||||||
|
- from: project-mgmt
|
||||||
|
to: project-dashboard
|
||||||
|
type: dependency
|
||||||
|
- from: repo-registry
|
||||||
|
to: project-dashboard
|
||||||
|
type: related
|
||||||
|
- from: project-mgmt
|
||||||
|
to: tags-groups
|
||||||
|
type: related
|
||||||
|
- from: project-mgmt
|
||||||
|
to: blueprint-view
|
||||||
|
type: dependency
|
||||||
|
|
||||||
|
# 后端依赖
|
||||||
|
- from: github-auth
|
||||||
|
to: github-publish
|
||||||
|
type: dependency
|
||||||
|
- from: git-ops
|
||||||
|
to: github-publish
|
||||||
|
type: dependency
|
||||||
|
|
||||||
|
# 前后端关联
|
||||||
|
- from: git-ops
|
||||||
|
to: project-dashboard
|
||||||
|
type: related
|
||||||
|
- from: github-auth
|
||||||
|
to: project-dashboard
|
||||||
|
type: related
|
||||||
|
- from: devtools-scan
|
||||||
|
to: project-tools
|
||||||
|
type: related
|
||||||
|
- from: project-env-scan
|
||||||
|
to: project-tools
|
||||||
|
type: related
|
||||||
|
- from: blueprint-reader
|
||||||
|
to: blueprint-view
|
||||||
|
type: dependency
|
||||||
|
|
||||||
|
# 基础设施关联
|
||||||
|
- from: cicd-workflow
|
||||||
|
to: auto-update
|
||||||
|
type: dependency
|
||||||
|
- from: v2rayn-import
|
||||||
|
to: wsl-proxy
|
||||||
|
type: dependency
|
||||||
|
- from: sqlite-db
|
||||||
|
to: project-mgmt
|
||||||
|
type: dependency
|
||||||
|
- from: auto-update
|
||||||
|
to: settings-page
|
||||||
|
type: related
|
||||||
40
.blueprint/modules/auto-update.md
Normal file
40
.blueprint/modules/auto-update.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
id: auto-update
|
||||||
|
name: 客户端自动更新
|
||||||
|
status: done
|
||||||
|
progress: 90
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
基于 tauri-plugin-updater 的客户端自动更新,检查阿里云 OSS 上的 latest.json,发现新版本后下载安装。
|
||||||
|
完整指南见 docs/tauri-updater-guide.md。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 更新检测
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx
|
||||||
|
|
||||||
|
点击「检查更新」请求 latest.json 比对版本号。
|
||||||
|
|
||||||
|
### ✅ 下载 & 安装
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx
|
||||||
|
|
||||||
|
下载安装包(带进度条),安装后自动重启。
|
||||||
|
|
||||||
|
### ✅ 错误提示
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx
|
||||||
|
|
||||||
|
错误信息完整展示,支持一键复制。
|
||||||
|
|
||||||
|
### ✅ Capabilities 权限配置
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/capabilities/default.json
|
||||||
|
|
||||||
|
updater:default + process:default + http 域名放行。
|
||||||
|
|
||||||
|
### 💭 自动检查更新
|
||||||
|
- status: concept
|
||||||
|
- notes: 应用启动时自动检查,发现新版本后在状态栏提示,而非需要手动进入设置页。
|
||||||
29
.blueprint/modules/blueprint-reader.md
Normal file
29
.blueprint/modules/blueprint-reader.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
id: blueprint-reader
|
||||||
|
name: 蓝图文件读取
|
||||||
|
status: planned
|
||||||
|
progress: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
Rust 后端 tauri command,读取项目目录下的 .blueprint/ 文件并解析为结构化数据返回给前端。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### 📋 读取 manifest.yaml
|
||||||
|
- status: todo
|
||||||
|
- complexity: M
|
||||||
|
- files: src-tauri/src/commands/blueprint.rs (待创建)
|
||||||
|
- acceptance: 读取并解析 manifest.yaml,返回 areas、modules、edges 结构体
|
||||||
|
|
||||||
|
### 📋 读取 modules/*.md
|
||||||
|
- status: todo
|
||||||
|
- complexity: M
|
||||||
|
- files: src-tauri/src/commands/blueprint.rs (待创建)
|
||||||
|
- acceptance: 解析 markdown frontmatter 和内容,提取任务卡列表(标题、status、complexity、files、acceptance)
|
||||||
|
|
||||||
|
### 📋 合并返回完整蓝图数据
|
||||||
|
- status: todo
|
||||||
|
- complexity: S
|
||||||
|
- files: src-tauri/src/commands/blueprint.rs (待创建)
|
||||||
|
- acceptance: 一次调用返回完整蓝图(manifest + 所有模块详情 + 任务卡),前端无需多次请求
|
||||||
47
.blueprint/modules/blueprint-view.md
Normal file
47
.blueprint/modules/blueprint-view.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
id: blueprint-view
|
||||||
|
name: 蓝图可视化
|
||||||
|
status: in_progress
|
||||||
|
progress: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
读取项目 .blueprint/ 目录中的 manifest.yaml 和 modules/*.md,用 React Flow 渲染项目全景架构图。点击模块节点展示详情和任务卡列表。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ .blueprint/ 数据结构设计
|
||||||
|
- status: done
|
||||||
|
- files: .blueprint/manifest.yaml, .blueprint/CONVENTIONS.md
|
||||||
|
|
||||||
|
定义 manifest.yaml 格式、模块文件格式、任务卡格式、CONVENTIONS 规则。
|
||||||
|
|
||||||
|
### 📋 蓝图读取 Command
|
||||||
|
- status: todo
|
||||||
|
- complexity: M
|
||||||
|
- files: src-tauri/src/commands/blueprint.rs (待创建)
|
||||||
|
- acceptance: 实现 get_blueprint tauri command,读取指定项目路径下的 .blueprint/manifest.yaml 和 modules/*.md,解析后返回结构化数据
|
||||||
|
|
||||||
|
### 📋 React Flow 节点图渲染
|
||||||
|
- status: todo
|
||||||
|
- complexity: L
|
||||||
|
- files: src/components/dashboard/BlueprintModal.tsx (待创建)
|
||||||
|
- depends: blueprint-reader
|
||||||
|
- acceptance: 将 manifest.yaml 的 modules 渲染为自定义节点(带状态色标、进度条),edges 渲染为连线,areas 渲染为分组背景
|
||||||
|
|
||||||
|
### 📋 模块详情面板
|
||||||
|
- status: todo
|
||||||
|
- complexity: M
|
||||||
|
- files: src/components/dashboard/BlueprintModal.tsx
|
||||||
|
- depends: blueprint-reader
|
||||||
|
- acceptance: 点击节点后在底部/侧边展示模块描述和任务卡列表,任务卡按状态分组显示
|
||||||
|
|
||||||
|
### 📋 项目卡片蓝图入口
|
||||||
|
- status: todo
|
||||||
|
- complexity: S
|
||||||
|
- files: src/components/dashboard/ProjectCard.tsx
|
||||||
|
- acceptance: 项目卡片新增「蓝图」按钮,点击打开 BlueprintModal,检测到无 .blueprint/ 时显示引导提示
|
||||||
|
|
||||||
|
### 💭 统计汇总条
|
||||||
|
- status: concept
|
||||||
|
- notes: 模态框顶部展示项目完成度统计条(已完成 X / 进行中 X / 规划中 X / 构思中 X)
|
||||||
42
.blueprint/modules/cicd-workflow.md
Normal file
42
.blueprint/modules/cicd-workflow.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
id: cicd-workflow
|
||||||
|
name: CI/CD 工作流
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
CI/CD 配置管理和 GitHub Actions Workflow 文件生成。支持两种部署类型:Web SSH 部署和 Tauri OSS 分发。
|
||||||
|
完整部署教程见 docs/tauri-cicd-guide.md。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ CI/CD 配置 CRUD
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/cicd.rs, src/components/dashboard/CicdModal.tsx
|
||||||
|
|
||||||
|
保存和读取 CI/CD 配置(部署类型、触发分支、构建命令等)。
|
||||||
|
|
||||||
|
### ✅ Workflow 文件生成
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/cicd.rs
|
||||||
|
|
||||||
|
根据配置自动生成 .github/workflows/ 下的 YAML 文件。
|
||||||
|
|
||||||
|
### ✅ Web SSH 部署模板
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/cicd.rs
|
||||||
|
|
||||||
|
生成基于 appleboy/ssh-action 的部署 workflow。
|
||||||
|
|
||||||
|
### ✅ Tauri OSS 分发模板
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/cicd.rs
|
||||||
|
|
||||||
|
生成 Tauri 构建 + 签名 + 阿里云 OSS 上传的 release workflow。
|
||||||
|
|
||||||
|
### ✅ Actions 运行历史
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/CicdModal.tsx
|
||||||
|
|
||||||
|
查看 GitHub Actions 运行状态,支持手动触发。
|
||||||
29
.blueprint/modules/devtools-scan.md
Normal file
29
.blueprint/modules/devtools-scan.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
id: devtools-scan
|
||||||
|
name: 开发工具扫描
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
扫描系统和 WSL 中安装的开发工具(Node.js、pnpm、Rust、Python、Docker 等),统一展示版本信息。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ Windows 工具扫描
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/devtools.rs, src/components/devtools/
|
||||||
|
|
||||||
|
扫描 Windows 环境中的开发工具及版本。
|
||||||
|
|
||||||
|
### ✅ WSL 工具扫描
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/devtools.rs
|
||||||
|
|
||||||
|
通过 WSL 命令扫描 Linux 环境中的开发工具。
|
||||||
|
|
||||||
|
### ✅ 工具信息持久化
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/devtools.rs
|
||||||
|
|
||||||
|
保存、更新、删除扫描到的工具信息。
|
||||||
37
.blueprint/modules/git-ops.md
Normal file
37
.blueprint/modules/git-ops.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
id: git-ops
|
||||||
|
name: Git 操作
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
基于 libgit2 (git2-rs) 的 Git 操作后端,提供 clone、status、fetch、pull、push、分支管理等功能。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 基础操作
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/git_ops.rs
|
||||||
|
|
||||||
|
git clone, status, fetch, pull, push, head commit 查询。
|
||||||
|
|
||||||
|
### ✅ 分支管理
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/git_ops.rs
|
||||||
|
|
||||||
|
创建分支、列举分支、切换分支。
|
||||||
|
|
||||||
|
### ✅ 上游状态检测
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/git_ops.rs
|
||||||
|
|
||||||
|
检测本地分支落后远程的提交数。
|
||||||
|
|
||||||
|
### 💭 冲突解决 UI
|
||||||
|
- status: concept
|
||||||
|
- notes: 检测到冲突文件时提供可视化 diff 和解决方案,尚未确定交互形式。
|
||||||
|
|
||||||
|
### 💭 Stash 管理
|
||||||
|
- status: concept
|
||||||
|
- notes: stash list/apply/drop,可能需要新的 UI 面板。
|
||||||
23
.blueprint/modules/github-auth.md
Normal file
23
.blueprint/modules/github-auth.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: github-auth
|
||||||
|
name: GitHub OAuth
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
GitHub OAuth Device Flow 认证,获取 token 后支持 API 操作和 Git 推送。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ OAuth Device Flow
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/github.rs, src/components/auth/
|
||||||
|
|
||||||
|
启动 OAuth 流程、轮询 token、保存账户信息。
|
||||||
|
|
||||||
|
### ✅ Token 管理
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/github.rs
|
||||||
|
|
||||||
|
保存、读取、清除 GitHub token,支持登出。
|
||||||
35
.blueprint/modules/github-publish.md
Normal file
35
.blueprint/modules/github-publish.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: github-publish
|
||||||
|
name: GitHub 发布
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
将本地项目发布到 GitHub,支持初始化 Git 仓库、创建远程仓库、推送代码、生成 .gitignore。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 扫描本地 Git 状态
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/publish.rs
|
||||||
|
|
||||||
|
检测项目是否已初始化 Git、是否有远程仓库。
|
||||||
|
|
||||||
|
### ✅ 创建 GitHub 仓库
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/publish.rs
|
||||||
|
|
||||||
|
通过 GitHub API 创建新仓库。
|
||||||
|
|
||||||
|
### ✅ Git 初始化 & 首次推送
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/publish.rs
|
||||||
|
|
||||||
|
git init、首次 commit、设置 remote、push。
|
||||||
|
|
||||||
|
### ✅ .gitignore 生成
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/publish.rs
|
||||||
|
|
||||||
|
根据项目类型生成 .gitignore 文件。
|
||||||
35
.blueprint/modules/project-dashboard.md
Normal file
35
.blueprint/modules/project-dashboard.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: project-dashboard
|
||||||
|
name: 项目看板
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
当前空间的项目看板视图,展示项目卡片列表,支持分组、搜索、快捷操作(打开编辑器/终端、Git 操作、CI/CD 等)。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 项目卡片列表
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/Dashboard.tsx, src/components/dashboard/ProjectCard.tsx
|
||||||
|
|
||||||
|
卡片展示项目名称、状态、标签,支持搜索过滤。
|
||||||
|
|
||||||
|
### ✅ 分组画布
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/GroupResourceCanvas.tsx
|
||||||
|
|
||||||
|
自定义分组,拖拽管理项目归属。
|
||||||
|
|
||||||
|
### ✅ 项目快捷操作
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/ProjectCard.tsx
|
||||||
|
|
||||||
|
从卡片直接打开编辑器、终端、Git 操作面板。
|
||||||
|
|
||||||
|
### ✅ 项目发现面板
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/DiscoveryPanel.tsx
|
||||||
|
|
||||||
|
发现未关联的本地项目。
|
||||||
23
.blueprint/modules/project-env-scan.md
Normal file
23
.blueprint/modules/project-env-scan.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: project-env-scan
|
||||||
|
name: 项目环境扫描
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
扫描单个项目目录中使用的技术栈和工具版本(如 package.json 中的 Node 版本、Cargo.toml 中的 Rust edition 等)。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 项目环境检测
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/project_scan.rs, src/components/dashboard/ProjectEnvModal.tsx
|
||||||
|
|
||||||
|
分析项目目录结构,识别技术栈和版本要求。
|
||||||
|
|
||||||
|
### ✅ 环境信息持久化
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/project_scan.rs
|
||||||
|
|
||||||
|
保存和读取项目环境工具信息。
|
||||||
35
.blueprint/modules/project-mgmt.md
Normal file
35
.blueprint/modules/project-mgmt.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: project-mgmt
|
||||||
|
name: 项目管理
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
项目的创建、编辑、删除,支持批量导入。项目分为 profile(跨空间共享的档案)和 workspace(空间内的挂载实例)两层。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 项目 CRUD
|
||||||
|
- status: done
|
||||||
|
- files: src/components/manage/ManagePage.tsx, src-tauri/src/commands/projects.rs
|
||||||
|
|
||||||
|
基础的项目增删改查。
|
||||||
|
|
||||||
|
### ✅ 批量导入项目
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/BatchAddModal.tsx
|
||||||
|
|
||||||
|
扫描文件夹子目录,批量创建项目。
|
||||||
|
|
||||||
|
### ✅ 项目档案(Profile)
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/projects.rs
|
||||||
|
|
||||||
|
跨空间共享的项目元信息:技术栈、仓库 URL、部署地址等。
|
||||||
|
|
||||||
|
### ✅ 子文件夹扫描
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/projects.rs
|
||||||
|
|
||||||
|
扫描指定目录下的子文件夹,辅助批量添加。
|
||||||
29
.blueprint/modules/project-templates.md
Normal file
29
.blueprint/modules/project-templates.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
id: project-templates
|
||||||
|
name: 项目模板
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
预设项目模板,包含内置模板(首次启动自动种植)和用户自定义模板,用于快速创建新项目。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 模板 CRUD
|
||||||
|
- status: done
|
||||||
|
- files: src/components/manage/TemplateEditor.tsx, src-tauri/src/commands/templates.rs
|
||||||
|
|
||||||
|
创建、编辑、删除项目模板。
|
||||||
|
|
||||||
|
### ✅ 从模板创建项目
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/templates.rs
|
||||||
|
|
||||||
|
选择模板后自动填充项目信息。
|
||||||
|
|
||||||
|
### ✅ 内置模板种植
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/templates.rs
|
||||||
|
|
||||||
|
首次启动时自动插入预设模板。
|
||||||
23
.blueprint/modules/project-tools.md
Normal file
23
.blueprint/modules/project-tools.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: project-tools
|
||||||
|
name: 项目工具链
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
管理项目关联的开发工具(数据库客户端、API 测试工具等),支持一键启动。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 工具 CRUD
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/ProjectToolsModal.tsx, src-tauri/src/commands/project_tools.rs
|
||||||
|
|
||||||
|
为项目添加/编辑/删除关联工具。
|
||||||
|
|
||||||
|
### ✅ 工具启动
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/project_tools.rs
|
||||||
|
|
||||||
|
一键启动关联工具,支持自定义启动命令。
|
||||||
29
.blueprint/modules/repo-registry.md
Normal file
29
.blueprint/modules/repo-registry.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
id: repo-registry
|
||||||
|
name: Git 仓库注册
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
统一管理 Git 仓库信息,支持从 GitHub 批量导入仓库,挂载到项目。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 仓库导入
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/ImportRepoModal.tsx, src-tauri/src/commands/git_repos.rs
|
||||||
|
|
||||||
|
从 GitHub 拉取用户仓库列表,批量导入到本地注册表。
|
||||||
|
|
||||||
|
### ✅ 仓库挂载
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/MountRepoModal.tsx, src-tauri/src/commands/git_repos.rs
|
||||||
|
|
||||||
|
将已注册的仓库关联到项目。
|
||||||
|
|
||||||
|
### ✅ 仓库注册表管理
|
||||||
|
- status: done
|
||||||
|
- files: src/components/dashboard/RepoRegistryModal.tsx
|
||||||
|
|
||||||
|
查看、搜索、删除已注册仓库。
|
||||||
35
.blueprint/modules/settings-page.md
Normal file
35
.blueprint/modules/settings-page.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: settings-page
|
||||||
|
name: 设置页
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
全局和空间级设置,包括编辑器检测、终端配置、WSL 发行版选择、应用更新。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 编辑器检测
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx, src-tauri/src/commands/settings.rs
|
||||||
|
|
||||||
|
自动扫描系统安装的编辑器(VS Code、Cursor 等)。
|
||||||
|
|
||||||
|
### ✅ 终端配置
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx
|
||||||
|
|
||||||
|
配置默认终端程序。
|
||||||
|
|
||||||
|
### ✅ WSL 发行版选择
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx
|
||||||
|
|
||||||
|
列举已安装的 WSL 发行版,选择默认使用的。
|
||||||
|
|
||||||
|
### ✅ 关于 & 更新
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx
|
||||||
|
|
||||||
|
显示当前版本号、检查更新、下载安装、错误信息可复制。
|
||||||
23
.blueprint/modules/shell-launch.md
Normal file
23
.blueprint/modules/shell-launch.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: shell-launch
|
||||||
|
name: 编辑器 & 终端启动
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
从应用内一键打开编辑器(VS Code、Cursor 等)和终端,支持 Windows 路径和 WSL 路径。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 打开编辑器
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/shell.rs
|
||||||
|
|
||||||
|
调用配置的编辑器打开项目目录。
|
||||||
|
|
||||||
|
### ✅ 打开终端
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/shell.rs
|
||||||
|
|
||||||
|
打开终端并 cd 到项目目录,支持 Windows Terminal、PowerShell 等。
|
||||||
23
.blueprint/modules/sqlite-db.md
Normal file
23
.blueprint/modules/sqlite-db.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: sqlite-db
|
||||||
|
name: SQLite 数据层
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
使用 rusqlite + r2d2 连接池管理本地 SQLite 数据库,存储项目、空间、配置、工具等所有持久化数据。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 连接池初始化
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/db.rs
|
||||||
|
|
||||||
|
应用启动时初始化数据库,创建表结构,r2d2 连接池管理。
|
||||||
|
|
||||||
|
### ✅ 数据库迁移
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/db.rs
|
||||||
|
|
||||||
|
表结构变更时的自动迁移逻辑。
|
||||||
23
.blueprint/modules/tags-groups.md
Normal file
23
.blueprint/modules/tags-groups.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: tags-groups
|
||||||
|
name: 标签 & 分组
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
项目标签系统和分组管理,用于项目分类和看板布局。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 标签管理
|
||||||
|
- status: done
|
||||||
|
- files: src/components/manage/TagsManager.tsx, src-tauri/src/commands/tags.rs
|
||||||
|
|
||||||
|
标签的增删改、颜色配置、项目关联。
|
||||||
|
|
||||||
|
### ✅ 分组管理
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/groups.rs
|
||||||
|
|
||||||
|
分组的增删改、排序、成员管理。
|
||||||
29
.blueprint/modules/v2rayn-import.md
Normal file
29
.blueprint/modules/v2rayn-import.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
id: v2rayn-import
|
||||||
|
name: v2rayN 节点读取
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
读取本地 v2rayN 的 SQLite 数据库,获取当前激活节点的连接信息,供 WSL 代理同步使用。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 数据库路径检测
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/proxy.rs, src/components/proxy/ProxyPage.tsx
|
||||||
|
|
||||||
|
自动扫描 v2rayN 的 guiNDB.db 路径,支持手动选择。
|
||||||
|
|
||||||
|
### ✅ 当前节点读取
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/proxy.rs
|
||||||
|
|
||||||
|
读取 v2rayN 数据库中的当前激活节点信息(协议类型、地址、端口、备注)。
|
||||||
|
|
||||||
|
### ✅ 节点列表扫描
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/proxy.rs
|
||||||
|
|
||||||
|
扫描全部节点列表。
|
||||||
29
.blueprint/modules/workspace-mgmt.md
Normal file
29
.blueprint/modules/workspace-mgmt.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
id: workspace-mgmt
|
||||||
|
name: 空间管理
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
多空间(Workspace)管理,每个空间可独立挂载项目、配置编辑器和终端。用户可在空间间切换,实现不同开发场景的隔离。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 空间 CRUD
|
||||||
|
- status: done
|
||||||
|
- files: src/components/spaces/SpacesPage.tsx, src-tauri/src/commands/spaces.rs
|
||||||
|
|
||||||
|
创建、编辑、删除空间,切换当前活跃空间。
|
||||||
|
|
||||||
|
### ✅ 空间级设置
|
||||||
|
- status: done
|
||||||
|
- files: src/components/settings/SettingsPage.tsx, src-tauri/src/commands/settings.rs
|
||||||
|
|
||||||
|
每个空间可配置独立的编辑器路径、终端、WSL 发行版等。
|
||||||
|
|
||||||
|
### ✅ 部署命令管理
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/spaces.rs
|
||||||
|
|
||||||
|
每个空间可保存常用部署命令(deploy commands)。
|
||||||
35
.blueprint/modules/wsl-proxy.md
Normal file
35
.blueprint/modules/wsl-proxy.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: wsl-proxy
|
||||||
|
name: WSL 代理同步
|
||||||
|
status: done
|
||||||
|
progress: 100
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
将 Windows 上的 v2rayN 代理配置同步到 WSL 环境,解决 WSL 下 curl/git/npm 等工具无法使用代理的问题。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### ✅ 代理同步
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/proxy.rs, src/components/proxy/ProxyPage.tsx
|
||||||
|
|
||||||
|
读取 v2rayN 配置,生成 WSL 的 proxy 环境变量脚本。
|
||||||
|
|
||||||
|
### ✅ 代理状态检测
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/network.rs, src/components/proxy/ProxyPage.tsx
|
||||||
|
|
||||||
|
检测 WSL 中当前的代理配置状态(是否已配置、IP 是否过期)。
|
||||||
|
|
||||||
|
### ✅ 代理清除
|
||||||
|
- status: done
|
||||||
|
- files: src-tauri/src/commands/proxy.rs
|
||||||
|
|
||||||
|
一键清除 WSL 中的代理配置。
|
||||||
|
|
||||||
|
### ✅ 代理一致性测试
|
||||||
|
- status: done
|
||||||
|
- files: src/components/proxy/ProxyPage.tsx
|
||||||
|
|
||||||
|
同时测试 Windows 和 WSL 的出口 IP,验证代理是否生效且一致。
|
||||||
44
CLAUDE.md
Normal file
44
CLAUDE.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Dev Manager Tauri
|
||||||
|
|
||||||
|
Git 多空间开发管理器,Tauri v2 + React + TypeScript + Rust。
|
||||||
|
|
||||||
|
## 项目蓝图
|
||||||
|
|
||||||
|
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
||||||
|
|
||||||
|
**每次对话都应遵循以下流程:**
|
||||||
|
|
||||||
|
### 1. 了解当前状态
|
||||||
|
对话开始时,先读取 `.blueprint/manifest.yaml` 了解项目模块和状态。
|
||||||
|
|
||||||
|
### 2. 讨论需求时更新蓝图
|
||||||
|
- 确认了新需求 → 在 manifest.yaml 新增模块,创建 `.blueprint/modules/<id>.md`
|
||||||
|
- 拆分了大需求 → 在模块文件中添加任务卡
|
||||||
|
- 调整了方向 → 更新模块关系(edges)
|
||||||
|
|
||||||
|
### 3. 完成实现后标记蓝图
|
||||||
|
- 完成了功能 → 更新对应任务卡状态为 `done`(前缀改为 ✅)
|
||||||
|
- 更新 manifest.yaml 顶部的 `updated` 日期
|
||||||
|
|
||||||
|
### 4. 任务卡规范
|
||||||
|
详见 `.blueprint/CONVENTIONS.md`,核心规则:
|
||||||
|
- 任务卡同时具备 `files` + `acceptance` + complexity `S/M` → 标记为 📋 可派发
|
||||||
|
- 可派发的任务卡可以直接交给 Claude Agent 独立完成
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
- 前端:React 19 + TypeScript + TailwindCSS + React Query + Zustand + React Flow
|
||||||
|
- 后端:Rust + Tauri v2 + SQLite (rusqlite + r2d2)
|
||||||
|
- 包管理:pnpm(禁止 npm/yarn)
|
||||||
|
- CI/CD:GitHub Actions + 阿里云 OSS(详见 docs/tauri-cicd-guide.md)
|
||||||
|
- 客户端更新:tauri-plugin-updater(详见 docs/tauri-updater-guide.md)
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
```
|
||||||
|
src/ 前端源码
|
||||||
|
src/components/ React 组件(按功能分目录)
|
||||||
|
src/lib/commands.ts Tauri command 调用封装
|
||||||
|
src-tauri/src/ Rust 后端
|
||||||
|
src-tauri/src/commands/ Tauri command 实现
|
||||||
|
.blueprint/ 项目蓝图(manifest + 模块详情 + 任务卡)
|
||||||
|
docs/ 部署和更新指南
|
||||||
|
```
|
||||||
@ -353,35 +353,11 @@ git push --tags
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 八、客户端更新检测(前端代码)
|
## 八、客户端自动更新
|
||||||
|
|
||||||
```typescript
|
客户端侧的完整实现(插件安装、权限配置、前端 UI 状态机、踩坑记录)已独立成文档:
|
||||||
import { check } from '@tauri-apps/plugin-updater'
|
|
||||||
import { relaunch } from '@tauri-apps/plugin-process'
|
|
||||||
|
|
||||||
async function checkForUpdate() {
|
**→ [tauri-updater-guide.md](./tauri-updater-guide.md)**
|
||||||
const update = await check()
|
|
||||||
if (update) {
|
|
||||||
console.log(`发现新版本: ${update.version}`)
|
|
||||||
// 下载并安装
|
|
||||||
await update.downloadAndInstall()
|
|
||||||
// 重启应用
|
|
||||||
await relaunch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
需要在 `src-tauri/src/lib.rs` 中注册插件:
|
|
||||||
```rust
|
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_process::init())
|
|
||||||
```
|
|
||||||
|
|
||||||
Cargo.toml 依赖:
|
|
||||||
```toml
|
|
||||||
tauri-plugin-updater = "2"
|
|
||||||
tauri-plugin-process = "2"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
408
docs/tauri-updater-guide.md
Normal file
408
docs/tauri-updater-guide.md
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
# Tauri v2 客户端自动更新指南
|
||||||
|
|
||||||
|
> **方案版本**: v1.0(2026-04-02)
|
||||||
|
> **适用范围**: Tauri v2 + 任意更新源(阿里云 OSS / GitHub Releases / 自建服务器)
|
||||||
|
> **验证状态**: 已在 dev-manager-tauri 项目实际跑通
|
||||||
|
>
|
||||||
|
> 本文档聚焦客户端侧的自动更新功能实现,涵盖从插件安装到 UI 交互的全链路。
|
||||||
|
> 服务端构建与发布流程请参考 [tauri-cicd-guide.md](./tauri-cicd-guide.md)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 整体流程
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端启动 / 用户点击「检查更新」
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
请求 endpoint 上的 latest.json
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
比较 latest.json.version vs 本地版本
|
||||||
|
│
|
||||||
|
┌────┴────┐
|
||||||
|
│ 无更新 │ 有新版本
|
||||||
|
│ 结束 │
|
||||||
|
│ ▼
|
||||||
|
│ 展示版本号 + 更新说明
|
||||||
|
│ 用户点击「立即更新」
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ 下载 .exe(带进度)
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ 验证签名 → 安装 → relaunch
|
||||||
|
└─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、安装依赖
|
||||||
|
|
||||||
|
### 1.1 Rust 端(src-tauri/Cargo.toml)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
tauri-plugin-process = "2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 前端(package.json)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @tauri-apps/plugin-updater @tauri-apps/plugin-process
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、注册插件
|
||||||
|
|
||||||
|
`src-tauri/src/lib.rs` 中注册两个插件:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
// ... 其他插件和配置
|
||||||
|
```
|
||||||
|
|
||||||
|
- `tauri_plugin_updater` — 检查更新、下载、验证签名、安装
|
||||||
|
- `tauri_plugin_process` — 安装后调用 `relaunch()` 重启应用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、配置 tauri.conf.json
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"bundle": {
|
||||||
|
"createUpdaterArtifacts": true // ⚠️ 必须!否则构建不生成 .exe.sig 签名文件
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "<公钥 base64>",
|
||||||
|
"endpoints": [
|
||||||
|
"https://<bucket>.<endpoint>/latest.json"
|
||||||
|
],
|
||||||
|
"dialog": false // false = 用代码控制更新 UI;true = 系统原生弹窗
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键说明
|
||||||
|
|
||||||
|
| 字段 | 作用 | 注意事项 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `createUpdaterArtifacts` | 构建时生成 `.exe.sig` 签名文件 | Tauri v2 默认 false,不开启则签名静默跳过 |
|
||||||
|
| `pubkey` | 客户端用来验证下载文件签名 | 是**公钥文件**的 base64,不是私钥 |
|
||||||
|
| `endpoints` | `latest.json` 的 URL 列表 | 可配多个,按顺序尝试 |
|
||||||
|
| `dialog` | 是否使用原生弹窗 | 推荐 false,自定义 UI 体验更好 |
|
||||||
|
|
||||||
|
### pubkey 生成方法
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 生成密钥对
|
||||||
|
pnpm tauri signer generate -w "$HOME\.tauri\your-app.key"
|
||||||
|
|
||||||
|
# 获取公钥 base64(填入 pubkey 字段)
|
||||||
|
$content = Get-Content "$HOME\.tauri\your-app.key.pub" -Raw
|
||||||
|
$content = $content -replace "`r`n", "`n"
|
||||||
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($content)
|
||||||
|
[Convert]::ToBase64String($bytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、配置 Capabilities 权限
|
||||||
|
|
||||||
|
`src-tauri/capabilities/default.json` 中必须声明权限,否则前端 API 调用会被拦截:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"identifier": "default",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
// ... 其他权限 ...
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{ "url": "https://*.aliyuncs.com/**" } // ⚠️ 放行更新源域名
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updater:default", // ⚠️ 允许 check()、downloadAndInstall()
|
||||||
|
"process:default" // ⚠️ 允许 relaunch()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**三项缺一不可**:
|
||||||
|
- `updater:default` — 否则报 `updater.check not allowed`
|
||||||
|
- `process:default` — 否则 `relaunch()` 无法调用
|
||||||
|
- http allow 列表包含更新源域名 — 否则报 `Could not fetch a valid release JSON`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、latest.json 格式
|
||||||
|
|
||||||
|
更新源需要提供一个 `latest.json`,格式如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.1.11",
|
||||||
|
"pub_date": "2026-04-02T05:29:44Z",
|
||||||
|
"notes": "版本 v0.1.11 已发布",
|
||||||
|
"platforms": {
|
||||||
|
"windows-x86_64": {
|
||||||
|
"signature": "<.exe.sig 文件内容>",
|
||||||
|
"url": "https://<bucket>.<endpoint>/releases/v0.1.11/<app>_0.1.11_x64-setup.exe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `version` | 新版本号,updater 用它和本地版本比较 |
|
||||||
|
| `pub_date` | ISO 8601 格式,必须有 |
|
||||||
|
| `notes` | 更新说明,前端通过 `update.body` 获取 |
|
||||||
|
| `platforms` | 按平台提供下载链接和签名 |
|
||||||
|
| `signature` | `.exe.sig` 文件内容(base64),客户端用 pubkey 验证 |
|
||||||
|
| `url` | 安装包下载地址,必须可公开访问 |
|
||||||
|
|
||||||
|
### 多平台支持
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platforms": {
|
||||||
|
"windows-x86_64": { "signature": "...", "url": "...setup.exe" },
|
||||||
|
"darwin-x86_64": { "signature": "...", "url": "...app.tar.gz" },
|
||||||
|
"darwin-aarch64": { "signature": "...", "url": "...app.tar.gz" },
|
||||||
|
"linux-x86_64": { "signature": "...", "url": "...AppImage.tar.gz" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、前端实现(React + TypeScript)
|
||||||
|
|
||||||
|
### 6.1 状态机设计
|
||||||
|
|
||||||
|
更新流程有明确的状态流转,用联合类型建模最清晰:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type UpdateState =
|
||||||
|
| { status: "idle" } // 初始状态
|
||||||
|
| { status: "checking" } // 正在检查
|
||||||
|
| { status: "latest" } // 已是最新
|
||||||
|
| { status: "available"; version: string; notes: string; downloadAndInstall: () => Promise<void> }
|
||||||
|
| { status: "downloading"; progress: number } // 下载中(0~100)
|
||||||
|
| { status: "error"; message: string } // 出错
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 完整组件参考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||||
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
|
||||||
|
function UpdateSection() {
|
||||||
|
const [appVersion, setAppVersion] = useState("");
|
||||||
|
const [state, setState] = useState<UpdateState>({ status: "idle" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getVersion().then(setAppVersion).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCheck = async () => {
|
||||||
|
setState({ status: "checking" });
|
||||||
|
try {
|
||||||
|
const update = await checkUpdate();
|
||||||
|
if (!update?.available) {
|
||||||
|
setState({ status: "latest" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState({
|
||||||
|
status: "available",
|
||||||
|
version: update.version,
|
||||||
|
notes: update.body ?? "",
|
||||||
|
downloadAndInstall: async () => {
|
||||||
|
let downloaded = 0;
|
||||||
|
let total = 0;
|
||||||
|
await update.downloadAndInstall((event) => {
|
||||||
|
if (event.event === "Started") {
|
||||||
|
total = event.data.contentLength ?? 0;
|
||||||
|
setState({ status: "downloading", progress: 0 });
|
||||||
|
} else if (event.event === "Progress") {
|
||||||
|
downloaded += event.data.chunkLength;
|
||||||
|
const progress = total > 0 ? Math.round((downloaded / total) * 100) : 0;
|
||||||
|
setState({ status: "downloading", progress });
|
||||||
|
} else if (event.event === "Finished") {
|
||||||
|
setState({ status: "idle" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await relaunch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState({ status: "error", message: String(e) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>当前版本 v{appVersion}</p>
|
||||||
|
|
||||||
|
{state.status === "latest" && <span>已是最新版本</span>}
|
||||||
|
|
||||||
|
{state.status === "available" && (
|
||||||
|
<>
|
||||||
|
<span>v{state.version} 可用</span>
|
||||||
|
<button onClick={state.downloadAndInstall}>立即更新</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.status === "downloading" && (
|
||||||
|
<span>下载中 {state.progress}%</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.status === "error" && (
|
||||||
|
<div>
|
||||||
|
<p>{state.message}</p>
|
||||||
|
<button onClick={() => navigator.clipboard.writeText(state.message)}>
|
||||||
|
复制错误
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.status !== "available" && (
|
||||||
|
<button
|
||||||
|
onClick={handleCheck}
|
||||||
|
disabled={state.status === "checking" || state.status === "downloading"}
|
||||||
|
>
|
||||||
|
{state.status === "checking" ? "检查中…" : "检查更新"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 下载事件说明
|
||||||
|
|
||||||
|
`downloadAndInstall` 的回调事件顺序:
|
||||||
|
|
||||||
|
| 事件 | 数据 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `Started` | `{ contentLength?: number }` | 开始下载,contentLength 可能为 0 |
|
||||||
|
| `Progress` | `{ chunkLength: number }` | 每个数据块,累加计算进度 |
|
||||||
|
| `Finished` | 无 | 下载完成,随后自动安装 |
|
||||||
|
|
||||||
|
> `downloadAndInstall` 完成后立即调用 `relaunch()` 重启应用,新版本生效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、版本号管理
|
||||||
|
|
||||||
|
Tauri v2 项目有**三处版本号**必须保持一致:
|
||||||
|
|
||||||
|
| 文件 | 字段 | 作用 |
|
||||||
|
|------|------|------|
|
||||||
|
| `src-tauri/tauri.conf.json` | `"version"` | updater 用来和 latest.json 比较 |
|
||||||
|
| `src-tauri/Cargo.toml` | `version` | Rust 构建元数据 |
|
||||||
|
| `package.json` | `"version"` | 前端构建元数据 |
|
||||||
|
|
||||||
|
### 发版步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 三处改版本号(如 0.2.0)
|
||||||
|
# 2. 提交
|
||||||
|
git add -A
|
||||||
|
git commit -m "release: v0.2.0"
|
||||||
|
# 3. 打 tag + push(PowerShell 不支持 &&,分开执行)
|
||||||
|
git tag v0.2.0
|
||||||
|
git push
|
||||||
|
git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
> **版本号不一致的后果**:`latest.json` 中的 version 高于本地版本时才会提示更新。
|
||||||
|
> 如果 `tauri.conf.json` 的版本没改,每次启动都会提示"有新版本"即使已经更新过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、更新源选择
|
||||||
|
|
||||||
|
| 更新源 | 优点 | 缺点 | 适用场景 |
|
||||||
|
|--------|------|------|---------|
|
||||||
|
| **阿里云 OSS** | 国内快,CDN 加速 | 需要额外配置 OSS | 面向国内用户 |
|
||||||
|
| **GitHub Releases** | 零配置,tauri-action 自动生成 | 国内访问慢/不稳定 | 面向海外用户 |
|
||||||
|
| **自建服务器** | 完全可控 | 需要运维 | 企业内网分发 |
|
||||||
|
|
||||||
|
Tauri v2 的 `endpoints` 支持多个 URL,可以配置主备:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"endpoints": [
|
||||||
|
"https://<oss-bucket>.oss-cn-hangzhou.aliyuncs.com/latest.json",
|
||||||
|
"https://github.com/<user>/<repo>/releases/latest/download/latest.json"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、调试技巧
|
||||||
|
|
||||||
|
### 9.1 验证 latest.json 可访问
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
curl https://<your-endpoint>/latest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- 返回 JSON → 正常
|
||||||
|
- 返回 `AccessDenied` → OSS Bucket 未设为公共读
|
||||||
|
- 返回 404 → CI/CD 未上传或路径不对
|
||||||
|
|
||||||
|
### 9.2 开发模式测试
|
||||||
|
|
||||||
|
`pnpm tauri dev` 模式下也能调用 `check()`,但注意:
|
||||||
|
- 开发模式的版本号来自 `tauri.conf.json`
|
||||||
|
- 如果本地版本号和 latest.json 相同或更高,不会触发更新
|
||||||
|
|
||||||
|
### 9.3 强制触发更新(调试用)
|
||||||
|
|
||||||
|
临时把 `tauri.conf.json` 的版本号改低(如 `0.0.1`),再运行 `pnpm tauri dev` 点检查更新即可触发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、踩坑清单
|
||||||
|
|
||||||
|
| # | 问题 | 现象 | 解决方案 |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| 1 | 缺少 `updater:default` | `updater.check not allowed` | `capabilities/default.json` 添加 `updater:default` |
|
||||||
|
| 2 | 缺少 `process:default` | `relaunch()` 无效 | `capabilities/default.json` 添加 `process:default` |
|
||||||
|
| 3 | http 未放行更新源域名 | `Could not fetch a valid release JSON` | capabilities http allow 加更新源域名 |
|
||||||
|
| 4 | OSS Bucket 为私有 | 同上,curl 返回 `AccessDenied` | Bucket ACL 改为**公共读** |
|
||||||
|
| 5 | `createUpdaterArtifacts` 未开启 | 构建成功但无 `.exe.sig` | `tauri.conf.json` → `bundle.createUpdaterArtifacts: true` |
|
||||||
|
| 6 | pubkey 填了私钥 | `failed to decode pubkey` | 用公钥文件的 base64 |
|
||||||
|
| 7 | 版本号三处不一致 | 更新后仍提示有新版本 | `tauri.conf.json`、`Cargo.toml`、`package.json` 必须同步 |
|
||||||
|
| 8 | latest.json 缺少 `pub_date` | updater 解析失败 | 必须包含 ISO 8601 格式的日期 |
|
||||||
|
| 9 | signature 字段为空 | 签名验证失败 | 检查 CI/CD 是否正确读取了 `.exe.sig` 文件内容 |
|
||||||
|
| 10 | `dialog: true` 时无法自定义 UI | 弹出系统原生对话框 | 设为 `false`,用代码实现自定义更新界面 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件速查
|
||||||
|
|
||||||
|
新项目最小配置清单:
|
||||||
|
|
||||||
|
```
|
||||||
|
src-tauri/Cargo.toml → 添加 tauri-plugin-updater、tauri-plugin-process 依赖
|
||||||
|
package.json → pnpm add @tauri-apps/plugin-updater @tauri-apps/plugin-process
|
||||||
|
src-tauri/src/lib.rs → 注册两个插件
|
||||||
|
src-tauri/tauri.conf.json → bundle.createUpdaterArtifacts + plugins.updater 配置
|
||||||
|
src-tauri/capabilities/*.json → updater:default + process:default + http allow
|
||||||
|
前端组件 → 调用 check() + downloadAndInstall() + relaunch()
|
||||||
|
```
|
||||||
@ -18,6 +18,7 @@
|
|||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||||
|
"@xyflow/react": "^12.10.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
|
|||||||
189
pnpm-lock.yaml
generated
189
pnpm-lock.yaml
generated
@ -32,6 +32,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-updater':
|
'@tauri-apps/plugin-updater':
|
||||||
specifier: ^2.10.0
|
specifier: ^2.10.0
|
||||||
version: 2.10.0
|
version: 2.10.0
|
||||||
|
'@xyflow/react':
|
||||||
|
specifier: ^12.10.2
|
||||||
|
version: 12.10.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
@ -40,7 +43,7 @@ importers:
|
|||||||
version: 19.2.4(react@19.2.4)
|
version: 19.2.4(react@19.2.4)
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^5.0.12
|
specifier: ^5.0.12
|
||||||
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)
|
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.2.2
|
specifier: ^4.2.2
|
||||||
@ -654,6 +657,24 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||||
|
|
||||||
|
'@types/d3-color@3.1.3':
|
||||||
|
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||||
|
|
||||||
|
'@types/d3-drag@3.0.7':
|
||||||
|
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||||
|
|
||||||
|
'@types/d3-selection@3.0.11':
|
||||||
|
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||||
|
|
||||||
|
'@types/d3-transition@3.0.9':
|
||||||
|
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||||
|
|
||||||
|
'@types/d3-zoom@3.0.8':
|
||||||
|
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@ -671,6 +692,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@xyflow/react@12.10.2':
|
||||||
|
resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
|
'@xyflow/system@0.0.76':
|
||||||
|
resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.10:
|
baseline-browser-mapping@2.10.10:
|
||||||
resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==}
|
resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@ -684,12 +714,53 @@ packages:
|
|||||||
caniuse-lite@1.0.30001781:
|
caniuse-lite@1.0.30001781:
|
||||||
resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
|
resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
|
||||||
|
|
||||||
|
classcat@5.0.5:
|
||||||
|
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
|
||||||
|
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
|
d3-color@3.1.0:
|
||||||
|
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1:
|
||||||
|
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-ease@3.0.1:
|
||||||
|
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-selection@3.0.0:
|
||||||
|
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-timer@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-transition@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
d3-selection: 2 - 3
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@ -906,6 +977,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0:
|
||||||
|
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
vite@7.3.1:
|
vite@7.3.1:
|
||||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@ -949,6 +1025,21 @@ packages:
|
|||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
zustand@4.5.7:
|
||||||
|
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
|
||||||
|
engines: {node: '>=12.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=16.8'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=16.8'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
zustand@5.0.12:
|
zustand@5.0.12:
|
||||||
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
|
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
@ -1424,6 +1515,27 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
|
'@types/d3-color@3.1.3': {}
|
||||||
|
|
||||||
|
'@types/d3-drag@3.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-color': 3.1.3
|
||||||
|
|
||||||
|
'@types/d3-selection@3.0.11': {}
|
||||||
|
|
||||||
|
'@types/d3-transition@3.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3-zoom@3.0.8':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-interpolate': 3.0.4
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/react-dom@19.2.3(@types/react@19.2.14)':
|
'@types/react-dom@19.2.3(@types/react@19.2.14)':
|
||||||
@ -1446,6 +1558,29 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@xyflow/react@12.10.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@xyflow/system': 0.0.76
|
||||||
|
classcat: 5.0.5
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
zustand: 4.5.7(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
|
'@xyflow/system@0.0.76':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-drag': 3.0.7
|
||||||
|
'@types/d3-interpolate': 3.0.4
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
'@types/d3-transition': 3.0.9
|
||||||
|
'@types/d3-zoom': 3.0.8
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-zoom: 3.0.0
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.10: {}
|
baseline-browser-mapping@2.10.10: {}
|
||||||
|
|
||||||
browserslist@4.28.1:
|
browserslist@4.28.1:
|
||||||
@ -1458,10 +1593,48 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001781: {}
|
caniuse-lite@1.0.30001781: {}
|
||||||
|
|
||||||
|
classcat@5.0.5: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
d3-color@3.1.0: {}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1: {}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
|
||||||
|
d3-ease@3.0.1: {}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
|
||||||
|
d3-selection@3.0.0: {}
|
||||||
|
|
||||||
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
|
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-ease: 3.0.1
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@ -1661,6 +1834,10 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0):
|
vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.4
|
esbuild: 0.27.4
|
||||||
@ -1676,7 +1853,15 @@ snapshots:
|
|||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
zustand@5.0.12(@types/react@19.2.14)(react@19.2.4):
|
zustand@4.5.7(@types/react@19.2.14)(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
|
zustand@5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
react: 19.2.4
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
|
|||||||
22
src-tauri/Cargo.lock
generated
22
src-tauri/Cargo.lock
generated
@ -825,7 +825,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dev-manager-tauri"
|
name = "dev-manager-tauri"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -837,6 +837,7 @@ dependencies = [
|
|||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
@ -4121,6 +4122,19 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serialize-to-javascript"
|
name = "serialize-to-javascript"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -5381,6 +5395,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
@ -38,4 +38,5 @@ ureq = { version = "2", features = ["json"] }
|
|||||||
open = "5"
|
open = "5"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
|
||||||
|
|||||||
299
src-tauri/src/commands/blueprint.rs
Normal file
299
src-tauri/src/commands/blueprint.rs
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// ── manifest.yaml 对应结构 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct BlueprintManifest {
|
||||||
|
pub version: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub updated: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub areas: Vec<BlueprintArea>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub modules: Vec<BlueprintModule>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub edges: Vec<BlueprintEdge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct BlueprintArea {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct BlueprintModule {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub area: String,
|
||||||
|
pub status: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub progress: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub position: Vec<f64>,
|
||||||
|
// 运行时填充:从 modules/<id>.md 解析
|
||||||
|
#[serde(skip_deserializing, default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(skip_deserializing, default)]
|
||||||
|
pub decisions: Vec<String>,
|
||||||
|
#[serde(skip_deserializing, default)]
|
||||||
|
pub tasks: Vec<BlueprintTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct BlueprintEdge {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub edge_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct BlueprintTask {
|
||||||
|
pub title: String,
|
||||||
|
pub status: String,
|
||||||
|
pub complexity: Option<String>,
|
||||||
|
pub files: Option<String>,
|
||||||
|
pub depends: Option<String>,
|
||||||
|
pub acceptance: Option<String>,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 完整蓝图返回结构 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
pub struct BlueprintData {
|
||||||
|
pub manifest: BlueprintManifest,
|
||||||
|
pub stats: BlueprintStats,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
pub struct BlueprintStats {
|
||||||
|
pub total_modules: u32,
|
||||||
|
pub done: u32,
|
||||||
|
pub in_progress: u32,
|
||||||
|
pub planned: u32,
|
||||||
|
pub concept: u32,
|
||||||
|
pub total_tasks: u32,
|
||||||
|
pub tasks_done: u32,
|
||||||
|
pub dispatchable: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tauri command ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_blueprint(project_path: String) -> Result<Option<BlueprintData>, String> {
|
||||||
|
let bp_dir = Path::new(&project_path).join(".blueprint");
|
||||||
|
let manifest_path = bp_dir.join("manifest.yaml");
|
||||||
|
|
||||||
|
if !manifest_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let yaml_str = std::fs::read_to_string(&manifest_path)
|
||||||
|
.map_err(|e| format!("读取 manifest.yaml 失败: {e}"))?;
|
||||||
|
let mut manifest: BlueprintManifest = serde_yaml::from_str(&yaml_str)
|
||||||
|
.map_err(|e| format!("解析 manifest.yaml 失败: {e}"))?;
|
||||||
|
|
||||||
|
// 读取每个模块的 markdown 文件
|
||||||
|
let modules_dir = bp_dir.join("modules");
|
||||||
|
for module in &mut manifest.modules {
|
||||||
|
let md_path = modules_dir.join(format!("{}.md", module.id));
|
||||||
|
if md_path.exists() {
|
||||||
|
let content = std::fs::read_to_string(&md_path).unwrap_or_default();
|
||||||
|
let parsed = parse_module_md(&content);
|
||||||
|
module.description = parsed.description;
|
||||||
|
module.decisions = parsed.decisions;
|
||||||
|
module.tasks = parsed.tasks;
|
||||||
|
|
||||||
|
// 自动计算 progress:有任务卡时按完成比例,无任务卡保留 manifest 中的值
|
||||||
|
if !module.tasks.is_empty() {
|
||||||
|
let done_count = module.tasks.iter().filter(|t| t.status == "done").count();
|
||||||
|
module.progress = ((done_count as f64 / module.tasks.len() as f64) * 100.0).round() as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
let stats = compute_stats(&manifest);
|
||||||
|
|
||||||
|
Ok(Some(BlueprintData { manifest, stats }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── markdown 解析 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct ParsedModule {
|
||||||
|
description: Option<String>,
|
||||||
|
decisions: Vec<String>,
|
||||||
|
tasks: Vec<BlueprintTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_module_md(content: &str) -> ParsedModule {
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
let mut description = None;
|
||||||
|
let mut decisions: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
let body = skip_frontmatter(content);
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum Section { None, Overview, Decisions, Tasks }
|
||||||
|
let mut section = Section::None;
|
||||||
|
let mut overview_lines: Vec<&str> = Vec::new();
|
||||||
|
let mut current_task: Option<BlueprintTask> = None;
|
||||||
|
|
||||||
|
for line in body.lines() {
|
||||||
|
// 二级标题切换段落
|
||||||
|
if line.starts_with("## ") {
|
||||||
|
if let Some(task) = current_task.take() {
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
let heading = line.trim_start_matches("## ").trim();
|
||||||
|
section = match heading {
|
||||||
|
"概述" => Section::Overview,
|
||||||
|
"决策记录" => Section::Decisions,
|
||||||
|
"任务卡" => Section::Tasks,
|
||||||
|
_ => Section::None,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 三级标题 = 任务卡(仅在任务卡段落内)
|
||||||
|
if line.starts_with("### ") && (section == Section::Tasks || section == Section::None) {
|
||||||
|
section = Section::Tasks;
|
||||||
|
if let Some(task) = current_task.take() {
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
let title_raw = line.trim_start_matches("### ").trim();
|
||||||
|
let (status, title) = parse_task_title(title_raw);
|
||||||
|
current_task = Some(BlueprintTask {
|
||||||
|
title: title.to_string(),
|
||||||
|
status,
|
||||||
|
complexity: None,
|
||||||
|
files: None,
|
||||||
|
depends: None,
|
||||||
|
acceptance: None,
|
||||||
|
notes: None,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match section {
|
||||||
|
Section::Overview => {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
overview_lines.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section::Decisions => {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
// 收集 "- xxx" 格式的决策条目
|
||||||
|
if let Some(item) = trimmed.strip_prefix("- ") {
|
||||||
|
if !item.is_empty() {
|
||||||
|
decisions.push(item.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section::Tasks => {
|
||||||
|
if let Some(ref mut task) = current_task {
|
||||||
|
let trimmed = line.trim().trim_start_matches("- ");
|
||||||
|
if let Some(val) = trimmed.strip_prefix("status: ") {
|
||||||
|
task.status = val.trim().to_string();
|
||||||
|
} else if let Some(val) = trimmed.strip_prefix("complexity: ") {
|
||||||
|
task.complexity = Some(val.trim().to_string());
|
||||||
|
} else if let Some(val) = trimmed.strip_prefix("files: ") {
|
||||||
|
task.files = Some(val.trim().to_string());
|
||||||
|
} else if let Some(val) = trimmed.strip_prefix("depends: ") {
|
||||||
|
task.depends = Some(val.trim().to_string());
|
||||||
|
} else if let Some(val) = trimmed.strip_prefix("acceptance: ") {
|
||||||
|
task.acceptance = Some(val.trim().to_string());
|
||||||
|
} else if let Some(val) = trimmed.strip_prefix("notes: ") {
|
||||||
|
task.notes = Some(val.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = current_task {
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !overview_lines.is_empty() {
|
||||||
|
description = Some(overview_lines.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedModule { description, decisions, tasks }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_frontmatter(content: &str) -> &str {
|
||||||
|
if content.starts_with("---") {
|
||||||
|
if let Some(end) = content[3..].find("\n---") {
|
||||||
|
let skip = 3 + end + 4; // "---" + content + "\n---"
|
||||||
|
return content.get(skip..).unwrap_or("").trim_start_matches('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_task_title(raw: &str) -> (String, &str) {
|
||||||
|
// "✅ 基础操作" → ("done", "基础操作")
|
||||||
|
if let Some(rest) = raw.strip_prefix("✅ ") {
|
||||||
|
return ("done".to_string(), rest);
|
||||||
|
}
|
||||||
|
if let Some(rest) = raw.strip_prefix("🔵 ") {
|
||||||
|
return ("in_progress".to_string(), rest);
|
||||||
|
}
|
||||||
|
if let Some(rest) = raw.strip_prefix("📋 ") {
|
||||||
|
return ("todo".to_string(), rest);
|
||||||
|
}
|
||||||
|
if let Some(rest) = raw.strip_prefix("💭 ") {
|
||||||
|
return ("concept".to_string(), rest);
|
||||||
|
}
|
||||||
|
("todo".to_string(), raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_stats(manifest: &BlueprintManifest) -> BlueprintStats {
|
||||||
|
let mut stats = BlueprintStats {
|
||||||
|
total_modules: manifest.modules.len() as u32,
|
||||||
|
done: 0,
|
||||||
|
in_progress: 0,
|
||||||
|
planned: 0,
|
||||||
|
concept: 0,
|
||||||
|
total_tasks: 0,
|
||||||
|
tasks_done: 0,
|
||||||
|
dispatchable: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for m in &manifest.modules {
|
||||||
|
match m.status.as_str() {
|
||||||
|
"done" => stats.done += 1,
|
||||||
|
"in_progress" => stats.in_progress += 1,
|
||||||
|
"planned" => stats.planned += 1,
|
||||||
|
"concept" => stats.concept += 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
for t in &m.tasks {
|
||||||
|
stats.total_tasks += 1;
|
||||||
|
if t.status == "done" {
|
||||||
|
stats.tasks_done += 1;
|
||||||
|
}
|
||||||
|
// 可派发:todo + 有 files + 有 acceptance + complexity S 或 M
|
||||||
|
if t.status == "todo"
|
||||||
|
&& t.files.is_some()
|
||||||
|
&& t.acceptance.is_some()
|
||||||
|
&& matches!(
|
||||||
|
t.complexity.as_deref(),
|
||||||
|
Some("S") | Some("M")
|
||||||
|
)
|
||||||
|
{
|
||||||
|
stats.dispatchable += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats
|
||||||
|
}
|
||||||
30
src-tauri/src/commands/canvas.rs
Normal file
30
src-tauri/src/commands/canvas.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use crate::db::pool;
|
||||||
|
use rusqlite::params;
|
||||||
|
|
||||||
|
/// 读取产品组的画布状态(JSON 字符串),不存在时返回 null
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_canvas_state(group_id: String) -> Result<Option<String>, String> {
|
||||||
|
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||||
|
match conn.query_row(
|
||||||
|
"SELECT data FROM canvas_states WHERE group_id = ?1",
|
||||||
|
params![group_id],
|
||||||
|
|row| row.get::<_, String>(0),
|
||||||
|
) {
|
||||||
|
Ok(data) => Ok(Some(data)),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存(覆盖)产品组的画布状态
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn save_canvas_state(group_id: String, data: String) -> Result<(), String> {
|
||||||
|
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO canvas_states (group_id, data, updated_at)
|
||||||
|
VALUES (?1, ?2, datetime('now'))",
|
||||||
|
params![group_id, data],
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -16,3 +16,5 @@ pub mod publish;
|
|||||||
pub mod templates;
|
pub mod templates;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod cicd;
|
pub mod cicd;
|
||||||
|
pub mod blueprint;
|
||||||
|
pub mod canvas;
|
||||||
|
|||||||
@ -379,4 +379,13 @@ fn migrate(conn: &rusqlite::Connection) {
|
|||||||
created_at TEXT DEFAULT (datetime('now'))
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
").expect("create cicd_configs table");
|
").expect("create cicd_configs table");
|
||||||
|
|
||||||
|
// 资源视角画布状态(每个产品组独立保存)
|
||||||
|
conn.execute_batch("
|
||||||
|
CREATE TABLE IF NOT EXISTS canvas_states (
|
||||||
|
group_id TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
").expect("create canvas_states table");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ mod db;
|
|||||||
mod models;
|
mod models;
|
||||||
use git2;
|
use git2;
|
||||||
|
|
||||||
use commands::{activity::*, cicd::*, devtools::*, git_ops::*, git_repos::*, github::*, groups::*, network::{get_wsl_ip, sync_wsl_proxy, get_wsl_proxy_status, clear_wsl_proxy, test_wsl_proxy_ip, test_win_proxy_ip}, project_scan::*, project_tools::*, projects::{batch_create_projects, create_project, delete_project, get_project, get_projects, scan_subfolders, update_project, update_project_tags}, proxy::*, publish::*, settings::{detect_editors, get_settings, search_editor_path, update_settings}, shell::*, spaces::*, tags::*, templates::*};
|
use commands::{activity::*, blueprint::*, canvas::*, cicd::*, devtools::*, git_ops::*, git_repos::*, github::*, groups::*, network::{get_wsl_ip, sync_wsl_proxy, get_wsl_proxy_status, clear_wsl_proxy, test_wsl_proxy_ip, test_win_proxy_ip}, project_scan::*, project_tools::*, projects::{batch_create_projects, create_project, delete_project, get_project, get_projects, scan_subfolders, update_project, update_project_tags}, proxy::*, publish::*, settings::{detect_editors, get_settings, search_editor_path, update_settings}, shell::*, spaces::*, tags::*, templates::*};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@ -148,6 +148,11 @@ pub fn run() {
|
|||||||
get_cicd_config,
|
get_cicd_config,
|
||||||
save_cicd_config,
|
save_cicd_config,
|
||||||
generate_workflow,
|
generate_workflow,
|
||||||
|
// blueprint
|
||||||
|
get_blueprint,
|
||||||
|
// canvas state
|
||||||
|
get_canvas_state,
|
||||||
|
save_canvas_state,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
656
src/components/dashboard/BlueprintModal.tsx
Normal file
656
src/components/dashboard/BlueprintModal.tsx
Normal file
@ -0,0 +1,656 @@
|
|||||||
|
import { useState, useCallback, useMemo } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
type Node,
|
||||||
|
type Edge,
|
||||||
|
type NodeTypes,
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
MarkerType,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import {
|
||||||
|
getBlueprint,
|
||||||
|
type BlueprintData,
|
||||||
|
type BlueprintModule,
|
||||||
|
type BlueprintTask,
|
||||||
|
type BlueprintArea,
|
||||||
|
} from "../../lib/commands";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projectName: string;
|
||||||
|
projectPath: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 状态色标 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STATUS_CONFIG: Record<string, { color: string; bg: string; label: string }> = {
|
||||||
|
done: { color: "#22C55E", bg: "#F0FDF4", label: "已完成" },
|
||||||
|
in_progress: { color: "#3B82F6", bg: "#EFF6FF", label: "进行中" },
|
||||||
|
planned: { color: "#EAB308", bg: "#FEFCE8", label: "已规划" },
|
||||||
|
concept: { color: "#9CA3AF", bg: "#F9FAFB", label: "构思中" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const TASK_PREFIX: Record<string, string> = {
|
||||||
|
done: "✅",
|
||||||
|
in_progress: "🔵",
|
||||||
|
todo: "📋",
|
||||||
|
concept: "💭",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 领域分组节点 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function AreaNode({ data }: { data: { label: string; color: string } }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-xl border-2 border-dashed px-4 pt-2 pb-3 min-w-[200px] min-h-[100px] pointer-events-none"
|
||||||
|
style={{
|
||||||
|
borderColor: data.color + "60",
|
||||||
|
background: data.color + "08",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="w-3 h-1 rounded-full" style={{ background: data.color }} />
|
||||||
|
<span className="text-[11px] font-bold uppercase tracking-wider" style={{ color: data.color }}>
|
||||||
|
{data.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 模块节点 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function ModuleNode({ data }: { data: BlueprintModule & { areaColor?: string; onClick: () => void } }) {
|
||||||
|
const cfg = STATUS_CONFIG[data.status] ?? STATUS_CONFIG.concept;
|
||||||
|
const tasksDone = data.tasks.filter((t) => t.status === "done").length;
|
||||||
|
const tasksTotal = data.tasks.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={data.onClick}
|
||||||
|
className="cursor-pointer rounded-lg border-2 shadow-sm px-3 py-2.5 min-w-[160px] max-w-[200px] transition-all hover:shadow-md hover:scale-[1.02]"
|
||||||
|
style={{
|
||||||
|
borderColor: cfg.color,
|
||||||
|
background: cfg.bg,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: data.areaColor ?? cfg.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Handle type="target" position={Position.Left} className="!w-2 !h-2 !-left-1" style={{ background: cfg.color }} />
|
||||||
|
<Handle type="source" position={Position.Right} className="!w-2 !h-2 !-right-1" style={{ background: cfg.color }} />
|
||||||
|
<Handle type="target" position={Position.Top} className="!w-2 !h-2 !-top-1" style={{ background: cfg.color }} />
|
||||||
|
<Handle type="source" position={Position.Bottom} className="!w-2 !h-2 !-bottom-1" style={{ background: cfg.color }} />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 mb-1">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: cfg.color }} />
|
||||||
|
<span className="text-xs font-semibold text-gray-800 truncate">{data.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden mb-1">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all"
|
||||||
|
style={{ width: `${data.progress}%`, background: cfg.color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-gray-500">{cfg.label}</span>
|
||||||
|
{tasksTotal > 0 && (
|
||||||
|
<span className="text-[10px] text-gray-400">{tasksDone}/{tasksTotal}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeTypes: NodeTypes = {
|
||||||
|
module: ModuleNode,
|
||||||
|
area: AreaNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 布局计算 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const NODE_W = 180;
|
||||||
|
const NODE_H = 75;
|
||||||
|
const GAP_X = 40;
|
||||||
|
const GAP_Y = 30;
|
||||||
|
const AREA_PAD_X = 20;
|
||||||
|
const AREA_PAD_Y = 40;
|
||||||
|
const AREA_GAP = 50;
|
||||||
|
const COLS_PER_AREA = 4;
|
||||||
|
|
||||||
|
function autoLayout(
|
||||||
|
areas: BlueprintArea[],
|
||||||
|
modules: BlueprintModule[],
|
||||||
|
) {
|
||||||
|
const areaNodes: Node[] = [];
|
||||||
|
const moduleNodes: Node[] = [];
|
||||||
|
let areaOffsetY = 0;
|
||||||
|
|
||||||
|
for (const area of areas) {
|
||||||
|
const areaModules = modules.filter((m) => m.area === area.id);
|
||||||
|
if (areaModules.length === 0) continue;
|
||||||
|
|
||||||
|
const cols = Math.min(areaModules.length, COLS_PER_AREA);
|
||||||
|
const rows = Math.ceil(areaModules.length / cols);
|
||||||
|
|
||||||
|
const areaW = cols * (NODE_W + GAP_X) - GAP_X + AREA_PAD_X * 2;
|
||||||
|
const areaH = rows * (NODE_H + GAP_Y) - GAP_Y + AREA_PAD_Y + AREA_PAD_X;
|
||||||
|
|
||||||
|
areaNodes.push({
|
||||||
|
id: `area-${area.id}`,
|
||||||
|
type: "area",
|
||||||
|
position: { x: 0, y: areaOffsetY },
|
||||||
|
data: { label: area.name, color: area.color ?? "#6B7280" },
|
||||||
|
style: { width: areaW, height: areaH },
|
||||||
|
selectable: false,
|
||||||
|
draggable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
areaModules.forEach((m, i) => {
|
||||||
|
const col = i % cols;
|
||||||
|
const row = Math.floor(i / cols);
|
||||||
|
moduleNodes.push({
|
||||||
|
id: m.id,
|
||||||
|
type: "module",
|
||||||
|
position: {
|
||||||
|
x: AREA_PAD_X + col * (NODE_W + GAP_X),
|
||||||
|
y: AREA_PAD_Y + row * (NODE_H + GAP_Y),
|
||||||
|
},
|
||||||
|
parentId: `area-${area.id}`,
|
||||||
|
extent: "parent" as const,
|
||||||
|
data: { ...m, areaColor: area.color },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
areaOffsetY += areaH + AREA_GAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { areaNodes, moduleNodes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 主组件 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | null;
|
||||||
|
|
||||||
|
export function BlueprintModal({ projectName, projectPath, onClose }: Props) {
|
||||||
|
const [sidePanel, setSidePanel] = useState<SidePanel>(null);
|
||||||
|
|
||||||
|
const { data: blueprint, isLoading } = useQuery<BlueprintData | null>({
|
||||||
|
queryKey: ["blueprint", projectPath],
|
||||||
|
queryFn: () => getBlueprint(projectPath),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNodeClick = useCallback((mod: BlueprintModule) => {
|
||||||
|
setSidePanel({ type: "module", module: mod });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { nodes, edges } = useMemo(() => {
|
||||||
|
if (!blueprint) return { nodes: [] as Node[], edges: [] as Edge[] };
|
||||||
|
|
||||||
|
const { areaNodes, moduleNodes } = autoLayout(
|
||||||
|
blueprint.manifest.areas,
|
||||||
|
blueprint.manifest.modules,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 注入 onClick
|
||||||
|
for (const n of moduleNodes) {
|
||||||
|
const mod = blueprint.manifest.modules.find((m) => m.id === n.id);
|
||||||
|
if (mod) {
|
||||||
|
n.data = { ...n.data, onClick: () => handleNodeClick(mod) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowNodes: Node[] = [...areaNodes, ...moduleNodes];
|
||||||
|
|
||||||
|
const flowEdges: Edge[] = blueprint.manifest.edges.map((e, i) => ({
|
||||||
|
id: `e-${i}`,
|
||||||
|
source: e.from,
|
||||||
|
target: e.to,
|
||||||
|
type: "smoothstep",
|
||||||
|
animated: e.edge_type === "dependency",
|
||||||
|
style: {
|
||||||
|
stroke: e.edge_type === "dependency" ? "#3B82F6" : "#D1D5DB",
|
||||||
|
strokeWidth: e.edge_type === "dependency" ? 2 : 1,
|
||||||
|
strokeDasharray: e.edge_type === "related" ? "6 4" : undefined,
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: e.edge_type === "dependency" ? "#3B82F6" : "#D1D5DB",
|
||||||
|
width: 16,
|
||||||
|
height: 12,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { nodes: flowNodes, edges: flowEdges };
|
||||||
|
}, [blueprint, handleNodeClick]);
|
||||||
|
|
||||||
|
const stats = blueprint?.stats;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-xl shadow-2xl w-[95vw] max-w-[1200px] h-[85vh] flex flex-col">
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div className="px-5 py-3 border-b border-gray-200 flex items-center justify-between shrink-0">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-gray-800">项目蓝图</h2>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
|
{projectName}
|
||||||
|
{blueprint?.manifest.updated && (
|
||||||
|
<span className="ml-2 text-gray-300">更新于 {blueprint.manifest.updated}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计条 */}
|
||||||
|
{stats && (
|
||||||
|
<div className="px-5 py-2.5 border-b border-gray-100 flex items-center gap-4 shrink-0 flex-wrap">
|
||||||
|
<StatBadge color="#22C55E" label="已完成" count={stats.done} />
|
||||||
|
<StatBadge color="#3B82F6" label="进行中" count={stats.in_progress} />
|
||||||
|
<StatBadge color="#EAB308" label="已规划" count={stats.planned} />
|
||||||
|
<StatBadge color="#9CA3AF" label="构思中" count={stats.concept} />
|
||||||
|
<div className="h-4 w-px bg-gray-200" />
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
任务 {stats.tasks_done}/{stats.total_tasks}
|
||||||
|
</span>
|
||||||
|
{stats.dispatchable > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSidePanel((p) => p?.type === "next" ? null : { type: "next" })}
|
||||||
|
className={`text-xs px-2 py-0.5 rounded-full transition-colors ${
|
||||||
|
sidePanel?.type === "next"
|
||||||
|
? "text-white bg-blue-600"
|
||||||
|
: "text-blue-600 bg-blue-50 hover:bg-blue-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📋 {stats.dispatchable} 可派发
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{/* 总进度条 */}
|
||||||
|
<div className="flex-1 flex items-center gap-2 ml-2 min-w-[120px]">
|
||||||
|
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-green-500 rounded-full transition-all"
|
||||||
|
style={{ width: `${stats.total_modules > 0 ? Math.round((stats.done / stats.total_modules) * 100) : 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400 shrink-0">
|
||||||
|
{stats.total_modules > 0 ? Math.round((stats.done / stats.total_modules) * 100) : 0}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 图例 */}
|
||||||
|
{blueprint && (
|
||||||
|
<div className="px-5 py-1.5 border-b border-gray-50 flex items-center gap-5 shrink-0">
|
||||||
|
<span className="text-[10px] text-gray-400">图例:</span>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="#3B82F6" strokeWidth="2" /><polygon points="20,1 24,4 20,7" fill="#3B82F6" /></svg>
|
||||||
|
<span className="text-[10px] text-gray-400">依赖</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="#D1D5DB" strokeWidth="1" strokeDasharray="4 3" /><polygon points="20,1 24,4 20,7" fill="#D1D5DB" /></svg>
|
||||||
|
<span className="text-[10px] text-gray-400">关联</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-3 w-px bg-gray-200" />
|
||||||
|
{blueprint.manifest.areas.map((a) => (
|
||||||
|
<div key={a.id} className="flex items-center gap-1">
|
||||||
|
<span className="w-2.5 h-2.5 rounded border" style={{ background: (a.color ?? "#6B7280") + "20", borderColor: (a.color ?? "#6B7280") + "60" }} />
|
||||||
|
<span className="text-[10px] text-gray-400">{a.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主体区域 */}
|
||||||
|
<div className="flex-1 flex min-h-0">
|
||||||
|
{/* React Flow 画布 */}
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
) : !blueprint ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-gray-50 flex items-center justify-center text-2xl">🗺️</div>
|
||||||
|
<p className="text-gray-400 text-sm">该项目尚未创建蓝图</p>
|
||||||
|
<p className="text-gray-300 text-xs">
|
||||||
|
在项目根目录创建 <code className="bg-gray-100 px-1.5 py-0.5 rounded text-gray-500">.blueprint/</code> 目录即可启用
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
fitViewOptions={{ padding: 0.3 }}
|
||||||
|
minZoom={0.2}
|
||||||
|
maxZoom={2}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
nodesDraggable={false}
|
||||||
|
>
|
||||||
|
<Background gap={20} size={1} color="#f0f0f0" />
|
||||||
|
<Controls position="bottom-left" showInteractive={false} />
|
||||||
|
</ReactFlow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 侧边面板 */}
|
||||||
|
{sidePanel && (
|
||||||
|
<div className="w-[340px] border-l border-gray-200 overflow-y-auto shrink-0 bg-gray-50/50">
|
||||||
|
{sidePanel.type === "module" ? (
|
||||||
|
<ModuleDetail
|
||||||
|
module={sidePanel.module}
|
||||||
|
areas={blueprint?.manifest.areas ?? []}
|
||||||
|
onClose={() => setSidePanel(null)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<NextActionsPanel
|
||||||
|
modules={blueprint?.manifest.modules ?? []}
|
||||||
|
onSelectModule={(mod) => setSidePanel({ type: "module", module: mod })}
|
||||||
|
onClose={() => setSidePanel(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 统计小标签 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function StatBadge({ color, label, count }: { color: string; label: string; count: number }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: color }} />
|
||||||
|
<span className="text-xs text-gray-500">{label}</span>
|
||||||
|
<span className="text-xs font-semibold text-gray-700">{count}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 模块详情面板 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function ModuleDetail({ module: mod, areas, onClose }: { module: BlueprintModule; areas: BlueprintArea[]; onClose: () => void }) {
|
||||||
|
const cfg = STATUS_CONFIG[mod.status] ?? STATUS_CONFIG.concept;
|
||||||
|
const area = areas.find((a) => a.id === mod.area);
|
||||||
|
|
||||||
|
const grouped = useMemo(() => {
|
||||||
|
const groups: Record<string, BlueprintTask[]> = {
|
||||||
|
done: [],
|
||||||
|
in_progress: [],
|
||||||
|
todo: [],
|
||||||
|
concept: [],
|
||||||
|
};
|
||||||
|
for (const t of mod.tasks) {
|
||||||
|
const key = groups[t.status] ? t.status : "concept";
|
||||||
|
groups[key].push(t);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}, [mod.tasks]);
|
||||||
|
|
||||||
|
const groupLabels: Record<string, string> = {
|
||||||
|
done: "已完成",
|
||||||
|
in_progress: "进行中",
|
||||||
|
todo: "待开发",
|
||||||
|
concept: "构思中",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-3 h-3 rounded-full" style={{ background: cfg.color }} />
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800">{mod.name}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-xs text-gray-400">{cfg.label} · {mod.progress}%</span>
|
||||||
|
{area && (
|
||||||
|
<span
|
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded"
|
||||||
|
style={{ background: (area.color ?? "#6B7280") + "15", color: area.color ?? "#6B7280" }}
|
||||||
|
>
|
||||||
|
{area.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="w-full h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full rounded-full" style={{ width: `${mod.progress}%`, background: cfg.color }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
|
{mod.description && (
|
||||||
|
<p className="text-xs text-gray-500 leading-relaxed bg-white rounded-lg px-3 py-2 border border-gray-100">
|
||||||
|
{mod.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 决策记录 */}
|
||||||
|
{mod.decisions && mod.decisions.length > 0 && (
|
||||||
|
<div className="bg-amber-50 rounded-lg px-3 py-2 border border-amber-100">
|
||||||
|
<p className="text-[10px] font-semibold text-amber-600 uppercase tracking-wider mb-1">决策记录</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{mod.decisions.map((d, i) => (
|
||||||
|
<li key={i} className="text-xs text-amber-700 flex gap-1.5">
|
||||||
|
<span className="shrink-0 text-amber-400">·</span>
|
||||||
|
<span>{d}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 任务卡列表 */}
|
||||||
|
{mod.tasks.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{(["in_progress", "todo", "concept", "done"] as const).map((status) => {
|
||||||
|
const tasks = grouped[status];
|
||||||
|
if (!tasks || tasks.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div key={status}>
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider mb-1.5">
|
||||||
|
{groupLabels[status]} ({tasks.length})
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{tasks.map((t, i) => (
|
||||||
|
<TaskCard key={i} task={t} moduleName={mod.name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 任务卡 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TaskCard({ task, moduleName }: { task: BlueprintTask; moduleName?: string }) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const prefix = TASK_PREFIX[task.status] ?? "";
|
||||||
|
const isDispatchable =
|
||||||
|
task.status === "todo" &&
|
||||||
|
task.files &&
|
||||||
|
task.acceptance &&
|
||||||
|
(task.complexity === "S" || task.complexity === "M");
|
||||||
|
|
||||||
|
const handleCopyForAgent = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const lines = [
|
||||||
|
`请完成以下任务(完成后更新 .blueprint/modules/ 中对应的任务卡状态为 ✅ done):`,
|
||||||
|
``,
|
||||||
|
`## ${task.title}`,
|
||||||
|
moduleName ? `所属模块: ${moduleName}` : "",
|
||||||
|
task.complexity ? `复杂度: ${task.complexity}` : "",
|
||||||
|
``,
|
||||||
|
task.files ? `### 涉及文件\n${task.files}` : "",
|
||||||
|
task.acceptance ? `\n### 验收标准\n${task.acceptance}` : "",
|
||||||
|
task.depends ? `\n### 前置依赖\n${task.depends}` : "",
|
||||||
|
task.notes ? `\n### 补充说明\n${task.notes}` : "",
|
||||||
|
].filter(Boolean);
|
||||||
|
navigator.clipboard.writeText(lines.join("\n"));
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className={`rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
|
||||||
|
isDispatchable
|
||||||
|
? "border-blue-200 bg-blue-50/50 hover:bg-blue-50"
|
||||||
|
: "border-gray-100 bg-white hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs shrink-0">{prefix}</span>
|
||||||
|
<span className="text-xs text-gray-700 flex-1">{task.title}</span>
|
||||||
|
{task.complexity && (
|
||||||
|
<span className="text-[10px] text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded shrink-0">
|
||||||
|
{task.complexity}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isDispatchable && (
|
||||||
|
<button
|
||||||
|
onClick={handleCopyForAgent}
|
||||||
|
className="text-[10px] text-blue-500 bg-blue-100 px-1.5 py-0.5 rounded shrink-0 hover:bg-blue-200 transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? "已复制" : "复制派发"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div className="mt-2 pt-2 border-t border-gray-100 space-y-1 text-[11px] text-gray-500">
|
||||||
|
{task.files && (
|
||||||
|
<p><span className="text-gray-400">文件:</span> <code className="font-mono text-gray-600 bg-gray-50 px-1 rounded">{task.files}</code></p>
|
||||||
|
)}
|
||||||
|
{task.acceptance && (
|
||||||
|
<p><span className="text-gray-400">验收:</span> {task.acceptance}</p>
|
||||||
|
)}
|
||||||
|
{task.depends && (
|
||||||
|
<p><span className="text-gray-400">依赖:</span> {task.depends}</p>
|
||||||
|
)}
|
||||||
|
{task.notes && (
|
||||||
|
<p><span className="text-gray-400">备注:</span> {task.notes}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 下一步汇总面板 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function NextActionsPanel({
|
||||||
|
modules,
|
||||||
|
onSelectModule,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
modules: BlueprintModule[];
|
||||||
|
onSelectModule: (mod: BlueprintModule) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
// 收集所有可派发和进行中的任务
|
||||||
|
const actionItems = useMemo(() => {
|
||||||
|
const items: { module: BlueprintModule; task: BlueprintTask; dispatchable: boolean }[] = [];
|
||||||
|
for (const mod of modules) {
|
||||||
|
for (const t of mod.tasks) {
|
||||||
|
const dispatchable =
|
||||||
|
t.status === "todo" &&
|
||||||
|
!!t.files &&
|
||||||
|
!!t.acceptance &&
|
||||||
|
(t.complexity === "S" || t.complexity === "M");
|
||||||
|
if (dispatchable || t.status === "in_progress") {
|
||||||
|
items.push({ module: mod, task: t, dispatchable });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 进行中排前面,可派发排后面
|
||||||
|
items.sort((a, b) => {
|
||||||
|
if (a.task.status === "in_progress" && b.task.status !== "in_progress") return -1;
|
||||||
|
if (a.task.status !== "in_progress" && b.task.status === "in_progress") return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}, [modules]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800">下一步</h3>
|
||||||
|
<button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actionItems.length === 0 ? (
|
||||||
|
<p className="text-xs text-gray-400 text-center py-6">暂无可执行任务</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{actionItems.filter((i) => i.task.status === "in_progress").length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-semibold text-blue-500 uppercase tracking-wider mb-1.5">进行中</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{actionItems
|
||||||
|
.filter((i) => i.task.status === "in_progress")
|
||||||
|
.map((item, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectModule(item.module)}
|
||||||
|
className="text-[10px] text-gray-400 hover:text-blue-500 mb-0.5 transition-colors"
|
||||||
|
>
|
||||||
|
{item.module.name} →
|
||||||
|
</button>
|
||||||
|
<TaskCard task={item.task} moduleName={item.module.name} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{actionItems.filter((i) => i.dispatchable).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wider mb-1.5">可派发</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{actionItems
|
||||||
|
.filter((i) => i.dispatchable)
|
||||||
|
.map((item, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectModule(item.module)}
|
||||||
|
className="text-[10px] text-gray-400 hover:text-blue-500 mb-0.5 transition-colors"
|
||||||
|
>
|
||||||
|
{item.module.name} →
|
||||||
|
</button>
|
||||||
|
<TaskCard task={item.task} moduleName={item.module.name} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { getGroups, getGroupMaps, getProjects, getSpaces, getTags, getAllProjectTagMaps, type ProductGroup, type Project } from "../../lib/commands";
|
import { getGroups, getGroupMaps, getProjects, getSpaces, getTags, getAllProjectTagMaps, type ProductGroup, type Project } from "../../lib/commands";
|
||||||
import { ProjectCard } from "./ProjectCard";
|
import { ProjectCard } from "./ProjectCard";
|
||||||
import { DeployModal } from "./DeployModal";
|
import { DeployModal } from "./DeployModal";
|
||||||
|
import { GroupResourceCanvas } from "./GroupResourceCanvas";
|
||||||
import { ManagePage } from "../manage/ManagePage";
|
import { ManagePage } from "../manage/ManagePage";
|
||||||
import { BatchAddModal } from "./BatchAddModal";
|
import { BatchAddModal } from "./BatchAddModal";
|
||||||
import { MountRepoModal } from "./MountRepoModal";
|
import { MountRepoModal } from "./MountRepoModal";
|
||||||
@ -14,6 +15,7 @@ import { openUrl } from "@tauri-apps/plugin-opener";
|
|||||||
|
|
||||||
type DashView = "board" | "manage";
|
type DashView = "board" | "manage";
|
||||||
type BoardTab = "groups" | "standalone";
|
type BoardTab = "groups" | "standalone";
|
||||||
|
type GroupView = "default" | "resource";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
spacesJson: SpacesJson | null;
|
spacesJson: SpacesJson | null;
|
||||||
@ -216,6 +218,7 @@ interface GroupBoardSectionProps {
|
|||||||
|
|
||||||
function GroupBoardSection({ group: g, members, spacesJson }: GroupBoardSectionProps) {
|
function GroupBoardSection({ group: g, members, spacesJson }: GroupBoardSectionProps) {
|
||||||
const [deployTarget, setDeployTarget] = useState<Project | null>(null);
|
const [deployTarget, setDeployTarget] = useState<Project | null>(null);
|
||||||
|
const [groupView, setGroupView] = useState<GroupView>("default");
|
||||||
|
|
||||||
const winMembers = members.filter(({ project: p }) => p.platform !== "wsl");
|
const winMembers = members.filter(({ project: p }) => p.platform !== "wsl");
|
||||||
const wslMembers = members.filter(({ project: p }) => p.platform === "wsl");
|
const wslMembers = members.filter(({ project: p }) => p.platform === "wsl");
|
||||||
@ -231,8 +234,39 @@ function GroupBoardSection({ group: g, members, spacesJson }: GroupBoardSectionP
|
|||||||
<span className="ml-auto text-xs text-gray-400 bg-white border border-gray-200 rounded-full px-2.5 py-0.5">
|
<span className="ml-auto text-xs text-gray-400 bg-white border border-gray-200 rounded-full px-2.5 py-0.5">
|
||||||
{members.length} 个项目
|
{members.length} 个项目
|
||||||
</span>
|
</span>
|
||||||
|
{/* 视图切换 */}
|
||||||
|
<div className="flex items-center rounded-lg border border-gray-200 overflow-hidden text-[11px] font-medium">
|
||||||
|
<button
|
||||||
|
onClick={() => setGroupView("default")}
|
||||||
|
className={`px-2.5 py-1 transition-colors ${
|
||||||
|
groupView === "default"
|
||||||
|
? "bg-gray-700 text-white"
|
||||||
|
: "bg-white text-gray-500 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
默认视图
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setGroupView("resource")}
|
||||||
|
className={`px-2.5 py-1 transition-colors border-l border-gray-200 ${
|
||||||
|
groupView === "resource"
|
||||||
|
? "bg-gray-700 text-white"
|
||||||
|
: "bg-white text-gray-500 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
资源视角
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── 资源视角画布 ── */}
|
||||||
|
{groupView === "resource" && (
|
||||||
|
<GroupResourceCanvas members={members} groupId={g.id} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── 默认视图 ── */}
|
||||||
|
{groupView === "default" && (
|
||||||
|
<>
|
||||||
{/* 列头 */}
|
{/* 列头 */}
|
||||||
<div className="flex border-b border-gray-100 bg-gray-50/60">
|
<div className="flex border-b border-gray-100 bg-gray-50/60">
|
||||||
<div className="flex-1 px-5 py-2 text-xs font-semibold text-gray-400">本地项目</div>
|
<div className="flex-1 px-5 py-2 text-xs font-semibold text-gray-400">本地项目</div>
|
||||||
@ -277,6 +311,8 @@ function GroupBoardSection({ group: g, members, spacesJson }: GroupBoardSectionP
|
|||||||
: <p className="text-xs text-gray-300 py-5 text-center">暂无</p>
|
: <p className="text-xs text-gray-300 py-5 text-center">暂无</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{deployTarget && (
|
{deployTarget && (
|
||||||
<DeployModal
|
<DeployModal
|
||||||
|
|||||||
1096
src/components/dashboard/GroupResourceCanvas.tsx
Normal file
1096
src/components/dashboard/GroupResourceCanvas.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ import { ProjectToolsModal } from "./ProjectToolsModal";
|
|||||||
import { ProjectEnvModal } from "./ProjectEnvModal";
|
import { ProjectEnvModal } from "./ProjectEnvModal";
|
||||||
import { CicdModal } from "./CicdModal";
|
import { CicdModal } from "./CicdModal";
|
||||||
import { PublishModal } from "./PublishModal";
|
import { PublishModal } from "./PublishModal";
|
||||||
|
import { BlueprintModal } from "./BlueprintModal";
|
||||||
import { getOtherSpacesInfo, recordPushToRemote, formatRelativeTime, type SpacesJson } from "../../lib/spacesSync";
|
import { getOtherSpacesInfo, recordPushToRemote, formatRelativeTime, type SpacesJson } from "../../lib/spacesSync";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
|||||||
const [showEnvScan, setShowEnvScan] = useState(false);
|
const [showEnvScan, setShowEnvScan] = useState(false);
|
||||||
const [showCicd, setShowCicd] = useState(false);
|
const [showCicd, setShowCicd] = useState(false);
|
||||||
const [showPublish, setShowPublish] = useState(false);
|
const [showPublish, setShowPublish] = useState(false);
|
||||||
|
const [showBlueprint, setShowBlueprint] = useState(false);
|
||||||
const [showBranchMenu, setShowBranchMenu] = useState(false);
|
const [showBranchMenu, setShowBranchMenu] = useState(false);
|
||||||
const [newBranchName, setNewBranchName] = useState("");
|
const [newBranchName, setNewBranchName] = useState("");
|
||||||
const [creatingBranch, setCreatingBranch] = useState(false);
|
const [creatingBranch, setCreatingBranch] = useState(false);
|
||||||
@ -622,6 +624,10 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
|||||||
<div className="border-t border-gray-100 px-5 py-2.5 flex items-center justify-between">
|
<div className="border-t border-gray-100 px-5 py-2.5 flex items-center justify-between">
|
||||||
<span className="text-[11px] text-gray-300">{(p.updated_at ?? "").slice(0, 10)}</span>
|
<span className="text-[11px] text-gray-300">{(p.updated_at ?? "").slice(0, 10)}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
<button onClick={() => setShowBlueprint(true)}
|
||||||
|
className="px-2.5 py-1.5 rounded-md text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors" title="项目蓝图">
|
||||||
|
🗺️
|
||||||
|
</button>
|
||||||
<button onClick={() => setShowCicd(true)}
|
<button onClick={() => setShowCicd(true)}
|
||||||
className="px-2.5 py-1.5 rounded-md text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors" title="CI/CD 控制台">
|
className="px-2.5 py-1.5 rounded-md text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors" title="CI/CD 控制台">
|
||||||
🚀
|
🚀
|
||||||
@ -687,6 +693,14 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showBlueprint && (p.win_path || p.wsl_path) && (
|
||||||
|
<BlueprintModal
|
||||||
|
projectName={p.name}
|
||||||
|
projectPath={p.win_path ?? p.wsl_path ?? ""}
|
||||||
|
onClose={() => setShowBlueprint(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Expanded activity */}
|
{/* Expanded activity */}
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="border-t border-gray-50 pt-3">
|
<div className="border-t border-gray-50 pt-3">
|
||||||
|
|||||||
@ -849,3 +849,74 @@ export function buildShareLink(node: V2raynNode): string {
|
|||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Blueprint ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface BlueprintTask {
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
complexity?: string;
|
||||||
|
files?: string;
|
||||||
|
depends?: string;
|
||||||
|
acceptance?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueprintModule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
area: string;
|
||||||
|
status: string;
|
||||||
|
progress: number;
|
||||||
|
position: number[];
|
||||||
|
description?: string;
|
||||||
|
decisions: string[];
|
||||||
|
tasks: BlueprintTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueprintEdge {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
edge_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueprintArea {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueprintStats {
|
||||||
|
total_modules: number;
|
||||||
|
done: number;
|
||||||
|
in_progress: number;
|
||||||
|
planned: number;
|
||||||
|
concept: number;
|
||||||
|
total_tasks: number;
|
||||||
|
tasks_done: number;
|
||||||
|
dispatchable: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueprintData {
|
||||||
|
manifest: {
|
||||||
|
version: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
updated?: string;
|
||||||
|
areas: BlueprintArea[];
|
||||||
|
modules: BlueprintModule[];
|
||||||
|
edges: BlueprintEdge[];
|
||||||
|
};
|
||||||
|
stats: BlueprintStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBlueprint = (projectPath: string) =>
|
||||||
|
invoke<BlueprintData | null>("get_blueprint", { projectPath });
|
||||||
|
|
||||||
|
|
||||||
|
// ── Canvas State ───────────────────────────────────────────────────────────────
|
||||||
|
export const getCanvasState = (groupId: string) =>
|
||||||
|
invoke<string | null>("get_canvas_state", { groupId });
|
||||||
|
|
||||||
|
export const saveCanvasState = (groupId: string, data: string) =>
|
||||||
|
invoke<void>("save_canvas_state", { groupId, data });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user