refactor: extract business logic to useProviderActions hook
Major improvements: - Create `src/hooks/useProviderActions.ts` (147 lines) - Consolidate provider operations (add, update, delete, switch) - Extract Claude plugin sync logic - Extract usage script save logic - Simplify `App.tsx` (347 → 226 lines, -35%) - Remove 8 callback functions - Remove Claude plugin sync logic - Remove usage script save logic - Cleaner and more maintainable - Replace `onNotify` prop with `toast` in: - `UsageScriptModal.tsx` - `McpPanel.tsx` - `McpFormModal.tsx` - `McpWizardModal.tsx` - Unified notification system using sonner Benefits: - Reduced coupling and improved maintainability - Business logic isolated in hooks, easier to test - Consistent notification system across the app
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Play, Wand2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Provider, UsageScript } from "../types";
|
||||
import { usageApi, type AppType } from "@/lib/api";
|
||||
import JsonEditor from "./JsonEditor";
|
||||
@@ -21,11 +22,6 @@ interface UsageScriptModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (script: UsageScript) => void;
|
||||
onNotify?: (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration?: number,
|
||||
) => void;
|
||||
}
|
||||
|
||||
// 预设模板(JS 对象字面量格式)
|
||||
@@ -91,7 +87,6 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
onNotify,
|
||||
}) => {
|
||||
const [script, setScript] = useState<UsageScript>(() => {
|
||||
return (
|
||||
@@ -109,19 +104,18 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
const handleSave = () => {
|
||||
// 验证脚本格式
|
||||
if (script.enabled && !script.code.trim()) {
|
||||
onNotify?.("脚本配置不能为空", "error");
|
||||
toast.error("脚本配置不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 基本的 JS 语法检查(检查是否包含 return 语句)
|
||||
if (script.enabled && !script.code.includes("return")) {
|
||||
onNotify?.("脚本必须包含 return 语句", "error", 5000);
|
||||
toast.error("脚本必须包含 return 语句", { duration: 5000 });
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(script);
|
||||
onClose();
|
||||
onNotify?.("用量查询配置已保存", "success", 2000);
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
@@ -136,12 +130,16 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
return `${planInfo} 剩余: ${plan.remaining} ${plan.unit}`;
|
||||
})
|
||||
.join(", ");
|
||||
onNotify?.(`测试成功!${summary}`, "success", 3000);
|
||||
toast.success(`测试成功!${summary}`, { duration: 3000 });
|
||||
} else {
|
||||
onNotify?.(`测试失败: ${result.error || "无数据返回"}`, "error", 5000);
|
||||
toast.error(`测试失败: ${result.error || "无数据返回"}`, {
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
onNotify?.(`测试失败: ${error?.message || "未知错误"}`, "error", 5000);
|
||||
toast.error(`测试失败: ${error?.message || "未知错误"}`, {
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
@@ -158,9 +156,11 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
printWidth: 80,
|
||||
});
|
||||
setScript({ ...script, code: formatted.trim() });
|
||||
onNotify?.("格式化成功", "success", 1000);
|
||||
toast.success("格式化成功", { duration: 1000 });
|
||||
} catch (error: any) {
|
||||
onNotify?.(`格式化失败: ${error?.message || "语法错误"}`, "error", 3000);
|
||||
toast.error(`格式化失败: ${error?.message || "语法错误"}`, {
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Save,
|
||||
AlertCircle,
|
||||
@@ -36,11 +37,6 @@ interface McpFormModalProps {
|
||||
) => Promise<void>;
|
||||
onClose: () => void;
|
||||
existingIds?: string[];
|
||||
onNotify?: (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration?: number,
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +51,6 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
onSave,
|
||||
onClose,
|
||||
existingIds = [],
|
||||
onNotify,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatTomlError, validateTomlConfig, validateJsonConfig } =
|
||||
@@ -278,7 +273,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
const handleSubmit = async () => {
|
||||
const trimmedId = formId.trim();
|
||||
if (!trimmedId) {
|
||||
onNotify?.(t("mcp.error.idRequired"), "error", 3000);
|
||||
toast.error(t("mcp.error.idRequired"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -296,7 +291,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
const tomlError = validateTomlConfig(formConfig);
|
||||
setConfigError(tomlError);
|
||||
if (tomlError) {
|
||||
onNotify?.(t("mcp.error.tomlInvalid"), "error", 3000);
|
||||
toast.error(t("mcp.error.tomlInvalid"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,7 +308,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || String(e);
|
||||
setConfigError(formatTomlError(msg));
|
||||
onNotify?.(t("mcp.error.tomlInvalid"), "error", 4000);
|
||||
toast.error(t("mcp.error.tomlInvalid"), { duration: 4000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -322,7 +317,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
const jsonError = validateJsonConfig(formConfig);
|
||||
setConfigError(jsonError);
|
||||
if (jsonError) {
|
||||
onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000);
|
||||
toast.error(t("mcp.error.jsonInvalid"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,7 +333,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
serverSpec = JSON.parse(formConfig) as McpServerSpec;
|
||||
} catch (e: any) {
|
||||
setConfigError(t("mcp.error.jsonInvalid"));
|
||||
onNotify?.(t("mcp.error.jsonInvalid"), "error", 4000);
|
||||
toast.error(t("mcp.error.jsonInvalid"), { duration: 4000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -346,11 +341,11 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
|
||||
// 前置必填校验
|
||||
if (serverSpec?.type === "stdio" && !serverSpec?.command?.trim()) {
|
||||
onNotify?.(t("mcp.error.commandRequired"), "error", 3000);
|
||||
toast.error(t("mcp.error.commandRequired"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
if (serverSpec?.type === "http" && !serverSpec?.url?.trim()) {
|
||||
onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000);
|
||||
toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -408,7 +403,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
const detail = extractErrorMessage(error);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
const msg = mapped || detail || t("mcp.error.saveFailed");
|
||||
onNotify?.(msg, "error", mapped || detail ? 6000 : 4000);
|
||||
toast.error(msg, { duration: mapped || detail ? 6000 : 4000 });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -678,7 +673,6 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
isOpen={isWizardOpen}
|
||||
onClose={() => setIsWizardOpen(false)}
|
||||
onApply={handleWizardApply}
|
||||
onNotify={onNotify}
|
||||
initialTitle={formId}
|
||||
initialServer={wizardInitialSpec}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Plus, Server, Check } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -13,16 +14,14 @@ import { McpServer } from "@/types";
|
||||
import McpListItem from "./McpListItem";
|
||||
import McpFormModal from "./McpFormModal";
|
||||
import { ConfirmDialog } from "../ConfirmDialog";
|
||||
import { extractErrorMessage, translateMcpBackendError } from "@/utils/errorUtils";
|
||||
import {
|
||||
extractErrorMessage,
|
||||
translateMcpBackendError,
|
||||
} from "@/utils/errorUtils";
|
||||
|
||||
interface McpPanelProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onNotify?: (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration?: number,
|
||||
) => void;
|
||||
appType: AppType;
|
||||
}
|
||||
|
||||
@@ -33,7 +32,6 @@ interface McpPanelProps {
|
||||
const McpPanel: React.FC<McpPanelProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onNotify,
|
||||
appType,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -91,20 +89,18 @@ const McpPanel: React.FC<McpPanelProps> = ({
|
||||
try {
|
||||
// 后台调用 API
|
||||
await mcpApi.setEnabled(appType, id, enabled);
|
||||
onNotify?.(
|
||||
toast.success(
|
||||
enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"),
|
||||
"success",
|
||||
1500,
|
||||
{ duration: 1500 },
|
||||
);
|
||||
} catch (e: any) {
|
||||
// 失败时回滚
|
||||
setServers(previousServers);
|
||||
const detail = extractErrorMessage(e);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
onNotify?.(
|
||||
toast.error(
|
||||
mapped || detail || t("mcp.error.saveFailed"),
|
||||
"error",
|
||||
mapped || detail ? 6000 : 5000,
|
||||
{ duration: mapped || detail ? 6000 : 5000 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -129,14 +125,13 @@ const McpPanel: React.FC<McpPanelProps> = ({
|
||||
await mcpApi.deleteServerInConfig(appType, id);
|
||||
await reload();
|
||||
setConfirmDialog(null);
|
||||
onNotify?.(t("mcp.msg.deleted"), "success", 1500);
|
||||
toast.success(t("mcp.msg.deleted"), { duration: 1500 });
|
||||
} catch (e: any) {
|
||||
const detail = extractErrorMessage(e);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
onNotify?.(
|
||||
toast.error(
|
||||
mapped || detail || t("mcp.error.deleteFailed"),
|
||||
"error",
|
||||
mapped || detail ? 6000 : 5000,
|
||||
{ duration: mapped || detail ? 6000 : 5000 },
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -156,14 +151,13 @@ const McpPanel: React.FC<McpPanelProps> = ({
|
||||
await reload();
|
||||
setIsFormOpen(false);
|
||||
setEditingId(null);
|
||||
onNotify?.(t("mcp.msg.saved"), "success", 1500);
|
||||
toast.success(t("mcp.msg.saved"), { duration: 1500 });
|
||||
} catch (e: any) {
|
||||
const detail = extractErrorMessage(e);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
onNotify?.(
|
||||
toast.error(
|
||||
mapped || detail || t("mcp.error.saveFailed"),
|
||||
"error",
|
||||
mapped || detail ? 6000 : 5000,
|
||||
{ duration: mapped || detail ? 6000 : 5000 },
|
||||
);
|
||||
// 继续抛出错误,让表单层可以给到直观反馈(避免被更高层遮挡)
|
||||
throw e;
|
||||
@@ -283,7 +277,6 @@ const McpPanel: React.FC<McpPanelProps> = ({
|
||||
existingIds={Object.keys(servers)}
|
||||
onSave={handleSave}
|
||||
onClose={handleCloseForm}
|
||||
onNotify={onNotify}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Save } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -15,11 +16,6 @@ interface McpWizardModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onApply: (title: string, json: string) => void;
|
||||
onNotify?: (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration?: number,
|
||||
) => void;
|
||||
initialTitle?: string;
|
||||
initialServer?: McpServerSpec;
|
||||
}
|
||||
@@ -80,7 +76,6 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onApply,
|
||||
onNotify,
|
||||
initialTitle,
|
||||
initialServer,
|
||||
}) => {
|
||||
@@ -137,15 +132,15 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
||||
|
||||
const handleApply = () => {
|
||||
if (!wizardTitle.trim()) {
|
||||
onNotify?.(t("mcp.error.idRequired"), "error", 3000);
|
||||
toast.error(t("mcp.error.idRequired"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
if (wizardType === "stdio" && !wizardCommand.trim()) {
|
||||
onNotify?.(t("mcp.error.commandRequired"), "error", 3000);
|
||||
toast.error(t("mcp.error.commandRequired"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
if (wizardType === "http" && !wizardUrl.trim()) {
|
||||
onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000);
|
||||
toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user