refactory: add tailwindcss and remove css
This commit is contained in:
1 parent
7909ee9bbf
commit
7b9c8c1bc0
64 files changed
+4906
-3930
No files matched your search
+1834
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/global.css",
|
||||
"baseColor": "gray",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
+70
-30
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,41 +13,81 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@radix-ui/react-avatar": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.10",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-context-menu": "^2.2.14",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-form": "^0.0.3",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-popover": "^1.0.6",
|
||||
"@radix-ui/react-select": "^1.2.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@radix-ui/react-hover-card": "^1.1.13",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-menubar": "^1.1.14",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||
"@radix-ui/react-popover": "^1.1.13",
|
||||
"@radix-ui/react-progress": "^1.1.6",
|
||||
"@radix-ui/react-radio-group": "^1.3.6",
|
||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slider": "^1.3.4",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-switch": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-toast": "^1.2.13",
|
||||
"@radix-ui/react-toggle": "^1.1.8",
|
||||
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@radix-ui/themes": "^2.0.3",
|
||||
"axios": "^1.4.0",
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"recharts": "^2.7.2",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"axios": "^1.9.0",
|
||||
"chart.js": "^4.4.9",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"react-chartjs-2": "^5.3.0"
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18.3.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"recharts": "^2.15.3",
|
||||
"shadcn": "^2.5.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.25.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20230821.0",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"globals": "^13.20.0",
|
||||
"@cloudflare/workers-types": "^4.20250520.0",
|
||||
"@types/node": "^22.15.19",
|
||||
"@types/react": "^18.3.21",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^13.24.0",
|
||||
"tw-animate-css": "^1.3.0",
|
||||
"typescript": "~5.1.6",
|
||||
"vite": "^4.4.5"
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Card, Flex, Text, Badge, Tabs } from "@radix-ui/themes";
|
||||
import { Box, Card, Flex, Text } from "@radix-ui/themes";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger,Badge } from "./ui";
|
||||
import { GlobeIcon } from "@radix-ui/react-icons";
|
||||
import MetricsChart from "./MetricsChart";
|
||||
import "../styles/components.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AgentCardProps, MetricType } from "../types";
|
||||
|
||||
@@ -66,7 +66,7 @@ const AgentCard = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<Card className="agent-card">
|
||||
<Card>
|
||||
<Flex justify="between" align="center" p="4">
|
||||
<Flex direction="column" gap="1">
|
||||
<Flex align="center" gap="2">
|
||||
@@ -105,17 +105,17 @@ const AgentCard = ({
|
||||
|
||||
<Box p="2" pt="0">
|
||||
{/* 指标图表区域 */}
|
||||
<Tabs.Root defaultValue="cpu">
|
||||
<Tabs.List>
|
||||
<Tabs defaultValue="cpu">
|
||||
<TabsList>
|
||||
{metricTypes.map((type) => (
|
||||
<Tabs.Trigger key={type} value={type}>
|
||||
<TabsTrigger key={type} value={type}>
|
||||
{t(`agent.metrics.${type}.title`) || type.toUpperCase()}
|
||||
</Tabs.Trigger>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</TabsList>
|
||||
|
||||
{metricTypes.map((type) => (
|
||||
<Tabs.Content key={type} value={type}>
|
||||
<TabsContent key={type} value={type}>
|
||||
<MetricsChart
|
||||
history={agent.metrics}
|
||||
metricType={type}
|
||||
@@ -124,9 +124,9 @@ const AgentCard = ({
|
||||
networkInterface="en0"
|
||||
loadType="1"
|
||||
/>
|
||||
</Tabs.Content>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs.Root>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from "react";
|
||||
import { DropdownMenu, Button, Text, Flex } from "@radix-ui/themes";
|
||||
import { Text, Flex } from "@radix-ui/themes";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "./ui";
|
||||
import { GlobeIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import { useLanguage } from "../providers/LanguageProvider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -9,20 +15,22 @@ const LanguageSelector: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="2" className="nav-button">
|
||||
<Flex align="center" gap="1">
|
||||
<GlobeIcon width="14" height="14" />
|
||||
<Text size="2" className="language-text">
|
||||
{currentLanguage === "zh-CN" ? "中文" : "English"}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Flex
|
||||
align="center"
|
||||
gap="1"
|
||||
className="inline-flex items-center justify-center rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<GlobeIcon width="14" height="14" />
|
||||
<Text size="2">
|
||||
{currentLanguage === "zh-CN" ? "中文" : "English"}
|
||||
</Text>
|
||||
</Flex>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{availableLanguages.map((lang) => (
|
||||
<DropdownMenu.Item
|
||||
<DropdownMenuItem
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
>
|
||||
@@ -39,10 +47,10 @@ const LanguageSelector: React.FC = () => {
|
||||
<CheckIcon width="14" height="14" />
|
||||
)}
|
||||
</Flex>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Box, Flex, Text, Container, Theme, Separator } from "@radix-ui/themes";
|
||||
import { Box, Flex, Text, Container, Theme } from "@radix-ui/themes";
|
||||
import { Separator, Button, Toaster } from "./ui";
|
||||
import Navbar from "./Navbar";
|
||||
import "../styles/components.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faRss } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faYoutube } from "@fortawesome/free-brands-svg-icons";
|
||||
@@ -18,65 +18,72 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
return (
|
||||
<Theme appearance="light">
|
||||
<Flex direction="column" className="layout-container">
|
||||
<Flex direction="column" >
|
||||
{/* 顶部导航栏 */}
|
||||
<Navbar />
|
||||
|
||||
{/* 主要内容 */}
|
||||
<Box className="main-content">{children}</Box>
|
||||
<Box>{children}</Box>
|
||||
|
||||
{/* 页脚 */}
|
||||
<Box className="footer">
|
||||
<Box>
|
||||
<Container>
|
||||
<Separator size="4" mb="4" color="gray" />
|
||||
<Separator color="gray" />
|
||||
<Flex justify="center" align="center" py="3" direction="column">
|
||||
<Text size="2" color="gray">
|
||||
{t("footer.copyright", { year: currentYear })}
|
||||
</Text>
|
||||
<Flex gap="3" mt="2">
|
||||
<a
|
||||
href="https://zaunist.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faRss}
|
||||
size="lg"
|
||||
className="footer-link-icon"
|
||||
/>
|
||||
<Text size="2">{t("footer.blog")}</Text>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/@zaunist"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faYoutube}
|
||||
size="lg"
|
||||
className="footer-link-icon"
|
||||
/>
|
||||
<Text size="2">{t("footer.youtube")}</Text>
|
||||
</a>
|
||||
<a
|
||||
href="https://mail.mdzz.uk"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEnvelope}
|
||||
size="lg"
|
||||
className="footer-link-icon"
|
||||
/>
|
||||
<Text size="2">{t("footer.tempMail")}</Text>
|
||||
</a>
|
||||
<Button variant="link" asChild>
|
||||
<a
|
||||
href="https://zaunist.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faRss}
|
||||
size="lg"
|
||||
|
||||
/>
|
||||
<Text size="2">{t("footer.blog")}</Text>
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="link" asChild>
|
||||
<a
|
||||
href="https://www.youtube.com/@zaunist"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faYoutube}
|
||||
size="lg"
|
||||
|
||||
/>
|
||||
<Text size="2">{t("footer.youtube")}</Text>
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="link" asChild>
|
||||
<a
|
||||
href="https://mail.mdzz.uk"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEnvelope}
|
||||
size="lg"
|
||||
|
||||
/>
|
||||
<Text size="2">{t("footer.tempMail")}</Text>
|
||||
</a>
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
<Toaster />
|
||||
</Flex>
|
||||
</Theme>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,14 @@ import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Box, Select, Flex } from "@radix-ui/themes";
|
||||
import { Box, Flex } from "@radix-ui/themes";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectTrigger,
|
||||
SelectItem,
|
||||
SelectValue, // 添加 SelectValue 到导入列表
|
||||
} from "./ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { MetricHistory, MetricType } from "../types";
|
||||
@@ -976,19 +983,21 @@ const MetricsChart: React.FC<MetricsChartProps> = ({
|
||||
if (metricType === "disk" && availableDiskDevices.length > 0) {
|
||||
return (
|
||||
<Box mb="2">
|
||||
<Select.Root
|
||||
<Select
|
||||
value={selectedDiskDevice}
|
||||
onValueChange={setSelectedDiskDevice}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableDiskDevices.map((device) => (
|
||||
<Select.Item key={device.value} value={device.value}>
|
||||
<SelectItem key={device.value} value={device.value}>
|
||||
{device.label}
|
||||
</Select.Item>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -996,35 +1005,39 @@ const MetricsChart: React.FC<MetricsChartProps> = ({
|
||||
if (metricType === "network" && availableNetworkInterfaces.length > 0) {
|
||||
return (
|
||||
<Flex gap="2" mb="2" align="center">
|
||||
<Select.Root
|
||||
<Select
|
||||
value={selectedNetworkInterface}
|
||||
onValueChange={setSelectedNetworkInterface}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableNetworkInterfaces.map((iface) => (
|
||||
<Select.Item key={iface.value} value={iface.value}>
|
||||
<SelectItem key={iface.value} value={iface.value}>
|
||||
{iface.label}
|
||||
</Select.Item>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Select.Root
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedNetworkMetric}
|
||||
onValueChange={(value) =>
|
||||
setSelectedNetworkMetric(value as "received" | "sent")
|
||||
}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Item value="received">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="received">
|
||||
{t("agent.metrics.network.received")}
|
||||
</Select.Item>
|
||||
<Select.Item value="sent">
|
||||
</SelectItem>
|
||||
<SelectItem value="sent">
|
||||
{t("agent.metrics.network.sent")}
|
||||
</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1033,25 +1046,23 @@ const MetricsChart: React.FC<MetricsChartProps> = ({
|
||||
if (metricType === "load") {
|
||||
return (
|
||||
<Box mb="2">
|
||||
<Select.Root
|
||||
<Select
|
||||
value={selectedLoadType}
|
||||
onValueChange={(value) =>
|
||||
setSelectedLoadType(value as "1" | "5" | "15")
|
||||
}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Item value="1">
|
||||
{t("agent.metrics.load.1min")}
|
||||
</Select.Item>
|
||||
<Select.Item value="5">
|
||||
{t("agent.metrics.load.5min")}
|
||||
</Select.Item>
|
||||
<Select.Item value="15">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">{t("agent.metrics.load.1min")}</SelectItem>
|
||||
<SelectItem value="5">{t("agent.metrics.load.5min")}</SelectItem>
|
||||
<SelectItem value="15">
|
||||
{t("agent.metrics.load.15min")}
|
||||
</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Box, Card, Flex, Text, Badge } from "@radix-ui/themes";
|
||||
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||
import {
|
||||
CheckCircledIcon,
|
||||
CrossCircledIcon,
|
||||
QuestionMarkCircledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Card, Badge } from "./ui";
|
||||
import { MonitorWithDailyStatsAndStatusHistory } from "../types/monitors";
|
||||
import "../styles/components.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import StatusBar from "./StatusBar";
|
||||
import ResponseTimeChart from "./ResponseTimeChart";
|
||||
@@ -60,7 +60,7 @@ const MonitorCard = ({ monitor }: MonitorCardProps) => {
|
||||
const currentStatus = monitor.status || "pending";
|
||||
|
||||
return (
|
||||
<Card className="monitor-card">
|
||||
<Card>
|
||||
<Flex justify="between" align="start" p="4" gap="2" direction="column">
|
||||
<Flex justify="between" align="center" style={{ width: "100%" }}>
|
||||
<Flex align="center" gap="2">
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||
import { Box, Flex, Text, Container } from "@radix-ui/themes";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
Separator,
|
||||
Button,
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
DropdownMenu,
|
||||
Separator,
|
||||
Container,
|
||||
} from "@radix-ui/themes";
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from "./ui";
|
||||
import {
|
||||
ExitIcon,
|
||||
PersonIcon,
|
||||
@@ -22,7 +25,6 @@ import {
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useAuth } from "../providers/AuthProvider";
|
||||
import { useState, useEffect } from "react";
|
||||
import "../styles/components.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LanguageSelector from "./LanguageSelector";
|
||||
|
||||
@@ -61,18 +63,18 @@ const Navbar = () => {
|
||||
|
||||
return (
|
||||
<Box className={`navbar-wrapper ${isScrolled ? "scrolled" : ""}`}>
|
||||
<Box className="navbar-container">
|
||||
<Container size="3">
|
||||
<Box >
|
||||
<Container size="4">
|
||||
<Flex
|
||||
justify="between"
|
||||
align="center"
|
||||
py="2"
|
||||
className="navbar-content"
|
||||
|
||||
>
|
||||
{/* Logo 部分 */}
|
||||
<Link to="/" className="navbar-logo-link">
|
||||
<Link to="/" >
|
||||
<Flex align="center" gap="2">
|
||||
<Box className="navbar-logo">
|
||||
<Box >
|
||||
<PieChartIcon width="20" height="20" />
|
||||
</Box>
|
||||
<Text size="4" weight="bold">
|
||||
@@ -85,14 +87,14 @@ const Navbar = () => {
|
||||
<Flex align="center" gap="2">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Flex className="navbar-links" align="center">
|
||||
<Flex align="center">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className={`nav-link ${
|
||||
isActive("/dashboard") ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<Button variant="ghost" size="2" className="nav-button">
|
||||
<Button variant="ghost">
|
||||
<DashboardIcon width="14" height="14" />
|
||||
<Text ml="1" size="2">
|
||||
{t("navbar.dashboard")}
|
||||
@@ -106,7 +108,7 @@ const Navbar = () => {
|
||||
isActive("/monitors") ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<Button variant="ghost" size="2" className="nav-button">
|
||||
<Button variant="ghost">
|
||||
<ActivityLogIcon width="14" height="14" />
|
||||
<Text ml="1" size="2">
|
||||
{t("navbar.apiMonitors")}
|
||||
@@ -120,7 +122,7 @@ const Navbar = () => {
|
||||
isActive("/agents") ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<Button variant="ghost" size="2" className="nav-button">
|
||||
<Button variant="ghost">
|
||||
<CubeIcon width="14" height="14" />
|
||||
<Text ml="1" size="2">
|
||||
{t("navbar.agentMonitors")}
|
||||
@@ -134,7 +136,7 @@ const Navbar = () => {
|
||||
isActive("/status/config") ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<Button variant="ghost" size="2" className="nav-button">
|
||||
<Button variant="ghost">
|
||||
<PieChartIcon width="14" height="14" />
|
||||
<Text ml="1" size="2">
|
||||
{t("navbar.statusPage")}
|
||||
@@ -148,7 +150,7 @@ const Navbar = () => {
|
||||
isActive("/notifications") ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<Button variant="ghost" size="2" className="nav-button">
|
||||
<Button variant="ghost">
|
||||
<BellIcon width="14" height="14" />
|
||||
<Text ml="1" size="2">
|
||||
{t("navbar.notifications")}
|
||||
@@ -157,83 +159,80 @@ const Navbar = () => {
|
||||
</Link>
|
||||
</Flex>
|
||||
|
||||
<Separator orientation="vertical" mx="3" />
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
{/* 语言选择器 */}
|
||||
<LanguageSelector />
|
||||
|
||||
<Separator orientation="vertical" mx="3" />
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" className="user-menu-button">
|
||||
<Flex align="center" gap="1">
|
||||
<Avatar
|
||||
size="1"
|
||||
radius="full"
|
||||
fallback={
|
||||
user?.username?.charAt(0).toUpperCase() || "U"
|
||||
}
|
||||
color="blue"
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Flex
|
||||
align="center"
|
||||
gap="1"
|
||||
className="inline-flex items-center justify-center rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
alt={user?.username}
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
<Box display={{ initial: "none", sm: "block" }}>
|
||||
<Text size="2">{user?.username}</Text>
|
||||
</Box>
|
||||
<ChevronDownIcon width="12" height="12" />
|
||||
</Flex>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" className="user-dropdown">
|
||||
<DropdownMenu.Label>
|
||||
</Avatar>
|
||||
<Box display={{ initial: "none", sm: "block" }}>
|
||||
<Text size="2">{user?.username}</Text>
|
||||
</Box>
|
||||
<ChevronDownIcon width="12" height="12" />
|
||||
</Flex>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="user-dropdown">
|
||||
<DropdownMenuLabel>
|
||||
<Text size="1" color="gray">
|
||||
{t("navbar.loggedInAs")}
|
||||
</Text>
|
||||
<Text size="2" weight="bold">
|
||||
{user?.username}
|
||||
</Text>
|
||||
</DropdownMenu.Label>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{user?.role === "admin" && (
|
||||
<DropdownMenu.Item onClick={() => navigate("/users")}>
|
||||
<DropdownMenuItem onClick={() => navigate("/users")}>
|
||||
<Flex gap="2" align="center">
|
||||
<UserIcon width="14" height="14" />
|
||||
<Text size="2">{t("navbar.userManagement")}</Text>
|
||||
</Flex>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenu.Item onClick={() => navigate("/profile")}>
|
||||
<DropdownMenuItem onClick={() => navigate("/profile")}>
|
||||
<Flex gap="2" align="center">
|
||||
<PersonIcon width="14" height="14" />
|
||||
<Text size="2">{t("navbar.profile")}</Text>
|
||||
</Flex>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenu.Item color="red" onClick={handleLogout}>
|
||||
<DropdownMenuItem color="red" onClick={handleLogout}>
|
||||
<Flex gap="2" align="center">
|
||||
<ExitIcon width="14" height="14" />
|
||||
<Text size="2">{t("navbar.logout")}</Text>
|
||||
</Flex>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : (
|
||||
<Flex gap="2" align="center">
|
||||
{/* 语言选择器 */}
|
||||
<LanguageSelector />
|
||||
|
||||
<Separator orientation="vertical" mx="2" />
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigate("/login")}
|
||||
size="2"
|
||||
>
|
||||
<Button variant="ghost" onClick={() => navigate("/login")}>
|
||||
{t("navbar.login")}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Box, Tooltip, Text, Dialog, Flex, Button } from "@radix-ui/themes";
|
||||
import { Box, Text, Flex } from "@radix-ui/themes";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
DialogTitle,
|
||||
DialogClose,
|
||||
} from "./ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DailyStats } from "../types/monitors";
|
||||
|
||||
@@ -18,7 +29,8 @@ interface StatusBarProps {
|
||||
*/
|
||||
const StatusBar: React.FC<StatusBarProps> = ({ dailyStats = [] }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedDayData, setSelectedDayData] = useState<EnrichedDailyStats | null>(null);
|
||||
const [selectedDayData, setSelectedDayData] =
|
||||
useState<EnrichedDailyStats | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// console.log("StatusBar组件的dailyStats: ", dailyStats); // 调试信息可以暂时注释或移除
|
||||
@@ -95,13 +107,13 @@ const StatusBar: React.FC<StatusBarProps> = ({ dailyStats = [] }) => {
|
||||
>
|
||||
{dailyHistory?.map((dayData) => {
|
||||
// 确保 dayData 和 monitor_id 存在
|
||||
const key = dayData && dayData.monitor_id
|
||||
? `${dayData.monitor_id}-${dayData.date}-${Math.random()}`
|
||||
: `day-${dayData?.date}-${Math.random()}`;
|
||||
const key =
|
||||
dayData && dayData.monitor_id
|
||||
? `${dayData.monitor_id}-${dayData.date}-${Math.random()}`
|
||||
: `day-${dayData?.date}-${Math.random()}`;
|
||||
return (
|
||||
<Tooltip
|
||||
key={key}
|
||||
content={
|
||||
<Tooltip key={key}>
|
||||
<TooltipContent>
|
||||
<>
|
||||
<Text as="span" size="1" mb="1">
|
||||
{t("common.date")}:{" "}
|
||||
@@ -122,34 +134,41 @@ const StatusBar: React.FC<StatusBarProps> = ({ dailyStats = [] }) => {
|
||||
{dayData.availability.toFixed(2)}%
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "50px",
|
||||
backgroundColor: getColor(dayData.status),
|
||||
borderRadius: "2px",
|
||||
transition: "background-color 0.2s",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
}}
|
||||
onClick={() => handleDayClick(dayData)}
|
||||
/>
|
||||
</TooltipContent>
|
||||
<TooltipTrigger>
|
||||
<Box
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "50px",
|
||||
backgroundColor: getColor(dayData.status),
|
||||
borderRadius: "2px",
|
||||
transition: "background-color 0.2s",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
}}
|
||||
onClick={() => handleDayClick(dayData)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{selectedDayData && (
|
||||
<Dialog.Root open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog.Trigger />
|
||||
<Dialog.Content style={{ maxWidth: 450 }}>
|
||||
<Dialog.Title>
|
||||
{t(`📅 ${new Date(selectedDayData.date).toLocaleDateString()} ${t("common.status")}: ${selectedDayData.status === "up"
|
||||
? t("monitor.status.normal")
|
||||
: t("monitor.status.failure")}`)}
|
||||
</Dialog.Title>
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<DialogTrigger />
|
||||
<DialogContent style={{ maxWidth: 450 }}>
|
||||
<DialogTitle>
|
||||
{t(
|
||||
`📅 ${new Date(selectedDayData.date).toLocaleDateString()} ${t(
|
||||
"common.status"
|
||||
)}: ${
|
||||
selectedDayData.status === "up"
|
||||
? t("monitor.status.normal")
|
||||
: t("monitor.status.failure")
|
||||
}`
|
||||
)}
|
||||
</DialogTitle>
|
||||
|
||||
<Flex direction="column" gap="3">
|
||||
<Text as="div" size="2">
|
||||
@@ -183,14 +202,14 @@ const StatusBar: React.FC<StatusBarProps> = ({ dailyStats = [] }) => {
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="soft" color="gray">
|
||||
<DialogClose>
|
||||
<Button variant="secondary" color="gray">
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</DialogClose>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Select, Text } from "@radix-ui/themes";
|
||||
import { Text } from "@radix-ui/themes";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectSeparator,
|
||||
SelectValue,
|
||||
SelectGroup,
|
||||
} from "./ui";
|
||||
|
||||
// 状态码选项定义
|
||||
export const specificStatusCodes = [
|
||||
@@ -61,47 +71,32 @@ const StatusCodeSelect = ({
|
||||
}: StatusCodeSelectProps) => {
|
||||
return (
|
||||
<>
|
||||
<Select.Root
|
||||
<Select
|
||||
name={name}
|
||||
value={value.toString()}
|
||||
onValueChange={(value) => onChange(parseInt(value))}
|
||||
required={required}
|
||||
>
|
||||
<Select.Trigger style={{ width: "100%" }} />
|
||||
<Select.Content position="popper" sideOffset={5}>
|
||||
<SelectTrigger style={{ width: "100%" }}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" sideOffset={5}>
|
||||
{specificStatusCodes.map((group) => (
|
||||
<Select.Group key={group.group}>
|
||||
<Select.Label
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
padding: "8px 12px",
|
||||
color: "var(--gray-10)",
|
||||
}}
|
||||
>
|
||||
{group.group}
|
||||
</Select.Label>
|
||||
<SelectGroup key={group.group}>
|
||||
<SelectLabel>{group.group}</SelectLabel>
|
||||
{group.codes.map((code) => (
|
||||
<Select.Item
|
||||
key={code.value}
|
||||
value={code.value.toString()}
|
||||
style={{
|
||||
padding: code.isRange
|
||||
? "8px 12px 8px 30px"
|
||||
: "8px 12px 8px 40px",
|
||||
fontWeight: code.isRange ? "bold" : "normal",
|
||||
}}
|
||||
>
|
||||
<SelectItem key={code.value} value={code.value.toString()}>
|
||||
{code.label}
|
||||
</Select.Item>
|
||||
</SelectItem>
|
||||
))}
|
||||
{group !==
|
||||
specificStatusCodes[specificStatusCodes.length - 1] && (
|
||||
<Select.Separator style={{ margin: "8px 0" }} />
|
||||
<SelectSeparator style={{ margin: "8px 0" }} />
|
||||
)}
|
||||
</Select.Group>
|
||||
</SelectGroup>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Text size="1" color="gray" style={{ marginTop: "4px" }}>
|
||||
选择预期的HTTP状态码或状态码范围
|
||||
</Text>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Card, Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { Box, Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { Card } from "./ui";
|
||||
import { ReactNode } from "react";
|
||||
import "../styles/components.css";
|
||||
|
||||
interface StatusItem {
|
||||
icon: ReactNode;
|
||||
@@ -30,14 +30,7 @@ const StatusSummaryCard = ({ title, items }: StatusSummaryCardProps) => {
|
||||
{items.map((item, index) => (
|
||||
<Flex key={index} justify="between" align="center">
|
||||
<Flex align="center" gap="2">
|
||||
<Box
|
||||
className="status-icon-container"
|
||||
style={{
|
||||
background: item.bgColor || "var(--gray-3)",
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
}}
|
||||
>
|
||||
<Box className="status-icon-container">
|
||||
<Box style={{ color: item.iconColor }}>{item.icon}</Box>
|
||||
</Box>
|
||||
<Text size="2">{item.label}</Text>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
|
||||
const AlertDialogTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Trigger>
|
||||
>(({ ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Trigger ref={ref} {...props} />
|
||||
));
|
||||
AlertDialogTrigger.displayName = AlertDialogPrimitive.Trigger.displayName;
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80", // Simplified background for example, original had animations
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
// "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AlertDialogPrimitive.Content>
|
||||
</AlertDialogPortal>
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
// "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
// className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName;
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||
// className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -0,0 +1,46 @@
|
||||
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 badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,58 @@
|
||||
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 transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col rounded-xl border py-4 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } 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<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export * from "./card"
|
||||
export * from "./textarea"
|
||||
export * from "./badge"
|
||||
export * from "./button"
|
||||
export * from "./tabs"
|
||||
export * from "./avatar"
|
||||
export * from "./dropdown-menu"
|
||||
export * from "./separator"
|
||||
export * from "./select"
|
||||
export * from "./tooltip"
|
||||
export * from "./dialog"
|
||||
export * from "./switch"
|
||||
export * from "./alert-dialog"
|
||||
export * from "./textarea"
|
||||
export * from "./table"
|
||||
export * from "./checkbox"
|
||||
export * from "./sonner"
|
||||
export * from "./input"
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,183 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator-root"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn(
|
||||
"flex flex-col outline-none shadow-none", // 修改点:添加 outline-none shadow-none
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 py-0 pl-1 pr-2 shadow-none outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
// 基础布局和排版样式
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium",
|
||||
// 为所有可动画属性启用平滑过渡
|
||||
"transition-all",
|
||||
// focus-visible 状态:提供清晰的聚焦环,增强可访问性
|
||||
// 使用具体的颜色替换 ring-ring 和 ring-offset-background
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
|
||||
// 禁用状态的样式
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
// 默认(非激活)状态的样式:
|
||||
// - 使用具体的颜色替换 text-muted-foreground 和 hover:text-primary
|
||||
"text-slate-500 dark:text-slate-400 hover:text-gray-600 dark:hover:text-gray-400 border-2 border-transparent",
|
||||
// 激活(选中)状态的样式:
|
||||
// - 使用具体的颜色替换 data-[state=active]:text-primary
|
||||
"data-[state=active]:text-gray-700 dark:data-[state=active]:text-gray-400 data-[state=active]:font-semibold",
|
||||
// - 使用具体的颜色替换 data-[state=active]:bg-primary/10
|
||||
"data-[state=active]:bg-gray-100 dark:data-[state=active]:bg-gray-500/20", // 示例:浅蓝色背景
|
||||
// - 使用具体的颜色替换 data-[state=active]:border-primary
|
||||
"data-[state=active]:border-gray-600 dark:data-[state=active]:border-gray-500",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn(
|
||||
"flex-1 outline-none",
|
||||
"pt-2", // padding-top: 24px
|
||||
"shadow-none", // box-shadow: none
|
||||
"border-0", // border: none (ensuring no borders are applied)
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { RouterProvider } from "react-router-dom";
|
||||
import { AuthProvider } from "./providers/AuthProvider";
|
||||
import { LanguageProvider } from "./providers/LanguageProvider";
|
||||
import "@radix-ui/themes/styles.css";
|
||||
import "./styles/index.css";
|
||||
import "./locales/config";
|
||||
import "./styles/global.css";
|
||||
import "./i18n/config";
|
||||
import router from "./router";
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { Monitor, Agent } from "../types";
|
||||
import { getDashboardData } from "../api/dashboard";
|
||||
import StatusSummaryCard from "../components/StatusSummaryCard";
|
||||
import "../styles/components.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Dashboard = () => {
|
||||
@@ -106,8 +105,8 @@ const Dashboard = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Box className="dashboard-container">
|
||||
<Container size="3" py="5">
|
||||
<Box>
|
||||
<Container size="4">
|
||||
<Box>
|
||||
{/* 状态摘要 */}
|
||||
<Box pb="6">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, Flex, Heading, Text, Button, Card, Grid } from "@radix-ui/themes";
|
||||
import { Box, Flex, Heading, Text, Grid } from "@radix-ui/themes";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ActivityLogIcon,
|
||||
@@ -33,15 +34,15 @@ const Home = () => {
|
||||
|
||||
<Flex gap="4" mt="4">
|
||||
{isAuthenticated ? (
|
||||
<Button size="3" asChild>
|
||||
<Button asChild>
|
||||
<Link to="/dashboard">{t("home.getStarted")}</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button size="3" asChild>
|
||||
<Button asChild>
|
||||
<Link to="/login">{t("home.getStarted")}</Link>
|
||||
</Button>
|
||||
<Button size="3" asChild>
|
||||
<Button asChild>
|
||||
<a
|
||||
href="https://github.com/zaunist/xugou"
|
||||
target="_blank"
|
||||
@@ -61,28 +62,28 @@ const Home = () => {
|
||||
{t("home.features.title")}
|
||||
</Heading>
|
||||
<Grid columns={{ initial: "1", sm: "2", md: "4" }} gap="4">
|
||||
<Card size="3">
|
||||
<Card>
|
||||
<Flex direction="column" gap="2" align="center" p="4">
|
||||
<ActivityLogIcon width="32" height="32" />
|
||||
<Heading size="4">{t("home.features.monitoring")}</Heading>
|
||||
<Text align="center">{t("home.features.monitoring.desc")}</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card size="3">
|
||||
<Card>
|
||||
<Flex direction="column" gap="2" align="center" p="4">
|
||||
<DesktopIcon width="32" height="32" />
|
||||
<Heading size="4">{t("home.features.dashboard")}</Heading>
|
||||
<Text align="center">{t("home.features.dashboard.desc")}</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card size="3">
|
||||
<Card>
|
||||
<Flex direction="column" gap="2" align="center" p="4">
|
||||
<LightningBoltIcon width="32" height="32" />
|
||||
<Heading size="4">{t("home.features.alerts")}</Heading>
|
||||
<Text align="center">{t("home.features.alerts.desc")}</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card size="3">
|
||||
<Card>
|
||||
<Flex direction="column" gap="2" align="center" p="4">
|
||||
<MixerHorizontalIcon width="32" height="32" />
|
||||
<Heading size="4">{t("home.features.statusPage")}</Heading>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex, Heading, Text, Button } from "@radix-ui/themes";
|
||||
import { Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { Button } from "@/components/ui";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,7 +26,7 @@ const NotFound = () => {
|
||||
<Text align="center" style={{ maxWidth: "500px" }}>
|
||||
{t("notFound.message")}
|
||||
</Text>
|
||||
<Button size="3" mt="4" asChild>
|
||||
<Button asChild>
|
||||
<Link to="/">{t("notFound.button")}</Link>
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Card,
|
||||
Badge,
|
||||
Grid,
|
||||
Avatar,
|
||||
} from "@radix-ui/themes";
|
||||
import { Box, Flex, Heading, Text, Grid, Container } from "@radix-ui/themes";
|
||||
|
||||
import { Button, Card, Badge, Avatar, AvatarFallback } from "@/components/ui";
|
||||
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
Pencil1Icon,
|
||||
@@ -23,15 +16,11 @@ import {
|
||||
GlobeIcon,
|
||||
Link2Icon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import * as Toast from "@radix-ui/react-toast";
|
||||
import {
|
||||
getAgent,
|
||||
deleteAgent,
|
||||
getAgentMetrics,
|
||||
} from "../../api/agents";
|
||||
import { getAgent, deleteAgent, getAgentMetrics } from "../../api/agents";
|
||||
import { Agent } from "../../types/agents";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AgentCard from "../../components/AgentCard";
|
||||
import { toast } from "sonner"; // Added
|
||||
|
||||
// 定义客户端状态颜色映射
|
||||
const statusColors: Record<string, "red" | "green" | "yellow" | "gray"> = {
|
||||
@@ -48,9 +37,6 @@ const AgentDetail = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState("");
|
||||
const [toastType, setToastType] = useState<"success" | "error">("success");
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fetchAgentData = async () => {
|
||||
@@ -141,33 +127,21 @@ const AgentDetail = () => {
|
||||
|
||||
try {
|
||||
setDeleteLoading(true);
|
||||
// 显示正在删除的Toast消息
|
||||
setToastMessage(t("agent.deleting"));
|
||||
setToastType("success");
|
||||
setToastOpen(true);
|
||||
toast.loading(t("agent.deleting")); // Added
|
||||
|
||||
const response = await deleteAgent(Number(id));
|
||||
|
||||
if (response.success) {
|
||||
// 显示删除成功的Toast消息
|
||||
setToastMessage(t("agent.deleteSuccess"));
|
||||
setToastType("success");
|
||||
setToastOpen(true);
|
||||
|
||||
// 延长延迟时间,确保Toast消息有足够时间显示
|
||||
toast.success(t("agent.deleteSuccess"));
|
||||
setTimeout(() => {
|
||||
navigate("/agents");
|
||||
}, 3000);
|
||||
} else {
|
||||
setToastMessage(response.message || t("agent.deleteError"));
|
||||
setToastType("error");
|
||||
setToastOpen(true);
|
||||
toast.error(response.message || t("agent.deleteError"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除客户端失败:", error);
|
||||
setToastMessage(t("agent.deleteError"));
|
||||
setToastType("error");
|
||||
setToastOpen(true);
|
||||
toast.error(t("agent.deleteError"));
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
@@ -176,7 +150,7 @@ const AgentDetail = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Box>
|
||||
<Flex className="loading-container">
|
||||
<Flex>
|
||||
<Text>{t("agents.loadingDetail")}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
@@ -186,7 +160,7 @@ const AgentDetail = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Box>
|
||||
<Flex className="error-container">
|
||||
<Flex>
|
||||
<Card>
|
||||
<Flex direction="column" align="center" gap="4">
|
||||
<Heading size="6">{t("common.loadingError")}</Heading>
|
||||
@@ -204,7 +178,7 @@ const AgentDetail = () => {
|
||||
if (!agent) {
|
||||
return (
|
||||
<Box>
|
||||
<Flex justify="center" align="center" style={{ minHeight: "60vh" }}>
|
||||
<Flex justify="center" align="center">
|
||||
<Card>
|
||||
<Flex direction="column" align="center" gap="4">
|
||||
<Heading size="6">{t("agents.notFound")}</Heading>
|
||||
@@ -220,10 +194,10 @@ const AgentDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="page-container detail-page">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Container size="4">
|
||||
<Flex justify="between" align="center">
|
||||
<Flex align="center" gap="2">
|
||||
<Button variant="soft" size="1" onClick={() => navigate("/agents")}>
|
||||
<Button variant="secondary" onClick={() => navigate("/agents")}>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Heading size="6">{t("agent.details")}</Heading>
|
||||
@@ -234,17 +208,23 @@ const AgentDetail = () => {
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Flex gap="2">
|
||||
<Button variant="soft" onClick={handleRefresh} disabled={loading}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
<ReloadIcon />
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
<Button variant="soft" onClick={() => navigate(`/agents/edit/${id}`)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/agents/edit/${id}`)}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
{t("agent.edit")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="soft"
|
||||
color="red"
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
@@ -254,10 +234,12 @@ const AgentDetail = () => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Box className="detail-content">
|
||||
<Card mb="4">
|
||||
<Box>
|
||||
<Card>
|
||||
<Flex align="center" gap="4" mb="4">
|
||||
<Avatar size="5" fallback={agent.name.charAt(0)} color="indigo" />
|
||||
<Avatar>
|
||||
<AvatarFallback>{agent.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Heading size="5">{agent.name}</Heading>
|
||||
</Box>
|
||||
@@ -415,33 +397,7 @@ const AgentDetail = () => {
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Toast.Provider swipeDirection="right">
|
||||
<Toast.Root
|
||||
className={`ToastRoot`}
|
||||
open={toastOpen}
|
||||
onOpenChange={setToastOpen}
|
||||
duration={3000}
|
||||
style={{
|
||||
backgroundColor:
|
||||
toastType === "success" ? "var(--green-9)" : "var(--red-9)",
|
||||
borderRadius: "8px",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Toast.Title className="ToastTitle">
|
||||
{toastType === "success" ? t("common.success") : t("common.error")}
|
||||
</Toast.Title>
|
||||
<Toast.Description className="ToastDescription">
|
||||
{toastMessage}
|
||||
</Toast.Description>
|
||||
<Toast.Close className="ToastClose">
|
||||
<Cross2Icon />
|
||||
</Toast.Close>
|
||||
</Toast.Root>
|
||||
<Toast.Viewport className="ToastViewport" />
|
||||
</Toast.Provider>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,15 +5,29 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
IconButton,
|
||||
Grid,
|
||||
Container,
|
||||
} from "@radix-ui/themes";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Badge,
|
||||
IconButton,
|
||||
Dialog,
|
||||
Grid,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
Tabs,
|
||||
} from "@radix-ui/themes";
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui";
|
||||
|
||||
import {
|
||||
PlusIcon,
|
||||
Pencil1Icon,
|
||||
@@ -23,11 +37,7 @@ import {
|
||||
ViewGridIcon,
|
||||
TrashIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
getAllAgents,
|
||||
deleteAgent,
|
||||
getAgentMetrics,
|
||||
} from "../../api/agents";
|
||||
import { getAllAgents, deleteAgent, getAgentMetrics } from "../../api/agents";
|
||||
import AgentCard from "../../components/AgentCard";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Agent } from "../../types";
|
||||
@@ -132,17 +142,9 @@ const AgentsList = () => {
|
||||
return (
|
||||
<Grid columns={{ initial: "1" }} gap="4">
|
||||
{agents.map((agent) => (
|
||||
<Box key={agent.id} style={{ position: "relative" }}>
|
||||
<Box key={agent.id} className="relative">
|
||||
<AgentCard agent={agent} />
|
||||
<Flex
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
right: "10px",
|
||||
zIndex: 1,
|
||||
}}
|
||||
gap="2"
|
||||
>
|
||||
<Flex gap="2" className="absolute top-4 right-4">
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
@@ -178,42 +180,28 @@ const AgentsList = () => {
|
||||
// 展示表格视图
|
||||
const renderTableView = () => {
|
||||
return (
|
||||
<Table.Root variant="surface">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("agents.table.name")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("agents.table.host")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("agents.table.ip")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("agents.table.status")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("agents.table.os")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("agents.table.version")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("agents.table.actions")}
|
||||
</Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell>{t("agents.table.name")}</TableCell>
|
||||
<TableCell>{t("agents.table.host")}</TableCell>
|
||||
<TableCell>{t("agents.table.ip")}</TableCell>
|
||||
<TableCell>{t("agents.table.status")}</TableCell>
|
||||
<TableCell>{t("agents.table.os")}</TableCell>
|
||||
<TableCell>{t("agents.table.version")}</TableCell>
|
||||
<TableCell>{t("agents.table.actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{agents.map((agent) => (
|
||||
<Table.Row key={agent.id}>
|
||||
<Table.Cell>
|
||||
<TableRow key={agent.id}>
|
||||
<TableCell>
|
||||
<Text weight="medium">{agent.name}</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{agent.hostname || t("common.notFound")}</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>
|
||||
{agent.ip_addresses
|
||||
? (() => {
|
||||
@@ -230,8 +218,8 @@ const AgentsList = () => {
|
||||
})()
|
||||
: t("common.notFound")}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge color={statusColors[agent.status || "unknown"]}>
|
||||
{agent.status === "active"
|
||||
? t("agent.status.online")
|
||||
@@ -239,14 +227,14 @@ const AgentsList = () => {
|
||||
? t("agent.status.connecting")
|
||||
: t("agent.status.offline")}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{agent.os || t("common.notFound")}</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{agent.version || t("common.notFound")}</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Flex gap="2">
|
||||
<IconButton
|
||||
variant="soft"
|
||||
@@ -268,18 +256,18 @@ const AgentsList = () => {
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
// 加载中显示
|
||||
if (loading) {
|
||||
return (
|
||||
<Box className="page-container detail-page">
|
||||
<Box>
|
||||
<Flex justify="center" align="center" p="4">
|
||||
<Text>{t("common.loading")}</Text>
|
||||
</Flex>
|
||||
@@ -291,12 +279,12 @@ const AgentsList = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Box className="page-container detail-page">
|
||||
<Card mb="4">
|
||||
<Flex p="3" style={{ backgroundColor: "var(--red-3)" }}>
|
||||
<Text style={{ color: "var(--red-9)" }}>{error}</Text>
|
||||
<Card>
|
||||
<Flex>
|
||||
<Text>{error}</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Button variant="soft" onClick={() => window.location.reload()} mt="2">
|
||||
<Button variant="secondary" onClick={() => window.location.reload()}>
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -304,98 +292,101 @@ const AgentsList = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div className="page-container detail-page">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Heading size="6">{t("agents.pageTitle")}</Heading>
|
||||
<Flex gap="3">
|
||||
<Tabs.Root defaultValue="card">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger
|
||||
value="card"
|
||||
onClick={() => setViewMode("card")}
|
||||
title={t("agents.cardView")}
|
||||
>
|
||||
<ViewGridIcon />
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="table"
|
||||
onClick={() => setViewMode("table")}
|
||||
title={t("agents.tableView")}
|
||||
>
|
||||
<LayoutIcon />
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
<Button onClick={handleRefresh} disabled={loading}>
|
||||
<ReloadIcon />
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"点击添加客户端按钮(空列表),准备导航到/agents/create"
|
||||
);
|
||||
try {
|
||||
navigate("/agents/create");
|
||||
} catch (err) {
|
||||
console.error("导航到添加客户端页面失败:", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
{t("agents.create")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<div className="detail-content">
|
||||
{agents.length === 0 ? (
|
||||
<Card>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
p="6"
|
||||
gap="3"
|
||||
<Container size="4">
|
||||
<Flex justify="between" align="center">
|
||||
<Heading size="6">{t("agents.pageTitle")}</Heading>
|
||||
<Flex gap="3">
|
||||
<Tabs defaultValue="card">
|
||||
<TabsList>
|
||||
<TabsTrigger
|
||||
value="card"
|
||||
onClick={() => setViewMode("card")}
|
||||
title={t("agents.cardView")}
|
||||
>
|
||||
<Text>{t("agents.noAgents")}</Text>
|
||||
<Button onClick={() => navigate("/agents/create")}>
|
||||
<PlusIcon />
|
||||
{t("agents.create")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
) : viewMode === "table" ? (
|
||||
// 表格视图
|
||||
renderTableView()
|
||||
) : (
|
||||
// 卡片视图
|
||||
renderCardView()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ViewGridIcon />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="table"
|
||||
onClick={() => setViewMode("table")}
|
||||
title={t("agents.tableView")}
|
||||
>
|
||||
<LayoutIcon />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
<ReloadIcon />
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"点击添加客户端按钮(空列表),准备导航到/agents/create"
|
||||
);
|
||||
try {
|
||||
navigate("/agents/create");
|
||||
} catch (err) {
|
||||
console.error("导航到添加客户端页面失败:", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
{t("agents.create")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Box className="mt-4 space-x-2">
|
||||
{agents.length === 0 ? (
|
||||
<Card>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
p="6"
|
||||
gap="3"
|
||||
>
|
||||
<Text>{t("agents.noAgents")}</Text>
|
||||
<Button onClick={() => navigate("/agents/create")}>
|
||||
<PlusIcon />
|
||||
{t("agents.create")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
) : viewMode === "table" ? (
|
||||
// 表格视图
|
||||
renderTableView()
|
||||
) : (
|
||||
// 卡片视图
|
||||
renderCardView()
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Dialog.Root open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>{t("common.deleteConfirmation")}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>{t("common.deleteConfirmation")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("common.deleteConfirmMessage")}
|
||||
</Dialog.Description>
|
||||
</DialogDescription>
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="soft" color="gray">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary" color="gray">
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</DialogClose>
|
||||
<Button color="red" onClick={handleDeleteConfirm}>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Card,
|
||||
Code,
|
||||
Separator,
|
||||
} from "@radix-ui/themes";
|
||||
import { Box, Flex, Heading, Text, Code } from "@radix-ui/themes";
|
||||
import { Button, Card, Separator } from "@/components/ui";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CopyIcon,
|
||||
@@ -83,177 +75,109 @@ const CreateAgent = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div className="page-container detail-page">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Flex align="center" gap="2">
|
||||
<Button variant="soft" size="1" onClick={() => navigate("/agents")}>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Heading size="6">{t("agent.form.title.create")}</Heading>
|
||||
</Flex>
|
||||
<Flex justify="between" align="center">
|
||||
<Flex align="center" gap="2">
|
||||
<Button variant="secondary" onClick={() => navigate("/agents")}>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Heading size="6">{t("agent.form.title.create")}</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Card>
|
||||
<Flex direction="column" gap="5">
|
||||
{/* 提示信息 */}
|
||||
<Box>
|
||||
<Flex gap="2">
|
||||
<InfoCircledIcon />
|
||||
<Text>{t("agent.add.note")}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<div className="detail-content">
|
||||
<Card>
|
||||
<Flex direction="column" gap="5">
|
||||
{/* 提示信息 */}
|
||||
<Box
|
||||
style={{
|
||||
padding: "10px",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "var(--accent-3)",
|
||||
color: "var(--accent-11)",
|
||||
}}
|
||||
>
|
||||
<Flex gap="2">
|
||||
<InfoCircledIcon />
|
||||
<Text>{t("agent.add.note")}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* 服务端地址 */}
|
||||
<Box>
|
||||
<Text as="label" size="2" weight="bold">
|
||||
{t("agent.add.serverAddress")}
|
||||
</Text>
|
||||
<Flex gap="2">
|
||||
<Text className="token-display">{serverUrl}</Text>
|
||||
<Button variant="secondary" onClick={handleCopyServerUrl}>
|
||||
{serverUrlCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
{serverUrlCopied ? t("common.copied") : t("common.copy")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text size="1" color="gray">
|
||||
{t("agent.add.serverAddressHelp")}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 服务端地址 */}
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
weight="bold"
|
||||
style={{ display: "block", marginBottom: "6px" }}
|
||||
>
|
||||
{t("agent.add.serverAddress")}
|
||||
</Text>
|
||||
{/* 客户端Token */}
|
||||
<Box>
|
||||
<Text as="label" size="2" weight="bold">
|
||||
{t("agent.add.registrationToken")}
|
||||
</Text>
|
||||
{loading ? (
|
||||
<Text>{t("agent.add.generatingToken")}</Text>
|
||||
) : (
|
||||
<>
|
||||
<Flex gap="2">
|
||||
<Text
|
||||
className="token-display"
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "var(--gray-3)",
|
||||
borderRadius: "4px",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
flex: 1,
|
||||
overflowX: "auto",
|
||||
}}
|
||||
>
|
||||
{serverUrl}
|
||||
</Text>
|
||||
<Button variant="soft" onClick={handleCopyServerUrl}>
|
||||
{serverUrlCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
{serverUrlCopied ? t("common.copied") : t("common.copy")}
|
||||
<Text className="token-display">{token}</Text>
|
||||
<Button variant="secondary" onClick={handleCopyToken}>
|
||||
{tokenCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
{tokenCopied ? t("common.copied") : t("common.copy")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text size="1" color="gray" style={{ marginTop: "6px" }}>
|
||||
{t("agent.add.serverAddressHelp")}
|
||||
<Text size="1" color="gray">
|
||||
{t("agent.add.tokenHelp")}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 客户端Token */}
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
weight="bold"
|
||||
style={{ display: "block", marginBottom: "6px" }}
|
||||
>
|
||||
{t("agent.add.registrationToken")}
|
||||
</Text>
|
||||
{loading ? (
|
||||
<Text>{t("agent.add.generatingToken")}</Text>
|
||||
) : (
|
||||
<>
|
||||
<Flex gap="2">
|
||||
<Text
|
||||
className="token-display"
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "var(--gray-3)",
|
||||
borderRadius: "4px",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
flex: 1,
|
||||
overflowX: "auto",
|
||||
}}
|
||||
>
|
||||
{token}
|
||||
</Text>
|
||||
<Button variant="soft" onClick={handleCopyToken}>
|
||||
{tokenCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
{tokenCopied ? t("common.copied") : t("common.copy")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text size="1" color="gray" style={{ marginTop: "6px" }}>
|
||||
{t("agent.add.tokenHelp")}
|
||||
</Text>
|
||||
</>
|
||||
<Separator />
|
||||
|
||||
{/* 安装指南 */}
|
||||
<Box>
|
||||
<Flex align="baseline" gap="2" mb="3">
|
||||
<Heading size="4">{t("agent.add.installGuide")}</Heading>
|
||||
<Text size="2" color="gray">
|
||||
{t(
|
||||
"agent.add.installCommandHelpText",
|
||||
"使用以下命令自动下载并安装 Agent:"
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Separator size="4" />
|
||||
|
||||
{/* 安装指南 */}
|
||||
<Box>
|
||||
<Flex align="baseline" gap="2" mb="3">
|
||||
<Heading size="4">{t("agent.add.installGuide")}</Heading>
|
||||
<Text size="2" color="gray">
|
||||
{t(
|
||||
"agent.add.installCommandHelpText",
|
||||
"使用以下命令自动下载并安装 Agent:"
|
||||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Card
|
||||
variant="surface"
|
||||
style={{ backgroundColor: "var(--gray-2)" }}
|
||||
>
|
||||
<Flex direction="column" gap="3" p="3">
|
||||
<Text as="div" size="2" weight="bold" mb="1">
|
||||
{t(
|
||||
"agent.add.oneLinerCommandTitle",
|
||||
"一键安装命令 (Linux/macOS/Windows):"
|
||||
)}
|
||||
</Text>
|
||||
<Code
|
||||
size="2"
|
||||
style={{
|
||||
display: "block",
|
||||
padding: "12px",
|
||||
whiteSpace: "pre-wrap",
|
||||
lineHeight: "1.5",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{oneLinerInstallCommand}
|
||||
</Code>
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={handleCopyInstallCommand}
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
>
|
||||
{installCommandCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
{installCommandCopied
|
||||
? t("common.copied")
|
||||
: t("common.copy")}
|
||||
</Button>
|
||||
<Text size="1" color="gray" style={{ marginTop: "4px" }}>
|
||||
{t(
|
||||
"agent.add.oneLinerCommandNote",
|
||||
"注意: 此脚本在 Linux 上安装服务时需要 sudo 权限。Windows 用户请参考脚本内的提示手动下载。"
|
||||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* 返回按钮 */}
|
||||
<Flex justify="end" gap="3">
|
||||
<Button variant="soft" onClick={() => navigate("/agents")}>
|
||||
{t("agent.add.returnToList")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Flex direction="column" gap="3" p="3">
|
||||
<Text as="div" size="2" weight="bold" mb="1">
|
||||
{t(
|
||||
"agent.add.oneLinerCommandTitle",
|
||||
"一键安装命令 (Linux/macOS/Windows):"
|
||||
)}
|
||||
</Text>
|
||||
<Code size="2">{oneLinerInstallCommand}</Code>
|
||||
<Button variant="secondary" onClick={handleCopyInstallCommand}>
|
||||
{installCommandCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
{installCommandCopied ? t("common.copied") : t("common.copy")}
|
||||
</Button>
|
||||
<Text size="1" color="gray">
|
||||
{t(
|
||||
"agent.add.oneLinerCommandNote",
|
||||
"注意: 此脚本在 Linux 上安装服务时需要 sudo 权限。Windows 用户请参考脚本内的提示手动下载。"
|
||||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* 返回按钮 */}
|
||||
<Flex justify="end" gap="3">
|
||||
<Button variant="secondary" onClick={() => navigate("/agents")}>
|
||||
{t("agent.add.returnToList")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Card,
|
||||
TextField,
|
||||
} from "@radix-ui/themes";
|
||||
import { ArrowLeftIcon, Cross2Icon } from "@radix-ui/react-icons";
|
||||
import * as Toast from "@radix-ui/react-toast";
|
||||
import { Box, Flex, Heading, Text, TextField } from "@radix-ui/themes";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
import { ArrowLeftIcon } from "@radix-ui/react-icons";
|
||||
import { getAgent, updateAgent } from "../../api/agents";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const EditAgent = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState("");
|
||||
const [toastType, setToastType] = useState<"success" | "error">("success");
|
||||
const [formData, setFormData] = useState<{
|
||||
name: string;
|
||||
status: "active" | "inactive" | "connecting" | "unknown";
|
||||
@@ -78,14 +68,14 @@ const EditAgent = () => {
|
||||
const response = await updateAgent(Number(id), payload);
|
||||
|
||||
if (response.success) {
|
||||
setToastMessage(t("agent.form.updateSuccess"));
|
||||
setToastType("success");
|
||||
setToastOpen(true);
|
||||
toast.success(t("agent.form.updateSuccess"));
|
||||
|
||||
// 短暂延迟后导航,让用户有时间看到提示
|
||||
setTimeout(() => {
|
||||
navigate("/agents");
|
||||
}, 1500);
|
||||
} else {
|
||||
toast.error(t("agent.form.updateError", "Failed to update agent."));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -93,7 +83,7 @@ const EditAgent = () => {
|
||||
if (notFound) {
|
||||
return (
|
||||
<Box>
|
||||
<Flex justify="center" align="center" style={{ minHeight: "60vh" }}>
|
||||
<Flex justify="center" align="center">
|
||||
<Card>
|
||||
<Flex direction="column" align="center" gap="4" p="4">
|
||||
<Heading size="6">{t("agents.notFound")}</Heading>
|
||||
@@ -110,90 +100,52 @@ const EditAgent = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div className="page-container detail-page">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Flex align="center" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
size="1"
|
||||
onClick={() => navigate(`/agents/${id}`)}
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Heading size="6">
|
||||
{t("agent.form.editingClient", { name: formData.name })}
|
||||
</Heading>
|
||||
</Flex>
|
||||
<Flex justify="between" align="center">
|
||||
<Flex align="center" gap="2">
|
||||
<Button variant="secondary" onClick={() => navigate(`/agents/${id}`)}>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Heading size="6">
|
||||
{t("agent.form.editingClient", { name: formData.name })}
|
||||
</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Card>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Flex direction="column" gap="4">
|
||||
<Box mb="4">
|
||||
<Text as="label" size="2" mb="1" weight="bold">
|
||||
{t("agent.form.name")}{" "}
|
||||
<Text size="2" color="red">
|
||||
*
|
||||
</Text>
|
||||
</Text>
|
||||
<TextField.Input
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder={t("agent.form.namePlaceholder")}
|
||||
required
|
||||
/>
|
||||
<Text size="1" color="gray" mt="1">
|
||||
{t("agent.form.nameHelp")}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<div className="detail-content">
|
||||
<Card>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Flex direction="column" gap="4">
|
||||
<Box mb="4">
|
||||
<Text as="label" size="2" mb="1" weight="bold">
|
||||
{t("agent.form.name")}{" "}
|
||||
<Text size="2" color="red">
|
||||
*
|
||||
</Text>
|
||||
</Text>
|
||||
<TextField.Input
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder={t("agent.form.namePlaceholder")}
|
||||
required
|
||||
/>
|
||||
<Text size="1" color="gray" mt="1">
|
||||
{t("agent.form.nameHelp")}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Flex justify="end" mt="4" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => navigate(`/agents/${id}`)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading
|
||||
? t("common.savingChanges")
|
||||
: t("common.saveChanges")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Toast.Provider swipeDirection="right">
|
||||
<Toast.Root
|
||||
className="ToastRoot"
|
||||
open={toastOpen}
|
||||
onOpenChange={setToastOpen}
|
||||
duration={2000}
|
||||
style={{
|
||||
backgroundColor:
|
||||
toastType === "success" ? "var(--green-9)" : "var(--red-9)",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<Toast.Title className="ToastTitle">
|
||||
{toastType === "success"
|
||||
? t("common.success")
|
||||
: t("common.error")}
|
||||
</Toast.Title>
|
||||
<Toast.Description className="ToastDescription">
|
||||
{toastMessage}
|
||||
</Toast.Description>
|
||||
<Toast.Close className="ToastClose">
|
||||
<Cross2Icon />
|
||||
</Toast.Close>
|
||||
</Toast.Root>
|
||||
<Toast.Viewport className="ToastViewport" />
|
||||
</Toast.Provider>
|
||||
</div>
|
||||
<Flex justify="end" mt="4" gap="2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/agents/${id}`)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? t("common.savingChanges") : t("common.saveChanges")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { Button, Card, Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { Button, Card, Input } from "@/components/ui";
|
||||
import { useAuth } from "../../providers/AuthProvider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -69,26 +70,21 @@ const Login = () => {
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Flex direction="column" gap="3">
|
||||
<div className="input-wrapper">
|
||||
<input
|
||||
placeholder={t("login.username")}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
className="text-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-wrapper">
|
||||
<input
|
||||
placeholder={t("login.password")}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="text-input"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t("login.username")}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
className="text-gray-900"
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("login.password")}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="text-gray-900"
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? t("common.loading") : t("login.button")}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { Button, Card, Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
import { useAuth } from "../../providers/AuthProvider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
@@ -5,14 +5,23 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Card,
|
||||
TextField,
|
||||
Select,
|
||||
TextArea,
|
||||
Table,
|
||||
IconButton,
|
||||
} from "@radix-ui/themes";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Textarea,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectTrigger,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
} from "@/components/ui";
|
||||
import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||
import { createMonitor } from "../../api/monitors";
|
||||
import StatusCodeSelect from "../../components/StatusCodeSelect";
|
||||
@@ -146,11 +155,7 @@ const CreateMonitor = () => {
|
||||
<div className="page-container detail-page">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Flex align="center" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
size="1"
|
||||
onClick={() => navigate("/monitors")}
|
||||
>
|
||||
<Button variant="secondary" onClick={() => navigate("/monitors")}>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Heading size="6">{t("monitor.form.title.create")}</Heading>
|
||||
@@ -163,11 +168,7 @@ const CreateMonitor = () => {
|
||||
<Box pt="2">
|
||||
<Flex direction="column" gap="4">
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.name")} *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -180,11 +181,7 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
URL *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -197,38 +194,30 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.method")} *
|
||||
</Text>
|
||||
<Select.Root
|
||||
<Select
|
||||
name="method"
|
||||
value={formData.method}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({ ...prev, method: value }))
|
||||
}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Item value="GET">GET</Select.Item>
|
||||
<Select.Item value="POST">POST</Select.Item>
|
||||
<Select.Item value="PUT">PUT</Select.Item>
|
||||
<Select.Item value="DELETE">DELETE</Select.Item>
|
||||
<Select.Item value="HEAD">HEAD</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<SelectTrigger />
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="HEAD">HEAD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Flex gap="4">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Box>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.interval")} *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -244,12 +233,8 @@ const CreateMonitor = () => {
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Box>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.timeout")} *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -264,11 +249,7 @@ const CreateMonitor = () => {
|
||||
</Flex>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.expectedStatus")} *
|
||||
</Text>
|
||||
<StatusCodeSelect
|
||||
@@ -279,39 +260,26 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.headers")}
|
||||
</Text>
|
||||
<Box
|
||||
style={{
|
||||
border: "1px solid var(--gray-6)",
|
||||
borderRadius: "6px",
|
||||
padding: "8px",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>
|
||||
<Box>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
{t("monitor.form.headerName")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{t("monitor.form.headerValue")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell
|
||||
style={{ width: "40px" }}
|
||||
></Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{headers.map((header, index) => (
|
||||
<Table.Row key={index}>
|
||||
<Table.Cell>
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<TextField.Input
|
||||
placeholder={t(
|
||||
"monitor.form.headerNamePlaceholder"
|
||||
@@ -325,8 +293,8 @@ const CreateMonitor = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField.Input
|
||||
placeholder={t(
|
||||
"monitor.form.headerValuePlaceholder"
|
||||
@@ -340,8 +308,8 @@ const CreateMonitor = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
variant="soft"
|
||||
color="red"
|
||||
@@ -350,15 +318,14 @@ const CreateMonitor = () => {
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Flex justify="end" mt="2">
|
||||
<Button
|
||||
size="1"
|
||||
variant="soft"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
setHeaders([...headers, { key: "", value: "" }])
|
||||
}
|
||||
@@ -375,19 +342,14 @@ const CreateMonitor = () => {
|
||||
|
||||
{showBodyField && (
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.body")}
|
||||
</Text>
|
||||
<TextArea
|
||||
<Textarea
|
||||
name="body"
|
||||
value={formData.body}
|
||||
onChange={handleChange}
|
||||
placeholder={t("monitor.form.bodyPlaceholder")}
|
||||
style={{ minHeight: "100px" }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -395,7 +357,10 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
|
||||
<Flex justify="end" mt="4" gap="2">
|
||||
<Button variant="soft" onClick={() => navigate("/monitors")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate("/monitors")}
|
||||
>
|
||||
{t("monitor.form.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
|
||||
@@ -5,14 +5,25 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
TextField,
|
||||
IconButton,
|
||||
Container,
|
||||
} from "@radix-ui/themes";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
TextField,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
Table,
|
||||
IconButton,
|
||||
TextArea,
|
||||
} from "@radix-ui/themes";
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Textarea,
|
||||
} from "@/components/ui";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
UpdateIcon,
|
||||
@@ -217,7 +228,7 @@ const EditMonitor = () => {
|
||||
if (loadingData) {
|
||||
return (
|
||||
<Box>
|
||||
<Flex justify="center" align="center" style={{ minHeight: "60vh" }}>
|
||||
<Flex justify="center" align="center">
|
||||
<Text>{t("common.loading")}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
@@ -227,7 +238,7 @@ const EditMonitor = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Box>
|
||||
<Flex justify="center" align="center" style={{ minHeight: "60vh" }}>
|
||||
<Flex justify="center" align="center">
|
||||
<Card>
|
||||
<Flex direction="column" align="center" gap="4" p="4">
|
||||
<Heading size="6">{t("monitor.notExist")}</Heading>
|
||||
@@ -243,13 +254,12 @@ const EditMonitor = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Container size="4">
|
||||
<div className="page-container detail-page">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Flex align="center" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
size="1"
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/monitors/${id}`)}
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
@@ -266,11 +276,7 @@ const EditMonitor = () => {
|
||||
<Box pt="2">
|
||||
<Flex direction="column" gap="4">
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.name")} *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -283,11 +289,7 @@ const EditMonitor = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
URL *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -300,38 +302,30 @@ const EditMonitor = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.method")} *
|
||||
</Text>
|
||||
<Select.Root
|
||||
<Select
|
||||
name="method"
|
||||
value={formData.method}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({ ...prev, method: value }))
|
||||
}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Item value="GET">GET</Select.Item>
|
||||
<Select.Item value="POST">POST</Select.Item>
|
||||
<Select.Item value="PUT">PUT</Select.Item>
|
||||
<Select.Item value="DELETE">DELETE</Select.Item>
|
||||
<Select.Item value="HEAD">HEAD</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<SelectTrigger />
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="HEAD">HEAD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Flex gap="4">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Box>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.interval")} *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -347,12 +341,8 @@ const EditMonitor = () => {
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Box>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.timeout")} *
|
||||
</Text>
|
||||
<TextField.Input
|
||||
@@ -367,11 +357,7 @@ const EditMonitor = () => {
|
||||
</Flex>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.expectedStatus")} *
|
||||
</Text>
|
||||
<StatusCodeSelect
|
||||
@@ -382,39 +368,25 @@ const EditMonitor = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.headers")}
|
||||
</Text>
|
||||
<Box
|
||||
style={{
|
||||
border: "1px solid var(--gray-6)",
|
||||
borderRadius: "6px",
|
||||
padding: "8px",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>
|
||||
<Box>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
{t("monitor.form.headerName")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{t("monitor.form.headerValue")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell
|
||||
style={{ width: "40px" }}
|
||||
></Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{headers.map((header, index) => (
|
||||
<Table.Row key={index}>
|
||||
<Table.Cell>
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<TextField.Input
|
||||
placeholder={t(
|
||||
"monitor.form.headerNamePlaceholder"
|
||||
@@ -428,8 +400,8 @@ const EditMonitor = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField.Input
|
||||
placeholder={t(
|
||||
"monitor.form.headerValuePlaceholder"
|
||||
@@ -443,8 +415,8 @@ const EditMonitor = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
variant="soft"
|
||||
color="red"
|
||||
@@ -453,15 +425,14 @@ const EditMonitor = () => {
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Flex justify="end" mt="2">
|
||||
<Button
|
||||
size="1"
|
||||
variant="soft"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
setHeaders([...headers, { key: "", value: "" }])
|
||||
}
|
||||
@@ -478,19 +449,14 @@ const EditMonitor = () => {
|
||||
|
||||
{showBodyField && (
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
style={{ marginBottom: "4px", display: "block" }}
|
||||
>
|
||||
<Text as="label" size="2">
|
||||
{t("monitor.form.body")}
|
||||
</Text>
|
||||
<TextArea
|
||||
<Textarea
|
||||
name="body"
|
||||
value={formData.body}
|
||||
onChange={handleChange}
|
||||
placeholder={t("monitor.form.bodyPlaceholder")}
|
||||
style={{ minHeight: "100px" }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -499,7 +465,7 @@ const EditMonitor = () => {
|
||||
|
||||
<Flex justify="end" mt="4" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/monitors/${id}`)}
|
||||
>
|
||||
{t("monitor.form.cancel")}
|
||||
@@ -515,7 +481,7 @@ const EditMonitor = () => {
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Card,
|
||||
Grid,
|
||||
Badge,
|
||||
} from "@radix-ui/themes";
|
||||
import { Box, Flex, Heading, Text, Grid,Container } from "@radix-ui/themes";
|
||||
import { Button, Card, Badge } from "@/components/ui";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
Pencil1Icon,
|
||||
TrashIcon,
|
||||
ReloadIcon,
|
||||
Cross2Icon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import * as Toast from "@radix-ui/react-toast";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
getMonitor,
|
||||
deleteMonitor,
|
||||
@@ -49,9 +40,6 @@ const MonitorDetail = () => {
|
||||
useState<MonitorWithDailyStatsAndStatusHistory | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState("");
|
||||
const [toastType, setToastType] = useState<"success" | "error">("success");
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 组件加载时获取数据
|
||||
@@ -102,13 +90,11 @@ const MonitorDetail = () => {
|
||||
setLoading(true);
|
||||
const response = await checkMonitor(parseInt(id));
|
||||
if (response.success) {
|
||||
setToastMessage(t("monitor.checkCompleted"));
|
||||
setToastType("success");
|
||||
setToastOpen(true);
|
||||
|
||||
toast.success(t("monitor.checkCompleted"));
|
||||
} else {
|
||||
setToastMessage(t("monitor.checkFailed"));
|
||||
setToastType("error");
|
||||
setToastOpen(true);
|
||||
|
||||
toast.error(t("monitor.checkFailed"));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -119,18 +105,16 @@ const MonitorDetail = () => {
|
||||
|
||||
const response = await deleteMonitor(parseInt(id));
|
||||
if (response.success) {
|
||||
setToastMessage(t("monitor.deleteSuccess"));
|
||||
setToastType("success");
|
||||
setToastOpen(true);
|
||||
|
||||
toast.success(t("monitor.deleteSuccess")); // Added
|
||||
|
||||
// 短暂延迟后导航,让用户有时间看到提示
|
||||
setTimeout(() => {
|
||||
navigate("/monitors");
|
||||
}, 1500);
|
||||
} else {
|
||||
setToastMessage(t("monitor.deleteFailed"));
|
||||
setToastType("error");
|
||||
setToastOpen(true);
|
||||
|
||||
toast.error(t("monitor.deleteFailed")); // Added
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,7 +141,7 @@ const MonitorDetail = () => {
|
||||
<Text style={{ color: "var(--red-9)" }}>
|
||||
{error || t("monitor.notExist")}
|
||||
</Text>
|
||||
<Button variant="soft" onClick={() => navigate("/monitors")} mt="2">
|
||||
<Button variant="secondary" onClick={() => navigate("/monitors")}>
|
||||
{t("monitor.returnToList")}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -165,15 +149,10 @@ const MonitorDetail = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="monitor-detail" p="4">
|
||||
<div>
|
||||
<Container size="4">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Flex align="center" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
size="1"
|
||||
onClick={() => navigate("/monitors")}
|
||||
>
|
||||
<Button variant="secondary" onClick={() => navigate("/monitors")}>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Heading size="6">{monitor.name}</Heading>
|
||||
@@ -186,18 +165,18 @@ const MonitorDetail = () => {
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Flex gap="2">
|
||||
<Button variant="soft" onClick={handleCheck}>
|
||||
<Button variant="secondary" onClick={handleCheck}>
|
||||
<ReloadIcon />
|
||||
{t("monitor.manualCheck")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="soft"
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/monitors/edit/${id}`)}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
{t("monitor.edit")}
|
||||
</Button>
|
||||
<Button variant="soft" color="red" onClick={handleDelete}>
|
||||
<Button variant="secondary" onClick={handleDelete}>
|
||||
<TrashIcon />
|
||||
{t("monitor.delete")}
|
||||
</Button>
|
||||
@@ -238,7 +217,7 @@ const MonitorDetail = () => {
|
||||
</Flex>
|
||||
</Card>
|
||||
{/* 添加响应时间图表 */}
|
||||
<Card mt="4">
|
||||
<Card>
|
||||
<Flex direction="column" gap="3" mt="4">
|
||||
<Heading size="4">{t("monitor.oneDayHistory")}</Heading>
|
||||
<Box>
|
||||
@@ -249,7 +228,7 @@ const MonitorDetail = () => {
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card mt="4">
|
||||
<Card>
|
||||
<Flex direction="column" gap="3">
|
||||
<Heading size="4">{t("monitor.MonthsHistory")}</Heading>
|
||||
<Box>
|
||||
@@ -258,35 +237,7 @@ const MonitorDetail = () => {
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
<Toast.Provider swipeDirection="right">
|
||||
<Toast.Root
|
||||
className="ToastRoot"
|
||||
open={toastOpen}
|
||||
onOpenChange={setToastOpen}
|
||||
duration={3000}
|
||||
style={{
|
||||
backgroundColor:
|
||||
toastType === "success" ? "var(--green-9)" : "var(--red-9)",
|
||||
borderRadius: "8px",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Toast.Title className="ToastTitle">
|
||||
{toastType === "success"
|
||||
? t("common.success")
|
||||
: t("common.error")}
|
||||
</Toast.Title>
|
||||
<Toast.Description className="ToastDescription">
|
||||
{toastMessage}
|
||||
</Toast.Description>
|
||||
<Toast.Close className="ToastClose">
|
||||
<Cross2Icon />
|
||||
</Toast.Close>
|
||||
</Toast.Root>
|
||||
<Toast.Viewport className="ToastViewport" />
|
||||
</Toast.Provider>
|
||||
</div>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,15 +5,29 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
IconButton,
|
||||
Grid,
|
||||
Container,
|
||||
} from "@radix-ui/themes";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Badge,
|
||||
Card,
|
||||
IconButton,
|
||||
Grid,
|
||||
Tabs,
|
||||
Dialog,
|
||||
} from "@radix-ui/themes";
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/components/ui";
|
||||
|
||||
import {
|
||||
PlusIcon,
|
||||
Pencil1Icon,
|
||||
@@ -160,7 +174,7 @@ const MonitorsList = () => {
|
||||
// 加载中显示
|
||||
if (loading) {
|
||||
return (
|
||||
<Box className="page-container detail-page">
|
||||
<Box>
|
||||
<Flex justify="center" align="center" p="4">
|
||||
<Text>{t("common.loading")}</Text>
|
||||
</Flex>
|
||||
@@ -171,13 +185,13 @@ const MonitorsList = () => {
|
||||
// 错误显示
|
||||
if (error) {
|
||||
return (
|
||||
<Box className="page-container detail-page">
|
||||
<Card mb="4">
|
||||
<Flex p="3" style={{ backgroundColor: "var(--red-3)" }}>
|
||||
<Text style={{ color: "var(--red-9)" }}>{error}</Text>
|
||||
<Box>
|
||||
<Card>
|
||||
<Flex>
|
||||
<Text>{error}</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Button variant="soft" onClick={() => window.location.reload()} mt="2">
|
||||
<Button variant="secondary" onClick={() => window.location.reload()}>
|
||||
{t("monitors.retry")}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -185,205 +199,181 @@ const MonitorsList = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div className="page-container detail-page">
|
||||
<Flex justify="between" align="center" className="detail-header">
|
||||
<Heading size="6">{t("monitors.pageTitle")}</Heading>
|
||||
<Flex gap="3">
|
||||
<Tabs.Root defaultValue="grid">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="grid" onClick={() => setView("grid")}>
|
||||
<ViewGridIcon />
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="list" onClick={() => setView("list")}>
|
||||
<LayoutIcon />
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
<Button onClick={handleRefresh} disabled={loading}>
|
||||
<ReloadIcon />
|
||||
{t("monitors.refresh")}
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/monitors/create")}>
|
||||
<PlusIcon />
|
||||
{t("monitors.create")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Container size="4">
|
||||
<Flex justify="between" align="center">
|
||||
<Heading size="6">{t("monitors.pageTitle")}</Heading>
|
||||
<Flex className="mt-4 space-x-2">
|
||||
<Tabs defaultValue="grid">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid" onClick={() => setView("grid")}>
|
||||
<ViewGridIcon />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="list" onClick={() => setView("list")}>
|
||||
<LayoutIcon />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
<ReloadIcon />
|
||||
{t("monitors.refresh")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate("/monitors/create")}
|
||||
>
|
||||
<PlusIcon />
|
||||
{t("monitors.create")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<div className="detail-content">
|
||||
{monitors.length === 0 ? (
|
||||
<Card>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
p="6"
|
||||
gap="3"
|
||||
>
|
||||
<Text>{t("monitors.notFound")}</Text>
|
||||
<Button onClick={() => navigate("/monitors/create")}>
|
||||
<PlusIcon />
|
||||
{t("monitors.addOne")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
) : view === "list" ? (
|
||||
// 列表视图
|
||||
<Table.Root variant="surface">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("monitors.table.name")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("monitors.table.url")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("monitors.table.status")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("monitors.table.responseTime")}
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>
|
||||
{t("monitors.table.actions")}
|
||||
</Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Container className="mt-4 space-x-2">
|
||||
{monitors.length === 0 ? (
|
||||
<Card>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
p="6"
|
||||
gap="3"
|
||||
>
|
||||
<Text>{t("monitors.notFound")}</Text>
|
||||
<Button onClick={() => navigate("/monitors/create")}>
|
||||
<PlusIcon />
|
||||
{t("monitors.addOne")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
) : view === "list" ? (
|
||||
// 列表视图
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell>{t("monitors.table.name")}</TableCell>
|
||||
<TableCell>{t("monitors.table.url")}</TableCell>
|
||||
<TableCell>{t("monitors.table.status")}</TableCell>
|
||||
<TableCell>{t("monitors.table.responseTime")}</TableCell>
|
||||
<TableCell>{t("monitors.table.actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<Table.Body>
|
||||
{monitors.map((monitor) => (
|
||||
<Table.Row key={`${monitor.id}-${Math.random()}`}>
|
||||
<Table.Cell>
|
||||
<Text weight="medium">{monitor.name}</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Text style={{ wordBreak: "break-all" }}>
|
||||
{monitor.url}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Flex align="center" gap="2">
|
||||
<StatusIcon status={monitor.status} />
|
||||
<Badge color={statusColors[monitor.status]}>
|
||||
{monitor.status === "up"
|
||||
? t("monitors.status.up")
|
||||
: monitor.status === "down"
|
||||
? t("monitors.status.down")
|
||||
: t("monitor.status.pending")}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Text>
|
||||
{monitor.response_time
|
||||
? `${monitor.response_time}ms`
|
||||
: "-"}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Flex gap="2">
|
||||
<IconButton
|
||||
variant="soft"
|
||||
onClick={() => navigate(`/monitors/${monitor.id}`)}
|
||||
title={t("monitors.viewDetails")}
|
||||
>
|
||||
<InfoCircledIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="soft"
|
||||
onClick={() =>
|
||||
navigate(`/monitors/edit/${monitor.id}`)
|
||||
}
|
||||
title={t("monitors.edit")}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="soft"
|
||||
color="red"
|
||||
onClick={() => handleDeleteClick(monitor.id)}
|
||||
title={t("monitors.delete")}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
) : (
|
||||
// 网格视图 - 使用 MonitorCard 组件
|
||||
<Grid columns={{ initial: "1" }} gap="4">
|
||||
<TableBody>
|
||||
{monitors.map((monitor) => (
|
||||
<Box
|
||||
key={`${monitor.id}-${Math.random()}`}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<MonitorCard monitor={monitor} />
|
||||
<Flex
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
right: "10px",
|
||||
zIndex: 1,
|
||||
}}
|
||||
gap="2"
|
||||
>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
onClick={() => navigate(`/monitors/${monitor.id}`)}
|
||||
title={t("monitors.viewDetails")}
|
||||
>
|
||||
<InfoCircledIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
onClick={() => navigate(`/monitors/edit/${monitor.id}`)}
|
||||
title={t("monitors.edit")}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
color="red"
|
||||
onClick={() => handleDeleteClick(monitor.id)}
|
||||
title={t("monitors.delete")}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Box>
|
||||
<TableRow key={`${monitor.id}-${Math.random()}`}>
|
||||
<TableCell>
|
||||
<Text weight="medium">{monitor.name}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{monitor.url}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Flex align="center" gap="2">
|
||||
<StatusIcon status={monitor.status} />
|
||||
<Badge color={statusColors[monitor.status]}>
|
||||
{monitor.status === "up"
|
||||
? t("monitors.status.up")
|
||||
: monitor.status === "down"
|
||||
? t("monitors.status.down")
|
||||
: t("monitor.status.pending")}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>
|
||||
{monitor.response_time
|
||||
? `${monitor.response_time}ms`
|
||||
: "-"}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Flex gap="2">
|
||||
<IconButton
|
||||
variant="soft"
|
||||
onClick={() => navigate(`/monitors/${monitor.id}`)}
|
||||
title={t("monitors.viewDetails")}
|
||||
>
|
||||
<InfoCircledIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="soft"
|
||||
onClick={() => navigate(`/monitors/edit/${monitor.id}`)}
|
||||
title={t("monitors.edit")}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="soft"
|
||||
color="red"
|
||||
onClick={() => handleDeleteClick(monitor.id)}
|
||||
title={t("monitors.delete")}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
// 网格视图 - 使用 MonitorCard 组件
|
||||
<Grid columns={{ initial: "1" }} gap="4">
|
||||
{monitors.map((monitor) => (
|
||||
<Box key={`${monitor.id}-${Math.random()}`} className="relative">
|
||||
<MonitorCard monitor={monitor} />
|
||||
<Flex gap="2" className="absolute top-4 right-4">
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
onClick={() => navigate(`/monitors/${monitor.id}`)}
|
||||
title={t("monitors.viewDetails")}
|
||||
>
|
||||
<InfoCircledIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
onClick={() => navigate(`/monitors/edit/${monitor.id}`)}
|
||||
title={t("monitors.edit")}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
color="red"
|
||||
onClick={() => handleDeleteClick(monitor.id)}
|
||||
title={t("monitors.delete")}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Dialog.Root open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>{t("common.deleteConfirmation")}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>{t("common.deleteConfirmation")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("common.deleteConfirmMessage")}
|
||||
</Dialog.Description>
|
||||
</DialogDescription>
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="soft" color="gray">
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
<Button color="red" onClick={handleDeleteConfirm}>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
<DialogClose>
|
||||
<Button variant="secondary">{t("common.cancel")}</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={handleDeleteConfirm}>{t("common.delete")}</Button>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, Flex, Heading, Text, Grid, Badge, Theme } from "@radix-ui/themes";
|
||||
import { Box, Flex, Heading, Text, Grid, Theme } from "@radix-ui/themes";
|
||||
import { Badge } from "@/components/ui";
|
||||
import { getStatusPageData } from "../../api/status";
|
||||
import AgentCard from "../../components/AgentCard";
|
||||
import MonitorCard from "../../components/MonitorCard";
|
||||
@@ -74,10 +75,8 @@ const StatusPage = () => {
|
||||
<Theme appearance="light">
|
||||
<Box>
|
||||
<div className="page-container">
|
||||
<Flex justify="center" align="center" style={{ minHeight: "50vh" }}>
|
||||
<Text size="3" style={{ color: "var(--red-9)" }}>
|
||||
{error}
|
||||
</Text>
|
||||
<Flex justify="center" align="center">
|
||||
<Text size="3">{error}</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
</Box>
|
||||
@@ -90,7 +89,7 @@ const StatusPage = () => {
|
||||
<Theme appearance="light">
|
||||
<Box>
|
||||
<div className="page-container">
|
||||
<Flex justify="center" align="center" style={{ minHeight: "50vh" }}>
|
||||
<Flex justify="center" align="center">
|
||||
<Text size="3">{t("common.loading")}</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
@@ -114,11 +113,11 @@ const StatusPage = () => {
|
||||
<Heading size="9" align="center">
|
||||
{pageTitle}
|
||||
</Heading>
|
||||
<Text size="5" align="center" style={{ maxWidth: "800px" }}>
|
||||
<Text size="5" align="center">
|
||||
{pageDescription}
|
||||
</Text>
|
||||
<Flex gap="2" mt="2">
|
||||
<Badge size="2">
|
||||
<Badge>
|
||||
{t("statusPage.lastUpdated")}: {t("statusPage.justNow")}
|
||||
</Badge>
|
||||
</Flex>
|
||||
@@ -146,7 +145,7 @@ const StatusPage = () => {
|
||||
</Heading>
|
||||
<Grid columns={{ initial: "1" }} gap="4">
|
||||
{data.agents.map((agent) => (
|
||||
<Box key={agent.id} style={{ position: "relative" }}>
|
||||
<Box key={agent.id}>
|
||||
<AgentCard agent={agent} showIpAddress={false} />
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -4,21 +4,23 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
Card,
|
||||
Button,
|
||||
TextField,
|
||||
TextArea,
|
||||
Tabs,
|
||||
Container,
|
||||
} from "@radix-ui/themes";
|
||||
import { EyeOpenIcon, CopyIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import * as Toast from "@radix-ui/react-toast";
|
||||
import {
|
||||
getStatusPageConfig,
|
||||
saveStatusPageConfig,
|
||||
} from "../../api/status";
|
||||
Card,
|
||||
Button,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Textarea,
|
||||
Checkbox,
|
||||
} from "@/components/ui";
|
||||
import { EyeOpenIcon, CopyIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import { toast } from "sonner";
|
||||
import { getStatusPageConfig, saveStatusPageConfig } from "../../api/status";
|
||||
import { StatusPageConfig as StatusConfig } from "../../types/status";
|
||||
import "../../styles/components.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// 监控项带选择状态
|
||||
@@ -49,9 +51,6 @@ interface StatusConfigWithDetails {
|
||||
const StatusPageConfig = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showSuccessToast, setShowSuccessToast] = useState(false);
|
||||
const [showErrorToast, setShowErrorToast] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -161,13 +160,9 @@ const StatusPageConfig = () => {
|
||||
const response = await saveStatusPageConfig(configToSave);
|
||||
|
||||
if (response) {
|
||||
setToastMessage(t("statusPageConfig.configSaved"));
|
||||
setShowSuccessToast(true);
|
||||
setTimeout(() => setShowSuccessToast(false), 3000);
|
||||
toast.success(t("statusPageConfig.configSaved"));
|
||||
} else {
|
||||
setToastMessage(response || t("statusPageConfig.saveError"));
|
||||
setShowErrorToast(true);
|
||||
setTimeout(() => setShowErrorToast(false), 3000);
|
||||
toast.error(t("statusPageConfig.saveError"));
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
@@ -191,471 +186,249 @@ const StatusPageConfig = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Container>
|
||||
<div className="page-container detail-page">
|
||||
{/* 美化顶部导航栏 */}
|
||||
<Box mb="5">
|
||||
<Flex
|
||||
justify="between"
|
||||
align="center"
|
||||
className="detail-header"
|
||||
py="3"
|
||||
<Container>
|
||||
<Box>
|
||||
<Flex justify="between" align="center">
|
||||
<Flex align="center">
|
||||
<Heading size="5" weight="medium">
|
||||
{t("statusPageConfig.title")}
|
||||
</Heading>
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mr-2"
|
||||
onClick={handlePreview}
|
||||
>
|
||||
<Flex align="center" gap="2">
|
||||
<Heading size="5" weight="medium">
|
||||
{t("statusPageConfig.title")}
|
||||
</Heading>
|
||||
</Flex>
|
||||
<Flex gap="3" align="center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="2"
|
||||
className="nav-button config-button"
|
||||
onClick={handlePreview}
|
||||
>
|
||||
<EyeOpenIcon width="16" height="16" />
|
||||
<Text size="2">{t("statusPageConfig.preview")}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
variant="soft"
|
||||
size="2"
|
||||
className="nav-button config-button"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
<EyeOpenIcon width="8" height="8" />
|
||||
<Text size="2">{t("statusPageConfig.preview")}</Text>
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"
|
||||
fill="currentColor"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"
|
||||
fill="currentColor"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
dur="0.75s"
|
||||
values="0 12 12;360 12 12"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</svg>
|
||||
</span>
|
||||
<Text size="2">{t("common.savingChanges")}</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckIcon width="16" height="16" />
|
||||
<Text size="2">{t("statusPageConfig.save")}</Text>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<div className="detail-content">
|
||||
<Card size="2">
|
||||
<Tabs.Root defaultValue="general" className="config-tabs">
|
||||
<Tabs.List className="config-tabs-list">
|
||||
<Tabs.Trigger value="general" className="tab-trigger">
|
||||
{t("statusPageConfig.general")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="services" className="tab-trigger">
|
||||
{t("statusPageConfig.services")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="agents" className="tab-trigger">
|
||||
{t("statusPageConfig.agents")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Box pt="5" px="2" className="tab-content-container">
|
||||
<Tabs.Content value="general" className="tab-content">
|
||||
<Flex direction="column" gap="5">
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
weight="medium"
|
||||
style={{ marginBottom: "6px", display: "block" }}
|
||||
>
|
||||
{t("statusPageConfig.pageTitle")}
|
||||
</Text>
|
||||
<TextField.Input
|
||||
name="title"
|
||||
value={config.title}
|
||||
onChange={handleChange}
|
||||
placeholder={t(
|
||||
"statusPageConfig.pageTitlePlaceholder"
|
||||
)}
|
||||
size="3"
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
dur="0.75s"
|
||||
values="0 12 12;360 12 12"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</Box>
|
||||
</path>
|
||||
</svg>
|
||||
</span>
|
||||
<Text size="2">{t("common.savingChanges")}</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckIcon width="8" height="8" />
|
||||
<Text size="2">{t("statusPageConfig.save")}</Text>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Card className="mt-4">
|
||||
<Tabs defaultValue="general">
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">
|
||||
{t("statusPageConfig.general")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="services">
|
||||
{t("statusPageConfig.services")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agents">
|
||||
{t("statusPageConfig.agents")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
weight="medium"
|
||||
style={{ marginBottom: "6px", display: "block" }}
|
||||
>
|
||||
{t("statusPageConfig.pageDescription")}
|
||||
</Text>
|
||||
<TextArea
|
||||
name="description"
|
||||
value={config.description}
|
||||
onChange={handleChange}
|
||||
placeholder={t(
|
||||
"statusPageConfig.pageDescriptionPlaceholder"
|
||||
)}
|
||||
style={{ minHeight: "80px" }}
|
||||
size="3"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text
|
||||
as="label"
|
||||
size="2"
|
||||
weight="medium"
|
||||
style={{ marginBottom: "6px", display: "block" }}
|
||||
>
|
||||
{t("statusPageConfig.publicUrl")}
|
||||
</Text>
|
||||
<Flex gap="2">
|
||||
<Box style={{ flex: 1, position: "relative" }}>
|
||||
<TextField.Input
|
||||
value={config.publicUrl}
|
||||
readOnly
|
||||
style={{
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
paddingRight: "16px",
|
||||
}}
|
||||
size="3"
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="2"
|
||||
className="nav-button copy-button"
|
||||
onClick={handleCopyUrl}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckIcon width="16" height="16" />
|
||||
<Text size="2">{t("common.copied")}</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon width="16" height="16" />
|
||||
<Text size="2">{t("common.copy")}</Text>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text
|
||||
size="1"
|
||||
color="gray"
|
||||
style={{
|
||||
marginTop: "6px",
|
||||
display: "block",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
{t("statusPageConfig.publicUrlHelp")}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
style={{
|
||||
background: "var(--gray-2)",
|
||||
padding: "12px 14px",
|
||||
borderRadius: "8px",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as="div"
|
||||
size="2"
|
||||
color="gray"
|
||||
style={{ lineHeight: "1.5" }}
|
||||
>
|
||||
{t("statusPageConfig.selectionNote")}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="services" className="tab-content">
|
||||
<Flex direction="column" gap="5">
|
||||
<Text size="2" color="gray" mb="3">
|
||||
{t("statusPageConfig.selectServicesPrompt")}
|
||||
</Text>
|
||||
|
||||
{config.monitors.length === 0 ? (
|
||||
<Text color="gray">{t("monitors.noMonitors")}</Text>
|
||||
) : (
|
||||
<Box>
|
||||
{config.monitors.map((monitor) => {
|
||||
console.log(
|
||||
`【${t("statusPageConfig.serviceRendering")}】${
|
||||
monitor.name
|
||||
}(${monitor.id}), ${t(
|
||||
"statusPageConfig.selectedStatus"
|
||||
)}: ${monitor.selected}`
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
key={monitor.id}
|
||||
align="center"
|
||||
justify="between"
|
||||
className="service-item"
|
||||
>
|
||||
<Text size="3">{monitor.name}</Text>
|
||||
<div className="custom-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`monitor-${monitor.id}`}
|
||||
checked={monitor.selected}
|
||||
onChange={(e) => {
|
||||
console.log(
|
||||
`${t(
|
||||
"statusPageConfig.monitorStatusChange"
|
||||
)}: ${monitor.name}(${monitor.id}), ${t(
|
||||
"statusPageConfig.from"
|
||||
)} ${monitor.selected} ${t(
|
||||
"statusPageConfig.to"
|
||||
)} ${e.target.checked}`
|
||||
);
|
||||
handleMonitorChange(
|
||||
monitor.id,
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`monitor-${monitor.id}`}
|
||||
className={
|
||||
monitor.selected ? "checked" : ""
|
||||
}
|
||||
></label>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="agents" className="tab-content">
|
||||
<Flex direction="column" gap="5">
|
||||
<Text size="2" color="gray" mb="3">
|
||||
{t("statusPageConfig.selectAgentsPrompt")}
|
||||
</Text>
|
||||
|
||||
{config.agents.length === 0 ? (
|
||||
<Text color="gray">{t("agents.noAgents")}</Text>
|
||||
) : (
|
||||
<Box>
|
||||
{config.agents.map((agent) => {
|
||||
console.log(
|
||||
`【${t("statusPageConfig.agentRendering")}】${
|
||||
agent.name
|
||||
}(${agent.id}), ${t(
|
||||
"statusPageConfig.selectedStatus"
|
||||
)}: ${agent.selected}`
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
key={agent.id}
|
||||
align="center"
|
||||
justify="between"
|
||||
className="service-item"
|
||||
>
|
||||
<Text size="3">{agent.name}</Text>
|
||||
<div className="custom-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`agent-${agent.id}`}
|
||||
checked={agent.selected}
|
||||
onChange={(e) => {
|
||||
console.log(
|
||||
`${t(
|
||||
"statusPageConfig.agentStatusChange"
|
||||
)}: ${agent.name}(${agent.id}), ${t(
|
||||
"statusPageConfig.from"
|
||||
)} ${agent.selected} ${t(
|
||||
"statusPageConfig.to"
|
||||
)} ${e.target.checked}`
|
||||
);
|
||||
handleAgentChange(
|
||||
agent.id,
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`agent-${agent.id}`}
|
||||
className={agent.selected ? "checked" : ""}
|
||||
></label>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Tabs.Content>
|
||||
<Box pt="2" px="2">
|
||||
<TabsContent value="general">
|
||||
<Flex direction="column" gap="2">
|
||||
<Box>
|
||||
<Text as="label" className="text-sm font-medium">
|
||||
{t("statusPageConfig.pageTitle")}
|
||||
</Text>
|
||||
<TextField.Input
|
||||
name="title"
|
||||
value={config.title}
|
||||
onChange={handleChange}
|
||||
placeholder={t("statusPageConfig.pageTitlePlaceholder")}
|
||||
/>
|
||||
</Box>
|
||||
</Tabs.Root>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast 提示 */}
|
||||
<Toast.Provider swipeDirection="right">
|
||||
<Toast.Viewport className="ToastViewport" />
|
||||
<Box>
|
||||
<Text as="label" className="text-sm font-medium">
|
||||
{t("statusPageConfig.pageDescription")}
|
||||
</Text>
|
||||
<Textarea
|
||||
name="description"
|
||||
value={config.description}
|
||||
onChange={handleChange}
|
||||
placeholder={t(
|
||||
"statusPageConfig.pageDescriptionPlaceholder"
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{showSuccessToast && (
|
||||
<Toast.Root
|
||||
className="ToastRoot"
|
||||
duration={3000}
|
||||
style={{ backgroundColor: "var(--green-9)", borderRadius: "8px" }}
|
||||
>
|
||||
<div className="ToastContent">
|
||||
<div
|
||||
className="ToastIcon"
|
||||
style={{
|
||||
backgroundColor: "var(--green-10)",
|
||||
borderRadius: "50%",
|
||||
padding: "8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CheckIcon color="white" width="18" height="18" />
|
||||
</div>
|
||||
<div className="ToastText">
|
||||
<div
|
||||
className="ToastTitle"
|
||||
style={{ fontSize: "15px", fontWeight: "600" }}
|
||||
>
|
||||
{t("common.success")}
|
||||
</div>
|
||||
<div
|
||||
className="ToastDescription"
|
||||
style={{ fontSize: "13px", opacity: "0.9" }}
|
||||
>
|
||||
{toastMessage}
|
||||
</div>
|
||||
</div>
|
||||
<Toast.Close
|
||||
className="ToastClose"
|
||||
style={{
|
||||
opacity: "0.8",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "white",
|
||||
fontSize: "20px",
|
||||
cursor: "pointer",
|
||||
padding: "4px 8px",
|
||||
}}
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
×
|
||||
</Toast.Close>
|
||||
</div>
|
||||
</Toast.Root>
|
||||
)}
|
||||
<Box>
|
||||
<Text as="label" className="text-sm font-medium">
|
||||
{t("statusPageConfig.publicUrl")}
|
||||
</Text>
|
||||
<Flex gap="2">
|
||||
<Box className="flex-1 relative">
|
||||
<TextField.Input
|
||||
value={config.publicUrl}
|
||||
readOnly
|
||||
className="w-full overflow-hidden text-ellipsis whitespace-nowrap pr-4"
|
||||
/>
|
||||
</Box>
|
||||
<Button variant="secondary" onClick={handleCopyUrl}>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckIcon width="16" height="16" />
|
||||
<Text>{t("common.copied")}</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon width="16" height="16" />
|
||||
<Text>{t("common.copy")}</Text>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text className="text-xs text-gray-500">
|
||||
{t("statusPageConfig.publicUrlHelp")}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{showErrorToast && (
|
||||
<Toast.Root
|
||||
className="ToastRoot"
|
||||
duration={3000}
|
||||
style={{ backgroundColor: "var(--red-9)", borderRadius: "8px" }}
|
||||
>
|
||||
<div className="ToastContent">
|
||||
<div
|
||||
className="ToastIcon"
|
||||
style={{
|
||||
backgroundColor: "var(--red-10)",
|
||||
borderRadius: "50%",
|
||||
padding: "8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.5 0.875C3.83475 0.875 0.875 3.83475 0.875 7.5C0.875 11.1652 3.83475 14.125 7.5 14.125C11.1652 14.125 14.125 11.1652 14.125 7.5C14.125 3.83475 11.1652 0.875 7.5 0.875ZM7.5 8.5C6.94772 8.5 6.5 8.05228 6.5 7.5C6.5 6.94772 6.94772 6.5 7.5 6.5C8.05228 6.5 8.5 6.94772 8.5 7.5C8.5 8.05228 8.05228 8.5 7.5 8.5ZM6.75 4.25L7 6.25H8L8.25 4.25H6.75Z"
|
||||
fill="white"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ToastText">
|
||||
<div
|
||||
className="ToastTitle"
|
||||
style={{ fontSize: "15px", fontWeight: "600" }}
|
||||
>
|
||||
{t("common.error")}
|
||||
</div>
|
||||
<div
|
||||
className="ToastDescription"
|
||||
style={{ fontSize: "13px", opacity: "0.9" }}
|
||||
>
|
||||
{toastMessage}
|
||||
</div>
|
||||
</div>
|
||||
<Toast.Close
|
||||
className="ToastClose"
|
||||
style={{
|
||||
opacity: "0.8",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "white",
|
||||
fontSize: "20px",
|
||||
cursor: "pointer",
|
||||
padding: "4px 8px",
|
||||
}}
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
×
|
||||
</Toast.Close>
|
||||
</div>
|
||||
</Toast.Root>
|
||||
)}
|
||||
</Toast.Provider>
|
||||
</Container>
|
||||
</Box>
|
||||
<Box className="mt-1">
|
||||
<Text className="text-sm font-medium text-ellipsis">
|
||||
{t("statusPageConfig.selectionNote")}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="services">
|
||||
<Flex direction="column">
|
||||
<Text>{t("statusPageConfig.selectServicesPrompt")}</Text>
|
||||
|
||||
{config.monitors.length === 0 ? (
|
||||
<Text>{t("monitors.noMonitors")}</Text>
|
||||
) : (
|
||||
<Box>
|
||||
{config.monitors.map((monitor) => {
|
||||
console.log(
|
||||
`【${t("statusPageConfig.serviceRendering")}】${
|
||||
monitor.name
|
||||
}(${monitor.id}), ${t(
|
||||
"statusPageConfig.selectedStatus"
|
||||
)}: ${monitor.selected}`
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
key={monitor.id}
|
||||
className="items-center justify-between p-2 hover:bg-gray-50 rounded-md"
|
||||
>
|
||||
<Text>{monitor.name}</Text>
|
||||
<Checkbox
|
||||
id={`monitor-${monitor.id}`}
|
||||
className="ml-auto"
|
||||
checked={monitor.selected}
|
||||
onCheckedChange={(checked) => {
|
||||
const newCheckedState = !!checked;
|
||||
console.log(
|
||||
`${t(
|
||||
"statusPageConfig.monitorStatusChange"
|
||||
)}: ${monitor.name}(${monitor.id}), ${t(
|
||||
"statusPageConfig.from"
|
||||
)} ${monitor.selected} ${t(
|
||||
"statusPageConfig.to"
|
||||
)} ${newCheckedState}`
|
||||
);
|
||||
handleMonitorChange(monitor.id, newCheckedState);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="agents">
|
||||
<Flex direction="column">
|
||||
<Text>{t("statusPageConfig.selectAgentsPrompt")}</Text>
|
||||
|
||||
{config.agents.length === 0 ? (
|
||||
<Text color="gray">{t("agents.noAgents")}</Text>
|
||||
) : (
|
||||
<Box>
|
||||
{config.agents.map((agent) => {
|
||||
console.log(
|
||||
`【${t("statusPageConfig.agentRendering")}】${
|
||||
agent.name
|
||||
}(${agent.id}), ${t(
|
||||
"statusPageConfig.selectedStatus"
|
||||
)}: ${agent.selected}`
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
key={agent.id}
|
||||
className="items-center justify-between p-2 hover:bg-gray-50 rounded-md"
|
||||
>
|
||||
<Text size="3">{agent.name}</Text>
|
||||
<Checkbox
|
||||
id={`agent-${agent.id}`}
|
||||
checked={agent.selected}
|
||||
className="ml-auto"
|
||||
onCheckedChange={(checked) => {
|
||||
const newCheckedState = !!checked;
|
||||
console.log(
|
||||
`${t("statusPageConfig.agentStatusChange")}: ${
|
||||
agent.name
|
||||
}(${agent.id}), ${t("statusPageConfig.from")} ${
|
||||
agent.selected
|
||||
} ${t(
|
||||
"statusPageConfig.to"
|
||||
)} ${newCheckedState}`
|
||||
);
|
||||
handleAgentChange(agent.id, newCheckedState);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</TabsContent>
|
||||
</Box>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
TextField,
|
||||
Box,
|
||||
} from "@radix-ui/themes";
|
||||
import { Flex, Heading, Text, TextField, Box } from "@radix-ui/themes";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
import { useAuth } from "../../providers/AuthProvider";
|
||||
import { updateUser, changePassword } from "../../api/users";
|
||||
import { UpdateUserRequest, ChangePasswordRequest } from "../../types/users";
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Table, Text, Flex, Heading, Box } from "@radix-ui/themes";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Text,
|
||||
Flex,
|
||||
Heading,
|
||||
AlertDialog,
|
||||
Box,
|
||||
} from "@radix-ui/themes";
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogAction,
|
||||
} from "@/components/ui";
|
||||
import { getAllUsers, deleteUser } from "../../api/users";
|
||||
import { User } from "../../types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -107,48 +109,37 @@ const UsersList = () => {
|
||||
<Table.Cell>{user.role}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Flex gap="2">
|
||||
<Button
|
||||
size="1"
|
||||
onClick={() => navigate(`/users/${user.id}`)}
|
||||
>
|
||||
<Button variant="secondary" onClick={() => navigate(`/users/${user.id}`)}>
|
||||
{t("common.edit")}
|
||||
</Button>
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger>
|
||||
<Button size="1" color="red">
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Title>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger>
|
||||
{t("common.delete")}
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>
|
||||
{t("common.deleteConfirmation")}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("users.deleteConfirm", {
|
||||
username: user.username,
|
||||
})}
|
||||
</AlertDialog.Description>
|
||||
</AlertDialogDescription>
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<AlertDialog.Cancel>
|
||||
<Button variant="soft" color="gray">
|
||||
<AlertDialogCancel>
|
||||
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setUserToDelete(user.id);
|
||||
handleDelete();
|
||||
}}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</AlertDialog.Action>
|
||||
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => {
|
||||
setUserToDelete(user.id);
|
||||
handleDelete();
|
||||
}}>
|
||||
{t("common.delete")}
|
||||
</AlertDialogAction>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Flex>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
useEffect,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import i18n from "../locales/config";
|
||||
import i18n from "../i18n/config";
|
||||
import { LanguageContextType } from "../types/language";
|
||||
|
||||
const availableLanguages = [
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
/* 通用动画定义 */
|
||||
|
||||
/* 淡入效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 淡出效果 */
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 从上方滑入 */
|
||||
@keyframes slideInFromTop {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 从下方滑入 */
|
||||
@keyframes slideInFromBottom {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 缩放入场 */
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 脉冲动画 - 适用于状态指示器 */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(0, 200, 0, 0.4);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 6px rgba(0, 200, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(0, 200, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 呼吸灯效果 */
|
||||
@keyframes breathe {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 边框闪烁 */
|
||||
@keyframes borderPulse {
|
||||
0% {
|
||||
border-color: rgba(0, 100, 255, 0.4);
|
||||
}
|
||||
50% {
|
||||
border-color: rgba(0, 100, 255, 1);
|
||||
}
|
||||
100% {
|
||||
border-color: rgba(0, 100, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 悬浮效果类 */
|
||||
.hover-lift {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 通用过渡效果类 */
|
||||
.transition-all {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.transition-transform {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* 动画应用类 */
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.animate-slideInFromTop {
|
||||
animation: slideInFromTop 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.animate-slideInFromBottom {
|
||||
animation: slideInFromBottom 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.animate-scaleIn {
|
||||
animation: scaleIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.animate-breathe {
|
||||
animation: breathe 2s infinite;
|
||||
}
|
||||
|
||||
.animate-rotate {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.animate-borderPulse {
|
||||
animation: borderPulse 2s infinite;
|
||||
}
|
||||
@@ -1,801 +0,0 @@
|
||||
/* 状态页配置组件样式 */
|
||||
.page-container.detail-page {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 微动画 */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Toast 样式 */
|
||||
.ToastViewport {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
gap: 12px;
|
||||
width: 380px;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
z-index: 2147483647;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ToastRoot {
|
||||
background-color: var(--green-9);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
padding: 16px;
|
||||
color: white;
|
||||
transform-origin: bottom right;
|
||||
animation: toast-slide-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.ToastRoot[style*="background-color: var(--red-9)"] {
|
||||
background-color: var(--red-9);
|
||||
}
|
||||
|
||||
.ToastContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ToastIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ToastText {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ToastTitle {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ToastDescription {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.ToastClose {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.ToastClose:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 服务项样式 */
|
||||
.service-item {
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
background-color: var(--gray-3);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
/* 自定义复选框样式 */
|
||||
.custom-checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-checkbox input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.custom-checkbox label {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid var(--gray-6);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
box-sizing: border-box;
|
||||
background-color: var(--gray-1);
|
||||
}
|
||||
|
||||
/* 选中状态的样式 */
|
||||
.custom-checkbox input[type="checkbox"]:checked + label,
|
||||
.custom-checkbox label.checked {
|
||||
background-color: var(--blue-9);
|
||||
border-color: var(--blue-9);
|
||||
box-shadow: 0 0 0 2px rgba(0, 120, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 选中状态的勾选图标 */
|
||||
.custom-checkbox input[type="checkbox"]:checked + label:after,
|
||||
.custom-checkbox label.checked:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 3px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-checkbox input[type="checkbox"]:focus + label {
|
||||
box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.15);
|
||||
}
|
||||
|
||||
.custom-checkbox input[type="checkbox"]:hover:not(:checked) + label {
|
||||
background-color: var(--gray-3);
|
||||
border-color: var(--gray-7);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes toast-slide-in {
|
||||
from {
|
||||
transform: translateX(calc(100% + 24px));
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航栏样式 */
|
||||
.navbar-wrapper {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-bottom: 1px solid var(--gray-3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-wrapper.scrolled {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid rgba(230, 230, 230, 0.7);
|
||||
}
|
||||
|
||||
.navbar-wrapper:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
height: 60px;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
.scrolled .navbar-content {
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.navbar-logo-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.navbar-logo-link:hover {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.navbar-logo-link:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.navbar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--blue-9);
|
||||
transition: color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-logo-link:hover .navbar-logo {
|
||||
color: var(--blue-10);
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
transition: all 0.25s ease;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
color: var(--gray-10);
|
||||
font-weight: 500;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background-color: var(--gray-2);
|
||||
color: var(--gray-12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nav-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 删除激活状态的下划线,改为背景色变化 */
|
||||
.nav-link.active .nav-button {
|
||||
background-color: var(--gray-2);
|
||||
color: var(--blue-9);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 添加底部滑入动画效果 */
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: var(--blue-9);
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nav-link:hover::after,
|
||||
.nav-link.active::after {
|
||||
width: 20px;
|
||||
left: calc(50% - 10px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 添加点击波纹效果 */
|
||||
.nav-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
transition: width 0.6s ease, height 0.6s ease, opacity 0.6s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.nav-button:active::before {
|
||||
width: 120%;
|
||||
height: 300%;
|
||||
opacity: 1;
|
||||
transition: width 0.3s ease, height 0.3s ease, opacity 0s;
|
||||
}
|
||||
|
||||
/* 配置页按钮样式 */
|
||||
.config-button {
|
||||
min-width: 110px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
gap: 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.config-button:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.config-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.config-button [data-size="2"] {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.config-button svg {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
min-width: 80px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.user-menu-button {
|
||||
transition: all 0.25s ease;
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.user-menu-button:hover {
|
||||
background-color: var(--gray-2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.user-menu-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
min-width: 220px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--gray-3);
|
||||
animation: dropdown-fade 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform-origin: top right;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 下拉菜单项动画 */
|
||||
[data-radix-dropdown-menu-content] [data-radix-dropdown-menu-item] {
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
margin: 2px 4px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-radix-dropdown-menu-content] [data-radix-dropdown-menu-item]:hover {
|
||||
background-color: var(--gray-2);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
[data-radix-dropdown-menu-content]
|
||||
[data-radix-dropdown-menu-item][data-accent-color="red"]:hover {
|
||||
background-color: var(--red-2);
|
||||
}
|
||||
|
||||
/* 菜单分隔线动画 */
|
||||
[data-radix-dropdown-menu-content] [data-radix-dropdown-menu-separator] {
|
||||
margin: 6px 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
/* 菜单内容动画 */
|
||||
[data-radix-dropdown-menu-content]
|
||||
[data-radix-dropdown-menu-item]
|
||||
[data-size="2"] {
|
||||
font-size: 13px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
[data-radix-dropdown-menu-content]
|
||||
[data-radix-dropdown-menu-item]:hover
|
||||
[data-size="2"] {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
@keyframes dropdown-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面切换过渡动画 */
|
||||
.main-content {
|
||||
animation: page-fade-in 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes page-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
/* 配置页标签样式 */
|
||||
.config-tabs {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.config-tabs-list {
|
||||
border-bottom: none !important;
|
||||
padding: 0 8px 0 4px;
|
||||
margin-bottom: 0;
|
||||
gap: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
/* Radix UI Tabs 样式重置 - 强力覆盖 */
|
||||
/* 这些选择器会覆盖 Radix UI 的默认样式 */
|
||||
button[data-radix-collection-item][role="tab"],
|
||||
button[data-orientation="horizontal"][role="tab"],
|
||||
button[data-state][role="tab"],
|
||||
.rt-TabsTrigger,
|
||||
.tab-trigger,
|
||||
[role="tab"] {
|
||||
all: unset !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
padding: 10px 14px;
|
||||
background-color: transparent;
|
||||
border-radius: 4px 4px 0 0;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--gray-11);
|
||||
text-decoration: none !important;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
button[data-radix-collection-item][role="tab"]:focus,
|
||||
button[data-orientation="horizontal"][role="tab"]:focus,
|
||||
button[data-state][role="tab"]:focus,
|
||||
.rt-TabsTrigger:focus,
|
||||
.tab-trigger:focus,
|
||||
[role="tab"]:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
button[data-radix-collection-item][role="tab"]:focus-visible,
|
||||
button[data-orientation="horizontal"][role="tab"]:focus-visible,
|
||||
button[data-state][role="tab"]:focus-visible,
|
||||
.rt-TabsTrigger:focus-visible,
|
||||
.tab-trigger:focus-visible,
|
||||
[role="tab"]:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
background-color: var(--gray-2);
|
||||
}
|
||||
|
||||
button[data-radix-collection-item][role="tab"][data-state="active"],
|
||||
button[data-orientation="horizontal"][role="tab"][data-state="active"],
|
||||
button[data-state="active"][role="tab"],
|
||||
.rt-TabsTrigger[data-state="active"],
|
||||
.tab-trigger[data-state="active"],
|
||||
[role="tab"][data-state="active"] {
|
||||
color: var(--blue-9);
|
||||
font-weight: 600;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
button[data-radix-collection-item][role="tab"][data-state="active"]::after,
|
||||
button[data-orientation="horizontal"][role="tab"][data-state="active"]::after,
|
||||
button[data-state="active"][role="tab"]::after,
|
||||
.rt-TabsTrigger[data-state="active"]::after,
|
||||
.tab-trigger[data-state="active"]::after,
|
||||
[role="tab"][data-state="active"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: transparent;
|
||||
transform: scaleX(1);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
button[data-radix-collection-item][role="tab"]:hover,
|
||||
button[data-orientation="horizontal"][role="tab"]:hover,
|
||||
button[data-state][role="tab"]:hover,
|
||||
.rt-TabsTrigger:hover,
|
||||
.tab-trigger:hover,
|
||||
[role="tab"]:hover {
|
||||
color: var(--blue-10);
|
||||
background-color: var(--gray-2);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
button[data-radix-collection-item][role="tab"]:not(
|
||||
[data-state="active"]
|
||||
):hover::after,
|
||||
button[data-orientation="horizontal"][role="tab"]:not(
|
||||
[data-state="active"]
|
||||
):hover::after,
|
||||
button[data-state][role="tab"]:not([data-state="active"]):hover::after,
|
||||
.rt-TabsTrigger:not([data-state="active"]):hover::after,
|
||||
.tab-trigger:not([data-state="active"]):hover::after,
|
||||
[role="tab"]:not([data-state="active"]):hover::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
button[data-radix-collection-item][role="tab"]:active,
|
||||
button[data-orientation="horizontal"][role="tab"]:active,
|
||||
button[data-state][role="tab"]:active,
|
||||
.rt-TabsTrigger:active,
|
||||
.tab-trigger:active,
|
||||
[role="tab"]:active {
|
||||
transform: translateY(0);
|
||||
background-color: var(--gray-3);
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* 标签内容区样式 */
|
||||
[data-radix-tabs-content],
|
||||
[role="tabpanel"],
|
||||
.rt-TabsContent,
|
||||
.tab-content {
|
||||
padding-top: 24px !important;
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
animation: tab-content-fade-in 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-content-container {
|
||||
border-top: none !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@keyframes tab-content-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签列表样式 */
|
||||
[data-orientation="horizontal"] {
|
||||
border-bottom: none !important;
|
||||
padding-bottom: 0 !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
/* 解决Safari特定渲染问题 */
|
||||
@media not all and (min-resolution: 0.001dpcm) {
|
||||
@supports (-webkit-appearance: none) {
|
||||
button[role="tab"],
|
||||
.tab-trigger,
|
||||
.rt-TabsTrigger {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单输入字段样式 */
|
||||
[data-radix-text-field-input] {
|
||||
transition: all 0.2s ease;
|
||||
border-color: var(--gray-5);
|
||||
font-size: 14px !important;
|
||||
box-sizing: border-box;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
[data-radix-text-field-input]:hover {
|
||||
border-color: var(--gray-7);
|
||||
}
|
||||
|
||||
[data-radix-text-field-input]:focus {
|
||||
border-color: var(--blue-8);
|
||||
box-shadow: 0 0 0 1px var(--blue-5);
|
||||
}
|
||||
|
||||
[data-radix-text-field-input][readonly] {
|
||||
background-color: var(--gray-1);
|
||||
color: var(--gray-11);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
textarea {
|
||||
transition: all 0.2s ease;
|
||||
border-color: var(--gray-5) !important;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
textarea:hover {
|
||||
border-color: var(--gray-7) !important;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
border-color: var(--blue-8) !important;
|
||||
box-shadow: 0 0 0 1px var(--blue-5);
|
||||
}
|
||||
|
||||
/* 布局样式 */
|
||||
.layout-container {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
padding: 16px 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* 仪表盘样式 */
|
||||
.dashboard-container {
|
||||
width: 100%;
|
||||
min-height: calc(100vh - 112px);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.status-summary-card {
|
||||
height: 100%;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.status-summary-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.status-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.monitor-card,
|
||||
.agent-card {
|
||||
height: 100%;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.monitor-card:hover,
|
||||
.agent-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--accent-9);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-link-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 覆盖 Radix UI TabsTrigger 底部边框 */
|
||||
html body .rt-TabsTrigger:where([data-state="active"])::before,
|
||||
html body .rt-TabsTrigger[data-state="active"]::before {
|
||||
background-color: none !important;
|
||||
display: none !important;
|
||||
height: 0 !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* 确保所有相关的伪元素都被覆盖 */
|
||||
html body button[role="tab"][data-state="active"]::before,
|
||||
html body [role="tab"][data-state="active"]::before,
|
||||
html body .tab-trigger[data-state="active"]::before {
|
||||
background-color: none !important;
|
||||
display: none !important;
|
||||
height: 0 !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
+110
-186
@@ -1,197 +1,121 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 全局样式设置 */
|
||||
:root {
|
||||
--max-content-width: 1200px;
|
||||
--content-padding: 0 16px;
|
||||
--header-height: 64px;
|
||||
--footer-height: 56px;
|
||||
--page-top-spacing: 24px;
|
||||
--section-spacing: 16px;
|
||||
|
||||
/* 基础字体设置 */
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.13 0.028 261.692);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.13 0.028 261.692);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.13 0.028 261.692);
|
||||
--primary: oklch(0.21 0.034 264.665);
|
||||
--primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--secondary: oklch(0.967 0.003 264.542);
|
||||
--secondary-foreground: oklch(0.21 0.034 264.665);
|
||||
--muted: oklch(0.967 0.003 264.542);
|
||||
--muted-foreground: oklch(0.551 0.027 264.364);
|
||||
--accent: oklch(0.967 0.003 264.542);
|
||||
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.928 0.006 264.531);
|
||||
--input: oklch(0.928 0.006 264.531);
|
||||
--ring: oklch(0.707 0.022 261.325);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0.002 247.839);
|
||||
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
||||
--sidebar-primary: oklch(0.21 0.034 264.665);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--sidebar-border: oklch(0.928 0.006 264.531);
|
||||
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||
}
|
||||
|
||||
/* 重置默认样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
.dark {
|
||||
--background: oklch(0.13 0.028 261.692);
|
||||
--foreground: oklch(0.985 0.002 247.839);
|
||||
--card: oklch(0.21 0.034 264.665);
|
||||
--card-foreground: oklch(0.985 0.002 247.839);
|
||||
--popover: oklch(0.21 0.034 264.665);
|
||||
--popover-foreground: oklch(0.985 0.002 247.839);
|
||||
--primary: oklch(0.928 0.006 264.531);
|
||||
--primary-foreground: oklch(0.21 0.034 264.665);
|
||||
--secondary: oklch(0.278 0.033 256.848);
|
||||
--secondary-foreground: oklch(0.985 0.002 247.839);
|
||||
--muted: oklch(0.278 0.033 256.848);
|
||||
--muted-foreground: oklch(0.707 0.022 261.325);
|
||||
--accent: oklch(0.278 0.033 256.848);
|
||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.034 264.665);
|
||||
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent: oklch(0.278 0.033 256.848);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* 按钮基础样式 */
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
/* 浅色主题设置 */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* App样式 */
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
/* 页面容器样式 */
|
||||
.page-container {
|
||||
max-width: var(--max-content-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
/* 主要内容区域样式 */
|
||||
.main-content {
|
||||
min-height: calc(100vh - var(--header-height) - var(--footer-height));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 详情页顶部间隙 */
|
||||
.detail-page {
|
||||
padding-top: var(--page-top-spacing);
|
||||
}
|
||||
|
||||
/* 详情页标题栏 */
|
||||
.detail-header {
|
||||
margin-bottom: var(--section-spacing);
|
||||
}
|
||||
|
||||
/* 详情页内容区域 */
|
||||
.detail-content {
|
||||
margin-top: var(--section-spacing);
|
||||
}
|
||||
|
||||
/* 表单输入样式 */
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
border: 1px solid var(--gray-7);
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-panel);
|
||||
color: var(--gray-12);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.text-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-8);
|
||||
box-shadow: 0 0 0 1px var(--accent-8);
|
||||
}
|
||||
|
||||
.text-input::placeholder {
|
||||
color: var(--gray-9);
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
/* 响应式布局辅助类 */
|
||||
@media (max-width: 768px) {
|
||||
.hide-on-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.hide-on-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 辅助类 */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* 导入全局基础样式 */
|
||||
@import "./global.css";
|
||||
|
||||
/* 导入动画样式 */
|
||||
@import "./animations.css";
|
||||
|
||||
/* 导入组件样式 */
|
||||
@import "./components.css";
|
||||
|
||||
/* 导入模拟 Tailwind 类样式 */
|
||||
@import "./tailwind-classes.css";
|
||||
|
||||
/* 导入页面样式 */
|
||||
@import "./pages.css";
|
||||
@@ -1,218 +0,0 @@
|
||||
/* AgentDetail页面样式 */
|
||||
.detail-page .detail-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.system-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.system-info-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.system-info-value {
|
||||
color: var(--gray-12);
|
||||
}
|
||||
|
||||
/* 加载状态和错误状态样式 */
|
||||
.loading-container,
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
/* AgentsList页面样式 */
|
||||
.agent-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.agent-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-indicator-active {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--green-9);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator-connecting {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--amber-9);
|
||||
}
|
||||
|
||||
.status-indicator-inactive {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--red-9);
|
||||
}
|
||||
|
||||
.status-bar-active {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, var(--green-9), var(--cyan-9));
|
||||
}
|
||||
|
||||
.status-bar-connecting {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, var(--amber-9), var(--yellow-9));
|
||||
}
|
||||
|
||||
.status-bar-inactive {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, var(--red-9), var(--amber-9));
|
||||
}
|
||||
|
||||
/* 登录页面样式 */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f9fafb;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
font-size: 1.875rem; /* 相当于text-3xl */
|
||||
font-weight: 800; /* 相当于font-extrabold */
|
||||
color: #111827; /* 相当于text-gray-900 */
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.875rem; /* 相当于text-sm */
|
||||
color: #4b5563; /* 相当于text-gray-600 */
|
||||
}
|
||||
|
||||
.login-link {
|
||||
font-weight: 500;
|
||||
color: #2563eb; /* 相当于text-blue-600 */
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #3b82f6; /* 相当于text-blue-500 */
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.login-input-group {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.login-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db; /* 相当于border-gray-300 */
|
||||
color: #111827; /* 相当于text-gray-900 */
|
||||
font-size: 0.875rem; /* 相当于sm:text-sm */
|
||||
}
|
||||
|
||||
.login-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6; /* 相当于focus:border-blue-500 */
|
||||
z-index: 10; /* 相当于focus:z-10 */
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25); /* 相当于focus:ring-blue-500 */
|
||||
}
|
||||
|
||||
.login-input-top {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.login-input-bottom {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: #dc2626; /* 相当于text-red-600 */
|
||||
font-size: 0.875rem; /* 相当于text-sm */
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-checkbox {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: #2563eb; /* 相当于text-blue-600 */
|
||||
border-color: #d1d5db; /* 相当于border-gray-300 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.login-checkbox-label {
|
||||
margin-left: 8px;
|
||||
display: block;
|
||||
font-size: 0.875rem; /* 相当于text-sm */
|
||||
color: #111827; /* 相当于text-gray-900 */
|
||||
}
|
||||
|
||||
.login-forgot {
|
||||
font-size: 0.875rem; /* 相当于text-sm */
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
/* 模拟 Login.tsx 中使用的 Tailwind 类名样式 */
|
||||
|
||||
/* 布局类 */
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.space-y-8 > * + * {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.space-y-6 > * + * {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* 间距类 */
|
||||
.py-12 {
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* 响应式尺寸 */
|
||||
@media (min-width: 640px) {
|
||||
.sm\:px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:px-8 {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 颜色类 */
|
||||
.bg-gray-50 {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-gray-900 {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.placeholder-gray-500::placeholder {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
/* 文本样式 */
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-extrabold {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 表单元素样式 */
|
||||
.appearance-none {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-none {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.rounded-t-md {
|
||||
border-top-left-radius: 0.375rem;
|
||||
border-top-right-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-b-md {
|
||||
border-bottom-left-radius: 0.375rem;
|
||||
border-bottom-right-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* 特殊样式 */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.-space-y-px > * + * {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* 输入框焦点样式 */
|
||||
.focus\:outline-none:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.focus\:ring-blue-500:focus {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.focus\:border-blue-500:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.focus\:z-10:focus {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.hover\:text-blue-500:hover {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* 复选框样式 */
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* 工具函数集合
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param dateString 日期字符串
|
||||
* @param locale 地区设置
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
export const formatDate = (
|
||||
dateString: string,
|
||||
locale: string = "zh-CN"
|
||||
): string => {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
/**
|
||||
* 截断文本
|
||||
* @param text 需要截断的文本
|
||||
* @param maxLength 最大长度
|
||||
* @returns 截断后的文本
|
||||
*/
|
||||
export const truncateText = (text: string, maxLength: number): string => {
|
||||
if (text.length <= maxLength) return text;
|
||||
return `${text.substring(0, maxLength)}...`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param func 要执行的函数
|
||||
* @param wait 等待时间(毫秒)
|
||||
* @returns 防抖处理后的函数
|
||||
*/
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: number | null = null;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
func(...args);
|
||||
}, wait);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param func 要执行的函数
|
||||
* @param limit 时间限制(毫秒)
|
||||
* @returns 节流处理后的函数
|
||||
*/
|
||||
export const throttle = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let inThrottle: boolean = false;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取查询参数
|
||||
* @param search 查询字符串
|
||||
* @returns 查询参数对象
|
||||
*/
|
||||
export const getQueryParams = (search: string): Record<string, string> => {
|
||||
const params = new URLSearchParams(search);
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
params.forEach((value, key) => {
|
||||
result[key] = value;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 深拷贝对象
|
||||
* @param obj 要拷贝的对象
|
||||
* @returns 拷贝后的对象
|
||||
*/
|
||||
export const deepClone = <T>(obj: T): T => {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机ID生成器
|
||||
* @param length ID长度
|
||||
* @returns 随机ID
|
||||
*/
|
||||
export const generateId = (length: number = 8): string => {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -20,7 +20,13 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".", // 相对于 tsconfig.app.json 文件,即 frontend/ 目录
|
||||
"paths": {
|
||||
"@/*": ["./src/*"] // 将 @/* 解析到 frontend/src/*
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
}
|
||||
+13
-6
@@ -1,13 +1,20 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"), // __dirname 指向 vite.config.ts 文件所在的目录
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8787',
|
||||
"/api": {
|
||||
target: "http://localhost:8787",
|
||||
changeOrigin: true,
|
||||
// 如果后端 API 不包含 /api 前缀,可以重写路径
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
@@ -17,4 +24,4 @@ export default defineConfig({
|
||||
// 如果需要指定前端开发服务器端口
|
||||
port: 5173,
|
||||
},
|
||||
})
|
||||
});
|
||||
Reference in new issue
Block a user