Files
FunConnect/client/ui/src/pages/Settings.tsx
2026-02-25 20:52:13 +08:00

292 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
<a href="https://fc.funmc.cn" target="_blank" rel="noopener" className="text-accent-green hover:underline">fc.funmc.cn</a>
</div>
<div className="flex justify-between">
<span className="text-text-muted">使</span>
<a href="https://fc.funmc.cn/docs" target="_blank" rel="noopener" className="text-accent-green hover:underline">fc.funmc.cn/docs</a>
</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>
)
}