import { useMemo, useState } from "react"; import { Link, useParams } from "react-router"; import { useDataProvider, useList, useOne } from "@refinedev/core"; import { Alert, Button, Card, CardContent, ConfirmDialog, DetailList, Field, Input, Modal, PageHeader, PageState, Select, StatusPill, conflictMessage, } from "./lib/ui.jsx"; import { useTitle } from "./lib/hooks.js"; const createInitial = { protocol: "socks5", host: "", port: "", username: "", password: "", expected_public_ip: "", expected_region: "", }; const healthText = { unchecked: "未检测", healthy: "健康", unhealthy: "不健康", disabled: "已停用", }; const protocolOptions = ["http", "https", "socks4", "socks5"].map((value) => ({ value, label: value, })); function ExitHealthPill({ exit }) { const known = Object.hasOwn(healthText, exit.health_status); const tone = exit.health_status === "healthy" ? "success" : exit.health_status === "disabled" ? "neutral" : exit.health_status === "unchecked" ? "warning" : "danger"; return ( ); } function ExitActions({ exit, busy, onAction }) { const disabled = busy || exit.health_status === "disabled"; return (
); } function ExitCreateModal({ open, onClose, onSubmit, busy, error }) { const [form, setForm] = useState(createInitial); const update = (key, value) => setForm((current) => ({ ...current, [key]: value })); const port = Number(form.port); const credentialsValid = !form.password || !!form.username; const valid = form.host.trim() && Number.isInteger(port) && port > 0 && port <= 65535 && credentialsValid; async function submit(event) { event.preventDefault(); if (!valid) return; const created = await onSubmit({ protocol: form.protocol, host: form.host.trim(), port, username: form.username, password: form.password, expected_public_ip: form.expected_public_ip.trim(), expected_region: form.expected_region.trim(), }); if (created) { setForm(createInitial); onClose(); } } return ( } >
{error ? ( {conflictMessage( error, "出口地址或认证信息与现有资源冲突;表单内容已保留。", )} ) : null}
update("host", event.target.value)} placeholder="如 proxy.example" /> update("port", event.target.value)} /> update("username", event.target.value)} /> update("password", event.target.value)} invalid={!credentialsValid} /> update("expected_public_ip", event.target.value) } /> update("expected_region", event.target.value) } />
); } function ExitCard({ exit, boundAccounts, bindingsError, busy, onAction }) { return (
{exit.protocol}://{exit.host}:{exit.port}

{exit.id} · 用户名 {exit.username || "无"} · 密码{" "} {exit.password || "无"}

出口IP: {exit.observed_public_ip || "尚无观测"}

最近检测: {exit.last_checked_at ? new Date(exit.last_checked_at).toLocaleString("zh-CN") : "未检测"}

绑定账号: {bindingsError ? ( "状态未知" ) : ( {boundAccounts?.length ? boundAccounts.join("、") : "未绑定"} )}
); } export function NetworkExitList() { const dataProvider = useDataProvider()("default"); const [createOpen, setCreateOpen] = useState(false); const [busy, setBusy] = useState(""); const [createError, setCreateError] = useState(null); const [notice, setNotice] = useState(null); const [disabling, setDisabling] = useState(null); const { result, query } = useList({ resource: "network-exits" }); const error = query.error; const isPending = query.isPending; const exits = result.data ?? []; const { result: bindingResult, query: browsersQuery } = useList({ resource: "browsers", queryOptions: { retry: false }, }); const browsers = bindingResult.data ?? []; const bindingsError = browsersQuery.error; const refetchBrowsers = browsersQuery.refetch; const accountsByExit = useMemo(() => { const map = new Map(); for (const runtime of browsers) { if (!runtime.network_exit_id || !runtime.account_id) continue; map.set(runtime.network_exit_id, [ ...(map.get(runtime.network_exit_id) || []), runtime.account_id, ]); } return map; }, [browsers]); useTitle("CreatorHub · 网络出口"); async function createExit(data) { setBusy("create"); setCreateError(null); try { await dataProvider.create({ resource: "network-exits", variables: data }); await query.refetch(); setNotice({ variant: "success", text: "网络出口已创建;检测通过后才可用于账号绑定。", }); return true; } catch (reason) { setCreateError(reason); return false; } finally { setBusy(""); } } async function runAction(exit, action) { setBusy(exit.id); setNotice(null); try { await dataProvider.networkExitAction(exit.id, action); await Promise.all([query.refetch(), refetchBrowsers()]); setNotice({ variant: "success", text: action === "check" ? "出口检测完成。" : "出口已停用;绑定账号不会自动恢复。", }); } catch (reason) { setNotice({ variant: "destructive", text: conflictMessage( reason, action === "check" ? "已停用的出口不能检测。" : "出口当前不能停用。", ), }); } finally { setBusy(""); } } return (
{notice ? ( {notice.text} ) : null} {error ? ( {error.message} ) : null} {bindingsError ? ( 环境不可用{bindingsError.status ? `(${bindingsError.status})` : ""} :无法读取账号绑定,状态暂时未知。 ) : null}
{exits.map((exit) => ( action === "disable" ? setDisabling(exit) : runAction(exit, action) } /> ))}
{ setCreateOpen(false); setCreateError(null); }} onSubmit={createExit} busy={busy === "create"} error={createError} /> setDisabling(null)} onConfirm={() => { const exit = disabling; setDisabling(null); runAction(exit, "disable"); }} title="停用网络出口" confirmLabel="确认停用" busy={busy === disabling?.id} body={`停用网络出口 ${disabling?.id}?绑定账号将暂停,排队任务进入 hold。`} />
); } export function NetworkExitDetail() { const { id } = useParams(); const { result, error, isPending } = useOne({ resource: "network-exits", id, queryOptions: { retry: false }, }); const exit = result; useTitle("CreatorHub · 出口详情"); if (isPending) return ; if (error || !exit) return ( {error?.message || "网络出口不存在"} ); return (
{exit.protocol}://{exit.host}:{exit.port} } > {exit.id}], [ "健康 / 版本", {healthText[exit.health_status] ?? exit.health_status} ·{" "} {exit.version} , ], [ "用户名", {exit.username || "无"}, ], [ "密码", {exit.password || "无"}, ], [ "出口IP", {exit.observed_public_ip || "尚无观测"} , ], [ "最近检测", exit.last_checked_at ? new Date(exit.last_checked_at).toLocaleString("zh-CN") : "未检测", ], ]} />
); }