mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-02 08:56:38 +08:00
增加拖拽排序
This commit is contained in:
@@ -63,6 +63,9 @@ export const resumeForwardService = (forwardId: number) => Network.post("/forwar
|
||||
// 转发诊断操作
|
||||
export const diagnoseForward = (forwardId: number) => Network.post("/forward/diagnose", { forwardId });
|
||||
|
||||
// 转发排序操作
|
||||
export const updateForwardOrder = (data: { forwards: Array<{ id: number; inx: number }> }) => Network.post("/forward/update-order", data);
|
||||
|
||||
// 限速规则CRUD操作 - 全部使用POST请求
|
||||
export const createSpeedLimit = (data: any) => Network.post("/speed-limit/create", data);
|
||||
export const getSpeedLimitList = () => Network.post("/speed-limit/list");
|
||||
|
||||
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
|
||||
|
||||
// 缓存相关常量
|
||||
const CACHE_PREFIX = 'vite_config_';
|
||||
const VERSION = "1.2.2";
|
||||
const VERSION = "1.2.3";
|
||||
|
||||
// 缓存策略:
|
||||
// - 长期有效,不设置过期时间
|
||||
|
||||
@@ -11,6 +11,25 @@ import { Switch } from "@heroui/switch";
|
||||
import { Alert } from "@heroui/alert";
|
||||
import { Accordion, AccordionItem } from "@heroui/accordion";
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
rectSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import {
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
import AdminLayout from "@/layouts/admin";
|
||||
import {
|
||||
@@ -22,8 +41,10 @@ import {
|
||||
userTunnel,
|
||||
pauseForwardService,
|
||||
resumeForwardService,
|
||||
diagnoseForward
|
||||
diagnoseForward,
|
||||
updateForwardOrder
|
||||
} from "@/api";
|
||||
import { JwtUtil } from "@/utils/jwt";
|
||||
|
||||
interface Forward {
|
||||
id: number;
|
||||
@@ -41,6 +62,7 @@ interface Forward {
|
||||
createdTime: string;
|
||||
userName?: string;
|
||||
userId?: number;
|
||||
inx?: number;
|
||||
}
|
||||
|
||||
interface Tunnel {
|
||||
@@ -110,6 +132,9 @@ export default function ForwardPage() {
|
||||
}
|
||||
});
|
||||
|
||||
// 拖拽排序相关状态
|
||||
const [forwardOrder, setForwardOrder] = useState<number[]>([]);
|
||||
|
||||
// 模态框状态
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
@@ -166,6 +191,55 @@ export default function ForwardPage() {
|
||||
setViewMode(newMode);
|
||||
try {
|
||||
localStorage.setItem('forward-view-mode', newMode);
|
||||
|
||||
// 切换到直接显示模式时,初始化拖拽排序顺序
|
||||
if (newMode === 'direct') {
|
||||
// 在平铺模式下,只对当前用户的转发进行排序
|
||||
const currentUserId = JwtUtil.getUserIdFromToken();
|
||||
let userForwards = forwards;
|
||||
if (currentUserId !== null) {
|
||||
userForwards = forwards.filter((f: Forward) => f.userId === currentUserId);
|
||||
}
|
||||
|
||||
// 检查数据库中是否有排序信息
|
||||
const hasDbOrdering = userForwards.some((f: Forward) => f.inx !== undefined && f.inx !== 0);
|
||||
|
||||
if (hasDbOrdering) {
|
||||
// 使用数据库中的排序信息
|
||||
const dbOrder = userForwards
|
||||
.sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0))
|
||||
.map((f: Forward) => f.id);
|
||||
setForwardOrder(dbOrder);
|
||||
|
||||
// 同步到localStorage
|
||||
try {
|
||||
localStorage.setItem('forward-order', JSON.stringify(dbOrder));
|
||||
} catch (error) {
|
||||
console.warn('无法保存排序到localStorage:', error);
|
||||
}
|
||||
} else {
|
||||
// 使用本地存储的顺序
|
||||
const savedOrder = localStorage.getItem('forward-order');
|
||||
if (savedOrder) {
|
||||
try {
|
||||
const orderIds = JSON.parse(savedOrder);
|
||||
const validOrder = orderIds.filter((id: number) =>
|
||||
userForwards.some((f: Forward) => f.id === id)
|
||||
);
|
||||
userForwards.forEach((forward: Forward) => {
|
||||
if (!validOrder.includes(forward.id)) {
|
||||
validOrder.push(forward.id);
|
||||
}
|
||||
});
|
||||
setForwardOrder(validOrder);
|
||||
} catch {
|
||||
setForwardOrder(userForwards.map((f: Forward) => f.id));
|
||||
}
|
||||
} else {
|
||||
setForwardOrder(userForwards.map((f: Forward) => f.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法保存显示模式到localStorage:', error);
|
||||
}
|
||||
@@ -181,10 +255,62 @@ export default function ForwardPage() {
|
||||
]);
|
||||
|
||||
if (forwardsRes.code === 0) {
|
||||
setForwards(forwardsRes.data?.map((forward: any) => ({
|
||||
const forwardsData = forwardsRes.data?.map((forward: any) => ({
|
||||
...forward,
|
||||
serviceRunning: forward.status === 1
|
||||
})) || []);
|
||||
})) || [];
|
||||
setForwards(forwardsData);
|
||||
|
||||
// 初始化拖拽排序顺序
|
||||
if (viewMode === 'direct') {
|
||||
// 在平铺模式下,只对当前用户的转发进行排序
|
||||
const currentUserId = JwtUtil.getUserIdFromToken();
|
||||
let userForwards = forwardsData;
|
||||
if (currentUserId !== null) {
|
||||
userForwards = forwardsData.filter((f: Forward) => f.userId === currentUserId);
|
||||
}
|
||||
|
||||
// 检查数据库中是否有排序信息
|
||||
const hasDbOrdering = userForwards.some((f: Forward) => f.inx !== undefined && f.inx !== 0);
|
||||
|
||||
if (hasDbOrdering) {
|
||||
// 使用数据库中的排序信息
|
||||
const dbOrder = userForwards
|
||||
.sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0))
|
||||
.map((f: Forward) => f.id);
|
||||
setForwardOrder(dbOrder);
|
||||
|
||||
// 同步到localStorage
|
||||
try {
|
||||
localStorage.setItem('forward-order', JSON.stringify(dbOrder));
|
||||
} catch (error) {
|
||||
console.warn('无法保存排序到localStorage:', error);
|
||||
}
|
||||
} else {
|
||||
// 使用本地存储的顺序
|
||||
const savedOrder = localStorage.getItem('forward-order');
|
||||
if (savedOrder) {
|
||||
try {
|
||||
const orderIds = JSON.parse(savedOrder);
|
||||
// 验证保存的顺序是否仍然有效(只包含当前用户的转发)
|
||||
const validOrder = orderIds.filter((id: number) =>
|
||||
userForwards.some((f: Forward) => f.id === id)
|
||||
);
|
||||
// 添加新的转发ID(如果存在)
|
||||
userForwards.forEach((forward: Forward) => {
|
||||
if (!validOrder.includes(forward.id)) {
|
||||
validOrder.push(forward.id);
|
||||
}
|
||||
});
|
||||
setForwardOrder(validOrder);
|
||||
} catch {
|
||||
setForwardOrder(userForwards.map((f: Forward) => f.id));
|
||||
}
|
||||
} else {
|
||||
setForwardOrder(userForwards.map((f: Forward) => f.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.error(forwardsRes.msg || '获取转发列表失败');
|
||||
}
|
||||
@@ -206,7 +332,10 @@ export default function ForwardPage() {
|
||||
const groupForwardsByUserAndTunnel = (): UserGroup[] => {
|
||||
const userMap = new Map<string, UserGroup>();
|
||||
|
||||
forwards.forEach(forward => {
|
||||
// 获取排序后的转发列表
|
||||
const sortedForwards = getSortedForwards();
|
||||
|
||||
sortedForwards.forEach(forward => {
|
||||
const userKey = forward.userId ? forward.userId.toString() : 'unknown';
|
||||
const userName = forward.userName || '未知用户';
|
||||
|
||||
@@ -687,7 +816,7 @@ export default function ForwardPage() {
|
||||
);
|
||||
} else {
|
||||
// 直接显示模式下,过滤指定隧道的转发
|
||||
forwardsToExport = forwards.filter(forward => forward.tunnelId === selectedTunnelForExport);
|
||||
forwardsToExport = getSortedForwards().filter(forward => forward.tunnelId === selectedTunnelForExport);
|
||||
}
|
||||
|
||||
if (forwardsToExport.length === 0) {
|
||||
@@ -876,13 +1005,135 @@ export default function ForwardPage() {
|
||||
return addresses.length;
|
||||
};
|
||||
|
||||
// 处理拖拽结束
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const oldIndex = forwardOrder.indexOf(Number(active.id));
|
||||
const newIndex = forwardOrder.indexOf(Number(over.id));
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);
|
||||
setForwardOrder(newOrder);
|
||||
|
||||
// 保存到localStorage
|
||||
try {
|
||||
localStorage.setItem('forward-order', JSON.stringify(newOrder));
|
||||
} catch (error) {
|
||||
console.warn('无法保存排序到localStorage:', error);
|
||||
}
|
||||
|
||||
// 持久化到数据库
|
||||
try {
|
||||
const forwardsToUpdate = newOrder.map((id, index) => ({
|
||||
id,
|
||||
inx: index
|
||||
}));
|
||||
|
||||
const response = await updateForwardOrder({ forwards: forwardsToUpdate });
|
||||
if (response.code === 0) {
|
||||
// 更新本地数据中的 inx 字段
|
||||
setForwards(prev => prev.map(forward => {
|
||||
const updatedForward = forwardsToUpdate.find(f => f.id === forward.id);
|
||||
if (updatedForward) {
|
||||
return { ...forward, inx: updatedForward.inx };
|
||||
}
|
||||
return forward;
|
||||
}));
|
||||
} else {
|
||||
toast.error('保存排序失败:' + (response.msg || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存排序到数据库失败:', error);
|
||||
toast.error('保存排序失败,请重试');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 传感器配置
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
// 根据排序顺序获取转发列表
|
||||
const getSortedForwards = (): Forward[] => {
|
||||
// 在平铺模式下,只显示当前用户的转发
|
||||
let filteredForwards = forwards;
|
||||
if (viewMode === 'direct') {
|
||||
const currentUserId = JwtUtil.getUserIdFromToken();
|
||||
if (currentUserId !== null) {
|
||||
filteredForwards = forwards.filter(forward => forward.userId === currentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用数据库中的 inx 字段进行排序
|
||||
const sortedForwards = [...filteredForwards].sort((a, b) => {
|
||||
const aInx = a.inx ?? 0;
|
||||
const bInx = b.inx ?? 0;
|
||||
return aInx - bInx;
|
||||
});
|
||||
|
||||
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
||||
if (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[] = [];
|
||||
|
||||
forwardOrder.forEach(id => {
|
||||
const forward = forwardMap.get(id);
|
||||
if (forward) {
|
||||
localSortedForwards.push(forward);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加不在排序列表中的转发(新添加的)
|
||||
filteredForwards.forEach(forward => {
|
||||
if (!forwardOrder.includes(forward.id)) {
|
||||
localSortedForwards.push(forward);
|
||||
}
|
||||
});
|
||||
|
||||
return localSortedForwards;
|
||||
}
|
||||
|
||||
return sortedForwards;
|
||||
};
|
||||
|
||||
// 可拖拽的转发卡片组件
|
||||
const SortableForwardCard = ({ forward }: { forward: Forward }) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: forward.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||
{renderForwardCard(forward)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染转发卡片
|
||||
const renderForwardCard = (forward: Forward) => {
|
||||
const statusDisplay = getStatusDisplay(forward.status);
|
||||
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
||||
|
||||
return (
|
||||
<Card key={forward.id} className="shadow-sm border border-divider hover:shadow-md transition-shadow duration-200">
|
||||
<Card key={forward.id} className="group shadow-sm border border-divider hover:shadow-md transition-shadow duration-200">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -890,6 +1141,13 @@ export default function ForwardPage() {
|
||||
<p className="text-xs text-default-500 truncate">{forward.tunnelName}</p>
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={forward.serviceRunning}
|
||||
@@ -1044,7 +1302,9 @@ export default function ForwardPage() {
|
||||
<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>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-foreground">转发管理</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 显示模式切换按钮 */}
|
||||
<Button
|
||||
@@ -1190,9 +1450,22 @@ export default function ForwardPage() {
|
||||
) : (
|
||||
/* 直接显示模式 */
|
||||
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) => renderForwardCard(forward))}
|
||||
</div>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={getSortedForwards().map(f => f.id)}
|
||||
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} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
/* 空状态 */
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
|
||||
@@ -688,51 +688,58 @@ export default function UserPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5 mt-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
onPress={() => handleEdit(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={<EditIcon className="w-3 h-3" />}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="warning"
|
||||
onPress={() => handleResetFlow(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="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" clipRule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="success"
|
||||
onPress={() => handleManageTunnels(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={<SettingsIcon className="w-3 h-3" />}
|
||||
>
|
||||
权限
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
onPress={() => handleDelete(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={<DeleteIcon className="w-3 h-3" />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<div className="space-y-1.5 mt-3">
|
||||
{/* 第一行:编辑和重置 */}
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
onPress={() => handleEdit(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={<EditIcon className="w-3 h-3" />}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="warning"
|
||||
onPress={() => handleResetFlow(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="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" clipRule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 第二行:权限和删除 */}
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="success"
|
||||
onPress={() => handleManageTunnels(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={<SettingsIcon className="w-3 h-3" />}
|
||||
>
|
||||
权限
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
onPress={() => handleDelete(user)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={<DeleteIcon className="w-3 h-3" />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user