mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-22 13:33:29 +08:00
cfefe6b52a
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application. ## Changes ### UsageScriptModal.tsx - Replace custom modal structure (fixed positioning, backdrop) with Dialog component - Remove X icon import (Dialog includes built-in close button) - Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports - Add Button component import for action buttons - Update props interface to include isOpen boolean prop - Restructure component layout: - Use DialogHeader with DialogTitle for header section - Apply -mx-6 px-6 pattern for full-width scrollable content - Use DialogFooter with flex-col sm:flex-row sm:justify-between layout - Convert custom buttons to Button components: - Test/Format buttons: variant="outline" size="sm" - Cancel button: variant="ghost" size="sm" - Save button: variant="default" size="sm" - Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting) ### App.tsx - Update UsageScriptModal usage to pass isOpen prop - Use Boolean(usageProvider) to control dialog open state ## Benefits - **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component - **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes - **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally - **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs All TypeScript type checks and Prettier formatting checks pass.
348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { Plus, Settings } from "lucide-react";
|
|
import type { Provider, UsageScript } from "@/types";
|
|
import {
|
|
useProvidersQuery,
|
|
useAddProviderMutation,
|
|
useUpdateProviderMutation,
|
|
useDeleteProviderMutation,
|
|
useSwitchProviderMutation,
|
|
} from "@/lib/query";
|
|
import {
|
|
providersApi,
|
|
settingsApi,
|
|
type AppType,
|
|
type ProviderSwitchEvent,
|
|
} from "@/lib/api";
|
|
import { extractErrorMessage } from "@/utils/errorUtils";
|
|
import { AppSwitcher } from "@/components/AppSwitcher";
|
|
import { ModeToggle } from "@/components/mode-toggle";
|
|
import { ProviderList } from "@/components/providers/ProviderList";
|
|
import { AddProviderDialog } from "@/components/providers/AddProviderDialog";
|
|
import { EditProviderDialog } from "@/components/providers/EditProviderDialog";
|
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
import { SettingsDialog } from "@/components/settings/SettingsDialog";
|
|
import { UpdateBadge } from "@/components/UpdateBadge";
|
|
import UsageScriptModal from "@/components/UsageScriptModal";
|
|
import McpPanel from "@/components/mcp/McpPanel";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
function App() {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
|
|
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
const [isAddOpen, setIsAddOpen] = useState(false);
|
|
const [isMcpOpen, setIsMcpOpen] = useState(false);
|
|
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
|
|
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
|
|
const [confirmDelete, setConfirmDelete] = useState<Provider | null>(null);
|
|
|
|
const { data, isLoading, refetch } = useProvidersQuery(activeApp);
|
|
const providers = useMemo(() => data?.providers ?? {}, [data]);
|
|
const currentProviderId = data?.currentProviderId ?? "";
|
|
|
|
const addProviderMutation = useAddProviderMutation(activeApp);
|
|
const updateProviderMutation = useUpdateProviderMutation(activeApp);
|
|
const deleteProviderMutation = useDeleteProviderMutation(activeApp);
|
|
const switchProviderMutation = useSwitchProviderMutation(activeApp);
|
|
|
|
useEffect(() => {
|
|
let unsubscribe: (() => void) | undefined;
|
|
|
|
const setupListener = async () => {
|
|
try {
|
|
unsubscribe = await providersApi.onSwitched(
|
|
async (event: ProviderSwitchEvent) => {
|
|
if (event.appType === activeApp) {
|
|
await refetch();
|
|
}
|
|
},
|
|
);
|
|
} catch (error) {
|
|
console.error("[App] Failed to subscribe provider switch event", error);
|
|
}
|
|
};
|
|
|
|
setupListener();
|
|
return () => {
|
|
unsubscribe?.();
|
|
};
|
|
}, [activeApp, refetch]);
|
|
|
|
const handleNotify = useCallback(
|
|
(message: string, type: "success" | "error", duration?: number) => {
|
|
const options = duration ? { duration } : undefined;
|
|
if (type === "error") {
|
|
toast.error(message, options);
|
|
} else {
|
|
toast.success(message, options);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
const handleOpenWebsite = useCallback(
|
|
async (url: string) => {
|
|
try {
|
|
await settingsApi.openExternal(url);
|
|
} catch (error) {
|
|
const detail =
|
|
extractErrorMessage(error) ||
|
|
t("notifications.openLinkFailed", {
|
|
defaultValue: "链接打开失败",
|
|
});
|
|
toast.error(detail);
|
|
}
|
|
},
|
|
[t],
|
|
);
|
|
|
|
const handleAddProvider = useCallback(
|
|
async (provider: Omit<Provider, "id">) => {
|
|
await addProviderMutation.mutateAsync(provider);
|
|
},
|
|
[addProviderMutation],
|
|
);
|
|
|
|
const handleEditProvider = useCallback(
|
|
async (provider: Provider) => {
|
|
try {
|
|
await updateProviderMutation.mutateAsync(provider);
|
|
await providersApi.updateTrayMenu();
|
|
setEditingProvider(null);
|
|
} catch {
|
|
// 错误提示由 mutation 统一处理
|
|
}
|
|
},
|
|
[updateProviderMutation],
|
|
);
|
|
|
|
const handleSyncClaudePlugin = useCallback(
|
|
async (provider: Provider) => {
|
|
if (activeApp !== "claude") return;
|
|
|
|
try {
|
|
const settings = await settingsApi.get();
|
|
if (!settings?.enableClaudePluginIntegration) {
|
|
return;
|
|
}
|
|
|
|
const isOfficial = provider.category === "official";
|
|
await settingsApi.applyClaudePluginConfig({ official: isOfficial });
|
|
|
|
toast.success(
|
|
isOfficial
|
|
? t("notifications.appliedToClaudePlugin", {
|
|
defaultValue: "已同步为官方配置",
|
|
})
|
|
: t("notifications.removedFromClaudePlugin", {
|
|
defaultValue: "已移除 Claude 插件配置",
|
|
}),
|
|
{ duration: 2200 },
|
|
);
|
|
} catch (error) {
|
|
const detail =
|
|
extractErrorMessage(error) ||
|
|
t("notifications.syncClaudePluginFailed", {
|
|
defaultValue: "同步 Claude 插件失败",
|
|
});
|
|
toast.error(detail, { duration: 4200 });
|
|
}
|
|
},
|
|
[activeApp, t],
|
|
);
|
|
|
|
const handleSwitchProvider = useCallback(
|
|
async (provider: Provider) => {
|
|
try {
|
|
await switchProviderMutation.mutateAsync(provider.id);
|
|
await handleSyncClaudePlugin(provider);
|
|
} catch {
|
|
// 错误提示由 mutation 与同步函数处理
|
|
}
|
|
},
|
|
[switchProviderMutation, handleSyncClaudePlugin],
|
|
);
|
|
|
|
const handleRequestDelete = useCallback((provider: Provider) => {
|
|
setConfirmDelete(provider);
|
|
}, []);
|
|
|
|
const handleConfirmDelete = useCallback(async () => {
|
|
if (!confirmDelete) return;
|
|
try {
|
|
await deleteProviderMutation.mutateAsync(confirmDelete.id);
|
|
} finally {
|
|
setConfirmDelete(null);
|
|
}
|
|
}, [confirmDelete, deleteProviderMutation]);
|
|
|
|
const handleImportSuccess = useCallback(async () => {
|
|
await refetch();
|
|
try {
|
|
await providersApi.updateTrayMenu();
|
|
} catch (error) {
|
|
console.error("[App] Failed to refresh tray menu", error);
|
|
}
|
|
}, [refetch]);
|
|
|
|
const handleSaveUsageScript = useCallback(
|
|
async (provider: Provider, script: UsageScript) => {
|
|
try {
|
|
const updatedProvider: Provider = {
|
|
...provider,
|
|
meta: {
|
|
...provider.meta,
|
|
usage_script: script,
|
|
},
|
|
};
|
|
|
|
await providersApi.update(updatedProvider, activeApp);
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["providers", activeApp],
|
|
});
|
|
toast.success(
|
|
t("provider.usageSaved", {
|
|
defaultValue: "用量查询配置已保存",
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
const detail =
|
|
extractErrorMessage(error) ||
|
|
t("provider.usageSaveFailed", {
|
|
defaultValue: "用量查询配置保存失败",
|
|
});
|
|
toast.error(detail);
|
|
}
|
|
},
|
|
[activeApp, queryClient, t],
|
|
);
|
|
|
|
return (
|
|
<div className="flex h-screen flex-col bg-gray-50 dark:bg-gray-950">
|
|
<header className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-800 dark:bg-gray-900">
|
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<a
|
|
href="https://github.com/farion1231/cc-switch"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-xl font-semibold text-blue-500 transition-colors hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
|
|
>
|
|
CC Switch
|
|
</a>
|
|
<ModeToggle />
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsSettingsOpen(true)}
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
</Button>
|
|
<UpdateBadge onClick={() => setIsSettingsOpen(true)} />
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
|
<Button
|
|
variant="mcp"
|
|
onClick={() => setIsMcpOpen(true)}
|
|
className="min-w-[80px]"
|
|
>
|
|
MCP
|
|
</Button>
|
|
<Button onClick={() => setIsAddOpen(true)}>
|
|
<Plus className="h-4 w-4" />
|
|
{t("header.addProvider", { defaultValue: "添加供应商" })}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex-1 overflow-y-auto">
|
|
<div className="mx-auto max-w-4xl px-6 py-6">
|
|
<ProviderList
|
|
providers={providers}
|
|
currentProviderId={currentProviderId}
|
|
appType={activeApp}
|
|
isLoading={isLoading}
|
|
onSwitch={handleSwitchProvider}
|
|
onEdit={setEditingProvider}
|
|
onDelete={handleRequestDelete}
|
|
onConfigureUsage={setUsageProvider}
|
|
onOpenWebsite={handleOpenWebsite}
|
|
onCreate={() => setIsAddOpen(true)}
|
|
/>
|
|
</div>
|
|
</main>
|
|
|
|
<AddProviderDialog
|
|
open={isAddOpen}
|
|
onOpenChange={setIsAddOpen}
|
|
appType={activeApp}
|
|
onSubmit={handleAddProvider}
|
|
/>
|
|
|
|
<EditProviderDialog
|
|
open={Boolean(editingProvider)}
|
|
provider={editingProvider}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setEditingProvider(null);
|
|
}
|
|
}}
|
|
onSubmit={handleEditProvider}
|
|
appType={activeApp}
|
|
/>
|
|
|
|
{usageProvider && (
|
|
<UsageScriptModal
|
|
provider={usageProvider}
|
|
appType={activeApp}
|
|
isOpen={Boolean(usageProvider)}
|
|
onClose={() => setUsageProvider(null)}
|
|
onSave={(script) => {
|
|
void handleSaveUsageScript(usageProvider, script);
|
|
}}
|
|
onNotify={handleNotify}
|
|
/>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
isOpen={Boolean(confirmDelete)}
|
|
title={t("confirm.deleteProvider", { defaultValue: "删除供应商" })}
|
|
message={
|
|
confirmDelete
|
|
? t("confirm.deleteProviderMessage", {
|
|
name: confirmDelete.name,
|
|
defaultValue: `确定删除 ${confirmDelete.name} 吗?`,
|
|
})
|
|
: ""
|
|
}
|
|
onConfirm={() => void handleConfirmDelete()}
|
|
onCancel={() => setConfirmDelete(null)}
|
|
/>
|
|
|
|
<SettingsDialog
|
|
open={isSettingsOpen}
|
|
onOpenChange={setIsSettingsOpen}
|
|
onImportSuccess={handleImportSuccess}
|
|
/>
|
|
|
|
<McpPanel
|
|
open={isMcpOpen}
|
|
onOpenChange={setIsMcpOpen}
|
|
appType={activeApp}
|
|
onNotify={handleNotify}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|