feat: 仪表盘全局快启栏
新增 global_tools 表及 CRUD 命令,Dashboard 顶部增加独立的「快启」工具栏, 可配置与项目无关的日常应用。ProjectToolsModal 重构为通用组件,支持注入 自定义 commands / queryKey;ProjectCard 快启行始终显示并内联添加按钮。
This commit is contained in:
parent
6c24d34bb4
commit
fea43b0b8a
@ -44,6 +44,29 @@
|
|||||||
"todo": 1,
|
"todo": 1,
|
||||||
"total": 116
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -109,6 +109,84 @@ pub fn remove_project_tool(id: String) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 全局快启命令(写入 global_tools 表,无项目 FK 约束)────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_global_tools() -> Result<Vec<ProjectTool>, 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::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn add_global_tool(input: ProjectToolInput) -> Result<String, String> {
|
||||||
|
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 路径 + 可选参数)
|
/// 启动工具(通用:exe 路径 + 可选参数)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn launch_tool(exe_path: String, args: Option<String>) -> Result<(), String> {
|
pub fn launch_tool(exe_path: String, args: Option<String>) -> Result<(), String> {
|
||||||
|
|||||||
@ -424,6 +424,18 @@ fn migrate(conn: &rusqlite::Connection) {
|
|||||||
status TEXT NOT NULL DEFAULT 'active'
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
);
|
);
|
||||||
").expect("create blueprint_buffs table");
|
").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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -138,6 +138,11 @@ pub fn run() {
|
|||||||
update_project_tool,
|
update_project_tool,
|
||||||
remove_project_tool,
|
remove_project_tool,
|
||||||
launch_tool,
|
launch_tool,
|
||||||
|
// global quick launch
|
||||||
|
get_global_tools,
|
||||||
|
add_global_tool,
|
||||||
|
update_global_tool,
|
||||||
|
remove_global_tool,
|
||||||
// devtools
|
// devtools
|
||||||
scan_dev_tools,
|
scan_dev_tools,
|
||||||
scan_wsl_dev_tools,
|
scan_wsl_dev_tools,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useQueryClient } 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 { ProjectCard } from "./ProjectCard";
|
||||||
import { DeployModal } from "./DeployModal";
|
import { DeployModal } from "./DeployModal";
|
||||||
import { GroupResourceCanvas } from "./GroupResourceCanvas";
|
import { GroupResourceCanvas } from "./GroupResourceCanvas";
|
||||||
@ -9,9 +9,11 @@ import { ManagePage } from "../manage/ManagePage";
|
|||||||
import { BatchAddModal } from "./BatchAddModal";
|
import { BatchAddModal } from "./BatchAddModal";
|
||||||
import { MountRepoModal } from "./MountRepoModal";
|
import { MountRepoModal } from "./MountRepoModal";
|
||||||
import { DiscoveryPanel } from "./DiscoveryPanel";
|
import { DiscoveryPanel } from "./DiscoveryPanel";
|
||||||
|
import { ProjectToolsModal } from "./ProjectToolsModal";
|
||||||
import { StatusBadge } from "../ui/Badge";
|
import { StatusBadge } from "../ui/Badge";
|
||||||
import { getDiscoverableProjects, type SpacesJson } from "../../lib/spacesSync";
|
import { getDiscoverableProjects, type SpacesJson } from "../../lib/spacesSync";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
|
import { useUIStore } from "../../store/ui";
|
||||||
|
|
||||||
type DashView = "board" | "manage";
|
type DashView = "board" | "manage";
|
||||||
type BoardTab = "groups" | "standalone";
|
type BoardTab = "groups" | "standalone";
|
||||||
@ -141,6 +143,8 @@ export function Dashboard({ spacesJson }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<GlobalQuickLaunchBar />
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
<div className="w-full px-8 py-8 space-y-8 pb-16">
|
<div className="w-full px-8 py-8 space-y-8 pb-16">
|
||||||
|
|
||||||
@ -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 (
|
||||||
|
<div className="shrink-0 border-b border-gray-100 bg-white px-8 py-2 flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-[11px] font-semibold text-orange-600 bg-orange-50 border border-orange-200 rounded px-1.5 py-0.5 shrink-0">
|
||||||
|
快启
|
||||||
|
</span>
|
||||||
|
{tools.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<button
|
||||||
|
key={tool.id}
|
||||||
|
onClick={() => handleLaunch(tool.exe_path, tool.args, tool.name)}
|
||||||
|
title={tool.exe_path}
|
||||||
|
className="px-2.5 py-1 rounded-md text-xs border border-gray-200 text-gray-600 hover:bg-orange-50 hover:border-orange-300 hover:text-orange-800 transition-colors"
|
||||||
|
>
|
||||||
|
{tool.icon} {tool.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => { setMgrDefaultAdd(true); setShowMgr(true); }}
|
||||||
|
className="px-2 py-1 rounded-md text-xs border border-dashed border-orange-200 text-orange-400 hover:border-orange-400 hover:text-orange-600 hover:bg-orange-50 transition-colors"
|
||||||
|
title="添加快启应用"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => { setMgrDefaultAdd(true); setShowMgr(true); }}
|
||||||
|
className="px-2.5 py-1 rounded-md text-xs border border-dashed border-orange-200 text-orange-400 hover:border-orange-400 hover:text-orange-600 hover:bg-orange-50 transition-colors"
|
||||||
|
>
|
||||||
|
+ 配置日常应用
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => { setMgrDefaultAdd(false); setShowMgr(true); }}
|
||||||
|
className="ml-auto text-gray-300 hover:text-gray-500 transition-colors"
|
||||||
|
title="管理快启应用"
|
||||||
|
>
|
||||||
|
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{showMgr && (
|
||||||
|
<ProjectToolsModal
|
||||||
|
projectId="__global__"
|
||||||
|
projectName="日常应用"
|
||||||
|
defaultOpenAdd={mgrDefaultAdd}
|
||||||
|
commands={globalToolCmds}
|
||||||
|
queryKey={GLOBAL_QUERY_KEY}
|
||||||
|
onClose={() => { setShowMgr(false); setMgrDefaultAdd(false); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── StandaloneBoardSection ────────────────────────────────────────────────── */
|
/* ── StandaloneBoardSection ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
function StandaloneBoardSection({
|
function StandaloneBoardSection({
|
||||||
|
|||||||
@ -60,6 +60,7 @@ interface Props {
|
|||||||
export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [showToolsMgr, setShowToolsMgr] = useState(false);
|
const [showToolsMgr, setShowToolsMgr] = useState(false);
|
||||||
|
const [toolsMgrDefaultAdd, setToolsMgrDefaultAdd] = useState(false);
|
||||||
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);
|
||||||
@ -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) && (
|
||||||
<div className="border-t border-gray-100 px-5 py-3 flex flex-col gap-2">
|
<div className="border-t border-gray-100 px-5 py-3 flex flex-col gap-2">
|
||||||
{p.wsl_path && (
|
{p.wsl_path && (
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
@ -687,9 +688,10 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{projectTools.length > 0 && (
|
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
<span className="text-[11px] font-semibold text-purple-700 bg-purple-50 border border-purple-200 rounded px-1.5 py-0.5 shrink-0">工具</span>
|
<span className="text-[11px] font-semibold text-purple-700 bg-purple-50 border border-purple-200 rounded px-1.5 py-0.5 shrink-0">快启</span>
|
||||||
|
{projectTools.length > 0 ? (
|
||||||
|
<>
|
||||||
{projectTools.map((tool) => (
|
{projectTools.map((tool) => (
|
||||||
<button key={tool.id} onClick={() => handleLaunchTool(tool.exe_path, tool.args, tool.name)}
|
<button key={tool.id} onClick={() => handleLaunchTool(tool.exe_path, tool.args, tool.name)}
|
||||||
className="px-2.5 py-1 rounded-md text-xs border border-gray-200 text-gray-600 hover:bg-purple-50 hover:border-purple-300 hover:text-purple-800 transition-colors"
|
className="px-2.5 py-1 rounded-md text-xs border border-gray-200 text-gray-600 hover:bg-purple-50 hover:border-purple-300 hover:text-purple-800 transition-colors"
|
||||||
@ -697,9 +699,22 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
|||||||
{tool.icon} {tool.name}
|
{tool.icon} {tool.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
<button
|
||||||
|
onClick={() => { setToolsMgrDefaultAdd(true); setShowToolsMgr(true); }}
|
||||||
|
className="px-2 py-1 rounded-md text-xs border border-dashed border-purple-200 text-purple-400 hover:border-purple-400 hover:text-purple-600 hover:bg-purple-50 transition-colors"
|
||||||
|
title="添加快启应用">
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => { setToolsMgrDefaultAdd(true); setShowToolsMgr(true); }}
|
||||||
|
className="px-2.5 py-1 rounded-md text-xs border border-dashed border-purple-200 text-purple-400 hover:border-purple-400 hover:text-purple-600 hover:bg-purple-50 transition-colors">
|
||||||
|
+ 配置快启应用
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── 底部操作栏 ── */}
|
{/* ── 底部操作栏 ── */}
|
||||||
@ -752,7 +767,8 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
|||||||
<ProjectToolsModal
|
<ProjectToolsModal
|
||||||
projectId={p.id}
|
projectId={p.id}
|
||||||
projectName={p.name}
|
projectName={p.name}
|
||||||
onClose={() => setShowToolsMgr(false)}
|
defaultOpenAdd={toolsMgrDefaultAdd}
|
||||||
|
onClose={() => { setShowToolsMgr(false); setToolsMgrDefaultAdd(false); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,13 @@ import {
|
|||||||
type ProjectTool,
|
type ProjectTool,
|
||||||
type ProjectToolInput,
|
type ProjectToolInput,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
|
||||||
|
export interface ToolCommands {
|
||||||
|
getTools: () => Promise<ProjectTool[]>;
|
||||||
|
addTool: (input: ProjectToolInput) => Promise<string>;
|
||||||
|
updateTool: (id: string, input: ProjectToolInput) => Promise<void>;
|
||||||
|
removeTool: (id: string) => Promise<void>;
|
||||||
|
}
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
import { Dialog } from "../ui/Dialog";
|
import { Dialog } from "../ui/Dialog";
|
||||||
|
|
||||||
@ -20,20 +27,32 @@ interface Props {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
onClose: () => void;
|
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 showToast = useUIStore((s) => s.showToast);
|
||||||
const qc = useQueryClient();
|
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({
|
const { data: tools = [], refetch } = useQuery({
|
||||||
queryKey: ["projectTools", projectId],
|
queryKey: resolvedKey,
|
||||||
queryFn: () => getProjectTools(projectId),
|
queryFn: () => cmds.getTools(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [editing, setEditing] = useState<ProjectTool | null>(null); // null = 新增模式
|
const [editing, setEditing] = useState<ProjectTool | null>(null); // null = 新增模式
|
||||||
const [form, setForm] = useState<ProjectToolInput>(EMPTY);
|
const [form, setForm] = useState<ProjectToolInput>(EMPTY);
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(defaultOpenAdd ?? false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const openAdd = () => {
|
const openAdd = () => {
|
||||||
@ -80,12 +99,12 @@ export function ProjectToolsModal({ projectId, projectName, onClose }: Props) {
|
|||||||
sort_order: editing?.sort_order ?? tools.length,
|
sort_order: editing?.sort_order ?? tools.length,
|
||||||
};
|
};
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await updateProjectTool(editing.id, input);
|
await cmds.updateTool(editing.id, input);
|
||||||
} else {
|
} else {
|
||||||
await addProjectTool(projectId, input);
|
await cmds.addTool(input);
|
||||||
}
|
}
|
||||||
await refetch();
|
await refetch();
|
||||||
qc.invalidateQueries({ queryKey: ["projectTools", projectId] });
|
qc.invalidateQueries({ queryKey: resolvedKey });
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
showToast(editing ? "已更新 ✓" : "已添加 ✓");
|
showToast(editing ? "已更新 ✓" : "已添加 ✓");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -97,9 +116,9 @@ export function ProjectToolsModal({ projectId, projectName, onClose }: Props) {
|
|||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await removeProjectTool(id);
|
await cmds.removeTool(id);
|
||||||
await refetch();
|
await refetch();
|
||||||
qc.invalidateQueries({ queryKey: ["projectTools", projectId] });
|
qc.invalidateQueries({ queryKey: resolvedKey });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(`❌ ${e}`);
|
showToast(`❌ ${e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -222,6 +222,20 @@ export const removeProjectTool = (id: string) =>
|
|||||||
export const launchTool = (exePath: string, args?: string) =>
|
export const launchTool = (exePath: string, args?: string) =>
|
||||||
invoke<void>("launch_tool", { exePath, args });
|
invoke<void>("launch_tool", { exePath, args });
|
||||||
|
|
||||||
|
// ── Global Quick Launch ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const getGlobalTools = () =>
|
||||||
|
invoke<ProjectTool[]>("get_global_tools");
|
||||||
|
|
||||||
|
export const addGlobalTool = (input: ProjectToolInput) =>
|
||||||
|
invoke<string>("add_global_tool", { input });
|
||||||
|
|
||||||
|
export const updateGlobalTool = (id: string, input: ProjectToolInput) =>
|
||||||
|
invoke<void>("update_global_tool", { id, input });
|
||||||
|
|
||||||
|
export const removeGlobalTool = (id: string) =>
|
||||||
|
invoke<void>("remove_global_tool", { id });
|
||||||
|
|
||||||
// ── Dev Tools ─────────────────────────────────────────────────────────────────
|
// ── Dev Tools ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface ScannedTool {
|
export interface ScannedTool {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user