修复部分浏览器判定为app的bug

This commit is contained in:
qaq
2025-08-21 21:52:04 +08:00
parent 07a0b9e3fa
commit 365387576f
13 changed files with 183 additions and 168 deletions
@@ -64,7 +64,6 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
NodeService nodeService; NodeService nodeService;
@Override @Override
public R createForward(ForwardDto forwardDto) { public R createForward(ForwardDto forwardDto) {
// 1. 获取当前用户信息 // 1. 获取当前用户信息
@@ -455,24 +454,23 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
return R.err("入口节点不存在"); return R.err("入口节点不存在");
} }
// 5. 解析目标地址,取第一个地址作为诊断目标
String[] remoteAddresses = forward.getRemoteAddr().split(",");
String targetAddress = remoteAddresses[0].trim();
// 提取IP和端口
String targetIp = extractIpFromAddress(targetAddress);
int targetPort = extractPortFromAddress(targetAddress);
if (targetIp == null || targetPort == -1) {
return R.err("无法解析目标地址: " + targetAddress);
}
List<DiagnosisResult> results = new ArrayList<>(); List<DiagnosisResult> results = new ArrayList<>();
String[] remoteAddresses = forward.getRemoteAddr().split(",");
// 6. 根据隧道类型执行不同的诊断策略 // 6. 根据隧道类型执行不同的诊断策略
if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) { if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) {
// 端口转发:入口节点直接TCP ping目标地址 // 端口转发:入口节点直接TCP ping目标地址
for (String remoteAddress : remoteAddresses) {
// 提取IP和端口
String targetIp = extractIpFromAddress(remoteAddress);
int targetPort = extractPortFromAddress(remoteAddress);
if (targetIp == null || targetPort == -1) {
return R.err("无法解析目标地址: " + remoteAddress);
}
DiagnosisResult result = performTcpPingDiagnosis(inNode, targetIp, targetPort, "转发->目标"); DiagnosisResult result = performTcpPingDiagnosis(inNode, targetIp, targetPort, "转发->目标");
results.add(result); results.add(result);
}
} else { } else {
// 隧道转发:入口TCP ping出口,出口TCP ping目标 // 隧道转发:入口TCP ping出口,出口TCP ping目标
Node outNode = nodeService.getNodeById(tunnel.getOutNodeId()); Node outNode = nodeService.getNodeById(tunnel.getOutNodeId());
@@ -485,10 +483,19 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
results.add(inToOutResult); results.add(inToOutResult);
// 出口TCP ping目标 // 出口TCP ping目标
for (String remoteAddress : remoteAddresses) {
// 提取IP和端口
String targetIp = extractIpFromAddress(remoteAddress);
int targetPort = extractPortFromAddress(remoteAddress);
if (targetIp == null || targetPort == -1) {
return R.err("无法解析目标地址: " + remoteAddress);
}
DiagnosisResult outToTargetResult = performTcpPingDiagnosis(outNode, targetIp, targetPort, "出口->目标"); DiagnosisResult outToTargetResult = performTcpPingDiagnosis(outNode, targetIp, targetPort, "出口->目标");
results.add(outToTargetResult); results.add(outToTargetResult);
} }
}
// 7. 构建诊断报告 // 7. 构建诊断报告
Map<String, Object> diagnosisReport = new HashMap<>(); Map<String, Object> diagnosisReport = new HashMap<>();
diagnosisReport.put("forwardId", id); diagnosisReport.put("forwardId", id);
@@ -645,8 +652,8 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
JSONObject tcpPingData = new JSONObject(); JSONObject tcpPingData = new JSONObject();
tcpPingData.put("ip", targetIp); tcpPingData.put("ip", targetIp);
tcpPingData.put("port", port); tcpPingData.put("port", port);
tcpPingData.put("count", 4); tcpPingData.put("count", 2);
tcpPingData.put("timeout", 5000); // 5秒超时 tcpPingData.put("timeout", 3000); // 5秒超时
// 发送TCP ping命令到节点 // 发送TCP ping命令到节点
GostDto gostResult = WebSocketServer.send_msg(node.getId(), tcpPingData, "TcpPing"); GostDto gostResult = WebSocketServer.send_msg(node.getId(), tcpPingData, "TcpPing");
@@ -1200,7 +1207,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
// 创建新远程服务 // 创建新远程服务
GostDto createResult = GostUtil.UpdateRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy(), interfaceName); GostDto createResult = GostUtil.UpdateRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy(), interfaceName);
if (createResult.getMsg().contains(GOST_NOT_FOUND_MSG)) { if (createResult.getMsg().contains(GOST_NOT_FOUND_MSG)) {
createResult = GostUtil.AddRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(),protocol, forward.getStrategy(), interfaceName); createResult = GostUtil.AddRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy(), interfaceName);
} }
return isGostOperationSuccess(createResult) ? R.ok() : R.err(createResult.getMsg()); return isGostOperationSuccess(createResult) ? R.ok() : R.err(createResult.getMsg());
} }
@@ -1378,17 +1385,17 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
public void updateForwardA(Forward forward) { public void updateForwardA(Forward forward) {
Tunnel tunnel = validateTunnel(forward.getTunnelId()); Tunnel tunnel = validateTunnel(forward.getTunnelId());
if (tunnel == null) { if (tunnel == null) {
return ; return;
} }
UserTunnel userTunnel = getUserTunnel(forward.getUserId(), tunnel.getId().intValue()); UserTunnel userTunnel = getUserTunnel(forward.getUserId(), tunnel.getId().intValue());
NodeInfo nodeInfo = getRequiredNodes(tunnel); NodeInfo nodeInfo = getRequiredNodes(tunnel);
if (nodeInfo.isHasError()) { if (nodeInfo.isHasError()) {
return ; return;
} }
Integer limiter; Integer limiter;
if (userTunnel == null) { if (userTunnel == null) {
limiter = null; limiter = null;
}else { } else {
limiter = userTunnel.getSpeedId(); limiter = userTunnel.getSpeedId();
} }
updateGostServices(forward, tunnel, limiter, nodeInfo, userTunnel); updateGostServices(forward, tunnel, limiter, nodeInfo, userTunnel);
+1 -1
View File
@@ -1 +1 @@
VITE_API_BASE=http://192.168.100.9:6365 VITE_API_BASE=http://192.168.31.126:6365
+10
View File
@@ -85,6 +85,16 @@
overscroll-behavior: none; overscroll-behavior: none;
} }
/* 使用动态视口单位,避免移动端地址栏导致的额外滚动 */
@supports (height: 100dvh) {
#root { min-height: 100dvh; }
}
/* iOS Safari 旧版本兼容 */
@supports (-webkit-touch-callout: none) {
#root { min-height: -webkit-fill-available; }
}
/* 防止加载时的闪烁 */ /* 防止加载时的闪烁 */
* { * {
box-sizing: border-box; box-sizing: border-box;
+3 -12
View File
@@ -1,15 +1,6 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { getPanelAddresses} from '@/utils/panel'; import { getPanelAddresses, isWebViewFunc} from '@/utils/panel';
// 检测是否在WebView中运行
const isWebView = (): boolean => {
if((window as any).JsInterface !== undefined) {
return true;
}else if((window as any).webkit && (window as any).webkit.messageHandlers) {
return true;
}else {
return false;
}
};
interface PanelAddress { interface PanelAddress {
name: string; name: string;
@@ -34,7 +25,7 @@ function getWebViewPanelAddress() {
let baseURL: string = ''; let baseURL: string = '';
export const reinitializeBaseURL = () => { export const reinitializeBaseURL = () => {
if (isWebView()) { if (isWebViewFunc()) {
getWebViewPanelAddress(); getWebViewPanelAddress();
} else { } else {
baseURL = import.meta.env.VITE_API_BASE ? `${import.meta.env.VITE_API_BASE}/api/v1/` : '/api/v1/'; baseURL = import.meta.env.VITE_API_BASE ? `${import.meta.env.VITE_API_BASE}/api/v1/` : '/api/v1/';
+2 -7
View File
@@ -5,6 +5,7 @@ import {
NavbarBrand, NavbarBrand,
NavbarContent, NavbarContent,
} from "@heroui/navbar"; } from "@heroui/navbar";
import { isWebViewFunc } from '@/utils/panel';
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Logo } from "@/components/icons"; import { Logo } from "@/components/icons";
@@ -18,13 +19,7 @@ export const Navbar = () => {
// 检测是否在WebView中运行 // 检测是否在WebView中运行
useEffect(() => { useEffect(() => {
if((window as any).JsInterface !== undefined) { setIsWebView(isWebViewFunc());
setIsWebView(true);
}else if((window as any).webkit && (window as any).webkit.messageHandlers) {
setIsWebView(true);
}else {
setIsWebView(false);
}
}, []); }, []);
+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.3.1"; const VERSION = "1.3.2";
const APP_VERSION = "1.0.1"; const APP_VERSION = "1.0.1";
const getInitialConfig = () => { const getInitialConfig = () => {
-11
View File
@@ -1,5 +1,3 @@
import { Link } from "@heroui/link";
import { Navbar } from "@/components/navbar"; import { Navbar } from "@/components/navbar";
export default function DefaultLayout({ export default function DefaultLayout({
@@ -13,15 +11,6 @@ export default function DefaultLayout({
<main className="container mx-auto max-w-7xl px-4 sm:px-6 flex-grow pt-4 sm:pt-16"> <main className="container mx-auto max-w-7xl px-4 sm:px-6 flex-grow pt-4 sm:pt-16">
{children} {children}
</main> </main>
<footer className="w-full flex items-center justify-center py-3">
<Link
isExternal
className="flex items-center gap-1 text-current"
href="https://heroui.com"
title="heroui.com homepage"
>
</Link>
</footer>
</div> </div>
); );
} }
+1 -1
View File
@@ -109,7 +109,7 @@ export default function ChangePasswordPage() {
return ( return (
<DefaultLayout> <DefaultLayout>
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10 min-h-[calc(100vh-200px)]"> <section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10 min-h-[calc(100dvh-200px)]">
<div className="w-full max-w-lg"> <div className="w-full max-w-lg">
<Card className="w-full"> <Card className="w-full">
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center"> <CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
+1 -1
View File
@@ -105,7 +105,7 @@ const CONFIG_ITEMS: ConfigItem[] = [
const getInitialConfigs = (): Record<string, string> => { const getInitialConfigs = (): Record<string, string> => {
if (typeof window === 'undefined') return {}; if (typeof window === 'undefined') return {};
const configKeys = ['app_name', 'captcha_enabled', 'captcha_type']; const configKeys = ['app_name', 'captcha_enabled', 'captcha_type', 'ip'];
const initialConfigs: Record<string, string> = {}; const initialConfigs: Record<string, string> = {};
try { try {
+4 -12
View File
@@ -5,7 +5,7 @@ import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import axios from 'axios'; import axios from 'axios';
import { isWebViewFunc } from '@/utils/panel';
import { siteConfig } from '@/config/site'; import { siteConfig } from '@/config/site';
import { title } from "@/components/primitives"; import { title } from "@/components/primitives";
import DefaultLayout from "@/layouts/default"; import DefaultLayout from "@/layouts/default";
@@ -65,13 +65,7 @@ export default function IndexPage() {
}, []); }, []);
// 检测是否在WebView中运行 // 检测是否在WebView中运行
useEffect(() => { useEffect(() => {
if((window as any).JsInterface !== undefined) { setIsWebView(isWebViewFunc());
setIsWebView(true);
}else if((window as any).webkit && (window as any).webkit.messageHandlers) {
setIsWebView(true);
}else {
setIsWebView(false);
}
}, []); }, []);
// 验证表单 // 验证表单
const validateForm = (): boolean => { const validateForm = (): boolean => {
@@ -259,7 +253,7 @@ export default function IndexPage() {
return ( return (
<DefaultLayout> <DefaultLayout>
<section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 pb-20 min-h-[calc(100vh-120px)] sm:min-h-[calc(100vh-200px)]"> <section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 pb-20 min-h-[calc(100dvh-120px)] sm:min-h-[calc(100dvh-200px)]">
<div className="w-full max-w-md px-4 sm:px-0"> <div className="w-full max-w-md px-4 sm:px-0">
<Card className="w-full"> <Card className="w-full">
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center"> <CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
@@ -311,7 +305,6 @@ export default function IndexPage() {
{/* 版权信息 - 固定在底部,不占据布局空间 */} {/* 版权信息 - 固定在底部,不占据布局空间 */}
{isWebView && (
<div className="fixed inset-x-0 bottom-4 text-center py-4"> <div className="fixed inset-x-0 bottom-4 text-center py-4">
<p className="text-xs text-gray-400 dark:text-gray-500"> <p className="text-xs text-gray-400 dark:text-gray-500">
Powered by{' '} Powered by{' '}
@@ -325,10 +318,9 @@ export default function IndexPage() {
</a> </a>
</p> </p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1"> <p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
v{siteConfig.app_version} v{ isWebView ? siteConfig.app_version : siteConfig.version}
</p> </p>
</div> </div>
)}
+23 -2
View File
@@ -5,10 +5,10 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure
import { Input } from "@heroui/input"; import { Input } from "@heroui/input";
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { isWebViewFunc } from '@/utils/panel';
import { siteConfig } from '@/config/site';
import { updatePassword } from '@/api'; import { updatePassword } from '@/api';
import { safeLogout } from '@/utils/logout'; import { safeLogout } from '@/utils/logout';
interface PasswordForm { interface PasswordForm {
newUsername: string; newUsername: string;
currentPassword: string; currentPassword: string;
@@ -16,6 +16,7 @@ interface PasswordForm {
confirmPassword: string; confirmPassword: string;
} }
interface MenuItem { interface MenuItem {
path: string; path: string;
label: string; label: string;
@@ -237,8 +238,28 @@ export default function ProfilePage() {
</CardBody> </CardBody>
</Card> </Card>
<div className="fixed inset-x-0 bottom-20 text-center py-4">
<p className="text-xs text-gray-400 dark:text-gray-500">
Powered by{' '}
<a
href="https://github.com/bqlpfy/flux-panel"
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
flux-panel
</a>
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
v{ isWebViewFunc() ? siteConfig.app_version : siteConfig.version}
</p>
</div> </div>
</div>
{/* 修改密码弹窗 */} {/* 修改密码弹窗 */}
<Modal <Modal
+2 -2
View File
@@ -77,9 +77,9 @@ export const SettingsPage = () => {
}, []); }, []);
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900"> <div className="min-h-screen bg-gray-50 dark:bg-black">
{/* 顶部导航 */} {/* 顶部导航 */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> <div className="bg-white dark:bg-black border-b border-gray-200 dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 py-4"> <div className="max-w-4xl mx-auto px-4 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
+10
View File
@@ -38,6 +38,16 @@ export async function deletePanelAddress(name: string){
} }
} }
export function isWebViewFunc(){
if((window as any).JsInterface !== undefined && (window as any).JsInterface.getPanelAddresses !== undefined) {
return true;
}else if((window as any).webkit && (window as any).webkit.messageHandlers && (window as any).webkit.messageHandlers.getPanelAddresses !== undefined) {
return true;
}else {
return false;
}
}
// 验证面板地址格式 // 验证面板地址格式
export function validatePanelAddress(address: string): boolean { export function validatePanelAddress(address: string): boolean {
try { try {