feat: 仪表盘全局快启栏
新增 global_tools 表及 CRUD 命令,Dashboard 顶部增加独立的「快启」工具栏, 可配置与项目无关的日常应用。ProjectToolsModal 重构为通用组件,支持注入 自定义 commands / queryKey;ProjectCard 快启行始终显示并内联添加按钮。
This commit is contained in:
parent
6c24d34bb4
commit
fea43b0b8a
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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<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 路径 + 可选参数)
|
||||
#[tauri::command]
|
||||
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'
|
||||
);
|
||||
").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)]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<GlobalQuickLaunchBar />
|
||||
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
<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 ────────────────────────────────────────────────── */
|
||||
|
||||
function StandaloneBoardSection({
|
||||
|
||||
@ -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) && (
|
||||
<div className="border-t border-gray-100 px-5 py-3 flex flex-col gap-2">
|
||||
{p.wsl_path && (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
@ -687,9 +688,10 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{projectTools.length > 0 && (
|
||||
<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) => (
|
||||
<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"
|
||||
@ -697,9 +699,22 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
||||
{tool.icon} {tool.name}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* ── 底部操作栏 ── */}
|
||||
@ -752,7 +767,8 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
|
||||
<ProjectToolsModal
|
||||
projectId={p.id}
|
||||
projectName={p.name}
|
||||
onClose={() => setShowToolsMgr(false)}
|
||||
defaultOpenAdd={toolsMgrDefaultAdd}
|
||||
onClose={() => { setShowToolsMgr(false); setToolsMgrDefaultAdd(false); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -9,6 +9,13 @@ import {
|
||||
type ProjectTool,
|
||||
type ProjectToolInput,
|
||||
} 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 { 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<ProjectTool | null>(null); // null = 新增模式
|
||||
const [form, setForm] = useState<ProjectToolInput>(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}`);
|
||||
}
|
||||
|
||||
@ -222,6 +222,20 @@ export const removeProjectTool = (id: string) =>
|
||||
export const launchTool = (exePath: string, args?: string) =>
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ScannedTool {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user