mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 09:06:38 +08:00
修复bug
This commit is contained in:
@@ -243,11 +243,11 @@ export default function AdminLayout({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-black">
|
||||
<div className={`flex ${isMobile ? 'min-h-screen' : 'h-screen'} bg-gray-50 dark:bg-black`}>
|
||||
{/* 移动端遮罩层 */}
|
||||
{isMobile && mobileMenuVisible && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||
className="fixed inset-0 backdrop-blur-sm bg-white/50 dark:bg-black/30 z-40"
|
||||
onClick={hideMobileMenu}
|
||||
/>
|
||||
)}
|
||||
@@ -263,7 +263,8 @@ export default function AdminLayout({
|
||||
z-50
|
||||
transition-transform duration-300 ease-in-out
|
||||
flex flex-col
|
||||
h-full
|
||||
${isMobile ? 'h-screen' : 'h-full'}
|
||||
${isMobile ? 'top-0 left-0' : ''}
|
||||
`}>
|
||||
{/* Logo 区域 */}
|
||||
<div className="px-3 py-3 h-14 flex items-center">
|
||||
@@ -277,7 +278,7 @@ export default function AdminLayout({
|
||||
</div>
|
||||
|
||||
{/* 菜单导航 */}
|
||||
<nav className="flex-1 px-4 py-6">
|
||||
<nav className="flex-1 px-4 py-6 overflow-y-auto">
|
||||
<ul className="space-y-1">
|
||||
{filteredMenuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
@@ -306,7 +307,7 @@ export default function AdminLayout({
|
||||
</nav>
|
||||
|
||||
{/* 底部版权信息 */}
|
||||
<div className="px-4 py-2 pb-4 mt-auto">
|
||||
<div className="px-4 py-2 pb-4 mt-auto flex-shrink-0">
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||
Powered by{' '}
|
||||
@@ -324,7 +325,7 @@ export default function AdminLayout({
|
||||
</aside>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<div className={`flex flex-col flex-1 ${isMobile ? 'min-h-0' : 'h-full overflow-hidden'}`}>
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="bg-white dark:bg-black shadow-md border-b border-gray-200 dark:border-gray-600 h-14 flex items-center justify-between px-4 lg:px-6 relative z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -387,7 +388,7 @@ export default function AdminLayout({
|
||||
</header>
|
||||
|
||||
{/* 主内容 */}
|
||||
<main className="flex-1 overflow-y-auto bg-gray-100 dark:bg-black">
|
||||
<main className={`flex-1 bg-gray-100 dark:bg-black ${isMobile ? '' : 'overflow-y-auto'}`}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -122,6 +122,20 @@ export default function ForwardPage() {
|
||||
const [forwards, setForwards] = useState<Forward[]>([]);
|
||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||
|
||||
// 检测是否为移动端
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
// 显示模式状态 - 从localStorage读取,默认为分类显示
|
||||
const [viewMode, setViewMode] = useState<'grouped' | 'direct'>(() => {
|
||||
try {
|
||||
@@ -1009,12 +1023,21 @@ export default function ForwardPage() {
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) return;
|
||||
if (!active || !over || active.id === over.id) return;
|
||||
|
||||
const oldIndex = forwardOrder.indexOf(Number(active.id));
|
||||
const newIndex = forwardOrder.indexOf(Number(over.id));
|
||||
// 确保 forwardOrder 存在且有效
|
||||
if (!forwardOrder || forwardOrder.length === 0) return;
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const activeId = Number(active.id);
|
||||
const overId = Number(over.id);
|
||||
|
||||
// 检查 ID 是否有效
|
||||
if (isNaN(activeId) || isNaN(overId)) return;
|
||||
|
||||
const oldIndex = forwardOrder.indexOf(activeId);
|
||||
const newIndex = forwardOrder.indexOf(overId);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
|
||||
const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);
|
||||
setForwardOrder(newOrder);
|
||||
|
||||
@@ -1052,7 +1075,7 @@ export default function ForwardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 传感器配置
|
||||
// 传感器配置 - 使用默认配置避免错误
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
@@ -1062,6 +1085,11 @@ export default function ForwardPage() {
|
||||
|
||||
// 根据排序顺序获取转发列表
|
||||
const getSortedForwards = (): Forward[] => {
|
||||
// 确保 forwards 数组存在且有效
|
||||
if (!forwards || forwards.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 在平铺模式下,只显示当前用户的转发
|
||||
let filteredForwards = forwards;
|
||||
if (viewMode === 'direct') {
|
||||
@@ -1071,6 +1099,11 @@ export default function ForwardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 确保过滤后的转发列表有效
|
||||
if (!filteredForwards || filteredForwards.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 优先使用数据库中的 inx 字段进行排序
|
||||
const sortedForwards = [...filteredForwards].sort((a, b) => {
|
||||
const aInx = a.inx ?? 0;
|
||||
@@ -1079,7 +1112,7 @@ export default function ForwardPage() {
|
||||
});
|
||||
|
||||
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
||||
if (forwardOrder.length > 0 && sortedForwards.every(f => f.inx === undefined || f.inx === 0)) {
|
||||
if (forwardOrder && forwardOrder.length > 0 && sortedForwards.every(f => f.inx === undefined || f.inx === 0)) {
|
||||
const forwardMap = new Map(filteredForwards.map(f => [f.id, f]));
|
||||
const localSortedForwards: Forward[] = [];
|
||||
|
||||
@@ -1105,6 +1138,11 @@ export default function ForwardPage() {
|
||||
|
||||
// 可拖拽的转发卡片组件
|
||||
const SortableForwardCard = ({ forward }: { forward: Forward }) => {
|
||||
// 确保 forward 对象有效
|
||||
if (!forward || !forward.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@@ -1115,20 +1153,20 @@ export default function ForwardPage() {
|
||||
} = useSortable({ id: forward.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
||||
transition: transition || undefined,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||
{renderForwardCard(forward)}
|
||||
<div ref={setNodeRef} style={style} {...attributes}>
|
||||
{renderForwardCard(forward, listeners)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染转发卡片
|
||||
const renderForwardCard = (forward: Forward) => {
|
||||
const renderForwardCard = (forward: Forward, listeners?: any) => {
|
||||
const statusDisplay = getStatusDisplay(forward.status);
|
||||
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
||||
|
||||
@@ -1142,7 +1180,16 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
{viewMode === 'direct' && (
|
||||
<div className="cursor-grab active:cursor-grabbing p-1 text-default-400 hover:text-default-600 transition-colors opacity-0 group-hover:opacity-100">
|
||||
<div
|
||||
className={`cursor-grab active:cursor-grabbing p-2 text-default-400 hover:text-default-600 transition-colors touch-manipulation ${
|
||||
isMobile
|
||||
? 'opacity-100' // 移动端始终显示
|
||||
: 'opacity-0 group-hover:opacity-100 sm:opacity-0 sm:group-hover:opacity-100'
|
||||
}`}
|
||||
{...listeners}
|
||||
title={isMobile ? "长按拖拽排序" : "拖拽排序"}
|
||||
style={{ touchAction: 'none' }}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" />
|
||||
</svg>
|
||||
@@ -1363,6 +1410,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 根据显示模式渲染不同内容 */}
|
||||
{viewMode === 'grouped' ? (
|
||||
/* 按用户和隧道分组的转发列表 */
|
||||
@@ -1420,7 +1468,7 @@ export default function ForwardPage() {
|
||||
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))}
|
||||
{tunnelGroup.forwards.map((forward) => renderForwardCard(forward, undefined))}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
))}
|
||||
@@ -1454,14 +1502,17 @@ export default function ForwardPage() {
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={() => {}} // 添加空的 onDragStart 处理器
|
||||
>
|
||||
<SortableContext
|
||||
items={getSortedForwards().map(f => f.id)}
|
||||
items={getSortedForwards().map(f => f.id || 0).filter(id => id > 0)}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
{getSortedForwards().map((forward) => (
|
||||
<SortableForwardCard key={forward.id} forward={forward} />
|
||||
forward && forward.id ? (
|
||||
<SortableForwardCard key={forward.id} forward={forward} />
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Textarea } from "@heroui/input";
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal";
|
||||
import { Chip } from "@heroui/chip";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Alert } from "@heroui/alert";
|
||||
import { Progress } from "@heroui/progress";
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -871,6 +872,28 @@ export default function NodePage() {
|
||||
max={65535}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<Alert
|
||||
color="primary"
|
||||
variant="flat"
|
||||
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址"
|
||||
className="mt-4"
|
||||
/>
|
||||
<Alert
|
||||
color="primary"
|
||||
variant="flat"
|
||||
description="入口ip是用于展示在转发页面,面向用户的访问地址"
|
||||
className="mt-4"
|
||||
/>
|
||||
<Alert
|
||||
color="primary"
|
||||
variant="flat"
|
||||
description="实在理解不到说明你没这个需求,都填节点的服务器ip就行"
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -770,12 +770,14 @@ export default function TunnelPage() {
|
||||
isInvalid={!!errors.protocol}
|
||||
errorMessage={errors.protocol}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
>
|
||||
<SelectItem key="tls">TLS</SelectItem>
|
||||
<SelectItem key="wss">WSS</SelectItem>
|
||||
<SelectItem key="tcp">TCP</SelectItem>
|
||||
<SelectItem key="mtls">mTLS</SelectItem>
|
||||
<SelectItem key="mwss">mWSS</SelectItem>
|
||||
<SelectItem key="mtcp">mTCP</SelectItem>
|
||||
<SelectItem key="quic">QUIC</SelectItem>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
|
||||
@@ -19,13 +19,12 @@ import {
|
||||
useDisclosure
|
||||
} from "@heroui/modal";
|
||||
import { Chip } from "@heroui/chip";
|
||||
import { Pagination } from "@heroui/pagination";
|
||||
import { Select, SelectItem } from "@heroui/select";
|
||||
import { RadioGroup, Radio } from "@heroui/radio";
|
||||
import { DatePicker } from "@heroui/date-picker";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Progress } from "@heroui/progress";
|
||||
import { Tabs, Tab } from "@heroui/tabs";
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
User,
|
||||
@@ -191,13 +190,7 @@ export default function UserPage() {
|
||||
|
||||
if (response.code === 0) {
|
||||
const data = response.data || {};
|
||||
setUsers(data.records || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: data.total || 0,
|
||||
current: data.current || 1,
|
||||
size: data.size || 10
|
||||
}));
|
||||
setUsers(data || []);
|
||||
} else {
|
||||
toast.error(response.msg || '获取用户列表失败');
|
||||
}
|
||||
@@ -748,18 +741,6 @@ export default function UserPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分页 */}
|
||||
{pagination.total > 0 && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Pagination
|
||||
total={Math.ceil(pagination.total / pagination.size)}
|
||||
page={pagination.current}
|
||||
onChange={(page) => setPagination(prev => ({ ...prev, current: page }))}
|
||||
showControls
|
||||
showShadow
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户表单模态框 */}
|
||||
<Modal
|
||||
@@ -877,16 +858,22 @@ export default function UserPage() {
|
||||
<Modal
|
||||
isOpen={isTunnelModalOpen}
|
||||
onClose={onTunnelModalClose}
|
||||
size="5xl"
|
||||
size="4xl"
|
||||
scrollBehavior="outside"
|
||||
isDismissable={false}
|
||||
classNames={{
|
||||
base: "max-w-[95vw] sm:max-w-4xl"
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
用户 {currentUser?.user} 的隧道权限管理
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Tabs defaultSelectedKey="assign">
|
||||
<Tab key="assign" title="分配新权限">
|
||||
<div className="space-y-6">
|
||||
{/* 分配新权限部分 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">分配新权限</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Select
|
||||
@@ -991,9 +978,11 @@ export default function UserPage() {
|
||||
分配权限
|
||||
</Button>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab key="list" title="已有权限">
|
||||
</div>
|
||||
|
||||
{/* 已有权限部分 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">已有权限</h3>
|
||||
<Table
|
||||
aria-label="用户隧道权限列表"
|
||||
classNames={{
|
||||
@@ -1083,8 +1072,8 @@ export default function UserPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onTunnelModalClose}>
|
||||
@@ -1100,6 +1089,7 @@ export default function UserPage() {
|
||||
onClose={onEditTunnelModalClose}
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
isDismissable={false}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
/* 移动端视口高度优化 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 解决移动端100vh问题 */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.h-screen {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端滚动优化 */
|
||||
@media (max-width: 768px) {
|
||||
* {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@config "../../tailwind.config.js"
|
||||
Reference in New Issue
Block a user