mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-06 10:06:36 +08:00
feat(ui): add list view for node and user pages (#350)
add list view feature
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
# 052 Node and User List View
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Add a list view and a card view toggle for Node Page and User Page, so they match exactly how the Monitor Page does it.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [x] Add `viewMode` state toggle with `LayoutGrid` and `List` icons to `pages/node.tsx` header area.
|
||||||
|
- [x] Implement Table view rendering for Node list in `pages/node.tsx`.
|
||||||
|
- [x] Add `viewMode` state toggle with `LayoutGrid` and `List` icons to `pages/user.tsx` header area.
|
||||||
|
- [x] Implement Table view rendering for User list in `pages/user.tsx`.
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
<meta name="apple-mobile-web-app-title" content="FLVX" />
|
<meta name="apple-mobile-web-app-title" content="FLVX" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<title>FLVX</title>
|
<title>FLVX</title>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1110,7 +1110,7 @@ export default function DashboardPage() {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
className="flex justify-between items-center p-3 border border-default-200 dark:border-default-100 rounded-lg"
|
className="flex justify-between items-center p-3 border border-default-200 dark:border-default-100 rounded-lg"
|
||||||
>
|
>
|
||||||
<code className="text-sm flex-1 mr-3 text-foreground">
|
<code className="font-mono text-sm flex-1 mr-3 text-foreground">
|
||||||
{item.address}
|
{item.address}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ export const AnnouncementBanner = ({
|
|||||||
</ol>
|
</ol>
|
||||||
),
|
),
|
||||||
code: ({ children }) => (
|
code: ({ children }) => (
|
||||||
<code className="rounded bg-blue-100/80 dark:bg-blue-900/40 px-1 py-0.5 text-[0.92em]">
|
<code className="font-mono rounded bg-blue-100/80 dark:bg-blue-900/40 px-1 py-0.5 text-[0.92em]">
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
),
|
),
|
||||||
pre: ({ children }) => (
|
pre: ({ children }) => (
|
||||||
<pre className="mb-2 overflow-x-auto rounded-md bg-blue-100/70 dark:bg-blue-900/40 p-2.5 text-xs leading-relaxed">
|
<pre className="mb-2 font-mono overflow-x-auto rounded-md bg-blue-100/70 dark:bg-blue-900/40 p-2.5 text-xs leading-relaxed">
|
||||||
{children}
|
{children}
|
||||||
</pre>
|
</pre>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4977,7 +4977,7 @@ export default function ForwardPage() {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
className="flex justify-between items-center p-3 border border-default-200 dark:border-default-100 rounded-lg"
|
className="flex justify-between items-center p-3 border border-default-200 dark:border-default-100 rounded-lg"
|
||||||
>
|
>
|
||||||
<code className="text-sm flex-1 mr-3 text-foreground">
|
<code className="font-mono text-sm flex-1 mr-3 text-foreground">
|
||||||
{item.address}
|
{item.address}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ import { CSS } from "@dnd-kit/utilities";
|
|||||||
|
|
||||||
import { SearchBar } from "@/components/search-bar";
|
import { SearchBar } from "@/components/search-bar";
|
||||||
import { AnimatedPage } from "@/components/animated-page";
|
import { AnimatedPage } from "@/components/animated-page";
|
||||||
|
import { LayoutGrid, List, Terminal, ArrowUpCircle, RotateCcw, Edit, Trash2 } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableColumn,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
} from "@/shadcn-bridge/heroui/table";
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
@@ -292,6 +301,10 @@ export default function NodePage() {
|
|||||||
"node-active-tab",
|
"node-active-tab",
|
||||||
"local",
|
"local",
|
||||||
);
|
);
|
||||||
|
const [viewMode, setViewMode] = useLocalStorageState<"list" | "grid">(
|
||||||
|
"node-view-mode",
|
||||||
|
"grid",
|
||||||
|
);
|
||||||
|
|
||||||
// Backward-compat: older versions stored extra tab values.
|
// Backward-compat: older versions stored extra tab values.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1639,6 +1652,21 @@ export default function NodePage() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* 视图切换按钮 */}
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
className="text-default-600 hidden sm:flex"
|
||||||
|
onPress={() => setViewMode(viewMode === "list" ? "grid" : "list")}
|
||||||
|
>
|
||||||
|
{viewMode === "list" ? (
|
||||||
|
<LayoutGrid className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<List className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* 筛选按钮 */}
|
{/* 筛选按钮 */}
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
@@ -1738,6 +1766,198 @@ export default function NodePage() {
|
|||||||
: "暂无本地节点,点击上方按钮开始创建"
|
: "暂无本地节点,点击上方按钮开始创建"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : viewMode === "list" ? (
|
||||||
|
<Card>
|
||||||
|
<Table aria-label="节点列表" className="overflow-x-auto min-w-full">
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn>节点名称</TableColumn>
|
||||||
|
<TableColumn>状态</TableColumn>
|
||||||
|
<TableColumn>协议/端口</TableColumn>
|
||||||
|
<TableColumn>系统信息</TableColumn>
|
||||||
|
<TableColumn>到期时间</TableColumn>
|
||||||
|
<TableColumn>操作</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody items={displayNodes}>
|
||||||
|
{(node) => {
|
||||||
|
const isRemoteNode = node.isRemote === 1;
|
||||||
|
const remoteUsage = isRemoteNode
|
||||||
|
? remoteUsageMap[node.id]
|
||||||
|
: null;
|
||||||
|
const expiryMeta = getNodeExpiryMeta(
|
||||||
|
node.expiryTime,
|
||||||
|
node.renewalCycle,
|
||||||
|
);
|
||||||
|
const connectionStatusMeta = getConnectionStatusMeta(
|
||||||
|
node.connectionStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRemark = Boolean(node.remark?.trim());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={node.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-foreground text-sm">
|
||||||
|
{node.name}
|
||||||
|
</span>
|
||||||
|
{hasRemark && (
|
||||||
|
<Chip size="sm" variant="flat" className="h-4 px-1 text-[10px]">
|
||||||
|
{node.remark}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-default-500 font-mono mt-0.5">
|
||||||
|
{isRemoteNode ? new URL(node.remoteUrl || "").hostname : node.serverIp}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
variant="flat"
|
||||||
|
size="sm"
|
||||||
|
color={connectionStatusMeta.color}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${node.connectionStatus === "online" ? "bg-success" : node.syncError ? "bg-danger" : "bg-default-400"} ${node.systemInfo && node.connectionStatus === "online" ? "animate-pulse" : ""}`} />
|
||||||
|
<span>{connectionStatusMeta.text}</span>
|
||||||
|
</div>
|
||||||
|
</Chip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{isRemoteNode ? "Remote" : "Local"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-default-500 font-mono">
|
||||||
|
{isRemoteNode
|
||||||
|
? (remoteUsage ? `${remoteUsage.portRangeStart}-${remoteUsage.portRangeEnd}` : "未知端口")
|
||||||
|
: node.port}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-0.5 whitespace-nowrap text-[11px] min-w-[100px]">
|
||||||
|
{!isRemoteNode && node.systemInfo ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-default-500">CPU</span>
|
||||||
|
<span className={node.systemInfo.cpuUsage > 80 ? "text-danger font-medium" : ""}>
|
||||||
|
{node.systemInfo.cpuUsage.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-default-500">RAM</span>
|
||||||
|
<span className={node.systemInfo.memoryUsage > 80 ? "text-danger font-medium" : ""}>
|
||||||
|
{node.systemInfo.memoryUsage.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-default-400">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-1 items-start whitespace-nowrap min-w-[100px]">
|
||||||
|
{node.expiryTime ? (
|
||||||
|
<>
|
||||||
|
<span className="text-sm">
|
||||||
|
{new Date(node.expiryTime).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
{expiryMeta && expiryMeta.state !== "healthy" && expiryMeta.state !== "permanent" && (
|
||||||
|
<Chip
|
||||||
|
color={expiryMeta.tone}
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
className="h-5 px-1 text-[10px]"
|
||||||
|
>
|
||||||
|
{expiryMeta.label}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-default-400 text-sm">无限期</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
{!isRemoteNode && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
color="success"
|
||||||
|
className="min-w-8 min-h-8 px-1"
|
||||||
|
title="安装命令"
|
||||||
|
isLoading={node.copyLoading}
|
||||||
|
onPress={() => openInstallSelector(node)}
|
||||||
|
>
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isRemoteNode && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
color="warning"
|
||||||
|
className="min-w-8 min-h-8 px-1"
|
||||||
|
title="升级"
|
||||||
|
isDisabled={node.connectionStatus !== "online"}
|
||||||
|
isLoading={node.upgradeLoading}
|
||||||
|
onPress={() => {
|
||||||
|
setUpgradeTargetNodeId(node.id);
|
||||||
|
openUpgradeModal("single");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowUpCircle className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isRemoteNode && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
color="secondary"
|
||||||
|
className="min-w-8 min-h-8 px-1"
|
||||||
|
title="回退"
|
||||||
|
isDisabled={node.connectionStatus !== "online"}
|
||||||
|
isLoading={node.rollbackLoading}
|
||||||
|
onPress={() => handleRollbackNode(node)}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isRemoteNode && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
color="primary"
|
||||||
|
className="min-w-8 min-h-8 px-1"
|
||||||
|
title="编辑"
|
||||||
|
onPress={() => handleEdit(node)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
color="danger"
|
||||||
|
className="min-w-8 min-h-8 px-1"
|
||||||
|
title="删除"
|
||||||
|
onPress={() => handleDelete(node)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { parseDate } from "@internationalized/date";
|
import { parseDate } from "@internationalized/date";
|
||||||
|
import { LayoutGrid, List } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnimatedPage,
|
AnimatedPage,
|
||||||
@@ -190,6 +191,10 @@ export default function UserPage() {
|
|||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
||||||
|
const [viewMode, setViewMode] = useLocalStorageState<"list" | "grid">(
|
||||||
|
"user-view-mode",
|
||||||
|
"grid",
|
||||||
|
);
|
||||||
const [pagination, setPagination] = useState<PaginationType>({
|
const [pagination, setPagination] = useState<PaginationType>({
|
||||||
current: 1,
|
current: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
@@ -1040,9 +1045,23 @@ export default function UserPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button color="primary" size="sm" variant="flat" onPress={handleAdd}>
|
<div className="flex items-center gap-2">
|
||||||
新增
|
<Button
|
||||||
</Button>
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => setViewMode(viewMode === "list" ? "grid" : "list")}
|
||||||
|
>
|
||||||
|
{viewMode === "list" ? (
|
||||||
|
<LayoutGrid className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<List className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" size="sm" variant="flat" onPress={handleAdd}>
|
||||||
|
新增
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 用户列表 */}
|
{/* 用户列表 */}
|
||||||
@@ -1059,6 +1078,179 @@ export default function UserPage() {
|
|||||||
</p>
|
</p>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
) : viewMode === "list" ? (
|
||||||
|
<Card>
|
||||||
|
<Table aria-label="用户列表" className="overflow-x-auto min-w-full">
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn>用户名</TableColumn>
|
||||||
|
<TableColumn>状态</TableColumn>
|
||||||
|
<TableColumn>流量统计</TableColumn>
|
||||||
|
<TableColumn>配额限制</TableColumn>
|
||||||
|
<TableColumn>规则数量</TableColumn>
|
||||||
|
<TableColumn>到期时间</TableColumn>
|
||||||
|
<TableColumn>操作</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody items={users}>
|
||||||
|
{(user) => {
|
||||||
|
const userStatus = getUserStatus(user);
|
||||||
|
const expStatus = user.expTime
|
||||||
|
? getExpireStatus(user.expTime)
|
||||||
|
: null;
|
||||||
|
const usedFlow = calculateUserTotalUsedFlow(user);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-foreground text-sm">
|
||||||
|
{user.name || user.user}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-default-500">
|
||||||
|
@{user.user}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-1 items-start">
|
||||||
|
<Chip
|
||||||
|
className="text-xs"
|
||||||
|
color={userStatus.color}
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{userStatus.text}
|
||||||
|
</Chip>
|
||||||
|
{user.disabledByQuota ? (
|
||||||
|
<Chip
|
||||||
|
className="text-[10px] h-5 px-1"
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
配额超额
|
||||||
|
</Chip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
<span className="text-default-500">已用:</span>
|
||||||
|
<span className="text-danger font-medium whitespace-nowrap">
|
||||||
|
{formatFlow(usedFlow)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
<span className="text-default-500">限制:</span>
|
||||||
|
<span className="text-default-700 font-medium whitespace-nowrap">
|
||||||
|
{formatFlow(user.flow, "gb")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-0.5 whitespace-nowrap text-[11px]">
|
||||||
|
{((user.dailyQuotaGB ?? 0) > 0 || (user.monthlyQuotaGB ?? 0) > 0) ? (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-1 justify-between">
|
||||||
|
<span className="text-default-500">日配额:</span>
|
||||||
|
<span>{formatFlow(user.dailyUsedBytes ?? 0)} / {formatQuotaLimit(user.dailyQuotaGB)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 justify-between">
|
||||||
|
<span className="text-default-500">月配额:</span>
|
||||||
|
<span>{formatFlow(user.monthlyUsedBytes ?? 0)} / {formatQuotaLimit(user.monthlyQuotaGB)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-default-400">无配额</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm font-medium">{user.num}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-1 items-start whitespace-nowrap">
|
||||||
|
{user.expTime ? (
|
||||||
|
<>
|
||||||
|
<span className="text-sm">
|
||||||
|
{new Date(user.expTime).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
{expStatus && (
|
||||||
|
<Chip
|
||||||
|
color={expStatus.color}
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
className="h-5 px-1 text-[10px]"
|
||||||
|
>
|
||||||
|
{expStatus.text}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-default-400 text-sm">无限期</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
color="success"
|
||||||
|
className="min-h-8 min-w-[50px] px-2"
|
||||||
|
startContent={<SettingsIcon className="w-3 h-3" />}
|
||||||
|
onPress={() => handleManageTunnels(user)}
|
||||||
|
>
|
||||||
|
权限
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
className="text-default-600 hover:text-primary min-h-8 min-w-8"
|
||||||
|
onPress={() => handleEdit(user)}
|
||||||
|
>
|
||||||
|
<EditIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
className="text-default-600 hover:text-warning min-h-8 min-w-8"
|
||||||
|
title="重置流量"
|
||||||
|
onPress={() => handleResetFlow(user)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
className="text-default-600 hover:text-danger min-h-8 min-w-8"
|
||||||
|
onPress={() => handleDelete(user)}
|
||||||
|
>
|
||||||
|
<DeleteIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<StaggerList className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
<StaggerList className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{users.map((user) => {
|
{users.map((user) => {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
|
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
|
|||||||
Reference in New Issue
Block a user