按「管家准入与退出」撤销实测无使用的功能,台账见 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>
268 lines
9.6 KiB
TypeScript
268 lines
9.6 KiB
TypeScript
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>
|
||
);
|
||
}
|