This commit is contained in:
qaq
2025-08-15 12:25:06 +08:00
parent 23ad5ef68b
commit 6f80e26a8f
32 changed files with 98 additions and 32 deletions
+2 -1
View File
@@ -94,7 +94,8 @@ CREATE TABLE `statistics_flow` (
`user_id` int(10) NOT NULL, `user_id` int(10) NOT NULL,
`flow` bigint(20) NOT NULL, `flow` bigint(20) NOT NULL,
`total_flow` bigint(20) NOT NULL, `total_flow` bigint(20) NOT NULL,
`time` varchar(100) NOT NULL `time` varchar(100) NOT NULL,
`created_time` bigint(20) NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- -------------------------------------------------------- -- --------------------------------------------------------
+24
View File
@@ -809,9 +809,33 @@ CREATE TABLE IF NOT EXISTS \`statistics_flow\` (
\`flow\` bigint(20) NOT NULL, \`flow\` bigint(20) NOT NULL,
\`total_flow\` bigint(20) NOT NULL, \`total_flow\` bigint(20) NOT NULL,
\`time\` varchar(100) NOT NULL, \`time\` varchar(100) NOT NULL,
\`created_time\` bigint(20) NOT NULL,
PRIMARY KEY (\`id\`) PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- statistics_flow 表:添加 created_time 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'statistics_flow'
AND column_name = 'created_time'
),
'ALTER TABLE \`statistics_flow\` ADD COLUMN \`created_time\` BIGINT(20) NOT NULL DEFAULT 0 COMMENT "创建时间毫秒时间戳";',
'SELECT "Column \`created_time\` already exists in \`statistics_flow\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有记录设置当前毫秒时间戳(仅当 created_time 为 0 或 NULL 时)
UPDATE \`statistics_flow\`
SET \`created_time\` = UNIX_TIMESTAMP() * 1000
WHERE \`created_time\` = 0 OR \`created_time\` IS NULL;
@@ -16,6 +16,7 @@ import javax.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
@@ -33,6 +34,19 @@ public class StatisticsFlowAsync {
public void statistics_flow() { public void statistics_flow() {
LocalDateTime currentHour = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0); LocalDateTime currentHour = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
String hourString = currentHour.format(DateTimeFormatter.ofPattern("HH:mm")); String hourString = currentHour.format(DateTimeFormatter.ofPattern("HH:mm"));
long time = new Date().getTime();
// 删除48小时前的数据
long nowMs = System.currentTimeMillis();
long cutoffMs = nowMs - 48L * 60 * 60 * 1000;
statisticsFlowService.remove(
new LambdaQueryWrapper<StatisticsFlow>()
.lt(StatisticsFlow::getCreateTime, cutoffMs)
);
List<User> list = userService.list(); List<User> list = userService.list();
List<StatisticsFlow> statisticsFlowList = new ArrayList<>(); List<StatisticsFlow> statisticsFlowList = new ArrayList<>();
@@ -65,6 +79,7 @@ public class StatisticsFlowAsync {
statisticsFlow.setFlow(incrementFlow); statisticsFlow.setFlow(incrementFlow);
statisticsFlow.setTotalFlow(currentTotalFlow); statisticsFlow.setTotalFlow(currentTotalFlow);
statisticsFlow.setTime(hourString); statisticsFlow.setTime(hourString);
statisticsFlow.setCreateTime(time);
statisticsFlowList.add(statisticsFlow); statisticsFlowList.add(statisticsFlow);
} }
@@ -4,11 +4,15 @@ import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.resource.FontCache; import cloud.tianai.captcha.resource.FontCache;
import cloud.tianai.captcha.resource.ResourceStore; import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource; import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import static cloud.tianai.captcha.common.constant.CommonConstant.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class CaptchaResourceConfiguration { public class CaptchaResourceConfiguration {
@@ -22,16 +26,24 @@ public class CaptchaResourceConfiguration {
@PostConstruct @PostConstruct
public void init() { public void init() {
// 添加自定义背景图片
for (int i = 1; i <= 10; i++) {
ResourceMap template = new ResourceMap("default", 4);
template.put("active.png", new Resource(ClassPathResourceProvider.NAME, "slide/" + i + "/1.png"));
template.put("fixed.png", new Resource(ClassPathResourceProvider.NAME, "slide/" + i + "/2.png"));
resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template);
}
// 添加自定义背景图片 // 添加自定义背景图片
for (String option : OPTIONS) { for (String option : OPTIONS) {
for (int i = 1; i <= 26; i++) { for (int i = 1; i <= 26; i++) {
resourceStore.addResource(option, new Resource("classpath", "bgimages/"+i + ".jpg", "default")); resourceStore.addResource(option, new Resource("classpath", "bgimages/" + i + ".jpg", "default"));
} }
} }
//添加自定义字体 //添加自定义字体
resourceStore.addResource(FontCache.FONT_TYPE,new Resource("classpath", "fonts/SIMSUN.TTC", "default")); resourceStore.addResource(FontCache.FONT_TYPE, new Resource("classpath", "fonts/SIMSUN.TTC", "default"));
} }
} }
@@ -28,5 +28,7 @@ public class StatisticsFlow {
private String time; private String time;
private Long createTime;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
// 缓存相关常量 // 缓存相关常量
const CACHE_PREFIX = 'vite_config_'; const CACHE_PREFIX = 'vite_config_';
const VERSION = "1.2.8"; const VERSION = "1.2.9";
const getInitialConfig = () => { const getInitialConfig = () => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+9 -2
View File
@@ -63,6 +63,7 @@ export default function DashboardPage() {
const [userTunnels, setUserTunnels] = useState<UserTunnel[]>([]); const [userTunnels, setUserTunnels] = useState<UserTunnel[]>([]);
const [forwardList, setForwardList] = useState<Forward[]>([]); const [forwardList, setForwardList] = useState<Forward[]>([]);
const [statisticsFlows, setStatisticsFlows] = useState<StatisticsFlow[]>([]); const [statisticsFlows, setStatisticsFlows] = useState<StatisticsFlow[]>([]);
const [isAdmin, setIsAdmin] = useState(false);
const [addressModalOpen, setAddressModalOpen] = useState(false); const [addressModalOpen, setAddressModalOpen] = useState(false);
const [addressModalTitle, setAddressModalTitle] = useState(''); const [addressModalTitle, setAddressModalTitle] = useState('');
@@ -166,6 +167,10 @@ export default function DashboardPage() {
setForwardList([]); setForwardList([]);
setStatisticsFlows([]); setStatisticsFlows([]);
// 检查用户是否是管理员
const adminStatus = localStorage.getItem('admin');
setIsAdmin(adminStatus === 'true');
loadPackageData(); loadPackageData();
localStorage.setItem('e', '/dashboard'); localStorage.setItem('e', '/dashboard');
}, []); }, []);
@@ -751,8 +756,9 @@ export default function DashboardPage() {
</CardBody> </CardBody>
</Card> </Card>
{/* 隧道权限 */} {/* 隧道权限 - 管理员不显示 */}
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md"> {!isAdmin && (
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<svg className="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
@@ -828,6 +834,7 @@ export default function DashboardPage() {
)} )}
</CardBody> </CardBody>
</Card> </Card>
)}
{/* 转发配置 */} {/* 转发配置 */}
<Card className="border border-gray-200 dark:border-default-200 shadow-md"> <Card className="border border-gray-200 dark:border-default-200 shadow-md">
+23 -6
View File
@@ -10,6 +10,7 @@ import DefaultLayout from "@/layouts/default";
import { login, LoginData, checkCaptcha } from "@/api"; import { login, LoginData, checkCaptcha } from "@/api";
import "@/utils/tac.css"; import "@/utils/tac.css";
import "@/utils/tac.min.js"; import "@/utils/tac.min.js";
import bgImage from "@/images/bg.jpg";
interface LoginForm { interface LoginForm {
@@ -31,6 +32,8 @@ interface CaptchaConfig {
} }
interface CaptchaStyle { interface CaptchaStyle {
btnUrl?: string;
bgUrl?: string;
logoUrl?: string | null; logoUrl?: string | null;
moveTrackMaskBgColor?: string; moveTrackMaskBgColor?: string;
moveTrackMaskBorderColor?: string; moveTrackMaskBorderColor?: string;
@@ -117,7 +120,6 @@ export default function IndexPage() {
performLogin(); performLogin();
}, },
validFail: (_: any, _captcha: any, tac: any) => { validFail: (_: any, _captcha: any, tac: any) => {
toast.error("验证码验证失败,请重试");
tac.reloadCaptcha(); tac.reloadCaptcha();
}, },
btnCloseFun: (_event: any, tac: any) => { btnCloseFun: (_event: any, tac: any) => {
@@ -130,10 +132,19 @@ export default function IndexPage() {
} }
}; };
// 检测暗黑模式
const isDarkMode = document.documentElement.classList.contains('dark') ||
document.documentElement.getAttribute('data-theme') === 'dark' ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
// 根据主题调整颜色
const trackColor = isDarkMode ? "#4a5568" : "#7db0be"; // 暗黑模式使用更深的灰蓝色
const style: CaptchaStyle = { const style: CaptchaStyle = {
logoUrl: null, // 去除默认logo bgUrl: bgImage,
moveTrackMaskBgColor: "#0298f8", logoUrl: null,
moveTrackMaskBorderColor: "#0298f8" moveTrackMaskBgColor: trackColor,
moveTrackMaskBorderColor: trackColor
}; };
tacInstanceRef.current = new window.TAC(config, style); tacInstanceRef.current = new window.TAC(config, style);
@@ -289,14 +300,20 @@ export default function IndexPage() {
{/* 验证码弹层 */} {/* 验证码弹层 */}
{showCaptcha && ( {showCaptcha && (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
{/* 背景遮罩层 - 模糊效果 */} {/* 背景遮罩层 - 模糊效果,暗黑模式下更深 */}
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm captcha-backdrop-enter" /> <div className="absolute inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm captcha-backdrop-enter" />
{/* 验证码容器 */} {/* 验证码容器 */}
<div className="mb-4"> <div className="mb-4">
<div <div
id="captcha-container" id="captcha-container"
ref={captchaContainerRef} ref={captchaContainerRef}
className="w-full flex justify-center" className="w-full flex justify-center"
style={{
filter: document.documentElement.classList.contains('dark') ||
document.documentElement.getAttribute('data-theme') === 'dark' ||
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'brightness(0.8) contrast(0.9)' : 'none'
}}
/> />
</div> </div>
</div> </div>
+1 -13
View File
@@ -879,21 +879,9 @@ export default function NodePage() {
<Alert <Alert
color="primary" color="primary"
variant="flat" variant="flat"
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址" description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址。入口ip是用于展示在转发页面,面向用户的访问地址。实在理解不到说明你没这个需求,都填节点的服务器ip就行!"
className="mt-4" className="mt-4"
/> />
<Alert
color="primary"
variant="flat"
description="入口ip是用于展示在转发页面,面向用户的访问地址"
className="mt-4"
/>
<Alert
color="primary"
variant="flat"
description="实在理解不到说明你没这个需求,都填节点的服务器ip就行"
className="mt-4"
/>
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
+6 -6
View File
@@ -100,8 +100,8 @@ export default function TunnelPage() {
inNodeId: null, inNodeId: null,
outNodeId: null, outNodeId: null,
protocol: 'tls', protocol: 'tls',
tcpListenAddr: '0.0.0.0', tcpListenAddr: '[::]',
udpListenAddr: '0.0.0.0', udpListenAddr: '[::]',
interfaceName: '', interfaceName: '',
flow: 1, flow: 1,
trafficRatio: 1.0, trafficRatio: 1.0,
@@ -195,8 +195,8 @@ export default function TunnelPage() {
inNodeId: null, inNodeId: null,
outNodeId: null, outNodeId: null,
protocol: 'tls', protocol: 'tls',
tcpListenAddr: '0.0.0.0', tcpListenAddr: '[::]',
udpListenAddr: '0.0.0.0', udpListenAddr: '[::]',
interfaceName: '', interfaceName: '',
flow: 1, flow: 1,
trafficRatio: 1.0, trafficRatio: 1.0,
@@ -216,8 +216,8 @@ export default function TunnelPage() {
inNodeId: tunnel.inNodeId, inNodeId: tunnel.inNodeId,
outNodeId: tunnel.outNodeId || null, outNodeId: tunnel.outNodeId || null,
protocol: tunnel.protocol || 'tls', protocol: tunnel.protocol || 'tls',
tcpListenAddr: tunnel.tcpListenAddr || '0.0.0.0', tcpListenAddr: tunnel.tcpListenAddr || '[::]',
udpListenAddr: tunnel.udpListenAddr || '0.0.0.0', udpListenAddr: tunnel.udpListenAddr || '[::]',
interfaceName: tunnel.interfaceName || '', interfaceName: tunnel.interfaceName || '',
flow: tunnel.flow, flow: tunnel.flow,
trafficRatio: tunnel.trafficRatio, trafficRatio: tunnel.trafficRatio,
+1 -1
View File
@@ -988,7 +988,7 @@ export default function UserPage() {
aria-label="用户隧道权限列表" aria-label="用户隧道权限列表"
classNames={{ classNames={{
wrapper: "shadow-none", wrapper: "shadow-none",
th: "bg-gray-50 text-gray-700 font-medium" th: "bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 font-medium"
}} }}
> >
<TableHeader> <TableHeader>