mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-11 03:36:37 +08:00
适配ios端
This commit is contained in:
@@ -255,3 +255,4 @@ gitee/
|
|||||||
*.crt
|
*.crt
|
||||||
*.cert
|
*.cert
|
||||||
android-app/
|
android-app/
|
||||||
|
ios-app/
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VITE_API_BASE=http://127.0.0.1:6365
|
VITE_API_BASE=http://192.168.100.9:6365
|
||||||
|
|||||||
@@ -67,6 +67,9 @@
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
|
/* 禁用弹性滚动 */
|
||||||
|
overscroll-behavior: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色主题样式 */
|
/* 暗色主题样式 */
|
||||||
@@ -78,6 +81,8 @@
|
|||||||
#root {
|
#root {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
/* 确保根元素也禁用弹性滚动 */
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 防止加载时的闪烁 */
|
/* 防止加载时的闪烁 */
|
||||||
@@ -88,6 +93,8 @@
|
|||||||
/* 确保在主题切换时平滑过渡 */
|
/* 确保在主题切换时平滑过渡 */
|
||||||
html {
|
html {
|
||||||
transition: color-scheme 0.2s ease;
|
transition: color-scheme 0.2s ease;
|
||||||
|
/* 禁用整个页面的弹性滚动 */
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 防止布局切换时的闪屏 */
|
/* 防止布局切换时的闪屏 */
|
||||||
@@ -106,6 +113,19 @@
|
|||||||
.pc-mode.dark #root {
|
.pc-mode.dark #root {
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端滚动优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
html, body {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保固定定位的元素不受滚动影响 */
|
||||||
|
.fixed {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<meta key="title" content="flux-panel" property="og:title" />
|
<meta key="title" content="flux-panel" property="og:title" />
|
||||||
<meta
|
<meta
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build && node toFile.mjs",
|
||||||
"lint": "eslint --fix",
|
"lint": "eslint --fix",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||||
"@typescript-eslint/parser": "8.31.1",
|
"@typescript-eslint/parser": "8.31.1",
|
||||||
|
"@vitejs/plugin-legacy": "^5.4.3",
|
||||||
"@vitejs/plugin-react": "4.4.1",
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
"eslint": "9.25.1",
|
"eslint": "9.25.1",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const useH5Mode = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 简化的路由保护组件 - 使用 React Router 导航避免循环
|
// 简化的路由保护组件 - 使用 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 authenticated = isLoggedIn();
|
||||||
const isH5 = useH5Mode();
|
const isH5 = useH5Mode();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -79,6 +79,11 @@ const ProtectedRoute = ({ children, useSimpleLayout = false }: { children: React
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果跳过布局,直接返回子组件
|
||||||
|
if (skipLayout) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
// 根据模式和页面类型选择布局
|
// 根据模式和页面类型选择布局
|
||||||
let Layout;
|
let Layout;
|
||||||
if (isH5 && useSimpleLayout) {
|
if (isH5 && useSimpleLayout) {
|
||||||
@@ -147,7 +152,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="/change-password"
|
path="/change-password"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute skipLayout={true}>
|
||||||
<ChangePasswordPage />
|
<ChangePasswordPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,50 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { getPanelAddresses} from '@/utils/panel';
|
||||||
// 检测是否在WebView中运行
|
// 检测是否在WebView中运行
|
||||||
const isWebView = (): boolean => {
|
const isWebView = (): boolean => {
|
||||||
try {
|
if((window as any).JsInterface !== undefined) {
|
||||||
if (typeof (window as any).AndroidInterface !== 'undefined') {
|
return true;
|
||||||
const result = (window as any).AndroidInterface.isWebView();
|
}else if((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
return result === "true";
|
return true;
|
||||||
}
|
}else {
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取WebView中当前选中的面板地址
|
interface PanelAddress {
|
||||||
const getWebViewPanelAddress = (): string | null => {
|
name: string;
|
||||||
try {
|
address: string;
|
||||||
if (typeof (window as any).AndroidInterface !== 'undefined') {
|
inx: boolean;
|
||||||
const currentAddress = (window as any).AndroidInterface.getCurrentPanelAddress();
|
}
|
||||||
// 确保返回的地址不为空且已选中
|
|
||||||
if (currentAddress && currentAddress.trim()) {
|
const setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
|
||||||
return currentAddress;
|
newAddress.forEach(item => {
|
||||||
}
|
if (item.inx) {
|
||||||
return null;
|
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 = '';
|
let baseURL: string = '';
|
||||||
|
|
||||||
// 重新初始化baseURL的函数
|
|
||||||
export const reinitializeBaseURL = () => {
|
export const reinitializeBaseURL = () => {
|
||||||
if (isWebView()) {
|
if (isWebView()) {
|
||||||
const panelAddress = getWebViewPanelAddress();
|
getWebViewPanelAddress();
|
||||||
if (panelAddress && panelAddress.trim()) {
|
|
||||||
baseURL = `${panelAddress}/api/v1/`;
|
|
||||||
axios.defaults.baseURL = baseURL;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
baseURL = '';
|
|
||||||
axios.defaults.baseURL = '';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 非WebView环境使用默认配置
|
|
||||||
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/';
|
||||||
axios.defaults.baseURL = baseURL;
|
axios.defaults.baseURL = baseURL;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始调用
|
|
||||||
reinitializeBaseURL();
|
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<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
|
|||||||
@@ -18,21 +18,14 @@ export const Navbar = () => {
|
|||||||
|
|
||||||
// 检测是否在WebView中运行
|
// 检测是否在WebView中运行
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const detectWebView = () => {
|
if((window as any).JsInterface !== undefined) {
|
||||||
try {
|
setIsWebView(true);
|
||||||
// 尝试调用Android注册的JavaScript接口
|
}else if((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
if (typeof (window as any).AndroidInterface !== 'undefined') {
|
setIsWebView(true);
|
||||||
const result = (window as any).AndroidInterface.isWebView();
|
}else {
|
||||||
setIsWebView(result === "true");
|
setIsWebView(false);
|
||||||
} else {
|
}
|
||||||
setIsWebView(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setIsWebView(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
detectWebView();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ export type SiteConfig = typeof siteConfig;
|
|||||||
|
|
||||||
// 缓存相关常量
|
// 缓存相关常量
|
||||||
const CACHE_PREFIX = 'vite_config_';
|
const CACHE_PREFIX = 'vite_config_';
|
||||||
const VERSION = "1.3.0";
|
const VERSION = "1.3.1";
|
||||||
|
const APP_VERSION = "1.0.1";
|
||||||
|
|
||||||
const getInitialConfig = () => {
|
const getInitialConfig = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return {
|
return {
|
||||||
name: "flux-panel",
|
name: "flux-panel",
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
|
app_version: APP_VERSION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,11 +21,13 @@ const getInitialConfig = () => {
|
|||||||
return {
|
return {
|
||||||
name: cachedAppName,
|
name: cachedAppName,
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
|
app_version: APP_VERSION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: "flux-panel",
|
name: "flux-panel",
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
|
app_version: APP_VERSION,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 { siteConfig } from '@/config/site';
|
||||||
import { title } from "@/components/primitives";
|
import { title } from "@/components/primitives";
|
||||||
import DefaultLayout from "@/layouts/default";
|
import DefaultLayout from "@/layouts/default";
|
||||||
import { login, LoginData, checkCaptcha } from "@/api";
|
import { login, LoginData, checkCaptcha } from "@/api";
|
||||||
@@ -52,7 +53,7 @@ export default function IndexPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const tacInstanceRef = useRef<any>(null);
|
const tacInstanceRef = useRef<any>(null);
|
||||||
const captchaContainerRef = useRef<HTMLDivElement>(null);
|
const captchaContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isWebView, setIsWebView] = useState(false);
|
||||||
// 清理验证码实例
|
// 清理验证码实例
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
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 validateForm = (): boolean => {
|
||||||
const newErrors: Partial<LoginForm> = {};
|
const newErrors: Partial<LoginForm> = {};
|
||||||
@@ -249,7 +259,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 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(100vh-120px)] sm:min-h-[calc(100vh-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">
|
||||||
@@ -298,6 +308,30 @@ export default function IndexPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* 版权信息 - 固定在底部,不占据布局空间 */}
|
||||||
|
|
||||||
|
{isWebView && (
|
||||||
|
<div className="fixed inset-x-0 bottom-4 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{siteConfig.app_version}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* 验证码弹层 */}
|
{/* 验证码弹层 */}
|
||||||
{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">
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { updatePassword } from '@/api';
|
import { updatePassword } from '@/api';
|
||||||
import { safeLogout } from '@/utils/logout';
|
import { safeLogout } from '@/utils/logout';
|
||||||
import { siteConfig } from '@/config/site';
|
|
||||||
|
|
||||||
interface PasswordForm {
|
interface PasswordForm {
|
||||||
newUsername: string;
|
newUsername: string;
|
||||||
@@ -240,23 +239,6 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 版权信息 */}
|
|
||||||
<div className="text-center py-4 mt-auto">
|
|
||||||
<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{siteConfig.version}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 修改密码弹窗 */}
|
{/* 修改密码弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -5,100 +5,40 @@ import { Card, CardBody } from "@heroui/card";
|
|||||||
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 { reinitializeBaseURL } from '@/api/network';
|
import { reinitializeBaseURL } from '@/api/network';
|
||||||
|
import {
|
||||||
|
getPanelAddresses,
|
||||||
|
savePanelAddress,
|
||||||
|
setCurrentPanelAddress,
|
||||||
|
deletePanelAddress,
|
||||||
|
validatePanelAddress,
|
||||||
|
} from '@/utils/panel';
|
||||||
|
|
||||||
interface PanelAddress {
|
interface PanelAddress {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
|
inx: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [panelAddresses, setPanelAddresses] = useState<PanelAddress[]>([]);
|
const [panelAddresses, setPanelAddresses] = useState<PanelAddress[]>([]);
|
||||||
const [currentAddress, setCurrentAddress] = useState<string>('');
|
|
||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
const [newAddress, setNewAddress] = 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 setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
|
||||||
const url = new URL(address);
|
setPanelAddresses(newAddress);
|
||||||
|
}
|
||||||
// 检查主机名不能为空
|
|
||||||
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 loadPanelAddresses = () => {
|
const loadPanelAddresses = async () => {
|
||||||
try {
|
(window as any).setPanelAddresses = setPanelAddressesFunc
|
||||||
if (typeof (window as any).AndroidInterface !== 'undefined') {
|
getPanelAddresses();
|
||||||
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 addPanelAddress = () => {
|
const addPanelAddress = async () => {
|
||||||
if (!newName.trim() || !newAddress.trim()) {
|
if (!newName.trim() || !newAddress.trim()) {
|
||||||
toast.error('请输入名称和地址');
|
toast.error('请输入名称和地址');
|
||||||
return;
|
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');
|
toast.error('地址格式不正确,请检查:\n• 必须是完整的URL格式\n• 必须以 http:// 或 https:// 开头\n• 支持域名、IPv4、IPv6 地址\n• 端口号范围:1-65535\n• 示例:http://192.168.1.100:3000');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
(window as any).setPanelAddresses = setPanelAddressesFunc
|
||||||
try {
|
savePanelAddress(newName.trim(), newAddress.trim());
|
||||||
if (typeof (window as any).AndroidInterface !== 'undefined') {
|
setNewName('');
|
||||||
(window as any).AndroidInterface.savePanelAddress(newName.trim(), newAddress.trim());
|
setNewAddress('');
|
||||||
toast.success('添加成功');
|
toast.success('添加成功');
|
||||||
setNewName('');
|
|
||||||
setNewAddress('');
|
|
||||||
loadPanelAddresses();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('添加失败' + error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置当前面板地址
|
// 设置当前面板地址
|
||||||
const setCurrentPanel = (address: string) => {
|
const setCurrentPanel = async (name: string) => {
|
||||||
try {
|
(window as any).setPanelAddresses = setPanelAddressesFunc
|
||||||
if (typeof (window as any).AndroidInterface !== 'undefined') {
|
setCurrentPanelAddress(name);
|
||||||
(window as any).AndroidInterface.setCurrentPanelAddress(address);
|
reinitializeBaseURL();
|
||||||
setCurrentAddress(address);
|
|
||||||
reinitializeBaseURL();
|
|
||||||
toast.success('设置成功,API地址已更新');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('设置失败');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除面板地址
|
// 删除面板地址
|
||||||
const deletePanelAddress = (name: string, address: string) => {
|
const handleDeletePanelAddress = async (name: string) => {
|
||||||
try {
|
(window as any).setPanelAddresses = setPanelAddressesFunc
|
||||||
if (typeof (window as any).AndroidInterface !== 'undefined') {
|
deletePanelAddress(name);
|
||||||
(window as any).AndroidInterface.deletePanelAddress(name, address);
|
reinitializeBaseURL();
|
||||||
|
toast.success('删除成功');
|
||||||
// 如果删除的是当前选中的地址,重新初始化baseURL
|
|
||||||
if (currentAddress === address) {
|
|
||||||
setCurrentAddress('');
|
|
||||||
reinitializeBaseURL();
|
|
||||||
toast.success('删除成功,当前面板地址已清空');
|
|
||||||
} else {
|
|
||||||
toast.success('删除成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPanelAddresses();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('删除失败');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面加载时获取数据
|
// 页面加载时获取数据
|
||||||
@@ -228,7 +140,7 @@ export const SettingsPage = () => {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-gray-900 dark:text-white">{panel.name}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{panel.name}</span>
|
||||||
{currentAddress === panel.address && (
|
{panel.inx && (
|
||||||
<span className="px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 text-xs rounded">
|
<span className="px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 text-xs rounded">
|
||||||
当前
|
当前
|
||||||
</span>
|
</span>
|
||||||
@@ -237,12 +149,12 @@ export const SettingsPage = () => {
|
|||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{panel.address}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{panel.address}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{currentAddress !== panel.address && (
|
{!panel.inx && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onClick={() => setCurrentPanel(panel.address)}
|
onClick={() => setCurrentPanel(panel.name)}
|
||||||
>
|
>
|
||||||
设为当前
|
设为当前
|
||||||
</Button>
|
</Button>
|
||||||
@@ -251,7 +163,7 @@ export const SettingsPage = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="danger"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => deletePanelAddress(panel.name, panel.address)}
|
onClick={() => handleDeletePanelAddress(panel.name)}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -793,6 +793,9 @@ export default function TunnelPage() {
|
|||||||
<SelectItem key="tls">TLS</SelectItem>
|
<SelectItem key="tls">TLS</SelectItem>
|
||||||
<SelectItem key="wss">WSS</SelectItem>
|
<SelectItem key="wss">WSS</SelectItem>
|
||||||
<SelectItem key="tcp">TCP</SelectItem>
|
<SelectItem key="tcp">TCP</SelectItem>
|
||||||
|
<SelectItem key="mtls">MTLS</SelectItem>
|
||||||
|
<SelectItem key="mwss">MWSS</SelectItem>
|
||||||
|
<SelectItem key="mtcp">MTCP</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 获取面板地址列表
|
||||||
|
export async function getPanelAddresses(callback: string = "setPanelAddresses"){
|
||||||
|
if ((window as any).JsInterface && (window as any).JsInterface.getPanelAddresses) {
|
||||||
|
(window as any).JsInterface.getPanelAddresses(callback);
|
||||||
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
|
(window as any).webkit.messageHandlers.getPanelAddresses.postMessage(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存面板地址
|
||||||
|
export async function savePanelAddress(name: string, address: string){
|
||||||
|
if ((window as any).JsInterface) {
|
||||||
|
(window as any).JsInterface.savePanelAddress(name, address);
|
||||||
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
|
(window as any).webkit.messageHandlers.savePanelAddress.postMessage({ name, address });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置当前面板地址
|
||||||
|
export async function setCurrentPanelAddress(name: string) {
|
||||||
|
if ((window as any).JsInterface) {
|
||||||
|
(window as any).JsInterface.setCurrentPanelAddress(name);
|
||||||
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
|
(window as any).webkit.messageHandlers.setCurrentPanelAddress.postMessage({ name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除面板地址
|
||||||
|
export async function deletePanelAddress(name: string){
|
||||||
|
if ((window as any).JsInterface) {
|
||||||
|
(window as any).JsInterface.deletePanelAddress(name);
|
||||||
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
|
(window as any).webkit.messageHandlers.deletePanelAddress.postMessage({ name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证面板地址格式
|
||||||
|
export function 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
console.time('转换耗时');
|
||||||
|
const distPath = './dist/index.html';
|
||||||
|
let htmlText = fs.readFileSync(distPath, 'utf8');
|
||||||
|
let resultText = '';
|
||||||
|
let htmlArr = htmlText.match(/.*\n/g) || [];
|
||||||
|
htmlArr.forEach(str => {
|
||||||
|
str = str.replace(/\s?nomodule\s?/g,' ');
|
||||||
|
str = str.replace(/\s?crossorigin\s?/g,' ');
|
||||||
|
str = str.replace(/data-src/g,'src');
|
||||||
|
if(!/type="module"/i.test(str)) resultText += str;
|
||||||
|
});
|
||||||
|
fs.writeFileSync(distPath,resultText,'utf8');
|
||||||
|
console.timeEnd('转换耗时');
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import legacy from '@vitejs/plugin-legacy'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react()
|
react(),
|
||||||
|
legacy({
|
||||||
|
targets: ['defaults', 'not IE 11']
|
||||||
|
})
|
||||||
],
|
],
|
||||||
base: './',
|
base: './',
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Reference in New Issue
Block a user