feat(provider): integrate icon system into provider UI components
Add icon customization support to provider management interface: ## Type System Updates ### Provider Interface (src/types.ts) - Add optional `icon` field for icon name (e.g., "openai", "anthropic") - Add optional `iconColor` field for hex color (e.g., "#00A67E") ### Form Schema (src/lib/schemas/provider.ts) - Extend providerSchema with icon and iconColor optional fields - Maintain backward compatibility with existing providers ## UI Components ### ProviderCard (src/components/providers/ProviderCard.tsx) - Display ProviderIcon alongside provider name - Add icon container with hover animation effect - Adjust layout spacing for icon placement - Update translate offsets for action buttons ### BasicFormFields (src/components/providers/forms/BasicFormFields.tsx) - Add icon preview section showing current selection - Implement fullscreen icon picker dialog - Auto-apply default color from icon metadata on selection - Display provider name and icon status in preview ### AddProviderDialog & EditProviderDialog - Pass icon fields through form submission - Preserve icon data during provider updates This enables users to visually distinguish providers in the list with custom icons, improving UX for multi-provider setups.
This commit is contained in:
@@ -41,6 +41,8 @@ export function AddProviderDialog({
|
|||||||
notes: values.notes?.trim() || undefined,
|
notes: values.notes?.trim() || undefined,
|
||||||
websiteUrl: values.websiteUrl?.trim() || undefined,
|
websiteUrl: values.websiteUrl?.trim() || undefined,
|
||||||
settingsConfig: parsedConfig,
|
settingsConfig: parsedConfig,
|
||||||
|
icon: values.icon?.trim() || undefined,
|
||||||
|
iconColor: values.iconColor?.trim() || undefined,
|
||||||
...(values.presetCategory ? { category: values.presetCategory } : {}),
|
...(values.presetCategory ? { category: values.presetCategory } : {}),
|
||||||
...(values.meta ? { meta: values.meta } : {}),
|
...(values.meta ? { meta: values.meta } : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ export function EditProviderDialog({
|
|||||||
notes: values.notes?.trim() || undefined,
|
notes: values.notes?.trim() || undefined,
|
||||||
websiteUrl: values.websiteUrl?.trim() || undefined,
|
websiteUrl: values.websiteUrl?.trim() || undefined,
|
||||||
settingsConfig: parsedConfig,
|
settingsConfig: parsedConfig,
|
||||||
|
icon: values.icon?.trim() || undefined,
|
||||||
|
iconColor: values.iconColor?.trim() || undefined,
|
||||||
...(values.presetCategory ? { category: values.presetCategory } : {}),
|
...(values.presetCategory ? { category: values.presetCategory } : {}),
|
||||||
// 保留或更新 meta 字段
|
// 保留或更新 meta 字段
|
||||||
...(values.meta ? { meta: values.meta } : {}),
|
...(values.meta ? { meta: values.meta } : {}),
|
||||||
@@ -124,6 +126,8 @@ export function EditProviderDialog({
|
|||||||
settingsConfig: initialSettingsConfig,
|
settingsConfig: initialSettingsConfig,
|
||||||
category: provider.category,
|
category: provider.category,
|
||||||
meta: provider.meta,
|
meta: provider.meta,
|
||||||
|
icon: provider.icon,
|
||||||
|
iconColor: provider.iconColor,
|
||||||
}}
|
}}
|
||||||
showButtons={false}
|
showButtons={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { Provider } from "@/types";
|
|||||||
import type { AppId } from "@/lib/api";
|
import type { AppId } from "@/lib/api";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ProviderActions } from "@/components/providers/ProviderActions";
|
import { ProviderActions } from "@/components/providers/ProviderActions";
|
||||||
|
import { ProviderIcon } from "@/components/ProviderIcon";
|
||||||
import UsageFooter from "@/components/UsageFooter";
|
import UsageFooter from "@/components/UsageFooter";
|
||||||
|
|
||||||
interface DragHandleProps {
|
interface DragHandleProps {
|
||||||
@@ -139,6 +140,16 @@ export function ProviderCard({
|
|||||||
<GripVertical className="h-4 w-4" />
|
<GripVertical className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* 供应商图标 */}
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center border border-white/10 group-hover:scale-105 transition-transform duration-300">
|
||||||
|
<ProviderIcon
|
||||||
|
icon={provider.icon}
|
||||||
|
name={provider.name}
|
||||||
|
color={provider.iconColor}
|
||||||
|
size={28}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex flex-wrap items-center gap-2 min-h-[20px]">
|
<div className="flex flex-wrap items-center gap-2 min-h-[20px]">
|
||||||
<h3 className="text-base font-semibold leading-none">
|
<h3 className="text-base font-semibold leading-none">
|
||||||
@@ -185,7 +196,7 @@ export function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex items-center ml-auto">
|
<div className="relative flex items-center ml-auto">
|
||||||
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[16rem] group-focus-within:-translate-x-[16rem] sm:group-hover:-translate-x-[18rem] sm:group-focus-within:-translate-x-[18rem]">
|
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[14rem] group-focus-within:-translate-x-[14rem] sm:group-hover:-translate-x-[16rem] sm:group-focus-within:-translate-x-[16rem]">
|
||||||
<UsageFooter
|
<UsageFooter
|
||||||
provider={provider}
|
provider={provider}
|
||||||
providerId={provider.id}
|
providerId={provider.id}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
@@ -7,6 +8,17 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogClose,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { ProviderIcon } from "@/components/ProviderIcon";
|
||||||
|
import { IconPicker } from "@/components/IconPicker";
|
||||||
|
import { getIconMetadata } from "@/icons/extracted/metadata";
|
||||||
import type { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import type { ProviderFormData } from "@/lib/schemas/provider";
|
import type { ProviderFormData } from "@/lib/schemas/provider";
|
||||||
|
|
||||||
@@ -16,6 +28,20 @@ interface BasicFormFieldsProps {
|
|||||||
|
|
||||||
export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [iconDialogOpen, setIconDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const currentIcon = form.watch("icon");
|
||||||
|
const currentIconColor = form.watch("iconColor");
|
||||||
|
const providerName = form.watch("name") || "Provider";
|
||||||
|
const effectiveIconColor =
|
||||||
|
currentIconColor ||
|
||||||
|
(currentIcon ? getIconMetadata(currentIcon)?.defaultColor : undefined);
|
||||||
|
|
||||||
|
const handleIconSelect = (icon: string) => {
|
||||||
|
const meta = getIconMetadata(icon);
|
||||||
|
form.setValue("icon", icon);
|
||||||
|
form.setValue("iconColor", meta?.defaultColor ?? "");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -60,6 +86,87 @@ export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 图标配置 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<FormLabel>
|
||||||
|
{t("providerIcon.label", { defaultValue: "图标" })}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
{/* 图标预览 */}
|
||||||
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-muted">
|
||||||
|
<ProviderIcon
|
||||||
|
icon={currentIcon}
|
||||||
|
name={providerName}
|
||||||
|
color={effectiveIconColor}
|
||||||
|
size={48}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">{providerName}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{currentIcon ||
|
||||||
|
t("providerIcon.noIcon", { defaultValue: "未选择图标" })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Dialog open={iconDialogOpen} onOpenChange={setIconDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
{t("providerIcon.selectIcon", { defaultValue: "选择图标" })}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
variant="fullscreen"
|
||||||
|
zIndex="top"
|
||||||
|
overlayClassName="bg-[hsl(var(--background))] backdrop-blur-0"
|
||||||
|
className="p-0 sm:rounded-none"
|
||||||
|
>
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<div className="flex-shrink-0 px-6 py-4 flex items-center gap-4 border-b border-border-default bg-muted/40">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-lg font-semibold leading-tight">
|
||||||
|
{t("providerIcon.selectIcon", {
|
||||||
|
defaultValue: "选择图标",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("providerIcon.selectDescription", {
|
||||||
|
defaultValue: "为供应商选择一个图标",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-6">
|
||||||
|
<div className="space-y-6 max-w-5xl mx-auto w-full">
|
||||||
|
{/* 图标选择器 */}
|
||||||
|
<IconPicker
|
||||||
|
value={currentIcon}
|
||||||
|
onValueChange={handleIconSelect}
|
||||||
|
color={effectiveIconColor}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
{t("common.done", { defaultValue: "完成" })}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ interface ProviderFormProps {
|
|||||||
settingsConfig?: Record<string, unknown>;
|
settingsConfig?: Record<string, unknown>;
|
||||||
category?: ProviderCategory;
|
category?: ProviderCategory;
|
||||||
meta?: ProviderMeta;
|
meta?: ProviderMeta;
|
||||||
|
icon?: string;
|
||||||
|
iconColor?: string;
|
||||||
};
|
};
|
||||||
showButtons?: boolean;
|
showButtons?: boolean;
|
||||||
}
|
}
|
||||||
@@ -147,6 +149,8 @@ export function ProviderForm({
|
|||||||
: appId === "gemini"
|
: appId === "gemini"
|
||||||
? GEMINI_DEFAULT_CONFIG
|
? GEMINI_DEFAULT_CONFIG
|
||||||
: CLAUDE_DEFAULT_CONFIG,
|
: CLAUDE_DEFAULT_CONFIG,
|
||||||
|
icon: initialData?.icon ?? "",
|
||||||
|
iconColor: initialData?.iconColor ?? "",
|
||||||
}),
|
}),
|
||||||
[initialData, appId],
|
[initialData, appId],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ export const providerSchema = z.object({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
// 图标配置
|
||||||
|
icon: z.string().optional(),
|
||||||
|
iconColor: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProviderFormData = z.infer<typeof providerSchema>;
|
export type ProviderFormData = z.infer<typeof providerSchema>;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export interface Provider {
|
|||||||
isPartner?: boolean;
|
isPartner?: boolean;
|
||||||
// 可选:供应商元数据(仅存于 ~/.cc-switch/config.json,不写入 live 配置)
|
// 可选:供应商元数据(仅存于 ~/.cc-switch/config.json,不写入 live 配置)
|
||||||
meta?: ProviderMeta;
|
meta?: ProviderMeta;
|
||||||
|
// 图标配置
|
||||||
|
icon?: string; // 图标名称(如 "openai", "anthropic")
|
||||||
|
iconColor?: string; // 图标颜色(Hex 格式,如 "#00A67E")
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user