diff --git a/web/app/dashboard/config/page.tsx b/web/app/dashboard/config/page.tsx index fa54d34..0f7a715 100644 --- a/web/app/dashboard/config/page.tsx +++ b/web/app/dashboard/config/page.tsx @@ -1,23 +1,85 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useState, useCallback } from "react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { useToast } from "@/components/ui/use-toast" import { useRouter } from "next/navigation" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Switch } from "@/components/ui/switch" +import { Slider } from "@/components/ui/slider" +import { Plus, Trash2, Edit, Save, Download, Upload } from "lucide-react" + +interface PathMapping { + DefaultTarget: string + ExtensionMap?: Record + SizeThreshold?: number +} + +interface FixedPath { + Path: string + TargetHost: string + TargetURL: string +} + +interface CompressionConfig { + Enabled: boolean + Level: number +} + +interface Config { + MAP: Record + Compression: { + Gzip: CompressionConfig + Brotli: CompressionConfig + } + FixedPaths: FixedPath[] +} export default function ConfigPage() { - const [config, setConfig] = useState("") + const [config, setConfig] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const { toast } = useToast() const router = useRouter() - useEffect(() => { - fetchConfig() - }, []) + // 对话框状态 + const [pathDialogOpen, setPathDialogOpen] = useState(false) + const [newPathData, setNewPathData] = useState({ + path: "", + defaultTarget: "", + extensionMap: {} as Record, + sizeThreshold: 0, + }) + const [fixedPathDialogOpen, setFixedPathDialogOpen] = useState(false) + const [editingFixedPath, setEditingFixedPath] = useState(null) + const [newFixedPath, setNewFixedPath] = useState({ + Path: "", + TargetHost: "", + TargetURL: "", + }) + const [extensionMapDialogOpen, setExtensionMapDialogOpen] = useState(false) + const [editingPath, setEditingPath] = useState(null) + const [newExtension, setNewExtension] = useState({ ext: "", target: "" }) - const fetchConfig = async () => { + const fetchConfig = useCallback(async () => { try { const token = localStorage.getItem("token") if (!token) { @@ -43,7 +105,7 @@ export default function ConfigPage() { } const data = await response.json() - setConfig(JSON.stringify(data, null, 2)) + setConfig(data) } catch (error) { const message = error instanceof Error ? error.message : "获取配置失败" toast({ @@ -54,14 +116,17 @@ export default function ConfigPage() { } finally { setLoading(false) } - } + }, [router, toast]) + + useEffect(() => { + fetchConfig() + }, [fetchConfig]) const handleSave = async () => { + if (!config) return + setSaving(true) try { - // 验证 JSON 格式 - const parsedConfig = JSON.parse(config) - const token = localStorage.getItem("token") if (!token) { router.push("/login") @@ -74,7 +139,7 @@ export default function ConfigPage() { "Content-Type": "application/json", 'Authorization': `Bearer ${token}` }, - body: JSON.stringify(parsedConfig), + body: JSON.stringify(config), }) if (response.status === 401) { @@ -95,7 +160,7 @@ export default function ConfigPage() { } catch (error) { toast({ title: "错误", - description: error instanceof SyntaxError ? "JSON 格式错误" : error instanceof Error ? error.message : "保存配置失败", + description: error instanceof Error ? error.message : "保存配置失败", variant: "destructive", }) } finally { @@ -103,21 +168,183 @@ export default function ConfigPage() { } } - const handleFormat = () => { - try { - const parsedConfig = JSON.parse(config) - setConfig(JSON.stringify(parsedConfig, null, 2)) - toast({ - title: "成功", - description: "配置已格式化", - }) - } catch { + const addPath = () => { + if (!config) return + const { path, defaultTarget, sizeThreshold, extensionMap } = newPathData + + if (!path || !defaultTarget) { toast({ title: "错误", - description: "JSON 格式错误", + description: "路径和默认目标不能为空", variant: "destructive", }) + return } + + const newConfig = { ...config } + newConfig.MAP[path] = { + DefaultTarget: defaultTarget, + ...(sizeThreshold ? { SizeThreshold: sizeThreshold } : {}), + ...(Object.keys(extensionMap).length > 0 ? { ExtensionMap: extensionMap } : {}) + } + + setConfig(newConfig) + setPathDialogOpen(false) + setNewPathData({ + path: "", + defaultTarget: "", + extensionMap: {}, + sizeThreshold: 0, + }) + } + + const deletePath = (path: string) => { + if (!config) return + const newConfig = { ...config } + delete newConfig.MAP[path] + setConfig(newConfig) + } + + const updateCompression = (type: 'Gzip' | 'Brotli', field: 'Enabled' | 'Level', value: boolean | number) => { + if (!config) return + const newConfig = { ...config } + if (field === 'Enabled') { + newConfig.Compression[type].Enabled = value as boolean + } else { + newConfig.Compression[type].Level = value as number + } + setConfig(newConfig) + } + + const handleExtensionMapEdit = (path: string) => { + setEditingPath(path) + setExtensionMapDialogOpen(true) + } + + const addExtensionMap = () => { + if (!config || !editingPath) return + const { ext, target } = newExtension + if (!ext || !target) { + toast({ + title: "错误", + description: "扩展名和目标不能为空", + variant: "destructive", + }) + return + } + + const newConfig = { ...config } + const mapping = newConfig.MAP[editingPath] + if (typeof mapping === "string") { + newConfig.MAP[editingPath] = { + DefaultTarget: mapping, + ExtensionMap: { [ext]: target } + } + } else { + mapping.ExtensionMap = { + ...mapping.ExtensionMap, + [ext]: target + } + } + + setConfig(newConfig) + setNewExtension({ ext: "", target: "" }) + } + + const deleteExtensionMap = (path: string, ext: string) => { + if (!config) return + const newConfig = { ...config } + const mapping = newConfig.MAP[path] + if (typeof mapping !== "string" && mapping.ExtensionMap) { + const newExtensionMap = { ...mapping.ExtensionMap } + delete newExtensionMap[ext] + mapping.ExtensionMap = newExtensionMap + } + setConfig(newConfig) + } + + const addFixedPath = () => { + if (!config) return + const { Path, TargetHost, TargetURL } = newFixedPath + + if (!Path || !TargetHost || !TargetURL) { + toast({ + title: "错误", + description: "所有字段都不能为空", + variant: "destructive", + }) + return + } + + const newConfig = { ...config } + if (editingFixedPath) { + const index = newConfig.FixedPaths.findIndex(p => p.Path === editingFixedPath.Path) + if (index !== -1) { + newConfig.FixedPaths[index] = newFixedPath + } + } else { + newConfig.FixedPaths.push(newFixedPath) + } + + setConfig(newConfig) + setFixedPathDialogOpen(false) + setEditingFixedPath(null) + setNewFixedPath({ + Path: "", + TargetHost: "", + TargetURL: "", + }) + } + + const editFixedPath = (path: FixedPath) => { + setEditingFixedPath(path) + setNewFixedPath(path) + setFixedPathDialogOpen(true) + } + + const deleteFixedPath = (path: FixedPath) => { + if (!config) return + const newConfig = { ...config } + newConfig.FixedPaths = newConfig.FixedPaths.filter(p => p.Path !== path.Path) + setConfig(newConfig) + } + + const exportConfig = () => { + if (!config) return + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'proxy-config.json' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + const importConfig = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file) return + + const reader = new FileReader() + reader.onload = (e) => { + try { + const content = e.target?.result as string + const newConfig = JSON.parse(content) + setConfig(newConfig) + toast({ + title: "成功", + description: "配置已导入", + }) + } catch { + toast({ + title: "错误", + description: "配置文件格式错误", + variant: "destructive", + }) + } + } + reader.readAsText(file) } if (loading) { @@ -134,32 +361,311 @@ export default function ConfigPage() { return (
- - 代理服务配置 + + Proxy Go配置 +
+ + + +
-
-
- - - -
-
-