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,
|
||||
websiteUrl: values.websiteUrl?.trim() || undefined,
|
||||
settingsConfig: parsedConfig,
|
||||
icon: values.icon?.trim() || undefined,
|
||||
iconColor: values.iconColor?.trim() || undefined,
|
||||
...(values.presetCategory ? { category: values.presetCategory } : {}),
|
||||
...(values.meta ? { meta: values.meta } : {}),
|
||||
};
|
||||
|
||||
@@ -89,6 +89,8 @@ export function EditProviderDialog({
|
||||
notes: values.notes?.trim() || undefined,
|
||||
websiteUrl: values.websiteUrl?.trim() || undefined,
|
||||
settingsConfig: parsedConfig,
|
||||
icon: values.icon?.trim() || undefined,
|
||||
iconColor: values.iconColor?.trim() || undefined,
|
||||
...(values.presetCategory ? { category: values.presetCategory } : {}),
|
||||
// 保留或更新 meta 字段
|
||||
...(values.meta ? { meta: values.meta } : {}),
|
||||
@@ -124,6 +126,8 @@ export function EditProviderDialog({
|
||||
settingsConfig: initialSettingsConfig,
|
||||
category: provider.category,
|
||||
meta: provider.meta,
|
||||
icon: provider.icon,
|
||||
iconColor: provider.iconColor,
|
||||
}}
|
||||
showButtons={false}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Provider } from "@/types";
|
||||
import type { AppId } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ProviderActions } from "@/components/providers/ProviderActions";
|
||||
import { ProviderIcon } from "@/components/ProviderIcon";
|
||||
import UsageFooter from "@/components/UsageFooter";
|
||||
|
||||
interface DragHandleProps {
|
||||
@@ -139,6 +140,16 @@ export function ProviderCard({
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</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="flex flex-wrap items-center gap-2 min-h-[20px]">
|
||||
<h3 className="text-base font-semibold leading-none">
|
||||
@@ -185,7 +196,7 @@ export function ProviderCard({
|
||||
</div>
|
||||
|
||||
<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
|
||||
provider={provider}
|
||||
providerId={provider.id}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
@@ -7,6 +8,17 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
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 { ProviderFormData } from "@/lib/schemas/provider";
|
||||
|
||||
@@ -16,6 +28,20 @@ interface BasicFormFieldsProps {
|
||||
|
||||
export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
||||
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 (
|
||||
<>
|
||||
@@ -60,6 +86,87 @@ export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
||||
</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>;
|
||||
category?: ProviderCategory;
|
||||
meta?: ProviderMeta;
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
};
|
||||
showButtons?: boolean;
|
||||
}
|
||||
@@ -147,6 +149,8 @@ export function ProviderForm({
|
||||
: appId === "gemini"
|
||||
? GEMINI_DEFAULT_CONFIG
|
||||
: CLAUDE_DEFAULT_CONFIG,
|
||||
icon: initialData?.icon ?? "",
|
||||
iconColor: initialData?.iconColor ?? "",
|
||||
}),
|
||||
[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>;
|
||||
|
||||
@@ -20,6 +20,9 @@ export interface Provider {
|
||||
isPartner?: boolean;
|
||||
// 可选:供应商元数据(仅存于 ~/.cc-switch/config.json,不写入 live 配置)
|
||||
meta?: ProviderMeta;
|
||||
// 图标配置
|
||||
icon?: string; // 图标名称(如 "openai", "anthropic")
|
||||
iconColor?: string; // 图标颜色(Hex 格式,如 "#00A67E")
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
|
||||
Reference in New Issue
Block a user