新增 React Ant Design 前端骨架
This commit is contained in:
@@ -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>
|
||||
Generated
+2687
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 '客服'
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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`, {})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user