diff --git a/web/src/AccountsPage.jsx b/web/src/AccountsPage.jsx index d8b6035..c42167f 100644 --- a/web/src/AccountsPage.jsx +++ b/web/src/AccountsPage.jsx @@ -15,6 +15,8 @@ import { PageState, Select, StatusPill, + Table, + TagInput, Textarea, conflictMessage, } from "./lib/ui.jsx"; @@ -24,7 +26,7 @@ const createInitial = { name: "", platform: "", platform_account_key: "", - tags: "", + tags: [], cookies: "", }; @@ -126,10 +128,7 @@ function AccountCreateModal({ open, onClose, onSubmit, busy, error }) { name: form.name.trim(), platform: form.platform.trim(), platform_account_key: form.platform_account_key.trim(), - tags: form.tags - .split(/[,,]/) - .map((tag) => tag.trim()) - .filter(Boolean), + tags: form.tags, }; // cookies 非必填:留空代表创建后走扫码登录,凭据由后续同步链路补齐 if (form.cookies.trim()) data.cookies = form.cookies.trim(); @@ -216,12 +215,16 @@ function AccountCreateModal({ open, onClose, onSubmit, busy, error }) { } /> - - + update("tags", event.target.value)} - placeholder="如:主账号,直播" + onChange={(tags) => update("tags", tags)} + placeholder="如:主账号" /> - -
-
- - {account.name} - -

- {account.platform_account_key} · {account.platform} -

-
- -
-
- 授权: - {account.authorization_status === "authorized" ? "已授权" : "已撤销"} - · 账号: - {account.runtime_status === "active" ? "启用" : "暂停"} -
-
- 固定资源: - {bindingsError ? ( - "状态未知(环境数据不可用)" - ) : ( - - {binding?.name || "未绑定运行环境"} ·{" "} - {binding?.network_exit_id || "未绑定出口"} - - )} -
- - 查看账号 → - -
- - ); +function platformLabel(value) { + return platforms.find((platform) => platform.value === value)?.label || value; } export function AccountList() { @@ -378,16 +335,84 @@ export function AccountList() { empty={accounts.length === 0} emptyText="暂无账号。创建账号后,再前往运行环境绑定健康网络出口。" > -
- {accounts.map((account) => ( - - ))} -
+ ( +
+ + {account.name} + +

+ {account.platform_account_key} +

+
+ ), + }, + { + header: "平台", + width: "10%", + render: (account) => platformLabel(account.platform), + }, + { + header: "授权", + width: "10%", + render: (account) => + account.authorization_status === "authorized" + ? "已授权" + : "已撤销", + }, + { + header: "账号状态", + width: "14%", + render: (account) => + account.runtime_status === "active" ? "启用" : "暂停", + }, + { + header: "固定资源", + width: "24%", + render: (account) => { + const binding = bindingByAccount.get(account.id); + return bindingsError ? ( + + 状态未知(环境数据不可用) + + ) : ( +
+

+ {binding?.name || "未绑定运行环境"} +

+

+ {binding?.network_exit_id || "未绑定出口"} +

+
+ ); + }, + }, + { + header: "可调度", + width: "20%", + render: (account) => ( + + ), + }, + ]} + rows={accounts} + rowKey={(account) => account.id} + minWidth="760px" + /> new QueryClient({ defaultOptions: { queries: { retry: false } } }); @@ -98,13 +106,11 @@ function renderDetail(dataProvider) { describe("AccountList", () => { it("offers four platforms and submits only the account creation fields", async () => { const dataProvider = provider({ - create: vi - .fn() - .mockRejectedValue( - httpError("conflict", 409, { - reason_code: "duplicate_platform_account", - }), - ), + create: vi.fn().mockRejectedValue( + httpError("conflict", 409, { + reason_code: "duplicate_platform_account", + }), + ), }); renderList(dataProvider); await screen.findAllByText("店铺一号"); @@ -126,9 +132,10 @@ describe("AccountList", () => { fireEvent.change(within(dialog).getByRole("textbox", { name: "账号 ID" }), { target: { value: "shop-new" }, }); - fireEvent.change(within(dialog).getByRole("textbox", { name: "TAGS" }), { - target: { value: "主账号,直播" }, - }); + typeTags(within(dialog).getByRole("textbox", { name: "TAGS" }), [ + "主账号", + "直播", + ]); fireEvent.change(within(dialog).getByRole("textbox", { name: "Cookies" }), { target: { value: "sessionid=value; token=second" }, }); diff --git a/web/src/Layout.jsx b/web/src/Layout.jsx index c2b7dee..2d49b1e 100644 --- a/web/src/Layout.jsx +++ b/web/src/Layout.jsx @@ -1,93 +1,170 @@ -import { useEffect, useState } from 'react' -import { Link, Outlet, useLocation } from 'react-router' -import { cn } from './lib/ui.jsx' +import { useEffect, useState } from "react"; +import { Link, Outlet, useLocation } from "react-router"; +import { cn } from "./lib/ui.jsx"; -const expandedWidth = 240 -const collapsedWidth = 76 +const expandedWidth = 240; +const collapsedWidth = 76; const menu = [ - { section: '运营', items: [ - { to: '/accounts', icon: 'ri-account-circle-line', label: '社媒账号' }, - { to: '/tasks', icon: 'ri-task-line', label: '任务中心' }, - { to: '/audit', icon: 'ri-history-line', label: '审计记录' }, - ] }, - { section: '资源', items: [ - { to: '/browsers', icon: 'ri-hard-drive-3-line', label: '运行环境' }, - { to: '/network-exits', icon: 'ri-earth-line', label: '网络出口' }, - { to: '/browser-images', icon: 'ri-inbox-line', label: '镜像版本' }, - { to: '/gateways', icon: 'ri-router-line', label: '网关管理' }, - ] }, -] + { + section: "运营", + items: [ + { to: "/accounts", icon: "ri-account-circle-line", label: "社媒账号" }, + { to: "/tasks", icon: "ri-task-line", label: "任务中心" }, + { to: "/audit", icon: "ri-history-line", label: "审计记录" }, + ], + }, + { + section: "资源", + items: [ + { to: "/browsers", icon: "ri-hard-drive-3-line", label: "运行环境" }, + { to: "/network-exits", icon: "ri-earth-line", label: "网络出口" }, + { to: "/browser-images", icon: "ri-inbox-line", label: "镜像版本" }, + { to: "/gateways", icon: "ri-router-line", label: "网关管理" }, + ], + }, +]; function MenuLink({ to, icon, label, collapsed, onNavigate }) { - const { pathname } = useLocation() - const active = pathname === to || pathname.startsWith(`${to}/`) + const { pathname } = useLocation(); + const active = pathname === to || pathname.startsWith(`${to}/`); return ( - -