diff --git a/.gitignore b/.gitignore index a243df7..3344bb8 100644 --- a/.gitignore +++ b/.gitignore @@ -255,3 +255,4 @@ gitee/ *.crt *.cert android-app/ +ios-app/ diff --git a/vite-frontend/.env.development b/vite-frontend/.env.development index 2e92b25..de0bb1f 100644 --- a/vite-frontend/.env.development +++ b/vite-frontend/.env.development @@ -1 +1 @@ -VITE_API_BASE=http://127.0.0.1:6365 +VITE_API_BASE=http://192.168.100.9:6365 diff --git a/vite-frontend/index.html b/vite-frontend/index.html index 6619d89..5f830ed 100644 --- a/vite-frontend/index.html +++ b/vite-frontend/index.html @@ -67,6 +67,9 @@ background-color: #ffffff; color: #000000; transition: background-color 0.2s ease, color 0.2s ease; + /* 禁用弹性滚动 */ + overscroll-behavior: none; + -webkit-overflow-scrolling: touch; } /* 暗色主题样式 */ @@ -78,6 +81,8 @@ #root { min-height: 100vh; background-color: inherit; + /* 确保根元素也禁用弹性滚动 */ + overscroll-behavior: none; } /* 防止加载时的闪烁 */ @@ -88,6 +93,8 @@ /* 确保在主题切换时平滑过渡 */ html { transition: color-scheme 0.2s ease; + /* 禁用整个页面的弹性滚动 */ + overscroll-behavior: none; } /* 防止布局切换时的闪屏 */ @@ -106,6 +113,19 @@ .pc-mode.dark #root { background-color: #000000; } + + /* 移动端滚动优化 */ + @media (max-width: 768px) { + html, body { + overscroll-behavior: none; + -webkit-overflow-scrolling: touch; + } + + /* 确保固定定位的元素不受滚动影响 */ + .fixed { + overscroll-behavior: none; + } + } { }; // 简化的路由保护组件 - 使用 React Router 导航避免循环 -const ProtectedRoute = ({ children, useSimpleLayout = false }: { children: React.ReactNode, useSimpleLayout?: boolean }) => { +const ProtectedRoute = ({ children, useSimpleLayout = false, skipLayout = false }: { children: React.ReactNode, useSimpleLayout?: boolean, skipLayout?: boolean }) => { const authenticated = isLoggedIn(); const isH5 = useH5Mode(); const navigate = useNavigate(); @@ -79,6 +79,11 @@ const ProtectedRoute = ({ children, useSimpleLayout = false }: { children: React ); } + // 如果跳过布局,直接返回子组件 + if (skipLayout) { + return <>{children}; + } + // 根据模式和页面类型选择布局 let Layout; if (isH5 && useSimpleLayout) { @@ -147,7 +152,7 @@ function App() { + } diff --git a/vite-frontend/src/api/network.ts b/vite-frontend/src/api/network.ts index 42c628a..4004359 100644 --- a/vite-frontend/src/api/network.ts +++ b/vite-frontend/src/api/network.ts @@ -1,76 +1,50 @@ import axios, { AxiosResponse } from 'axios'; - +import { getPanelAddresses} from '@/utils/panel'; // 检测是否在WebView中运行 const isWebView = (): boolean => { - try { - if (typeof (window as any).AndroidInterface !== 'undefined') { - const result = (window as any).AndroidInterface.isWebView(); - return result === "true"; - } - return false; - } catch (error) { + if((window as any).JsInterface !== undefined) { + return true; + }else if((window as any).webkit && (window as any).webkit.messageHandlers) { + return true; + }else { return false; } }; -// 获取WebView中当前选中的面板地址 -const getWebViewPanelAddress = (): string | null => { - try { - if (typeof (window as any).AndroidInterface !== 'undefined') { - const currentAddress = (window as any).AndroidInterface.getCurrentPanelAddress(); - // 确保返回的地址不为空且已选中 - if (currentAddress && currentAddress.trim()) { - return currentAddress; - } - return null; +interface PanelAddress { + name: string; + address: string; + inx: boolean; +} + +const setPanelAddressesFunc = (newAddress: PanelAddress[]) => { + newAddress.forEach(item => { + if (item.inx) { + baseURL = `${item.address}/api/v1/`; + axios.defaults.baseURL = baseURL; } - return null; - } catch (error) { - return null; - } + }); +} + +function getWebViewPanelAddress() { + (window as any).setAddresses = setPanelAddressesFunc + getPanelAddresses("setAddresses"); }; -// 初始化时获取基础URL let baseURL: string = ''; -// 重新初始化baseURL的函数 export const reinitializeBaseURL = () => { if (isWebView()) { - const panelAddress = getWebViewPanelAddress(); - if (panelAddress && panelAddress.trim()) { - baseURL = `${panelAddress}/api/v1/`; - axios.defaults.baseURL = baseURL; - return true; - } else { - baseURL = ''; - axios.defaults.baseURL = ''; - return false; - } + getWebViewPanelAddress(); } else { - // 非WebView环境使用默认配置 baseURL = import.meta.env.VITE_API_BASE ? `${import.meta.env.VITE_API_BASE}/api/v1/` : '/api/v1/'; axios.defaults.baseURL = baseURL; - return true; } }; -// 初始调用 reinitializeBaseURL(); - -// 在开发环境下输出API配置信息 -if (import.meta.env.DEV) { - console.log('🌐 API Configuration:'); - console.log(' - VITE_API_BASE:', import.meta.env.VITE_API_BASE || '(undefined)'); - console.log(' - Base URL:', baseURL); - console.log(' - Environment:', import.meta.env.MODE); - console.log(' - Is WebView:', isWebView()); - if (isWebView()) { - console.log(' - WebView Panel Address:', getWebViewPanelAddress() || '(not set)'); - } -} - interface ApiResponse { code: number; msg: string; diff --git a/vite-frontend/src/components/navbar.tsx b/vite-frontend/src/components/navbar.tsx index d54bd8e..897a013 100644 --- a/vite-frontend/src/components/navbar.tsx +++ b/vite-frontend/src/components/navbar.tsx @@ -18,21 +18,14 @@ export const Navbar = () => { // 检测是否在WebView中运行 useEffect(() => { - const detectWebView = () => { - try { - // 尝试调用Android注册的JavaScript接口 - if (typeof (window as any).AndroidInterface !== 'undefined') { - const result = (window as any).AndroidInterface.isWebView(); - setIsWebView(result === "true"); - } else { - setIsWebView(false); - } - } catch (error) { - setIsWebView(false); - } - }; - - detectWebView(); + if((window as any).JsInterface !== undefined) { + setIsWebView(true); + }else if((window as any).webkit && (window as any).webkit.messageHandlers) { + setIsWebView(true); + }else { + setIsWebView(false); + } + }, []); useEffect(() => { diff --git a/vite-frontend/src/config/site.ts b/vite-frontend/src/config/site.ts index 9d637d7..d384fca 100644 --- a/vite-frontend/src/config/site.ts +++ b/vite-frontend/src/config/site.ts @@ -4,14 +4,15 @@ export type SiteConfig = typeof siteConfig; // 缓存相关常量 const CACHE_PREFIX = 'vite_config_'; -const VERSION = "1.3.0"; - +const VERSION = "1.3.1"; +const APP_VERSION = "1.0.1"; const getInitialConfig = () => { if (typeof window === 'undefined') { return { name: "flux-panel", version: VERSION, + app_version: APP_VERSION, }; } @@ -20,11 +21,13 @@ const getInitialConfig = () => { return { name: cachedAppName, version: VERSION, + app_version: APP_VERSION, }; } return { name: "flux-panel", version: VERSION, + app_version: APP_VERSION, }; }; diff --git a/vite-frontend/src/pages/index.tsx b/vite-frontend/src/pages/index.tsx index 65d7874..15e75ca 100644 --- a/vite-frontend/src/pages/index.tsx +++ b/vite-frontend/src/pages/index.tsx @@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom"; import toast from 'react-hot-toast'; import axios from 'axios'; +import { siteConfig } from '@/config/site'; import { title } from "@/components/primitives"; import DefaultLayout from "@/layouts/default"; import { login, LoginData, checkCaptcha } from "@/api"; @@ -52,7 +53,7 @@ export default function IndexPage() { const navigate = useNavigate(); const tacInstanceRef = useRef(null); const captchaContainerRef = useRef(null); - + const [isWebView, setIsWebView] = useState(false); // 清理验证码实例 useEffect(() => { return () => { @@ -62,7 +63,16 @@ export default function IndexPage() { } }; }, []); - + // 检测是否在WebView中运行 + useEffect(() => { + if((window as any).JsInterface !== undefined) { + setIsWebView(true); + }else if((window as any).webkit && (window as any).webkit.messageHandlers) { + setIsWebView(true); + }else { + setIsWebView(false); + } + }, []); // 验证表单 const validateForm = (): boolean => { const newErrors: Partial = {}; @@ -249,7 +259,7 @@ export default function IndexPage() { return ( -
+
@@ -298,6 +308,30 @@ export default function IndexPage() {
+ + {/* 版权信息 - 固定在底部,不占据布局空间 */} + + {isWebView && ( +
+

+ Powered by{' '} + + flux-panel + +

+

+ v{siteConfig.app_version} +

+
+ )} + + + {/* 验证码弹层 */} {showCaptcha && (
diff --git a/vite-frontend/src/pages/profile.tsx b/vite-frontend/src/pages/profile.tsx index 9609fb2..1f8ec1b 100644 --- a/vite-frontend/src/pages/profile.tsx +++ b/vite-frontend/src/pages/profile.tsx @@ -8,7 +8,6 @@ import { useNavigate } from 'react-router-dom'; import { updatePassword } from '@/api'; import { safeLogout } from '@/utils/logout'; -import { siteConfig } from '@/config/site'; interface PasswordForm { newUsername: string; @@ -240,23 +239,6 @@ export default function ProfilePage() {
- {/* 版权信息 */} -
-

- Powered by{' '} - - flux-panel - -

-

- v{siteConfig.version} -

-
{/* 修改密码弹窗 */} { const navigate = useNavigate(); const [panelAddresses, setPanelAddresses] = useState([]); - const [currentAddress, setCurrentAddress] = useState(''); const [newName, setNewName] = useState(''); const [newAddress, setNewAddress] = useState(''); - // 验证面板地址格式 - const validatePanelAddress = (address: string): boolean => { - try { - // 基本格式检查:必须以 http:// 或 https:// 开头 - if (!address.startsWith('http://') && !address.startsWith('https://')) { - return false; - } - // 使用URL构造函数验证完整URL格式 - const url = new URL(address); - - // 检查主机名不能为空 - if (!url.hostname || url.hostname.trim() === '') { - return false; - } - - // 检查主机名 - const hostname = url.hostname; - - // 支持 localhost - if (hostname === 'localhost') { - return true; - } - - // 支持 IPv4 地址 - const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; - if (ipv4Pattern.test(hostname)) { - const parts = hostname.split('.'); - return parts.every(part => { - const num = parseInt(part); - return num >= 0 && num <= 255; - }); - } - - // 支持 IPv6 地址 - const ipv6Pattern = /^\[([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]$|^\[([0-9a-fA-F]{1,4}:)*:([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}\]$/; - if (ipv6Pattern.test(hostname)) { - return true; - } - - // 支持域名 - const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/; - if (domainPattern.test(hostname)) { - return true; - } - - return false; - } catch (error) { - // URL构造函数失败说明格式不正确 - return false; - } - }; + const setPanelAddressesFunc = (newAddress: PanelAddress[]) => { + setPanelAddresses(newAddress); + } // 加载面板地址列表 - const loadPanelAddresses = () => { - try { - if (typeof (window as any).AndroidInterface !== 'undefined') { - const addressesStr = (window as any).AndroidInterface.getPanelAddresses(); - const currentAddr = (window as any).AndroidInterface.getCurrentPanelAddress(); - - if (addressesStr && addressesStr.trim()) { - const addresses = addressesStr.split(',').filter((item: string) => item.trim()).map((item: string) => { - const [name, address] = item.split('|'); - return { name: name || '未命名', address: address || '' }; - }); - setPanelAddresses(addresses); - } else { - // 如果没有数据或数据为空,清空列表 - setPanelAddresses([]); - } - - setCurrentAddress(currentAddr || ''); - } - } catch (error) { - console.error('加载面板地址失败:', error); - setPanelAddresses([]); - } + const loadPanelAddresses = async () => { + (window as any).setPanelAddresses = setPanelAddressesFunc + getPanelAddresses(); }; // 添加新面板地址 - const addPanelAddress = () => { + const addPanelAddress = async () => { if (!newName.trim() || !newAddress.trim()) { toast.error('请输入名称和地址'); return; @@ -109,54 +49,26 @@ export const SettingsPage = () => { toast.error('地址格式不正确,请检查:\n• 必须是完整的URL格式\n• 必须以 http:// 或 https:// 开头\n• 支持域名、IPv4、IPv6 地址\n• 端口号范围:1-65535\n• 示例:http://192.168.1.100:3000'); return; } - - try { - if (typeof (window as any).AndroidInterface !== 'undefined') { - (window as any).AndroidInterface.savePanelAddress(newName.trim(), newAddress.trim()); - toast.success('添加成功'); - setNewName(''); - setNewAddress(''); - loadPanelAddresses(); - } - } catch (error) { - toast.error('添加失败' + error); - } + (window as any).setPanelAddresses = setPanelAddressesFunc + savePanelAddress(newName.trim(), newAddress.trim()); + setNewName(''); + setNewAddress(''); + toast.success('添加成功'); }; // 设置当前面板地址 - const setCurrentPanel = (address: string) => { - try { - if (typeof (window as any).AndroidInterface !== 'undefined') { - (window as any).AndroidInterface.setCurrentPanelAddress(address); - setCurrentAddress(address); - reinitializeBaseURL(); - toast.success('设置成功,API地址已更新'); - } - } catch (error) { - toast.error('设置失败'); - } + const setCurrentPanel = async (name: string) => { + (window as any).setPanelAddresses = setPanelAddressesFunc + setCurrentPanelAddress(name); + reinitializeBaseURL(); }; // 删除面板地址 - const deletePanelAddress = (name: string, address: string) => { - try { - if (typeof (window as any).AndroidInterface !== 'undefined') { - (window as any).AndroidInterface.deletePanelAddress(name, address); - - // 如果删除的是当前选中的地址,重新初始化baseURL - if (currentAddress === address) { - setCurrentAddress(''); - reinitializeBaseURL(); - toast.success('删除成功,当前面板地址已清空'); - } else { - toast.success('删除成功'); - } - - loadPanelAddresses(); - } - } catch (error) { - toast.error('删除失败'); - } + const handleDeletePanelAddress = async (name: string) => { + (window as any).setPanelAddresses = setPanelAddressesFunc + deletePanelAddress(name); + reinitializeBaseURL(); + toast.success('删除成功'); }; // 页面加载时获取数据 @@ -228,7 +140,7 @@ export const SettingsPage = () => {
{panel.name} - {currentAddress === panel.address && ( + {panel.inx && ( 当前 @@ -237,12 +149,12 @@ export const SettingsPage = () => {

{panel.address}

- {currentAddress !== panel.address && ( + {!panel.inx && ( @@ -251,7 +163,7 @@ export const SettingsPage = () => { size="sm" color="danger" variant="light" - onClick={() => deletePanelAddress(panel.name, panel.address)} + onClick={() => handleDeletePanelAddress(panel.name)} > 删除 diff --git a/vite-frontend/src/pages/tunnel.tsx b/vite-frontend/src/pages/tunnel.tsx index c641dde..a5386d3 100644 --- a/vite-frontend/src/pages/tunnel.tsx +++ b/vite-frontend/src/pages/tunnel.tsx @@ -793,6 +793,9 @@ export default function TunnelPage() { TLS WSS TCP + MTLS + MWSS + MTCP