diff --git a/app/api/endpoint-groups/[id]/copy/route.ts b/app/api/endpoint-groups/[id]/copy/route.ts new file mode 100644 index 0000000..757df83 --- /dev/null +++ b/app/api/endpoint-groups/[id]/copy/route.ts @@ -0,0 +1,81 @@ +import { NextResponse } from "next/server" +import { auth } from "@/lib/auth" +import { getDb } from "@/lib/db" +import { endpointGroups, endpointToGroup } from "@/lib/db/schema/endpoint-groups" +import { eq, and } from "drizzle-orm" +import { generateId } from "@/lib/utils" +import { z } from "zod" + +export const runtime = 'edge' + +const copyEndpointGroupSchema = z.object({ + name: z.string().min(1, "名称不能为空"), + status: z.enum(["active", "inactive"]).optional(), +}) + +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth() + + const db = await getDb() + const { id } = await params + + const originalGroup = await db.query.endpointGroups.findFirst({ + where: and( + eq(endpointGroups.id, id), + eq(endpointGroups.userId, session!.user!.id!) + ) + }) + + if (!originalGroup) { + return NextResponse.json( + { error: "接口组不存在或无权访问" }, + { status: 404 } + ) + } + + const body = await request.json() + const { name, status } = copyEndpointGroupSchema.parse(body) + + const relations = await db.query.endpointToGroup.findMany({ + where: eq(endpointToGroup.groupId, id) + }) + + const newGroupId = generateId() + + await db.insert(endpointGroups).values({ + id: newGroupId, + name, + userId: session!.user!.id!, + status: status ?? "inactive", + createdAt: new Date(), + updatedAt: new Date(), + }) + + if (relations.length > 0) { + await db.insert(endpointToGroup).values( + relations.map(relation => ({ + endpointId: relation.endpointId, + groupId: newGroupId, + })) + ) + } + + return NextResponse.json({ id: newGroupId }) + } catch (error) { + console.error('复制接口组失败:', error) + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: error.errors[0].message }, + { status: 400 } + ) + } + return NextResponse.json( + { error: '复制接口组失败' }, + { status: 500 } + ) + } +} diff --git a/app/api/endpoint-groups/[id]/route.ts b/app/api/endpoint-groups/[id]/route.ts index e5814b1..80b7cef 100644 --- a/app/api/endpoint-groups/[id]/route.ts +++ b/app/api/endpoint-groups/[id]/route.ts @@ -3,9 +3,77 @@ import { auth } from "@/lib/auth" import { getDb } from "@/lib/db" import { endpointGroups, endpointToGroup } from "@/lib/db/schema/endpoint-groups" import { eq } from "drizzle-orm" +import { z } from "zod" export const runtime = 'edge' +const updateEndpointGroupSchema = z.object({ + name: z.string().min(1, "名称不能为空"), + endpointIds: z.array(z.string()).min(1, "至少需要一个接口"), +}) + +export async function PUT( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth() + + const db = await getDb() + const { id } = await params + + const group = await db.query.endpointGroups.findFirst({ + where: (groups, { and, eq }) => and( + eq(groups.id, id), + eq(groups.userId, session!.user!.id!) + ) + }) + + if (!group) { + return NextResponse.json( + { error: "接口组不存在或无权访问" }, + { status: 404 } + ) + } + + const body = await request.json() + const validatedData = updateEndpointGroupSchema.parse(body) + + // 更新接口组名称 + await db.update(endpointGroups) + .set({ + name: validatedData.name, + updatedAt: new Date(), + }) + .where(eq(endpointGroups.id, id)) + + // 删除旧的关联关系 + await db.delete(endpointToGroup).where(eq(endpointToGroup.groupId, id)) + + // 添加新的关联关系 + await db.insert(endpointToGroup).values( + validatedData.endpointIds.map(endpointId => ({ + groupId: id, + endpointId, + })) + ) + + return NextResponse.json({ success: true, id }) + } catch (error) { + console.error('更新接口组失败:', error) + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: error.errors[0].message }, + { status: 400 } + ) + } + return NextResponse.json( + { error: '更新接口组失败' }, + { status: 500 } + ) + } +} + export async function DELETE( request: Request, { params }: { params: Promise<{ id: string }> } diff --git a/app/api/endpoints/[endpointId]/copy/route.ts b/app/api/endpoints/[endpointId]/copy/route.ts new file mode 100644 index 0000000..7d7cb1d --- /dev/null +++ b/app/api/endpoints/[endpointId]/copy/route.ts @@ -0,0 +1,61 @@ +import { auth } from "@/lib/auth" +import { getDb } from "@/lib/db" +import { endpoints, insertEndpointSchema } from "@/lib/db/schema/endpoints" +import { and, eq } from "drizzle-orm" +import { NextResponse } from "next/server" +import { generateId } from "@/lib/utils" +import { z } from "zod" + +export const runtime = "edge" + +const copyEndpointSchema = z.object({ + name: z.string().min(1, "名称不能为空"), + status: z.enum(["active", "inactive"]).optional(), +}) + +export async function POST( + req: Request, + { params }: { params: Promise<{ endpointId: string }> } +) { + try { + const db = await getDb() + const session = await auth() + if (!session?.user) { + return new NextResponse("Unauthorized", { status: 401 }) + } + + const { endpointId } = await params + const body = await req.json() + const { name, status } = copyEndpointSchema.parse(body) + + const originalEndpoint = await db.query.endpoints.findFirst({ + where: and( + eq(endpoints.id, endpointId), + eq(endpoints.userId, session.user.id!) + ), + }) + + if (!originalEndpoint) { + return new NextResponse("Not found", { status: 404 }) + } + + const newEndpoint = insertEndpointSchema.parse({ + id: generateId(), + name, + channelId: originalEndpoint.channelId, + rule: originalEndpoint.rule, + userId: session.user.id!, + status: status ?? "inactive", + }) + + const created = await db.insert(endpoints).values(newEndpoint as any).returning() + + return NextResponse.json(created[0]) + } catch (error) { + if (error instanceof z.ZodError) { + return new NextResponse(error.message, { status: 400 }) + } + console.error("[ENDPOINT_COPY]", error) + return new NextResponse("Internal Error", { status: 500 }) + } +} diff --git a/components/endpoint-group-dialog.tsx b/components/endpoint-group-dialog.tsx new file mode 100644 index 0000000..b95a84e --- /dev/null +++ b/components/endpoint-group-dialog.tsx @@ -0,0 +1,219 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Plus, Loader2 } from "lucide-react" +import { useState, useEffect } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { useToast } from "@/components/ui/use-toast" +import { DropdownMenuItem } from "@/components/ui/dropdown-menu" +import { createEndpointGroup, updateEndpointGroup } from "@/lib/services/endpoint-groups" +import { useRouter } from "next/navigation" +import { EndpointGroupWithEndpoints } from "@/types/endpoint-group" +import { Endpoint } from "@/lib/db/schema/endpoints" +import { Checkbox } from "@/components/ui/checkbox" + +const endpointGroupSchema = z.object({ + name: z.string().min(1, "名称不能为空"), + endpointIds: z.array(z.string()).min(1, "至少需要选择一个接口"), +}) + +type EndpointGroupFormValues = z.infer + +interface EndpointGroupDialogProps { + mode?: "create" | "edit" + group?: EndpointGroupWithEndpoints + availableEndpoints: Endpoint[] + icon?: React.ReactNode + onSuccess?: () => void +} + +export function EndpointGroupDialog({ + mode = "create", + group, + availableEndpoints, + icon, + onSuccess, +}: EndpointGroupDialogProps) { + const [open, setOpen] = useState(false) + const [isPending, setIsPending] = useState(false) + const { toast } = useToast() + const router = useRouter() + + const form = useForm({ + resolver: zodResolver(endpointGroupSchema), + defaultValues: { + name: "", + endpointIds: [], + }, + }) + + useEffect(() => { + if (group && open && mode === "edit") { + form.reset({ + name: group.name, + endpointIds: group.endpointIds, + }) + } else if (mode === "create" && open) { + form.reset({ + name: "", + endpointIds: [], + }) + } + }, [group, open, mode, form]) + + const toggleEndpoint = (endpointId: string) => { + const currentIds = form.getValues("endpointIds") + const newIds = currentIds.includes(endpointId) + ? currentIds.filter(id => id !== endpointId) + : [...currentIds, endpointId] + form.setValue("endpointIds", newIds, { shouldValidate: true }) + } + + async function onSubmit(data: EndpointGroupFormValues) { + try { + setIsPending(true) + if (mode === "edit" && group) { + await updateEndpointGroup(group.id, data) + toast({ description: "接口组已更新" }) + } else { + await createEndpointGroup(data) + toast({ description: "接口组已创建" }) + } + setOpen(false) + form.reset() + onSuccess?.() + router.refresh() + } catch (error) { + console.error('Endpoint group dialog error:', error) + toast({ + variant: "destructive", + description: error instanceof Error ? error.message : (mode === "edit" ? "更新失败,请重试" : "创建失败,请重试") + }) + } finally { + setIsPending(false) + } + } + + const selectedCount = form.watch("endpointIds")?.length || 0 + + return ( + + + {mode === "edit" ? ( + e.preventDefault()}> + {icon} + 编辑 + + ) : ( + + )} + + + + + {mode === "edit" ? "编辑接口组" : "新建接口组"} + + + {mode === "edit" ? "修改现有的接口组" : "创建一个包含多个接口的接口组"} + + +
+
+ + ( + + + 接口组名称 + * + + + + + + + )} + /> + + ( + + + 选择接口 + * + +
+ 已选择 {selectedCount} 个接口 +
+
+
+ {availableEndpoints.length === 0 ? ( +
+ 暂无可用接口,请先创建推送接口 +
+ ) : ( + availableEndpoints.map(endpoint => ( +
+ toggleEndpoint(endpoint.id)} + /> +
+
{endpoint.name}
+
{endpoint.id}
+
+
+ )) + )} +
+
+ +
+ )} + /> + +
+ + +
+ +
+ +
+
+ ) +} diff --git a/components/endpoint-group-example.tsx b/components/endpoint-group-example.tsx index 1c49fdb..dbab5d7 100644 --- a/components/endpoint-group-example.tsx +++ b/components/endpoint-group-example.tsx @@ -10,7 +10,7 @@ import { DialogTitle } from "@/components/ui/dialog" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { generateExampleBody } from "@/lib/utils" +import { generateExampleBody } from "@/lib/generator" interface EndpointGroupExampleProps { group: EndpointGroupWithEndpoints | null diff --git a/components/endpoint-group-table.tsx b/components/endpoint-group-table.tsx index 5e1ab12..4b8093d 100644 --- a/components/endpoint-group-table.tsx +++ b/components/endpoint-group-table.tsx @@ -1,7 +1,7 @@ "use client" import { useState } from "react" -import { Loader2, Trash, Eye, Power, Send } from "lucide-react" +import { Loader2, Trash, Eye, Power, Send, Pencil, Copy } from "lucide-react" import { Table, @@ -21,12 +21,21 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useToast } from "@/components/ui/use-toast" import { EndpointGroupWithEndpoints } from "@/types/endpoint-group" -import { deleteEndpointGroup, toggleEndpointGroupStatus, testEndpointGroup } from "@/lib/services/endpoint-groups" +import { deleteEndpointGroup, toggleEndpointGroupStatus, testEndpointGroup, copyEndpointGroup } from "@/lib/services/endpoint-groups" import { formatDate } from "@/lib/utils" +import { generateExampleBody } from "@/lib/generator" import { EndpointGroupExample } from "./endpoint-group-example" import { DropdownMenu, @@ -35,13 +44,17 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { MoreHorizontal } from "lucide-react" +import { EndpointGroupDialog } from "./endpoint-group-dialog" +import { Endpoint } from "@/lib/db/schema/endpoints" +import { TestPushDialog } from "./test-push-dialog" interface EndpointGroupTableProps { groups: EndpointGroupWithEndpoints[] + availableEndpoints: Endpoint[] onGroupsUpdate: () => void } -export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTableProps) { +export function EndpointGroupTable({ groups, availableEndpoints, onGroupsUpdate }: EndpointGroupTableProps) { const [searchQuery, setSearchQuery] = useState("") const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [groupToDelete, setGroupToDelete] = useState(null) @@ -49,6 +62,14 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl const [viewExample, setViewExample] = useState(null) const [isLoading, setIsLoading] = useState(null) const [isTesting, setIsTesting] = useState(null) + const [copyDialogOpen, setCopyDialogOpen] = useState(false) + const [groupToCopy, setGroupToCopy] = useState(null) + const [isCopying, setIsCopying] = useState(false) + const [copyName, setCopyName] = useState("") + const [copyStatus, setCopyStatus] = useState<"active" | "inactive">("inactive") + const [testDialogOpen, setTestDialogOpen] = useState(false) + const [groupToTest, setGroupToTest] = useState(null) + const [testInitialContent, setTestInitialContent] = useState("") const { toast } = useToast() const filteredGroups = groups.filter((group) => { @@ -103,28 +124,52 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl } } - const handleTest = async (group: EndpointGroupWithEndpoints) => { - if (group.endpoints.length === 0) { + const handleCopy = async () => { + if (!groupToCopy || !copyName.trim()) return + + try { + setIsCopying(true) + await copyEndpointGroup(groupToCopy.id, copyName, copyStatus) + onGroupsUpdate() + toast({ description: "接口组已复制" }) + setCopyDialogOpen(false) + setCopyName("") + setCopyStatus("inactive") + } catch (error) { + console.error('Error copying endpoint group:', error) + toast({ + variant: "destructive", + description: error instanceof Error ? error.message : "复制失败,请重试" + }) + } finally { + setIsCopying(false) + } + } + + const handleTest = async (testData: any) => { + if (!groupToTest) return + + if (groupToTest.endpoints.length === 0) { toast({ variant: "destructive", description: "接口组内没有接口,无法测试" }) - return + throw new Error("接口组内没有接口") } // 检查是否所有接口都有规则 - const hasInvalidRule = group.endpoints.some(e => !e.rule) + const hasInvalidRule = groupToTest.endpoints.some(e => !e.rule) if (hasInvalidRule) { toast({ variant: "destructive", description: "接口组中存在未配置规则的接口" }) - return + throw new Error("接口组中存在未配置规则的接口") } - setIsTesting(group.id) + setIsTesting(groupToTest.id) try { - const result = await testEndpointGroup(group) + const result = await testEndpointGroup(groupToTest, testData) toast({ title: "测试结果", description: `成功: ${result.successCount}, 失败: ${result.failedCount}`, @@ -135,6 +180,7 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl variant: "destructive", description: error instanceof Error ? error.message : "测试失败" }) + throw error } finally { setIsTesting(null) } @@ -147,6 +193,31 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl : "bg-red-50 text-red-700 ring-1 ring-inset ring-red-600/20" }` } + + const getEndpointCountDisplay = (group: EndpointGroupWithEndpoints) => { + const totalCount = group.endpoints.length + const activeCount = group.endpoints.filter(e => e.status === "active").length + const inactiveCount = totalCount - activeCount + + if (inactiveCount === 0) { + return {totalCount} + } + + return ( + + + + + {activeCount} / {totalCount} + + + +

{inactiveCount} 个接口已禁用

+
+
+
+ ) + } return (
@@ -159,6 +230,10 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl className="h-9" />
+
@@ -187,7 +262,7 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl {group.id} {group.name} - {group.endpoints.length} + {getEndpointCountDisplay(group)} {group.status === "active" ? "启用" : "禁用"} @@ -207,16 +282,43 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl 查看示例 handleTest(group)} - disabled={isTesting === group.id || group.status === "inactive"} + onClick={() => { + setGroupToTest(group) + const allRules = group.endpoints.flatMap(e => e.rule ? [e.rule] : []) + const combinedRule = allRules.join('\n') + const exampleBody = generateExampleBody(allRules.length > 0 ? combinedRule : '{}') + // 如果任一规则包含 ${body}(不带点号),且生成的示例体为空或只有默认消息,则使用纯文本 + const hasBodyOnly = allRules.some(rule => rule.includes('${body}') && !rule.includes('${body.')) + if (hasBodyOnly && Object.keys(exampleBody).length <= 1) { + setTestInitialContent("示例消息内容") + } else { + setTestInitialContent(JSON.stringify(exampleBody, null, 4)) + } + setTestDialogOpen(true) + }} + disabled={group.status === "inactive"} > - {isTesting === group.id ? ( - - ) : ( - - )} + 测试推送 + } + /> + { + setGroupToCopy(group) + setCopyName(`${group.name}-副本`) + setCopyStatus(group.status) + setCopyDialogOpen(true) + }} + > + + 复制 + handleToggleStatus(group.id)} disabled={isLoading === group.id} @@ -268,6 +370,70 @@ export function EndpointGroupTable({ groups, onGroupsUpdate }: EndpointGroupTabl + + + + + 复制接口组 + + 复制接口组: {groupToCopy?.name} + + +
+
+ + setCopyName(e.target.value)} + /> +
+
+ +
+ setCopyStatus(checked ? "active" : "inactive")} + /> +
+
+
+ + 取消 + + {isCopying && } + 确认复制 + + +
+
+ + + 接口组: {groupToTest?.name} +
+ 包含 {groupToTest?.endpoints.length} 个接口 +
+ 您可以修改下方的测试内容,支持 JSON 对象或纯文本格式。 + + } + initialContent={testInitialContent} + isTesting={isTesting === groupToTest?.id} + onTest={handleTest} + /> (null) const [isDeleting, setIsDeleting] = useState(false) const [isTesting, setIsTesting] = useState(null) + const [copyDialogOpen, setCopyDialogOpen] = useState(false) + const [endpointToCopy, setEndpointToCopy] = useState(null) + const [isCopying, setIsCopying] = useState(false) + const [copyName, setCopyName] = useState("") + const [copyStatus, setCopyStatus] = useState<"active" | "inactive">("inactive") const { toast } = useToast() const [viewExample, setViewExample] = useState(null) const router = useRouter() const [selectedEndpoints, setSelectedEndpoints] = useState([]) const [createGroupDialogOpen, setCreateGroupDialogOpen] = useState(false) const [isLoading, setIsLoading] = useState(null) + const [testDialogOpen, setTestDialogOpen] = useState(false) + const [endpointToTest, setEndpointToTest] = useState(null) + const [testInitialContent, setTestInitialContent] = useState("") const filteredEndpoints = endpoints?.filter((endpoint) => { if (!searchQuery.trim()) return true @@ -125,12 +137,38 @@ export function EndpointTable({ } } - async function handleTest(endpoint: Endpoint) { - setIsTesting(endpoint.id) + const handleCopy = async () => { + if (!endpointToCopy || !copyName.trim()) return + + try { + setIsCopying(true) + await copyEndpoint(endpointToCopy.id, copyName, copyStatus) + onEndpointsUpdate() + toast({ description: "接口已复制" }) + setCopyDialogOpen(false) + setCopyName("") + setCopyStatus("inactive") + router.refresh() + } catch (error) { + console.error('Error copying endpoint:', error) + toast({ + variant: "destructive", + description: error instanceof Error ? error.message : "复制失败,请重试" + }) + } finally { + setIsCopying(false) + } + } + + async function handleTest(testData: any) { + if (!endpointToTest) return + + setIsTesting(endpointToTest.id) try { await testEndpoint( - endpoint.id, - endpoint.rule, + endpointToTest.id, + endpointToTest.rule, + testData ) toast({ title: "测试成功", @@ -143,6 +181,7 @@ export function EndpointTable({ description: error instanceof Error ? error.message : "请检查配置是否正确", variant: "destructive", }) + throw error // 重新抛出错误,让 TestPushDialog 知道测试失败了 } finally { setIsTesting(null) } @@ -274,14 +313,21 @@ export function EndpointTable({ 查看示例
handleTest(endpoint)} - disabled={isTesting === endpoint.id || endpoint.status !== 'active'} + onClick={() => { + setEndpointToTest(endpoint) + const exampleBody = generateExampleBody(endpoint.rule) + // 如果生成的示例体为空对象或只有默认消息,且规则包含 ${body},则使用纯文本 + const ruleHasBodyOnly = endpoint.rule.includes('${body}') && !endpoint.rule.includes('${body.') + if (ruleHasBodyOnly && Object.keys(exampleBody).length <= 1) { + setTestInitialContent("示例消息内容") + } else { + setTestInitialContent(JSON.stringify(exampleBody, null, 4)) + } + setTestDialogOpen(true) + }} + disabled={endpoint.status !== 'active'} > - {isTesting === endpoint.id ? ( - - ) : ( - - )} + 测试推送 } /> + { + setEndpointToCopy(endpoint) + setCopyName(`${endpoint.name}-副本`) + setCopyStatus(endpoint.status) + setCopyDialogOpen(true) + }} + > + + 复制 + handleToggleStatus(endpoint.id)} @@ -340,6 +397,68 @@ export function EndpointTable({ + + + + 复制接口 + + 复制接口: {endpointToCopy?.name} + + +
+
+ + setCopyName(e.target.value)} + /> +
+
+ +
+ setCopyStatus(checked ? "active" : "inactive")} + /> +
+
+
+ + 取消 + + {isCopying && } + 确认复制 + + +
+
+ + + 接口: {endpointToTest?.name} +
+ 您可以修改下方的测试内容,支持 JSON 对象或纯文本格式。 + + } + initialContent={testInitialContent} + isTesting={isTesting === endpointToTest?.id} + onTest={handleTest} + /> + )} diff --git a/components/test-push-dialog.tsx b/components/test-push-dialog.tsx new file mode 100644 index 0000000..20d1ad5 --- /dev/null +++ b/components/test-push-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import { useState, useEffect } from "react" +import { Loader2 } from "lucide-react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Button } from "@/components/ui/button" +import { useToast } from "@/components/ui/use-toast" + +interface TestPushDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + title: string + description: string | React.ReactNode + initialContent: string + isTesting: boolean + onTest: (testData: any) => Promise +} + +export function TestPushDialog({ + open, + onOpenChange, + title, + description, + initialContent, + isTesting, + onTest, +}: TestPushDialogProps) { + const [testContent, setTestContent] = useState(initialContent) + const { toast } = useToast() + + // 当对话框打开或 initialContent 变化时,更新内容 + useEffect(() => { + if (open) { + setTestContent(initialContent) + } + }, [open, initialContent]) + + const handleFormatJson = () => { + try { + const parsed = JSON.parse(testContent) + setTestContent(JSON.stringify(parsed, null, 4)) + toast({ + description: "JSON 格式化成功", + }) + } catch (error) { + console.error("Error formatting JSON:", error) + toast({ + variant: "destructive", + description: "无法格式化:内容不是有效的 JSON 格式", + }) + } + } + + const handleTest = async () => { + if (!testContent.trim()) { + toast({ + variant: "destructive", + description: "请输入测试内容", + }) + return + } + + let testData: any + // 尝试解析为 JSON,如果失败则作为纯文本字符串 + try { + testData = JSON.parse(testContent) + } catch (error) { + // 不是有效的 JSON,将其作为纯文本字符串使用 + console.error("Error parsing JSON:", error) + testData = testContent.trim() + } + + try { + await onTest(testData) + onOpenChange(false) + } catch (error) { + console.error("Error during test push:", error) + } + } + + return ( + + + + {title} + +
{description}
+
+
+
+
+
+ + +
+