mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +08:00
1d44b1ba41
Signed-off-by: Hu Butui <hot123tea123@gmail.com>
320 lines
9.9 KiB
TypeScript
320 lines
9.9 KiB
TypeScript
import { useState, useCallback, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Layers } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
import { UniversalProviderCard } from "./UniversalProviderCard";
|
|
import { UniversalProviderFormModal } from "./UniversalProviderFormModal";
|
|
import { universalProvidersApi } from "@/lib/api";
|
|
import type { UniversalProvider, UniversalProvidersMap } from "@/types";
|
|
|
|
export function UniversalProviderPanel() {
|
|
const { t } = useTranslation();
|
|
|
|
// 状态
|
|
const [providers, setProviders] = useState<UniversalProvidersMap>({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
const [editingProvider, setEditingProvider] =
|
|
useState<UniversalProvider | null>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{
|
|
open: boolean;
|
|
id: string;
|
|
name: string;
|
|
}>({ open: false, id: "", name: "" });
|
|
const [syncConfirm, setSyncConfirm] = useState<{
|
|
open: boolean;
|
|
id: string;
|
|
name: string;
|
|
}>({ open: false, id: "", name: "" });
|
|
|
|
// 加载数据
|
|
const loadProviders = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await universalProvidersApi.getAll();
|
|
setProviders(data);
|
|
} catch (error) {
|
|
console.error("Failed to load universal providers:", error);
|
|
toast.error(
|
|
t("universalProvider.loadError", {
|
|
defaultValue: "加载统一供应商失败",
|
|
}),
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [t]);
|
|
|
|
useEffect(() => {
|
|
loadProviders();
|
|
}, [loadProviders]);
|
|
|
|
// 添加/编辑供应商
|
|
const handleSave = useCallback(
|
|
async (provider: UniversalProvider) => {
|
|
try {
|
|
await universalProvidersApi.upsert(provider);
|
|
|
|
// 新建模式下自动同步到各应用
|
|
if (!editingProvider) {
|
|
await universalProvidersApi.sync(provider.id);
|
|
}
|
|
|
|
toast.success(
|
|
editingProvider
|
|
? t("universalProvider.updated", {
|
|
defaultValue: "统一供应商已更新",
|
|
})
|
|
: t("universalProvider.addedAndSynced", {
|
|
defaultValue: "统一供应商已添加并同步",
|
|
}),
|
|
);
|
|
loadProviders();
|
|
setEditingProvider(null);
|
|
} catch (error) {
|
|
console.error("Failed to save universal provider:", error);
|
|
toast.error(
|
|
t("universalProvider.saveError", {
|
|
defaultValue: "保存统一供应商失败",
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
[editingProvider, loadProviders, t],
|
|
);
|
|
|
|
// 保存并同步供应商
|
|
const handleSaveAndSync = useCallback(
|
|
async (provider: UniversalProvider) => {
|
|
try {
|
|
await universalProvidersApi.upsert(provider);
|
|
await universalProvidersApi.sync(provider.id);
|
|
toast.success(
|
|
t("universalProvider.savedAndSynced", {
|
|
defaultValue: "已保存并同步到所有应用",
|
|
}),
|
|
);
|
|
loadProviders();
|
|
setEditingProvider(null);
|
|
} catch (error) {
|
|
console.error("Failed to save and sync universal provider:", error);
|
|
toast.error(
|
|
t("universalProvider.saveAndSyncError", {
|
|
defaultValue: "保存并同步失败",
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
[loadProviders, t],
|
|
);
|
|
|
|
// 删除供应商
|
|
const handleDelete = useCallback(async () => {
|
|
if (!deleteConfirm.id) return;
|
|
|
|
try {
|
|
await universalProvidersApi.delete(deleteConfirm.id);
|
|
toast.success(
|
|
t("universalProvider.deleted", { defaultValue: "统一供应商已删除" }),
|
|
);
|
|
loadProviders();
|
|
} catch (error) {
|
|
console.error("Failed to delete universal provider:", error);
|
|
toast.error(
|
|
t("universalProvider.deleteError", {
|
|
defaultValue: "删除统一供应商失败",
|
|
}),
|
|
);
|
|
} finally {
|
|
setDeleteConfirm({ open: false, id: "", name: "" });
|
|
}
|
|
}, [deleteConfirm.id, loadProviders, t]);
|
|
|
|
// 同步供应商
|
|
const handleSync = useCallback(async () => {
|
|
if (!syncConfirm.id) return;
|
|
|
|
try {
|
|
await universalProvidersApi.sync(syncConfirm.id);
|
|
toast.success(
|
|
t("universalProvider.synced", { defaultValue: "已同步到所有应用" }),
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to sync universal provider:", error);
|
|
toast.error(
|
|
t("universalProvider.syncError", {
|
|
defaultValue: "同步统一供应商失败",
|
|
}),
|
|
);
|
|
} finally {
|
|
setSyncConfirm({ open: false, id: "", name: "" });
|
|
}
|
|
}, [syncConfirm.id, t]);
|
|
|
|
// 打开同步确认
|
|
const handleSyncClick = useCallback(
|
|
(id: string) => {
|
|
const provider = providers[id];
|
|
setSyncConfirm({
|
|
open: true,
|
|
id,
|
|
name: provider?.name || id,
|
|
});
|
|
},
|
|
[providers],
|
|
);
|
|
|
|
// 复制供应商
|
|
const handleDuplicate = useCallback(
|
|
async (provider: UniversalProvider) => {
|
|
const duplicated: UniversalProvider = {
|
|
...JSON.parse(JSON.stringify(provider)),
|
|
id: crypto.randomUUID(),
|
|
name: `${provider.name} copy`,
|
|
createdAt: Date.now(),
|
|
};
|
|
try {
|
|
await universalProvidersApi.upsert(duplicated);
|
|
await universalProvidersApi.sync(duplicated.id);
|
|
toast.success(
|
|
t("universalProvider.duplicatedAndSynced", {
|
|
defaultValue: "统一供应商已复制并同步",
|
|
}),
|
|
);
|
|
loadProviders();
|
|
} catch (error) {
|
|
console.error("Failed to duplicate universal provider:", error);
|
|
toast.error(
|
|
t("universalProvider.duplicateError", {
|
|
defaultValue: "复制统一供应商失败",
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
[loadProviders, t],
|
|
);
|
|
|
|
// 打开编辑
|
|
const handleEdit = useCallback((provider: UniversalProvider) => {
|
|
setEditingProvider(provider);
|
|
setIsFormOpen(true);
|
|
}, []);
|
|
|
|
// 打开删除确认
|
|
const handleDeleteClick = useCallback(
|
|
(id: string) => {
|
|
const provider = providers[id];
|
|
setDeleteConfirm({
|
|
open: true,
|
|
id,
|
|
name: provider?.name || id,
|
|
});
|
|
},
|
|
[providers],
|
|
);
|
|
|
|
const providerList = Object.values(providers);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 头部 */}
|
|
<div className="flex items-center gap-2">
|
|
<Layers className="h-5 w-5 text-primary" />
|
|
<h2 className="text-lg font-semibold">
|
|
{t("universalProvider.title", { defaultValue: "统一供应商" })}
|
|
</h2>
|
|
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
|
{providerList.length}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 描述 */}
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("universalProvider.description", {
|
|
defaultValue:
|
|
"统一供应商可以同时管理 Claude、Codex 和 Gemini 的配置。修改后会自动同步到所有启用的应用。",
|
|
})}
|
|
</p>
|
|
|
|
{/* 供应商列表 */}
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
</div>
|
|
) : providerList.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed py-12 text-center">
|
|
<Layers className="mb-3 h-10 w-10 text-muted-foreground/50" />
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("universalProvider.empty", {
|
|
defaultValue: "还没有统一供应商",
|
|
})}
|
|
</p>
|
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
|
{t("universalProvider.emptyHint", {
|
|
defaultValue: "点击下方「添加统一供应商」按钮创建一个",
|
|
})}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{providerList.map((provider) => (
|
|
<UniversalProviderCard
|
|
key={provider.id}
|
|
provider={provider}
|
|
onEdit={handleEdit}
|
|
onDelete={handleDeleteClick}
|
|
onSync={handleSyncClick}
|
|
onDuplicate={handleDuplicate}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 表单模态框 */}
|
|
<UniversalProviderFormModal
|
|
isOpen={isFormOpen}
|
|
onClose={() => {
|
|
setIsFormOpen(false);
|
|
setEditingProvider(null);
|
|
}}
|
|
onSave={handleSave}
|
|
onSaveAndSync={handleSaveAndSync}
|
|
editingProvider={editingProvider}
|
|
/>
|
|
|
|
{/* 删除确认对话框 */}
|
|
<ConfirmDialog
|
|
isOpen={deleteConfirm.open}
|
|
title={t("universalProvider.deleteConfirmTitle", {
|
|
defaultValue: "删除统一供应商",
|
|
})}
|
|
message={t("universalProvider.deleteConfirmDescription", {
|
|
defaultValue: `确定要删除 "${deleteConfirm.name}" 吗?这将同时删除它在各应用中生成的供应商配置。`,
|
|
name: deleteConfirm.name,
|
|
})}
|
|
confirmText={t("common.delete", { defaultValue: "删除" })}
|
|
onConfirm={handleDelete}
|
|
onCancel={() => setDeleteConfirm({ open: false, id: "", name: "" })}
|
|
/>
|
|
|
|
{/* 同步确认对话框 */}
|
|
<ConfirmDialog
|
|
isOpen={syncConfirm.open}
|
|
title={t("universalProvider.syncConfirmTitle", {
|
|
defaultValue: "同步统一供应商",
|
|
})}
|
|
message={t("universalProvider.syncConfirmDescription", {
|
|
defaultValue: `同步 "${syncConfirm.name}" 将会覆盖 Claude、Codex 和 Gemini 中关联的供应商配置。确定要继续吗?`,
|
|
name: syncConfirm.name,
|
|
})}
|
|
confirmText={t("universalProvider.syncConfirm", {
|
|
defaultValue: "同步",
|
|
})}
|
|
onConfirm={handleSync}
|
|
onCancel={() => setSyncConfirm({ open: false, id: "", name: "" })}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|