feat: complete stage 2 core refactor
This commit is contained in:
77
src/components/providers/AddProviderDialog.tsx
Normal file
77
src/components/providers/AddProviderDialog.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { Provider } from "@/types";
|
||||
import type { AppType } from "@/lib/api";
|
||||
import {
|
||||
ProviderForm,
|
||||
type ProviderFormValues,
|
||||
} from "@/components/providers/forms/ProviderForm";
|
||||
|
||||
interface AddProviderDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
appType: AppType;
|
||||
onSubmit: (provider: Omit<Provider, "id">) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function AddProviderDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
appType,
|
||||
onSubmit,
|
||||
}: AddProviderDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (values: ProviderFormValues) => {
|
||||
const parsedConfig = JSON.parse(values.settingsConfig) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const providerData: Omit<Provider, "id"> = {
|
||||
name: values.name.trim(),
|
||||
websiteUrl: values.websiteUrl?.trim() || undefined,
|
||||
settingsConfig: parsedConfig,
|
||||
meta: {},
|
||||
};
|
||||
|
||||
await onSubmit(providerData);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[onSubmit, onOpenChange],
|
||||
);
|
||||
|
||||
const submitLabel =
|
||||
appType === "claude"
|
||||
? t("provider.addClaudeProvider", { defaultValue: "添加 Claude 供应商" })
|
||||
: t("provider.addCodexProvider", { defaultValue: "添加 Codex 供应商" });
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{submitLabel}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("provider.addDescription", {
|
||||
defaultValue: "填写信息后即可在列表中快速切换供应商。",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ProviderForm
|
||||
submitLabel={t("common.add", { defaultValue: "添加" })}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
84
src/components/providers/EditProviderDialog.tsx
Normal file
84
src/components/providers/EditProviderDialog.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { Provider } from "@/types";
|
||||
import {
|
||||
ProviderForm,
|
||||
type ProviderFormValues,
|
||||
} from "@/components/providers/forms/ProviderForm";
|
||||
|
||||
interface EditProviderDialogProps {
|
||||
open: boolean;
|
||||
provider: Provider | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (provider: Provider) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function EditProviderDialog({
|
||||
open,
|
||||
provider,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
}: EditProviderDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (values: ProviderFormValues) => {
|
||||
if (!provider) return;
|
||||
|
||||
const parsedConfig = JSON.parse(values.settingsConfig) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const updatedProvider: Provider = {
|
||||
...provider,
|
||||
name: values.name.trim(),
|
||||
websiteUrl: values.websiteUrl?.trim() || undefined,
|
||||
settingsConfig: parsedConfig,
|
||||
};
|
||||
|
||||
await onSubmit(updatedProvider);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[onSubmit, onOpenChange, provider],
|
||||
);
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("provider.editProvider", { defaultValue: "编辑供应商" })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("provider.editDescription", {
|
||||
defaultValue: "更新配置后将立即应用到当前供应商。",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ProviderForm
|
||||
submitLabel={t("common.save", { defaultValue: "保存" })}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
initialData={{
|
||||
name: provider.name,
|
||||
websiteUrl: provider.websiteUrl,
|
||||
settingsConfig: provider.settingsConfig,
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
75
src/components/providers/ProviderActions.tsx
Normal file
75
src/components/providers/ProviderActions.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { BarChart3, Check, Play, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ProviderActionsProps {
|
||||
isCurrent: boolean;
|
||||
onSwitch: () => void;
|
||||
onEdit: () => void;
|
||||
onConfigureUsage: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export function ProviderActions({
|
||||
isCurrent,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onConfigureUsage,
|
||||
onDelete,
|
||||
}: ProviderActionsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isCurrent ? "secondary" : "default"}
|
||||
onClick={onSwitch}
|
||||
disabled={isCurrent}
|
||||
className={cn(
|
||||
"w-[96px]",
|
||||
isCurrent && "text-muted-foreground hover:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isCurrent ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
{t("provider.inUse", { defaultValue: "已启用" })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
{t("provider.enable", { defaultValue: "启用" })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button size="sm" variant="outline" onClick={onEdit}>
|
||||
{t("common.edit", { defaultValue: "编辑" })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onConfigureUsage}
|
||||
title={t("provider.configureUsage", { defaultValue: "配置用量查询" })}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onDelete}
|
||||
disabled={isCurrent}
|
||||
className={cn(
|
||||
"text-destructive hover:text-destructive",
|
||||
isCurrent && "text-muted-foreground hover:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
155
src/components/providers/ProviderCard.tsx
Normal file
155
src/components/providers/ProviderCard.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useMemo } from "react";
|
||||
import { GripVertical, Link } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type {
|
||||
DraggableAttributes,
|
||||
DraggableSyntheticListeners,
|
||||
} from "@dnd-kit/core";
|
||||
import type { Provider } from "@/types";
|
||||
import type { AppType } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ProviderActions } from "@/components/providers/ProviderActions";
|
||||
import UsageFooter from "@/components/UsageFooter";
|
||||
|
||||
interface DragHandleProps {
|
||||
attributes: DraggableAttributes;
|
||||
listeners: DraggableSyntheticListeners;
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
interface ProviderCardProps {
|
||||
provider: Provider;
|
||||
isCurrent: boolean;
|
||||
appType: AppType;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
onConfigureUsage: (provider: Provider) => void;
|
||||
onOpenWebsite: (url: string) => void;
|
||||
dragHandleProps?: DragHandleProps;
|
||||
}
|
||||
|
||||
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
||||
if (provider.websiteUrl) {
|
||||
return provider.websiteUrl;
|
||||
}
|
||||
|
||||
const config = provider.settingsConfig;
|
||||
|
||||
if (config && typeof config === "object") {
|
||||
const envBase = (config as Record<string, any>)?.env?.ANTHROPIC_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,
|
||||
appType,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onConfigureUsage,
|
||||
onOpenWebsite,
|
||||
dragHandleProps,
|
||||
}: ProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fallbackUrlText = t("provider.notConfigured", {
|
||||
defaultValue: "未配置接口地址",
|
||||
});
|
||||
|
||||
const displayUrl = useMemo(() => {
|
||||
return extractApiUrl(provider, fallbackUrlText);
|
||||
}, [provider, fallbackUrlText]);
|
||||
|
||||
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
||||
|
||||
const handleOpenWebsite = () => {
|
||||
if (!displayUrl || displayUrl === fallbackUrlText) {
|
||||
return;
|
||||
}
|
||||
onOpenWebsite(displayUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border bg-card p-4 shadow-sm transition-[box-shadow,transform] duration-200",
|
||||
isCurrent
|
||||
? "border-primary/70 bg-primary/5"
|
||||
: "border-border hover:border-primary/40",
|
||||
dragHandleProps?.isDragging && "cursor-grabbing border-primary/60 shadow-lg",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex flex-1 items-start gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"mt-1 flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-muted-foreground transition-colors hover:border-muted hover:text-foreground",
|
||||
dragHandleProps?.isDragging && "border-primary text-primary",
|
||||
)}
|
||||
aria-label={t("provider.dragHandle", { defaultValue: "拖拽排序" })}
|
||||
{...(dragHandleProps?.attributes ?? {})}
|
||||
{...(dragHandleProps?.listeners ?? {})}
|
||||
>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-base font-semibold leading-none">
|
||||
{provider.name}
|
||||
</h3>
|
||||
{isCurrent && (
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
{t("provider.currentlyUsing", { defaultValue: "当前使用" })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenWebsite}
|
||||
className="inline-flex items-center gap-1 text-sm text-primary transition-colors hover:underline"
|
||||
title={displayUrl}
|
||||
>
|
||||
<Link className="h-3.5 w-3.5" />
|
||||
<span className="truncate">{displayUrl}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProviderActions
|
||||
isCurrent={isCurrent}
|
||||
onSwitch={() => onSwitch(provider)}
|
||||
onEdit={() => onEdit(provider)}
|
||||
onConfigureUsage={() => onConfigureUsage(provider)}
|
||||
onDelete={() => onDelete(provider)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UsageFooter
|
||||
providerId={provider.id}
|
||||
appType={appType}
|
||||
usageEnabled={usageEnabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/components/providers/ProviderEmptyState.tsx
Normal file
32
src/components/providers/ProviderEmptyState.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Users } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface ProviderEmptyStateProps {
|
||||
onCreate?: () => void;
|
||||
}
|
||||
|
||||
export function ProviderEmptyState({ onCreate }: ProviderEmptyStateProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-muted-foreground/30 p-10 text-center">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
||||
<Users className="h-7 w-7 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t("provider.noProviders", { defaultValue: "暂无供应商" })}
|
||||
</h3>
|
||||
<p className="mt-2 max-w-sm text-sm text-muted-foreground">
|
||||
{t("provider.noProvidersDescription", {
|
||||
defaultValue: "开始添加一个供应商以快速完成切换。",
|
||||
})}
|
||||
</p>
|
||||
{onCreate && (
|
||||
<Button className="mt-6" onClick={onCreate}>
|
||||
{t("provider.addProvider", { defaultValue: "添加供应商" })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
src/components/providers/ProviderList.tsx
Normal file
153
src/components/providers/ProviderList.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import type { CSSProperties } from "react";
|
||||
import type { Provider } from "@/types";
|
||||
import type { AppType } from "@/lib/api";
|
||||
import { useDragSort } from "@/hooks/useDragSort";
|
||||
import { ProviderCard } from "@/components/providers/ProviderCard";
|
||||
import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState";
|
||||
|
||||
interface ProviderListProps {
|
||||
providers: Record<string, Provider>;
|
||||
currentProviderId: string;
|
||||
appType: AppType;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
onConfigureUsage?: (provider: Provider) => void;
|
||||
onOpenWebsite: (url: string) => void;
|
||||
onCreate?: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function ProviderList({
|
||||
providers,
|
||||
currentProviderId,
|
||||
appType,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onConfigureUsage,
|
||||
onOpenWebsite,
|
||||
onCreate,
|
||||
isLoading = false,
|
||||
}: ProviderListProps) {
|
||||
const { sortedProviders, sensors, handleDragEnd } = useDragSort(
|
||||
providers,
|
||||
appType,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{[0, 1, 2].map((index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-28 w-full rounded-lg border border-dashed border-muted-foreground/40 bg-muted/40"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortedProviders.length === 0) {
|
||||
return <ProviderEmptyState onCreate={onCreate} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={sortedProviders.map((provider) => provider.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{sortedProviders.map((provider) => (
|
||||
<SortableProviderCard
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
isCurrent={provider.id === currentProviderId}
|
||||
appType={appType}
|
||||
onSwitch={onSwitch}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onConfigureUsage={onConfigureUsage}
|
||||
onOpenWebsite={onOpenWebsite}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
interface SortableProviderCardProps {
|
||||
provider: Provider;
|
||||
isCurrent: boolean;
|
||||
appType: AppType;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
onConfigureUsage?: (provider: Provider) => void;
|
||||
onOpenWebsite: (url: string) => void;
|
||||
}
|
||||
|
||||
function SortableProviderCard({
|
||||
provider,
|
||||
isCurrent,
|
||||
appType,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onConfigureUsage,
|
||||
onOpenWebsite,
|
||||
}: SortableProviderCardProps) {
|
||||
const {
|
||||
setNodeRef,
|
||||
attributes,
|
||||
listeners,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: provider.id });
|
||||
|
||||
const style: CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
<ProviderCard
|
||||
provider={provider}
|
||||
isCurrent={isCurrent}
|
||||
appType={appType}
|
||||
onSwitch={onSwitch}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onConfigureUsage={
|
||||
onConfigureUsage
|
||||
? (item) => onConfigureUsage(item)
|
||||
: () => undefined
|
||||
}
|
||||
onOpenWebsite={onOpenWebsite}
|
||||
dragHandleProps={{
|
||||
attributes,
|
||||
listeners,
|
||||
isDragging,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
166
src/components/providers/forms/ProviderForm.tsx
Normal file
166
src/components/providers/forms/ProviderForm.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
import {
|
||||
providerSchema,
|
||||
type ProviderFormData,
|
||||
} from "@/lib/schemas/provider";
|
||||
|
||||
interface ProviderFormProps {
|
||||
submitLabel: string;
|
||||
onSubmit: (values: ProviderFormData) => void;
|
||||
onCancel: () => void;
|
||||
initialData?: {
|
||||
name?: string;
|
||||
websiteUrl?: string;
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG_PLACEHOLDER = `{
|
||||
"env": {},
|
||||
"config": {}
|
||||
}`;
|
||||
|
||||
export function ProviderForm({
|
||||
submitLabel,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
initialData,
|
||||
}: ProviderFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const defaultValues: ProviderFormData = useMemo(
|
||||
() => ({
|
||||
name: initialData?.name ?? "",
|
||||
websiteUrl: initialData?.websiteUrl ?? "",
|
||||
settingsConfig: initialData?.settingsConfig
|
||||
? JSON.stringify(initialData.settingsConfig, null, 2)
|
||||
: DEFAULT_CONFIG_PLACEHOLDER,
|
||||
}),
|
||||
[initialData],
|
||||
);
|
||||
|
||||
const form = useForm<ProviderFormData>({
|
||||
resolver: zodResolver(providerSchema),
|
||||
defaultValues,
|
||||
mode: "onSubmit",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(defaultValues);
|
||||
}, [defaultValues, form]);
|
||||
|
||||
const isDarkMode = useMemo(() => {
|
||||
if (theme === "dark") return true;
|
||||
if (theme === "light") return false;
|
||||
return typeof window !== "undefined"
|
||||
? window.document.documentElement.classList.contains("dark")
|
||||
: false;
|
||||
}, [theme]);
|
||||
|
||||
const handleSubmit = (values: ProviderFormData) => {
|
||||
onSubmit({
|
||||
...values,
|
||||
websiteUrl: values.websiteUrl?.trim() ?? "",
|
||||
settingsConfig: values.settingsConfig.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("provider.name", { defaultValue: "供应商名称" })}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("provider.namePlaceholder", {
|
||||
defaultValue: "例如:Claude 官方",
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="websiteUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("provider.websiteUrl", { defaultValue: "官网链接" })}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("provider.configJson", { defaultValue: "配置 JSON" })}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="rounded-md border">
|
||||
<JsonEditor
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={DEFAULT_CONFIG_PLACEHOLDER}
|
||||
darkMode={isDarkMode}
|
||||
rows={14}
|
||||
showValidation
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" type="button" onClick={onCancel}>
|
||||
{t("common.cancel", { defaultValue: "取消" })}
|
||||
</Button>
|
||||
<Button type="submit">{submitLabel}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export type ProviderFormValues = ProviderFormData;
|
||||
Reference in New Issue
Block a user