mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
适配ios端
This commit is contained in:
@@ -255,3 +255,4 @@ gitee/
|
||||
*.crt
|
||||
*.cert
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<meta key="title" content="flux-panel" property="og:title" />
|
||||
<meta
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build": "tsc && vite build && node toFile.mjs",
|
||||
"lint": "eslint --fix",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
@@ -69,6 +69,7 @@
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"@vitejs/plugin-legacy": "^5.4.3",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"eslint": "9.25.1",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
|
||||
@@ -59,7 +59,7 @@ const useH5Mode = () => {
|
||||
};
|
||||
|
||||
// 简化的路由保护组件 - 使用 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() {
|
||||
<Route
|
||||
path="/change-password"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ProtectedRoute skipLayout={true}>
|
||||
<ChangePasswordPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
|
||||
@@ -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<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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<any>(null);
|
||||
const captchaContainerRef = useRef<HTMLDivElement>(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<LoginForm> = {};
|
||||
@@ -249,7 +259,7 @@ export default function IndexPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Card className="w-full">
|
||||
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
||||
@@ -298,6 +308,30 @@ export default function IndexPage() {
|
||||
</Card>
|
||||
</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 && (
|
||||
<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 { safeLogout } from '@/utils/logout';
|
||||
import { siteConfig } from '@/config/site';
|
||||
|
||||
interface PasswordForm {
|
||||
newUsername: string;
|
||||
@@ -240,23 +239,6 @@ export default function ProfilePage() {
|
||||
|
||||
</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
|
||||
|
||||
@@ -5,100 +5,40 @@ import { Card, CardBody } from "@heroui/card";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import toast from 'react-hot-toast';
|
||||
import { reinitializeBaseURL } from '@/api/network';
|
||||
import {
|
||||
getPanelAddresses,
|
||||
savePanelAddress,
|
||||
setCurrentPanelAddress,
|
||||
deletePanelAddress,
|
||||
validatePanelAddress,
|
||||
} from '@/utils/panel';
|
||||
|
||||
interface PanelAddress {
|
||||
name: string;
|
||||
address: string;
|
||||
address: string;
|
||||
inx: boolean;
|
||||
}
|
||||
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [panelAddresses, setPanelAddresses] = useState<PanelAddress[]>([]);
|
||||
const [currentAddress, setCurrentAddress] = useState<string>('');
|
||||
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 = () => {
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
@@ -237,12 +149,12 @@ export const SettingsPage = () => {
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{panel.address}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentAddress !== panel.address && (
|
||||
{!panel.inx && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onClick={() => setCurrentPanel(panel.address)}
|
||||
onClick={() => setCurrentPanel(panel.name)}
|
||||
>
|
||||
设为当前
|
||||
</Button>
|
||||
@@ -251,7 +163,7 @@ export const SettingsPage = () => {
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant="light"
|
||||
onClick={() => deletePanelAddress(panel.name, panel.address)}
|
||||
onClick={() => handleDeletePanelAddress(panel.name)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
|
||||
@@ -793,6 +793,9 @@ export default function TunnelPage() {
|
||||
<SelectItem key="tls">TLS</SelectItem>
|
||||
<SelectItem key="wss">WSS</SelectItem>
|
||||
<SelectItem key="tcp">TCP</SelectItem>
|
||||
<SelectItem key="mtls">MTLS</SelectItem>
|
||||
<SelectItem key="mwss">MWSS</SelectItem>
|
||||
<SelectItem key="mtcp">MTCP</SelectItem>
|
||||
</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 react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react()
|
||||
react(),
|
||||
legacy({
|
||||
targets: ['defaults', 'not IE 11']
|
||||
})
|
||||
],
|
||||
base: './',
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user