refactor: reorganize project by component

This commit is contained in:
2026-09-18 19:00:31 +08:00
parent 03ee6e3d6c
commit 2275075d7e
140 changed files with 3692 additions and 3275 deletions
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { Link, Outlet, useLocation } from "react-router";
import { cn } from "./lib/ui.jsx";
import { cn } from "../shared/ui/ui.jsx";
const expandedWidth = 240;
const collapsedWidth = 76;
@@ -99,7 +99,7 @@ function Sidebar({ collapsed, onNavigate }) {
);
}
export function CreatorHubLayout({ children }) {
export function CreatorHubLayout() {
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const { pathname } = useLocation();
@@ -112,11 +112,10 @@ export function CreatorHubLayout({ children }) {
const desktop =
typeof window !== "undefined" &&
window.matchMedia("(min-width: 768px)").matches;
const menuLabel = desktop
? collapsed
? "展开导航"
: "收起导航"
: "打开导航";
let menuLabel = "打开导航";
if (desktop) {
menuLabel = collapsed ? "展开导航" : "收起导航";
}
return (
<div className="flex min-h-screen bg-[#f7f8fa]">
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useNavigate } from "react-router";
import { Alert, Button, Field, Input } from "./lib/ui.jsx";
import { Alert, Button, Field, Input } from "../shared/ui/ui.jsx";
// 开发阶段:本地保存 Basic 凭证,仅在请求层自动附带。开发代理(vite)不注入 Authorization。
export function LoginPage() {
+14 -14
View File
@@ -2,22 +2,22 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { HashRouter, Navigate, Route, Routes, useLocation } from "react-router";
import { Refine } from "@refinedev/core";
import { dataProvider } from "./dataProvider";
import { dataProvider } from "../shared/api/dataProvider";
import { CreatorHubLayout } from "./Layout";
import { LoginPage } from "./LoginPage";
import { AccountCreatePage, AccountDetail, AccountList } from "./AccountsPage";
import { AuditPage } from "./AuditPage";
import { BrowserCreatePage, BrowserDetail, BrowserList } from "./BrowsersPage";
import { BrowserVersionList } from "./BrowserVersionsPage";
import { DraftPage } from "./DraftPage";
import { GatewayList } from "./GatewaysPage";
import { NetworkExitDetail, NetworkExitList } from "./NetworkExitsPage";
import { TaskDetail, TaskList } from "./TasksPage";
import { AttemptDetail } from "./TracePages";
import { AccountEditPage } from "./AccountEditPage";
import { CreatorCompetitorsPage } from "./CreatorCompetitorsPage";
import { CreatorSettingsPage } from "./CreatorSettingsPage";
import { CreatorWorkbenchPage } from "./CreatorWorkbenchPage";
import { AccountCreatePage, AccountDetail, AccountList } from "../features/accounts/AccountsPage";
import { AccountEditPage } from "../features/accounts/AccountEditPage";
import { CreatorCompetitorsPage } from "../features/creator/competitors/CreatorCompetitorsPage";
import { CreatorSettingsPage } from "../features/creator/settings/CreatorSettingsPage";
import { CreatorWorkbenchPage } from "../features/creator/workbench/CreatorWorkbenchPage";
import { AuditPage } from "../features/operations/AuditPage";
import { BrowserCreatePage, BrowserDetail, BrowserList } from "../features/environments/BrowsersPage";
import { BrowserVersionList } from "../features/environments/BrowserVersionsPage";
import { DraftPage } from "../features/operations/DraftPage";
import { GatewayList } from "../features/environments/GatewaysPage";
import { NetworkExitDetail, NetworkExitList } from "../features/environments/NetworkExitsPage";
import { TaskDetail, TaskList } from "../features/operations/TasksPage";
import { AttemptDetail } from "../features/operations/TracePages";
import "remixicon/fonts/remixicon.css";
import "./styles.css";
@@ -16,8 +16,8 @@ import {
Textarea,
dateTime,
conflictMessage,
useUnsavedChanges,
} from "./lib/ui.jsx";
} from "../../shared/ui/ui.jsx";
import { useUnsavedChanges } from "../../shared/hooks/hooks.js";
const emptyProfile = {
login_username: "",
@@ -19,8 +19,8 @@ import {
TagInput,
Textarea,
conflictMessage,
} from "./lib/ui.jsx";
import { useTitle } from "./lib/hooks.js";
} from "../../shared/ui/ui.jsx";
import { useTitle } from "../../shared/hooks/hooks.js";
const createInitial = {
name: "",
@@ -51,7 +51,7 @@ const reasonText = {
};
// 账号可恢复性判定:授权、绑定、出口健康、账号暂停四层都要看。
export function accountReadiness(account, binding, bindingsError = false) {
export function accountReadiness(account, binding, bindingError = null) {
if (account.authorization_status !== "authorized")
return {
label: "授权已撤销",
@@ -59,7 +59,7 @@ export function accountReadiness(account, binding, bindingsError = false) {
canResume: false,
ready: false,
};
if (bindingsError)
if (bindingError)
return {
label: "环境状态未知",
reason: "environment_unavailable",
@@ -73,11 +73,12 @@ export function accountReadiness(account, binding, bindingsError = false) {
canResume: false,
ready: false,
};
const blocked = binding.cleanup_pending
? "runtime_stop_pending"
: binding.network_exit_id && binding.network_exit_health !== "healthy"
? "network_exit_unhealthy"
: "";
let blocked = "";
if (binding.cleanup_pending) {
blocked = "runtime_stop_pending";
} else if (binding.network_exit_id && binding.network_exit_health !== "healthy") {
blocked = "network_exit_unhealthy";
}
if (blocked)
return {
label: reasonText[blocked],
@@ -105,12 +106,13 @@ export function accountReadiness(account, binding, bindingsError = false) {
};
}
function ReadinessPill({ readiness, paused }) {
const tone = readiness.ready
? "success"
: paused && readiness.canResume
? "warning"
: "danger";
function ReadinessPill({ readiness, runtimeStatus }) {
let tone = "danger";
if (readiness.ready) {
tone = "success";
} else if (runtimeStatus === "paused" && readiness.canResume) {
tone = "warning";
}
return <StatusPill tone={tone} label={readiness.label} />;
}
@@ -595,84 +597,96 @@ export function AccountList() {
{
header: "状态",
width: "16%",
render: (account) =>
account.source_type === "owned" ? (
<div className="space-y-1">
<StatusPill
tone={
account.runtime_status === "active"
? "success"
: "neutral"
}
label={
account.runtime_status === "active" ? "启用" : "暂停"
}
/>
<p className="text-xs text-muted">
{account.authorization_status === "authorized"
? "已授权"
: "已撤销"}
</p>
</div>
) : (
render: (account) => {
if (account.source_type === "owned") {
return (
<div className="space-y-1">
<StatusPill
tone={
account.runtime_status === "active"
? "success"
: "neutral"
}
label={
account.runtime_status === "active" ? "启用" : "暂停"
}
/>
<p className="text-xs text-muted">
{account.authorization_status === "authorized"
? "已授权"
: "已撤销"}
</p>
</div>
);
}
return (
<StatusPill
tone={account.enabled ? "success" : "neutral"}
label={account.enabled ? "监测中" : "已暂停"}
/>
),
);
},
},
{
header: "操作",
width: "18%",
render: (account) => (
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => openTags(account)}>
render: (account) => {
const actions = [
<Button key="tags" size="sm" onClick={() => openTags(account)}>
编辑标签
</Button>
{account.source_type === "owned" ? (
<>
<Button
size="sm"
as={Link}
to={`/accounts/${account.id}/edit`}
>
编辑账号
</Button>
<Button
size="sm"
as={Link}
to={`/accounts/${account.id}`}
>
查看账号
</Button>
</>
) : (
<>
<Button
size="sm"
onClick={() => syncCompetitor(account)}
busy={actionBusy === account.id}
>
同步
</Button>
<Button
size="sm"
onClick={() => toggleCompetitor(account)}
busy={actionBusy === account.id}
>
{account.enabled ? "暂停" : "恢复"}
</Button>
</>
)}
</Button>,
];
if (account.source_type === "owned") {
actions.push(
<Button
key="edit"
size="sm"
as={Link}
to={`/accounts/${account.id}/edit`}
>
编辑账号
</Button>,
<Button
key="view"
size="sm"
as={Link}
to={`/accounts/${account.id}`}
>
查看账号
</Button>,
);
} else {
actions.push(
<Button
key="sync"
size="sm"
onClick={() => syncCompetitor(account)}
busy={actionBusy === account.id}
>
同步
</Button>,
<Button
key="toggle"
size="sm"
onClick={() => toggleCompetitor(account)}
busy={actionBusy === account.id}
>
{account.enabled ? "暂停" : "恢复"}
</Button>,
);
}
actions.push(
<Button
key="delete"
size="sm"
onClick={() => deleteAccount(account)}
busy={deleteBusy === `${account.source_type}:${account.id}`}
>
删除
</Button>
</div>
),
</Button>,
);
return <div className="flex flex-wrap gap-2">{actions}</div>;
},
},
]}
rows={visibleRows}
@@ -717,6 +731,60 @@ export function AccountList() {
);
}
function BindingContent({ browsersError, binding }) {
if (browsersError) {
return (
<p className="text-sm text-muted">
运行环境与网络出口状态未知;重试成功后再执行依赖资源状态的操作。
</p>
);
}
if (binding) {
return (
<>
<DetailList
rows={[
["运行环境", `${binding.name}(${binding.alias})`],
[
"网络出口",
binding.network_exit_id
? `${binding.network_exit_id} · ${binding.network_exit_health || "未知状态"}`
: "当前机器直连",
],
["绑定版本", binding.binding_version],
[
"不可调度原因",
binding.schedule_block_reason
? reasonText[binding.schedule_block_reason] ||
binding.schedule_block_reason
: "无",
],
]}
/>
<Link
to="/browsers"
className="inline-flex text-sm font-medium text-primary hover:underline"
>
前往运行环境
</Link>
</>
);
}
return (
<>
<p className="text-sm text-muted">
尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。
</p>
<Link
to="/browsers"
className="inline-flex text-sm font-medium text-primary hover:underline"
>
前往运行环境
</Link>
</>
);
}
export function AccountDetail() {
const { id } = useParams();
const navigate = useNavigate();
@@ -750,7 +818,7 @@ export function AccountDetail() {
? undefined
: browsers.find((item) => item.account_id === id);
const readiness = account
? accountReadiness(account, binding, !!browsersError)
? accountReadiness(account, binding, browsersError)
: null;
useTitle("CreatorHub · 账号详情");
@@ -839,7 +907,7 @@ export function AccountDetail() {
<h2 className="text-base font-semibold">账号状态</h2>
<ReadinessPill
readiness={readiness}
paused={account.runtime_status === "paused"}
runtimeStatus={account.runtime_status}
/>
<DetailList
rows={[
@@ -878,51 +946,7 @@ export function AccountDetail() {
<Card>
<CardContent className="space-y-4">
<h2 className="text-base font-semibold">固定资源</h2>
{browsersError ? (
<p className="text-sm text-muted">
运行环境与网络出口状态未知;重试成功后再执行依赖资源状态的操作。
</p>
) : binding ? (
<>
<DetailList
rows={[
["运行环境", `${binding.name}(${binding.alias})`],
[
"网络出口",
binding.network_exit_id
? `${binding.network_exit_id} · ${binding.network_exit_health || "未知状态"}`
: "当前机器直连",
],
["绑定版本", binding.binding_version],
[
"不可调度原因",
binding.schedule_block_reason
? reasonText[binding.schedule_block_reason] ||
binding.schedule_block_reason
: "无",
],
]}
/>
<Link
to="/browsers"
className="inline-flex text-sm font-medium text-primary hover:underline"
>
前往运行环境
</Link>
</>
) : (
<>
<p className="text-sm text-muted">
尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。
</p>
<Link
to="/browsers"
className="inline-flex text-sm font-medium text-primary hover:underline"
>
前往运行环境
</Link>
</>
)}
<BindingContent browsersError={browsersError} binding={binding} />
</CardContent>
</Card>
<Card>
@@ -9,11 +9,11 @@ import {
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Refine } from "@refinedev/core";
import { MemoryRouter, Route, Routes } from "react-router";
import { AccountEditPage } from "./AccountEditPage";
import { AccountList } from "./AccountsPage";
import { CreatorCompetitorsPage } from "./CreatorCompetitorsPage";
import { CreatorSettingsPage } from "./CreatorSettingsPage";
import { CreatorWorkbenchPage } from "./CreatorWorkbenchPage";
import { AccountEditPage } from "../accounts/AccountEditPage";
import { AccountList } from "../accounts/AccountsPage";
import { CreatorCompetitorsPage } from "./competitors/CreatorCompetitorsPage";
import { CreatorSettingsPage } from "./settings/CreatorSettingsPage";
import { CreatorWorkbenchPage } from "./workbench/CreatorWorkbenchPage";
const profile = {
id: "account-a",
@@ -16,8 +16,8 @@ import {
Textarea,
dateTime,
conflictMessage,
useUnsavedChanges,
} from "./lib/ui.jsx";
} from "../../../shared/ui/ui.jsx";
import { useUnsavedChanges } from "../../../shared/hooks/hooks.js";
const platformOptions = [
{ value: "douyin", label: "抖音" },
@@ -10,8 +10,8 @@ import {
PageHeader,
PageState,
conflictMessage,
useUnsavedChanges,
} from "./lib/ui.jsx";
} from "../../../shared/ui/ui.jsx";
import { useUnsavedChanges } from "../../../shared/hooks/hooks.js";
const editableFields = [
"lookback_days",
@@ -15,8 +15,8 @@ import {
Textarea,
dateTime,
conflictMessage,
useUnsavedChanges,
} from "./lib/ui.jsx";
} from "../../../shared/ui/ui.jsx";
import { useUnsavedChanges } from "../../../shared/hooks/hooks.js";
const tabs = [
["comments", "评论"],
@@ -11,8 +11,8 @@ import {
PageState,
Switch,
Table,
} from "./lib/ui.jsx";
import { useTitle } from "./lib/hooks.js";
} from "../../shared/ui/ui.jsx";
import { useTitle } from "../../shared/hooks/hooks.js";
const versionPattern = /^\d[A-Za-z0-9._-]{0,63}$/;
const pathPattern = /^\/[A-Za-z0-9._+~/-]{1,4095}$/;
@@ -16,9 +16,8 @@ import {
Select,
StatusPill,
conflictMessage,
useUnsavedChanges,
} from "./lib/ui.jsx";
import { useTitle } from "./lib/hooks.js";
} from "../../shared/ui/ui.jsx";
import { useTitle, useUnsavedChanges } from "../../shared/hooks/hooks.js";
const aliasPattern = /^[a-z0-9][a-z0-9-]{0,31}$/;
const platformOptions = ["windows", "linux", "macos"].map((value) => ({
@@ -10,8 +10,8 @@ import {
PageHeader,
PageState,
Table,
} from "./lib/ui.jsx";
import { useTitle } from "./lib/hooks.js";
} from "../../shared/ui/ui.jsx";
import { useTitle } from "../../shared/hooks/hooks.js";
const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/;
@@ -17,8 +17,8 @@ import {
StatusPill,
Table,
conflictMessage,
} from "./lib/ui.jsx";
import { useTitle } from "./lib/hooks.js";
} from "../../shared/ui/ui.jsx";
import { useTitle } from "../../shared/hooks/hooks.js";
const createInitial = {
protocol: "socks5",
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router'
import { useList } from '@refinedev/core'
import { Alert, Button, PageHeader, PageState, Table } from './lib/ui.jsx'
import { useTitle } from './lib/hooks.js'
import { Alert, Button, PageHeader, PageState, Table } from '../../shared/ui/ui.jsx'
import { useTitle } from '../../shared/hooks/hooks.js'
const startOfDay = value => (value ? new Date(`${value}T00:00:00`).toISOString() : '')
const endOfDay = value => (value ? new Date(`${value}T23:59:59.999`).toISOString() : '')
@@ -4,8 +4,8 @@ import { useDataProvider, useList, useOne } from '@refinedev/core'
import {
Alert, Button, Card, CardContent, Checkbox, ConfirmDialog, DetailList, Field, Input, Modal,
PageHeader, PageState, StatusPill, conflictMessage,
} from './lib/ui.jsx'
import { useTitle } from './lib/hooks.js'
} from '../../shared/ui/ui.jsx'
import { useTitle } from '../../shared/hooks/hooks.js'
export function DraftPage() {
const { id } = useParams()
@@ -3,8 +3,8 @@ import { Link, useNavigate, useParams } from 'react-router'
import { useDataProvider, useList, useOne } from '@refinedev/core'
import {
Alert, Button, Card, CardContent, DetailList, Field, PageHeader, PageState, Select, StatusPill,
} from './lib/ui.jsx'
import { useTitle } from './lib/hooks.js'
} from '../../shared/ui/ui.jsx'
import { useTitle } from '../../shared/hooks/hooks.js'
const stateTone = { queued: 'neutral', executing: 'info', succeeded: 'success', failed: 'danger', needs_confirmation: 'warning', policy_hold: 'warning', cancelled: 'neutral' }
const stateText = { queued: '已排队', executing: '执行中', succeeded: '已成功', failed: '失败', needs_confirmation: '需要人工确认', policy_hold: '策略暂停', cancelled: '已取消' }
@@ -1,7 +1,7 @@
import { Link, useParams } from 'react-router'
import { useOne } from '@refinedev/core'
import { Alert, Button, Card, CardContent, DetailList, PageHeader, PageState } from './lib/ui.jsx'
import { useTitle } from './lib/hooks.js'
import { Alert, Button, Card, CardContent, DetailList, PageHeader, PageState } from '../../shared/ui/ui.jsx'
import { useTitle } from '../../shared/hooks/hooks.js'
function PendingOrError({ pending, error, children }) {
if (pending) return <PageState pending />
-16
View File
@@ -1,16 +0,0 @@
import { useEffect } from 'react'
export function useTitle(title) {
useEffect(() => { document.title = title }, [title])
}
// 401 之外的业务错误(404/409/503…)直接展示后端 message;401 在 dataProvider 里转跳登录页。
export function displayError(error) {
return error?.message || '操作失败'
}
export function conflictError(error, fallback) {
if (error?.status === 409) return `冲突(409):${error.body?.reason || fallback}`
if (error?.status === 503) return `资源未就绪(503):${error.message}`
return displayError(error)
}
+39
View File
@@ -0,0 +1,39 @@
import { useEffect } from 'react'
export function useTitle(title) {
useEffect(() => { document.title = title }, [title])
}
// 401 之外的业务错误(404/409/503…)直接展示后端 message;401 在 dataProvider 里转跳登录页。
export function displayError(error) {
return error?.message || '操作失败'
}
export function conflictError(error, fallback) {
if (error?.status === 409) return `冲突(409):${error.body?.reason || fallback}`
if (error?.status === 503) return `资源未就绪(503):${error.message}`
return displayError(error)
}
export function useUnsavedChanges(dirty) {
useEffect(() => {
if (!dirty) return undefined
const beforeUnload = (event) => {
event.preventDefault()
event.returnValue = ""
}
const click = (event) => {
const anchor = event.target.closest?.("a[href]")
if (!anchor || !anchor.getAttribute("href")?.startsWith("#/")) return
if (window.confirm("当前内容尚未保存,确定离开吗?")) return
event.preventDefault()
event.stopPropagation()
}
window.addEventListener("beforeunload", beforeUnload)
document.addEventListener("click", click, true)
return () => {
window.removeEventListener("beforeunload", beforeUnload)
document.removeEventListener("click", click, true)
}
}, [dirty])
}
@@ -21,29 +21,6 @@ export function conflictMessage(error, fallback) {
export const dateTime = (value) => new Date(value).toLocaleString("zh-CN");
export function useUnsavedChanges(dirty) {
useEffect(() => {
if (!dirty) return undefined;
const beforeUnload = (event) => {
event.preventDefault();
event.returnValue = "";
};
const click = (event) => {
const anchor = event.target.closest?.("a[href]");
if (!anchor || !anchor.getAttribute("href")?.startsWith("#/")) return;
if (window.confirm("当前内容尚未保存,确定离开吗?")) return;
event.preventDefault();
event.stopPropagation();
};
window.addEventListener("beforeunload", beforeUnload);
document.addEventListener("click", click, true);
return () => {
window.removeEventListener("beforeunload", beforeUnload);
document.removeEventListener("click", click, true);
};
}, [dirty]);
}
/**
* 页面加载/错误/空态的统一外壳。
*/