Initial commit: FunConnect project with server, relay, client and admin panel
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
299
client/ui/src/pages/Settings.tsx
Normal file
299
client/ui/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useRelayNodeStore } from '../stores/relayNodeStore'
|
||||
import { useConfigStore } from '../stores/configStore'
|
||||
import { useAuthStore } from '../stores/authStore'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate()
|
||||
const { nodes, loading, fetchNodes, addNode, removeNode, reportPing } = useRelayNodeStore()
|
||||
const { config, customServerUrl, setCustomServer, clearCustomServer } = useConfigStore()
|
||||
const { logout } = useAuthStore()
|
||||
const [newName, setNewName] = useState('')
|
||||
const [newUrl, setNewUrl] = useState('')
|
||||
const [newRegion, setNewRegion] = useState('')
|
||||
const [adding, setAdding] = useState(false)
|
||||
const [addError, setAddError] = useState('')
|
||||
const [pinging, setPinging] = useState<string | null>(null)
|
||||
const [showServerChange, setShowServerChange] = useState(false)
|
||||
const [newServerUrl, setNewServerUrl] = useState('')
|
||||
const [serverError, setServerError] = useState('')
|
||||
const [changingServer, setChangingServer] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchNodes()
|
||||
}, [])
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setAddError('')
|
||||
setAdding(true)
|
||||
try {
|
||||
await addNode(newName, newUrl, newRegion || undefined)
|
||||
setNewName('')
|
||||
setNewUrl('')
|
||||
setNewRegion('')
|
||||
} catch (e: any) {
|
||||
setAddError(String(e))
|
||||
} finally {
|
||||
setAdding(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePing = async (id: string, url: string) => {
|
||||
setPinging(id)
|
||||
try {
|
||||
const start = performance.now()
|
||||
await fetch(`${url}/ping`, { signal: AbortSignal.timeout(4000) }).catch(() => {})
|
||||
const rtt = Math.round(performance.now() - start)
|
||||
await reportPing(id, rtt)
|
||||
} finally {
|
||||
setPinging(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<h1 className="text-xl font-bold text-text-primary">设置</h1>
|
||||
<p className="text-text-muted text-xs mt-0.5">管理应用配置</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6 max-w-2xl">
|
||||
{/* Server config */}
|
||||
<div className="card">
|
||||
<h2 className="text-sm font-semibold text-text-primary mb-1">服务器连接</h2>
|
||||
<p className="text-xs text-text-muted mb-4">当前连接的服务器信息</p>
|
||||
|
||||
<div className="p-3 rounded-lg bg-bg-tertiary border border-border mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-primary">
|
||||
{config?.server_name || 'FunMC Server'}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted font-mono mt-1">
|
||||
{config?.server_url || '未配置'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-accent-green rounded-full animate-pulse"></span>
|
||||
<span className="text-xs text-accent-green">已连接</span>
|
||||
</div>
|
||||
</div>
|
||||
{customServerUrl && (
|
||||
<p className="text-xs text-accent-orange mt-2">使用自定义服务器</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showServerChange ? (
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={newServerUrl}
|
||||
onChange={(e) => setNewServerUrl(e.target.value)}
|
||||
placeholder="输入新的服务器地址"
|
||||
className="input-field"
|
||||
/>
|
||||
{serverError && (
|
||||
<p className="text-xs text-accent-red">{serverError}</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
setServerError('')
|
||||
setChangingServer(true)
|
||||
try {
|
||||
let url = newServerUrl.trim()
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = 'http://' + url
|
||||
}
|
||||
await setCustomServer(url)
|
||||
const { config: newConfig } = useConfigStore.getState()
|
||||
if (newConfig && newConfig.server_url) {
|
||||
await logout()
|
||||
navigate('/login')
|
||||
} else {
|
||||
setServerError('无法连接到服务器')
|
||||
}
|
||||
} catch (e) {
|
||||
setServerError(String(e))
|
||||
} finally {
|
||||
setChangingServer(false)
|
||||
}
|
||||
}}
|
||||
disabled={changingServer || !newServerUrl.trim()}
|
||||
className="btn-primary flex-1"
|
||||
>
|
||||
{changingServer ? '连接中...' : '切换服务器'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowServerChange(false)
|
||||
setNewServerUrl('')
|
||||
setServerError('')
|
||||
}}
|
||||
className="btn-secondary"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowServerChange(true)}
|
||||
className="btn-secondary text-sm"
|
||||
>
|
||||
切换服务器
|
||||
</button>
|
||||
{customServerUrl && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
clearCustomServer()
|
||||
await logout()
|
||||
navigate('/setup')
|
||||
}}
|
||||
className="btn-secondary text-sm text-accent-orange"
|
||||
>
|
||||
恢复默认服务器
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Relay nodes */}
|
||||
<div className="card">
|
||||
<h2 className="text-sm font-semibold text-text-primary mb-1">中继服务器节点</h2>
|
||||
<p className="text-xs text-text-muted mb-4">
|
||||
支持多节点,P2P 穿透失败时自动选择延迟最低的节点进行流量中继。
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-xs text-text-muted">加载中...</p>
|
||||
) : nodes.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">暂无节点</p>
|
||||
) : (
|
||||
<div className="space-y-2 mb-4">
|
||||
{nodes.map((node) => (
|
||||
<div key={node.id} className="flex items-center gap-2 p-2.5 rounded-lg bg-bg-tertiary border border-border">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-text-primary truncate">{node.name}</span>
|
||||
{node.region && node.region !== 'auto' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-bg-hover text-text-muted">
|
||||
{node.region}
|
||||
</span>
|
||||
)}
|
||||
{node.priority > 0 && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-accent-green/15 text-accent-green border border-accent-green/20">
|
||||
主节点
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-0.5">
|
||||
<p className="text-xs text-text-muted font-mono truncate">{node.url}</p>
|
||||
{node.last_ping_ms != null && (
|
||||
<span className={`text-xs font-mono flex-shrink-0 ${
|
||||
node.last_ping_ms < 80 ? 'text-accent-green' :
|
||||
node.last_ping_ms < 200 ? 'text-accent-orange' : 'text-accent-red'
|
||||
}`}>
|
||||
{node.last_ping_ms} ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => handlePing(node.id, node.url)}
|
||||
disabled={pinging === node.id}
|
||||
className="text-xs px-2 py-1 rounded text-text-muted hover:text-accent-blue hover:bg-accent-blue/10 transition-colors"
|
||||
>
|
||||
{pinging === node.id ? '测速...' : '测速'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeNode(node.id)}
|
||||
className="text-text-muted hover:text-accent-red transition-colors p-1"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add form */}
|
||||
<form onSubmit={handleAdd} className="border-t border-border pt-4 space-y-2">
|
||||
<p className="text-xs text-text-secondary font-medium mb-2">添加节点</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="input-field w-28"
|
||||
placeholder="名称"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
className="input-field flex-1"
|
||||
placeholder="https://relay.example.com"
|
||||
value={newUrl}
|
||||
onChange={(e) => setNewUrl(e.target.value)}
|
||||
type="url"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
className="input-field w-24"
|
||||
placeholder="区域"
|
||||
value={newRegion}
|
||||
onChange={(e) => setNewRegion(e.target.value)}
|
||||
/>
|
||||
<button type="submit" disabled={adding} className="btn-secondary flex-shrink-0">
|
||||
{adding ? '添加中' : '添加'}
|
||||
</button>
|
||||
</div>
|
||||
{addError && <p className="text-xs text-accent-red">{addError}</p>}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* About */}
|
||||
<div className="card">
|
||||
<h2 className="text-sm font-semibold text-text-primary mb-3">关于</h2>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">版本</span>
|
||||
<span className="text-text-primary font-mono">0.1.0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">开发团队</span>
|
||||
<span className="text-accent-purple font-medium">魔幻方</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">官网</span>
|
||||
<a href="https://funmc.com" target="_blank" rel="noopener" className="text-accent-green hover:underline">funmc.com</a>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">框架</span>
|
||||
<span className="text-text-primary">Tauri 2 + React</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">协议</span>
|
||||
<span className="text-text-primary">QUIC (quinn) over UDP</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-1">
|
||||
<svg className="w-4 h-4 text-accent-purple" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-text-primary">魔幻方开发</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">© 2024 魔幻方. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user