mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 23:56:36 +08:00
增加拖拽排序
This commit is contained in:
Binary file not shown.
@@ -42,6 +42,7 @@ CREATE TABLE `forward` (
|
||||
`created_time` bigint(20) NOT NULL,
|
||||
`updated_time` bigint(20) NOT NULL,
|
||||
`status` int(10) NOT NULL
|
||||
`inx` int(10) NOT NULL DEFAULT 0,
|
||||
) 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'
|
||||
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 表(如果不存在)
|
||||
CREATE TABLE IF NOT EXISTS \`vite_config\` (
|
||||
\`id\` int(10) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
@@ -85,4 +85,6 @@ public class ForwardWithTunnelDto {
|
||||
private Long outFlow;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inx;
|
||||
}
|
||||
@@ -87,4 +87,15 @@ public class ForwardController extends BaseController {
|
||||
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 Integer inx;
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.admin.common.dto.ForwardUpdateDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.entity.Forward;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -71,4 +72,11 @@ public interface ForwardService extends IService<Forward> {
|
||||
* @return 诊断结果
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@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:port, [ipv6]:port, domain:port
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
f.in_flow AS inFlow,
|
||||
f.strategy AS strategy,
|
||||
f.out_flow AS outFlow,
|
||||
f.inx AS inx,
|
||||
t.name AS tunnelName,
|
||||
t.in_ip AS inIp,
|
||||
t.out_ip AS outIp,
|
||||
@@ -49,6 +50,7 @@
|
||||
f.in_flow AS inFlow,
|
||||
f.out_flow AS outFlow,
|
||||
f.strategy AS strategy,
|
||||
f.inx AS inx,
|
||||
t.name AS tunnelName,
|
||||
t.in_ip AS inIp,
|
||||
t.out_ip AS outIp,
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"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/alert": "^2.2.24",
|
||||
"@heroui/autocomplete": "^2.3.25",
|
||||
@@ -44,10 +47,12 @@
|
||||
"@react-types/shared": "3.30.0",
|
||||
"@tailwindcss/postcss": "4.1.11",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"axios": "^1.11.0",
|
||||
"clsx": "2.1.1",
|
||||
"framer-motion": "11.18.2",
|
||||
"react": "18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"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 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