mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Remove the category-based grouping logic from ProviderPresetSelector, letting the array position in each preset config file be the single source of truth for display order. Move partner presets (PatewayAI, 火山Agentplan, BytePlus, DouBaoSeed) right after Shengsuanyun across all 6 config files so they appear earlier in the UI.
221 lines
8.1 KiB
TypeScript
221 lines
8.1 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { FormLabel } from "@/components/ui/form";
|
|
import { ClaudeIcon, CodexIcon, GeminiIcon } from "@/components/BrandIcons";
|
|
import { Zap, Star, Layers, Settings2 } from "lucide-react";
|
|
import type { ProviderPreset } from "@/config/claudeProviderPresets";
|
|
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
|
|
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
|
|
import type { ClaudeDesktopProviderPreset } from "@/config/claudeDesktopProviderPresets";
|
|
import type { OpenCodeProviderPreset } from "@/config/opencodeProviderPresets";
|
|
import type { OpenClawProviderPreset } from "@/config/openclawProviderPresets";
|
|
import type { HermesProviderPreset } from "@/config/hermesProviderPresets";
|
|
import type { ProviderCategory } from "@/types";
|
|
import {
|
|
universalProviderPresets,
|
|
type UniversalProviderPreset,
|
|
} from "@/config/universalProviderPresets";
|
|
import { ProviderIcon } from "@/components/ProviderIcon";
|
|
|
|
type AnyPreset =
|
|
| ProviderPreset
|
|
| CodexProviderPreset
|
|
| GeminiProviderPreset
|
|
| ClaudeDesktopProviderPreset
|
|
| OpenCodeProviderPreset
|
|
| OpenClawProviderPreset
|
|
| HermesProviderPreset;
|
|
|
|
type PresetEntry = {
|
|
id: string;
|
|
preset: AnyPreset;
|
|
};
|
|
|
|
interface ProviderPresetSelectorProps {
|
|
selectedPresetId: string | null;
|
|
presetEntries: PresetEntry[];
|
|
presetCategoryLabels: Record<string, string>;
|
|
onPresetChange: (value: string) => void;
|
|
onUniversalPresetSelect?: (preset: UniversalProviderPreset) => void;
|
|
onManageUniversalProviders?: () => void;
|
|
category?: ProviderCategory; // 当前选中的分类
|
|
}
|
|
|
|
export function ProviderPresetSelector({
|
|
selectedPresetId,
|
|
presetEntries,
|
|
presetCategoryLabels,
|
|
onPresetChange,
|
|
onUniversalPresetSelect,
|
|
onManageUniversalProviders,
|
|
category,
|
|
}: ProviderPresetSelectorProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const getCategoryHint = (): React.ReactNode => {
|
|
switch (category) {
|
|
case "official":
|
|
return t("providerForm.officialHint", {
|
|
defaultValue: "💡 官方供应商使用浏览器登录,无需配置 API Key",
|
|
});
|
|
case "cn_official":
|
|
return t("providerForm.cnOfficialApiKeyHint", {
|
|
defaultValue: "💡 国产官方供应商只需填写 API Key,请求地址已预设",
|
|
});
|
|
case "aggregator":
|
|
return t("providerForm.aggregatorApiKeyHint", {
|
|
defaultValue: "💡 聚合服务供应商只需填写 API Key 即可使用",
|
|
});
|
|
case "third_party":
|
|
return t("providerForm.thirdPartyApiKeyHint", {
|
|
defaultValue: "💡 第三方供应商需要填写 API Key 和请求地址",
|
|
});
|
|
case "custom":
|
|
return t("providerForm.customApiKeyHint", {
|
|
defaultValue: "💡 自定义配置需手动填写所有必要字段",
|
|
});
|
|
case "omo":
|
|
return t("providerForm.omoHint", {
|
|
defaultValue:
|
|
"💡 OMO 配置管理 Agent 模型分配,兼容 oh-my-openagent.jsonc / oh-my-opencode.jsonc",
|
|
});
|
|
default:
|
|
return t("providerPreset.hint", {
|
|
defaultValue: "选择预设后可继续调整下方字段。",
|
|
});
|
|
}
|
|
};
|
|
|
|
const renderPresetIcon = (preset: AnyPreset) => {
|
|
const iconType = preset.theme?.icon;
|
|
if (!iconType) return null;
|
|
|
|
switch (iconType) {
|
|
case "claude":
|
|
return <ClaudeIcon size={14} />;
|
|
case "codex":
|
|
return <CodexIcon size={14} />;
|
|
case "gemini":
|
|
return <GeminiIcon size={14} />;
|
|
case "generic":
|
|
return <Zap size={14} />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getPresetButtonClass = (isSelected: boolean, preset: AnyPreset) => {
|
|
const baseClass =
|
|
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
|
|
|
if (isSelected) {
|
|
if (preset.theme?.backgroundColor) {
|
|
return `${baseClass} text-white`;
|
|
}
|
|
return `${baseClass} bg-blue-500 text-white dark:bg-blue-600`;
|
|
}
|
|
|
|
return `${baseClass} bg-accent text-muted-foreground hover:bg-accent/80`;
|
|
};
|
|
|
|
const getPresetButtonStyle = (isSelected: boolean, preset: AnyPreset) => {
|
|
if (!isSelected || !preset.theme?.backgroundColor) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
backgroundColor: preset.theme.backgroundColor,
|
|
color: preset.theme.textColor || "#FFFFFF",
|
|
};
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<FormLabel>{t("providerPreset.label")}</FormLabel>
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => onPresetChange("custom")}
|
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
selectedPresetId === "custom"
|
|
? "bg-blue-500 text-white dark:bg-blue-600"
|
|
: "bg-accent text-muted-foreground hover:bg-accent/80"
|
|
}`}
|
|
>
|
|
{t("providerPreset.custom")}
|
|
</button>
|
|
|
|
{presetEntries.map((entry) => {
|
|
const isSelected = selectedPresetId === entry.id;
|
|
const isPartner = entry.preset.isPartner;
|
|
const presetCategory = entry.preset.category ?? "others";
|
|
return (
|
|
<button
|
|
key={entry.id}
|
|
type="button"
|
|
onClick={() => onPresetChange(entry.id)}
|
|
className={`${getPresetButtonClass(isSelected, entry.preset)} relative`}
|
|
style={getPresetButtonStyle(isSelected, entry.preset)}
|
|
title={
|
|
presetCategoryLabels[presetCategory] ??
|
|
t("providerPreset.other")
|
|
}
|
|
>
|
|
{renderPresetIcon(entry.preset)}
|
|
{entry.preset.nameKey
|
|
? t(entry.preset.nameKey)
|
|
: entry.preset.name}
|
|
{isPartner && (
|
|
<span className="absolute -top-1 -right-1 flex items-center gap-0.5 rounded-full bg-gradient-to-r from-amber-500 to-yellow-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-md">
|
|
<Star className="h-2.5 w-2.5 fill-current" />
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{onUniversalPresetSelect && universalProviderPresets.length > 0 && (
|
|
<>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{universalProviderPresets.map((preset) => (
|
|
<button
|
|
key={`universal-${preset.providerType}`}
|
|
type="button"
|
|
onClick={() => onUniversalPresetSelect(preset)}
|
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-accent text-muted-foreground hover:bg-accent/80 relative"
|
|
title={t("universalProvider.hint", {
|
|
defaultValue:
|
|
"跨应用统一配置,自动同步到 Claude/Codex/Gemini",
|
|
})}
|
|
>
|
|
<ProviderIcon icon={preset.icon} name={preset.name} size={14} />
|
|
{preset.name}
|
|
<span className="absolute -top-1 -right-1 flex items-center gap-0.5 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-md">
|
|
<Layers className="h-2.5 w-2.5" />
|
|
</span>
|
|
</button>
|
|
))}
|
|
{onManageUniversalProviders && (
|
|
<button
|
|
type="button"
|
|
onClick={onManageUniversalProviders}
|
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-accent text-muted-foreground hover:bg-accent/80"
|
|
title={t("universalProvider.manage", {
|
|
defaultValue: "管理统一供应商",
|
|
})}
|
|
>
|
|
<Settings2 className="h-4 w-4" />
|
|
{t("universalProvider.manage", {
|
|
defaultValue: "管理",
|
|
})}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<p className="text-xs text-muted-foreground">{getCategoryHint()}</p>
|
|
</div>
|
|
);
|
|
}
|