dev-manager-tauri/src/components/dashboard/GlobalToolsModal.tsx
lanrtop 412ab8491f
All checks were successful
Push & PR Check / check (push) Successful in 1m23s
Push & PR Check / check (pull_request) Successful in 58s
feat(feature-pruning): 撤销零数据/死代码功能(留撤盘点批次 1)
按「管家准入与退出」撤销实测无使用的功能,台账见 docs/tasks/2026-10-04-feature-pruning.md:
- 项目级工具(project_tools 0 行):删除项目级 command;保留全局快启,
  project_tools.rs/ProjectToolsModal 改名 global_tools.rs/GlobalToolsModal
- 部署命令(deploy_commands 0 行):删除 4 个 command、DeployModal 与「部署配置」按钮;
  删除未编译的 deploy_info.rs 与空壳 DeployInfoPanel
- 宿主机应用(0 行)、项目个人备忘(1 条,表与数据保留)、活动日志与 relations(0 行)
- GitHub 残留:git_accounts 建表与 github_client_* 设置种子;publish.rs 改名 local_git.rs
- 空壳 NetworkPage

数据原则:不 DROP 表、不删已有行,只移除建表语句、两段 FK 迁移与读写代码。
核查后撤回 server_network_config:镜像选择功能在用(ServerSoftwareBlock 调用)。

Rules-Applied: R01,R05
Feature-Confirmed: true

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:34:32 +09:00

268 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { open } from "@tauri-apps/plugin-dialog";
import {
getGlobalTools,
addGlobalTool,
updateGlobalTool,
removeGlobalTool,
type GlobalTool,
type GlobalToolInput,
} from "../../lib/commands";
import { useUIStore } from "../../store/ui";
import { Dialog } from "../ui/Dialog";
export const GLOBAL_TOOLS_QUERY_KEY = ["globalTools"];
const COMMON_ICONS = ["🔧", "🎬", "🎨", "📷", "🗄️", "📮", "🌐", "📊", "🎵", "📐", "🔬", "⚙️", "📁", "🖨️", "🎮"];
const EMPTY: GlobalToolInput = { name: "", icon: "🔧", exe_path: "", args: "" };
interface Props {
onClose: () => void;
defaultOpenAdd?: boolean;
}
export function GlobalToolsModal({ onClose, defaultOpenAdd }: Props) {
const showToast = useUIStore((s) => s.showToast);
const qc = useQueryClient();
const { data: tools = [], refetch } = useQuery({
queryKey: GLOBAL_TOOLS_QUERY_KEY,
queryFn: () => getGlobalTools(),
});
const [editing, setEditing] = useState<GlobalTool | null>(null); // null = 新增模式
const [form, setForm] = useState<GlobalToolInput>(EMPTY);
const [showForm, setShowForm] = useState(defaultOpenAdd ?? false);
const [saving, setSaving] = useState(false);
const openAdd = () => {
setEditing(null);
setForm(EMPTY);
setShowForm(true);
};
const openEdit = (tool: GlobalTool) => {
setEditing(tool);
setForm({ name: tool.name, icon: tool.icon, exe_path: tool.exe_path, args: tool.args ?? "" });
setShowForm(true);
};
const set = (k: keyof GlobalToolInput, v: string) =>
setForm((f) => ({ ...f, [k]: v }));
const pickExe = async () => {
const selected = await open({
directory: false,
multiple: false,
filters: [{ name: "可执行文件", extensions: ["exe", "cmd", "bat", "lnk"] }],
});
if (!selected) return;
const path = selected as string;
set("exe_path", path);
// 如果名称还是空的,从文件名推断
if (!form.name.trim()) {
const parts = path.replace(/\\/g, "/").split("/");
const filename = parts[parts.length - 1];
const name = filename.replace(/\.(exe|cmd|bat|lnk)$/i, "");
set("name", name);
}
};
const handleSave = async () => {
if (!form.name.trim()) { showToast("❌ 工具名称不能为空"); return; }
if (!form.exe_path.trim()) { showToast("❌ 程序路径不能为空"); return; }
setSaving(true);
try {
const input: GlobalToolInput = {
...form,
args: form.args?.trim() || undefined,
sort_order: editing?.sort_order ?? tools.length,
};
if (editing) {
await updateGlobalTool(editing.id, input);
} else {
await addGlobalTool(input);
}
await refetch();
qc.invalidateQueries({ queryKey: GLOBAL_TOOLS_QUERY_KEY });
setShowForm(false);
showToast(editing ? "已更新 ✓" : "已添加 ✓");
} catch (e) {
showToast(`❌ ${e}`);
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
try {
await removeGlobalTool(id);
await refetch();
qc.invalidateQueries({ queryKey: GLOBAL_TOOLS_QUERY_KEY });
} catch (e) {
showToast(`❌ ${e}`);
}
};
const inputCls = "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200";
return (
<Dialog title="工具管理 · 日常应用" onClose={onClose} size="lg">
<div className="px-6 py-4">
{/* 工具列表 */}
{tools.length > 0 ? (
<div className="space-y-2 mb-4">
{tools.map((tool) => (
<div
key={tool.id}
className="flex items-center gap-3 px-3 py-2.5 bg-gray-50 rounded-lg border border-gray-100 group"
>
<span className="text-lg w-7 text-center shrink-0">{tool.icon}</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800">{tool.name}</p>
<p className="text-xs text-gray-400 font-mono truncate">{tool.exe_path}</p>
{tool.args && (
<p className="text-xs text-blue-400 font-mono">{tool.args}</p>
)}
</div>
<div className="flex gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button
onClick={() => openEdit(tool)}
className="px-2 py-1 text-xs border border-gray-200 rounded text-gray-500 hover:bg-white"
>
编辑
</button>
<button
onClick={() => handleDelete(tool.id)}
className="px-2 py-1 text-xs border border-red-100 rounded text-red-400 hover:bg-red-50"
>
删除
</button>
</div>
</div>
))}
</div>
) : !showForm ? (
<div className="text-center py-8 text-gray-400 mb-4">
<p className="text-3xl mb-2">🧰</p>
<p className="text-sm">还没有配置工具</p>
<p className="text-xs mt-1">添加常用软件,一键从项目卡片启动</p>
</div>
) : null}
{/* 添加/编辑表单 */}
{showForm ? (
<div className="border border-blue-100 rounded-xl bg-blue-50/40 p-4 space-y-3">
<p className="text-xs font-semibold text-blue-600 uppercase tracking-wide">
{editing ? "编辑工具" : "添加工具"}
</p>
{/* 图标选择 */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-1.5">图标</label>
<div className="flex gap-1.5 flex-wrap">
{COMMON_ICONS.map((ic) => (
<button
key={ic}
onClick={() => set("icon", ic)}
className={`w-8 h-8 rounded-lg text-base flex items-center justify-center border transition-colors ${
form.icon === ic
? "border-blue-400 bg-blue-50"
: "border-gray-200 bg-white hover:border-gray-300"
}`}
>
{ic}
</button>
))}
<input
className="w-8 h-8 rounded-lg border border-gray-200 text-center text-base bg-white outline-none focus:border-blue-400"
value={COMMON_ICONS.includes(form.icon) ? "" : form.icon}
onChange={(e) => set("icon", e.target.value)}
placeholder="…"
maxLength={2}
title="自定义 emoji"
/>
</div>
</div>
{/* 名称 + 路径 */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">名称 *</label>
<input
className={inputCls}
value={form.name}
onChange={(e) => set("name", e.target.value)}
placeholder="RealityCapture"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">启动参数(可选)</label>
<input
className={`${inputCls} font-mono`}
value={form.args ?? ""}
onChange={(e) => set("args", e.target.value)}
placeholder="-load project.rcproj"
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">程序路径 *</label>
<div className="flex gap-2">
<input
className={`${inputCls} font-mono flex-1`}
value={form.exe_path}
onChange={(e) => set("exe_path", e.target.value)}
placeholder="C:\Program Files\...\app.exe"
/>
<button
onClick={pickExe}
className="px-3 py-2 rounded-lg border border-gray-200 text-sm text-gray-600 hover:bg-gray-50 shrink-0"
>
浏览
</button>
</div>
</div>
<div className="flex justify-end gap-2 pt-1">
<button
onClick={() => setShowForm(false)}
className="px-3 py-1.5 text-sm border border-gray-200 rounded-lg text-gray-600 hover:bg-gray-50"
>
取消
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{saving ? "保存中…" : "保存"}
</button>
</div>
</div>
) : (
<button
onClick={openAdd}
className="w-full py-2.5 rounded-lg border border-dashed border-gray-300 text-sm text-gray-500 hover:border-blue-400 hover:text-blue-600 hover:bg-blue-50/40 transition-colors"
>
+ 添加工具
</button>
)}
</div>
<div className="flex justify-end px-6 py-4 border-t border-gray-100">
<button
onClick={onClose}
className="px-4 py-2 rounded-lg text-sm border border-gray-200 text-gray-600 hover:bg-gray-50"
>
完成
</button>
</div>
</Dialog>
);
}