mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-11 19:56:37 +08:00
增加转发分类显示
This commit is contained in:
@@ -1 +1 @@
|
|||||||
VITE_API_BASE=https://1f.ink
|
VITE_API_BASE=http://127.0.0.1:6365
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroui/accordion": "^2.2.20",
|
||||||
"@heroui/alert": "^2.2.22",
|
"@heroui/alert": "^2.2.22",
|
||||||
"@heroui/avatar": "^2.2.18",
|
"@heroui/avatar": "^2.2.18",
|
||||||
"@heroui/button": "2.2.22",
|
"@heroui/button": "2.2.22",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type SiteConfig = typeof siteConfig;
|
|||||||
|
|
||||||
// 缓存相关常量
|
// 缓存相关常量
|
||||||
const CACHE_PREFIX = 'vite_config_';
|
const CACHE_PREFIX = 'vite_config_';
|
||||||
|
const VERSION = "1.1.0";
|
||||||
|
|
||||||
// 缓存策略:
|
// 缓存策略:
|
||||||
// - 长期有效,不设置过期时间
|
// - 长期有效,不设置过期时间
|
||||||
@@ -14,7 +15,7 @@ const getInitialConfig = () => {
|
|||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return {
|
return {
|
||||||
name: "哆啦A梦",
|
name: "哆啦A梦",
|
||||||
version: "1.0.9",
|
version: VERSION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,12 +23,12 @@ const getInitialConfig = () => {
|
|||||||
if (cachedAppName) {
|
if (cachedAppName) {
|
||||||
return {
|
return {
|
||||||
name: cachedAppName,
|
name: cachedAppName,
|
||||||
version: "1.0.9",
|
version: VERSION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: "哆啦A梦",
|
name: "哆啦A梦",
|
||||||
version: "1.0.9",
|
version: VERSION,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Chip } from "@heroui/chip";
|
|||||||
import { Spinner } from "@heroui/spinner";
|
import { Spinner } from "@heroui/spinner";
|
||||||
import { Switch } from "@heroui/switch";
|
import { Switch } from "@heroui/switch";
|
||||||
import { Alert } from "@heroui/alert";
|
import { Alert } from "@heroui/alert";
|
||||||
|
import { Accordion, AccordionItem } from "@heroui/accordion";
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import AdminLayout from "@/layouts/admin";
|
import AdminLayout from "@/layouts/admin";
|
||||||
@@ -81,6 +82,19 @@ interface DiagnosisResult {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加分组接口
|
||||||
|
interface UserGroup {
|
||||||
|
userId: number | null;
|
||||||
|
userName: string;
|
||||||
|
tunnelGroups: TunnelGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TunnelGroup {
|
||||||
|
tunnelId: number;
|
||||||
|
tunnelName: string;
|
||||||
|
forwards: Forward[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function ForwardPage() {
|
export default function ForwardPage() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [forwards, setForwards] = useState<Forward[]>([]);
|
const [forwards, setForwards] = useState<Forward[]>([]);
|
||||||
@@ -149,6 +163,47 @@ export default function ForwardPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 按用户和隧道分组转发数据
|
||||||
|
const groupForwardsByUserAndTunnel = (): UserGroup[] => {
|
||||||
|
const userMap = new Map<string, UserGroup>();
|
||||||
|
|
||||||
|
forwards.forEach(forward => {
|
||||||
|
const userKey = forward.userId ? forward.userId.toString() : 'unknown';
|
||||||
|
const userName = forward.userName || '未知用户';
|
||||||
|
|
||||||
|
if (!userMap.has(userKey)) {
|
||||||
|
userMap.set(userKey, {
|
||||||
|
userId: forward.userId || null,
|
||||||
|
userName,
|
||||||
|
tunnelGroups: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userGroup = userMap.get(userKey)!;
|
||||||
|
let tunnelGroup = userGroup.tunnelGroups.find(tg => tg.tunnelId === forward.tunnelId);
|
||||||
|
|
||||||
|
if (!tunnelGroup) {
|
||||||
|
tunnelGroup = {
|
||||||
|
tunnelId: forward.tunnelId,
|
||||||
|
tunnelName: forward.tunnelName,
|
||||||
|
forwards: []
|
||||||
|
};
|
||||||
|
userGroup.tunnelGroups.push(tunnelGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelGroup.forwards.push(forward);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 排序:先按用户名,再按隧道名
|
||||||
|
const result = Array.from(userMap.values());
|
||||||
|
result.sort((a, b) => a.userName.localeCompare(b.userName));
|
||||||
|
result.forEach(userGroup => {
|
||||||
|
userGroup.tunnelGroups.sort((a, b) => a.tunnelName.localeCompare(b.tunnelName));
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: {[key: string]: string} = {};
|
const newErrors: {[key: string]: string} = {};
|
||||||
@@ -598,44 +653,8 @@ export default function ForwardPage() {
|
|||||||
return addresses.length;
|
return addresses.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 渲染转发卡片
|
||||||
|
const renderForwardCard = (forward: Forward) => {
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<AdminLayout>
|
|
||||||
<div className="flex items-center justify-center h-64">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Spinner size="sm" />
|
|
||||||
<span className="text-default-600">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminLayout>
|
|
||||||
<div className="px-3 lg:px-6 py-8">
|
|
||||||
{/* 页面头部 */}
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h1 className="text-2xl font-bold text-foreground">转发管理</h1>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
onPress={handleAdd}
|
|
||||||
startContent={
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
新增转发
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 转发卡片网格 */}
|
|
||||||
{forwards.length > 0 ? (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
|
||||||
{forwards.map((forward) => {
|
|
||||||
const statusDisplay = getStatusDisplay(forward.status);
|
const statusDisplay = getStatusDisplay(forward.status);
|
||||||
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
||||||
|
|
||||||
@@ -692,8 +711,6 @@ export default function ForwardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer px-2 py-1 bg-default-50 dark:bg-default-100/50 rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
|
className={`cursor-pointer px-2 py-1 bg-default-50 dark:bg-default-100/50 rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
|
||||||
hasMultipleAddresses(forward.remoteAddr) ? 'hover:bg-default-100 dark:hover:bg-default-200/50' : ''
|
hasMultipleAddresses(forward.remoteAddr) ? 'hover:bg-default-100 dark:hover:bg-default-200/50' : ''
|
||||||
@@ -725,11 +742,6 @@ export default function ForwardPage() {
|
|||||||
<Chip variant="flat" size="sm" className="text-xs">
|
<Chip variant="flat" size="sm" className="text-xs">
|
||||||
{formatFlow((forward.inFlow || 0) + (forward.outFlow || 0))}
|
{formatFlow((forward.inFlow || 0) + (forward.outFlow || 0))}
|
||||||
</Chip>
|
</Chip>
|
||||||
{forward.userName && (
|
|
||||||
<Chip variant="flat" size="sm" className="text-xs">
|
|
||||||
{forward.userName}
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -781,7 +793,105 @@ export default function ForwardPage() {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
<span className="text-default-600">正在加载...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userGroups = groupForwardsByUserAndTunnel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<div className="px-3 lg:px-6 py-8">
|
||||||
|
{/* 页面头部 */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-foreground">转发管理</h1>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onPress={handleAdd}
|
||||||
|
startContent={
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
新增转发
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 按用户和隧道分组的转发列表 */}
|
||||||
|
{userGroups.length > 0 ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{userGroups.map((userGroup) => (
|
||||||
|
<Card key={userGroup.userId || 'unknown'} className="shadow-sm border border-divider w-full overflow-hidden">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between w-full min-w-0">
|
||||||
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
<div className="w-10 h-10 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg className="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h2 className="text-base font-medium text-foreground truncate max-w-[150px] sm:max-w-[250px] md:max-w-[350px] lg:max-w-[450px]">{userGroup.userName}</h2>
|
||||||
|
<p className="text-xs text-default-500 truncate max-w-[150px] sm:max-w-[250px] md:max-w-[350px] lg:max-w-[450px]">
|
||||||
|
{userGroup.tunnelGroups.length} 个隧道,
|
||||||
|
{userGroup.tunnelGroups.reduce((total, tg) => total + tg.forwards.length, 0)} 个转发
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Chip color="primary" variant="flat" size="sm" className="text-xs flex-shrink-0 ml-2">
|
||||||
|
用户
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardBody className="pt-0">
|
||||||
|
<Accordion variant="splitted" className="px-0">
|
||||||
|
{userGroup.tunnelGroups.map((tunnelGroup) => (
|
||||||
|
<AccordionItem
|
||||||
|
key={tunnelGroup.tunnelId}
|
||||||
|
aria-label={tunnelGroup.tunnelName}
|
||||||
|
title={
|
||||||
|
<div className="flex items-center justify-between w-full min-w-0 pr-4">
|
||||||
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
<div className="w-8 h-8 bg-success-100 dark:bg-success-900/30 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg className="w-4 h-4 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-foreground truncate max-w-[120px] sm:max-w-[200px] md:max-w-[300px] lg:max-w-[400px]">{tunnelGroup.tunnelName}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
|
||||||
|
<Chip variant="flat" size="sm" className="text-xs">
|
||||||
|
{tunnelGroup.forwards.filter(f => f.serviceRunning).length}/{tunnelGroup.forwards.length}
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className="shadow-none border border-divider"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 p-4">
|
||||||
|
{tunnelGroup.forwards.map((forward) => renderForwardCard(forward))}
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
@@ -903,8 +1013,6 @@ export default function ForwardPage() {
|
|||||||
<SelectItem key="hash" >哈希模式 - IP哈希</SelectItem>
|
<SelectItem key="hash" >哈希模式 - IP哈希</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
Reference in New Issue
Block a user