mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Standardize icon sizes and improve rendering consistency across the application interface. Changes: - ProviderIcon: Add fontSize sync with size prop for embedded SVG scaling, improve fallback text sizing calculation - AppSwitcher: Replace brand icon components with unified ProviderIcon, standardize icon size to 20px across all app tabs - ProviderCard: Reduce icon size from 26px to 20px for better visual balance in card layout - DeepLinkImportDialog: Increase confirmation icon size from 64px to 80px for better visibility - BasicFormFields: Reduce icon picker button spacing from space-y-6 to space-y-2 for improved layout density Icon rendering improvements: - Embedded SVGs now properly scale with fontSize set to match size prop - Fallback initials use responsive font sizing (50% of icon size, minimum 12px) - Consistent 20px standard for navigation and card icons - Better visual hierarchy with appropriate sizing for different contexts This refactor improves visual consistency and makes icon rendering more predictable across different components.
225 lines
7.8 KiB
TypeScript
225 lines
7.8 KiB
TypeScript
import { useMemo } from "react";
|
|
import { GripVertical } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type {
|
|
DraggableAttributes,
|
|
DraggableSyntheticListeners,
|
|
} from "@dnd-kit/core";
|
|
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 {
|
|
attributes: DraggableAttributes;
|
|
listeners: DraggableSyntheticListeners;
|
|
isDragging: boolean;
|
|
}
|
|
|
|
interface ProviderCardProps {
|
|
provider: Provider;
|
|
isCurrent: boolean;
|
|
appId: AppId;
|
|
onSwitch: (provider: Provider) => void;
|
|
onEdit: (provider: Provider) => void;
|
|
onDelete: (provider: Provider) => void;
|
|
onConfigureUsage: (provider: Provider) => void;
|
|
onOpenWebsite: (url: string) => void;
|
|
onDuplicate: (provider: Provider) => void;
|
|
dragHandleProps?: DragHandleProps;
|
|
}
|
|
|
|
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
|
// 优先级 1: 备注
|
|
if (provider.notes?.trim()) {
|
|
return provider.notes.trim();
|
|
}
|
|
|
|
// 优先级 2: 官网地址
|
|
if (provider.websiteUrl) {
|
|
return provider.websiteUrl;
|
|
}
|
|
|
|
// 优先级 3: 从配置中提取请求地址
|
|
const config = provider.settingsConfig;
|
|
|
|
if (config && typeof config === "object") {
|
|
const envBase =
|
|
(config as Record<string, any>)?.env?.ANTHROPIC_BASE_URL ||
|
|
(config as Record<string, any>)?.env?.GOOGLE_GEMINI_BASE_URL;
|
|
if (typeof envBase === "string" && envBase.trim()) {
|
|
return envBase;
|
|
}
|
|
|
|
const baseUrl = (config as Record<string, any>)?.config;
|
|
|
|
if (typeof baseUrl === "string" && baseUrl.includes("base_url")) {
|
|
const match = baseUrl.match(/base_url\s*=\s*['"]([^'"]+)['"]/);
|
|
if (match?.[1]) {
|
|
return match[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
return fallbackText;
|
|
};
|
|
|
|
export function ProviderCard({
|
|
provider,
|
|
isCurrent,
|
|
appId,
|
|
onSwitch,
|
|
onEdit,
|
|
onDelete,
|
|
onConfigureUsage,
|
|
onOpenWebsite,
|
|
onDuplicate,
|
|
dragHandleProps,
|
|
}: ProviderCardProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const fallbackUrlText = t("provider.notConfigured", {
|
|
defaultValue: "未配置接口地址",
|
|
});
|
|
|
|
const displayUrl = useMemo(() => {
|
|
return extractApiUrl(provider, fallbackUrlText);
|
|
}, [provider, fallbackUrlText]);
|
|
|
|
// 判断是否为可点击的 URL(备注不可点击)
|
|
const isClickableUrl = useMemo(() => {
|
|
// 如果有备注,则不可点击
|
|
if (provider.notes?.trim()) {
|
|
return false;
|
|
}
|
|
// 如果显示的是回退文本,也不可点击
|
|
if (displayUrl === fallbackUrlText) {
|
|
return false;
|
|
}
|
|
// 其他情况(官网地址或请求地址)可点击
|
|
return true;
|
|
}, [provider.notes, displayUrl, fallbackUrlText]);
|
|
|
|
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
|
|
|
const handleOpenWebsite = () => {
|
|
if (!isClickableUrl) {
|
|
return;
|
|
}
|
|
onOpenWebsite(displayUrl);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"glass-card relative overflow-hidden rounded-xl p-4 transition-all duration-300",
|
|
"group hover:bg-black/[0.02] dark:hover:bg-white/[0.02] hover:border-primary/50",
|
|
isCurrent
|
|
? "border-primary/50 bg-primary/5 shadow-[0_0_20px_rgba(59,130,246,0.15)]"
|
|
: "hover:scale-[1.01]",
|
|
dragHandleProps?.isDragging &&
|
|
"cursor-grabbing border-primary shadow-lg scale-105 z-10",
|
|
)}
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
|
|
<div className="relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="flex flex-1 items-center gap-2">
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"-ml-1.5 flex-shrink-0 cursor-grab active:cursor-grabbing p-1.5",
|
|
"text-muted-foreground/50 hover:text-muted-foreground transition-colors",
|
|
dragHandleProps?.isDragging && "cursor-grabbing",
|
|
)}
|
|
aria-label={t("provider.dragHandle")}
|
|
{...(dragHandleProps?.attributes ?? {})}
|
|
{...(dragHandleProps?.listeners ?? {})}
|
|
>
|
|
<GripVertical className="h-4 w-4" />
|
|
</button>
|
|
|
|
{/* 供应商图标 */}
|
|
<div className="h-8 w-8 rounded-lg bg-white/5 flex items-center justify-center border border-gray-200 dark:border-white/10 group-hover:scale-105 transition-transform duration-300">
|
|
<ProviderIcon
|
|
icon={provider.icon}
|
|
name={provider.name}
|
|
color={provider.iconColor}
|
|
size={20}
|
|
/>
|
|
</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">
|
|
{provider.name}
|
|
</h3>
|
|
{provider.category === "third_party" &&
|
|
provider.meta?.isPartner && (
|
|
<span
|
|
className="text-yellow-500 dark:text-yellow-400"
|
|
title={t("provider.officialPartner", {
|
|
defaultValue: "官方合作伙伴",
|
|
})}
|
|
>
|
|
⭐
|
|
</span>
|
|
)}
|
|
<span
|
|
className={cn(
|
|
"rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-500 dark:text-green-400 transition-opacity duration-200",
|
|
isCurrent ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
)}
|
|
>
|
|
{t("provider.currentlyUsing")}
|
|
</span>
|
|
</div>
|
|
|
|
{displayUrl && (
|
|
<button
|
|
type="button"
|
|
onClick={handleOpenWebsite}
|
|
className={cn(
|
|
"inline-flex items-center text-sm max-w-[280px]",
|
|
isClickableUrl
|
|
? "text-blue-500 transition-colors hover:underline dark:text-blue-400 cursor-pointer"
|
|
: "text-muted-foreground cursor-default",
|
|
)}
|
|
title={displayUrl}
|
|
disabled={!isClickableUrl}
|
|
>
|
|
<span className="truncate">{displayUrl}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex items-center ml-auto">
|
|
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[12.25rem] group-focus-within:-translate-x-[12.25rem] sm:group-hover:-translate-x-[14.25rem] sm:group-focus-within:-translate-x-[14.25rem]">
|
|
<UsageFooter
|
|
provider={provider}
|
|
providerId={provider.id}
|
|
appId={appId}
|
|
usageEnabled={usageEnabled}
|
|
isCurrent={isCurrent}
|
|
inline={true}
|
|
/>
|
|
</div>
|
|
|
|
<div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0">
|
|
<ProviderActions
|
|
isCurrent={isCurrent}
|
|
onSwitch={() => onSwitch(provider)}
|
|
onEdit={() => onEdit(provider)}
|
|
onDuplicate={() => onDuplicate(provider)}
|
|
onConfigureUsage={() => onConfigureUsage(provider)}
|
|
onDelete={() => onDelete(provider)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|