码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
ng-webot/web/src/pages/ProxySettings.tsx
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>
  )
}