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:
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user