2025-08-27 11:00:53 +08:00
|
|
|
|
import React from "react";
|
|
|
|
|
|
import { Provider } from "../types";
|
2025-09-06 23:57:10 +08:00
|
|
|
|
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
|
|
interface ProviderListProps {
|
2025-08-27 11:00:53 +08:00
|
|
|
|
providers: Record<string, Provider>;
|
|
|
|
|
|
currentProviderId: string;
|
|
|
|
|
|
onSwitch: (id: string) => void;
|
|
|
|
|
|
onDelete: (id: string) => void;
|
|
|
|
|
|
onEdit: (id: string) => void;
|
2025-08-04 22:16:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ProviderList: React.FC<ProviderListProps> = ({
|
|
|
|
|
|
providers,
|
|
|
|
|
|
currentProviderId,
|
|
|
|
|
|
onSwitch,
|
2025-08-05 09:51:41 +08:00
|
|
|
|
onDelete,
|
2025-08-27 11:00:53 +08:00
|
|
|
|
onEdit,
|
2025-08-04 22:16:26 +08:00
|
|
|
|
}) => {
|
2025-09-06 23:57:10 +08:00
|
|
|
|
// 提取API地址(兼容不同供应商配置:Claude env / Codex TOML)
|
2025-08-07 15:48:30 +08:00
|
|
|
|
const getApiUrl = (provider: Provider): string => {
|
|
|
|
|
|
try {
|
2025-09-06 23:57:10 +08:00
|
|
|
|
const cfg = provider.settingsConfig;
|
|
|
|
|
|
// Claude/Anthropic: 从 env 中读取
|
|
|
|
|
|
if (cfg?.env?.ANTHROPIC_BASE_URL) {
|
|
|
|
|
|
return cfg.env.ANTHROPIC_BASE_URL;
|
2025-08-07 15:48:30 +08:00
|
|
|
|
}
|
2025-09-06 23:57:10 +08:00
|
|
|
|
// Codex: 从 TOML 配置中解析 base_url
|
|
|
|
|
|
if (typeof cfg?.config === "string" && cfg.config.includes("base_url")) {
|
|
|
|
|
|
const match = cfg.config.match(/base_url\s*=\s*"([^"]+)"/);
|
|
|
|
|
|
if (match && match[1]) return match[1];
|
|
|
|
|
|
}
|
|
|
|
|
|
return "未配置官网地址";
|
2025-08-07 15:48:30 +08:00
|
|
|
|
} catch {
|
2025-08-27 11:00:53 +08:00
|
|
|
|
return "配置错误";
|
2025-08-07 15:48:30 +08:00
|
|
|
|
}
|
2025-08-27 11:00:53 +08:00
|
|
|
|
};
|
2025-08-07 15:48:30 +08:00
|
|
|
|
|
2025-08-06 09:56:27 +08:00
|
|
|
|
const handleUrlClick = async (url: string) => {
|
|
|
|
|
|
try {
|
2025-08-27 11:00:53 +08:00
|
|
|
|
await window.api.openExternal(url);
|
2025-08-06 09:56:27 +08:00
|
|
|
|
} catch (error) {
|
2025-08-27 11:00:53 +08:00
|
|
|
|
console.error("打开链接失败:", error);
|
2025-08-06 09:56:27 +08:00
|
|
|
|
}
|
2025-08-27 11:00:53 +08:00
|
|
|
|
};
|
2025-08-06 09:56:27 +08:00
|
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
|
return (
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<div className="space-y-4">
|
2025-08-04 22:16:26 +08:00
|
|
|
|
{Object.values(providers).length === 0 ? (
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
|
<div className="w-16 h-16 mx-auto mb-4 bg-[var(--color-bg-tertiary)] rounded-full flex items-center justify-center">
|
|
|
|
|
|
<Users size={24} className="text-[var(--color-text-tertiary)]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h3 className="text-lg font-medium text-[var(--color-text-primary)] mb-2">
|
|
|
|
|
|
还没有添加任何供应商
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="text-[var(--color-text-secondary)] text-sm">
|
|
|
|
|
|
点击右上角的"添加供应商"按钮开始配置您的第一个API供应商
|
|
|
|
|
|
</p>
|
2025-08-04 22:16:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<div className="space-y-3">
|
2025-08-04 22:16:26 +08:00
|
|
|
|
{Object.values(providers).map((provider) => {
|
2025-08-27 11:00:53 +08:00
|
|
|
|
const isCurrent = provider.id === currentProviderId;
|
2025-09-06 16:21:21 +08:00
|
|
|
|
const apiUrl = getApiUrl(provider);
|
2025-08-27 11:00:53 +08:00
|
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
|
return (
|
2025-08-27 11:00:53 +08:00
|
|
|
|
<div
|
|
|
|
|
|
key={provider.id}
|
2025-09-06 16:21:21 +08:00
|
|
|
|
className={`bg-white rounded-lg border p-4 transition-all duration-200 ${
|
|
|
|
|
|
isCurrent
|
|
|
|
|
|
? "border-[var(--color-primary)] ring-1 ring-[var(--color-primary)]/20 bg-[var(--color-primary)]/5"
|
|
|
|
|
|
: "border-[var(--color-border)] hover:border-[var(--color-border-hover)] hover:shadow-sm"
|
|
|
|
|
|
}`}
|
2025-08-04 22:16:26 +08:00
|
|
|
|
>
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<div className="flex items-center gap-3 mb-2">
|
|
|
|
|
|
<h3 className="font-medium text-[var(--color-text-primary)]">
|
|
|
|
|
|
{provider.name}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
{isCurrent && (
|
|
|
|
|
|
<div className="inline-flex items-center gap-1 px-2 py-1 bg-[var(--color-success)]/10 text-[var(--color-success)] rounded-md text-xs font-medium">
|
|
|
|
|
|
<CheckCircle2 size={12} />
|
|
|
|
|
|
当前使用
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-06 23:57:10 +08:00
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
2025-09-06 16:21:21 +08:00
|
|
|
|
{provider.websiteUrl ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
handleUrlClick(provider.websiteUrl!);
|
|
|
|
|
|
}}
|
2025-09-06 23:57:10 +08:00
|
|
|
|
className="inline-flex items-center gap-1 text-[var(--color-primary)] hover:opacity-90 transition-colors"
|
2025-09-06 16:21:21 +08:00
|
|
|
|
title={`访问 ${provider.websiteUrl}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{provider.websiteUrl}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : (
|
2025-09-07 11:36:09 +08:00
|
|
|
|
<span
|
|
|
|
|
|
className="text-[var(--color-text-secondary)]"
|
|
|
|
|
|
title={apiUrl}
|
|
|
|
|
|
>
|
2025-09-06 16:21:21 +08:00
|
|
|
|
{apiUrl}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-08-06 09:56:27 +08:00
|
|
|
|
</div>
|
2025-08-27 11:00:53 +08:00
|
|
|
|
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<div className="flex items-center gap-2 ml-4">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onSwitch(provider.id)}
|
|
|
|
|
|
disabled={isCurrent}
|
|
|
|
|
|
className={`inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
|
|
|
|
|
isCurrent
|
|
|
|
|
|
? "bg-[var(--color-bg-tertiary)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
|
|
|
|
|
: "bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)]"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Play size={14} />
|
|
|
|
|
|
{isCurrent ? "使用中" : "启用"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onEdit(provider.id)}
|
|
|
|
|
|
className="p-1.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
|
|
|
|
|
title="编辑供应商"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Edit3 size={16} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => onDelete(provider.id)}
|
|
|
|
|
|
disabled={isCurrent}
|
|
|
|
|
|
className={`p-1.5 rounded-md transition-colors ${
|
|
|
|
|
|
isCurrent
|
|
|
|
|
|
? "text-[var(--color-text-tertiary)] cursor-not-allowed"
|
|
|
|
|
|
: "text-[var(--color-text-secondary)] hover:text-[var(--color-error)] hover:bg-[var(--color-error-light)]"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
title="删除供应商"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 size={16} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-08-04 22:16:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-27 11:00:53 +08:00
|
|
|
|
);
|
2025-08-04 22:16:26 +08:00
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-08-27 11:00:53 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
2025-08-27 11:00:53 +08:00
|
|
|
|
export default ProviderList;
|