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 (
>
}
>
);
}
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")
: "未检测",
],
]}
/>
);
}