mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-06 18:06:36 +08:00
feat(ui): redesign with transparent liquid glass (#414)
* docs: add design spec for iOS 26 Liquid Glass UI redesign * docs: add implementation plan for iOS 26 Liquid Glass UI redesign * feat(ui): add liquid glass CSS variables and update root layouts * feat(ui): update Card and Modal components to iOS 26 Liquid Glass style * feat(ui): redesign dashboard page to Liquid Glass style * feat(ui): redesign node management page cards and charts * feat(ui): redesign monitor page to Liquid Glass style * feat(ui): redesign group and panel-sharing cards to Liquid Glass * feat(ui): redesign settings and user components to Liquid Glass * feat(ui): redesign profile and auth pages to Liquid Glass * fix(ui): revert to LineChart to fix compilation issues * refactor(ui): extract dashboard user header and cleanup admin layout * feat(ui): refine monitor page hero metrics and fix build * fix(ui): real data for monitor hero metrics and card padding adjustments * fix(ui): apply global card padding and fix index layouts * feat(ui): implement liquid-glass-react for all Card components * fix(ui): resolve liquid-glass-react rendering issues causing invisible text and broken layout * fix(ui): native CSS liquid-glass effect replacing NPM package * fix(ui): cleanup duplicate closing tags and complete liquid glass tables * fix(ui): apply perfect rounded corners to list views and table headers in non-compact mode * fix(ui): apply liquid glass styling to grouped rules view in forward page * fix(ui): re-implement liquid-glass natively to prevent text distortion and remove package dependency * fix(ui): remove mouse distortion and preserve proper liquid glass texture * fix(ui): remove svg filter causing text distortion and replace with pure css liquid glass * fix(ui): apply properly scoped border radius to table headers * fix(ui): remove hardcoded bg-default-50/60 from TableHeader * fix(ui): apply global liquid glass styling and transparent overlays to all modals and dialogs * fix(ui): remove absolute/relative class conflicts hiding Dialog modales and implement pure transparent layout * feat(ui): apply global liquid glass styling to cards, buttons, and diagnosis modals
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { AnimatedPage } from "@/components/animated-page";
|
||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "@/shadcn-bridge/heroui/dropdown";
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -15,6 +22,8 @@ import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-ba
|
||||
import { AnnouncementModal } from "@/pages/dashboard/components/announcement-modal";
|
||||
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
|
||||
import { MetricCard } from "@/pages/dashboard/components/metric-card";
|
||||
import { getSessionName } from "@/utils/session";
|
||||
import { safeLogout } from "@/utils/logout";
|
||||
import {
|
||||
formatNodeRenewalTime,
|
||||
getNodeRenewalCycleLabel,
|
||||
@@ -35,6 +44,15 @@ interface AddressItem {
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const navigate = useNavigate();
|
||||
const username = getSessionName() || "User";
|
||||
|
||||
const handleLogout = () => {
|
||||
safeLogout();
|
||||
toast.success("已退出登录");
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const {
|
||||
loading,
|
||||
userInfo,
|
||||
@@ -630,6 +648,48 @@ export default function DashboardPage() {
|
||||
|
||||
return (
|
||||
<AnimatedPage className="px-3 lg:px-6 py-2 lg:py-4">
|
||||
{/* 顶部个人状态和下拉菜单 */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl lg:text-3xl font-bold text-foreground">Good morning, {username}</h1>
|
||||
<p className="text-sm text-default-500 mt-1">Here's what's happening with your network today.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
className="rounded-full w-10 h-10 min-w-0 bg-primary text-white font-bold text-sm shadow-[0_4px_12px_rgba(0,122,255,0.3)]"
|
||||
isIconOnly
|
||||
variant="solid"
|
||||
>
|
||||
{username.slice(0, 2).toUpperCase()}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="用户菜单" className="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl border border-white/50 dark:border-white/10 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)]">
|
||||
<DropdownItem
|
||||
key="profile"
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
|
||||
}
|
||||
onPress={() => navigate("/profile")}
|
||||
>
|
||||
个人资料
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd" /></svg>
|
||||
}
|
||||
onPress={handleLogout}
|
||||
>
|
||||
退出登录
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{announcement && <AnnouncementBanner announcement={announcement} />}
|
||||
{announcement && (
|
||||
<AnnouncementModal
|
||||
@@ -823,7 +883,7 @@ export default function DashboardPage() {
|
||||
|
||||
{/* 隧道权限 - 管理员不显示 */}
|
||||
{!isAdmin && (
|
||||
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md">
|
||||
<Card className="mb-6 lg:mb-8">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
@@ -945,7 +1005,7 @@ export default function DashboardPage() {
|
||||
)}
|
||||
|
||||
{/* 规则配置 */}
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
@@ -1016,7 +1076,7 @@ export default function DashboardPage() {
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
className={`block px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 rounded font-mono text-xs truncate ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-green-200 dark:hover:bg-green-500/30" : ""}`}
|
||||
className={`block px-2 py-1 bg-default-50/50 dark:bg-default-100/20 backdrop-blur-md rounded-lg border border-default-200/50 dark:border-default-700/50 font-mono text-xs truncate text-foreground transition-colors duration-200 ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-default-100 dark:hover:bg-default-200/50" : ""}`}
|
||||
disabled={!hasMultipleIps(forward.inIp)}
|
||||
title={formatInAddress(
|
||||
forward.inIp,
|
||||
@@ -1037,7 +1097,7 @@ export default function DashboardPage() {
|
||||
↓
|
||||
</div>
|
||||
<button
|
||||
className={`block px-2 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded font-mono text-xs truncate ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-500/30" : ""}`}
|
||||
className={`block px-2 py-1 bg-default-50/50 dark:bg-default-100/20 backdrop-blur-md rounded-lg border border-default-200/50 dark:border-default-700/50 font-mono text-xs truncate text-foreground transition-colors duration-200 ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-default-100 dark:hover:bg-default-200/50" : ""}`}
|
||||
disabled={
|
||||
!hasMultipleRemoteAddresses(
|
||||
forward.remoteAddr,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
@@ -28,7 +27,7 @@ export const FlowChartCard = ({
|
||||
formatFlow,
|
||||
}: FlowChartCardProps) => {
|
||||
return (
|
||||
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md">
|
||||
<Card className="mb-6 lg:mb-8">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
@@ -51,17 +50,16 @@ export const FlowChartCard = ({
|
||||
) : (
|
||||
<div className="h-64 lg:h-80 w-full">
|
||||
<ResponsiveContainer height="100%" width="100%">
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid className="opacity-30" strokeDasharray="3 3" />
|
||||
<LineChart data={chartData} margin={{ top: 20, right: 0, left: -20, bottom: 0 }}>
|
||||
<XAxis
|
||||
axisLine={{ stroke: "#e5e7eb", strokeWidth: 1 }}
|
||||
axisLine={false}
|
||||
dataKey="time"
|
||||
tick={{ fontSize: 12 }}
|
||||
tick={{ fill: "currentColor", opacity: 0.5, fontSize: 12 }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={{ stroke: "#e5e7eb", strokeWidth: 1 }}
|
||||
tick={{ fontSize: 12 }}
|
||||
axisLine={false}
|
||||
tick={{ fill: "currentColor", opacity: 0.5, fontSize: 12 }}
|
||||
tickFormatter={(value: number | string) => {
|
||||
const v = typeof value === "number" ? value : Number(value);
|
||||
|
||||
@@ -97,27 +95,20 @@ export const FlowChartCard = ({
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-default-100 border border-default-200 rounded-lg shadow-lg p-3">
|
||||
<div className="bg-white/70 dark:bg-zinc-900/70 backdrop-blur-xl border border-white/50 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-4">
|
||||
<p className="font-medium text-foreground">{`时间: ${label ?? ""}`}</p>
|
||||
<p className="text-primary">{`流量: ${formatFlow(flowValue)}`}</p>
|
||||
<p className="text-primary font-semibold mt-1">{`流量: ${formatFlow(flowValue)}`}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
cursor={{ fill: "rgba(0, 122, 255, 0.05)" }}
|
||||
/>
|
||||
<Line
|
||||
activeDot={{
|
||||
r: 4,
|
||||
stroke: "#8b5cf6",
|
||||
strokeWidth: 2,
|
||||
fill: "#fff",
|
||||
}}
|
||||
dataKey="flow"
|
||||
dot={false}
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={3}
|
||||
stroke="#007aff"
|
||||
type="monotone"
|
||||
/>
|
||||
</LineChart>
|
||||
|
||||
@@ -18,22 +18,24 @@ export const MetricCard = ({
|
||||
bottomContent,
|
||||
}: MetricCardProps) => {
|
||||
return (
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<CardBody className="p-3 lg:p-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Card className="h-48 flex flex-col justify-between overflow-hidden">
|
||||
<CardBody className="!p-4 md:!p-6 h-full flex flex-col justify-between overflow-visible">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs lg:text-sm text-default-600 truncate">
|
||||
<p className="text-sm font-medium text-default-600 truncate">
|
||||
{title}
|
||||
</p>
|
||||
<div
|
||||
className={`p-1.5 lg:p-2 rounded-lg flex-shrink-0 ${iconClassName}`}
|
||||
className={`p-2 rounded-xl flex-shrink-0 ${iconClassName}`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-base lg:text-xl font-bold text-foreground truncate">
|
||||
<p className="text-2xl lg:text-3xl font-bold text-foreground truncate mt-2">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-auto pt-4">
|
||||
{bottomContent}
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
@@ -3689,7 +3689,7 @@ export default function ForwardPage() {
|
||||
return (
|
||||
<Card
|
||||
key={forward.id}
|
||||
className="group h-full flex flex-col shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden"
|
||||
className="group h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
<CardHeader className="pb-2 md:pb-2">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
@@ -3966,7 +3966,7 @@ export default function ForwardPage() {
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between mb-6 gap-3">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<Button
|
||||
color={activeFilterCount > 0 ? "primary" : "default"}
|
||||
className="bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 font-semibold hover:bg-default/20 dark:hover:bg-default/30"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
@@ -3982,7 +3982,6 @@ export default function ForwardPage() {
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
variant={activeFilterCount > 0 ? "flat" : "bordered"}
|
||||
onPress={() => setIsSearchModalOpen(true)}
|
||||
>
|
||||
高级筛选 {activeFilterCount > 0 ? `(${activeFilterCount})` : ""}
|
||||
@@ -4177,7 +4176,7 @@ export default function ForwardPage() {
|
||||
{sortedForwards.length} 条规则
|
||||
</span>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-xl border border-divider bg-content1 shadow-md">
|
||||
<Card className="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/40 dark:bg-zinc-900/40 backdrop-blur-2xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
|
||||
<DndContext
|
||||
collisionDetection={pointerWithin}
|
||||
sensors={sensors}
|
||||
@@ -4190,9 +4189,10 @@ export default function ForwardPage() {
|
||||
<Table
|
||||
aria-label="全部规则列表"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors group",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -4253,10 +4253,10 @@ export default function ForwardPage() {
|
||||
</Table>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||
暂无规则配置
|
||||
@@ -4302,7 +4302,7 @@ export default function ForwardPage() {
|
||||
</DndContext>
|
||||
</>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||
暂无规则配置
|
||||
@@ -4327,9 +4327,9 @@ export default function ForwardPage() {
|
||||
return (
|
||||
<div
|
||||
key={`grouped-table-${group.userId}-${group.userName}`}
|
||||
className="overflow-hidden rounded-xl border border-divider bg-content1 shadow-md"
|
||||
className="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-divider bg-default-100/40 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-white/20 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-md px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{group.userName}
|
||||
@@ -4415,10 +4415,10 @@ export default function ForwardPage() {
|
||||
collapsed={collapsed}
|
||||
countClassName="text-xs text-default-600"
|
||||
groupUserId={group.userId}
|
||||
headerClassName="flex items-center justify-between border-b border-divider bg-default-100/60 px-4 py-2.5"
|
||||
headerClassName="flex items-center justify-between border-b border-white/20 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-md px-4 py-2.5"
|
||||
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||
tunnel={tunnel}
|
||||
wrapperClassName="overflow-hidden rounded-lg border border-divider bg-default-50/60"
|
||||
wrapperClassName="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/40 dark:bg-zinc-900/40 backdrop-blur-2xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
|
||||
onToggleCollapsed={() =>
|
||||
toggleTunnelGroupCollapsed(
|
||||
group.userId,
|
||||
@@ -4439,9 +4439,10 @@ export default function ForwardPage() {
|
||||
aria-label={`${group.userName}-${tunnel.tunnelName}规则列表`}
|
||||
className={`table-fixed ${FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS}`}
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -4561,7 +4562,7 @@ export default function ForwardPage() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||
暂无规则配置
|
||||
@@ -4637,10 +4638,10 @@ export default function ForwardPage() {
|
||||
collapsed={collapsed}
|
||||
countClassName="text-xs text-default-600"
|
||||
groupUserId={group.userId}
|
||||
headerClassName="flex items-center justify-between rounded-lg bg-default-100/60 px-3 py-2"
|
||||
headerClassName="flex items-center justify-between rounded-lg bg-white/40 dark:bg-black/40 backdrop-blur-md border border-white/20 dark:border-white/10 px-3 py-2"
|
||||
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||
tunnel={tunnel}
|
||||
wrapperClassName="rounded-xl border border-divider bg-default-50/60 space-y-3"
|
||||
wrapperClassName="rounded-xl border border-divider bg-white/20 dark:bg-black/20 space-y-3"
|
||||
onToggleCollapsed={() =>
|
||||
toggleTunnelGroupCollapsed(
|
||||
group.userId,
|
||||
@@ -4682,7 +4683,7 @@ export default function ForwardPage() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||
暂无规则配置
|
||||
@@ -5415,7 +5416,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody className="bg-content1">
|
||||
<ModalBody className="bg-transparent">
|
||||
{diagnosisResult ? (
|
||||
<div className="space-y-4">
|
||||
{diagnosisLoading && (
|
||||
@@ -5939,7 +5940,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter className="bg-content1 border-t border-divider">
|
||||
<ModalFooter className="bg-transparent border-t border-white/20 dark:border-white/10">
|
||||
<Button variant="light" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
|
||||
@@ -489,7 +489,15 @@ export default function GroupPage() {
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Table aria-label="隧道分组列表">
|
||||
<Table
|
||||
aria-label="隧道分组列表"
|
||||
classNames={{
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn>名称</TableColumn>
|
||||
<TableColumn>隧道</TableColumn>
|
||||
@@ -561,7 +569,15 @@ export default function GroupPage() {
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Table aria-label="用户分组列表">
|
||||
<Table
|
||||
aria-label="用户分组列表"
|
||||
classNames={{
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn>名称</TableColumn>
|
||||
<TableColumn>用户</TableColumn>
|
||||
@@ -665,7 +681,15 @@ export default function GroupPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table aria-label="分组权限列表">
|
||||
<Table
|
||||
aria-label="分组权限列表"
|
||||
classNames={{
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn>ID</TableColumn>
|
||||
<TableColumn>用户分组</TableColumn>
|
||||
|
||||
@@ -8,9 +8,7 @@ import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { title } from "@/components/primitives";
|
||||
import { VersionFooter } from "@/components/version-footer";
|
||||
import DefaultLayout from "@/layouts/default";
|
||||
import { login, LoginData, checkCaptcha, getConfigByName } from "@/api";
|
||||
import { writeLoginSession } from "@/utils/session";
|
||||
import { useWebViewMode } from "@/hooks/useWebViewMode";
|
||||
@@ -152,29 +150,36 @@ export default function IndexPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 pb-20 min-h-[calc(100dvh-120px)] sm:min-h-[calc(100dvh-200px)]">
|
||||
<div className="relative flex flex-col min-h-screen bg-mesh-gradient overflow-hidden">
|
||||
<section className="flex flex-col items-center justify-center flex-1 w-full p-4 relative z-10">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="w-full max-w-md px-4 sm:px-0"
|
||||
className="w-full max-w-[420px] px-4 sm:px-0"
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
>
|
||||
<Card className="w-full">
|
||||
<Card className="w-full bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_20px_40px_rgba(0,0,0,0.15)] border-white/80 dark:border-white/10 rounded-[32px] p-2 sm:p-4">
|
||||
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
||||
<h1 className={title({ size: "sm" })}>登陆</h1>
|
||||
<p className="text-small text-default-500 mt-2">
|
||||
请输入您的账号信息
|
||||
<div className="w-14 h-14 bg-primary rounded-2xl flex items-center justify-center mb-4 shadow-sm text-white">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">Welcome to Flvx</h1>
|
||||
<p className="text-sm text-default-500 mt-2 font-medium">
|
||||
Sign in to manage your networks
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardBody className="px-6 py-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardBody className="px-6 py-6 mt-2">
|
||||
<div className="flex flex-col gap-5">
|
||||
<Input
|
||||
classNames={{
|
||||
inputWrapper: "bg-white/50 dark:bg-black/50 backdrop-blur-md border border-white/60 dark:border-white/10 h-12 shadow-sm rounded-xl",
|
||||
input: "text-base font-medium",
|
||||
}}
|
||||
errorMessage={errors.username}
|
||||
isDisabled={loading}
|
||||
isInvalid={!!errors.username}
|
||||
label="用户名"
|
||||
placeholder="请输入用户名"
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
value={form.username}
|
||||
variant="bordered"
|
||||
onChange={(e) =>
|
||||
@@ -184,10 +189,14 @@ export default function IndexPage() {
|
||||
/>
|
||||
|
||||
<Input
|
||||
classNames={{
|
||||
inputWrapper: "bg-white/50 dark:bg-black/50 backdrop-blur-md border border-white/60 dark:border-white/10 h-12 shadow-sm rounded-xl",
|
||||
input: "text-base font-medium tracking-wider",
|
||||
}}
|
||||
isDisabled={loading}
|
||||
isInvalid={!!errors.password}
|
||||
label="密码"
|
||||
placeholder="请输入密码"
|
||||
label="Password"
|
||||
placeholder="••••••••"
|
||||
type="password"
|
||||
value={form.password}
|
||||
variant="bordered"
|
||||
@@ -198,14 +207,12 @@ export default function IndexPage() {
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-2"
|
||||
color="primary"
|
||||
className="mt-4 h-12 rounded-xl bg-primary text-white font-bold text-base shadow-[0_8px_16px_rgba(0,122,255,0.3)] transition-transform active:scale-[0.98]"
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
size="lg"
|
||||
onPress={handleLogin}
|
||||
>
|
||||
{loading ? (showCaptcha ? "验证中..." : "登录中...") : "登录"}
|
||||
{loading ? (showCaptcha ? "Verifying..." : "Signing in...") : "Sign In"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
@@ -275,6 +282,6 @@ export default function IndexPage() {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,9 +267,10 @@ export default function LimitPage() {
|
||||
aria-label="限速规则列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||
import { getMonitorNodes } from "@/api";
|
||||
import { MonitorView } from "@/pages/node/monitor-view";
|
||||
import { TunnelMonitorView } from "@/pages/node/tunnel-monitor-view";
|
||||
import { useNodeRealtime } from "@/pages/node/use-node-realtime";
|
||||
|
||||
type MonitorNode = {
|
||||
id: number;
|
||||
@@ -20,6 +21,16 @@ type MonitorNode = {
|
||||
|
||||
type MonitorTab = "nodes" | "tunnels";
|
||||
|
||||
const formatBytesPerSecond = (bytesPerSecond: number): string => {
|
||||
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
|
||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
export default function MonitorPage() {
|
||||
const [nodes, setNodes] = useState<MonitorNodeApiItem[]>([]);
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
@@ -27,6 +38,55 @@ export default function MonitorPage() {
|
||||
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
|
||||
const [activeTab, setActiveTab] = useState<MonitorTab>("nodes");
|
||||
|
||||
const [realtimeNodeStatus, setRealtimeNodeStatus] = useState<Record<number, "online" | "offline">>({});
|
||||
const [realtimeNodeMetrics, setRealtimeNodeMetrics] = useState<Record<number, any>>({});
|
||||
|
||||
const handleRealtimeMessage = useCallback((message: any) => {
|
||||
const nodeId = Number(message?.id ?? 0);
|
||||
if (!nodeId || Number.isNaN(nodeId)) return;
|
||||
|
||||
const type = String(message?.type ?? "");
|
||||
const payload = message?.data;
|
||||
|
||||
if (type === "status") {
|
||||
const status = Number(payload);
|
||||
setRealtimeNodeStatus((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: status === 1 ? "online" : "offline",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "metric") {
|
||||
let raw = payload;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
raw = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!raw || typeof raw !== "object") return;
|
||||
const metric = raw as any;
|
||||
|
||||
setRealtimeNodeMetrics((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: {
|
||||
cpuUsage: Number(metric.cpuUsage ?? metric.cpu_usage ?? 0),
|
||||
netInSpeed: Number(metric.netInSpeed ?? metric.net_in_speed ?? 0),
|
||||
netOutSpeed: Number(metric.netOutSpeed ?? metric.net_out_speed ?? 0),
|
||||
tcpConns: Number(metric.tcpConns ?? metric.tcp_conns ?? 0),
|
||||
udpConns: Number(metric.udpConns ?? metric.udp_conns ?? 0),
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useNodeRealtime({
|
||||
onMessage: handleRealtimeMessage,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const loadNodes = useCallback(async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) setNodesLoading(true);
|
||||
@@ -73,22 +133,101 @@ export default function MonitorPage() {
|
||||
.map((n) => ({
|
||||
id: Number(n.id),
|
||||
name: String(n.name ?? ""),
|
||||
connectionStatus: n.status === 1 ? "online" : "offline",
|
||||
connectionStatus: realtimeNodeStatus[Number(n.id)] || (n.status === 1 ? "online" : "offline"),
|
||||
version: n.version,
|
||||
}));
|
||||
|
||||
return new Map<number, MonitorNode>(list.map((n) => [n.id, n]));
|
||||
}, [nodes]);
|
||||
}, [nodes, realtimeNodeStatus]);
|
||||
|
||||
const aggregateMetrics = useMemo(() => {
|
||||
let totalCpu = 0;
|
||||
let totalConns = 0;
|
||||
let totalNetIn = 0;
|
||||
let totalNetOut = 0;
|
||||
let onlineCount = 0;
|
||||
|
||||
nodeMap.forEach((node) => {
|
||||
if (node.connectionStatus === "online") {
|
||||
onlineCount++;
|
||||
const metric = realtimeNodeMetrics[node.id];
|
||||
if (metric) {
|
||||
totalCpu += metric.cpuUsage;
|
||||
totalConns += metric.tcpConns + metric.udpConns;
|
||||
totalNetIn += metric.netInSpeed;
|
||||
totalNetOut += metric.netOutSpeed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const avgCpu = onlineCount > 0 ? totalCpu / onlineCount : 0;
|
||||
return {
|
||||
avgCpu,
|
||||
totalConns,
|
||||
totalBandwidth: totalNetIn + totalNetOut,
|
||||
};
|
||||
}, [nodeMap, realtimeNodeMetrics]);
|
||||
|
||||
return (
|
||||
<AnimatedPage className="px-3 lg:px-6 py-8">
|
||||
{/* 顶部英雄数据指标 (Hero Metrics) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||
<div className="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">System Load</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.avgCpu.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-primary/40 h-2 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-3 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-1.5 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-4 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-2.5 rounded-t-sm" />
|
||||
<div className="w-full bg-primary h-5 rounded-t-sm shadow-[0_0_10px_rgba(0,122,255,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||
<div className="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">Active Connections</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.totalConns}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-success/40 h-3.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-4.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-2.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-6 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-5.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success h-7 rounded-t-sm shadow-[0_0_10px_rgba(52,199,89,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||
<div className="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">Bandwidth</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{formatBytesPerSecond(aggregateMetrics.totalBandwidth)}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-secondary/40 h-2 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-3 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-5 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-8 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-7 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary h-10 rounded-t-sm shadow-[0_0_10px_rgba(175,82,222,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-xl font-semibold truncate">监控</h2>
|
||||
<div className="text-xs text-default-500 truncate">
|
||||
实时节点状态 + 隧道质量检测 + 历史指标图表 + 服务监控(TCP/ICMP)
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold truncate">监控面板</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -114,11 +253,11 @@ export default function MonitorPage() {
|
||||
</div>
|
||||
|
||||
{/* Tab Switcher */}
|
||||
<div className="flex items-center gap-1 p-1 rounded-xl bg-default-100 dark:bg-default-50/10 w-fit">
|
||||
<div className="flex items-center gap-1 p-1 rounded-2xl bg-white/40 dark:bg-black/40 backdrop-blur-lg border border-white/50 dark:border-white/10 w-fit shadow-sm">
|
||||
<button
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||
activeTab === "nodes"
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
|
||||
: "text-default-500 hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setActiveTab("nodes")}
|
||||
@@ -127,9 +266,9 @@ export default function MonitorPage() {
|
||||
节点
|
||||
</button>
|
||||
<button
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||
activeTab === "tunnels"
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
|
||||
: "text-default-500 hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setActiveTab("tunnels")}
|
||||
|
||||
@@ -1767,9 +1767,10 @@ export default function NodePage() {
|
||||
aria-label="节点列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1949,7 +1950,7 @@ export default function NodePage() {
|
||||
{(listeners) => (
|
||||
<Card
|
||||
key={node.id}
|
||||
className={`group relative overflow-visible shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 h-full flex flex-col ${node.expiryReminderDismissed ? "" : expiryMeta.accentClassName}`}
|
||||
className={`group relative overflow-visible h-full flex flex-col ${node.expiryReminderDismissed ? "" : expiryMeta.accentClassName}`}
|
||||
data-node-card="true"
|
||||
>
|
||||
<CardHeader className="pb-3 md:pb-3">
|
||||
@@ -1979,13 +1980,20 @@ export default function NodePage() {
|
||||
)}
|
||||
<div className="min-w-0 flex-1 pt-0.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate text-sm leading-5">
|
||||
<h3 className="font-semibold text-foreground truncate text-lg leading-5">
|
||||
{node.name}
|
||||
</h3>
|
||||
<span
|
||||
className={`h-2.5 w-2.5 rounded-full flex-shrink-0 ${connectionStatusMeta.color === "success" ? "bg-emerald-500" : "bg-rose-500"}`}
|
||||
<div
|
||||
className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium flex-shrink-0
|
||||
${connectionStatusMeta.color === "success" ? "bg-emerald-500/20 text-emerald-600" : "bg-rose-500/20 text-rose-600"}
|
||||
`}
|
||||
title={connectionStatusMeta.text}
|
||||
/>
|
||||
>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${connectionStatusMeta.color === "success" ? "bg-emerald-500" : "bg-rose-500"}`}
|
||||
/>
|
||||
{connectionStatusMeta.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -174,7 +174,7 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="group relative overflow-hidden shadow-sm border border-divider dark:border-default-100 hover:-translate-y-1 hover:shadow-lg transition-all duration-300 h-full flex flex-col cursor-pointer bg-background"
|
||||
className="group relative hover:-translate-y-1 hover:shadow-[0_20px_40px_rgba(0,0,0,0.15)] transition-all duration-300 h-full flex flex-col cursor-pointer"
|
||||
onClick={onPress}
|
||||
>
|
||||
{/* Dynamic top gradient bar based on status */}
|
||||
@@ -208,8 +208,8 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
|
||||
<span className="font-mono font-medium">{isOnline && metric ? `${metric.cpuUsage.toFixed(1)}%` : "-"}</span>
|
||||
</div>
|
||||
<Progress
|
||||
className="h-1"
|
||||
color={getColorByUsage(metric?.cpuUsage)}
|
||||
size="sm"
|
||||
value={isOnline && metric ? metric.cpuUsage : 0}
|
||||
/>
|
||||
</div>
|
||||
@@ -219,6 +219,7 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
|
||||
<span className="font-mono font-medium">{isOnline && metric ? `${metric.memoryUsage.toFixed(1)}%` : "-"}</span>
|
||||
</div>
|
||||
<Progress
|
||||
className="h-1"
|
||||
color={getColorByUsage(metric?.memoryUsage)}
|
||||
size="sm"
|
||||
value={isOnline && metric ? metric.memoryUsage : 0}
|
||||
@@ -1157,9 +1158,10 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
aria-label="节点列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1321,7 +1323,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
{ label: "↑ 上行速度", value: formatBytesPerSecond(detailRealtimeMetric.netOutSpeed), color: "primary" as const },
|
||||
{ label: "运行时间", value: formatUptime(detailRealtimeMetric.uptime), color: "default" as const },
|
||||
].map((item) => (
|
||||
<Card key={item.label} className="border border-divider/60 shadow-sm hover:shadow-md transition-shadow bg-gradient-to-br from-background to-default-50/50">
|
||||
<Card key={item.label} className="hover:shadow-[0_15px_40px_rgba(0,0,0,0.15)] transition-shadow">
|
||||
<CardBody className="py-3 px-4 flex flex-col items-center justify-center min-h-[5rem]">
|
||||
<span className="text-[11px] text-default-500 mb-1.5">{item.label}</span>
|
||||
<span className={`text-sm font-semibold font-mono ${item.color === 'danger' ? 'text-danger' : item.color === 'warning' ? 'text-warning' : item.color === 'success' ? 'text-success' : item.color === 'primary' ? 'text-primary' : ''}`}>
|
||||
@@ -1444,7 +1446,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
|
||||
{/* Active monitor info bar */}
|
||||
{resolvedActiveMonitor && (
|
||||
<div className="flex items-center justify-between gap-3 p-3 rounded-lg bg-default-50 dark:bg-default-100/30 border border-divider">
|
||||
<div className="flex items-center justify-between gap-3 p-3 rounded-2xl bg-white/40 dark:bg-black/40 backdrop-blur-md border border-white/50 dark:border-white/10 shadow-[0_4px_12px_rgba(0,0,0,0.05)]">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-wrap">
|
||||
<Chip size="sm" color="primary" variant="flat">{resolvedActiveMonitor.type.toUpperCase()}</Chip>
|
||||
<span className="font-mono text-xs text-default-500">{resolvedActiveMonitor.target}</span>
|
||||
@@ -1584,9 +1586,10 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
aria-label="监控记录"
|
||||
className="w-full overflow-x-auto"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
@@ -987,9 +987,10 @@ export function TunnelMonitorView({ viewMode = "grid" }: TunnelMonitorViewProps)
|
||||
aria-label="隧道列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
@@ -459,7 +459,7 @@ export default function PanelSharingPage() {
|
||||
{shares.map((share) => (
|
||||
<Card
|
||||
key={share.id}
|
||||
className="border border-divider shadow-sm"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<CardHeader className="flex justify-between pb-2 md:pb-2">
|
||||
<h3 className="font-bold">{share.name}</h3>
|
||||
@@ -606,7 +606,7 @@ export default function PanelSharingPage() {
|
||||
{remoteUsageNodes.map((node) => (
|
||||
<Card
|
||||
key={node.nodeId}
|
||||
className="border border-divider shadow-sm"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<CardHeader className="flex justify-between pb-2 md:pb-2">
|
||||
<h3 className="font-bold">{node.nodeName}</h3>
|
||||
|
||||
@@ -202,7 +202,7 @@ export default function ProfilePage() {
|
||||
<div className="px-3 lg:px-6 py-8 flex flex-col h-full">
|
||||
<div className="space-y-6 flex-1">
|
||||
{/* 用户信息卡片 */}
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<Card>
|
||||
<CardBody className="p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
|
||||
@@ -242,7 +242,7 @@ export default function ProfilePage() {
|
||||
</Card>
|
||||
|
||||
{/* 功能网格 */}
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<Card>
|
||||
<CardBody className="p-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* 管理员功能 */}
|
||||
|
||||
@@ -183,7 +183,7 @@ export const SettingsPage = () => {
|
||||
{/* 内容区域 */}
|
||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||
<div className="space-y-6">
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
更新设置
|
||||
@@ -213,7 +213,7 @@ export const SettingsPage = () => {
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
显示设置
|
||||
@@ -247,7 +247,7 @@ export const SettingsPage = () => {
|
||||
</Card>
|
||||
|
||||
{/* 添加新地址 */}
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
添加新面板地址
|
||||
@@ -275,7 +275,7 @@ export const SettingsPage = () => {
|
||||
</Card>
|
||||
|
||||
{/* 地址列表 */}
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
已保存的面板地址
|
||||
|
||||
@@ -1583,9 +1583,10 @@ export default function TunnelPage() {
|
||||
aria-label="隧道列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1720,7 +1721,7 @@ export default function TunnelPage() {
|
||||
{(listeners) => (
|
||||
<Card
|
||||
key={tunnel.id}
|
||||
className="group shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden"
|
||||
className="group overflow-hidden"
|
||||
>
|
||||
<CardHeader className="pb-2 md:pb-2">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
@@ -1985,7 +1986,7 @@ export default function TunnelPage() {
|
||||
)
|
||||
) : (
|
||||
/* 空状态 */
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
||||
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||
暂无隧道配置
|
||||
@@ -3152,7 +3153,7 @@ export default function TunnelPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody className="bg-content1">
|
||||
<ModalBody className="bg-transparent">
|
||||
{diagnosisResult ? (
|
||||
<div className="space-y-4">
|
||||
{diagnosisLoading && (
|
||||
@@ -3674,7 +3675,7 @@ export default function TunnelPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter className="bg-content1 border-t border-divider">
|
||||
<ModalFooter className="bg-transparent border-t border-white/20 dark:border-white/10">
|
||||
<Button variant="light" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
|
||||
@@ -1084,9 +1084,10 @@ export default function UserPage() {
|
||||
aria-label="用户列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1108,14 +1109,11 @@ export default function UserPage() {
|
||||
return (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`shrink-0 w-2 h-2 rounded-full ${
|
||||
userStatus.color === "success"
|
||||
? "bg-success"
|
||||
: "bg-danger"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 w-10 h-10 rounded-full bg-primary text-white font-bold text-sm relative">
|
||||
{(user.name || user.user).slice(0, 2).toUpperCase()}
|
||||
<span className={`absolute bottom-0 right-0 w-3 h-3 border-2 border-white dark:border-zinc-900 rounded-full ${userStatus.color === "success" ? "bg-success" : "bg-danger"}`} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-foreground text-sm">
|
||||
{user.name || user.user}
|
||||
@@ -1252,16 +1250,22 @@ export default function UserPage() {
|
||||
|
||||
return (
|
||||
<StaggerItem key={user.id}>
|
||||
<Card className="shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden h-full">
|
||||
<Card className="overflow-hidden h-full">
|
||||
<CardHeader className="pb-2 md:pb-2">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate text-sm">
|
||||
{user.name || user.user}
|
||||
</h3>
|
||||
<p className="text-xs text-default-500 truncate">
|
||||
@{user.user}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex items-center justify-center shrink-0 w-10 h-10 rounded-full bg-primary text-white font-bold text-sm relative">
|
||||
{(user.name || user.user).slice(0, 2).toUpperCase()}
|
||||
<span className={`absolute bottom-0 right-0 w-3 h-3 border-2 border-white dark:border-zinc-900 rounded-full ${userStatus.color === "success" ? "bg-success" : "bg-danger"}`} />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate text-sm">
|
||||
{user.name || user.user}
|
||||
</h3>
|
||||
<p className="text-xs text-default-500 truncate">
|
||||
@{user.user}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
<Chip
|
||||
@@ -1896,9 +1900,9 @@ export default function UserPage() {
|
||||
<Table
|
||||
aria-label="用户隧道权限列表"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||
tr: "hover:bg-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
Reference in New Issue
Block a user