mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-05 09:36:37 +08:00
增加拖拽排序
This commit is contained in:
Binary file not shown.
@@ -42,6 +42,7 @@ CREATE TABLE `forward` (
|
|||||||
`created_time` bigint(20) NOT NULL,
|
`created_time` bigint(20) NOT NULL,
|
||||||
`updated_time` bigint(20) NOT NULL,
|
`updated_time` bigint(20) NOT NULL,
|
||||||
`status` int(10) NOT NULL
|
`status` int(10) NOT NULL
|
||||||
|
`inx` int(10) NOT NULL DEFAULT 0,
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "forward-panel",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -756,6 +756,29 @@ UPDATE \`forward\`
|
|||||||
SET \`strategy\` = 'fifo'
|
SET \`strategy\` = 'fifo'
|
||||||
WHERE \`strategy\` IS NULL;
|
WHERE \`strategy\` IS NULL;
|
||||||
|
|
||||||
|
-- forward 表:添加 inx 字段(排序索引)
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'forward'
|
||||||
|
AND column_name = 'inx'
|
||||||
|
),
|
||||||
|
'ALTER TABLE \`forward\` ADD COLUMN \`inx\` INT(10) DEFAULT 0 COMMENT "排序索引";',
|
||||||
|
'SELECT "Column \`inx\` already exists in \`forward\`";'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 为现有数据设置默认排序索引
|
||||||
|
UPDATE \`forward\`
|
||||||
|
SET \`inx\` = 0
|
||||||
|
WHERE \`inx\` IS NULL;
|
||||||
|
|
||||||
-- 创建 vite_config 表(如果不存在)
|
-- 创建 vite_config 表(如果不存在)
|
||||||
CREATE TABLE IF NOT EXISTS \`vite_config\` (
|
CREATE TABLE IF NOT EXISTS \`vite_config\` (
|
||||||
\`id\` int(10) NOT NULL AUTO_INCREMENT,
|
\`id\` int(10) NOT NULL AUTO_INCREMENT,
|
||||||
|
|||||||
@@ -85,4 +85,6 @@ public class ForwardWithTunnelDto {
|
|||||||
private Long outFlow;
|
private Long outFlow;
|
||||||
|
|
||||||
private String strategy;
|
private String strategy;
|
||||||
|
|
||||||
|
private Integer inx;
|
||||||
}
|
}
|
||||||
@@ -87,4 +87,15 @@ public class ForwardController extends BaseController {
|
|||||||
return forwardService.diagnoseForward(forwardId);
|
return forwardService.diagnoseForward(forwardId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新转发排序
|
||||||
|
* @param params 包含forwards数组的参数,每个元素包含id和inx
|
||||||
|
* @return 更新结果
|
||||||
|
*/
|
||||||
|
@LogAnnotation
|
||||||
|
@PostMapping("/update-order")
|
||||||
|
public R updateForwardOrder(@RequestBody Map<String, Object> params) {
|
||||||
|
return forwardService.updateForwardOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,6 @@ public class Forward extends BaseEntity{
|
|||||||
|
|
||||||
private Long outFlow;
|
private Long outFlow;
|
||||||
|
|
||||||
|
private Integer inx;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.admin.common.dto.ForwardUpdateDto;
|
|||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import com.admin.entity.Forward;
|
import com.admin.entity.Forward;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -71,4 +72,11 @@ public interface ForwardService extends IService<Forward> {
|
|||||||
* @return 诊断结果
|
* @return 诊断结果
|
||||||
*/
|
*/
|
||||||
R diagnoseForward(Long id);
|
R diagnoseForward(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新转发排序
|
||||||
|
* @param params 包含forwards数组的参数
|
||||||
|
* @return 更新结果
|
||||||
|
*/
|
||||||
|
R updateForwardOrder(Map<String, Object> params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -452,6 +452,68 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
return R.ok(diagnosisReport);
|
return R.ok(diagnosisReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R updateForwardOrder(Map<String, Object> params) {
|
||||||
|
try {
|
||||||
|
// 1. 获取当前用户信息
|
||||||
|
UserInfo currentUser = getCurrentUserInfo();
|
||||||
|
|
||||||
|
// 2. 验证参数
|
||||||
|
if (!params.containsKey("forwards")) {
|
||||||
|
return R.err("缺少forwards参数");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> forwardsList = (List<Map<String, Object>>) params.get("forwards");
|
||||||
|
if (forwardsList == null || forwardsList.isEmpty()) {
|
||||||
|
return R.err("forwards参数不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证用户权限(只能更新自己的转发)
|
||||||
|
if (currentUser.getRoleId() != ADMIN_ROLE_ID) {
|
||||||
|
// 普通用户只能更新自己的转发
|
||||||
|
List<Long> forwardIds = forwardsList.stream()
|
||||||
|
.map(item -> Long.valueOf(item.get("id").toString()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 检查所有转发是否属于当前用户
|
||||||
|
QueryWrapper<Forward> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.in("id", forwardIds);
|
||||||
|
queryWrapper.eq("user_id", currentUser.getUserId());
|
||||||
|
|
||||||
|
long count = this.count(queryWrapper);
|
||||||
|
if (count != forwardIds.size()) {
|
||||||
|
return R.err("只能更新自己的转发排序");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 批量更新排序
|
||||||
|
List<Forward> forwardsToUpdate = new ArrayList<>();
|
||||||
|
for (Map<String, Object> forwardData : forwardsList) {
|
||||||
|
Long id = Long.valueOf(forwardData.get("id").toString());
|
||||||
|
Integer inx = Integer.valueOf(forwardData.get("inx").toString());
|
||||||
|
|
||||||
|
Forward forward = new Forward();
|
||||||
|
forward.setId(id);
|
||||||
|
forward.setInx(inx);
|
||||||
|
forwardsToUpdate.add(forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 执行批量更新
|
||||||
|
boolean success = this.updateBatchById(forwardsToUpdate);
|
||||||
|
if (success) {
|
||||||
|
log.info("用户 {} 更新了 {} 个转发的排序", currentUser.getUserName(), forwardsToUpdate.size());
|
||||||
|
return R.ok("排序更新成功");
|
||||||
|
} else {
|
||||||
|
return R.err("排序更新失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新转发排序失败", e);
|
||||||
|
return R.err("更新排序时发生错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从地址字符串中提取IP地址
|
* 从地址字符串中提取IP地址
|
||||||
* 支持格式: ip:port, [ipv6]:port, domain:port
|
* 支持格式: ip:port, [ipv6]:port, domain:port
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
f.in_flow AS inFlow,
|
f.in_flow AS inFlow,
|
||||||
f.strategy AS strategy,
|
f.strategy AS strategy,
|
||||||
f.out_flow AS outFlow,
|
f.out_flow AS outFlow,
|
||||||
|
f.inx AS inx,
|
||||||
t.name AS tunnelName,
|
t.name AS tunnelName,
|
||||||
t.in_ip AS inIp,
|
t.in_ip AS inIp,
|
||||||
t.out_ip AS outIp,
|
t.out_ip AS outIp,
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
f.in_flow AS inFlow,
|
f.in_flow AS inFlow,
|
||||||
f.out_flow AS outFlow,
|
f.out_flow AS outFlow,
|
||||||
f.strategy AS strategy,
|
f.strategy AS strategy,
|
||||||
|
f.inx AS inx,
|
||||||
t.name AS tunnelName,
|
t.name AS tunnelName,
|
||||||
t.in_ip AS inIp,
|
t.in_ip AS inIp,
|
||||||
t.out_ip AS outIp,
|
t.out_ip AS outIp,
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@heroui/accordion": "^2.2.21",
|
"@heroui/accordion": "^2.2.21",
|
||||||
"@heroui/alert": "^2.2.24",
|
"@heroui/alert": "^2.2.24",
|
||||||
"@heroui/autocomplete": "^2.3.25",
|
"@heroui/autocomplete": "^2.3.25",
|
||||||
@@ -44,10 +47,12 @@
|
|||||||
"@react-types/shared": "3.30.0",
|
"@react-types/shared": "3.30.0",
|
||||||
"@tailwindcss/postcss": "4.1.11",
|
"@tailwindcss/postcss": "4.1.11",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"framer-motion": "11.18.2",
|
"framer-motion": "11.18.2",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-router-dom": "6.23.0",
|
"react-router-dom": "6.23.0",
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export const resumeForwardService = (forwardId: number) => Network.post("/forwar
|
|||||||
// 转发诊断操作
|
// 转发诊断操作
|
||||||
export const diagnoseForward = (forwardId: number) => Network.post("/forward/diagnose", { forwardId });
|
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请求
|
// 限速规则CRUD操作 - 全部使用POST请求
|
||||||
export const createSpeedLimit = (data: any) => Network.post("/speed-limit/create", data);
|
export const createSpeedLimit = (data: any) => Network.post("/speed-limit/create", data);
|
||||||
export const getSpeedLimitList = () => Network.post("/speed-limit/list");
|
export const getSpeedLimitList = () => Network.post("/speed-limit/list");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
|
|||||||
|
|
||||||
// 缓存相关常量
|
// 缓存相关常量
|
||||||
const CACHE_PREFIX = 'vite_config_';
|
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 { Alert } from "@heroui/alert";
|
||||||
import { Accordion, AccordionItem } from "@heroui/accordion";
|
import { Accordion, AccordionItem } from "@heroui/accordion";
|
||||||
import toast from 'react-hot-toast';
|
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 AdminLayout from "@/layouts/admin";
|
||||||
import {
|
import {
|
||||||
@@ -22,8 +41,10 @@ import {
|
|||||||
userTunnel,
|
userTunnel,
|
||||||
pauseForwardService,
|
pauseForwardService,
|
||||||
resumeForwardService,
|
resumeForwardService,
|
||||||
diagnoseForward
|
diagnoseForward,
|
||||||
|
updateForwardOrder
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
|
import { JwtUtil } from "@/utils/jwt";
|
||||||
|
|
||||||
interface Forward {
|
interface Forward {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -41,6 +62,7 @@ interface Forward {
|
|||||||
createdTime: string;
|
createdTime: string;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
inx?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Tunnel {
|
interface Tunnel {
|
||||||
@@ -110,6 +132,9 @@ export default function ForwardPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 拖拽排序相关状态
|
||||||
|
const [forwardOrder, setForwardOrder] = useState<number[]>([]);
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
@@ -166,6 +191,55 @@ export default function ForwardPage() {
|
|||||||
setViewMode(newMode);
|
setViewMode(newMode);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('forward-view-mode', newMode);
|
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) {
|
} catch (error) {
|
||||||
console.warn('无法保存显示模式到localStorage:', error);
|
console.warn('无法保存显示模式到localStorage:', error);
|
||||||
}
|
}
|
||||||
@@ -181,10 +255,62 @@ export default function ForwardPage() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (forwardsRes.code === 0) {
|
if (forwardsRes.code === 0) {
|
||||||
setForwards(forwardsRes.data?.map((forward: any) => ({
|
const forwardsData = forwardsRes.data?.map((forward: any) => ({
|
||||||
...forward,
|
...forward,
|
||||||
serviceRunning: forward.status === 1
|
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 {
|
} else {
|
||||||
toast.error(forwardsRes.msg || '获取转发列表失败');
|
toast.error(forwardsRes.msg || '获取转发列表失败');
|
||||||
}
|
}
|
||||||
@@ -206,7 +332,10 @@ export default function ForwardPage() {
|
|||||||
const groupForwardsByUserAndTunnel = (): UserGroup[] => {
|
const groupForwardsByUserAndTunnel = (): UserGroup[] => {
|
||||||
const userMap = new Map<string, 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 userKey = forward.userId ? forward.userId.toString() : 'unknown';
|
||||||
const userName = forward.userName || '未知用户';
|
const userName = forward.userName || '未知用户';
|
||||||
|
|
||||||
@@ -687,7 +816,7 @@ export default function ForwardPage() {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 直接显示模式下,过滤指定隧道的转发
|
// 直接显示模式下,过滤指定隧道的转发
|
||||||
forwardsToExport = forwards.filter(forward => forward.tunnelId === selectedTunnelForExport);
|
forwardsToExport = getSortedForwards().filter(forward => forward.tunnelId === selectedTunnelForExport);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forwardsToExport.length === 0) {
|
if (forwardsToExport.length === 0) {
|
||||||
@@ -876,13 +1005,135 @@ export default function ForwardPage() {
|
|||||||
return addresses.length;
|
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 renderForwardCard = (forward: Forward) => {
|
||||||
const statusDisplay = getStatusDisplay(forward.status);
|
const statusDisplay = getStatusDisplay(forward.status);
|
||||||
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
||||||
|
|
||||||
return (
|
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">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start w-full">
|
<div className="flex justify-between items-start w-full">
|
||||||
<div className="flex-1 min-w-0">
|
<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>
|
<p className="text-xs text-default-500 truncate">{forward.tunnelName}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 ml-2">
|
<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
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={forward.serviceRunning}
|
isSelected={forward.serviceRunning}
|
||||||
@@ -1044,7 +1302,9 @@ export default function ForwardPage() {
|
|||||||
<div className="px-3 lg:px-6 py-8">
|
<div className="px-3 lg:px-6 py-8">
|
||||||
{/* 页面头部 */}
|
{/* 页面头部 */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<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">
|
<div className="flex items-center gap-3">
|
||||||
{/* 显示模式切换按钮 */}
|
{/* 显示模式切换按钮 */}
|
||||||
<Button
|
<Button
|
||||||
@@ -1190,9 +1450,22 @@ export default function ForwardPage() {
|
|||||||
) : (
|
) : (
|
||||||
/* 直接显示模式 */
|
/* 直接显示模式 */
|
||||||
forwards.length > 0 ? (
|
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">
|
<DndContext
|
||||||
{forwards.map((forward) => renderForwardCard(forward))}
|
sensors={sensors}
|
||||||
</div>
|
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">
|
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
|||||||
@@ -688,51 +688,58 @@ export default function UserPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1.5 mt-3">
|
<div className="space-y-1.5 mt-3">
|
||||||
<Button
|
{/* 第一行:编辑和重置 */}
|
||||||
size="sm"
|
<div className="flex gap-1.5">
|
||||||
variant="flat"
|
<Button
|
||||||
color="primary"
|
size="sm"
|
||||||
onPress={() => handleEdit(user)}
|
variant="flat"
|
||||||
className="flex-1 min-h-8"
|
color="primary"
|
||||||
startContent={<EditIcon className="w-3 h-3" />}
|
onPress={() => handleEdit(user)}
|
||||||
>
|
className="flex-1 min-h-8"
|
||||||
编辑
|
startContent={<EditIcon className="w-3 h-3" />}
|
||||||
</Button>
|
>
|
||||||
<Button
|
编辑
|
||||||
size="sm"
|
</Button>
|
||||||
variant="flat"
|
<Button
|
||||||
color="warning"
|
size="sm"
|
||||||
onPress={() => handleResetFlow(user)}
|
variant="flat"
|
||||||
className="flex-1 min-h-8"
|
color="warning"
|
||||||
startContent={
|
onPress={() => handleResetFlow(user)}
|
||||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
className="flex-1 min-h-8"
|
||||||
<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" />
|
startContent={
|
||||||
</svg>
|
<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"
|
</Button>
|
||||||
variant="flat"
|
</div>
|
||||||
color="success"
|
|
||||||
onPress={() => handleManageTunnels(user)}
|
{/* 第二行:权限和删除 */}
|
||||||
className="flex-1 min-h-8"
|
<div className="flex gap-1.5">
|
||||||
startContent={<SettingsIcon className="w-3 h-3" />}
|
<Button
|
||||||
>
|
size="sm"
|
||||||
权限
|
variant="flat"
|
||||||
</Button>
|
color="success"
|
||||||
<Button
|
onPress={() => handleManageTunnels(user)}
|
||||||
size="sm"
|
className="flex-1 min-h-8"
|
||||||
variant="flat"
|
startContent={<SettingsIcon className="w-3 h-3" />}
|
||||||
color="danger"
|
>
|
||||||
onPress={() => handleDelete(user)}
|
权限
|
||||||
className="flex-1 min-h-8"
|
</Button>
|
||||||
startContent={<DeleteIcon className="w-3 h-3" />}
|
<Button
|
||||||
>
|
size="sm"
|
||||||
删除
|
variant="flat"
|
||||||
</Button>
|
color="danger"
|
||||||
|
onPress={() => handleDelete(user)}
|
||||||
|
className="flex-1 min-h-8"
|
||||||
|
startContent={<DeleteIcon className="w-3 h-3" />}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user