优化显示,新增流量重置

This commit is contained in:
qaq
2025-08-04 17:13:38 +08:00
parent 8ffb9218c4
commit 614d147032
13 changed files with 369 additions and 47 deletions
BIN
View File
Binary file not shown.
+2
View File
@@ -172,6 +172,8 @@ get_config_params() {
echo "🔧 请输入配置参数:"
echo "📡 节点端服务器和面板通信的地址,需要能正常访问(IPv6不需要加[])"
echo "不要套CDN,不支持HTTPS,直接输入面板服务器的ip"
echo "用于节点和面板通讯的地址,节点和面板通讯内容有加密!!!!!"
while true; do
read -p "当前面板服务器地址: " SERVER_HOST
if [ -n "$SERVER_HOST" ]; then
@@ -0,0 +1,16 @@
package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
public class ResetFlowDto {
@NotNull(message = "重置账号id不能为空")
private Integer id;
@NotNull(message = "重置类型不能为空")
private Integer type;
}
@@ -3,11 +3,7 @@ package com.admin.controller;
import com.admin.common.aop.LogAnnotation;
import com.admin.common.annotation.RequireRole;
import com.admin.common.dto.ChangePasswordDto;
import com.admin.common.dto.LoginDto;
import com.admin.common.dto.PageDto;
import com.admin.common.dto.UserDto;
import com.admin.common.dto.UserUpdateDto;
import com.admin.common.dto.*;
import com.admin.common.lang.R;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -78,4 +74,14 @@ public class UserController extends BaseController {
public R updatePassword(@Validated @RequestBody ChangePasswordDto changePasswordDto) {
return userService.updatePassword(changePasswordDto);
}
@LogAnnotation
@RequireRole
@PostMapping("/reset")
public R reset(@Validated @RequestBody ResetFlowDto resetFlowDto) {
return userService.reset(resetFlowDto);
}
}
@@ -1,10 +1,6 @@
package com.admin.service;
import com.admin.common.dto.ChangePasswordDto;
import com.admin.common.dto.LoginDto;
import com.admin.common.dto.PageDto;
import com.admin.common.dto.UserDto;
import com.admin.common.dto.UserUpdateDto;
import com.admin.common.dto.*;
import com.admin.common.lang.R;
import com.admin.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;
@@ -32,4 +28,6 @@ public interface UserService extends IService<User> {
R getUserPackageInfo();
R updatePassword(ChangePasswordDto changePasswordDto);
R reset(ResetFlowDto resetFlowDto);
}
@@ -2,13 +2,7 @@ package com.admin.service.impl;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.admin.common.dto.ChangePasswordDto;
import com.admin.common.dto.LoginDto;
import com.admin.common.dto.PageDto;
import com.admin.common.dto.UserDto;
import com.admin.common.dto.UserUpdateDto;
import com.admin.common.dto.UserPackageDto;
import com.admin.common.dto.GostDto;
import com.admin.common.dto.*;
import com.admin.common.lang.R;
import com.admin.common.task.DelayQueueManager;
import com.admin.common.task.DelayTask;
@@ -351,6 +345,24 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
}
}
@Override
public R reset(ResetFlowDto resetFlowDto) {
if (resetFlowDto.getType() == 1){ // 清零账号流量
User user = this.getById(resetFlowDto.getId());
if (user == null) return R.err(ERROR_USER_NOT_FOUND);
user.setInFlow(0L);
user.setOutFlow(0L);
this.updateById(user);
}else { // 清零隧道流量
UserTunnel tunnel = userTunnelService.getById(resetFlowDto.getId());
if (tunnel == null) return R.err("隧道不存在");
tunnel.setInFlow(0L);
tunnel.setOutFlow(0L);
userTunnelService.updateById(tunnel);
}
return R.ok();
}
// ========== 私有辅助方法 ==========
/**
@@ -691,7 +703,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
UserPackageDto.UserInfoDto userInfo = buildUserInfoDto(user);
// 2. 获取隧道权限详情
List<UserPackageDto.UserTunnelDetailDto> tunnelPermissions = getTunnelPermissions(user.getId(), roleId);
List<UserPackageDto.UserTunnelDetailDto> tunnelPermissions = getTunnelPermissions(user.getId());
// 3. 获取转发详情
List<UserPackageDto.UserForwardDetailDto> forwards = userMapper.getUserForwardDetails(user.getId().intValue());
@@ -731,15 +743,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* 获取隧道权限详情
*
* @param userId 用户ID
* @param roleId 角色ID
* @return 隧道权限详情列表
*/
private List<UserPackageDto.UserTunnelDetailDto> getTunnelPermissions(Long userId, Integer roleId) {
if (roleId != null && roleId == ADMIN_ROLE_ID) {
return userMapper.getAllTunnelsForAdmin();
} else {
return userMapper.getUserTunnelDetails(userId.intValue());
}
private List<UserPackageDto.UserTunnelDetailDto> getTunnelPermissions(Long userId) {
return userMapper.getUserTunnelDetails(userId.intValue());
}
/**
+3
View File
@@ -72,6 +72,9 @@ export const deleteSpeedLimit = (id: number) => Network.post("/speed-limit/delet
// 修改密码接口
export const updatePassword = (data: any) => Network.post("/user/updatePassword", data);
// 重置流量接口
export const resetUserFlow = (data: { id: number; type: number }) => Network.post("/user/reset", data);
// 网站配置相关接口
export const getConfigs = () => Network.post("/config/list");
export const getConfigByName = (name: string) => Network.post("/config/get", { name });
+1 -1
View File
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
// 缓存相关常量
const CACHE_PREFIX = 'vite_config_';
const VERSION = "1.2.0";
const VERSION = "1.2.1";
// 缓存策略:
// - 长期有效,不设置过期时间
+2 -2
View File
@@ -774,11 +774,11 @@ export default function DashboardPage() {
<div className="pt-2 border-t border-gray-200 dark:border-default-200">
<div className="grid grid-cols-3 gap-1 text-xs">
<div className="text-center">
<div className="text-default-500 mb-1">入站</div>
<div className="text-default-500 mb-1">上传</div>
<div className="font-medium text-green-600 dark:text-green-400 truncate">{formatFlow(forward.inFlow || 0)}</div>
</div>
<div className="text-center">
<div className="text-default-500 mb-1">出站</div>
<div className="text-default-500 mb-1">下载</div>
<div className="font-medium text-orange-600 dark:text-orange-400 truncate">{formatFlow(forward.outFlow || 0)}</div>
</div>
<div className="text-center">
+8 -7
View File
@@ -760,14 +760,15 @@ export default function ForwardPage() {
<Chip color={strategyDisplay.color as any} variant="flat" size="sm" className="text-xs">
{strategyDisplay.text}
</Chip>
<Chip variant="flat" size="sm" className="text-xs">
{formatFlow((forward.inFlow || 0) + (forward.outFlow || 0))}
</Chip>
{forward.userName && (
<Chip variant="flat" size="sm" className="text-xs">
{forward.userName}
<div className="flex items-center gap-1">
<Chip variant="flat" size="sm" className="text-xs" color="primary">
↑{formatFlow(forward.inFlow || 0)}
</Chip>
</div>
<Chip variant="flat" size="sm" className="text-xs" color="success">
↓{formatFlow(forward.outFlow || 0)}
</Chip>
)}
</div>
</div>
+59
View File
@@ -284,6 +284,34 @@ export default function NodePage() {
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 格式化开机时间
const formatUptime = (seconds: number): string => {
if (seconds === 0) return '-';
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}天${hours}小时`;
} else if (hours > 0) {
return `${hours}小时${minutes}分钟`;
} else {
return `${minutes}分钟`;
}
};
// 格式化流量
const formatTraffic = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 获取进度条颜色
const getProgressColor = (value: number, offline = false): "default" | "primary" | "secondary" | "success" | "warning" | "danger" => {
if (offline) return "default";
@@ -633,6 +661,15 @@ export default function NodePage() {
<span className="text-default-600">版本</span>
<span className="text-xs">{node.version || '未知'}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-default-600">开机时间</span>
<span className="text-xs">
{node.connectionStatus === 'online' && node.systemInfo
? formatUptime(node.systemInfo.uptime)
: '-'
}
</span>
</div>
</div>
{/* 系统监控 */}
@@ -700,6 +737,28 @@ export default function NodePage() {
</div>
</div>
</div>
{/* 流量统计 */}
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="text-center p-2 bg-primary-50 dark:bg-primary-100/20 rounded border border-primary-200 dark:border-primary-300/20">
<div className="text-primary-600 dark:text-primary-400 mb-0.5">↑ 上行流量</div>
<div className="font-mono text-primary-700 dark:text-primary-300">
{node.connectionStatus === 'online' && node.systemInfo
? formatTraffic(node.systemInfo.uploadTraffic)
: '-'
}
</div>
</div>
<div className="text-center p-2 bg-success-50 dark:bg-success-100/20 rounded border border-success-200 dark:border-success-300/20">
<div className="text-success-600 dark:text-success-400 mb-0.5">↓ 下行流量</div>
<div className="font-mono text-success-700 dark:text-success-300">
{node.connectionStatus === 'online' && node.systemInfo
? formatTraffic(node.systemInfo.downloadTraffic)
: '-'
}
</div>
</div>
</div>
</div>
{/* 操作按钮 */}
-6
View File
@@ -518,23 +518,17 @@ export default function TunnelPage() {
{/* 配置信息 */}
<div className="flex justify-between items-center pt-2 border-t border-divider">
<div className="text-left">
<div className="text-xs text-default-500">流量计算</div>
<div className="text-xs font-medium text-foreground">
{getFlowDisplay(tunnel.flow)}
</div>
</div>
<div className="text-right">
<div className="text-xs text-default-500">倍率</div>
<div className="text-xs font-medium text-foreground">
{tunnel.trafficRatio}x
</div>
</div>
</div>
{/* 时间信息 */}
<div className="text-xs text-default-500 pt-1 border-t border-divider">
创建时间: {formatTime(tunnel.createdTime)}
</div>
</div>
<div className="flex gap-1.5 mt-3">
+242 -6
View File
@@ -46,7 +46,8 @@ import {
getUserTunnelList,
removeUserTunnel,
updateUserTunnel,
getSpeedLimitList
getSpeedLimitList,
resetUserFlow
} from '@/api';
import { SearchIcon, PlusIcon, EditIcon, DeleteIcon, UserIcon, SettingsIcon } from '@/components/icons';
import { parseDate } from "@internationalized/date";
@@ -157,6 +158,16 @@ export default function UserPage() {
const { isOpen: isDeleteTunnelModalOpen, onOpen: onDeleteTunnelModalOpen, onClose: onDeleteTunnelModalClose } = useDisclosure();
const [tunnelToDelete, setTunnelToDelete] = useState<UserTunnel | null>(null);
// 重置流量确认相关状态
const { isOpen: isResetFlowModalOpen, onOpen: onResetFlowModalOpen, onClose: onResetFlowModalClose } = useDisclosure();
const [userToReset, setUserToReset] = useState<User | null>(null);
const [resetFlowLoading, setResetFlowLoading] = useState(false);
// 重置隧道流量确认相关状态
const { isOpen: isResetTunnelFlowModalOpen, onOpen: onResetTunnelFlowModalOpen, onClose: onResetTunnelFlowModalClose } = useDisclosure();
const [tunnelToReset, setTunnelToReset] = useState<UserTunnel | null>(null);
const [resetTunnelFlowLoading, setResetTunnelFlowLoading] = useState(false);
// 其他数据
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
const [speedLimits, setSpeedLimits] = useState<SpeedLimit[]>([]);
@@ -444,6 +455,70 @@ export default function UserPage() {
}
};
// 重置流量相关函数
const handleResetFlow = (user: User) => {
setUserToReset(user);
onResetFlowModalOpen();
};
const handleConfirmResetFlow = async () => {
if (!userToReset) return;
setResetFlowLoading(true);
try {
const response = await resetUserFlow({
id: userToReset.id,
type: 1 // 1表示重置用户流量
});
if (response.code === 0) {
toast.success('流量重置成功');
onResetFlowModalClose();
setUserToReset(null);
loadUsers(); // 重新加载用户列表
} else {
toast.error(response.msg || '重置失败');
}
} catch (error) {
toast.error('重置失败');
} finally {
setResetFlowLoading(false);
}
};
// 隧道流量重置相关函数
const handleResetTunnelFlow = (userTunnel: UserTunnel) => {
setTunnelToReset(userTunnel);
onResetTunnelFlowModalOpen();
};
const handleConfirmResetTunnelFlow = async () => {
if (!tunnelToReset) return;
setResetTunnelFlowLoading(true);
try {
const response = await resetUserFlow({
id: tunnelToReset.id,
type: 2 // 2表示重置隧道流量
});
if (response.code === 0) {
toast.success('隧道流量重置成功');
onResetTunnelFlowModalClose();
setTunnelToReset(null);
if (currentUser) {
loadUserTunnels(currentUser.id); // 重新加载隧道权限列表
}
} else {
toast.error(response.msg || '重置失败');
}
} catch (error) {
toast.error('重置失败');
} finally {
setResetTunnelFlowLoading(false);
}
};
// 过滤数据
const availableTunnels = tunnels.filter(
tunnel => !userTunnels.some(ut => ut.tunnelId === tunnel.id)
@@ -595,15 +670,16 @@ export default function UserPage() {
<div className="flex justify-between text-sm">
<span className="text-default-600">过期时间</span>
<div className="text-right">
<div className="text-xs">{formatDate(user.expTime)}</div>
{expStatus && (
{expStatus && expStatus.color === 'success' ? (
<div className="text-xs">{formatDate(user.expTime)}</div>
) : (
<Chip
color={expStatus.color}
color={expStatus?.color || 'default'}
variant="flat"
size="sm"
className="text-xs mt-0.5"
className="text-xs"
>
{expStatus.text}
{expStatus?.text || '未知状态'}
</Chip>
)}
</div>
@@ -623,6 +699,20 @@ export default function UserPage() {
>
编辑
</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"
@@ -959,6 +1049,18 @@ export default function UserPage() {
>
<EditIcon className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="flat"
color="warning"
isIconOnly
onClick={() => handleResetTunnelFlow(userTunnel)}
title="重置流量"
>
<svg className="w-4 h-4" 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"
@@ -1179,6 +1281,140 @@ export default function UserPage() {
</ModalFooter>
</ModalContent>
</Modal>
{/* 重置流量确认对话框 */}
<Modal
isOpen={isResetFlowModalOpen}
onClose={onResetFlowModalClose}
size="md"
scrollBehavior="outside"
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">
确认重置流量
</ModalHeader>
<ModalBody>
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-warning-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-warning" 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>
</div>
<div className="flex-1">
<p className="text-foreground">
确定要重置用户 <span className="font-semibold text-warning">"{userToReset?.user}"</span> 的流量吗?
</p>
<p className="text-small text-default-500 mt-1">
该操作只会重置账号流量不会重置隧道权限流量,重置后该用户的上下行流量将归零,此操作不可撤销。
</p>
<div className="mt-2 p-2 bg-warning-50 dark:bg-warning-100/10 rounded text-xs">
<div className="text-warning-700 dark:text-warning-300">
当前流量使用情况:
</div>
<div className="mt-1 space-y-1">
<div className="flex justify-between">
<span>上行流量:</span>
<span className="font-mono">{userToReset ? formatFlow(userToReset.inFlow || 0) : '-'}</span>
</div>
<div className="flex justify-between">
<span>下行流量:</span>
<span className="font-mono">{userToReset ? formatFlow(userToReset.outFlow || 0) : '-'}</span>
</div>
<div className="flex justify-between font-medium">
<span>总计:</span>
<span className="font-mono text-warning-700 dark:text-warning-300">
{userToReset ? formatFlow(calculateUserTotalUsedFlow(userToReset)) : '-'}
</span>
</div>
</div>
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button
variant="light"
onPress={onResetFlowModalClose}
>
取消
</Button>
<Button
color="warning"
onPress={handleConfirmResetFlow}
isLoading={resetFlowLoading}
>
确认重置
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* 重置隧道流量确认对话框 */}
<Modal
isOpen={isResetTunnelFlowModalOpen}
onClose={onResetTunnelFlowModalClose}
size="md"
scrollBehavior="outside"
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">
确认重置隧道流量
</ModalHeader>
<ModalBody>
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-warning-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-warning" 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>
</div>
<div className="flex-1">
<p className="text-foreground">
确定要重置用户 <span className="font-semibold">{currentUser?.user}</span> 对隧道 <span className="font-semibold text-warning">"{tunnelToReset?.tunnelName}"</span> 的流量吗?
</p>
<p className="text-small text-default-500 mt-1">
该操作只会重置隧道权限流量不会重置账号流量,重置后该隧道权限的上下行流量将归零,此操作不可撤销。
</p>
<div className="mt-2 p-2 bg-warning-50 dark:bg-warning-100/10 rounded text-xs">
<div className="text-warning-700 dark:text-warning-300">
当前流量使用情况:
</div>
<div className="mt-1 space-y-1">
<div className="flex justify-between">
<span>上行流量:</span>
<span className="font-mono">{tunnelToReset ? formatFlow(tunnelToReset.inFlow || 0) : '-'}</span>
</div>
<div className="flex justify-between">
<span>下行流量:</span>
<span className="font-mono">{tunnelToReset ? formatFlow(tunnelToReset.outFlow || 0) : '-'}</span>
</div>
<div className="flex justify-between font-medium">
<span>总计:</span>
<span className="font-mono text-warning-700 dark:text-warning-300">
{tunnelToReset ? formatFlow(calculateTunnelUsedFlow(tunnelToReset)) : '-'}
</span>
</div>
</div>
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button
variant="light"
onPress={onResetTunnelFlowModalClose}
>
取消
</Button>
<Button
color="warning"
onPress={handleConfirmResetTunnelFlow}
isLoading={resetTunnelFlowLoading}
>
确认重置
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
</AdminLayout>
);