From fdbb37ad48afc0c56952ceff93de416054ae0b1c Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 3 Sep 2026 18:39:29 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E7=BD=91=E7=BB=9C=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=E5=88=97=E8=A1=A8=E6=94=B9=E4=B8=BA=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/NetworkExitsPage.jsx | 148 +++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 57 deletions(-) diff --git a/web/src/NetworkExitsPage.jsx b/web/src/NetworkExitsPage.jsx index 2de0925..aecb2b6 100644 --- a/web/src/NetworkExitsPage.jsx +++ b/web/src/NetworkExitsPage.jsx @@ -15,6 +15,7 @@ import { PageState, Select, StatusPill, + Table, conflictMessage, } from "./lib/ui.jsx"; import { useTitle } from "./lib/hooks.js"; @@ -239,50 +240,90 @@ function ExitCreateModal({ open, onClose, onSubmit, busy, error }) { ); } -function ExitCard({ exit, boundAccounts, bindingsError, busy, onAction }) { - return ( - - -
-
- - {exit.protocol}://{exit.host}:{exit.port} - -

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

-
- +function ExitTable({ exits, accountsByExit, bindingsError, busy, onAction }) { + const columns = [ + { + header: "出口", + width: "20%", + render: (exit) => ( +
+ + {exit.protocol}://{exit.host}:{exit.port} + +

{exit.id}

-
- 出口IP: + ), + }, + { + header: "状态", + width: "9%", + render: (exit) => , + }, + { + header: "用户名", + width: "11%", + render: (exit) => ( + {exit.username || "无"} + ), + }, + { + header: "密码", + width: "11%", + render: (exit) => ( + {exit.password || "无"} + ), + }, + { + header: "出口IP", + width: "13%", + render: (exit) => ( + {exit.observed_public_ip || "尚无观测"} + ), + }, + { + header: "最近检测", + width: "15%", + render: (exit) => + exit.last_checked_at + ? new Date(exit.last_checked_at).toLocaleString("zh-CN") + : "未检测", + }, + { + header: "绑定账号", + width: "13%", + render: (exit) => { + if (bindingsError) return 状态未知; + const accounts = accountsByExit.get(exit.id); + return ( - {exit.observed_public_ip || "尚无观测"} + {accounts?.length ? accounts.join("、") : "未绑定"} -
-

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

-
- 绑定账号: - {bindingsError ? ( - "状态未知" - ) : ( - - {boundAccounts?.length ? boundAccounts.join("、") : "未绑定"} - - )} -
- - - + ); + }, + }, + { + header: "操作", + width: "8%", + align: "right", + render: (exit) => ( + + ), + }, + ]; + return ( + exit.id} + minWidth="1100px" + /> ); } @@ -402,22 +443,15 @@ export function NetworkExitList() { empty={exits.length === 0} emptyText="暂无网络出口。创建并检测健康后,才能绑定运行环境。" > -
- {exits.map((exit) => ( - - action === "disable" - ? setDisabling(exit) - : runAction(exit, action) - } - /> - ))} -
+ + action === "disable" ? setDisabling(exit) : runAction(exit, action) + } + /> Date: Fri, 4 Sep 2026 09:54:55 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E7=BD=91=E7=BB=9C=E5=87=BA=E5=8F=A3?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E4=BB=A5=20ID=20=E4=BD=9C=E4=B8=BA=E4=B8=BB?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/NetworkExitsPage.jsx | 22 +++++++++++----------- web/src/NetworkExitsPage.test.jsx | 5 ++++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/web/src/NetworkExitsPage.jsx b/web/src/NetworkExitsPage.jsx index aecb2b6..729a96b 100644 --- a/web/src/NetworkExitsPage.jsx +++ b/web/src/NetworkExitsPage.jsx @@ -244,16 +244,18 @@ function ExitTable({ exits, accountsByExit, bindingsError, busy, onAction }) { const columns = [ { header: "出口", - width: "20%", + width: "22%", render: (exit) => (
- {exit.protocol}://{exit.host}:{exit.port} + {exit.id} -

{exit.id}

+

+ {exit.protocol}://{exit.host}:{exit.port} +

), }, @@ -264,14 +266,14 @@ function ExitTable({ exits, accountsByExit, bindingsError, busy, onAction }) { }, { header: "用户名", - width: "11%", + width: "10%", render: (exit) => ( {exit.username || "无"} ), }, { header: "密码", - width: "11%", + width: "10%", render: (exit) => ( {exit.password || "无"} ), @@ -280,7 +282,9 @@ function ExitTable({ exits, accountsByExit, bindingsError, busy, onAction }) { header: "出口IP", width: "13%", render: (exit) => ( - {exit.observed_public_ip || "尚无观测"} + + {exit.observed_public_ip || "尚无观测"} + ), }, { @@ -309,11 +313,7 @@ function ExitTable({ exits, accountsByExit, bindingsError, busy, onAction }) { width: "8%", align: "right", render: (exit) => ( - + ), }, ]; diff --git a/web/src/NetworkExitsPage.test.jsx b/web/src/NetworkExitsPage.test.jsx index 919d535..102892c 100644 --- a/web/src/NetworkExitsPage.test.jsx +++ b/web/src/NetworkExitsPage.test.jsx @@ -74,10 +74,13 @@ describe("NetworkExitList", () => { expect(await screen.findByText(/创建并检测健康后/)).toBeTruthy(); }); - it("shows stored credentials and runs an explicit health check", async () => { + it("shows the exit ID as the primary identifier with stored credentials", async () => { const dataProvider = provider([networkExit]); renderExits(dataProvider); + const idLink = await screen.findByRole("link", { name: "exit-a" }); + expect(idLink.getAttribute("href")).toBe("/network-exits/exit-a"); + expect(screen.getByText("socks5://proxy.example:1080")).toBeTruthy(); expect((await screen.findAllByText(/proxy-user/)).length).toBeGreaterThan( 0, );