From 963b6a54a68a42d1e3711db353589d034ba958de Mon Sep 17 00:00:00 2001 From: hamster1963 <1410514192@qq.com> Date: Sat, 23 Nov 2024 19:28:55 +0800 Subject: [PATCH] feat: server page --- bun.lockb | Bin 135901 -> 137260 bytes index.html | 10 +- package.json | 3 + src/components/Footer.tsx | 5 +- src/components/Header.tsx | 4 +- src/components/ServerCard.tsx | 128 ++++++++++++++++++++++++++ src/components/ServerFlag.tsx | 48 ++++++++++ src/components/ServerOverview.tsx | 110 ++++++++++++++++++++++ src/components/ServerUsageBar.tsx | 23 +++++ src/components/ui/card.tsx | 85 +++++++++++++++++ src/components/ui/progress.tsx | 28 ++++++ src/hooks/use-websocket.tsx | 4 + src/index.css | 4 +- src/lib/logo-class.tsx | 148 ++++++++++++++++++++++++++++++ src/lib/utils.ts | 119 +++++++++++++++++++++++- src/lib/websocketContext.tsx | 14 --- src/lib/websocketProvider.tsx | 14 --- src/main.tsx | 4 +- src/pages/Server.tsx | 62 +++++++------ src/types/nezha-api.ts | 63 +++++++------ vite.config.ts | 4 +- 21 files changed, 784 insertions(+), 96 deletions(-) create mode 100644 src/components/ServerCard.tsx create mode 100644 src/components/ServerFlag.tsx create mode 100644 src/components/ServerOverview.tsx create mode 100644 src/components/ServerUsageBar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/lib/logo-class.tsx delete mode 100644 src/lib/websocketContext.tsx delete mode 100644 src/lib/websocketProvider.tsx diff --git a/bun.lockb b/bun.lockb index 2c56be6e300598eb5dca90e06ccb82bc0afc35dd..c75ce233c6ed53a87a35ed701cc3fe085dc49578 100755 GIT binary patch delta 24044 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 B6xRR% delta 23055 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 - Vite + React + TS + + + NEZHA
diff --git a/package.json b/package.json index 3e2c1c0..b43ce1d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.59.16", @@ -24,12 +25,14 @@ "@types/luxon": "^3.4.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "country-flag-icons": "^1.5.13", "framer-motion": "^11.11.10", "lucide-react": "^0.453.0", "luxon": "^3.5.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.27.0", + "react-use-websocket": "^4.11.1", "sonner": "^1.5.0", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7" diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 4870e24..7f3162f 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -7,9 +7,12 @@ const Footer: React.FC = () => {
©2020-{new Date().getFullYear()}{" "} - + Nezha + + Nezha-Dash +
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 388f2c2..49ed0ff 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -21,13 +21,13 @@ function Header() { className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!" /> - {"NezhaDash"} + {"NEZHA"}

- 哪吒监控面板 + 哪吒监控

diff --git a/src/components/ServerCard.tsx b/src/components/ServerCard.tsx new file mode 100644 index 0000000..06ce7df --- /dev/null +++ b/src/components/ServerCard.tsx @@ -0,0 +1,128 @@ +import ServerFlag from "@/components/ServerFlag"; +import ServerUsageBar from "@/components/ServerUsageBar"; + +import { cn, formatNezhaInfo } from "@/lib/utils"; +import { NezhaAPI } from "@/types/nezha-api"; +import { Card } from "./ui/card"; + +export default function ServerCard({ + serverInfo, +}: { + serverInfo: NezhaAPI; +}) { + + const { name, country_code, online, cpu, up, down, mem, stg } = + formatNezhaInfo(serverInfo); + + const showFlag = true + + + return online ? ( +
+ +
+ +
+ {showFlag ? : null} +
+
+

+ {name} +

+
+
+
+
+
+

{"CPU"}

+
+ {cpu.toFixed(2)}% +
+ +
+
+

{"MEM"}

+
+ {mem.toFixed(2)}% +
+ +
+
+

{"STG"}

+
+ {stg.toFixed(2)}% +
+ +
+
+

{"Upload"}

+
+ {up >= 1024 + ? `${(up / 1024).toFixed(2)}G/s` + : `${up.toFixed(2)}M/s`} +
+
+
+

{"Download"}

+
+ {down >= 1024 + ? `${(down / 1024).toFixed(2)}G/s` + : `${down.toFixed(2)}M/s`} +
+
+
+
+
+
+ ) : ( + +
+ +
+ {showFlag ? : null} +
+
+

+ {name} +

+
+
+
+ ); +} diff --git a/src/components/ServerFlag.tsx b/src/components/ServerFlag.tsx new file mode 100644 index 0000000..82fdbca --- /dev/null +++ b/src/components/ServerFlag.tsx @@ -0,0 +1,48 @@ +import { cn } from "@/lib/utils"; +import getUnicodeFlagIcon from "country-flag-icons/unicode"; +import { useEffect, useState } from "react"; + +export default function ServerFlag({ + country_code, + className, +}: { + country_code: string; + className?: string; +}) { + const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false); + + + useEffect(() => { + const checkEmojiSupport = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试 + if (!ctx) return; + ctx.fillStyle = "#000"; + ctx.textBaseline = "top"; + ctx.font = "32px Arial"; + ctx.fillText(emojiFlag, 0, 0); + + const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0; + setSupportsEmojiFlags(support); + }; + + checkEmojiSupport(); + }, []); + + if (!country_code) return null; + + if (supportsEmojiFlags && country_code.toLowerCase() === "tw") { + country_code = "cn"; + } + + return ( + + { !supportsEmojiFlags ? ( + + ) : ( + getUnicodeFlagIcon(country_code) + )} + + ); +} diff --git a/src/components/ServerOverview.tsx b/src/components/ServerOverview.tsx new file mode 100644 index 0000000..f2d3d4a --- /dev/null +++ b/src/components/ServerOverview.tsx @@ -0,0 +1,110 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { cn, formatBytes } from "@/lib/utils"; + +type ServerOverviewProps = { + online: number; + offline: number; + total: number; + up: number; + down: number; +} + + +export default function ServerOverview({ online, offline, total, up, down }: ServerOverviewProps) { + + return ( + <> +
+ + +
+

+ {"Totalservers"} +

+
+ + + +
+ {total} +
+
+
+
+
+ + +
+

+ {"Onlineservers"} +

+
+ + + + + +
+ {online} +
+
+
+
+
+ + +
+

+ {"Offlineservers"} +

+
+ + + + +
+ {offline} +
+
+
+
+
+ + +
+

+ {"Totalbandwidth"} +

+ +
+

+ ↑{formatBytes(up)} +

+

+ ↓{formatBytes(down)} +

+
+
+ +
+
+
+ + ); +} diff --git a/src/components/ServerUsageBar.tsx b/src/components/ServerUsageBar.tsx new file mode 100644 index 0000000..e4e1efb --- /dev/null +++ b/src/components/ServerUsageBar.tsx @@ -0,0 +1,23 @@ +import { Progress } from "@/components/ui/progress"; + +type ServerUsageBarProps = { + value: number; +}; + +export default function ServerUsageBar({ value }: ServerUsageBarProps) { + return ( + 90 + ? "bg-red-500" + : value > 70 + ? "bg-orange-400" + : "bg-green-500" + } + className={"h-[3px] rounded-sm"} + /> + ); +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..ab6efa5 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,85 @@ +import { cn } from "@/lib/utils"; +import * as React from "react"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..9b5e72e --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + indicatorClassName?: string + } +>(({ className, value, indicatorClassName, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/hooks/use-websocket.tsx b/src/hooks/use-websocket.tsx index 62fdd8a..e4b2219 100644 --- a/src/hooks/use-websocket.tsx +++ b/src/hooks/use-websocket.tsx @@ -19,6 +19,10 @@ export default function useWebSocket(url: string): WebSocketHook { const connect = useCallback(() => { if (isUnmounted.current) return; + console.log("Connecting to WebSocket..."); + + console.log("WebSocket URL:", url); + const ws = new WebSocket(url); setSocket(ws); socketRef.current = ws; diff --git a/src/index.css b/src/index.css index 2a5cd60..8e36c62 100644 --- a/src/index.css +++ b/src/index.css @@ -19,7 +19,7 @@ @layer base { :root { - --background: 0 0% 100%; + --background: 0 0% 98%; --foreground: 20 14.3% 4.1%; --card: 0 0% 100%; --card-foreground: 20 14.3% 4.1%; @@ -52,7 +52,7 @@ } .dark { - --background: 20 14.3% 4.1%; + --background: 30 15% 8%; --foreground: 60 9.1% 97.8%; --card: 20 14.3% 4.1%; --card-foreground: 60 9.1% 97.8%; diff --git a/src/lib/logo-class.tsx b/src/lib/logo-class.tsx new file mode 100644 index 0000000..99ce400 --- /dev/null +++ b/src/lib/logo-class.tsx @@ -0,0 +1,148 @@ +import type { SVGProps } from "react"; + +export function GetFontLogoClass(platform: string): string { + if ( + [ + "almalinux", + "alpine", + "aosc", + "apple", + "archlinux", + "archlabs", + "artix", + "budgie", + "centos", + "coreos", + "debian", + "deepin", + "devuan", + "docker", + "elementary", + "fedora", + "ferris", + "flathub", + "freebsd", + "gentoo", + "gnu-guix", + "illumos", + "kali-linux", + "linuxmint", + "mageia", + "mandriva", + "manjaro", + "nixos", + "openbsd", + "opensuse", + "pop-os", + "raspberry-pi", + "redhat", + "rocky-linux", + "sabayon", + "slackware", + "snappy", + "solus", + "tux", + "ubuntu", + "void", + "zorin", + ].indexOf(platform) > -1 + ) { + return platform; + } + if (platform == "darwin") { + return "apple"; + } + if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { + return "tux"; + } + if (platform == "amazon") { + return "redhat"; + } + if (platform == "arch") { + return "archlinux"; + } + if (platform.toLowerCase().includes("opensuse")) { + return "opensuse"; + } + return "tux"; +} + +export function GetOsName(platform: string): string { + if ( + [ + "almalinux", + "alpine", + "aosc", + "apple", + "archlinux", + "archlabs", + "artix", + "budgie", + "centos", + "coreos", + "debian", + "deepin", + "devuan", + "docker", + "fedora", + "ferris", + "flathub", + "freebsd", + "gentoo", + "gnu-guix", + "illumos", + "linuxmint", + "mageia", + "mandriva", + "manjaro", + "nixos", + "openbsd", + "opensuse", + "pop-os", + "redhat", + "sabayon", + "slackware", + "snappy", + "solus", + "tux", + "ubuntu", + "void", + "zorin", + ].indexOf(platform) > -1 + ) { + return platform.charAt(0).toUpperCase() + platform.slice(1); + } + if (platform == "darwin") { + return "macOS"; + } + if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { + return "Linux"; + } + if (platform == "amazon") { + return "Redhat"; + } + if (platform == "arch") { + return "Archlinux"; + } + if (platform.toLowerCase().includes("opensuse")) { + return "Opensuse"; + } + return "Linux"; +} + +export function MageMicrosoftWindows(props: SVGProps) { + return ( + + + + ); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a5ef193..fae75a3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,123 @@ -import { clsx, type ClassValue } from "clsx"; +import { NezhaAPI } from "@/types/nezha-api"; +import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function formatNezhaInfo(serverInfo: NezhaAPI) { + const lastActiveTime = parseISOTimestamp(serverInfo.last_active); + return { + ...serverInfo, + cpu: serverInfo.state.cpu || 0, + process: serverInfo.state.process_count || 0, + up: serverInfo.state.net_out_speed / 1024 / 1024 || 0, + down: serverInfo.state.net_in_speed / 1024 / 1024 || 0, + online: Date.now() - lastActiveTime <= 300000, + tcp: serverInfo.state.tcp_conn_count || 0, + udp: serverInfo.state.udp_conn_count || 0, + mem: (serverInfo.state.mem_used / serverInfo.host.mem_total) * 100 || 0, + swap: (serverInfo.state.swap_used / serverInfo.host.swap_total) * 100 || 0, + disk: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0, + stg: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0, + country_code: serverInfo.host.country_code, + }; +} + +export function formatBytes(bytes: number, decimals: number = 2) { + if (!+bytes) return "0 Bytes"; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = [ + "Bytes", + "KiB", + "MiB", + "GiB", + "TiB", + "PiB", + "EiB", + "ZiB", + "YiB", + ]; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; +} + +export function getDaysBetweenDates(date1: string, date2: string): number { + const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数 + const firstDate = new Date(date1); + const secondDate = new Date(date2); + + // 计算两个日期之间的天数差异 + return Math.round( + Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay), + ); +} + +export const fetcher = (url: string) => + fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText); + } + return res.json(); + }) + .then((data) => data.data) + .catch((err) => { + console.error(err); + throw err; + }); + +export const nezhaFetcher = async (url: string) => { + const res = await fetch(url); + + if (!res.ok) { + const error = new Error("An error occurred while fetching the data."); + // @ts-expect-error - res.json() returns a Promise + error.info = await res.json(); + // @ts-expect-error - res.status is a number + error.status = res.status; + throw error; + } + + return res.json(); +}; + +export function parseISOTimestamp(isoString: string): number { + return new Date(isoString).getTime(); +} + +export function formatRelativeTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + if (hours > 24) { + const days = Math.floor(hours / 24); + return `${days}d`; + } else if (hours > 0) { + return `${hours}h`; + } else if (minutes > 0) { + return `${minutes}m`; + } else if (seconds >= 0) { + return `${seconds}s`; + } + return "0s"; +} + +export function formatTime(timestamp: number): string { + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const seconds = date.getSeconds().toString().padStart(2, "0"); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} diff --git a/src/lib/websocketContext.tsx b/src/lib/websocketContext.tsx deleted file mode 100644 index 16f6ca8..0000000 --- a/src/lib/websocketContext.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createContext, useContext } from "react"; -import { WebSocketHook } from "../hooks/use-websocket"; - -export const WebSocketContext = createContext(undefined); - -export const useWebSocketContext = (): WebSocketHook => { - const context = useContext(WebSocketContext); - if (!context) { - throw new Error( - "useWebSocketContext must be used within a WebSocketProvider", - ); - } - return context; -}; diff --git a/src/lib/websocketProvider.tsx b/src/lib/websocketProvider.tsx deleted file mode 100644 index b951e12..0000000 --- a/src/lib/websocketProvider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { ReactNode } from "react"; -import useWebSocket from "../hooks/use-websocket"; -import { WebSocketContext } from "./websocketContext"; - -interface WebSocketProviderProps { - children: ReactNode; -} - -export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { - const ws = useWebSocket('/api/v1/ws/server'); - return ( - {children} - ); -}; diff --git a/src/main.tsx b/src/main.tsx index daa8280..6b7ab3a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,20 +6,18 @@ import { ThemeProvider } from "./components/ThemeProvider"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "sonner"; -import { WebSocketProvider } from "./lib/websocketProvider"; + const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById("root")!).render( - - , ); diff --git a/src/pages/Server.tsx b/src/pages/Server.tsx index 61b6208..5984ab1 100644 --- a/src/pages/Server.tsx +++ b/src/pages/Server.tsx @@ -1,38 +1,44 @@ -import { useWebSocketContext } from "@/lib/websocketContext"; -import { NezhaAPI } from "@/types/nezha-api"; - +import useWebSocket from 'react-use-websocket'; +import { NezhaAPIResponse } from "@/types/nezha-api"; +import ServerCard from '@/components/ServerCard'; +import { formatNezhaInfo } from '@/lib/utils'; +import ServerOverview from '@/components/ServerOverview'; export default function Servers() { - const { connected, message } = useWebSocketContext() + const { lastMessage, readyState } = useWebSocket('/api/v1/ws/server', { + shouldReconnect: () => true, // 自动重连 + reconnectInterval: 3000, // 重连间隔 + }); - if (!connected || !message) { - return ( -

连接中...

- ) + // 检查连接状态 + if (readyState !== 1) { + return null; } - const nezhaWsData = JSON.parse(message) as NezhaAPI[] + // 解析消息 + const nezhaWsData = lastMessage ? JSON.parse(lastMessage.data) as NezhaAPIResponse : null; - console.log(nezhaWsData) + if (!nezhaWsData) { + return

等待数据...

; + } + + // 计算服务器总数和在线数量 + const totalServers = nezhaWsData.servers.length; + const onlineServers = nezhaWsData.servers.filter(server => formatNezhaInfo(server).online).length; + const offlineServers = nezhaWsData.servers.filter(server => !formatNezhaInfo(server).online).length; + const up = nezhaWsData.servers.reduce((total, server) => total + server.state.net_out_transfer, 0); + const down = nezhaWsData.servers.reduce((total, server) => total + server.state.net_in_transfer, 0); return ( -
-
-
-

- 服务器 -

-

- 你可以在这里查看和管理全部的服务器。 - - 了解更多↗ - -

-
-
+
+ +
+ {nezhaWsData.servers.map((serverInfo) => ( + + ))} +
); -} +} \ No newline at end of file diff --git a/src/types/nezha-api.ts b/src/types/nezha-api.ts index f46aa77..6546e1a 100644 --- a/src/types/nezha-api.ts +++ b/src/types/nezha-api.ts @@ -1,39 +1,46 @@ +export interface NezhaAPIResponse { + now: number; + servers: NezhaAPI[]; +} + + export interface NezhaAPI { id: number; name: string; + last_active: string; host: NezhaAPIHost; - status: NezhaAPIStatus; + state: NezhaAPIStatus; } export interface NezhaAPIHost { - Platform: string; - PlatformVersion: string; - CPU: string[]; - MemTotal: number; - DiskTotal: number; - SwapTotal: number; - Arch: string; - BootTime: number; - CountryCode: string; - Version: string; + platform: string; + platform_version: string; + cpu: string[]; + mem_total: number; + disk_total: number; + swap_total: number; + arch: string; + boot_time: number; + country_code: string; + version: string; } export interface NezhaAPIStatus { - CPU: number; - MemUsed: number; - SwapUsed: number; - DiskUsed: number; - NetInTransfer: number; - NetOutTransfer: number; - NetInSpeed: number; - NetOutSpeed: number; - Uptime: number; - Load1: number; - Load5: number; - Load15: number; - TcpConnCount: number; - UdpConnCount: number; - ProcessCount: number; - Temperatures: number; - GPU: number; + cpu: number; + mem_used: number; + swap_used: number; + disk_used: number; + net_in_transfer: number; + net_out_transfer: number; + net_in_speed: number; + net_out_speed: number; + uptime: number; + load_1: number; + load_5: number; + load_15: number; + tcp_conn_count: number; + udp_conn_count: number; + process_count: number; + temperatures: number; + gpu: number; } diff --git a/vite.config.ts b/vite.config.ts index bdb68f3..ef849d0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,8 +12,8 @@ export default defineConfig({ }, server: { proxy: { - '/api/v1/ws': { - target: 'http://localhost:8008', + '/api/v1/ws/server': { + target: 'ws://localhost:8080', changeOrigin: true, ws: true, },