feat: 简化用户隧道权限分配,支持多选隧道批量分配 (#8)

- 新增批量分配接口 POST /tunnel/user/batch-assign
- 支持一次选择多个隧道进行分配
- 每个隧道可单独设置限速规则
- flow/num/flowResetTime/expTime 自动从用户设置继承
- 前端表单简化为隧道多选列表+限速选择
- 已分配的隧道显示'已分配'标记且不可重复选择
This commit is contained in:
root
2026-02-03 13:59:05 +00:00
parent 60fc80b6ac
commit 0e5cd86ed1
8 changed files with 369 additions and 278 deletions
@@ -0,0 +1,24 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Data
public class UserTunnelBatchAssignDto {
@NotNull(message = "用户ID不能为空")
private Integer userId;
@NotEmpty(message = "隧道列表不能为空")
private List<TunnelAssignItem> tunnels;
@Data
public static class TunnelAssignItem {
@NotNull(message = "隧道ID不能为空")
private Integer tunnelId;
private Integer speedId;
}
}
@@ -13,28 +13,15 @@ public class UserTunnelDto {
@NotNull(message = "隧道ID不能为空") @NotNull(message = "隧道ID不能为空")
private Integer tunnelId; private Integer tunnelId;
@NotNull(message = "流量限制不能为空")
@Min(value = 0, message = "流量限制不能小于0") @Min(value = 0, message = "流量限制不能小于0")
private Long flow; private Long flow;
@NotNull(message = "转发数量不能为空")
@Min(value = 0, message = "转发数量不能小于0") @Min(value = 0, message = "转发数量不能小于0")
private Integer num; private Integer num;
/**
* 流量重置时间(时间戳)
*/
@NotNull(message = "流量重置时间不能为空")
private Long flowResetTime; private Long flowResetTime;
/**
* 到期时间(时间戳)
*/
@NotNull(message = "到期时间不能为空")
private Long expTime; private Long expTime;
/**
* 限速规则ID(可选,null表示不限速)
*/
private Integer speedId; private Integer speedId;
} }
@@ -5,6 +5,7 @@ import com.admin.common.annotation.RequireRole;
import com.admin.common.dto.TunnelDto; import com.admin.common.dto.TunnelDto;
import com.admin.common.dto.TunnelUpdateDto; import com.admin.common.dto.TunnelUpdateDto;
import com.admin.common.dto.UserTunnelBatchAssignDto;
import com.admin.common.dto.UserTunnelDto; import com.admin.common.dto.UserTunnelDto;
import com.admin.common.dto.UserTunnelQueryDto; import com.admin.common.dto.UserTunnelQueryDto;
import com.admin.common.dto.UserTunnelUpdateDto; import com.admin.common.dto.UserTunnelUpdateDto;
@@ -78,6 +79,13 @@ public class TunnelController extends BaseController {
public R assignUserTunnel(@Validated @RequestBody UserTunnelDto userTunnelDto) { public R assignUserTunnel(@Validated @RequestBody UserTunnelDto userTunnelDto) {
return userTunnelService.assignUserTunnel(userTunnelDto); return userTunnelService.assignUserTunnel(userTunnelDto);
} }
@LogAnnotation
@RequireRole
@PostMapping("/user/batch-assign")
public R batchAssignUserTunnel(@Validated @RequestBody UserTunnelBatchAssignDto batchAssignDto) {
return userTunnelService.batchAssignUserTunnel(batchAssignDto);
}
/** /**
* 查询用户隧道权限列表 * 查询用户隧道权限列表
@@ -1,49 +1,23 @@
package com.admin.service; package com.admin.service;
import com.admin.common.dto.UserTunnelDto; import com.admin.common.dto.UserTunnelBatchAssignDto;
import com.admin.common.dto.UserTunnelQueryDto; import com.admin.common.dto.UserTunnelDto;
import com.admin.common.dto.UserTunnelUpdateDto; import com.admin.common.dto.UserTunnelQueryDto;
import com.admin.common.lang.R; import com.admin.common.dto.UserTunnelUpdateDto;
import com.admin.entity.UserTunnel; import com.admin.common.lang.R;
import com.baomidou.mybatisplus.extension.service.IService; import com.admin.entity.UserTunnel;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p> public interface UserTunnelService extends IService<UserTunnel> {
* 用户隧道权限服务类
* </p> R assignUserTunnel(UserTunnelDto userTunnelDto);
*
* @author QAQ R batchAssignUserTunnel(UserTunnelBatchAssignDto batchAssignDto);
* @since 2025-06-03
*/ R getUserTunnelList(UserTunnelQueryDto queryDto);
public interface UserTunnelService extends IService<UserTunnel> {
R removeUserTunnel(Integer id);
/**
* 分配用户隧道权限 R updateUserTunnel(UserTunnelUpdateDto updateDto);
* @param userTunnelDto 用户隧道权限数据
* @return 结果 }
*/
R assignUserTunnel(UserTunnelDto userTunnelDto);
/**
* 查询用户隧道权限列表
* @param queryDto 查询条件
* @return 结果
*/
R getUserTunnelList(UserTunnelQueryDto queryDto);
/**
* 删除用户隧道权限
* @param id ID
* @return 结果
*/
R removeUserTunnel(Integer id);
/**
* 更新用户隧道权限(包含流量、流量重置时间、到期时间)
* @param updateDto 更新数据
* @return 结果
*/
R updateUserTunnel(UserTunnelUpdateDto updateDto);
}
@@ -1,120 +1,178 @@
package com.admin.service.impl; package com.admin.service.impl;
import com.admin.common.dto.*; import com.admin.common.dto.*;
import com.admin.common.lang.R; import com.admin.common.lang.R;
import com.admin.entity.UserTunnel; import com.admin.entity.User;
import com.admin.mapper.TunnelMapper; import com.admin.entity.UserTunnel;
import com.admin.mapper.UserTunnelMapper; import com.admin.mapper.UserTunnelMapper;
import com.admin.service.TunnelService; import com.admin.service.UserService;
import com.admin.service.UserTunnelService; import com.admin.service.UserTunnelService;
import com.admin.service.ForwardService; import com.admin.service.ForwardService;
import com.admin.service.NodeService; import com.admin.entity.Forward;
import com.admin.common.utils.GostUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.admin.entity.Forward; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.admin.entity.Tunnel; import org.springframework.beans.BeanUtils;
import com.admin.entity.Node; import org.springframework.context.annotation.Lazy;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import javax.annotation.Resource;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import java.util.ArrayList;
import org.springframework.beans.BeanUtils; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import java.util.Set;
import org.springframework.context.annotation.Lazy; import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@Service
import javax.annotation.Resource; public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTunnel> implements UserTunnelService {
import java.util.List;
import java.util.Map; @Resource
@Lazy
/** private ForwardService forwardService;
* <p>
* 用户隧道权限服务实现类 @Resource
* 提供用户隧道权限的分配、查询、更新和删除功能 @Lazy
* 支持流量限制、数量限制、过期时间和限速规则的管理 private UserService userService;
* </p>
* @Override
* @author QAQ public R assignUserTunnel(UserTunnelDto userTunnelDto) {
* @since 2025-06-03 int count = this.count(new QueryWrapper<UserTunnel>()
*/ .eq("user_id", userTunnelDto.getUserId())
@Service .eq("tunnel_id", userTunnelDto.getTunnelId()));
public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTunnel> implements UserTunnelService { if (count > 0) return R.err("该用户已拥有此隧道权限");
User user = userService.getById(userTunnelDto.getUserId());
@Resource if (user == null) return R.err("用户不存在");
@Lazy
private ForwardService forwardService; UserTunnel userTunnel = new UserTunnel();
userTunnel.setUserId(userTunnelDto.getUserId());
@Override userTunnel.setTunnelId(userTunnelDto.getTunnelId());
public R assignUserTunnel(UserTunnelDto userTunnelDto) { userTunnel.setSpeedId(userTunnelDto.getSpeedId());
int count = this.count(new QueryWrapper<UserTunnel>().eq("user_id", userTunnelDto.getUserId()).eq("tunnel_id", userTunnelDto.getTunnelId())); userTunnel.setStatus(1);
if (count > 0) return R.err("该用户已拥有此隧道权限"); userTunnel.setInFlow(0L);
UserTunnel userTunnel = new UserTunnel(); userTunnel.setOutFlow(0L);
BeanUtils.copyProperties(userTunnelDto, userTunnel);
userTunnel.setStatus(1); userTunnel.setFlow(userTunnelDto.getFlow() != null ? userTunnelDto.getFlow() : user.getFlow());
this.save(userTunnel); userTunnel.setNum(userTunnelDto.getNum() != null ? userTunnelDto.getNum() : user.getNum());
return R.ok(); userTunnel.setFlowResetTime(userTunnelDto.getFlowResetTime() != null ? userTunnelDto.getFlowResetTime() : user.getFlowResetTime());
} userTunnel.setExpTime(userTunnelDto.getExpTime() != null ? userTunnelDto.getExpTime() : user.getExpTime());
@Override this.save(userTunnel);
public R getUserTunnelList(UserTunnelQueryDto queryDto) { return R.ok();
List<UserTunnelWithDetailDto> userTunnelWithDetails = this.baseMapper.getUserTunnelWithDetails(queryDto.getUserId()); }
return R.ok(userTunnelWithDetails);
} @Override
public R batchAssignUserTunnel(UserTunnelBatchAssignDto batchAssignDto) {
@Override User user = userService.getById(batchAssignDto.getUserId());
public R removeUserTunnel(Integer id) { if (user == null) return R.err("用户不存在");
UserTunnel userTunnel = this.getById(id);
if (userTunnel == null) return R.err("未找到对应的用户隧道权限记录"); List<Integer> requestedTunnelIds = batchAssignDto.getTunnels().stream()
.map(UserTunnelBatchAssignDto.TunnelAssignItem::getTunnelId)
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", userTunnel.getUserId()).eq("tunnel_id", userTunnel.getTunnelId())); .collect(Collectors.toList());
for (Forward forward : forwardList) {
forwardService.deleteForward(forward.getId()); Set<Integer> existingTunnelIds = this.list(
} new QueryWrapper<UserTunnel>()
this.removeById(id); .eq("user_id", batchAssignDto.getUserId())
return R.ok(); .in("tunnel_id", requestedTunnelIds)
} ).stream().map(UserTunnel::getTunnelId).collect(Collectors.toSet());
@Override List<UserTunnel> toSave = new ArrayList<>();
public R updateUserTunnel(UserTunnelUpdateDto updateDto) { List<Integer> skippedIds = new ArrayList<>();
UserTunnel userTunnel = this.getById(updateDto.getId());
if (userTunnel == null) return R.err("隧道不存在"); for (UserTunnelBatchAssignDto.TunnelAssignItem item : batchAssignDto.getTunnels()) {
boolean speedChanged = hasSpeedChanged(userTunnel.getSpeedId(), updateDto.getSpeedId()); if (existingTunnelIds.contains(item.getTunnelId())) {
userTunnel.setFlow(updateDto.getFlow()); skippedIds.add(item.getTunnelId());
userTunnel.setNum(updateDto.getNum()); continue;
updateOptionalProperty(userTunnel::setFlowResetTime, updateDto.getFlowResetTime()); }
updateOptionalProperty(userTunnel::setExpTime, updateDto.getExpTime());
updateOptionalProperty(userTunnel::setStatus, updateDto.getStatus()); UserTunnel ut = new UserTunnel();
userTunnel.setSpeedId(updateDto.getSpeedId()); ut.setUserId(batchAssignDto.getUserId());
this.updateById(userTunnel); ut.setTunnelId(item.getTunnelId());
if (speedChanged) { ut.setSpeedId(item.getSpeedId());
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", userTunnel.getUserId()).eq("tunnel_id", userTunnel.getTunnelId())); ut.setStatus(1);
for (Forward forward : forwardList) { ut.setInFlow(0L);
ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto(); ut.setOutFlow(0L);
forwardUpdateDto.setId(forward.getId()); ut.setFlow(user.getFlow());
forwardUpdateDto.setUserId(forward.getUserId()); ut.setNum(user.getNum());
forwardUpdateDto.setName(forward.getName()); ut.setFlowResetTime(user.getFlowResetTime());
forwardUpdateDto.setRemoteAddr(forward.getRemoteAddr()); ut.setExpTime(user.getExpTime());
forwardUpdateDto.setStrategy(forward.getStrategy()); toSave.add(ut);
forwardService.updateForward(forwardUpdateDto); }
}
} if (toSave.isEmpty()) {
return R.err("用户隧道权限更新失败"); return R.err("所选隧道用户均已拥有权限");
} }
private <T> void updateOptionalProperty(java.util.function.Consumer<T> setter, T value) { this.saveBatch(toSave);
if (value != null) {
setter.accept(value); if (!skippedIds.isEmpty()) {
} return R.ok("成功分配 " + toSave.size() + " 个隧道,跳过 " + skippedIds.size() + " 个已有权限的隧道");
} }
return R.ok();
private boolean hasSpeedChanged(Integer oldSpeedId, Integer newSpeedId) { }
if (oldSpeedId == null && newSpeedId == null) {
return false; @Override
} public R getUserTunnelList(UserTunnelQueryDto queryDto) {
if (oldSpeedId == null || newSpeedId == null) { List<UserTunnelWithDetailDto> userTunnelWithDetails = this.baseMapper.getUserTunnelWithDetails(queryDto.getUserId());
return true; return R.ok(userTunnelWithDetails);
} }
return !oldSpeedId.equals(newSpeedId);
} @Override
public R removeUserTunnel(Integer id) {
} UserTunnel userTunnel = this.getById(id);
if (userTunnel == null) return R.err("未找到对应的用户隧道权限记录");
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>()
.eq("user_id", userTunnel.getUserId())
.eq("tunnel_id", userTunnel.getTunnelId()));
for (Forward forward : forwardList) {
forwardService.deleteForward(forward.getId());
}
this.removeById(id);
return R.ok();
}
@Override
public R updateUserTunnel(UserTunnelUpdateDto updateDto) {
UserTunnel userTunnel = this.getById(updateDto.getId());
if (userTunnel == null) return R.err("隧道不存在");
boolean speedChanged = hasSpeedChanged(userTunnel.getSpeedId(), updateDto.getSpeedId());
userTunnel.setFlow(updateDto.getFlow());
userTunnel.setNum(updateDto.getNum());
updateOptionalProperty(userTunnel::setFlowResetTime, updateDto.getFlowResetTime());
updateOptionalProperty(userTunnel::setExpTime, updateDto.getExpTime());
updateOptionalProperty(userTunnel::setStatus, updateDto.getStatus());
userTunnel.setSpeedId(updateDto.getSpeedId());
this.updateById(userTunnel);
if (speedChanged) {
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>()
.eq("user_id", userTunnel.getUserId())
.eq("tunnel_id", userTunnel.getTunnelId()));
for (Forward forward : forwardList) {
ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto();
forwardUpdateDto.setId(forward.getId());
forwardUpdateDto.setUserId(forward.getUserId());
forwardUpdateDto.setName(forward.getName());
forwardUpdateDto.setRemoteAddr(forward.getRemoteAddr());
forwardUpdateDto.setStrategy(forward.getStrategy());
forwardService.updateForward(forwardUpdateDto);
}
}
return R.ok();
}
private <T> void updateOptionalProperty(java.util.function.Consumer<T> setter, T value) {
if (value != null) {
setter.accept(value);
}
}
private boolean hasSpeedChanged(Integer oldSpeedId, Integer newSpeedId) {
if (oldSpeedId == null && newSpeedId == null) {
return false;
}
if (oldSpeedId == null || newSpeedId == null) {
return true;
}
return !oldSpeedId.equals(newSpeedId);
}
}
+2
View File
@@ -46,6 +46,8 @@ export const updateTunnelOrder = (data: { tunnels: Array<{ id: number; inx: numb
// 用户隧道权限管理操作 - 全部使用POST请求 // 用户隧道权限管理操作 - 全部使用POST请求
export const assignUserTunnel = (data: any) => Network.post("/tunnel/user/assign", data); export const assignUserTunnel = (data: any) => Network.post("/tunnel/user/assign", data);
export const batchAssignUserTunnel = (data: { userId: number; tunnels: Array<{ tunnelId: number; speedId?: number | null }> }) =>
Network.post("/tunnel/user/batch-assign", data);
export const getUserTunnelList = (queryData: any = {}) => Network.post("/tunnel/user/list", queryData); export const getUserTunnelList = (queryData: any = {}) => Network.post("/tunnel/user/list", queryData);
export const removeUserTunnel = (params: any) => Network.post("/tunnel/user/remove", params); export const removeUserTunnel = (params: any) => Network.post("/tunnel/user/remove", params);
export const updateUserTunnel = (data: any) => Network.post("/tunnel/user/update", data); export const updateUserTunnel = (data: any) => Network.post("/tunnel/user/update", data);
+124 -95
View File
@@ -31,6 +31,7 @@ import {
UserForm, UserForm,
UserTunnel, UserTunnel,
UserTunnelForm, UserTunnelForm,
TunnelAssignItem,
Tunnel, Tunnel,
SpeedLimit, SpeedLimit,
Pagination as PaginationType Pagination as PaginationType
@@ -42,6 +43,7 @@ import {
deleteUser, deleteUser,
getTunnelList, getTunnelList,
assignUserTunnel, assignUserTunnel,
batchAssignUserTunnel,
getUserTunnelList, getUserTunnelList,
removeUserTunnel, removeUserTunnel,
updateUserTunnel, updateUserTunnel,
@@ -143,6 +145,7 @@ export default function UserPage() {
speedId: null speedId: null
}); });
const [assignLoading, setAssignLoading] = useState(false); const [assignLoading, setAssignLoading] = useState(false);
const [batchTunnelSelections, setBatchTunnelSelections] = useState<Map<number, number | null>>(new Map());
// 编辑隧道权限相关状态 // 编辑隧道权限相关状态
const { isOpen: isEditTunnelModalOpen, onOpen: onEditTunnelModalOpen, onClose: onEditTunnelModalClose } = useDisclosure(); const { isOpen: isEditTunnelModalOpen, onOpen: onEditTunnelModalOpen, onClose: onEditTunnelModalClose } = useDisclosure();
@@ -342,6 +345,7 @@ export default function UserPage() {
flowResetTime: 0, flowResetTime: 0,
speedId: null speedId: null
}); });
setBatchTunnelSelections(new Map());
onTunnelModalOpen(); onTunnelModalOpen();
loadUserTunnels(user.id); loadUserTunnels(user.id);
}; };
@@ -385,6 +389,37 @@ export default function UserPage() {
} }
}; };
const handleBatchAssignTunnel = async () => {
if (batchTunnelSelections.size === 0 || !currentUser) {
toast.error('请选择至少一个隧道');
return;
}
setAssignLoading(true);
try {
const tunnelsToAssign: TunnelAssignItem[] = Array.from(batchTunnelSelections.entries()).map(
([tunnelId, speedId]) => ({ tunnelId, speedId })
);
const response = await batchAssignUserTunnel({
userId: currentUser.id,
tunnels: tunnelsToAssign
});
if (response.code === 0) {
toast.success(response.msg || '分配成功');
setBatchTunnelSelections(new Map());
loadUserTunnels(currentUser.id);
} else {
toast.error(response.msg || '分配失败');
}
} catch (error) {
toast.error('分配失败');
} finally {
setAssignLoading(false);
}
};
const handleEditTunnel = (userTunnel: UserTunnel) => { const handleEditTunnel = (userTunnel: UserTunnel) => {
setEditTunnelForm({ setEditTunnelForm({
...userTunnel, ...userTunnel,
@@ -526,6 +561,34 @@ export default function UserPage() {
speedLimit => speedLimit.tunnelId === editTunnelForm?.tunnelId speedLimit => speedLimit.tunnelId === editTunnelForm?.tunnelId
); );
const getSpeedLimitsForTunnel = (tunnelId: number) => {
return speedLimits.filter(sl => sl.tunnelId === tunnelId);
};
const toggleTunnelSelection = (tunnelId: number) => {
setBatchTunnelSelections(prev => {
const newMap = new Map(prev);
if (newMap.has(tunnelId)) {
newMap.delete(tunnelId);
} else {
newMap.set(tunnelId, null);
}
return newMap;
});
};
const updateTunnelSpeedLimit = (tunnelId: number, speedId: number | null) => {
setBatchTunnelSelections(prev => {
const newMap = new Map(prev);
newMap.set(tunnelId, speedId);
return newMap;
});
};
const isTunnelAssigned = (tunnelId: number) => {
return userTunnels.some(ut => ut.tunnelId === tunnelId);
};
return ( return (
<div className="px-3 lg:px-6 py-8"> <div className="px-3 lg:px-6 py-8">
@@ -878,107 +941,73 @@ export default function UserPage() {
<div> <div>
<h3 className="text-lg font-semibold mb-4">分配新权限</h3> <h3 className="text-lg font-semibold mb-4">分配新权限</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="text-sm text-gray-500 bg-gray-50 dark:bg-gray-800 p-3 rounded-lg">
<Select 流量限制、转发数量、到期时间、流量重置时间将自动继承用户设置
label="选择隧道" </div>
selectedKeys={tunnelForm.tunnelId ? [tunnelForm.tunnelId.toString()] : []}
onSelectionChange={(keys) => { <div className="border rounded-lg divide-y dark:divide-gray-700 max-h-64 overflow-y-auto">
const value = Array.from(keys)[0] as string; {tunnels.map(tunnel => {
setTunnelForm(prev => ({ ...prev, tunnelId: Number(value) || null, speedId: null })); const isAssigned = isTunnelAssigned(tunnel.id);
}} const isSelected = batchTunnelSelections.has(tunnel.id);
> const tunnelSpeedLimits = getSpeedLimitsForTunnel(tunnel.id);
{availableTunnels.map(tunnel => (
<SelectItem key={tunnel.id.toString()} textValue={tunnel.name}> return (
{tunnel.name} <div
</SelectItem> key={tunnel.id}
))} className={`p-3 flex items-center justify-between gap-4 ${isAssigned ? 'bg-gray-100 dark:bg-gray-800' : ''}`}
</Select> >
<div className="flex items-center gap-3 flex-1">
<Select <input
label="限速规则" type="checkbox"
selectedKeys={tunnelForm.speedId ? [tunnelForm.speedId.toString()] : ["null"]} checked={isSelected}
onSelectionChange={(keys) => { disabled={isAssigned}
const value = Array.from(keys)[0] as string; onChange={() => toggleTunnelSelection(tunnel.id)}
setTunnelForm(prev => ({ ...prev, speedId: value === "null" ? null : Number(value) })); className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-50"
}} />
isDisabled={!tunnelForm.tunnelId} <span className={isAssigned ? 'text-gray-400' : ''}>
> {tunnel.name}
{[ </span>
<SelectItem key="null" textValue="不限速">不限速</SelectItem>, {isAssigned && (
...availableSpeedLimits.map(speedLimit => ( <Chip size="sm" variant="flat" color="default">已分配</Chip>
<SelectItem key={speedLimit.id.toString()} textValue={speedLimit.name}> )}
{speedLimit.name} </div>
</SelectItem>
)) {isSelected && !isAssigned && (
]} <Select
</Select> size="sm"
label="限速规则"
<Input className="w-40"
label="流量限制(GB)" selectedKeys={batchTunnelSelections.get(tunnel.id) !== null && batchTunnelSelections.get(tunnel.id) !== undefined ? [batchTunnelSelections.get(tunnel.id)!.toString()] : ["null"]}
type="number" onSelectionChange={(keys) => {
value={tunnelForm.flow.toString()} const value = Array.from(keys)[0] as string;
onChange={(e) => { updateTunnelSpeedLimit(tunnel.id, value === "null" ? null : Number(value));
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); }}
setTunnelForm(prev => ({ ...prev, flow: value })); >
}} {[
min="1" <SelectItem key="null" textValue="不限速">不限速</SelectItem>,
max="99999" ...tunnelSpeedLimits.map(sl => (
/> <SelectItem key={sl.id.toString()} textValue={sl.name}>
{sl.name}
<Input </SelectItem>
label="转发数量" ))
type="number" ]}
value={tunnelForm.num.toString()} </Select>
onChange={(e) => { )}
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); </div>
setTunnelForm(prev => ({ ...prev, num: value })); );
}} })}
min="1" {tunnels.length === 0 && (
max="99999" <div className="p-4 text-center text-gray-500">暂无可用隧道</div>
/> )}
<Select
label="流量重置日期"
selectedKeys={[tunnelForm.flowResetTime.toString()]}
onSelectionChange={(keys) => {
const value = Array.from(keys)[0] as string;
setTunnelForm(prev => ({ ...prev, flowResetTime: Number(value) }));
}}
>
<>
<SelectItem key="0" textValue="不重置">
不重置
</SelectItem>
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
每月{day}号(0点重置)
</SelectItem>
))}
</>
</Select>
<DatePicker
label="到期时间"
value={tunnelForm.expTime ? parseDate(tunnelForm.expTime.toISOString().split('T')[0]) as any : null}
onChange={(date) => {
if (date) {
const jsDate = new Date(date.year, date.month - 1, date.day, 23, 59, 59);
setTunnelForm(prev => ({ ...prev, expTime: jsDate }));
} else {
setTunnelForm(prev => ({ ...prev, expTime: null }));
}
}}
showMonthAndYearPickers
className="cursor-pointer"
/>
</div> </div>
<Button <Button
color="primary" color="primary"
onPress={handleAssignTunnel} onPress={handleBatchAssignTunnel}
isLoading={assignLoading} isLoading={assignLoading}
isDisabled={batchTunnelSelections.size === 0}
> >
分配权限 分配权限 {batchTunnelSelections.size > 0 && `(${batchTunnelSelections.size}个隧道)`}
</Button> </Button>
</div> </div>
</div> </div>
+9
View File
@@ -58,6 +58,15 @@ export interface UserTunnelForm {
speedId: number | null; speedId: number | null;
} }
export interface TunnelAssignItem {
tunnelId: number;
speedId: number | null;
}
export interface UserTunnelBatchAssignForm {
tunnels: TunnelAssignItem[];
}
export interface Tunnel { export interface Tunnel {
id: number; id: number;
name: string; name: string;