修复转发暂停问题 增加安卓apk

This commit is contained in:
qaq
2025-08-20 13:43:35 +08:00
parent 42d04da2a2
commit b1647ed309
82 changed files with 449473 additions and 291 deletions
+270
View File
@@ -0,0 +1,270 @@
import { useState, useEffect } from 'react';
import { Input } from "@heroui/input";
import { Button } from "@heroui/button";
import { Card, CardBody } from "@heroui/card";
import { useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast';
import { reinitializeBaseURL } from '@/api/network';
interface PanelAddress {
name: string;
address: string;
}
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 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 addPanelAddress = () => {
if (!newName.trim() || !newAddress.trim()) {
toast.error('请输入名称和地址');
return;
}
// 验证地址格式
if (!validatePanelAddress(newAddress.trim())) {
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('添加失败');
}
};
// 设置当前面板地址
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 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('删除失败');
}
};
// 页面加载时获取数据
useEffect(() => {
loadPanelAddresses();
}, []);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* 顶部导航 */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button
isIconOnly
variant="light"
onClick={() => navigate(-1)}
className="text-gray-600 dark:text-gray-300"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Button>
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">面板设置</h1>
</div>
</div>
</div>
{/* 内容区域 */}
<div className="max-w-4xl mx-auto px-4 py-6">
<div className="space-y-6">
{/* 添加新地址 */}
<Card className="border border-gray-200 dark:border-gray-700">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">添加新面板地址</h2>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="名称"
placeholder="请输入面板名称"
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
<Input
label="地址"
placeholder="http://192.168.1.100:3000"
value={newAddress}
onChange={(e) => setNewAddress(e.target.value)}
/>
</div>
<Button color="primary" onClick={addPanelAddress}>
添加
</Button>
</div>
</CardBody>
</Card>
{/* 地址列表 */}
<Card className="border border-gray-200 dark:border-gray-700">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">已保存的面板地址</h2>
{panelAddresses.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400 text-center py-8">暂无保存的面板地址</p>
) : (
<div className="space-y-3">
{panelAddresses.map((panel, index) => (
<div key={index} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
<div className="flex items-center justify-between">
<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 && (
<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>
)}
</div>
<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 && (
<Button
size="sm"
color="primary"
variant="flat"
onClick={() => setCurrentPanel(panel.address)}
>
设为当前
</Button>
)}
<Button
size="sm"
color="danger"
variant="light"
onClick={() => deletePanelAddress(panel.name, panel.address)}
>
删除
</Button>
</div>
</div>
</div>
))}
</div>
)}
</CardBody>
</Card>
</div>
</div>
</div>
);
};