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;
@@ -79,6 +80,13 @@ public class TunnelController extends BaseController {
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);
}
/** /**
* 查询用户隧道权限列表 * 查询用户隧道权限列表
* @param queryDto 查询条件 * @param queryDto 查询条件
@@ -1,5 +1,6 @@
package com.admin.service; package com.admin.service;
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;
@@ -7,43 +8,16 @@ import com.admin.common.lang.R;
import com.admin.entity.UserTunnel; import com.admin.entity.UserTunnel;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 用户隧道权限服务类
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
public interface UserTunnelService extends IService<UserTunnel> { public interface UserTunnelService extends IService<UserTunnel> {
/**
* 分配用户隧道权限
* @param userTunnelDto 用户隧道权限数据
* @return 结果
*/
R assignUserTunnel(UserTunnelDto userTunnelDto); R assignUserTunnel(UserTunnelDto userTunnelDto);
/** R batchAssignUserTunnel(UserTunnelBatchAssignDto batchAssignDto);
* 查询用户隧道权限列表
* @param queryDto 查询条件
* @return 结果
*/
R getUserTunnelList(UserTunnelQueryDto queryDto); R getUserTunnelList(UserTunnelQueryDto queryDto);
/**
* 删除用户隧道权限
* @param id ID
* @return 结果
*/
R removeUserTunnel(Integer id); R removeUserTunnel(Integer id);
/**
* 更新用户隧道权限(包含流量、流量重置时间、到期时间)
* @param updateDto 更新数据
* @return 结果
*/
R updateUserTunnel(UserTunnelUpdateDto updateDto); R updateUserTunnel(UserTunnelUpdateDto updateDto);
} }
@@ -2,59 +2,113 @@ 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.User;
import com.admin.entity.UserTunnel; import com.admin.entity.UserTunnel;
import com.admin.mapper.TunnelMapper;
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.common.utils.GostUtil;
import com.admin.entity.Forward; import com.admin.entity.Forward;
import com.admin.entity.Tunnel;
import com.admin.entity.Node;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Set;
import java.util.stream.Collectors;
/**
* <p>
* 用户隧道权限服务实现类
* 提供用户隧道权限的分配、查询、更新和删除功能
* 支持流量限制、数量限制、过期时间和限速规则的管理
* </p>
*
* @author QAQ
* @since 2025-06-03
*/
@Service @Service
public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTunnel> implements UserTunnelService { public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTunnel> implements UserTunnelService {
@Resource @Resource
@Lazy @Lazy
private ForwardService forwardService; private ForwardService forwardService;
@Resource
@Lazy
private UserService userService;
@Override @Override
public R assignUserTunnel(UserTunnelDto userTunnelDto) { public R assignUserTunnel(UserTunnelDto userTunnelDto) {
int count = this.count(new QueryWrapper<UserTunnel>().eq("user_id", userTunnelDto.getUserId()).eq("tunnel_id", userTunnelDto.getTunnelId())); int count = this.count(new QueryWrapper<UserTunnel>()
.eq("user_id", userTunnelDto.getUserId())
.eq("tunnel_id", userTunnelDto.getTunnelId()));
if (count > 0) return R.err("该用户已拥有此隧道权限"); if (count > 0) return R.err("该用户已拥有此隧道权限");
User user = userService.getById(userTunnelDto.getUserId());
if (user == null) return R.err("用户不存在");
UserTunnel userTunnel = new UserTunnel(); UserTunnel userTunnel = new UserTunnel();
BeanUtils.copyProperties(userTunnelDto, userTunnel); userTunnel.setUserId(userTunnelDto.getUserId());
userTunnel.setTunnelId(userTunnelDto.getTunnelId());
userTunnel.setSpeedId(userTunnelDto.getSpeedId());
userTunnel.setStatus(1); userTunnel.setStatus(1);
userTunnel.setInFlow(0L);
userTunnel.setOutFlow(0L);
userTunnel.setFlow(userTunnelDto.getFlow() != null ? userTunnelDto.getFlow() : user.getFlow());
userTunnel.setNum(userTunnelDto.getNum() != null ? userTunnelDto.getNum() : user.getNum());
userTunnel.setFlowResetTime(userTunnelDto.getFlowResetTime() != null ? userTunnelDto.getFlowResetTime() : user.getFlowResetTime());
userTunnel.setExpTime(userTunnelDto.getExpTime() != null ? userTunnelDto.getExpTime() : user.getExpTime());
this.save(userTunnel); this.save(userTunnel);
return R.ok(); return R.ok();
} }
@Override
public R batchAssignUserTunnel(UserTunnelBatchAssignDto batchAssignDto) {
User user = userService.getById(batchAssignDto.getUserId());
if (user == null) return R.err("用户不存在");
List<Integer> requestedTunnelIds = batchAssignDto.getTunnels().stream()
.map(UserTunnelBatchAssignDto.TunnelAssignItem::getTunnelId)
.collect(Collectors.toList());
Set<Integer> existingTunnelIds = this.list(
new QueryWrapper<UserTunnel>()
.eq("user_id", batchAssignDto.getUserId())
.in("tunnel_id", requestedTunnelIds)
).stream().map(UserTunnel::getTunnelId).collect(Collectors.toSet());
List<UserTunnel> toSave = new ArrayList<>();
List<Integer> skippedIds = new ArrayList<>();
for (UserTunnelBatchAssignDto.TunnelAssignItem item : batchAssignDto.getTunnels()) {
if (existingTunnelIds.contains(item.getTunnelId())) {
skippedIds.add(item.getTunnelId());
continue;
}
UserTunnel ut = new UserTunnel();
ut.setUserId(batchAssignDto.getUserId());
ut.setTunnelId(item.getTunnelId());
ut.setSpeedId(item.getSpeedId());
ut.setStatus(1);
ut.setInFlow(0L);
ut.setOutFlow(0L);
ut.setFlow(user.getFlow());
ut.setNum(user.getNum());
ut.setFlowResetTime(user.getFlowResetTime());
ut.setExpTime(user.getExpTime());
toSave.add(ut);
}
if (toSave.isEmpty()) {
return R.err("所选隧道用户均已拥有权限");
}
this.saveBatch(toSave);
if (!skippedIds.isEmpty()) {
return R.ok("成功分配 " + toSave.size() + " 个隧道,跳过 " + skippedIds.size() + " 个已有权限的隧道");
}
return R.ok();
}
@Override @Override
public R getUserTunnelList(UserTunnelQueryDto queryDto) { public R getUserTunnelList(UserTunnelQueryDto queryDto) {
List<UserTunnelWithDetailDto> userTunnelWithDetails = this.baseMapper.getUserTunnelWithDetails(queryDto.getUserId()); List<UserTunnelWithDetailDto> userTunnelWithDetails = this.baseMapper.getUserTunnelWithDetails(queryDto.getUserId());
@@ -66,7 +120,9 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
UserTunnel userTunnel = this.getById(id); UserTunnel userTunnel = this.getById(id);
if (userTunnel == null) return R.err("未找到对应的用户隧道权限记录"); if (userTunnel == null) return R.err("未找到对应的用户隧道权限记录");
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", userTunnel.getUserId()).eq("tunnel_id", userTunnel.getTunnelId())); List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>()
.eq("user_id", userTunnel.getUserId())
.eq("tunnel_id", userTunnel.getTunnelId()));
for (Forward forward : forwardList) { for (Forward forward : forwardList) {
forwardService.deleteForward(forward.getId()); forwardService.deleteForward(forward.getId());
} }
@@ -87,7 +143,9 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
userTunnel.setSpeedId(updateDto.getSpeedId()); userTunnel.setSpeedId(updateDto.getSpeedId());
this.updateById(userTunnel); this.updateById(userTunnel);
if (speedChanged) { if (speedChanged) {
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().eq("user_id", userTunnel.getUserId()).eq("tunnel_id", userTunnel.getTunnelId())); List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>()
.eq("user_id", userTunnel.getUserId())
.eq("tunnel_id", userTunnel.getTunnelId()));
for (Forward forward : forwardList) { for (Forward forward : forwardList) {
ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto(); ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto();
forwardUpdateDto.setId(forward.getId()); forwardUpdateDto.setId(forward.getId());
@@ -98,7 +156,7 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
forwardService.updateForward(forwardUpdateDto); forwardService.updateForward(forwardUpdateDto);
} }
} }
return R.err("用户隧道权限更新失败"); return R.ok();
} }
private <T> void updateOptionalProperty(java.util.function.Consumer<T> setter, T value) { private <T> void updateOptionalProperty(java.util.function.Consumer<T> setter, T value) {
+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);
+121 -92
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) => {
const value = Array.from(keys)[0] as string;
setTunnelForm(prev => ({ ...prev, tunnelId: Number(value) || null, speedId: null }));
}}
>
{availableTunnels.map(tunnel => (
<SelectItem key={tunnel.id.toString()} textValue={tunnel.name}>
{tunnel.name}
</SelectItem>
))}
</Select>
<Select <div className="border rounded-lg divide-y dark:divide-gray-700 max-h-64 overflow-y-auto">
label="限速规则" {tunnels.map(tunnel => {
selectedKeys={tunnelForm.speedId ? [tunnelForm.speedId.toString()] : ["null"]} const isAssigned = isTunnelAssigned(tunnel.id);
onSelectionChange={(keys) => { const isSelected = batchTunnelSelections.has(tunnel.id);
const value = Array.from(keys)[0] as string; const tunnelSpeedLimits = getSpeedLimitsForTunnel(tunnel.id);
setTunnelForm(prev => ({ ...prev, speedId: value === "null" ? null : Number(value) }));
}}
isDisabled={!tunnelForm.tunnelId}
>
{[
<SelectItem key="null" textValue="不限速">不限速</SelectItem>,
...availableSpeedLimits.map(speedLimit => (
<SelectItem key={speedLimit.id.toString()} textValue={speedLimit.name}>
{speedLimit.name}
</SelectItem>
))
]}
</Select>
<Input return (
label="流量限制(GB)" <div
type="number" key={tunnel.id}
value={tunnelForm.flow.toString()} className={`p-3 flex items-center justify-between gap-4 ${isAssigned ? 'bg-gray-100 dark:bg-gray-800' : ''}`}
onChange={(e) => { >
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); <div className="flex items-center gap-3 flex-1">
setTunnelForm(prev => ({ ...prev, flow: value })); <input
}} type="checkbox"
min="1" checked={isSelected}
max="99999" disabled={isAssigned}
/> onChange={() => toggleTunnelSelection(tunnel.id)}
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-50"
/>
<span className={isAssigned ? 'text-gray-400' : ''}>
{tunnel.name}
</span>
{isAssigned && (
<Chip size="sm" variant="flat" color="default">已分配</Chip>
)}
</div>
<Input {isSelected && !isAssigned && (
label="转发数量" <Select
type="number" size="sm"
value={tunnelForm.num.toString()} label="限速规则"
onChange={(e) => { className="w-40"
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); selectedKeys={batchTunnelSelections.get(tunnel.id) !== null && batchTunnelSelections.get(tunnel.id) !== undefined ? [batchTunnelSelections.get(tunnel.id)!.toString()] : ["null"]}
setTunnelForm(prev => ({ ...prev, num: value })); onSelectionChange={(keys) => {
}} const value = Array.from(keys)[0] as string;
min="1" updateTunnelSpeedLimit(tunnel.id, value === "null" ? null : Number(value));
max="99999" }}
/> >
{[
<Select <SelectItem key="null" textValue="不限速">不限速</SelectItem>,
label="流量重置日期" ...tunnelSpeedLimits.map(sl => (
selectedKeys={[tunnelForm.flowResetTime.toString()]} <SelectItem key={sl.id.toString()} textValue={sl.name}>
onSelectionChange={(keys) => { {sl.name}
const value = Array.from(keys)[0] as string; </SelectItem>
setTunnelForm(prev => ({ ...prev, flowResetTime: Number(value) })); ))
}} ]}
> </Select>
<> )}
<SelectItem key="0" textValue="不重置"> </div>
不重置 );
</SelectItem> })}
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => ( {tunnels.length === 0 && (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}> <div className="p-4 text-center text-gray-500">暂无可用隧道</div>
每月{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;