码桶
发现社区成员的开源项目
ProxySettings.tsx7.3 KB
import { useEffect, useState } from 'react'
import { api } from '@/lib/api'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Loader2, Globe, FlaskConical } from 'lucide-react'
import { toast } from 'sonner'
function Field({ label, value, onChange, type = 'text', placeholder, autoComplete }: { label: string; value: string; onChange: (v: string) => void; type?: string; placeholder?: string; autoComplete?: string }) {
return (
<div className="space-y-1.5">
<Label>{label}</Label>
<Input type={type} value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} autoComplete={autoComplete} className="h-9" />
</div>
)
}
export default function ProxySettings() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null)
const [test, setTest] = useState<{ ok: boolean; text: string; body?: string } | null>(null)
const [f, setF] = useState({
proxy_enabled: '0', proxy_ip: '', proxy_port: '', proxy_user: '', proxy_pass: '', proxy_protocol: 'http',
})
useEffect(() => {
setLoading(true)
api.post('/api/proxy_settings').then((d: any) => {
if (d?.ok) {
const s = d.settings || {}
setF({
proxy_enabled: s.proxy_enabled === '1' ? '1' : '0',
proxy_ip: s.proxy_ip || '', proxy_port: s.proxy_port || '',
proxy_user: s.proxy_user || '', proxy_pass: s.proxy_pass || '',
proxy_protocol: s.proxy_protocol || 'http',
})
}
}).catch(() => {}).finally(() => setLoading(false))
}, [])
const set = (k: string, v: string) => setF({ ...f, [k]: v })
const save = async () => {
setSaving(true); setMsg(null); setTest(null)
const d: any = await api.post('/api/proxy_settings_save', f)
setSaving(false)
if (d?.ok) {
setMsg({ ok: true, text: d.msg || '已保存并应用' })
if (d.proxy && d.proxy.error) setMsg({ ok: false, text: '已保存,但代理应用失败: ' + d.proxy.error })
} else {
setMsg({ ok: false, text: (d && d.msg) || '保存失败' })
}
}
const testConnect = async () => {
setTesting(true); setTest(null)
const d: any = await api.post('/api/proxy_test', { test_url: '' })
setTesting(false)
setTest({ ok: !!d?.ok, text: d?.msg || (d?.ok ? '连通成功' : '连通失败'), body: d?.body })
}
return (
<div className="mx-auto w-full max-w-3xl space-y-5 p-3 md:p-6">
<div className="space-y-1">
<h1 className="text-xl font-bold">代理服务器设置</h1>
<p className="text-sm text-muted-foreground">为机器人出站流量配置代理(AI 接口、图片生成、消息网关等统一经此代理转发)</p>
</div>
{loading ? (
<div className="flex justify-center py-20"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>
) : (
<Card>
<CardHeader>
<CardTitle>代理配置</CardTitle>
<CardDescription>保存后即时生效;测试连通性可验证代理是否可用</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{msg && (
<div className={'rounded-md border p-3 text-sm ' + (msg.ok ? 'border-green-500/30 bg-green-500/10 text-green-600' : 'border-red-500/30 bg-red-500/10 text-red-600')}>
{msg.text}
</div>
)}
<div className="flex items-center justify-between rounded-md border p-3">
<div>
<p className="text-sm font-semibold">启用代理</p>
<p className="text-xs text-muted-foreground">关闭后出站流量直连,不经代理</p>
</div>
<label className="relative inline-flex cursor-pointer items-center">
<input type="checkbox" className="peer sr-only" checked={f.proxy_enabled === '1'} onChange={(e) => set('proxy_enabled', e.target.checked ? '1' : '0')} />
<div className="h-5 w-9 rounded-full bg-border transition-colors peer-checked:bg-primary after:absolute after:left-0.5 after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-background after:transition-transform peer-checked:after:translate-x-4"></div>
</label>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<Field label="代理 IP" value={f.proxy_ip} onChange={(v) => set('proxy_ip', v)} placeholder="如 127.0.0.1 或 1.2.3.4" />
<Field label="代理端口" value={f.proxy_port} onChange={(v) => set('proxy_port', v)} placeholder="如 1080 / 7890" />
<Field label="代理账号" value={f.proxy_user} onChange={(v) => set('proxy_user', v)} placeholder="无账号可留空" autoComplete="new-password" />
<Field label="代理密码" value={f.proxy_pass} onChange={(v) => set('proxy_pass', v)} type="password" placeholder="无密码可留空" autoComplete="new-password" />
</div>
<div className="space-y-1.5">
<Label>代理协议</Label>
<select value={f.proxy_protocol} onChange={(e) => set('proxy_protocol', e.target.value)} className="h-9 w-full rounded-md border bg-background px-3 text-sm">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks5">SOCKS5</option>
</select>
</div>
<div className="flex flex-wrap gap-2">
<Button onClick={save} disabled={saving}>{saving ? '保存中…' : '保存配置'}</Button>
<Button variant="outline" onClick={testConnect} disabled={testing}>
{testing ? <Loader2 className="h-4 w-4 animate-spin" /> : <FlaskConical className="h-4 w-4" />} 测试连通性
</Button>
</div>
{test && (
<div className={'rounded-md border p-3 text-sm ' + (test.ok ? 'border-green-500/30 bg-green-500/10 text-green-600' : 'border-red-500/30 bg-red-500/10 text-red-600')}>
<div className="flex items-center gap-1.5 font-medium">
<Globe className="h-4 w-4" /> {test.text}
</div>
{test.body && <pre className="mt-2 overflow-auto rounded bg-black/20 p-2 text-xs">{test.body}</pre>}
</div>
)}
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-700 dark:text-amber-400">
<p className="font-semibold">部署建议</p>
<p className="mt-1 leading-relaxed">
为保证代理稳定性,建议在本机安装 Xray:入站代理使用普通 HTTP 协议,仅监听本地 IP(如 127.0.0.1);出站代理使用加密协议连接其他服务器。本页「代理 IP」填本机监听地址(如 127.0.0.1),协议选 HTTP 即可。
</p>
</div>
</CardContent>
</Card>
)}
</div>
)
}