commit e5682aacbd6210cc83281ba0ff4326ba29624613
Author: hamster1963 <1410514192@qq.com>
Date: Fri Nov 22 22:20:38 2024 +0800
update: init
diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml
new file mode 100644
index 0000000..cba4a7c
--- /dev/null
+++ b/.github/workflows/Build.yml
@@ -0,0 +1,55 @@
+name: Build and release static export
+
+on:
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: "latest"
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Build static export
+ run: |
+ bun run build --base=/dashboard
+
+ - name: Compress dist folder
+ run: zip -r dist.zip dist
+
+ - name: Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: dist.zip
+
+ changelog:
+ needs: release
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set node
+ uses: actions/setup-node@v4
+ with:
+ registry-url: https://registry.npmjs.org/
+ node-version: lts/*
+
+ - run: npx changelogithub
+ env:
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fe5e5ce
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Nezha-Dashboard
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..2c56be6
Binary files /dev/null and b/bun.lockb differ
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..10d70eb
--- /dev/null
+++ b/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/index.css",
+ "baseColor": "stone",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..79a552e
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,28 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": [
+ "warn",
+ { allowConstantExport: true },
+ ],
+ },
+ },
+);
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e4b78ea
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3e2c1c0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "nazha-dashboard-vite",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@fontsource/inter": "^5.1.0",
+ "@heroicons/react": "^2.2.0",
+ "@radix-ui/react-checkbox": "^1.1.2",
+ "@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-separator": "^1.1.0",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@tanstack/react-query": "^5.59.16",
+ "@tanstack/react-query-devtools": "^5.59.16",
+ "@tanstack/react-table": "^8.20.5",
+ "@types/luxon": "^3.4.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "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",
+ "sonner": "^1.5.0",
+ "tailwind-merge": "^2.5.4",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.13.0",
+ "@types/node": "^22.8.1",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react-swc": "^3.7.1",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.13.0",
+ "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-refresh": "^0.4.14",
+ "globals": "^15.11.0",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.14",
+ "typescript": "~5.6.3",
+ "typescript-eslint": "^8.11.0",
+ "vite": "^5.4.10"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..e9c7708
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..68dfb28
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
+import Header from "./components/Header";
+import Footer from "./components/Footer";
+import Server from "./pages/Server";
+
+const App: React.FC = () => {
+ return (
+
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/src/assets/apple-touch-icon.png b/src/assets/apple-touch-icon.png
new file mode 100644
index 0000000..5e07a89
Binary files /dev/null and b/src/assets/apple-touch-icon.png differ
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000..4870e24
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,19 @@
+// src/components/Footer.tsx
+import React from "react";
+
+const Footer: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 0000000..457581e
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+// import { LanguageSwitcher } from "@/components/LanguageSwitcher";
+import { ModeToggle } from "@/components/ThemeSwitcher";
+import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
+import { DateTime } from "luxon";
+import { useEffect, useRef, useState } from "react";
+
+function Header() {
+
+ return (
+
+
+
+
+

+
+ {"NezhaDash"}
+
+
+ 哪吒监控面板
+
+
+
+
+
+
+ );
+}
+
+// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
+const useInterval = (callback: () => void, delay: number | null) => {
+ const savedCallback = useRef<() => void>(() => { });
+ useEffect(() => {
+ savedCallback.current = callback;
+ });
+ useEffect(() => {
+ if (delay !== null) {
+ const interval = setInterval(() => savedCallback.current(), delay || 0);
+ return () => clearInterval(interval);
+ }
+ return undefined;
+ }, [delay]);
+};
+function Overview() {
+ const [mouted, setMounted] = useState(false);
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+ const timeOption = DateTime.TIME_SIMPLE;
+ timeOption.hour12 = true;
+ const [timeString, setTimeString] = useState(
+ DateTime.now().setLocale("en-US").toLocaleString(timeOption),
+ );
+ useInterval(() => {
+ setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption));
+ }, 1000);
+ return (
+
+ 👋 Overview
+
+
+ where the time is
+
+ {mouted ? (
+
{timeString}
+ ) : (
+
+ )}
+
+
+ );
+}
+export default Header;
diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx
new file mode 100644
index 0000000..0eb39bf
--- /dev/null
+++ b/src/components/ThemeProvider.tsx
@@ -0,0 +1,65 @@
+import { createContext, useEffect, useState, ReactNode } from "react";
+
+export type Theme = "dark" | "light" | "system";
+
+type ThemeProviderProps = {
+ children: ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+type ThemeProviderState = {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+};
+
+const initialState: ThemeProviderState = {
+ theme: "system",
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "vite-ui-theme",
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
+ );
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export { ThemeProviderContext };
diff --git a/src/components/ThemeSwitcher.tsx b/src/components/ThemeSwitcher.tsx
new file mode 100644
index 0000000..86a7dab
--- /dev/null
+++ b/src/components/ThemeSwitcher.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { cn } from "@/lib/utils";
+import { CheckCircleIcon } from "@heroicons/react/20/solid";
+import { Moon, Sun } from "lucide-react";
+import { Theme } from "@/components/ThemeProvider";
+import { useTheme } from "../hooks/use-theme";
+
+export function ModeToggle() {
+ const { setTheme, theme } = useTheme();
+
+ const handleSelect = (e: Event, newTheme: Theme) => {
+ e.preventDefault();
+ setTheme(newTheme);
+ };
+
+ return (
+
+
+
+
+
+ handleSelect(e, "light")}
+ >
+ Light
+ {theme === "light" && }
+
+ handleSelect(e, "dark")}
+ >
+ Dark
+ {theme === "dark" && }
+
+ handleSelect(e, "system")}
+ >
+ System
+ {theme === "system" && }
+
+
+
+ );
+}
diff --git a/src/components/loading/Loader.tsx b/src/components/loading/Loader.tsx
new file mode 100644
index 0000000..d8f2108
--- /dev/null
+++ b/src/components/loading/Loader.tsx
@@ -0,0 +1,13 @@
+const bars = Array(8).fill(0);
+
+export const Loader = ({ visible }: { visible: boolean }) => {
+ return (
+
+
+ {bars.map((_, i) => (
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..5373829
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..58d1768
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { Check } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..775c93a
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,120 @@
+import * as React from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { X } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogHeader.displayName = "DialogHeader";
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogFooter.displayName = "DialogFooter";
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+};
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..e209caa
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,198 @@
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..13c4c87
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..44912af
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..6d7f122
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..01b8b6d
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
new file mode 100644
index 0000000..73cf6b4
--- /dev/null
+++ b/src/components/ui/table.tsx
@@ -0,0 +1,117 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Table.displayName = "Table";
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHeader.displayName = "TableHeader";
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableBody.displayName = "TableBody";
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+ tr]:last:border-b-0",
+ className,
+ )}
+ {...props}
+ />
+));
+TableFooter.displayName = "TableFooter";
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableRow.displayName = "TableRow";
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+));
+TableHead.displayName = "TableHead";
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+));
+TableCell.displayName = "TableCell";
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = "TableCaption";
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
diff --git a/src/hooks/use-theme.ts b/src/hooks/use-theme.ts
new file mode 100644
index 0000000..fe7a46c
--- /dev/null
+++ b/src/hooks/use-theme.ts
@@ -0,0 +1,12 @@
+import { useContext } from "react";
+import { ThemeProviderContext } from "../components/ThemeProvider";
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined) {
+ throw new Error("useTheme must be used within a ThemeProvider");
+ }
+
+ return context;
+};
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..2a5cd60
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,234 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 20 14.3% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 20 14.3% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 20 14.3% 4.1%;
+ --primary: 24 9.8% 10%;
+ --primary-foreground: 60 9.1% 97.8%;
+ --secondary: 60 4.8% 95.9%;
+ --secondary-foreground: 24 9.8% 10%;
+ --muted: 60 4.8% 95.9%;
+ --muted-foreground: 25 5.3% 44.7%;
+ --accent: 60 4.8% 95.9%;
+ --accent-foreground: 24 9.8% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 20 5.9% 90%;
+ --input: 20 5.9% 90%;
+ --ring: 20 14.3% 4.1%;
+ --radius: 1rem;
+ --chart-1: 220 70% 50%;
+ --chart-2: 340 75% 55%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 160 60% 45%;
+ --chart-6: 180 50% 50%;
+ --chart-7: 216 50% 50%;
+ --chart-8: 252 50% 50%;
+ --chart-9: 288 50% 50%;
+ --chart-10: 324 50% 50%;
+ }
+
+ .dark {
+ --background: 20 14.3% 4.1%;
+ --foreground: 60 9.1% 97.8%;
+ --card: 20 14.3% 4.1%;
+ --card-foreground: 60 9.1% 97.8%;
+ --popover: 20 14.3% 4.1%;
+ --popover-foreground: 60 9.1% 97.8%;
+ --primary: 60 9.1% 97.8%;
+ --primary-foreground: 24 9.8% 10%;
+ --secondary: 12 6.5% 15.1%;
+ --secondary-foreground: 60 9.1% 97.8%;
+ --muted: 12 6.5% 15.1%;
+ --muted-foreground: 24 5.4% 63.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 60 9.1% 97.8%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 12 6.5% 15.1%;
+ --input: 12 6.5% 15.1%;
+ --ring: 24 5.7% 82.9%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 340 75% 55%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 160 60% 45%;
+ --chart-6: 180 50% 50%;
+ --chart-7: 216 50% 50%;
+ --chart-8: 252 50% 50%;
+ --chart-9: 288 50% 50%;
+ --chart-10: 324 50% 50%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ html {
+ @apply scroll-smooth;
+ }
+ body {
+ @apply bg-background text-foreground;
+ /* font-feature-settings: "rlig" 1, "calt" 1; */
+ font-synthesis-weight: none;
+ text-rendering: optimizeLegibility;
+ }
+}
+
+@layer utilities {
+ .step {
+ counter-increment: step;
+ }
+
+ .step:before {
+ @apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
+ @apply ml-[-50px] mt-[-4px];
+ content: counter(step);
+ }
+}
+
+@media (max-width: 640px) {
+ .container {
+ @apply px-4;
+ }
+}
+
+::selection {
+ @apply bg-stone-300 dark:bg-stone-800;
+}
+
+.hamster-loading-wrapper {
+ --size: 12px;
+ height: var(--size);
+ width: var(--size);
+ inset: 0;
+ z-index: 10;
+}
+
+.hamster-loading-wrapper[data-visible="false"] {
+ transform-origin: center;
+ animation: hamster-fade-out 0.2s ease forwards;
+}
+
+.hamster-spinner {
+ position: relative;
+ top: 50%;
+ left: 50%;
+ height: var(--size);
+ width: var(--size);
+}
+
+.hamster-loading-bar {
+ --gray11: hsl(0, 0%, 43.5%);
+ animation: hamster-spin 0.8s linear infinite;
+ background: var(--gray11);
+ border-radius: 6px;
+ height: 13%;
+ left: -10%;
+ position: absolute;
+ top: -3.9%;
+ width: 30%;
+}
+
+.hamster-loading-bar:nth-child(1) {
+ animation-delay: -0.8s;
+ transform: rotate(0deg) translate(120%);
+}
+
+.hamster-loading-bar:nth-child(2) {
+ animation-delay: -0.7s;
+ transform: rotate(45deg) translate(120%);
+}
+
+.hamster-loading-bar:nth-child(3) {
+ animation-delay: -0.6s;
+ transform: rotate(90deg) translate(120%);
+}
+
+.hamster-loading-bar:nth-child(4) {
+ animation-delay: -0.5s;
+ transform: rotate(135deg) translate(120%);
+}
+
+.hamster-loading-bar:nth-child(5) {
+ animation-delay: -0.4s;
+ transform: rotate(180deg) translate(120%);
+}
+
+.hamster-loading-bar:nth-child(6) {
+ animation-delay: -0.3s;
+ transform: rotate(225deg) translate(120%);
+}
+
+.hamster-loading-bar:nth-child(7) {
+ animation-delay: -0.2s;
+ transform: rotate(270deg) translate(120%);
+}
+
+.hamster-loading-bar:nth-child(8) {
+ animation-delay: -0.1s;
+ transform: rotate(315deg) translate(120%);
+}
+
+@keyframes hamster-fade-in {
+ 0% {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes hamster-fade-out {
+ 0% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+}
+
+@keyframes hamster-spin {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.15;
+ }
+}
+
+.scrollbar-hidden {
+ scrollbar-width: none; /* Firefox */
+}
+
+.scrollbar-hidden::-webkit-scrollbar {
+ display: none; /* Chrome, Safari 和 Opera */
+}
diff --git a/src/lib/nav-router.ts b/src/lib/nav-router.ts
new file mode 100644
index 0000000..c486b69
--- /dev/null
+++ b/src/lib/nav-router.ts
@@ -0,0 +1,30 @@
+export const navRouter = [
+ {
+ name: "服务器",
+ path: "/",
+ },
+ {
+ name: "服务(Dev)",
+ path: "/service",
+ },
+ {
+ name: "任务(Dev)",
+ path: "/task",
+ },
+ {
+ name: "告警(Dev)",
+ path: "/alarm",
+ },
+ {
+ name: "内网穿透(Dev)",
+ path: "/intranet",
+ },
+ {
+ name: "用户",
+ path: "/user",
+ },
+ {
+ name: "设置(Dev)",
+ path: "/setting",
+ },
+];
diff --git a/src/lib/nezha-api.ts b/src/lib/nezha-api.ts
new file mode 100644
index 0000000..492c87a
--- /dev/null
+++ b/src/lib/nezha-api.ts
@@ -0,0 +1,80 @@
+export const fetchUsers = async (token: string) => {
+ const response = await fetch("http://localhost:8008/api/v1/user", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ return data;
+};
+
+export const createUser = async (
+ token: string,
+ username: string,
+ password: string,
+) => {
+ const response = await fetch(`http://localhost:8008/api/v1/user`, {
+ method: "POST",
+ body: JSON.stringify({ username, password }),
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ return data;
+};
+
+export const deleteUser = async (token: string, ids: number[]) => {
+ const response = await fetch(
+ `http://localhost:8008/api/v1/batch-delete/user`,
+ {
+ method: "POST",
+ body: JSON.stringify(ids),
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ );
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ return data;
+};
+
+export const fetchServers = async (token: string) => {
+ const response = await fetch("http://localhost:8008/api/v1/server", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ return data;
+};
+
+export const deleteServer = async (token: string, ids: number[]) => {
+ const response = await fetch(
+ `http://localhost:8008/api/v1/batch-delete/server`,
+ {
+ method: "POST",
+ body: JSON.stringify(ids),
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ );
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ return data;
+};
diff --git a/src/lib/nezha-model.ts b/src/lib/nezha-model.ts
new file mode 100644
index 0000000..9e049c5
--- /dev/null
+++ b/src/lib/nezha-model.ts
@@ -0,0 +1,79 @@
+/**
+ * model.Server
+ */
+export interface ModelServer {
+ created_at: string;
+ /**
+ * DDNS配置
+ */
+ ddns_profiles: number[];
+ deleted_at: string;
+ /**
+ * 展示排序,越大越靠前
+ */
+ display_index: number;
+ /**
+ * 启用DDNS
+ */
+ enable_ddns: boolean;
+ /**
+ * 对游客隐藏
+ */
+ hide_for_guest?: boolean;
+ host?: ModelHost;
+ id: number;
+ last_active?: string;
+ name: string;
+ /**
+ * 管理员可见备注
+ */
+ note: string;
+ /**
+ * 公开备注
+ */
+ public_note: string;
+ state: ModelHostState;
+ updated_at: string;
+ uuid: string;
+}
+
+export interface ModelHost {
+ arch?: string;
+ boot_time?: number;
+ country_code?: string;
+ cpu?: string[];
+ disk_total?: number;
+ gpu?: string[];
+ ip?: string;
+ mem_total?: number;
+ platform?: string;
+ platform_version?: string;
+ swap_total?: number;
+ version?: string;
+ virtualization?: string;
+}
+
+export interface ModelHostState {
+ cpu?: number;
+ disk_used?: number;
+ gpu?: number[];
+ load_1?: number;
+ load_15?: number;
+ load_5?: number;
+ mem_used?: number;
+ net_in_speed?: number;
+ net_in_transfer?: number;
+ net_out_speed?: number;
+ net_out_transfer?: number;
+ process_count?: number;
+ swap_used?: number;
+ tcp_conn_count?: number;
+ temperatures?: ModelSensorTemperature[];
+ udp_conn_count?: number;
+ uptime?: number;
+}
+
+export interface ModelSensorTemperature {
+ name?: string;
+ temperature?: number;
+}
diff --git a/src/lib/useWebsocket.tsx b/src/lib/useWebsocket.tsx
new file mode 100644
index 0000000..0ae0f6d
--- /dev/null
+++ b/src/lib/useWebsocket.tsx
@@ -0,0 +1,89 @@
+import { useState, useEffect, useRef, useCallback } from 'react'
+
+export interface WebSocketHook {
+ socket: WebSocket | null
+ connected: boolean
+ onlineCount: number
+ message: string | null
+ sendMessage: (msg: string) => void
+}
+
+export default function useWebSocket(url: string): WebSocketHook {
+ const [socket, setSocket] = useState(null)
+ const [message, setMessage] = useState(null)
+ const [connected, setConnected] = useState(false)
+ const [onlineCount, setOnlineCount] = useState(0)
+ const socketRef = useRef(null)
+ const reconnectAttempts = useRef(0)
+ const reconnectTimeout = useRef(null)
+ const isUnmounted = useRef(false)
+
+ const connect = useCallback(() => {
+ if (isUnmounted.current) return
+
+ const ws = new WebSocket(url)
+ setSocket(ws)
+ socketRef.current = ws
+
+ ws.onopen = () => {
+ setConnected(true)
+ reconnectAttempts.current = 0
+ }
+
+ ws.onmessage = (event: MessageEvent) => {
+ setMessage(event.data)
+ const msgJson = JSON.parse(event.data)
+ if (msgJson.type === 'live') {
+ setOnlineCount(msgJson.data.count)
+ }
+ }
+
+ ws.onerror = (error) => {
+ console.error('WebSocket Error:', error)
+ }
+
+ ws.onclose = () => {
+ setConnected(false)
+ if (!isUnmounted.current) {
+ // Attempt to reconnect
+ if (reconnectAttempts.current < 5) {
+ const timeout = Math.pow(2, reconnectAttempts.current) * 1000 // Exponential backoff
+ reconnectAttempts.current += 1
+ reconnectTimeout.current = setTimeout(() => {
+ connect()
+ }, timeout)
+ } else {
+ console.warn('Max reconnect attempts reached.')
+ }
+ }
+ }
+ }, [url])
+
+ useEffect(() => {
+ connect()
+
+ return () => {
+ isUnmounted.current = true
+ if (socketRef.current) {
+ socketRef.current.close()
+ }
+ if (reconnectTimeout.current) {
+ clearTimeout(reconnectTimeout.current)
+ }
+ }
+ }, [connect])
+
+ // Function to send messages
+ const sendMessage = useCallback((msg: string) => {
+ if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
+ socketRef.current.send(msg)
+ } else {
+ console.warn(
+ 'WebSocket is not open. Ready state:',
+ socketRef.current?.readyState
+ )
+ }
+ }, [])
+
+ return { socket, message, sendMessage, connected, onlineCount }
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/src/lib/websocketProvider.tsx b/src/lib/websocketProvider.tsx
new file mode 100644
index 0000000..81ae95f
--- /dev/null
+++ b/src/lib/websocketProvider.tsx
@@ -0,0 +1,26 @@
+import { createContext, useContext, ReactNode } from 'react'
+import useWebSocket, { WebSocketHook } from './useWebsocket'
+
+
+interface WebSocketProviderProps {
+ children: ReactNode
+}
+
+const WebSocketContext = createContext(undefined)
+
+export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
+ const ws = useWebSocket('wss://dev-next.buycoffee.top:4433/api/v1/ws/server')
+ return (
+ {children}
+ )
+}
+
+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/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..293e182
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,25 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css";
+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
new file mode 100644
index 0000000..147044f
--- /dev/null
+++ b/src/pages/Server.tsx
@@ -0,0 +1,22 @@
+export default function Servers() {
+ return (
+
+
+
+
+ 服务器
+
+
+ 你可以在这里查看和管理全部的服务器。
+
+ 了解更多↗
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..1fd7518
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,59 @@
+module.exports = {
+ darkMode: ["class"],
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: "var(--font-sans)",
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ colors: {
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ chart: {
+ 1: "hsl(var(--chart-1))",
+ 2: "hsl(var(--chart-2))",
+ 3: "hsl(var(--chart-3))",
+ 4: "hsl(var(--chart-4))",
+ 5: "hsl(var(--chart-5))",
+ },
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+};
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..d1f4998
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
new file mode 100644
index 0000000..1847453
--- /dev/null
+++ b/tsconfig.app.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/footer.tsx","./src/components/header.tsx","./src/components/modetoggle.tsx","./src/components/privateroute.tsx","./src/components/profile.tsx","./src/components/themeprovider.tsx","./src/components/loading/loader.tsx","./src/components/ui/button.tsx","./src/components/ui/dropdown-menu.tsx","./src/hooks/useauth.tsx","./src/lib/nav-router.ts","./src/lib/utils.ts","./src/pages/alarm.tsx","./src/pages/intranet.tsx","./src/pages/login.tsx","./src/pages/server.tsx","./src/pages/service.tsx","./src/pages/setting.tsx","./src/pages/task.tsx","./src/pages/user.tsx"],"version":"5.6.3"}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..fec8c8e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..9dad701
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo
new file mode 100644
index 0000000..75ea001
--- /dev/null
+++ b/tsconfig.node.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./vite.config.ts"],"version":"5.6.3"}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..4e939e1
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,13 @@
+import path from "path";
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});