新增 React Ant Design 前端骨架

This commit is contained in:
yml2213
2026-07-07 18:15:02 +08:00
parent c2c168f18e
commit 45d4df74fc
61 changed files with 6869 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>order-site react</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "order-site-frontend-react",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"typecheck": "tsc -b --noEmit",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@tanstack/react-query": "^5.90.12",
"antd": "^6.1.1",
"axios": "^1.13.2",
"qrcode": "^1.5.4",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-router": "^7.10.1"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "~6.0.2",
"vite": "^8.0.7"
}
}
+79
View File
@@ -0,0 +1,79 @@
import { Suspense, lazy } from 'react'
import { Spin } from 'antd'
import { HashRouter, Navigate, Outlet, Route, Routes, useLocation } from 'react-router'
import { getAdminRole, hasAdminSession } from '@/utils/admin-auth'
import AdminLayout from '@/layouts/AdminLayout'
const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage'))
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage'))
const AdminTasksPage = lazy(() => import('@/pages/admin/AdminTasksPage'))
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
function RequireAdmin() {
const location = useLocation()
if (!hasAdminSession()) {
return <Navigate to="/admin/login" replace state={{ from: location }} />
}
return <Outlet />
}
function RequireRole({ roles }: { roles: Array<'admin' | 'operator' | 'support'> }) {
if (!roles.includes(getAdminRole())) {
return <Navigate to="/admin/dashboard" replace />
}
return <Outlet />
}
function LoginRoute() {
if (hasAdminSession()) {
return <Navigate to="/admin/dashboard" replace />
}
return <AdminLoginPage />
}
export default function App() {
return (
<HashRouter>
<Suspense fallback={<RouteLoading />}>
<Routes>
<Route path="/" element={<Navigate to="/admin/dashboard" replace />} />
<Route path="/admin/login" element={<LoginRoute />} />
<Route element={<RequireAdmin />}>
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<Navigate to="/admin/dashboard" replace />} />
<Route path="dashboard" element={<AdminDashboardPage />} />
<Route path="orders" element={<AdminOrdersPage />} />
<Route path="orders/:orderId" element={<AdminOrderDetailPage />} />
<Route path="tasks" element={<AdminTasksPage />} />
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
<Route element={<RequireRole roles={['admin']} />}>
<Route path="users" element={<NotFoundPage title="用户管理迁移中" />} />
<Route path="platform-shops" element={<NotFoundPage title="平台配置迁移中" />} />
<Route path="platform-fulfillment" element={<NotFoundPage title="履约配置迁移中" />} />
<Route path="audit-logs" element={<NotFoundPage title="审计日志迁移中" />} />
</Route>
</Route>
</Route>
<Route path="/claim/:token" element={<NotFoundPage title="领取页迁移中" />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</HashRouter>
)
}
function RouteLoading() {
return (
<div className="route-loading">
<Spin />
</div>
)
}
@@ -0,0 +1,7 @@
type JsonPreviewProps = {
value: unknown
}
export default function JsonPreview({ value }: JsonPreviewProps) {
return <pre className="json-preview">{JSON.stringify(value ?? {}, null, 2)}</pre>
}
@@ -0,0 +1,22 @@
import { Space, Typography } from 'antd'
import type { ReactNode } from 'react'
type PageHeaderProps = {
title: string
description?: string
extra?: ReactNode
}
export default function PageHeader({ title, description, extra }: PageHeaderProps) {
return (
<header className="page-header">
<div>
<Typography.Title level={2} className="page-title">
{title}
</Typography.Title>
{description ? <p className="page-description">{description}</p> : null}
</div>
{extra ? <Space wrap>{extra}</Space> : null}
</header>
)
}
@@ -0,0 +1,75 @@
import { Tag } from 'antd'
type StatusTagProps = {
value?: string | null
kind?: 'task' | 'pay' | 'delivery' | 'resource' | 'customer' | 'role' | 'plain'
}
const STATUS_LABELS: Record<string, string> = {
active: '生效中',
claimed: '已创建会话',
closed: '已关闭',
completed: '已完成',
consumed: '已核销',
dispatched_pending_return: '待退号',
failed: '失败',
link_generated: '已生成链接',
manual_review: '人工处理',
paid: '已支付',
pending: '待处理',
pending_binding_prepare: '待准备资源',
pending_claim: '待领取',
pending_payment: '待支付',
redeemed: '已兑换',
redeeming: '兑换中',
refunded: '已退款',
retry_pending: '待重试',
role_confirmed: '已确认角色',
unpaid: '未支付',
waiting_binding: '待绑定',
}
const STATUS_COLORS: Record<string, string> = {
active: 'green',
claimed: 'blue',
closed: 'default',
completed: 'green',
consumed: 'green',
failed: 'red',
link_generated: 'cyan',
manual_review: 'orange',
paid: 'green',
pending: 'gold',
pending_binding_prepare: 'gold',
pending_claim: 'gold',
pending_payment: 'default',
redeemed: 'green',
redeeming: 'blue',
refunded: 'purple',
retry_pending: 'volcano',
role_confirmed: 'blue',
unpaid: 'default',
waiting_binding: 'gold',
}
export default function StatusTag({ value, kind = 'plain' }: StatusTagProps) {
const normalized = String(value || '').trim()
if (!normalized) {
return <span className="muted">-</span>
}
const key = normalized.toLowerCase()
const label = STATUS_LABELS[key] || normalized
const color = STATUS_COLORS[key] || resolveFallbackColor(kind)
return <Tag color={color}>{label === normalized ? normalized : `${label} (${normalized})`}</Tag>
}
function resolveFallbackColor(kind: StatusTagProps['kind']) {
if (kind === 'pay' || kind === 'delivery') return 'blue'
if (kind === 'task') return 'geekblue'
if (kind === 'resource' || kind === 'customer') return 'cyan'
if (kind === 'role') return 'purple'
return 'default'
}
@@ -0,0 +1,59 @@
export const TASK_STATUS = {
PENDING_PAYMENT: "pending_payment",
PAID: "paid",
LINK_GENERATED: "link_generated",
CLAIMED: "claimed",
PENDING_BINDING_PREPARE: "pending_binding_prepare",
WAITING_BINDING: "waiting_binding",
ROLE_CONFIRMED: "role_confirmed",
REDEEMING: "redeeming",
DISPATCHED_PENDING_RETURN: "dispatched_pending_return",
COMPLETED: "completed",
REDEEMED: "redeemed",
RETRY_PENDING: "retry_pending",
MANUAL_REVIEW: "manual_review",
FAILED: "failed",
EXPIRED: "expired",
CLOSED: "closed",
} as const;
export type KnownTaskStatus = (typeof TASK_STATUS)[keyof typeof TASK_STATUS];
export type TaskStatus = KnownTaskStatus | (string & {});
const CLAIM_INACTIVE_STATUSES = new Set<TaskStatus>([
TASK_STATUS.COMPLETED,
TASK_STATUS.REDEEMED,
TASK_STATUS.MANUAL_REVIEW,
TASK_STATUS.FAILED,
TASK_STATUS.EXPIRED,
TASK_STATUS.CLOSED,
]);
const KUAISHOU_CLOUD_RESULT_STATUSES = new Set<TaskStatus>([
TASK_STATUS.DISPATCHED_PENDING_RETURN,
TASK_STATUS.COMPLETED,
TASK_STATUS.REDEEMED,
TASK_STATUS.MANUAL_REVIEW,
TASK_STATUS.FAILED,
]);
export function normalizeTaskStatus(value: unknown): TaskStatus {
return String(value || "").trim() as TaskStatus;
}
export function isClaimInactiveTaskStatus(status: unknown): boolean {
return CLAIM_INACTIVE_STATUSES.has(normalizeTaskStatus(status));
}
export function isKuaishouCloudRoleConfirmedStatus(status: unknown): boolean {
return normalizeTaskStatus(status) === TASK_STATUS.ROLE_CONFIRMED;
}
export function isKuaishouCloudCompletedStatus(status: unknown): boolean {
const normalized = normalizeTaskStatus(status);
return normalized === TASK_STATUS.COMPLETED || normalized === TASK_STATUS.REDEEMED;
}
export function hasKuaishouCloudRedeemResultStatus(status: unknown): boolean {
return KUAISHOU_CLOUD_RESULT_STATUSES.has(normalizeTaskStatus(status));
}
@@ -0,0 +1,149 @@
import {
AuditOutlined,
DashboardOutlined,
FileSearchOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
OrderedListOutlined,
SettingOutlined,
ShopOutlined,
TeamOutlined,
UnorderedListOutlined,
UserOutlined,
} from '@ant-design/icons'
import { App, Button, Dropdown, Layout, Menu, Space, Typography } from 'antd'
import type { MenuProps } from 'antd'
import { useMemo, useState } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router'
import { logoutAdmin } from '@/services/admin'
import {
clearAdminSession,
getAdminRole,
getAdminTokenExpiresAt,
getAdminUsername,
} from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
const SIDEBAR_COLLAPSED_KEY = 'react-admin-sidebar-collapsed'
export default function AdminLayout() {
const navigate = useNavigate()
const location = useLocation()
const { message } = App.useApp()
const [collapsed, setCollapsed] = useState(
() => window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1',
)
const role = getAdminRole()
const username = getAdminUsername() || 'admin'
const expiresAt = getAdminTokenExpiresAt()
const menuItems = useMemo<MenuProps['items']>(() => {
const items: MenuProps['items'] = [
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
]
if (role === 'admin') {
items.splice(1, 0, { key: '/admin/users', icon: <TeamOutlined />, label: '用户' })
items.push(
{ key: '/admin/cloudtentacles-records', icon: <FileSearchOutlined />, label: '发货记录' },
{
key: '/admin/platform-shops',
icon: <SettingOutlined />,
label: '平台配置',
},
{ key: '/admin/platform-fulfillment', icon: <ShopOutlined />, label: '履约配置' },
{ key: '/admin/audit-logs', icon: <AuditOutlined />, label: '审计' },
)
}
return items
}, [role])
async function submitLogout() {
try {
await logoutAdmin()
} catch {
// 后端退出是无状态的,本地清理优先。
} finally {
clearAdminSession()
message.success('已退出后台')
navigate('/admin/login', { replace: true })
}
}
function toggleCollapsed() {
const next = !collapsed
setCollapsed(next)
window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, next ? '1' : '0')
}
const selectedKey = resolveSelectedKey(location.pathname)
return (
<Layout className="admin-shell">
<Layout.Sider
width={232}
collapsedWidth={72}
collapsed={collapsed}
className="admin-sider"
trigger={null}
>
<div className="admin-brand">
<strong>{collapsed ? 'OS' : '运营后台'}</strong>
{!collapsed ? <span></span> : null}
</div>
<Menu
mode="inline"
selectedKeys={[selectedKey]}
items={menuItems}
onClick={({ key }) => navigate(String(key))}
/>
</Layout.Sider>
<Layout>
<Layout.Header className="admin-topbar">
<Button
aria-label={collapsed ? '展开导航' : '折叠导航'}
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={toggleCollapsed}
/>
<Space className="admin-user" size={12}>
<div className="admin-user-copy">
<Typography.Text strong>{username}</Typography.Text>
<Typography.Text type="secondary">
{roleLabel(role)}
{expiresAt ? ` · ${formatAdminDateTime(expiresAt)} 到期` : ''}
</Typography.Text>
</div>
<Dropdown
menu={{
items: [{ key: 'logout', label: '退出登录' }],
onClick: submitLogout,
}}
placement="bottomRight"
>
<Button icon={<UserOutlined />}></Button>
</Dropdown>
</Space>
</Layout.Header>
<Layout.Content className="admin-content">
<Outlet />
</Layout.Content>
</Layout>
</Layout>
)
}
function resolveSelectedKey(pathname: string) {
if (pathname.startsWith('/admin/orders')) return '/admin/orders'
if (pathname.startsWith('/admin/tasks')) return '/admin/tasks'
return pathname
}
function roleLabel(role: string) {
if (role === 'admin') return '管理员'
if (role === 'operator') return '普通运营'
return '客服'
}
+86
View File
@@ -0,0 +1,86 @@
import { Input, Modal, message as antdMessage } from 'antd'
import type { ModalFuncProps } from 'antd'
import { createElement } from 'react'
type MessageOptions = {
duration?: number
}
type PromptResult = {
value: string
}
export function showSuccess(message: string, options: MessageOptions = {}) {
return antdMessage.success({
content: message,
duration: options.duration,
})
}
export function showError(message: string, options: MessageOptions = {}) {
return antdMessage.error({
content: message,
duration: options.duration,
})
}
export function showConfirm(
message: string,
title = '确认操作',
options: ModalFuncProps = {},
) {
return new Promise<void>((resolve, reject) => {
Modal.confirm({
title,
content: message,
okText: '继续执行',
cancelText: '取消',
centered: true,
...options,
onOk: () => {
options.onOk?.()
resolve()
},
onCancel: () => {
options.onCancel?.()
reject('cancel')
},
})
})
}
export function showPrompt(message: string, title: string, options: ModalFuncProps = {}) {
let value = ''
return new Promise<PromptResult>((resolve, reject) => {
Modal.confirm({
title,
content: createElement('div', { className: 'feedback-prompt' }, [
createElement('p', { key: 'message' }, message),
createElement(Input, {
key: 'input',
autoFocus: true,
onChange: (event) => {
value = event.target.value
},
}),
]),
okText: '提交',
cancelText: '取消',
centered: true,
...options,
onOk: () => {
options.onOk?.()
resolve({ value })
},
onCancel: () => {
options.onCancel?.()
reject('cancel')
},
})
})
}
export function isFeedbackDismissed(error: unknown) {
return error === 'cancel' || error === 'close'
}
+136
View File
@@ -0,0 +1,136 @@
import axios from 'axios'
import { clearAdminSession, getAdminToken } from '@/utils/admin-auth'
export interface ApiEnvelope<T> {
code: number
msg: string
data: T
errorCode?: string
}
const http = axios.create({
baseURL: '/',
timeout: 50_000,
headers: {
'Content-Type': 'application/json',
},
})
http.interceptors.response.use(
(response) => {
const data = response.data as ApiEnvelope<unknown>
// Business-level error: HTTP 200 but code !== 0
if (typeof data?.code === 'number' && data.code !== 0) {
const error = new Error(data.msg || '操作失败') as Error & {
errorCode?: string
code?: number
}
error.errorCode = data.errorCode
error.code = data.code
return Promise.reject(error)
}
return response.data
},
(error) => {
const responseMessage =
typeof error?.response?.data?.msg === 'string' ? error.response.data.msg.trim() : ''
const fallbackMessage = resolveFallbackHttpMessage(error)
const normalizedError = new Error(responseMessage || fallbackMessage) as Error & {
errorCode?: string
status?: number
}
if (error?.response?.data?.errorCode) {
normalizedError.errorCode = String(error.response.data.errorCode)
}
if (typeof error?.response?.status === 'number') {
normalizedError.status = error.response.status
}
if (error?.config?.url && String(error.config.url).startsWith('/api/v1/admin')) {
const status = Number(error?.response?.status || 0)
const errorCode = String(error?.response?.data?.errorCode || '').trim()
if (status === 401 && shouldClearAdminSession(errorCode)) {
clearAdminSession()
if (
window.location.hash.startsWith('#/admin') &&
!window.location.hash.startsWith('#/admin/login')
) {
window.location.hash = '#/admin/login'
}
}
}
return Promise.reject(normalizedError)
},
)
http.interceptors.request.use((config) => {
if (String(config.url || '').startsWith('/api/v1/admin')) {
const token = getAdminToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
}
return config
})
function resolveFallbackHttpMessage(error: unknown) {
const message = String((error as { message?: string })?.message ?? '')
if (message.includes('timeout')) {
return '网络超时'
}
if (message === 'Network Error') {
return '网络连接错误'
}
if (typeof (error as { response?: { statusText?: string } })?.response?.statusText === 'string') {
return String((error as { response: { statusText: string } }).response.statusText).trim()
}
return '接口请求失败'
}
function shouldClearAdminSession(errorCode: string) {
return [
'admin_auth_required',
'admin_auth_invalid',
'admin_auth_expired',
'admin_auth_user_invalid',
'admin_auth_stale',
].includes(errorCode)
}
function request<T>(config: Parameters<typeof http.request<ApiEnvelope<T>>>[0]) {
return http.request<ApiEnvelope<T>, ApiEnvelope<T>>(config)
}
export function apiGet<T>(url: string, params?: Record<string, unknown>) {
return request<T>({ method: 'GET', url, params })
}
export function apiGetBlob(url: string, params?: Record<string, unknown>) {
return http.request<Blob, Blob>({
method: 'GET',
url,
params,
responseType: 'blob',
})
}
export function apiPost<T>(url: string, data?: unknown) {
return request<T>({ method: 'POST', url, data: data as Record<string, unknown> })
}
export function apiDelete<T>(url: string) {
return request<T>({ method: 'DELETE', url })
}
+50
View File
@@ -0,0 +1,50 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { App as AntdApp, ConfigProvider } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import 'antd/dist/reset.css'
import App from './App'
import './styles/main.css'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
},
},
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: '#1677ff',
borderRadius: 6,
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
},
components: {
Layout: {
bodyBg: '#f5f7fb',
headerBg: '#ffffff',
siderBg: '#ffffff',
},
Card: {
borderRadiusLG: 6,
},
},
}}
>
<AntdApp>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</AntdApp>
</ConfigProvider>
</React.StrictMode>,
)
@@ -0,0 +1,20 @@
import { Button, Empty } from 'antd'
import { useNavigate } from 'react-router'
type NotFoundPageProps = {
title?: string
}
export default function NotFoundPage({ title = '页面不存在' }: NotFoundPageProps) {
const navigate = useNavigate()
return (
<main className="empty-page">
<Empty description={title}>
<Button type="primary" onClick={() => navigate('/admin/dashboard')}>
</Button>
</Empty>
</main>
)
}
@@ -0,0 +1,51 @@
import { Alert, Card, Col, Row, Skeleton, Statistic } from 'antd'
import { useQuery } from '@tanstack/react-query'
import PageHeader from '@/components/admin/PageHeader'
import { fetchAdminDashboardSummary } from '@/services/admin'
const cards = [
{ key: 'todayOrders', label: '今日订单' },
{ key: 'paidPendingClaim', label: '待领取' },
{ key: 'claimingTasks', label: '领取中' },
{ key: 'redeemedToday', label: '今日成功' },
{ key: 'abnormalTasks', label: '异常任务' },
] as const
export default function AdminDashboardPage() {
const query = useQuery({
queryKey: ['admin-dashboard-summary'],
queryFn: () => fetchAdminDashboardSummary(),
})
const summary = query.data?.data
return (
<section className="page-stack">
<PageHeader title="系统概览" description="快速查看订单和交付链路的运行状态。" />
{query.error ? (
<Alert
type="error"
showIcon
message={query.error instanceof Error ? query.error.message : '读取概览失败'}
/>
) : null}
{query.isLoading ? (
<Card>
<Skeleton active paragraph={{ rows: 8 }} />
</Card>
) : (
<Row gutter={[16, 16]}>
{cards.map((card) => (
<Col xs={24} md={12} xl={8} key={card.key}>
<Card>
<Statistic title={card.label} value={summary?.[card.key] ?? 0} />
</Card>
</Col>
))}
</Row>
)}
</section>
)
}
@@ -0,0 +1,74 @@
import { LockOutlined, UserOutlined } from '@ant-design/icons'
import { App, Button, Card, Form, Input, Typography } from 'antd'
import { useState } from 'react'
import { useNavigate } from 'react-router'
import { loginAdmin } from '@/services/admin'
import { setAdminSession } from '@/utils/admin-auth'
type LoginForm = {
username: string
password: string
}
export default function AdminLoginPage() {
const navigate = useNavigate()
const { message } = App.useApp()
const [loading, setLoading] = useState(false)
async function submitLogin(values: LoginForm) {
setLoading(true)
try {
const response = await loginAdmin({
username: values.username.trim(),
password: values.password.trim(),
})
setAdminSession(response.data.token, response.data.expiresAt, response.data.user)
message.success('登录成功')
navigate('/admin/dashboard', { replace: true })
} catch (error) {
message.error(error instanceof Error ? error.message : '后台登录失败')
} finally {
setLoading(false)
}
}
return (
<main className="login-page">
<Card className="login-panel" variant="borderless">
<div className="login-heading">
<Typography.Text className="eyebrow">Order Site Admin</Typography.Text>
<Typography.Title level={1}></Typography.Title>
<Typography.Paragraph type="secondary">
使
</Typography.Paragraph>
</div>
<Form<LoginForm> layout="vertical" requiredMark={false} onFinish={submitLogin}>
<Form.Item
name="username"
label="账号"
rules={[{ required: true, message: '请输入账号' }]}
>
<Input prefix={<UserOutlined />} placeholder="输入账号" autoComplete="username" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="输入密码"
autoComplete="current-password"
/>
</Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}>
</Button>
</Form>
</Card>
</main>
)
}
@@ -0,0 +1,161 @@
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Alert, Button, Card, Descriptions, Skeleton, Space, Table, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useParams } from 'react-router'
import JsonPreview from '@/components/admin/JsonPreview'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminOrderDetail } from '@/services/admin'
import type { AdminOrderDetail, AdminTaskListItem } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
export default function AdminOrderDetailPage() {
const navigate = useNavigate()
const { orderId = '' } = useParams()
const query = useQuery({
queryKey: ['admin-order-detail', orderId],
queryFn: () => fetchAdminOrderDetail(orderId),
enabled: Boolean(orderId),
})
const detail = query.data?.data
if (query.isLoading) {
return (
<section className="page-stack">
<PageHeader title="订单详情" extra={<BackButton />} />
<Card>
<Skeleton active paragraph={{ rows: 10 }} />
</Card>
</section>
)
}
if (query.error || !detail) {
return (
<section className="page-stack">
<PageHeader title="订单详情" extra={<BackButton />} />
<Alert
type="error"
showIcon
message={query.error instanceof Error ? query.error.message : '订单不存在或读取失败'}
/>
</section>
)
}
const taskColumns: TableColumnsType<AdminTaskListItem> = [
{
title: '任务号',
dataIndex: 'taskNo',
minWidth: 180,
render: (_, row) => (
<Typography.Link onClick={() => navigate(`/admin/tasks/${row.taskId}`)}>
{row.taskNo}
</Typography.Link>
),
},
{
title: '状态',
minWidth: 220,
render: (_, row) => (
<Space size={[0, 6]} wrap>
<StatusTag value={row.status} kind="task" />
<StatusTag value={row.resourceStatus} kind="resource" />
<StatusTag value={row.customerStatus} kind="customer" />
</Space>
),
},
{
title: '角色',
minWidth: 160,
render: (_, row) => (
<div className="cell-stack">
<span>{row.roleName || '-'}</span>
<span className="muted">{row.roleId || '-'}</span>
</div>
),
},
{
title: '错误',
dataIndex: 'lastError',
minWidth: 220,
render: (value) => <span className="muted">{value || '-'}</span>,
},
]
return (
<section className="page-stack">
<PageHeader
title="订单详情"
description={detail.order.platformOrderId}
extra={<BackButton />}
/>
<Card title="基础信息">
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
<Descriptions.Item label="订单 ID">{detail.order.orderId}</Descriptions.Item>
<Descriptions.Item label="店铺">
{detail.order.shopName || detail.order.shopId || '-'}
</Descriptions.Item>
<Descriptions.Item label="平台订单号">{detail.order.platformOrderId}</Descriptions.Item>
<Descriptions.Item label="订单状态">
<StatusTag value={detail.order.orderStatus} />
</Descriptions.Item>
<Descriptions.Item label="支付状态">
<StatusTag value={detail.order.payStatus} kind="pay" />
</Descriptions.Item>
<Descriptions.Item label="金额">
{detail.order.totalAmount} {detail.order.currency}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{formatAdminDateTime(detail.order.createdAt)}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{formatAdminDateTime(detail.order.updatedAt)}
</Descriptions.Item>
<Descriptions.Item label="商品摘要">{detail.order.itemSummary || '-'}</Descriptions.Item>
</Descriptions>
</Card>
<Card title="商品明细">
<Table<AdminOrderDetail['items'][number]>
rowKey="orderItemId"
pagination={false}
dataSource={detail.items}
scroll={{ x: 760 }}
columns={[
{ title: '商品', dataIndex: 'itemTitle', minWidth: 240 },
{ title: 'SKU', dataIndex: 'skuCode', minWidth: 180 },
{ title: '数量', dataIndex: 'quantity', width: 90 },
{ title: '履约模式', dataIndex: 'deliveryMode', minWidth: 140 },
]}
/>
</Card>
<Card title="任务">
<Table<AdminTaskListItem>
rowKey="taskId"
pagination={false}
dataSource={detail.tasks}
columns={taskColumns}
scroll={{ x: 900 }}
/>
</Card>
<Card title="原始载荷">
<JsonPreview value={detail.order.rawPayload} />
</Card>
</section>
)
}
function BackButton() {
const navigate = useNavigate()
return (
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/admin/orders')}>
</Button>
)
}
@@ -0,0 +1,169 @@
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'
import { Button, Card, Form, Input, Select, Space, Table, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useNavigate, useSearchParams } from 'react-router'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminOrders } from '@/services/admin'
import type { AdminOrderListItem } from '@/types/admin'
import { adminPayStatusOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
type OrderFilterForm = {
platformOrderId?: string
payStatus?: string
skuCode?: string
}
export default function AdminOrdersPage() {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1) || 1
const pageSize = Number(searchParams.get('pageSize') || 20) || 20
const filters = {
platformOrderId: searchParams.get('platformOrderId') || '',
payStatus: searchParams.get('payStatus') || '',
skuCode: searchParams.get('skuCode') || '',
}
const queryParams = useMemo(
() => ({ page, pageSize, ...filters }),
[filters.platformOrderId, filters.payStatus, filters.skuCode, page, pageSize],
)
const query = useQuery({
queryKey: ['admin-orders', queryParams],
queryFn: () => fetchAdminOrders(queryParams),
})
const data = query.data?.data
const columns: TableColumnsType<AdminOrderListItem> = [
{
title: '订单信息',
dataIndex: 'platformOrderId',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Typography.Link onClick={() => navigate(`/admin/orders/${row.orderId}`)}>
{row.platformOrderId}
</Typography.Link>
<span className="muted">{row.shopName || row.shopId || row.provider}</span>
</div>
),
},
{
title: '商品 / 任务',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<span>{row.itemSummary || '-'}</span>
<span className="muted">
{row.itemCount} · {row.totalQuantity} · {row.taskCount}
</span>
</div>
),
},
{
title: '状态',
minWidth: 220,
render: (_, row) => (
<Space size={[0, 6]} wrap>
<StatusTag value={row.payStatus} kind="pay" />
<StatusTag value={row.resourceStatus} kind="resource" />
<StatusTag value={row.customerStatus} kind="customer" />
</Space>
),
},
{
title: '金额 / 时间',
minWidth: 180,
render: (_, row) => (
<div className="cell-stack">
<strong>{row.totalAmount || '0.00'} {row.currency}</strong>
<span className="muted">{formatAdminDateTime(row.createdAt)}</span>
</div>
),
},
]
function applyFilters(values: OrderFilterForm) {
setSearchParams(cleanParams({ ...values, page: 1, pageSize }))
}
function resetFilters() {
setSearchParams(cleanParams({ page: 1, pageSize }))
}
return (
<section className="page-stack">
<PageHeader title="订单" description="查看订单入库、商品匹配和任务拆分情况。" />
<Card>
<Form<OrderFilterForm>
layout="inline"
initialValues={filters}
onFinish={applyFilters}
className="filter-form"
>
<Form.Item name="platformOrderId">
<Input allowClear placeholder="平台订单号" />
</Form.Item>
<Form.Item name="payStatus">
<Select
allowClear
placeholder="支付状态"
options={adminPayStatusOptions}
style={{ minWidth: 140 }}
/>
</Form.Item>
<Form.Item name="skuCode">
<Input allowClear placeholder="SKU 标识" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
</Button>
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Card>
<Table<AdminOrderListItem>
rowKey="orderId"
loading={query.isLoading || query.isFetching}
columns={columns}
dataSource={data?.items || []}
scroll={{ x: 900 }}
pagination={{
current: data?.pagination.page || page,
pageSize: data?.pagination.pageSize || pageSize,
total: data?.pagination.total || 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setSearchParams(cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }))
},
}}
/>
</Card>
</section>
)
}
function cleanParams(params: Record<string, unknown>) {
const next = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
const normalized = String(value ?? '').trim()
if (normalized) {
next.set(key, normalized)
}
})
return next
}
@@ -0,0 +1,237 @@
import { ArrowLeftOutlined, CopyOutlined, ReloadOutlined } from '@ant-design/icons'
import {
Alert,
App,
Button,
Card,
Descriptions,
Empty,
Skeleton,
Space,
Table,
Tabs,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useParams } from 'react-router'
import JsonPreview from '@/components/admin/JsonPreview'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminTaskDetail } from '@/services/admin'
import type { AdminTaskDetail } from '@/types/admin'
import { formatTaskEventPayload, formatTaskEventType } from '@/utils/admin-display'
import { formatAdminDateTime } from '@/utils/admin-time'
type TaskEvent = AdminTaskDetail['events'][number]
export default function AdminTaskDetailPage() {
const navigate = useNavigate()
const { taskId = '' } = useParams()
const { message } = App.useApp()
const query = useQuery({
queryKey: ['admin-task-detail', taskId],
queryFn: () => fetchAdminTaskDetail(taskId),
enabled: Boolean(taskId),
})
const detail = query.data?.data
if (query.isLoading) {
return (
<section className="page-stack">
<PageHeader title="任务详情" extra={<BackButton />} />
<Card>
<Skeleton active paragraph={{ rows: 10 }} />
</Card>
</section>
)
}
if (query.error || !detail) {
return (
<section className="page-stack">
<PageHeader title="任务详情" extra={<BackButton />} />
<Alert
type="error"
showIcon
message={query.error instanceof Error ? query.error.message : '任务不存在或读取失败'}
/>
</section>
)
}
const resolvedDetail = detail
const flow = resolvedDetail.kuaishouCloudFulfillment
const eventColumns: TableColumnsType<TaskEvent> = [
{
title: '时间',
dataIndex: 'createdAt',
width: 180,
render: (value) => formatAdminDateTime(value),
},
{
title: '事件',
dataIndex: 'eventType',
width: 180,
render: (value) => formatTaskEventType(value),
},
{
title: '摘要',
render: (_, row) => formatTaskEventPayload(row.payload),
},
]
async function copyClaimUrl() {
const claimUrl = resolvedDetail.claimToken?.claimUrl || ''
if (!claimUrl) return
await navigator.clipboard.writeText(claimUrl)
message.success('领取链接已复制')
}
return (
<section className="page-stack">
<PageHeader
title="任务详情"
description={resolvedDetail.task.taskNo}
extra={
<Space wrap>
<Button icon={<ReloadOutlined />} onClick={() => query.refetch()}>
</Button>
<BackButton />
</Space>
}
/>
<Card title="基础信息">
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
<Descriptions.Item label="任务 ID">{resolvedDetail.task.taskId}</Descriptions.Item>
<Descriptions.Item label="任务号">{resolvedDetail.task.taskNo}</Descriptions.Item>
<Descriptions.Item label="平台订单号">
{resolvedDetail.task.platformOrderId || resolvedDetail.order?.platformOrderId || '-'}
</Descriptions.Item>
<Descriptions.Item label="任务状态">
<StatusTag value={resolvedDetail.task.status} kind="task" />
</Descriptions.Item>
<Descriptions.Item label="资源状态">
<StatusTag value={resolvedDetail.task.resourceStatus} kind="resource" />
</Descriptions.Item>
<Descriptions.Item label="客户步骤">
<StatusTag value={resolvedDetail.task.customerStatus} kind="customer" />
</Descriptions.Item>
<Descriptions.Item label="商品">
{resolvedDetail.orderItem?.skuName || resolvedDetail.task.skuName || '-'}
</Descriptions.Item>
<Descriptions.Item label="角色">
{resolvedDetail.task.roleName || '-'} {resolvedDetail.task.roleId ? `(${resolvedDetail.task.roleId})` : ''}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{formatAdminDateTime(resolvedDetail.task.updatedAt)}
</Descriptions.Item>
<Descriptions.Item label="最近错误" span={3}>
{resolvedDetail.task.lastError || '-'}
</Descriptions.Item>
</Descriptions>
</Card>
{resolvedDetail.claimToken ? (
<Card title="领取链接">
<Space direction="vertical" size={8} className="full-width">
<Typography.Text copyable={{ text: resolvedDetail.claimToken.claimUrl }}>
{resolvedDetail.claimToken.claimUrl}
</Typography.Text>
<Space wrap>
<StatusTag value={resolvedDetail.claimToken.status} />
<Typography.Text type="secondary">
{formatAdminDateTime(resolvedDetail.claimToken.expiredAt)}
</Typography.Text>
<Button icon={<CopyOutlined />} onClick={copyClaimUrl}>
</Button>
</Space>
</Space>
</Card>
) : null}
<Tabs
items={[
{
key: 'cloud',
label: '快手 Cloud',
children: flow ? <KuaishouCloudPanel flow={flow} /> : <Empty description="无快手 Cloud 履约上下文" />,
},
{
key: 'events',
label: '事件流水',
children: (
<Table<TaskEvent>
rowKey="eventId"
pagination={false}
columns={eventColumns}
dataSource={resolvedDetail.events}
scroll={{ x: 760 }}
/>
),
},
{
key: 'raw',
label: '原始数据',
children: <JsonPreview value={resolvedDetail} />,
},
]}
/>
</section>
)
}
function KuaishouCloudPanel({ flow }: { flow: NonNullable<AdminTaskDetail['kuaishouCloudFulfillment']> }) {
return (
<Card>
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
<Descriptions.Item label="来源账号">
{flow.binding.resolvedSourceLabel || flow.binding.resolvedSourceKey || '-'}
</Descriptions.Item>
<Descriptions.Item label="虚拟号">
{flow.binding.vnPhone || flow.binding.vnId || '-'}
</Descriptions.Item>
<Descriptions.Item label="绑定状态">
<StatusTag value={flow.binding.prepareStatus} />
</Descriptions.Item>
<Descriptions.Item label="角色">
{flow.role.name || '-'} {flow.role.rid ? `(${flow.role.rid})` : ''}
</Descriptions.Item>
<Descriptions.Item label="卡券">
<StatusTag value={flow.ticket.status} />
</Descriptions.Item>
<Descriptions.Item label="发货">
<StatusTag value={flow.dispatch.status} kind="delivery" />
</Descriptions.Item>
<Descriptions.Item label="核销">
<StatusTag value={flow.consume.status} />
</Descriptions.Item>
<Descriptions.Item label="退号">
<StatusTag value={flow.returnNumber.status} />
</Descriptions.Item>
<Descriptions.Item label="绑定链接" span={3}>
{flow.binding.bindUrl ? (
<Typography.Text copyable={{ text: flow.binding.bindUrl }}>
{flow.binding.bindUrl}
</Typography.Text>
) : (
'-'
)}
</Descriptions.Item>
</Descriptions>
</Card>
)
}
function BackButton() {
const navigate = useNavigate()
return (
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/admin/tasks')}>
</Button>
)
}
@@ -0,0 +1,191 @@
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import { Button, Card, Form, Input, Select, Space, Table, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useNavigate, useSearchParams } from 'react-router'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminTasks } from '@/services/admin'
import type { AdminTaskListItem } from '@/types/admin'
import { adminTaskStatusOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
type TaskFilterForm = {
status?: string
platformOrderId?: string
taskNo?: string
skuCode?: string
roleId?: string
}
export default function AdminTasksPage() {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1) || 1
const pageSize = Number(searchParams.get('pageSize') || 20) || 20
const filters = {
status: searchParams.get('status') || '',
platformOrderId: searchParams.get('platformOrderId') || '',
taskNo: searchParams.get('taskNo') || '',
skuCode: searchParams.get('skuCode') || '',
roleId: searchParams.get('roleId') || '',
}
const queryParams = useMemo(
() => ({ page, pageSize, ...filters }),
[
filters.platformOrderId,
filters.roleId,
filters.skuCode,
filters.status,
filters.taskNo,
page,
pageSize,
],
)
const query = useQuery({
queryKey: ['admin-tasks', queryParams],
queryFn: () => fetchAdminTasks(queryParams),
})
const data = query.data?.data
const columns: TableColumnsType<AdminTaskListItem> = [
{
title: '任务 / 订单',
minWidth: 230,
render: (_, row) => (
<div className="cell-stack">
<Typography.Link onClick={() => navigate(`/admin/tasks/${row.taskId}`)}>
{row.taskNo}
</Typography.Link>
<span className="muted">{row.platformOrderId}</span>
</div>
),
},
{
title: '商品',
minWidth: 210,
render: (_, row) => (
<div className="cell-stack">
<span>{row.skuName || '-'}</span>
<span className="muted">{row.skuCode || row.executorKey}</span>
</div>
),
},
{
title: '状态',
minWidth: 250,
render: (_, row) => (
<Space size={[0, 6]} wrap>
<StatusTag value={row.status} kind="task" />
<StatusTag value={row.resourceStatus} kind="resource" />
<StatusTag value={row.customerStatus} kind="customer" />
</Space>
),
},
{
title: '角色',
minWidth: 180,
render: (_, row) => (
<div className="cell-stack">
<span>{row.roleName || '-'}</span>
<span className="muted">{row.roleId || row.loginType || '-'}</span>
</div>
),
},
{
title: '时间 / 异常',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<span>{formatAdminDateTime(row.updatedAt)}</span>
<span className="muted">{row.lastError || '-'}</span>
</div>
),
},
]
function applyFilters(values: TaskFilterForm) {
setSearchParams(cleanParams({ ...values, page: 1, pageSize }))
}
function resetFilters() {
setSearchParams(cleanParams({ page: 1, pageSize }))
}
return (
<section className="page-stack">
<PageHeader title="任务" description="跟踪履约任务状态、客户步骤和资源准备进度。" />
<Card>
<Form<TaskFilterForm>
layout="inline"
initialValues={filters}
onFinish={applyFilters}
className="filter-form"
>
<Form.Item name="status">
<Select
allowClear
placeholder="任务状态"
options={adminTaskStatusOptions}
style={{ minWidth: 170 }}
/>
</Form.Item>
<Form.Item name="platformOrderId">
<Input allowClear placeholder="平台订单号" />
</Form.Item>
<Form.Item name="taskNo">
<Input allowClear placeholder="任务号" />
</Form.Item>
<Form.Item name="roleId">
<Input allowClear placeholder="角色 ID" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
</Button>
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Card>
<Table<AdminTaskListItem>
rowKey="taskId"
loading={query.isLoading || query.isFetching}
columns={columns}
dataSource={data?.items || []}
scroll={{ x: 1080 }}
pagination={{
current: data?.pagination.page || page,
pageSize: data?.pagination.pageSize || pageSize,
total: data?.pagination.total || 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setSearchParams(cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }))
},
}}
/>
</Card>
</section>
)
}
function cleanParams(params: Record<string, unknown>) {
const next = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
const normalized = String(value ?? '').trim()
if (normalized) {
next.set(key, normalized)
}
})
return next
}
@@ -0,0 +1,9 @@
import { apiGet } from '@/lib/http'
import type { AdminAuditLogItem, AdminPagination } from '@/types/admin'
export function fetchAdminAuditLogs(params?: Record<string, unknown>) {
return apiGet<{ items: AdminAuditLogItem[]; pagination: AdminPagination }>(
'/api/v1/admin/audit-logs',
params,
)
}
@@ -0,0 +1,14 @@
import { apiGet, apiPost } from '@/lib/http'
import type { AdminLoginResponse, AdminSessionSummary } from '@/types/admin'
export function loginAdmin(payload: { username: string; password: string }) {
return apiPost<AdminLoginResponse>('/api/v1/admin/auth/login', payload)
}
export function fetchAdminSession() {
return apiGet<AdminSessionSummary>('/api/v1/admin/auth/session')
}
export function logoutAdmin() {
return apiPost<{ success: boolean }>('/api/v1/admin/auth/logout', {})
}
@@ -0,0 +1,6 @@
import { apiGet } from '@/lib/http'
import type { AdminDashboardSummary } from '@/types/admin'
export function fetchAdminDashboardSummary() {
return apiGet<AdminDashboardSummary>('/api/v1/admin/dashboard/summary')
}
@@ -0,0 +1,7 @@
export * from './auth'
export * from './dashboard'
export * from './users'
export * from './audit-logs'
export * from './platform-config'
export * from './orders'
export * from './tasks'
@@ -0,0 +1,13 @@
import { apiGet } from '@/lib/http'
import type { AdminOrderDetail, AdminOrderListItem, AdminPagination } from '@/types/admin'
export function fetchAdminOrders(params?: Record<string, unknown>) {
return apiGet<{ items: AdminOrderListItem[]; pagination: AdminPagination }>(
'/api/v1/admin/orders',
params,
)
}
export function fetchAdminOrderDetail(orderId: number | string) {
return apiGet<AdminOrderDetail>(`/api/v1/admin/orders/${orderId}`)
}
@@ -0,0 +1,336 @@
import { apiDelete, apiGet, apiPost } from '@/lib/http'
import type {
AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesDeliveryRecordListResult,
AdminCloudtentaclesOverrideRuleConfig,
AdminCloudtentaclesSkuListResult,
AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSourceConfigResponse,
AdminCloudtentaclesSourceItem,
AdminCloudtentaclesSourcesConfig,
AdminCloudtentaclesValidateSessionResult,
} from '@/types/admin'
export function fetchAdminCloudtentaclesSourceConfig() {
return apiGet<AdminCloudtentaclesSourceConfigResponse>(
'/api/v1/admin/platform-config/cloudtentacles-source',
)
}
export function saveAdminCloudtentaclesSourceConfig(
payload:
| AdminCloudtentaclesSourcesConfig
| ({ sourceKey: string } & Partial<AdminCloudtentaclesSourceItem>),
) {
return apiPost<AdminCloudtentaclesSourceConfigResponse>(
'/api/v1/admin/platform-config/cloudtentacles-source',
payload,
)
}
export function deleteAdminCloudtentaclesSource(sourceKey: string) {
return apiDelete<{ success: boolean }>(
`/api/v1/admin/platform-config/cloudtentacles-source/${sourceKey}`,
)
}
export function fetchAdminCloudtentaclesOverrideRules() {
return apiGet<AdminCloudtentaclesOverrideRuleConfig>(
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
)
}
export function saveAdminCloudtentaclesOverrideRules(
payload: {
enabled: boolean
rules: Array<{
id?: string
enabled?: boolean
productName?: string
sourceKey?: string
deliveryItems?: Array<{
cloudSkuId?: number
cloudSkuName?: string
quantity?: number
}>
notes?: string
}>
},
) {
return apiPost<AdminCloudtentaclesOverrideRuleConfig>(
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
payload,
)
}
export function sendAdminCloudtentaclesSmsCode(payload: {
baseUrl?: string
username?: string
phone?: string
deviceId?: string
deviceType?: number
sourceKey?: string
}) {
return apiPost<AdminCloudtentaclesSendSmsResult>(
'/api/v1/admin/platform-config/cloudtentacles/send-sms-code',
payload,
)
}
export function testAdminCloudtentaclesLogin(payload: {
baseUrl?: string
username?: string
password?: string
phone?: string
code?: string
deviceId?: string
deviceType?: number
sourceKey?: string
}) {
return apiPost<AdminCloudtentaclesLoginTestResult>(
'/api/v1/admin/platform-config/cloudtentacles/test-login',
payload,
)
}
export function validateAdminCloudtentaclesSession(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
sourceKey?: string
}) {
return apiPost<AdminCloudtentaclesValidateSessionResult>(
'/api/v1/admin/platform-config/cloudtentacles/validate-session',
payload,
)
}
export function fetchAdminCloudtentaclesAsset(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/asset',
payload,
)
}
export function fetchAdminCloudtentaclesCategories(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/categories',
payload,
)
}
export function fetchAdminCloudtentaclesSkuList(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
sourceKey?: string
}) {
return apiPost<AdminCloudtentaclesSkuListResult>(
'/api/v1/admin/platform-config/cloudtentacles/sku/list',
payload,
)
}
export function buyAdminCloudtentaclesSku(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
id?: number
count?: number
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/sku/buy',
payload,
)
}
export function useAdminCloudtentaclesSku(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
id?: number
virtualNumberId?: number
phone?: string
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/sku/use',
payload,
)
}
export function fetchAdminCloudtentaclesKnapsack(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/knapsack',
payload,
)
}
export function fetchAdminCloudtentaclesDeliveryRecords(payload: {
sourceKey?: string
page?: number
size?: number
startDate?: string
endDate?: string
platformOrderId?: string
}) {
return apiPost<AdminCloudtentaclesDeliveryRecordListResult>(
'/api/v1/admin/cloudtentacles-records',
payload,
)
}
export function fetchAdminCloudtentaclesRecordSources() {
return apiGet<{
sources: Array<{
key: string
label: string
enabled: boolean
hasToken: boolean
loggedInAt: string
}>
}>('/api/v1/admin/cloudtentacles-records/sources')
}
export function fetchAdminCloudtentaclesVnList(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
key?: string
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/vn/list',
payload,
)
}
export function appointAdminCloudtentaclesVn(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
key?: string
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/vn/appoint',
payload,
)
}
export function generateAdminCloudtentaclesVnLoginCode(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
key?: string
id?: number
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/vn/generate-login-code',
payload,
)
}
export function fetchAdminCloudtentaclesVnCode(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
key?: string
phone?: string
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/vn/fetch-code',
payload,
)
}
export function verifyAdminCloudtentaclesVnCode(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
key?: string
id?: number
code?: string
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/vn/verify-code',
payload,
)
}
export function fetchAdminCloudtentaclesBindUrl(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
key?: string
id?: number
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/vn/bind-url',
payload,
)
}
export function backAdminCloudtentaclesVn(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
key?: string
id?: number
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/vn/back',
payload,
)
}
export function runAdminCloudtentaclesFullFlow(payload: {
baseUrl?: string
token?: string
deviceId?: string
deviceType?: number
skuId?: number
skuCount?: number
vnKey?: string
sourceKey?: string
}) {
return apiPost<Record<string, unknown>>(
'/api/v1/admin/platform-config/cloudtentacles/debug/full-flow',
payload,
)
}
@@ -0,0 +1,6 @@
export * from './notifications'
export * from './scheduled-jobs'
export * from './ninetyone'
export * from './kuaishou-eticket'
export * from './kuaishou-feifei'
export * from './cloudtentacles'
@@ -0,0 +1,60 @@
import { apiGet, apiPost } from '@/lib/http'
import type {
AdminKuaishouEticketConsumeResult,
AdminKuaishouEticketDetailResult,
AdminKuaishouEticketShopInfoResult,
AdminKuaishouEticketSourceConfig,
} from '@/types/admin'
export function fetchAdminKuaishouEticketSourceConfig() {
return apiGet<{
filePath: string
source: AdminKuaishouEticketSourceConfig
}>('/api/v1/admin/platform-config/kuaishou-eticket-source')
}
export function saveAdminKuaishouEticketSourceConfig(payload: AdminKuaishouEticketSourceConfig) {
return apiPost<{
filePath: string
source: AdminKuaishouEticketSourceConfig
}>('/api/v1/admin/platform-config/kuaishou-eticket-source', payload)
}
export function queryAdminKuaishouEticketDetail(payload: {
baseUrl?: string
shopId?: string
cookie?: string
eTicketId: string
}) {
return apiPost<AdminKuaishouEticketDetailResult>(
'/api/v1/admin/platform-config/kuaishou-eticket/query-detail',
payload,
)
}
export function queryAdminKuaishouEticketShopInfo(payload: {
baseUrl?: string
shopId?: string
cookie?: string
}) {
return apiPost<AdminKuaishouEticketShopInfoResult>(
'/api/v1/admin/platform-config/kuaishou-eticket/query-shop-info',
payload,
)
}
export function consumeAdminKuaishouEticket(payload: {
baseUrl?: string
shopId?: string
cookie?: string
eTicketId: string
oid?: string
formToken?: string
num?: number
storeId?: string
}) {
return apiPost<AdminKuaishouEticketConsumeResult>(
'/api/v1/admin/platform-config/kuaishou-eticket/consume',
payload,
)
}
@@ -0,0 +1,79 @@
import { apiGet, apiPost } from '@/lib/http'
import type {
AdminKuaishouFeifeiConfig,
AdminKuaishouFeifeiConfigResponse,
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiOrderResult,
AdminKuaishouFeifeiProductListResult,
AdminKuaishouFeifeiProductSyncResult,
} from '@/types/admin'
export function fetchAdminKuaishouFeifeiConfig() {
return apiGet<AdminKuaishouFeifeiConfigResponse>(
'/api/v1/admin/platform-config/kuaishou-feifei',
)
}
export function saveAdminKuaishouFeifeiConfig(payload: AdminKuaishouFeifeiConfig) {
return apiPost<AdminKuaishouFeifeiConfigResponse>(
'/api/v1/admin/platform-config/kuaishou-feifei',
payload,
)
}
export function matchAdminKuaishouFeifeiProduct(productName: string) {
return apiPost<AdminKuaishouFeifeiMatchResult>(
'/api/v1/admin/platform-config/kuaishou-feifei/match',
{ productName },
)
}
export function fetchAdminKuaishouFeifeiProducts(payload: {
page?: number
perPage?: number
status?: string
supplyProductName?: string
}) {
return apiPost<AdminKuaishouFeifeiProductListResult>(
'/api/v1/admin/platform-config/kuaishou-feifei/products',
payload,
)
}
export function syncAdminKuaishouFeifeiProducts(payload: {
status?: string
supplyProductName?: string
} = {}) {
return apiPost<AdminKuaishouFeifeiProductSyncResult>(
'/api/v1/admin/platform-config/kuaishou-feifei/sync-products',
payload,
)
}
export function createAdminKuaishouFeifeiTestOrder(payload: {
platformOrderNo: string
productCode: string
platformBuyNum?: number
platformAmount?: number | null
playerAccount?: string
playerGameRegion?: string
playerGameSrv?: string
playerGameRole?: string
submitPlayer?: boolean
notifyUrl?: string
}) {
return apiPost<AdminKuaishouFeifeiOrderResult>(
'/api/v1/admin/platform-config/kuaishou-feifei/test-order',
payload,
)
}
export function queryAdminKuaishouFeifeiOrder(payload: {
platformOrderNo?: string
orderNo?: string
}) {
return apiPost<AdminKuaishouFeifeiOrderResult>(
'/api/v1/admin/platform-config/kuaishou-feifei/query-order',
payload,
)
}
@@ -0,0 +1,42 @@
import { apiGet, apiPost } from '@/lib/http'
import type {
AdminNinetyoneOrderActionResult,
AdminNinetyoneOrderListResult,
} from '@/types/admin'
export function fetchAdminNinetyoneOrders(
params: {
page?: number
pageSize?: number
status?: string
} = {},
) {
const searchParams = new URLSearchParams()
if (params.page) {
searchParams.set('page', String(params.page))
}
if (params.pageSize) {
searchParams.set('pageSize', String(params.pageSize))
}
if (params.status) {
searchParams.set('status', params.status)
}
const queryString = searchParams.toString()
return apiGet<AdminNinetyoneOrderListResult>(
`/api/v1/admin/platform-config/ninetyone/orders${queryString ? `?${queryString}` : ''}`,
)
}
export function retryAdminNinetyoneOrder(orderId: number | string) {
return apiPost<AdminNinetyoneOrderActionResult>(
`/api/v1/admin/platform-config/ninetyone/orders/${orderId}/retry`,
{},
)
}
export function failAdminNinetyoneOrder(orderId: number | string, payload: { reason?: string }) {
return apiPost<AdminNinetyoneOrderActionResult>(
`/api/v1/admin/platform-config/ninetyone/orders/${orderId}/fail`,
payload,
)
}
@@ -0,0 +1,23 @@
import { apiGet, apiPost } from '@/lib/http'
import type { AdminNotificationConfig, AdminNotificationTestResult } from '@/types/admin'
export function fetchAdminNotificationConfig() {
return apiGet<{
filePath: string
source: AdminNotificationConfig
}>('/api/v1/admin/platform-config/notifications')
}
export function saveAdminNotificationConfig(payload: AdminNotificationConfig) {
return apiPost<{
filePath: string
source: AdminNotificationConfig
}>('/api/v1/admin/platform-config/notifications', payload)
}
export function testAdminNotification(payload: { title?: string; body?: string; url?: string }) {
return apiPost<AdminNotificationTestResult>(
'/api/v1/admin/platform-config/notifications/test',
payload,
)
}
@@ -0,0 +1,20 @@
import { apiGet, apiPost } from '@/lib/http'
import type { AdminScheduledJobsConfig, AdminScheduledJobsResponse } from '@/types/admin'
export function fetchAdminScheduledJobsConfig() {
return apiGet<AdminScheduledJobsResponse>('/api/v1/admin/platform-config/scheduled-jobs')
}
export function saveAdminScheduledJobsConfig(payload: AdminScheduledJobsConfig) {
return apiPost<AdminScheduledJobsResponse>(
'/api/v1/admin/platform-config/scheduled-jobs',
payload,
)
}
export function runAdminScheduledJob(jobId: string) {
return apiPost<{
result: Record<string, unknown>
runtime: AdminScheduledJobsResponse['runtime']
}>(`/api/v1/admin/platform-config/scheduled-jobs/${jobId}/run`, {})
}
@@ -0,0 +1,93 @@
import { apiGet, apiGetBlob, apiPost } from '@/lib/http'
import type {
AdminPagination,
AdminTaskActionResponse,
AdminTaskDetail,
AdminTaskListItem,
} from '@/types/admin'
export function fetchAdminTasks(params?: Record<string, unknown>) {
return apiGet<{ items: AdminTaskListItem[]; pagination: AdminPagination }>(
'/api/v1/admin/tasks',
params,
)
}
export function fetchAdminTaskDetail(taskId: number | string) {
return apiGet<AdminTaskDetail>(`/api/v1/admin/tasks/${taskId}`)
}
export function fetchAdminTaskScreenshot(taskId: number | string) {
return apiGetBlob(`/api/v1/admin/tasks/${taskId}/screenshot`)
}
export function retryAdminTask(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/retry`, {})
}
export function regenerateAdminTaskClaimLink(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/regenerate-claim-link`, {})
}
export function closeAdminTask(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/close`, {})
}
export function markAdminTaskManualReview(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/mark-manual-review`, {})
}
export function completeAdminTaskManualDispatch(
taskId: number | string,
payload: {
outcome: 'delivered' | 'failed'
resultMessage?: string
deliveryReference?: string
deliveredCredential?: string
},
) {
return apiPost<AdminTaskActionResponse>(
`/api/v1/admin/tasks/${taskId}/complete-manual-dispatch`,
payload,
)
}
export function prepareAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/prepare`,
{},
)
}
export function refreshAdminTaskKuaishouCloudRoleInfo(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/refresh-role-info`,
{},
)
}
export function rebindAdminTaskKuaishouCloudRole(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/rebind-role`,
{},
)
}
export function dispatchAdminTaskKuaishouCloudFulfillment(
taskId: number | string,
payload: {
ticketCode?: string
} = {},
) {
return apiPost<AdminTaskActionResponse>(
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/dispatch`,
payload,
)
}
export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/return-number`,
{},
)
}
@@ -0,0 +1,33 @@
import { apiGet, apiPost } from '@/lib/http'
import type { AdminPagination, AdminUserListItem } from '@/types/admin'
export function fetchAdminUsers(params?: Record<string, unknown>) {
return apiGet<{ items: AdminUserListItem[]; pagination: AdminPagination }>(
'/api/v1/admin/users',
params,
)
}
export function createAdminUser(payload: {
username: string
password: string
role: string
status?: string
}) {
return apiPost<{ user: AdminUserListItem }>('/api/v1/admin/users', payload)
}
export function updateAdminUserRole(userId: number | string, payload: { role: string }) {
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/role`, payload)
}
export function updateAdminUserStatus(userId: number | string, payload: { status: string }) {
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/status`, payload)
}
export function resetAdminUserPassword(userId: number | string, payload: { password: string }) {
return apiPost<{ user: AdminUserListItem }>(
`/api/v1/admin/users/${userId}/reset-password`,
payload,
)
}
+27
View File
@@ -0,0 +1,27 @@
import { apiGet, apiPost } from '@/lib/http'
import type { ClaimDetailData } from '@/types/claim'
export function fetchClaimDetail(token: string) {
return apiGet<ClaimDetailData>(`/api/v1/claim/${token}`)
}
export function verifyKuaishouCloudClaimTicket(
token: string,
payload: {
ticketCode: string
},
) {
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/verify-ticket`, payload)
}
export function confirmKuaishouCloudClaimRole(token: string) {
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
}
export function rebindKuaishouCloudClaimRole(token: string) {
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/rebind-role`, {})
}
export function redeemKuaishouCloudClaim(token: string) {
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/redeem`, {})
}
+195
View File
@@ -0,0 +1,195 @@
:root {
color: #1f2937;
background: #f5f7fb;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
button,
input,
textarea,
select {
font: inherit;
}
.login-page {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
background:
linear-gradient(135deg, rgba(22, 119, 255, 0.1), rgba(19, 194, 194, 0.08)),
#f5f7fb;
}
.login-panel {
width: min(440px, 100%);
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.12);
}
.login-heading {
margin-bottom: 24px;
}
.login-heading h1 {
margin: 8px 0 0;
font-size: 28px;
}
.eyebrow {
color: #1677ff;
font-weight: 700;
letter-spacing: 0;
}
.admin-shell {
min-height: 100vh;
}
.admin-sider {
border-right: 1px solid #edf0f5;
box-shadow: 4px 0 18px rgba(15, 23, 42, 0.04);
z-index: 2;
}
.admin-brand {
height: 64px;
display: grid;
align-content: center;
padding: 0 20px;
border-bottom: 1px solid #edf0f5;
}
.admin-brand strong {
color: #111827;
font-size: 18px;
}
.admin-brand span {
color: #6b7280;
font-size: 12px;
}
.admin-topbar {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid #edf0f5;
}
.admin-user {
margin-left: auto;
}
.admin-user-copy {
display: grid;
justify-items: end;
line-height: 1.3;
}
.admin-content {
padding: 20px;
}
.page-stack {
display: grid;
gap: 16px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.page-title {
margin: 0 !important;
}
.page-description {
margin: 6px 0 0;
color: #6b7280;
}
.filter-form {
row-gap: 12px;
}
.cell-stack {
display: grid;
gap: 4px;
}
.muted {
color: #6b7280;
}
.json-preview {
max-height: 460px;
overflow: auto;
margin: 0;
padding: 14px;
border: 1px solid #edf0f5;
border-radius: 6px;
background: #0f172a;
color: #dbeafe;
font-size: 12px;
line-height: 1.6;
}
.empty-page {
min-height: 50vh;
display: grid;
place-items: center;
}
.route-loading {
min-height: 100vh;
display: grid;
place-items: center;
}
.full-width {
width: 100%;
}
.feedback-prompt p {
margin-top: 0;
}
@media (max-width: 900px) {
.admin-sider {
position: fixed !important;
left: 0;
top: 0;
bottom: 0;
height: 100vh;
}
.admin-content {
padding: 14px;
}
.admin-user-copy {
display: none;
}
.page-header {
display: grid;
}
}
@@ -0,0 +1,13 @@
import type { AdminRole } from './auth'
export interface AdminAuditLogItem {
logId: number
actorUserId: number
actorUsername: string
actorRole: AdminRole
action: string
targetType: string
targetId: string
payload: Record<string, unknown>
createdAt: string
}
@@ -0,0 +1,22 @@
export type AdminRole = 'admin' | 'operator' | 'support'
export type AdminUserStatus = 'active' | 'disabled'
export interface AdminLoginResponse {
token: string
expiresAt: string
user: {
userId: number
username: string
role: AdminRole
}
}
export interface AdminSessionSummary {
authenticated: boolean
expiresAt: string
user: {
userId: number
username: string
role: AdminRole
}
}
@@ -0,0 +1,5 @@
export interface AdminPagination {
page: number
pageSize: number
total: number
}
@@ -0,0 +1,7 @@
export interface AdminDashboardSummary {
todayOrders: number
paidPendingClaim: number
claimingTasks: number
redeemedToday: number
abnormalTasks: number
}
@@ -0,0 +1,81 @@
// Common types
export type { AdminPagination } from './common'
// Auth types
export type { AdminRole, AdminUserStatus, AdminLoginResponse, AdminSessionSummary } from './auth'
// Users types
export type { AdminUserListItem } from './users'
// Audit logs types
export type { AdminAuditLogItem } from './audit-logs'
// Dashboard types
export type { AdminDashboardSummary } from './dashboard'
// Orders types
export type {
AdminOrderFulfillmentProgress,
AdminOrderListItem,
AdminOrderDetail,
} from './orders'
// Tasks types
export type {
AdminTaskListItem,
AdminTaskOperations,
AdminTaskActionResponse,
AdminTaskDetail,
} from './tasks'
// Platform config types
export type {
AdminNotificationBarkRecipient,
AdminNotificationWpushRecipient,
AdminNotificationConfig,
AdminNotificationSendResult,
AdminNotificationTestResult,
AdminCloudtentaclesMonitorAccount,
AdminScheduledJobAccountRuntime,
AdminScheduledJobCloudtentaclesAccount,
AdminScheduledJobItem,
AdminScheduledJobsConfig,
AdminScheduledJobRuntimeState,
AdminScheduledJobsResponse,
AdminNinetyoneOrderItem,
AdminNinetyoneOrderListResult,
AdminNinetyoneOrderActionResult,
AdminKuaishouEticketSourceConfig,
AdminKuaishouEticketShopConfigItem,
AdminKuaishouEticketShopInfoResult,
AdminKuaishouEticketDetailResult,
AdminKuaishouEticketConsumeResult,
AdminKuaishouFeifeiProductRule,
AdminKuaishouFeifeiConfig,
AdminKuaishouFeifeiEffectiveConfig,
AdminKuaishouFeifeiConfigResponse,
AdminKuaishouFeifeiProductSyncResult,
AdminKuaishouFeifeiProductMatch,
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiProductItem,
AdminKuaishouFeifeiProductListResult,
AdminKuaishouFeifeiOrderResult,
AdminCloudtentaclesSourceItem,
AdminCloudtentaclesSourcesConfig,
AdminCloudtentaclesSourceConfigResponse,
AdminCloudtentaclesOverrideDeliveryItem,
AdminCloudtentaclesOverrideRule,
AdminCloudtentaclesOverrideRuleConfig,
AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSessionSummary,
AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesValidateSessionResult,
AdminCloudtentaclesSkuItem,
AdminCloudtentaclesSkuListResult,
AdminCloudtentaclesDeliveryRecordItem,
AdminCloudtentaclesDeliveryRecordListResult,
} from './platform-config'
@@ -0,0 +1,72 @@
import type { AdminTaskListItem } from './tasks'
export interface AdminOrderFulfillmentProgress {
totalTaskCount: number
preparedTaskCount: number
completedTaskCount: number
resourceStatus: string
customerStatus: string
}
export interface AdminOrderListItem {
orderId: number
provider: string
platform: string
shopId: string
shopName: string
platformOrderId: string
orderStatus: string
payStatus: string
buyerId: string
buyerName: string
receiverContact: string
totalAmount: string
totalAmountFen: number
currency: string
paidAt: string | null
createdAt: string
updatedAt: string
itemCount: number
totalQuantity: number
itemSummary: string
taskCount: number
resourceStatus: string
customerStatus: string
preparedTaskCount: number
completedTaskCount: number
}
export interface AdminOrderDetail {
order: {
orderId: number
provider: string
platform: string
shopId: string
shopName: string
platformOrderId: string
orderStatus: string
payStatus: string
buyerId: string
buyerName: string
receiverContact: string
totalAmount: string
totalAmountFen: number
currency: string
paidAt: string | null
createdAt: string
updatedAt: string
itemSummary: string
rawPayload: Record<string, unknown>
fulfillmentProgress: AdminOrderFulfillmentProgress
}
items: Array<{
orderItemId: number
skuCode: string
skuName: string
itemTitle: string
quantity: number
deliveryMode: string
spec: Record<string, unknown>
}>
tasks: AdminTaskListItem[]
}
@@ -0,0 +1,181 @@
export interface AdminCloudtentaclesSourceItem {
key: string
label: string
enabled: boolean
baseUrl: string
username: string
password: string
phone: string
deviceId: string
deviceType: number
}
export interface AdminCloudtentaclesSourcesConfig {
enabled: boolean
sources: AdminCloudtentaclesSourceItem[]
}
export interface AdminCloudtentaclesPersistedSession {
token: string
tokenMasked: string
baseUrl: string
username: string
phoneMasked: string
loggedInAt: string
deviceId: string
deviceType: number
hasToken: boolean
}
export type AdminCloudtentaclesSessionItem = AdminCloudtentaclesPersistedSession
export interface AdminCloudtentaclesSessionsMap {
[sourceKey: string]: AdminCloudtentaclesPersistedSession
}
export interface AdminCloudtentaclesSourceConfigResponse {
filePath: string
sessionFilePath: string
enabled: boolean
sources: AdminCloudtentaclesSourceItem[]
sessions: AdminCloudtentaclesSessionsMap
}
export interface AdminCloudtentaclesOverrideDeliveryItem {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
export interface AdminCloudtentaclesOverrideRule {
id: string
enabled: boolean
productName: string
normalizedProductName: string
sourceKey: string
deliveryItems: AdminCloudtentaclesOverrideDeliveryItem[]
notes: string
}
export interface AdminCloudtentaclesOverrideRuleConfig {
filePath: string
enabled: boolean
rules: AdminCloudtentaclesOverrideRule[]
}
export interface AdminCloudtentaclesSendSmsResult {
baseUrl: string
username: string
phone: string
phoneMasked: string
sentAt: string
responseMessage: string
}
export interface AdminCloudtentaclesSessionSummary {
tokenMasked: string
permissionCount: number
permissions: string[]
persisted?: boolean
}
export interface AdminCloudtentaclesLoginTestResult {
baseUrl: string
username: string
phoneMasked: string
loggedInAt: string
responseMessage: string
token: string
session: AdminCloudtentaclesSessionSummary
userInfo: Record<string, unknown>
asset: number
}
export interface AdminCloudtentaclesValidateSessionResult {
baseUrl: string
loggedInAt: string
session: AdminCloudtentaclesSessionSummary
userInfo: Record<string, unknown>
asset: number
}
export interface AdminCloudtentaclesSkuItem {
id: number
categoriesId: number
name: string
description: string
image: string
inventory: number
price: number
listingTime: string
delistingTime: string
buyLimitMin: number
buyLimitMax: number
raw: Record<string, unknown>
}
export interface AdminCloudtentaclesSkuListResult {
baseUrl: string
itemCount: number
items: AdminCloudtentaclesSkuItem[]
rawItems: Record<string, unknown>[]
}
export interface AdminCloudtentaclesDeliveryRecordItem {
createdAt: string
recordId: string
virtualNumberId: number
userId: number
count: number
cdk: string
exchangeUrl: string
name: string
skuId: number
skuName: string
skuImage: string
phone: string
status: number
fulfillUser: string
operatorName: string
platformOrderId: string
taskId: number
taskNo: string
orderShopId: string
orderShopName: string
consumeShopId: string
consumeShopName: string
roleName: string
roleId: string
localTaskStatus: string
localDeliveryStatus: string
localDispatchStatus: string
localDispatchAt: string
localResultCode: string
localResultMessage: string
matchConfidence: string
raw: Record<string, unknown>
}
export interface AdminCloudtentaclesDeliveryRecordListResult {
baseUrl: string
page: number
size: number
total: number
sourceKey?: string
queryStartDate?: string
queryEndDate?: string
localOrderMatched?: boolean
items: AdminCloudtentaclesDeliveryRecordItem[]
raw: Record<string, unknown>
}
/** @deprecated Use AdminCloudtentaclesSourceItem + AdminCloudtentaclesSourcesConfig instead */
export interface AdminCloudtentaclesSourceConfig {
enabled: boolean
baseUrl: string
username: string
password: string
phone: string
deviceId: string
deviceType: number
}
@@ -0,0 +1,65 @@
export type {
AdminNotificationBarkRecipient,
AdminNotificationWpushRecipient,
AdminNotificationConfig,
AdminNotificationSendResult,
AdminNotificationTestResult,
} from './notifications'
export type {
AdminCloudtentaclesMonitorAccount,
AdminScheduledJobAccountRuntime,
AdminScheduledJobCloudtentaclesAccount,
AdminScheduledJobItem,
AdminScheduledJobsConfig,
AdminScheduledJobRuntimeState,
AdminScheduledJobsResponse,
} from './scheduled-jobs'
export type {
AdminNinetyoneOrderItem,
AdminNinetyoneOrderListResult,
AdminNinetyoneOrderActionResult,
} from './ninetyone'
export type {
AdminKuaishouEticketSourceConfig,
AdminKuaishouEticketShopConfigItem,
AdminKuaishouEticketShopInfoResult,
AdminKuaishouEticketDetailResult,
AdminKuaishouEticketConsumeResult,
} from './kuaishou-eticket'
export type {
AdminKuaishouFeifeiProductRule,
AdminKuaishouFeifeiConfig,
AdminKuaishouFeifeiEffectiveConfig,
AdminKuaishouFeifeiConfigResponse,
AdminKuaishouFeifeiProductSyncResult,
AdminKuaishouFeifeiProductMatch,
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiProductItem,
AdminKuaishouFeifeiProductListResult,
AdminKuaishouFeifeiOrderResult,
} from './kuaishou-feifei'
export type {
AdminCloudtentaclesSourceItem,
AdminCloudtentaclesSourcesConfig,
AdminCloudtentaclesSourceConfigResponse,
AdminCloudtentaclesOverrideDeliveryItem,
AdminCloudtentaclesOverrideRule,
AdminCloudtentaclesOverrideRuleConfig,
AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSessionSummary,
AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesValidateSessionResult,
AdminCloudtentaclesSkuItem,
AdminCloudtentaclesSkuListResult,
AdminCloudtentaclesDeliveryRecordItem,
AdminCloudtentaclesDeliveryRecordListResult,
} from './cloudtentacles'
@@ -0,0 +1,83 @@
export interface AdminKuaishouEticketShopConfigItem {
shopId: string
kshopName: string
cookie: string
cookieMasked: string
hasCookie: boolean
userAvatar: string
enabled: boolean
}
export interface AdminKuaishouEticketSourceConfig {
enabled: boolean
baseUrl: string
shops: AdminKuaishouEticketShopConfigItem[]
}
export interface AdminKuaishouEticketShopInfoResult {
baseUrl: string
ok: boolean
result: number
errorMessage: string
requestId: string
shop: {
shopId: string
kshopName: string
userAvatar: string
settleStatus: number
hasCookie: boolean
}
raw: Record<string, unknown>
}
export interface AdminKuaishouEticketDetailResult {
baseUrl: string
eTicketId: string
ok: boolean
alreadyConsumed: boolean
result: number
errorMessage: string
requestId: string
serverTimestamp: string
detail: {
uid: string
fulfillDetailId: string
sellerId: string
formToken: string
validEndTime: string
validStartTime: string
leftReverseCount: number
eTicketId: string
oid: string
totalCount: number
leftCount: number
status: string
} | null
goods: {
itemId: string
itemPicUrl: string
itemTitle: string
price: string
skuDesc: string
skuId: string
} | null
raw: Record<string, unknown>
}
export interface AdminKuaishouEticketConsumeResult {
baseUrl: string
eTicketId: string
oid: string
formToken: string
num: number
storeId: string
ok: boolean
consumed: boolean
alreadyConsumed: boolean
result: number
errorMessage: string
requestId: string
serverTimestamp: string
detail: AdminKuaishouEticketDetailResult['detail']
raw: Record<string, unknown>
}
@@ -0,0 +1,104 @@
export interface AdminKuaishouFeifeiProductRule {
id: string
enabled: boolean
productName: string
normalizedProductName: string
productCode: string
skuName: string
notes: string
}
export interface AdminKuaishouFeifeiConfig {
enabled: boolean
baseUrl: string
appKey: string
appSecret: string
timeoutMs: number
notifyUrl: string
productRules: AdminKuaishouFeifeiProductRule[]
}
export interface AdminKuaishouFeifeiEffectiveConfig {
enabled: boolean
baseUrl: string
timeoutMs: number
notifyUrl: string
hasAppKey: boolean
hasAppSecret: boolean
productRuleCount: number
}
export interface AdminKuaishouFeifeiConfigResponse {
filePath: string
source: AdminKuaishouFeifeiConfig
effective: AdminKuaishouFeifeiEffectiveConfig
}
export interface AdminKuaishouFeifeiProductSyncResult extends AdminKuaishouFeifeiConfigResponse {
sync: {
productCount: number
ruleCount: number
status: string
supplyProductName: string
}
}
export interface AdminKuaishouFeifeiProductMatch {
matchMode: string
productName: string
normalizedProductName: string
productCode: string
skuName: string
}
export interface AdminKuaishouFeifeiMatchResult {
productName: string
normalizedProductName: string
matched: boolean
match: AdminKuaishouFeifeiProductMatch | null
}
export interface AdminKuaishouFeifeiProductItem {
productCode: string
name: string
channel: string
externalProductId: string
imageUrl: string
status: string
supplyStatus: string
supplyPricePoints: number
feePoints: number
unitCostPoints: number
maxOrderQuantity: number
salePricePoints: number | null
raw: Record<string, unknown>
}
export interface AdminKuaishouFeifeiProductListResult {
items: AdminKuaishouFeifeiProductItem[]
total: number
page: number
perPage: number
raw: Record<string, unknown>
}
export interface AdminKuaishouFeifeiOrderResult {
orderNo: string
platformOrderNo: string
productCode: string
productName: string
rechargeStatus: number
rechargeStatusLabel: string
pointsCharged: number
playerAccount: string
platformBuyNum: number
rechargeResultMessage: string
createdAt: string
updatedAt: string
rechargeFinishAt: string
h5: {
entryUrl: string
rechargeUrl: string
}
raw: Record<string, unknown>
}
@@ -0,0 +1,32 @@
export interface AdminNinetyoneOrderItem {
orderId: number
orderNo: string
outTradeNo: string
orderStatus: string
payStatus: string
shopId: string
shopName: string
productNo: string
productName: string
buyNum: number
taskCount: number
failReason: string
createdAt: string
updatedAt: string
items: Array<Record<string, unknown>>
}
export interface AdminNinetyoneOrderListResult {
page: number
pageSize: number
total: number
items: AdminNinetyoneOrderItem[]
}
export interface AdminNinetyoneOrderActionResult {
orderId: number
orderNo: string
orderStatus: string
orderItemCount: number
taskCount: number
}
@@ -0,0 +1,50 @@
export interface AdminNotificationBarkRecipient {
id: string
name: string
deviceKey: string
deviceKeyMasked: string
enabled: boolean
}
export interface AdminNotificationWpushRecipient {
id: string
name: string
apiKey: string
apiKeyMasked: string
enabled: boolean
}
export interface AdminNotificationConfig {
enabled: boolean
channels: {
bark: {
enabled: boolean
serverUrl: string
recipients: AdminNotificationBarkRecipient[]
}
wpush: {
enabled: boolean
recipients: AdminNotificationWpushRecipient[]
}
}
}
export interface AdminNotificationSendResult {
channel: string
recipientId: string
recipientName: string
recipientKeyMasked: string
ok: boolean
status: number
errorMessage: string
response: unknown
}
export interface AdminNotificationTestResult {
enabled: boolean
channel: string
successCount: number
failedCount: number
skippedCount: number
results: AdminNotificationSendResult[]
}
@@ -0,0 +1,74 @@
export interface AdminScheduledJobCloudtentaclesAccount {
sourceKey: string
label: string
enabled: boolean
assetThreshold: number
}
export interface AdminScheduledJobItem {
id: string
type: string
enabled: boolean
intervalSeconds: number
cooldownSeconds: number
config: {
assetThreshold: number
accounts: AdminScheduledJobCloudtentaclesAccount[]
}
}
export interface AdminScheduledJobsConfig {
enabled: boolean
jobs: AdminScheduledJobItem[]
}
export interface AdminScheduledJobRuntimeState {
id: string
enabled: boolean
running: boolean
lastRunAt: string
lastFinishedAt: string
nextRunAt: string
lastStatus: string
lastMessage: string
lastAsset: number | null
lastThreshold: number | null
lastAccounts: AdminScheduledJobAccountRuntime[]
lastAccountCount: number
lastCheckedCount: number
lastOkCount: number
lastLowAssetCount: number
lastFailedCount: number
lastManual: boolean
}
export interface AdminScheduledJobAccountRuntime {
sourceKey: string
label: string
enabled: boolean
hasToken: boolean
ok: boolean
skipped: boolean
status: string
message: string
asset: number | null
threshold: number
checkedAt: string
}
export interface AdminCloudtentaclesMonitorAccount {
sourceKey: string
label: string
enabled: boolean
username: string
phoneMasked: string
hasToken: boolean
loggedInAt: string
}
export interface AdminScheduledJobsResponse {
filePath: string
source: AdminScheduledJobsConfig
runtime: AdminScheduledJobRuntimeState[]
cloudtentaclesAccounts: AdminCloudtentaclesMonitorAccount[]
}
@@ -0,0 +1,223 @@
export interface AdminTaskListItem {
taskId: number
taskNo: string
platformOrderId: string
skuCode: string
skuName: string
status: string
executorKey: string
deliveryStatus: string
resultCode: string
resultMessage: string
resourceStatus: string
customerStatus: string
loginType: string
roleName: string
roleId: string
runtimeSessionId: string
claimedAt: string | null
roleConfirmedAt: string | null
redeemedAt: string | null
retryCount: number
lastError: string
createdAt: string
updatedAt: string
claimToken: string
screenshotPath: string
}
export interface AdminTaskOperations {
canRetry: boolean
canRegenerateClaimLink: boolean
canClose: boolean
canMarkManualReview: boolean
canCompleteManualDispatch: boolean
canPrepareKuaishouCloudFulfillment: boolean
canRefreshKuaishouCloudRoleInfo: boolean
canRebindKuaishouCloudRole: boolean
canDispatchKuaishouCloudFulfillment: boolean
canReturnKuaishouCloudFulfillment: boolean
canViewSensitiveTaskData: boolean
}
export interface AdminTaskActionResponse {
task: {
taskId: number
taskNo: string
status: string
deliveryStatus: string
resultCode: string
resultMessage: string
primaryClaimTokenId: number | null
lastError: string
updatedAt: string
}
claimUrl?: string
token?: string
}
export interface AdminTaskDetail {
task: AdminTaskListItem
order: null | {
orderId: number
provider: string
platform: string
shopId: string
shopName: string
platformOrderId: string
payStatus: string
orderStatus: string
}
orderItem: null | {
orderItemId: number
skuCode: string
skuName: string
quantity: number
}
claimToken: null | {
primaryClaimTokenId: number
token: string
status: string
expiredAt: string
claimUrl: string
}
artifacts: Record<string, unknown>
screenshotUrl: string
review: {
required: boolean
screenshotCapturedAt: string | null
roleId: string
roleName: string
}
redeemResolution: null | {
status: string
taskStatus: string
replacementCount: number
finishedAt: string | null
attempts: Array<{
attempt: number
codeMasked: string
credentialType: string
outcome: string
resultCode: string
resultMessage: string
}>
}
kuaishouCloudFulfillment: null | {
flowType: string
configId: string
internalSkuCode: string
internalSkuName: string
ticket: {
code: string
capturedAt: string | null
status: string
verifiedAt: string | null
goodsTitle: string
leftCount: number
}
binding: {
prepareStatus: string
cloudSourceKeys: string[]
cloudSourceLabels: string[]
resolvedSourceKey: string
resolvedSourceLabel: string
skuId: number
skuName: string
vnKey: string
vnId: number
vnPhone: string
bindUrl: string
bindPreparedAt: string | null
bindExpiresAt: string | null
bindProbeAt: string | null
bindProbeStatus: string
bindProbeMessage: string
}
role: {
status: string
name: string
rid: string
refreshedAt: string | null
errorMessage: string
rawInfo: Record<string, unknown> | null
}
purchase: {
autoBuyEnabled: boolean
minAssetReserve: number
usedKnapsack: boolean
purchaseTriggered: boolean
assetBefore: number
assetAfter: number
purchaseAt: string | null
}
dispatch: {
status: string
dispatchAt: string | null
sendType: number
note: string
}
returnNumber: {
status: string
returnedAt: string | null
autoReturnEnabled: boolean
}
consume: {
status: string
shopId: string
shopName: string
autoConsumeEnabled: boolean
consumedAt: string | null
errorMessage: string
}
rebind: {
currentAttempt: number
history: Array<{
attempt: number
source: string
requestedAt: string | null
requestedBy: Record<string, unknown> | null
status: string
errorMessage: string
oldBinding: {
vnKey: string
vnId: number
vnPhone: string
bindUrl: string
bindPreparedAt: string | null
bindExpiresAt: string | null
roleName: string
roleId: string
}
newBinding: null | {
vnKey: string
vnId: number
vnPhone: string
bindUrl: string
bindPreparedAt: string | null
bindExpiresAt: string | null
}
}>
}
notes: string
}
manualDispatch: null | {
outcome: string
deliveryReference: string
deliveredCredential: string
resultMessage: string
completedAt: string | null
completedBy: null | {
userId: number
username: string
role: string
}
}
events: Array<{
eventId: number
eventType: string
payload: Record<string, unknown>
createdAt: string
}>
operations: AdminTaskOperations
}
@@ -0,0 +1,10 @@
import type { AdminRole, AdminUserStatus } from './auth'
export interface AdminUserListItem {
userId: number
username: string
role: AdminRole
status: AdminUserStatus
createdAt: string
updatedAt: string
}
+168
View File
@@ -0,0 +1,168 @@
import type { KnownTaskStatus } from '@/domain/task-status'
export type ClaimTokenStatus = 'active' | 'used' | 'expired' | 'revoked' | (string & {})
export type ClaimTaskStatus = KnownTaskStatus | (string & {})
export interface ClaimTaskInfo {
taskId: number
taskNo: string
status: ClaimTaskStatus
executorKey: string
requiresSupportReview: boolean
expiresAt: string | null
claimedAt: string | null
roleConfirmedAt: string | null
redeemedAt: string | null
loginType: string
lastError: string
runtimeSessionId: string
}
export interface ClaimOrderInfo {
orderId: number
platform: string
platformOrderId: string
payStatus: string
orderStatus: string
totalAmount: string
totalAmountFen: number
currency: string
}
export interface ClaimOrderItemInfo {
orderItemId: number
skuCode: string
skuName: string
quantity: number
}
export interface ClaimProductInfo {
title: string
skuCode: string
quantity: number
isBundle: boolean
items: Array<{
cloudSkuId: number
name: string
quantity: number
}>
}
export interface ClaimResultInfo {
resultCode: string
resultMessage: string
screenshotReady: boolean
screenshotUrl: string
}
export interface ClaimKuaishouCloudFlowInfo {
flowType: 'kuaishou_cloud'
shopId: string
shopName: string
guideImages: string[]
ticket: {
code: string
status: string
capturedAt: string | null
verifiedAt: string | null
goodsTitle: string
leftCount: number
}
binding: {
prepareStatus: string
cloudSourceKeys: string[]
resolvedSourceKey: string
skuId: number
skuName: string
vnKey: string
vnId: number
bindUrl: string
bindPreparedAt: string | null
bindExpiresAt: string | null
bindProbeAt: string | null
bindProbeStatus: string
bindProbeMessage: string
vnPhone: string
roleName: string
roleId: string
}
role: {
status: string
name: string
rid: string
refreshedAt: string | null
errorMessage: string
defaultName: string
defaultRid: string
defaultCapturedAt: string | null
defaultCaptureStatus: string
defaultErrorMessage: string
isDefaultRole: boolean
}
purchase: {
autoBuyEnabled: boolean
minAssetReserve: number
usedKnapsack: boolean
purchaseTriggered: boolean
assetBefore: number
assetAfter: number
purchaseAt: string | null
}
dispatch: {
status: string
dispatchAt: string | null
failedAt: string | null
failedStage: string
errorCode: string
errorMessage: string
note: string
}
returnNumber: {
status: string
returnedAt: string | null
}
consume: {
status: string
shopId: string
shopName: string
consumedAt: string | null
errorMessage: string
}
rebind?: {
currentAttempt: number
history: Array<Record<string, unknown>>
}
}
export interface ClaimKuaishouFeifeiFlowInfo {
flowType: 'kuaishou_feifei'
productCode: string
productName: string
platformOrderNo: string
orderNo: string
rechargeStatus: number
rechargeStatusLabel: string
rechargeResultMessage: string
claimUrl: string
consumeStatus: string
h5: {
entryUrl: string
rechargeUrl: string
}
lastSyncedAt: string | null
}
export interface ClaimDetailData {
tokenStatus: ClaimTokenStatus
claimUrl: string
flowType: 'kuaishou_cloud' | 'kuaishou_ct_assisted' | 'kuaishou_feifei' | (string & {})
task: ClaimTaskInfo
order: ClaimOrderInfo
orderItem: ClaimOrderItemInfo
product: ClaimProductInfo
session: unknown | null
kuaishouCloudFulfillment: ClaimKuaishouCloudFlowInfo | null
kuaishouFeifei: ClaimKuaishouFeifeiFlowInfo | null
result: ClaimResultInfo | null
}
@@ -0,0 +1,99 @@
const ADMIN_TOKEN_KEY = 'order-site-admin-token'
const ADMIN_EXPIRES_AT_KEY = 'order-site-admin-token-expires-at'
const ADMIN_USER_ID_KEY = 'order-site-admin-user-id'
const ADMIN_USERNAME_KEY = 'order-site-admin-username'
const ADMIN_ROLE_KEY = 'order-site-admin-role'
const ADMIN_ROLE_LEVEL: Record<'support' | 'operator' | 'admin', number> = {
support: 1,
operator: 2,
admin: 3,
}
export function getAdminToken() {
return localStorage.getItem(ADMIN_TOKEN_KEY) || ''
}
export function getAdminTokenExpiresAt() {
return localStorage.getItem(ADMIN_EXPIRES_AT_KEY) || ''
}
export function getAdminUserId() {
const value = Number(localStorage.getItem(ADMIN_USER_ID_KEY) || 0)
return Number.isFinite(value) && value > 0 ? value : 0
}
export function getAdminUsername() {
return localStorage.getItem(ADMIN_USERNAME_KEY) || ''
}
export function getAdminRole() {
const role = localStorage.getItem(ADMIN_ROLE_KEY)
if (role === 'admin' || role === 'operator' || role === 'support') {
return role
}
return 'support'
}
export function hasAdminRole(role: 'admin' | 'operator' | 'support') {
if (!hasAdminSession()) {
return false
}
return ADMIN_ROLE_LEVEL[getAdminRole()] >= ADMIN_ROLE_LEVEL[role]
}
export function setAdminSession(
token: string,
expiresAt: string,
user?: { userId: number; username: string; role: 'admin' | 'operator' | 'support' },
) {
localStorage.setItem(ADMIN_TOKEN_KEY, token)
localStorage.setItem(ADMIN_EXPIRES_AT_KEY, expiresAt)
if (user?.userId) {
localStorage.setItem(ADMIN_USER_ID_KEY, String(user.userId))
}
if (user?.username) {
localStorage.setItem(ADMIN_USERNAME_KEY, user.username)
}
if (user?.role) {
localStorage.setItem(ADMIN_ROLE_KEY, user.role)
}
}
export function clearAdminSession() {
localStorage.removeItem(ADMIN_TOKEN_KEY)
localStorage.removeItem(ADMIN_EXPIRES_AT_KEY)
localStorage.removeItem(ADMIN_USER_ID_KEY)
localStorage.removeItem(ADMIN_USERNAME_KEY)
localStorage.removeItem(ADMIN_ROLE_KEY)
}
export function hasAdminSession() {
const token = getAdminToken()
const expiresAt = getAdminTokenExpiresAt()
if (!token) {
return false
}
if (isExpired(expiresAt)) {
clearAdminSession()
return false
}
return true
}
function isExpired(expiresAt: string) {
if (!expiresAt) {
return false
}
const expiresAtMs = Date.parse(expiresAt)
return Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now()
}
@@ -0,0 +1,135 @@
export function formatStatusWithRaw(status: string, labelMap: Record<string, string> = {}) {
const normalized = String(status || '').trim()
if (!normalized) {
return '-'
}
const key = normalized.toLowerCase()
const label = labelMap[key]
if (!label || label === normalized) {
return normalized
}
return `${label} (${normalized})`
}
export function formatAuditAction(action: string) {
const labelMap: Record<string, string> = {
admin_user_created: '创建后台用户',
admin_user_role_updated: '修改用户角色',
admin_user_status_updated: '修改用户状态',
admin_user_password_reset: '重置用户密码',
task_regenerate_claim_link: '重发领取链接',
task_kuaishou_cloud_rebind_role: '换绑角色',
task_closed: '关闭任务',
task_mark_manual_review: '转人工处理',
task_complete_manual_dispatch: '完成人工履约',
platform_shop_config_updated: '更新店铺配置',
platform_fulfillment_config_updated: '更新履约配置',
}
return formatStatusWithRaw(action, labelMap)
}
export function formatAuditTargetType(targetType: string) {
const labelMap: Record<string, string> = {
admin_user: '后台用户',
task: '交付任务',
platform_config: '平台配置',
}
return formatStatusWithRaw(targetType, labelMap)
}
export function formatTaskEventType(eventType: string) {
const labelMap: Record<string, string> = {
manual_dispatch_completed: '人工履约已回写',
claim_redeem_code_used: '兑换码已使用',
claim_redeem_code_invalid: '兑换码错误',
claim_redeem_completed: '兑换链路完成',
kuaishou_cloud_rebind_requested: '发起角色换绑',
kuaishou_cloud_rebind_old_number_returned: '换绑旧号已退还',
kuaishou_cloud_rebind_prepared: '换绑资源已准备',
kuaishou_cloud_rebind_failed: '换绑失败',
}
return formatStatusWithRaw(eventType, labelMap)
}
export function formatRedeemOutcomeLabel(value: string) {
const normalized = String(value || '').trim()
switch (normalized) {
case 'success':
return '兑换成功'
case 'code_used':
return '兑换码已使用'
case 'code_invalid':
return '兑换码错误'
case 'captcha_rejected':
return '验证码错误'
case 'failed':
return '兑换失败'
default:
return normalized || '-'
}
}
export function formatRedeemResolutionStatus(value: string) {
return value === 'success' ? '已完成' : value === 'failed' ? '失败收口' : value || '-'
}
export function formatKuaishouRoleInfoLabel(value: string) {
const normalized = String(value || '').trim()
switch (normalized) {
case 'name':
return '角色名称'
case 'rid':
return '角色 ID'
default:
return normalized || '-'
}
}
export function formatTaskEventPayload(payload: Record<string, unknown>) {
const parts = [
payload.outcome ? `结果 ${payload.outcome}` : '',
payload.resultCode ? `代码 ${payload.resultCode}` : '',
payload.resultMessage ? `说明 ${payload.resultMessage}` : '',
payload.codeMasked ? `凭据 ${payload.codeMasked}` : '',
payload.previousCodeMasked ? `旧码 ${payload.previousCodeMasked}` : '',
payload.nextCodeMasked ? `新码 ${payload.nextCodeMasked}` : '',
payload.ticketCodeMasked ? `卡券号 ${payload.ticketCodeMasked}` : '',
payload.vnId ? `虚拟号 #${payload.vnId}` : '',
payload.oldVnId ? `旧虚拟号 #${payload.oldVnId}` : '',
payload.vnPhoneMasked ? `号码 ${payload.vnPhoneMasked}` : '',
payload.oldVnPhoneMasked ? `旧号码 ${payload.oldVnPhoneMasked}` : '',
payload.oldRoleName ? `旧角色 ${payload.oldRoleName}` : '',
payload.oldRoleId ? `旧角色ID ${payload.oldRoleId}` : '',
payload.attempt ? `${payload.attempt}` : '',
payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : '',
payload.sendType ? `发送类型 ${payload.sendType}` : '',
payload.note ? `备注 ${payload.note}` : '',
payload.previousOutcome
? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}`
: '',
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '',
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '',
payload.trigger ? `触发 ${payload.trigger}` : '',
payload.reason ? `原因 ${payload.reason}` : '',
payload.purchaseTriggered === true ? '已触发购买' : '',
payload.usedKnapsack === true ? '使用背包库存' : '',
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '',
payload.requestId ? `请求 ${payload.requestId}` : '',
payload.errorMessage ? `错误 ${payload.errorMessage}` : '',
].filter(Boolean)
if (parts.length > 0) {
return parts.join(' · ')
}
return JSON.stringify(payload || {})
}
@@ -0,0 +1,52 @@
export const adminPayStatusOptions = [
{ label: '全部', value: '' },
{ label: '未支付', value: 'unpaid' },
{ label: '已支付', value: 'paid' },
{ label: '失败', value: 'failed' },
{ label: '已退款', value: 'refunded' },
]
export const adminTaskStatusOptions = [
{ label: '全部', value: '' },
{ label: '待支付', value: 'pending_payment' },
{ label: '已支付', value: 'paid' },
{ label: '已生成链接', value: 'link_generated' },
{ label: '已创建会话', value: 'claimed' },
{ label: '已确认角色', value: 'role_confirmed' },
{ label: '兑换中', value: 'redeeming' },
{ label: '已兑换', value: 'redeemed' },
{ label: '待重试', value: 'retry_pending' },
{ label: '人工处理', value: 'manual_review' },
{ label: '已过期', value: 'expired' },
{ label: '已关闭', value: 'closed' },
]
export const adminUserRoleOptions = [
{ label: '全部', value: '' },
{ label: '管理员', value: 'admin' },
{ label: '普通运营', value: 'operator' },
{ label: '客服', value: 'support' },
]
export const adminUserStatusOptions = [
{ label: '全部', value: '' },
{ label: '生效中', value: 'active' },
{ label: '已停用', value: 'disabled' },
]
export const adminAuditActionOptions = [
{ label: '全部', value: '' },
{ label: '创建后台用户', value: 'admin_user_created' },
{ label: '修改用户角色', value: 'admin_user_role_updated' },
{ label: '修改用户状态', value: 'admin_user_status_updated' },
{ label: '重置用户密码', value: 'admin_user_password_reset' },
{ label: '换绑角色', value: 'task_kuaishou_cloud_rebind_role' },
{ label: '关闭任务', value: 'task_closed' },
{ label: '转人工处理', value: 'task_mark_manual_review' },
]
export const adminAuditTargetTypeOptions = [
{ label: '全部', value: '' },
{ label: '后台用户', value: 'admin_user' },
{ label: '交付任务', value: 'task' },
]
@@ -0,0 +1 @@
export { formatDateTime as formatAdminDateTime } from '@/utils/date-time'
@@ -0,0 +1,54 @@
const ISO_DATE_TIME_PATTERN =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/
export function isIsoDateTimeString(value: unknown): value is string {
return typeof value === 'string' && ISO_DATE_TIME_PATTERN.test(value.trim())
}
export function formatDateTime(value: string | null | undefined, fallback = '-') {
const normalized = String(value || '').trim()
if (!normalized) {
return fallback
}
const date = new Date(normalized)
if (Number.isNaN(date.getTime())) {
return normalized.replace('T', ' ').replace('Z', '')
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export function normalizeDateTimeValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map((item) => normalizeDateTimeValue(item))
}
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, currentValue]) => [
key,
normalizeDateTimeValue(currentValue),
]),
)
}
if (isIsoDateTimeString(value)) {
return formatDateTime(value, '')
}
return value
}
export function stringifyDisplayJson(value: unknown, space = 2) {
return JSON.stringify(normalizeDateTimeValue(value), null, space)
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"ignoreDeprecations": "6.0",
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
}
+31
View File
@@ -0,0 +1,31 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const apiTarget = process.env.VITE_API_TARGET || 'http://127.0.0.1'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(currentDir, 'src'),
},
},
server: {
host: '0.0.0.0',
port: 5174,
proxy: {
'/api': {
target: apiTarget,
changeOrigin: true,
},
'/health': {
target: apiTarget,
changeOrigin: true,
},
},
},
})