feat: add unique icons and colors for preset providers

Add visual theme system for provider presets with custom icons and brand colors:

- Add PresetTheme interface supporting icon type and custom colors
- Claude Official: Claude brand icon + orange theme (#D97757)
- Codex Official: Codex brand icon + dark gray theme (#1F2937)
- Other presets: Default to theme blue (bg-blue-500)
- Custom config: Uses theme blue for consistency

Technical changes:
- Extend ProviderPreset and CodexProviderPreset interfaces with optional theme field
- Update ProviderPresetSelector to render icons and apply theme colors
- Support both Tailwind classes and hex colors via inline styles
- Remove unused Link import from ProviderCard

This restores the unique visual identity for official providers while
maintaining a unified theme color for third-party presets.
This commit is contained in:
Jason
2025-10-19 23:11:48 +08:00
parent 505fa47feb
commit 0de818b8b1
4 changed files with 104 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { GripVertical, Link } from "lucide-react"; import { GripVertical } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { import type {
DraggableAttributes, DraggableAttributes,

View File

@@ -1,5 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form"; import { FormLabel } from "@/components/ui/form";
import { ClaudeIcon, CodexIcon } from "@/components/BrandIcons";
import { Zap } from "lucide-react";
import type { ProviderPreset } from "@/config/providerPresets"; import type { ProviderPreset } from "@/config/providerPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets"; import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import type { ProviderCategory } from "@/types"; import type { ProviderCategory } from "@/types";
@@ -58,6 +60,58 @@ export function ProviderPresetSelector({
} }
}; };
// 渲染预设按钮的图标
const renderPresetIcon = (preset: ProviderPreset | CodexProviderPreset) => {
const iconType = preset.theme?.icon;
if (!iconType) return null;
switch (iconType) {
case "claude":
return <ClaudeIcon size={14} />;
case "codex":
return <CodexIcon size={14} />;
case "generic":
return <Zap size={14} />;
default:
return null;
}
};
// 获取预设按钮的样式类名
const getPresetButtonClass = (
isSelected: boolean,
preset: ProviderPreset | CodexProviderPreset,
) => {
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-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700`;
};
// 获取预设按钮的内联样式(用于自定义背景色)
const getPresetButtonStyle = (
isSelected: boolean,
preset: ProviderPreset | CodexProviderPreset,
) => {
if (!isSelected || !preset.theme?.backgroundColor) {
return undefined;
}
return {
backgroundColor: preset.theme.backgroundColor,
color: preset.theme.textColor || "#FFFFFF",
};
};
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<FormLabel> <FormLabel>
@@ -70,7 +124,7 @@ export function ProviderPresetSelector({
onClick={() => onPresetChange("custom")} onClick={() => onPresetChange("custom")}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedPresetId === "custom" selectedPresetId === "custom"
? "bg-emerald-500 text-white dark:bg-emerald-600" ? "bg-blue-500 text-white dark:bg-blue-600"
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700" : "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`} }`}
> >
@@ -81,26 +135,27 @@ export function ProviderPresetSelector({
{categoryKeys.map((category) => { {categoryKeys.map((category) => {
const entries = groupedPresets[category]; const entries = groupedPresets[category];
if (!entries || entries.length === 0) return null; if (!entries || entries.length === 0) return null;
return entries.map((entry) => ( return entries.map((entry) => {
<button const isSelected = selectedPresetId === entry.id;
key={entry.id} return (
type="button" <button
onClick={() => onPresetChange(entry.id)} key={entry.id}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ type="button"
selectedPresetId === entry.id onClick={() => onPresetChange(entry.id)}
? "bg-emerald-500 text-white dark:bg-emerald-600" className={getPresetButtonClass(isSelected, entry.preset)}
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700" style={getPresetButtonStyle(isSelected, entry.preset)}
}`} title={
title={ presetCategoryLabels[category] ??
presetCategoryLabels[category] ?? t("providerPreset.categoryOther", {
t("providerPreset.categoryOther", { defaultValue: "其他",
defaultValue: "其他", })
}) }
} >
> {renderPresetIcon(entry.preset)}
{entry.preset.name} {entry.preset.name}
</button> </button>
)); );
});
})} })}
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">

View File

@@ -2,6 +2,7 @@
* Codex 预设供应商配置模板 * Codex 预设供应商配置模板
*/ */
import { ProviderCategory } from "../types"; import { ProviderCategory } from "../types";
import type { PresetTheme } from "./providerPresets";
export interface CodexProviderPreset { export interface CodexProviderPreset {
name: string; name: string;
@@ -15,6 +16,8 @@ export interface CodexProviderPreset {
isCustomTemplate?: boolean; // 标识是否为自定义模板 isCustomTemplate?: boolean; // 标识是否为自定义模板
// 新增:请求地址候选列表(用于地址管理/测速) // 新增:请求地址候选列表(用于地址管理/测速)
endpointCandidates?: string[]; endpointCandidates?: string[];
// 新增:视觉主题配置
theme?: PresetTheme;
} }
/** /**
@@ -63,6 +66,11 @@ export const codexProviderPresets: CodexProviderPreset[] = [
OPENAI_API_KEY: null, OPENAI_API_KEY: null,
}, },
config: ``, config: ``,
theme: {
icon: "codex",
backgroundColor: "#1F2937", // gray-800
textColor: "#FFFFFF",
},
}, },
{ {
name: "PackyCode", name: "PackyCode",

View File

@@ -10,6 +10,18 @@ export interface TemplateValueConfig {
editorValue: string; editorValue: string;
} }
/**
* 预设供应商的视觉主题配置
*/
export interface PresetTheme {
/** 图标类型:'claude' | 'codex' | 'generic' */
icon?: "claude" | "codex" | "generic";
/** 背景色(选中状态),支持 Tailwind 类名或 hex 颜色 */
backgroundColor?: string;
/** 文字色(选中状态),支持 Tailwind 类名或 hex 颜色 */
textColor?: string;
}
export interface ProviderPreset { export interface ProviderPreset {
name: string; name: string;
websiteUrl: string; websiteUrl: string;
@@ -22,6 +34,8 @@ export interface ProviderPreset {
templateValues?: Record<string, TemplateValueConfig>; // editorValue 存储编辑器中的实时输入值 templateValues?: Record<string, TemplateValueConfig>; // editorValue 存储编辑器中的实时输入值
// 新增:请求地址候选列表(用于地址管理/测速) // 新增:请求地址候选列表(用于地址管理/测速)
endpointCandidates?: string[]; endpointCandidates?: string[];
// 新增:视觉主题配置
theme?: PresetTheme;
} }
export const providerPresets: ProviderPreset[] = [ export const providerPresets: ProviderPreset[] = [
@@ -33,6 +47,11 @@ export const providerPresets: ProviderPreset[] = [
}, },
isOfficial: true, // 明确标识为官方预设 isOfficial: true, // 明确标识为官方预设
category: "official", category: "official",
theme: {
icon: "claude",
backgroundColor: "#D97757",
textColor: "#FFFFFF",
},
}, },
{ {
name: "DeepSeek", name: "DeepSeek",