feat(web): task-2 布局骨架+登录页(ProLayout 菜单对齐归档版、Basic Auth 登录、401 全局登出、/api 代理)+ 领域 API 层(路径对齐 dataProvider)
This commit is contained in:
Generated
+109
-54
@@ -10,11 +10,12 @@
|
||||
"@ant-design/pro-components": "^3.1.14-7",
|
||||
"@umijs/max": "^4.7.19",
|
||||
"antd": "^6.6.5",
|
||||
"umi": "^4.7.19"
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"typescript": "^5.0.3"
|
||||
}
|
||||
},
|
||||
@@ -5255,30 +5256,23 @@
|
||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"devOptional": true,
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
|
||||
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
"@types/react": "^19.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router": {
|
||||
@@ -8675,6 +8669,40 @@
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/preset-umi/node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/preset-umi/node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/preset-umi/node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/react-refresh-webpack-plugin": {
|
||||
"version": "0.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@umijs/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz",
|
||||
@@ -8778,6 +8806,23 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/renderer-react/node_modules/react-helmet-async": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",
|
||||
"integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"invariant": "^2.2.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"shallowequal": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.6.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/renderer-react/node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
@@ -8803,6 +8848,40 @@
|
||||
"react-router-dom": "6.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/server/node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/server/node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/server/node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/test": {
|
||||
"version": "4.7.19",
|
||||
"resolved": "https://registry.npmjs.org/@umijs/test/-/test-4.7.19.tgz",
|
||||
@@ -19629,28 +19708,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
|
||||
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
"scheduler": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
"react": "^19.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable": {
|
||||
@@ -19688,23 +19763,6 @@
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-helmet-async": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",
|
||||
"integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"invariant": "^2.2.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"shallowequal": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.6.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz",
|
||||
@@ -20622,13 +20680,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
|
||||
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
|
||||
+5
-3
@@ -10,11 +10,13 @@
|
||||
"@ant-design/icons": "^6.3.4",
|
||||
"@ant-design/pro-components": "^3.1.14-7",
|
||||
"@umijs/max": "^4.7.19",
|
||||
"antd": "^6.6.5"
|
||||
"antd": "^6.6.5",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"typescript": "^5.0.3"
|
||||
},
|
||||
"name": "creator-hub-web"
|
||||
|
||||
+3066
-77
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
import { history } from '@umijs/max';
|
||||
|
||||
// 全局认证守卫(对齐 web.archived AuthGate 语义):
|
||||
// - localStorage('creatorhub.auth') 缺失 → 一律重定向 /login
|
||||
// - /login 已登录 → 重定向 /(避免重复登录)
|
||||
export function onRouteChange(clientRoutes: unknown, location: { pathname: string }) {
|
||||
const authed = localStorage.getItem('creatorhub.auth') !== null;
|
||||
if (!authed && location.pathname !== '/login') {
|
||||
history.replace('/login');
|
||||
return;
|
||||
}
|
||||
if (authed && location.pathname === '/login') {
|
||||
history.replace('/');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,70 @@
|
||||
// TODO: 从 web.archived 迁移 layout-wrapper/index
|
||||
export default function Page() {
|
||||
return null;
|
||||
import { history } from '@umijs/max';
|
||||
import { ProLayout } from '@ant-design/pro-components';
|
||||
import { Dropdown } from 'antd';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import type { MenuDataItem } from '@ant-design/pro-components';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
// 菜单结构与 web.archived/src/app/Layout.jsx 完全一致(运营 / 资源 两组)。
|
||||
const menu: MenuDataItem[] = [
|
||||
{
|
||||
name: '运营',
|
||||
children: [
|
||||
{ path: '/accounts/import', name: '账号导入' },
|
||||
{ path: '/accounts', name: '我的账号' },
|
||||
{ path: '/accounts/monitoring', name: '监控账号' },
|
||||
{ path: '/creator/competitors', name: '竞品分析' },
|
||||
{ path: '/creator/workbench', name: '运营工作台' },
|
||||
{ path: '/creator/settings', name: '采集设置' },
|
||||
{ path: '/tasks', name: '任务中心' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '资源',
|
||||
children: [
|
||||
{ path: '/browsers', name: '运行环境' },
|
||||
{ path: '/network-exits', name: '网络出口' },
|
||||
{ path: '/gateways', name: '网关管理' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ProLayout
|
||||
title="CreatorHub"
|
||||
logo={false}
|
||||
layout="mix"
|
||||
fixSiderbar
|
||||
route={{ routes: menu }}
|
||||
menuItemRender={(item, dom) => (
|
||||
<a onClick={() => item.path && history.push(item.path)}>{dom}</a>
|
||||
)}
|
||||
avatarProps={{
|
||||
icon: <LogoutOutlined />,
|
||||
size: 'small',
|
||||
render: (_props, dom) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
onClick: () => {
|
||||
localStorage.removeItem('creatorhub.auth');
|
||||
history.replace('/login');
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{dom}
|
||||
</Dropdown>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ProLayout>
|
||||
);
|
||||
}
|
||||
|
||||
+56
-3
@@ -1,4 +1,57 @@
|
||||
// TODO: 从 web.archived 迁移 login
|
||||
export default function Page() {
|
||||
return null;
|
||||
import { history } from '@umijs/max';
|
||||
import { Alert, Button, Card, Form, Input } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { request } from '@/requestErrorConfig';
|
||||
|
||||
// 语义对齐 web.archived/src/app/LoginPage.jsx:
|
||||
// 提交 GET /api/gateways 校验 Basic 凭证,成功后写入 localStorage('creatorhub.auth')。
|
||||
export default function LoginPage() {
|
||||
const [form] = Form.useForm<{ username: string; password: string }>();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
async function onFinish({ username, password }: { username: string; password: string }) {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
const controller = new AbortController();
|
||||
const timer = window.setTimeout(() => controller.abort(), 15000);
|
||||
try {
|
||||
const response = await fetch('/api/gateways', {
|
||||
headers: { Authorization: `Basic ${btoa(`${username}:${password}`)}` },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (response.status === 401) throw new Error('用户名或密码不正确');
|
||||
if (!response.ok) throw new Error(`认证服务不可用(${response.status})`);
|
||||
localStorage.setItem('creatorhub.auth', `${username}:${password}`);
|
||||
history.replace('/');
|
||||
} catch (reason: any) {
|
||||
setError(
|
||||
reason.name === 'AbortError'
|
||||
? '认证请求超时,请检查控制面连接'
|
||||
: reason.message || '登录失败',
|
||||
);
|
||||
} finally {
|
||||
window.clearTimeout(timer);
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', alignItems: 'center', justifyContent: 'center', padding: 16 }}>
|
||||
<Card title="CreatorHub · 登录" style={{ width: 360 }}>
|
||||
{error ? <Alert type="error" message={error} style={{ marginBottom: 16 }} /> : null}
|
||||
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item label="用户名" name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input autoComplete="username" />
|
||||
</Form.Item>
|
||||
<Form.Item label="密码" name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||
<Input.Password autoComplete="current-password" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={busy}>
|
||||
{busy ? '登录中…' : '登录'}
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// 对齐 web.archived dataProvider 的请求语义(fetch → Basic Auth → 401 全局登出)。
|
||||
export const AUTH_STORAGE_KEY = 'creatorhub.auth';
|
||||
|
||||
export class RequestError extends Error {
|
||||
status: number;
|
||||
body?: unknown;
|
||||
constructor(message: string, status: number, body?: unknown) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
export function unauthorized(): never {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.history.replaceState({}, '', '/login');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}
|
||||
throw new RequestError('认证已过期,请重新登录', 401);
|
||||
}
|
||||
|
||||
export async function request(path: string, options: RequestInit = {}): Promise<any> {
|
||||
const auth = localStorage.getItem(AUTH_STORAGE_KEY);
|
||||
const headers: Record<string, string> = { ...(options.headers as Record<string, string>) };
|
||||
if (auth) headers.Authorization = `Basic ${btoa(auth)}`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`/api${path}`, { ...options, headers });
|
||||
} catch (networkError: any) {
|
||||
throw new RequestError(`网络请求失败:${networkError?.message ?? networkError}`, 0);
|
||||
}
|
||||
if (response.status === 401) unauthorized();
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
throw new RequestError(body.error || `请求失败 (${response.status})`, response.status, body);
|
||||
}
|
||||
if (response.status === 204) return null;
|
||||
const contentType = response.headers?.get?.('content-type') || '';
|
||||
if (contentType.toLowerCase().includes('text/html')) {
|
||||
throw new RequestError('API 返回了非 JSON 响应,请检查控制面路由', response.status);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function jsonOptions(method: string, data: unknown): RequestInit {
|
||||
return {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// 领域 API 层:资源路径与语义逐一对照 web.archived/src/shared/api/dataProvider.js。
|
||||
// 全部经由 requestErrorConfig.request(Basic Auth + 401 全局登出)。
|
||||
import { jsonOptions, request } from '@/requestErrorConfig';
|
||||
|
||||
export const resourcePaths = {
|
||||
browsers: '/browsers',
|
||||
gateways: '/gateways',
|
||||
accounts: '/phase-a/accounts',
|
||||
drafts: '/phase-a/drafts',
|
||||
confirmations: '/phase-a/confirmations',
|
||||
tasks: '/phase-a/tasks',
|
||||
attempts: '/phase-a/attempts',
|
||||
'network-exits': '/network-exits',
|
||||
'creator-accounts': '/creator/accounts',
|
||||
'creator-competitors': '/creator/competitors',
|
||||
'creator-competitor-share-jobs': '/creator/competitor-share-jobs',
|
||||
'creator-works': '/creator/works',
|
||||
'creator-comments': '/creator/comments',
|
||||
'creator-leads': '/creator/leads',
|
||||
'creator-rules': '/creator/rules',
|
||||
'creator-operations': '/creator/operations',
|
||||
'creator-conversations': '/creator/conversations',
|
||||
'creator-events': '/creator/events',
|
||||
'creator-listeners': '/creator/listeners',
|
||||
} as const;
|
||||
|
||||
export type Resource = keyof typeof resourcePaths;
|
||||
|
||||
// 与归档版 filterKeys 一致:资源 → 允许透传的查询字段。
|
||||
const filterKeys: Partial<Record<Resource, string[]>> = {
|
||||
drafts: ['account_id'],
|
||||
confirmations: ['draft_id'],
|
||||
tasks: ['account_id', 'draft_id', 'state'],
|
||||
'creator-competitors': ['platform'],
|
||||
'creator-competitor-share-jobs': ['platform', 'status'],
|
||||
'creator-works': ['platform', 'source_id', 'source_type', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares'],
|
||||
'creator-comments': ['platform', 'work_id'],
|
||||
'creator-leads': ['platform'],
|
||||
'creator-operations': ['account_id'],
|
||||
'creator-conversations': ['account_id'],
|
||||
'creator-events': ['account_id'],
|
||||
};
|
||||
|
||||
export interface ListParams {
|
||||
resource: Resource;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
filters?: Record<string, string | number | undefined>;
|
||||
}
|
||||
|
||||
// creator-* 走 page/page_size 分页;其余资源由后端返回全量或自带 total。
|
||||
export async function getList<T = any>({ resource, page, pageSize, filters = {} }: ListParams): Promise<{ data: T[]; total: number; hasNext?: boolean }> {
|
||||
const path = resourcePaths[resource];
|
||||
const query = new URLSearchParams();
|
||||
const allowed = filterKeys[resource] ?? [];
|
||||
for (const key of allowed) {
|
||||
const value = filters[key];
|
||||
if (value !== undefined && value !== null && `${value}` !== '') query.set(key, `${value}`);
|
||||
}
|
||||
if (resource.startsWith('creator-')) {
|
||||
if (page) query.set('page', `${page}`);
|
||||
if (pageSize) query.set('page_size', `${pageSize}`);
|
||||
}
|
||||
const records = await request(`${path}${query.size ? `?${query}` : ''}`);
|
||||
const data: T[] = Array.isArray(records) ? records : records.data;
|
||||
return {
|
||||
data: data.map((record: any, index: number) => ({ ...record, id: record.id ?? record.alias ?? record.version ?? record.name ?? index })),
|
||||
total: records.total ?? data.length,
|
||||
...(records && !Array.isArray(records) && Object.hasOwn(records, 'has_next') ? { hasNext: Boolean(records.has_next) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'browsers', 'drafts', 'confirmations', 'tasks', 'attempts', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules', 'creator-operations'];
|
||||
|
||||
export async function getOne<T = any>(resource: Resource, id: string | number): Promise<T> {
|
||||
if (!GETONE_ALLOWED.includes(resource)) throw new Error(`${resource} 不支持 getOne`);
|
||||
return request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
export async function create(resource: Resource, variables: unknown): Promise<any> {
|
||||
return request(resourcePaths[resource], jsonOptions('POST', variables));
|
||||
}
|
||||
|
||||
export async function update(resource: Resource, id: string | number, variables: unknown): Promise<void> {
|
||||
if (resource === 'browsers') throw new Error('browsers 不支持 update');
|
||||
await request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`, jsonOptions('PUT', variables));
|
||||
}
|
||||
|
||||
export async function remove(resource: Resource, id: string | number): Promise<void> {
|
||||
if (!['accounts', 'creator-competitors'].includes(resource)) throw new Error(`${resource} 不支持 deleteOne`);
|
||||
await request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// ===== 显式动词的领域动作(不伪装成 CRUD)=====
|
||||
|
||||
export async function browserAction(alias: string, action: 'start' | 'stop' | 'recycle'): Promise<void> {
|
||||
const targets: Record<string, [string, string]> = {
|
||||
start: [`/browsers/${encodeURIComponent(alias)}/start`, 'POST'],
|
||||
stop: [`/browsers/${encodeURIComponent(alias)}/stop`, 'POST'],
|
||||
recycle: [`/browsers/${encodeURIComponent(alias)}`, 'DELETE'],
|
||||
};
|
||||
const target = targets[action];
|
||||
if (!target) throw new Error(`未知运行环境操作: ${action}`);
|
||||
await request(target[0], { method: target[1] });
|
||||
}
|
||||
|
||||
export async function accountAction(id: string, action: 'pause' | 'resume'): Promise<void> {
|
||||
if (action !== 'pause' && action !== 'resume') throw new Error(`未知账号操作: ${action}`);
|
||||
await request(`/phase-a/accounts/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function createDraft(accountID: string, content: string) {
|
||||
return request('/phase-a/drafts', jsonOptions('POST', { account_id: accountID, content }));
|
||||
}
|
||||
|
||||
export function confirmDraft(draftID: string, accountVersion: number, draftVersion: number) {
|
||||
return request('/phase-a/confirmations', jsonOptions('POST', { draft_id: draftID, account_version: accountVersion, draft_version: draftVersion }));
|
||||
}
|
||||
|
||||
export function enqueueConfirmation(confirmationID: string) {
|
||||
return request('/phase-a/tasks', jsonOptions('POST', { confirmation_id: confirmationID }));
|
||||
}
|
||||
|
||||
export async function taskAction(id: string, action: 'verify' | 'resume' | 'finish' | 'cancel', data?: unknown): Promise<void> {
|
||||
if (!['verify', 'resume', 'finish', 'cancel'].includes(action)) throw new Error(`未知任务操作: ${action}`);
|
||||
const options = data ? jsonOptions('POST', data) : { method: 'POST' };
|
||||
await request(`/phase-a/tasks/${encodeURIComponent(id)}/${action}`, options);
|
||||
}
|
||||
|
||||
export async function networkExitAction(id: string, action: 'check' | 'disable' | 'enable'): Promise<any> {
|
||||
if (!['check', 'disable', 'enable'].includes(action)) throw new Error(`未知网络出口操作: ${action}`);
|
||||
return request(`/network-exits/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
// creator-* 领域的透传请求(路径由调用方给出,含列表页/actions)。
|
||||
export function creatorRequest(path: string, options?: RequestInit) {
|
||||
return request(path, options);
|
||||
}
|
||||
|
||||
export function creatorAction(path: string, data?: unknown) {
|
||||
return request(path, data === undefined ? { method: 'POST' } : jsonOptions('POST', data));
|
||||
}
|
||||
|
||||
export function creatorCreate(path: string, data: unknown) {
|
||||
return request(path, jsonOptions('POST', data));
|
||||
}
|
||||
|
||||
export function creatorGet(path: string) {
|
||||
return request(path);
|
||||
}
|
||||
|
||||
export function creatorUpdate(path: string, data: unknown) {
|
||||
return request(path, jsonOptions('PUT', data));
|
||||
}
|
||||
|
||||
export function creatorSyncConversation(id: string, limit = 200) {
|
||||
return request(`/creator/conversations/${encodeURIComponent(id)}/sync?limit=${encodeURIComponent(limit)}`, { method: 'POST' });
|
||||
}
|
||||
Reference in New Issue
Block a user