refactor: reorganize project by component
This commit is contained in:
@@ -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() {
|
||||
@@ -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",
|
||||
+2
-2
@@ -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: "抖音" },
|
||||
+2
-2
@@ -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",
|
||||
+2
-2
@@ -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", "评论"],
|
||||
+2
-2
@@ -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 />
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面加载/错误/空态的统一外壳。
|
||||
*/
|
||||
Reference in New Issue
Block a user