mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 04:02:02 +08:00
feat: add Universal Provider feature (#348)
* feat: add Universal Provider feature - Add Universal Provider data structures and type definitions - Implement backend CRUD operations and sync functionality - Add frontend UI components (UniversalProviderPanel, Card, FormModal) - Add NewAPI icon and preset configuration - Support cross-app (Claude/Codex/Gemini) configuration sync - Add website URL field for providers - Implement real-time refresh via event notifications - Add i18n support (Chinese/English/Japanese) * feat: integrate universal provider presets into add provider dialog - Add universal provider presets (NewAPI, Custom Gateway) to preset selector - Show universal presets with Layers icon badge in preset selector - Open UniversalProviderFormModal when universal preset is clicked - Pass initialPreset to auto-fill form when opened from add dialog - Add i18n keys for addSuccess/addFailed messages - Keep separate universal provider panel for management * refactor: move universal provider management to add dialog - Remove Layers button from main navigation header - Add 'Manage' button next to universal provider presets - Open UniversalProviderPanel from within add provider dialog - Add i18n keys for 'manage' in all locales * style: display universal provider presets on separate line - Move universal provider section to a new row with border separator - Add label '统一供应商:' to clarify the section * style: unify universal provider label style with preset label - Use FormLabel component for consistent styling - Add background to 'Manage' button matching preset buttons - Update icon size and button padding for consistency * feat: add sync functionality and JSON preview for Universal Provider * fix: add missing in_failover_queue field to Provider structs After rebasing to main, the Provider struct gained a new `in_failover_queue` field. This fix adds the missing field to the three to_*_provider() methods in UniversalProvider. * refactor: redesign AddProviderDialog with tab-based layout - Add tabs to separate app-specific providers and universal providers - Move "Add Universal Provider" button from panel header to footer - Remove unused handleAdd callback and clean up imports - Update emptyHint i18n text to reference the footer button * fix: append /v1 suffix to Codex base_url in Universal Provider Codex uses OpenAI-compatible API which requires the /v1 endpoint suffix. The Universal Provider now automatically appends /v1 to base_url when generating Codex provider config if not already present. - Handle trailing slashes to avoid double slashes - Apply fix to both backend (to_codex_provider) and frontend preview * feat: auto-sync universal provider to apps on creation Previously, users had to manually click sync after adding a universal provider. Now it automatically syncs to Claude/Codex/Gemini on creation, providing a smoother user experience. --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Edit2, Trash2, RefreshCw, Globe } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ProviderIcon } from "@/components/ProviderIcon";
|
||||
import type { UniversalProvider } from "@/types";
|
||||
|
||||
interface UniversalProviderCardProps {
|
||||
provider: UniversalProvider;
|
||||
onEdit: (provider: UniversalProvider) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onSync: (id: string) => void;
|
||||
}
|
||||
|
||||
export function UniversalProviderCard({
|
||||
provider,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onSync,
|
||||
}: UniversalProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 获取启用的应用列表
|
||||
const enabledApps: string[] = [
|
||||
provider.apps.claude ? "Claude" : null,
|
||||
provider.apps.codex ? "Codex" : null,
|
||||
provider.apps.gemini ? "Gemini" : null,
|
||||
].filter((app): app is string => app !== null);
|
||||
|
||||
return (
|
||||
<div className="group relative rounded-xl border border-border/50 bg-card p-4 transition-all hover:border-border hover:shadow-md">
|
||||
{/* 头部:图标和名称 */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent">
|
||||
<ProviderIcon icon={provider.icon} name={provider.name} size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{provider.name}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{provider.providerType}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => onSync(provider.id)}
|
||||
title={t("universalProvider.sync", { defaultValue: "同步到应用" })}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => onEdit(provider)}
|
||||
title={t("common.edit", { defaultValue: "编辑" })}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={() => onDelete(provider.id)}
|
||||
title={t("common.delete", { defaultValue: "删除" })}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 配置信息 */}
|
||||
<div className="mt-4 space-y-2">
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="truncate text-muted-foreground">
|
||||
{provider.baseUrl || "-"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 启用的应用 */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{enabledApps.map((app) => (
|
||||
<span
|
||||
key={app}
|
||||
className="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary"
|
||||
>
|
||||
{app}
|
||||
</span>
|
||||
))}
|
||||
{enabledApps.length === 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("universalProvider.noAppsEnabled", {
|
||||
defaultValue: "未启用任何应用",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
{provider.notes && (
|
||||
<p className="mt-3 text-xs text-muted-foreground line-clamp-2">
|
||||
{provider.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user