diff --git a/.blueprint/usage.json b/.blueprint/usage.json index 678ba18..845f14c 100644 --- a/.blueprint/usage.json +++ b/.blueprint/usage.json @@ -44,6 +44,29 @@ "todo": 1, "total": 116 } + }, + { + "at": "2026-04-06T12:10:41Z", + "blocked_reasons": [], + "conventions_version": "1.4.0", + "iteration": 1, + "modules": { + "blocked": 0, + "concept": 0, + "done": 33, + "in_progress": 0, + "planned": 0, + "stalled": [], + "total": 33 + }, + "tasks": { + "blocked": 0, + "dispatched": 0, + "done": 130, + "in_progress": 0, + "todo": 1, + "total": 135 + } } ] -} +} \ No newline at end of file diff --git a/src-tauri/src/commands/project_tools.rs b/src-tauri/src/commands/project_tools.rs index 7885cdf..016f699 100644 --- a/src-tauri/src/commands/project_tools.rs +++ b/src-tauri/src/commands/project_tools.rs @@ -109,6 +109,84 @@ pub fn remove_project_tool(id: String) -> Result<(), String> { Ok(()) } +// ── 全局快启命令(写入 global_tools 表,无项目 FK 约束)──────────────────────── + +#[tauri::command] +pub fn get_global_tools() -> Result, String> { + let conn = pool().get().map_err(|e| e.to_string())?; + let mut stmt = conn + .prepare( + "SELECT id, name, icon, exe_path, args, sort_order + FROM global_tools + ORDER BY sort_order ASC, rowid ASC", + ) + .map_err(|e| e.to_string())?; + let rows = stmt + .query_map([], |row| { + Ok(ProjectTool { + id: row.get(0)?, + project_id: "__global__".to_string(), + name: row.get(1)?, + icon: row.get(2)?, + exe_path: row.get(3)?, + args: row.get(4)?, + sort_order: row.get(5)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string()); + rows +} + +#[tauri::command] +pub fn add_global_tool(input: ProjectToolInput) -> Result { + let conn = pool().get().map_err(|e| e.to_string())?; + let id = uuid::Uuid::new_v4().to_string(); + conn.execute( + "INSERT INTO global_tools (id, name, icon, exe_path, args, sort_order) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![ + id, + input.name, + input.icon, + input.exe_path, + input.args, + input.sort_order.unwrap_or(0), + ], + ) + .map_err(|e| e.to_string())?; + Ok(id) +} + +#[tauri::command] +pub fn update_global_tool(id: String, input: ProjectToolInput) -> Result<(), String> { + let conn = pool().get().map_err(|e| e.to_string())?; + conn.execute( + "UPDATE global_tools + SET name = ?1, icon = ?2, exe_path = ?3, args = ?4, sort_order = ?5 + WHERE id = ?6", + params![ + input.name, + input.icon, + input.exe_path, + input.args, + input.sort_order.unwrap_or(0), + id, + ], + ) + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub fn remove_global_tool(id: String) -> Result<(), String> { + let conn = pool().get().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM global_tools WHERE id = ?1", params![id]) + .map_err(|e| e.to_string())?; + Ok(()) +} + /// 启动工具(通用:exe 路径 + 可选参数) #[tauri::command] pub fn launch_tool(exe_path: String, args: Option) -> Result<(), String> { diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index f2404c1..20964f0 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -424,6 +424,18 @@ fn migrate(conn: &rusqlite::Connection) { status TEXT NOT NULL DEFAULT 'active' ); ").expect("create blueprint_buffs table"); + + // 仪表盘全局快启应用(无项目 FK 约束) + conn.execute_batch(" + CREATE TABLE IF NOT EXISTS global_tools ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + icon TEXT NOT NULL DEFAULT '🔧', + exe_path TEXT NOT NULL, + args TEXT, + sort_order INTEGER DEFAULT 0 + ); + ").expect("create global_tools table"); } #[cfg(test)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3a0451a..3788675 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -138,6 +138,11 @@ pub fn run() { update_project_tool, remove_project_tool, launch_tool, + // global quick launch + get_global_tools, + add_global_tool, + update_global_tool, + remove_global_tool, // devtools scan_dev_tools, scan_wsl_dev_tools, diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index ce05202..60b5745 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; 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, getGlobalTools, addGlobalTool, updateGlobalTool, removeGlobalTool, launchTool, type ProductGroup, type Project, type ProjectToolInput } from "../../lib/commands"; import { ProjectCard } from "./ProjectCard"; import { DeployModal } from "./DeployModal"; import { GroupResourceCanvas } from "./GroupResourceCanvas"; @@ -9,9 +9,11 @@ import { ManagePage } from "../manage/ManagePage"; import { BatchAddModal } from "./BatchAddModal"; import { MountRepoModal } from "./MountRepoModal"; import { DiscoveryPanel } from "./DiscoveryPanel"; +import { ProjectToolsModal } from "./ProjectToolsModal"; import { StatusBadge } from "../ui/Badge"; import { getDiscoverableProjects, type SpacesJson } from "../../lib/spacesSync"; import { openUrl } from "@tauri-apps/plugin-opener"; +import { useUIStore } from "../../store/ui"; type DashView = "board" | "manage"; type BoardTab = "groups" | "standalone"; @@ -141,6 +143,8 @@ export function Dashboard({ spacesJson }: Props) { + +
@@ -412,6 +416,93 @@ function ProjectRow({ ); } +/* ── GlobalQuickLaunchBar ──────────────────────────────────────────────────── */ + +const GLOBAL_QUERY_KEY = ["globalTools"]; + +const globalToolCmds = { + getTools: () => getGlobalTools(), + addTool: (input: ProjectToolInput) => addGlobalTool(input), + updateTool: (id: string, input: ProjectToolInput) => updateGlobalTool(id, input), + removeTool: (id: string) => removeGlobalTool(id), +}; + +function GlobalQuickLaunchBar() { + const showToast = useUIStore((s) => s.showToast); + const [showMgr, setShowMgr] = useState(false); + const [mgrDefaultAdd, setMgrDefaultAdd] = useState(false); + + const { data: tools = [] } = useQuery({ + queryKey: GLOBAL_QUERY_KEY, + queryFn: () => getGlobalTools(), + staleTime: 30000, + }); + + const handleLaunch = async (exePath: string, args: string | null | undefined, name: string) => { + try { + await launchTool(exePath, args ?? undefined); + showToast(`已启动 ${name} ✓`); + } catch (e) { + showToast(`❌ ${e}`); + } + }; + + return ( +
+ + 快启 + + {tools.length > 0 ? ( + <> + {tools.map((tool) => ( + + ))} + + + ) : ( + + )} + + {showMgr && ( + { setShowMgr(false); setMgrDefaultAdd(false); }} + /> + )} +
+ ); +} + /* ── StandaloneBoardSection ────────────────────────────────────────────────── */ function StandaloneBoardSection({ diff --git a/src/components/dashboard/ProjectCard.tsx b/src/components/dashboard/ProjectCard.tsx index dd7b75a..aa917c8 100644 --- a/src/components/dashboard/ProjectCard.tsx +++ b/src/components/dashboard/ProjectCard.tsx @@ -60,6 +60,7 @@ interface Props { export function ProjectCard({ project: p, role, spacesJson }: Props) { const [expanded, setExpanded] = useState(false); const [showToolsMgr, setShowToolsMgr] = useState(false); + const [toolsMgrDefaultAdd, setToolsMgrDefaultAdd] = useState(false); const [showEnvScan, setShowEnvScan] = useState(false); const [showCicd, setShowCicd] = useState(false); const [showPublish, setShowPublish] = useState(false); @@ -655,7 +656,7 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) { )} {/* ── 快捷操作区 ── */} - {(p.wsl_path || p.win_path || projectTools.length > 0) && ( + {(p.wsl_path || p.win_path) && (
{p.wsl_path && (
@@ -687,18 +688,32 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
)} - {projectTools.length > 0 && ( -
- 工具 - {projectTools.map((tool) => ( - + ))} + - ))} -
- )} + + ) : ( + + )} +
)} @@ -752,7 +767,8 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) { setShowToolsMgr(false)} + defaultOpenAdd={toolsMgrDefaultAdd} + onClose={() => { setShowToolsMgr(false); setToolsMgrDefaultAdd(false); }} /> )} diff --git a/src/components/dashboard/ProjectToolsModal.tsx b/src/components/dashboard/ProjectToolsModal.tsx index 83fdae6..61898e5 100644 --- a/src/components/dashboard/ProjectToolsModal.tsx +++ b/src/components/dashboard/ProjectToolsModal.tsx @@ -9,6 +9,13 @@ import { type ProjectTool, type ProjectToolInput, } from "../../lib/commands"; + +export interface ToolCommands { + getTools: () => Promise; + addTool: (input: ProjectToolInput) => Promise; + updateTool: (id: string, input: ProjectToolInput) => Promise; + removeTool: (id: string) => Promise; +} import { useUIStore } from "../../store/ui"; import { Dialog } from "../ui/Dialog"; @@ -20,20 +27,32 @@ interface Props { projectId: string; projectName: string; onClose: () => void; + defaultOpenAdd?: boolean; + commands?: ToolCommands; + queryKey?: unknown[]; } -export function ProjectToolsModal({ projectId, projectName, onClose }: Props) { +export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpenAdd, commands, queryKey }: Props) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); + const cmds: ToolCommands = commands ?? { + getTools: () => getProjectTools(projectId), + addTool: (input) => addProjectTool(projectId, input), + updateTool: (id, input) => updateProjectTool(id, input), + removeTool: (id) => removeProjectTool(id), + }; + + const resolvedKey = queryKey ?? ["projectTools", projectId]; + const { data: tools = [], refetch } = useQuery({ - queryKey: ["projectTools", projectId], - queryFn: () => getProjectTools(projectId), + queryKey: resolvedKey, + queryFn: () => cmds.getTools(), }); const [editing, setEditing] = useState(null); // null = 新增模式 const [form, setForm] = useState(EMPTY); - const [showForm, setShowForm] = useState(false); + const [showForm, setShowForm] = useState(defaultOpenAdd ?? false); const [saving, setSaving] = useState(false); const openAdd = () => { @@ -80,12 +99,12 @@ export function ProjectToolsModal({ projectId, projectName, onClose }: Props) { sort_order: editing?.sort_order ?? tools.length, }; if (editing) { - await updateProjectTool(editing.id, input); + await cmds.updateTool(editing.id, input); } else { - await addProjectTool(projectId, input); + await cmds.addTool(input); } await refetch(); - qc.invalidateQueries({ queryKey: ["projectTools", projectId] }); + qc.invalidateQueries({ queryKey: resolvedKey }); setShowForm(false); showToast(editing ? "已更新 ✓" : "已添加 ✓"); } catch (e) { @@ -97,9 +116,9 @@ export function ProjectToolsModal({ projectId, projectName, onClose }: Props) { const handleDelete = async (id: string) => { try { - await removeProjectTool(id); + await cmds.removeTool(id); await refetch(); - qc.invalidateQueries({ queryKey: ["projectTools", projectId] }); + qc.invalidateQueries({ queryKey: resolvedKey }); } catch (e) { showToast(`❌ ${e}`); } diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 3af44b9..a967f05 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -222,6 +222,20 @@ export const removeProjectTool = (id: string) => export const launchTool = (exePath: string, args?: string) => invoke("launch_tool", { exePath, args }); +// ── Global Quick Launch ─────────────────────────────────────────────────────── + +export const getGlobalTools = () => + invoke("get_global_tools"); + +export const addGlobalTool = (input: ProjectToolInput) => + invoke("add_global_tool", { input }); + +export const updateGlobalTool = (id: string, input: ProjectToolInput) => + invoke("update_global_tool", { id, input }); + +export const removeGlobalTool = (id: string) => + invoke("remove_global_tool", { id }); + // ── Dev Tools ───────────────────────────────────────────────────────────────── export interface ScannedTool {