优化后台UI与开放文档排版设计
This commit is contained in:
+26
-2
@@ -80,8 +80,32 @@ export default function App() {
|
|||||||
locale={zhCN}
|
locale={zhCN}
|
||||||
theme={{
|
theme={{
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#1677ff',
|
colorPrimary: '#2563eb',
|
||||||
borderRadius: 6,
|
colorInfo: '#2563eb',
|
||||||
|
colorSuccess: '#16a34a',
|
||||||
|
colorWarning: '#d97706',
|
||||||
|
colorError: '#dc2626',
|
||||||
|
borderRadius: 8,
|
||||||
|
fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Card: {
|
||||||
|
paddingLG: 20,
|
||||||
|
borderRadiusLG: 10,
|
||||||
|
},
|
||||||
|
Table: {
|
||||||
|
headerBg: '#f8fafc',
|
||||||
|
headerColor: '#475569',
|
||||||
|
headerSplitColor: 'transparent',
|
||||||
|
rowHoverBg: '#f1f5f9',
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
fontWeight: 600,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
Tag: {
|
||||||
|
borderRadiusSM: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Breadcrumb, Typography, Space } from 'antd'
|
||||||
|
import { HomeOutlined } from '@ant-design/icons'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
title: string
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageHeaderProps {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
breadcrumbs?: BreadcrumbItem[]
|
||||||
|
extra?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageHeader: React.FC<PageHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
breadcrumbs = [],
|
||||||
|
extra,
|
||||||
|
}) => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<Link to="/" style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<HomeOutlined />
|
||||||
|
<span>首页</span>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...breadcrumbs.map((item) => ({
|
||||||
|
title: item.path ? <Link to={item.path}>{item.title}</Link> : item.title,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-header-wrapper">
|
||||||
|
<div className="page-header-main">
|
||||||
|
{breadcrumbs.length > 0 && (
|
||||||
|
<Breadcrumb items={items} className="page-header-breadcrumb" />
|
||||||
|
)}
|
||||||
|
<div className="page-header-heading">
|
||||||
|
<div>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0, fontWeight: 700, color: '#0f172a' }}>
|
||||||
|
{title}
|
||||||
|
</Typography.Title>
|
||||||
|
{subtitle && (
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 13, marginTop: 2, display: 'block' }}>
|
||||||
|
{subtitle}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{extra && <Space size="middle" className="page-header-extra">{extra}</Space>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeader
|
||||||
+773
-426
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,17 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Tag,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
|
CodeOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
OrderedListOutlined,
|
OrderedListOutlined,
|
||||||
@@ -53,13 +58,13 @@ const adminSections: SidebarSection[] = [
|
|||||||
key: 'products',
|
key: 'products',
|
||||||
label: '商品管理',
|
label: '商品管理',
|
||||||
icon: <AppstoreOutlined />,
|
icon: <AppstoreOutlined />,
|
||||||
children: [{ key: 'merchant-products', label: '商品', path: '/merchant-products' }],
|
children: [{ key: 'merchant-products', label: '商品列表', path: '/merchant-products' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'shops',
|
key: 'shops',
|
||||||
label: '商户管理',
|
label: '商户管理',
|
||||||
icon: <ShopOutlined />,
|
icon: <ShopOutlined />,
|
||||||
children: [{ key: 'shop-list', label: '商户列表', path: '/platform-merchants' }],
|
children: [{ key: 'shop-list', label: '平台商户', path: '/platform-merchants' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'orders',
|
key: 'orders',
|
||||||
@@ -71,11 +76,11 @@ const adminSections: SidebarSection[] = [
|
|||||||
key: 'staff',
|
key: 'staff',
|
||||||
label: '员工管理',
|
label: '员工管理',
|
||||||
icon: <TeamOutlined />,
|
icon: <TeamOutlined />,
|
||||||
children: [{ key: 'merchant-members', label: '成员', path: '/merchant-members' }],
|
children: [{ key: 'merchant-members', label: '成员管理', path: '/merchant-members' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'funds',
|
key: 'funds',
|
||||||
label: '积分',
|
label: '积分管理',
|
||||||
icon: <WalletOutlined />,
|
icon: <WalletOutlined />,
|
||||||
children: [{ key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet' }],
|
children: [{ key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet' }],
|
||||||
},
|
},
|
||||||
@@ -85,7 +90,7 @@ const adminSections: SidebarSection[] = [
|
|||||||
icon: <ApiOutlined />,
|
icon: <ApiOutlined />,
|
||||||
children: [
|
children: [
|
||||||
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
|
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
|
||||||
{ key: 'api-callbacks', label: '回调', path: '/merchant-callbacks' },
|
{ key: 'api-callbacks', label: '回调订阅', path: '/merchant-callbacks' },
|
||||||
{ key: 'api-docs', label: '对接文档', path: '/open-api' },
|
{ key: 'api-docs', label: '对接文档', path: '/open-api' },
|
||||||
{ key: 'api-debug', label: '调用调试', path: '/api-debug' },
|
{ key: 'api-debug', label: '调用调试', path: '/api-debug' },
|
||||||
],
|
],
|
||||||
@@ -103,7 +108,7 @@ const merchantSections: SidebarSection[] = [
|
|||||||
key: 'products',
|
key: 'products',
|
||||||
label: '商品管理',
|
label: '商品管理',
|
||||||
icon: <AppstoreOutlined />,
|
icon: <AppstoreOutlined />,
|
||||||
children: [{ key: 'merchant-products', label: '商品', path: '/merchant-products' }],
|
children: [{ key: 'merchant-products', label: '商品列表', path: '/merchant-products' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'orders',
|
key: 'orders',
|
||||||
@@ -113,7 +118,7 @@ const merchantSections: SidebarSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'funds',
|
key: 'funds',
|
||||||
label: '积分',
|
label: '积分管理',
|
||||||
icon: <WalletOutlined />,
|
icon: <WalletOutlined />,
|
||||||
children: [{ key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet' }],
|
children: [{ key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet' }],
|
||||||
},
|
},
|
||||||
@@ -121,7 +126,7 @@ const merchantSections: SidebarSection[] = [
|
|||||||
key: 'staff',
|
key: 'staff',
|
||||||
label: '员工管理',
|
label: '员工管理',
|
||||||
icon: <TeamOutlined />,
|
icon: <TeamOutlined />,
|
||||||
children: [{ key: 'merchant-members', label: '成员', path: '/merchant-members' }],
|
children: [{ key: 'merchant-members', label: '成员管理', path: '/merchant-members' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'open-api',
|
key: 'open-api',
|
||||||
@@ -129,7 +134,7 @@ const merchantSections: SidebarSection[] = [
|
|||||||
icon: <ApiOutlined />,
|
icon: <ApiOutlined />,
|
||||||
children: [
|
children: [
|
||||||
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
|
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
|
||||||
{ key: 'api-callbacks', label: '回调', path: '/merchant-callbacks' },
|
{ key: 'api-callbacks', label: '回调订阅', path: '/merchant-callbacks' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -205,10 +210,21 @@ export default function MainLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userMenu: MenuProps['items'] = [
|
const userMenu: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
key: 'user-info',
|
||||||
|
disabled: true,
|
||||||
|
label: (
|
||||||
|
<div style={{ padding: '4px 0' }}>
|
||||||
|
<div style={{ fontWeight: 600, color: '#0f172a' }}>{user?.nickname || user?.username}</div>
|
||||||
|
<div style={{ fontSize: 12, color: '#64748b' }}>角色:{isAdmin ? '平台超级管理员' : '商户成员'}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'logout',
|
key: 'logout',
|
||||||
icon: <LogoutOutlined />,
|
icon: <LogoutOutlined style={{ color: '#dc2626' }} />,
|
||||||
label: '退出登录',
|
label: <span style={{ color: '#dc2626' }}>退出登录</span>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
logout()
|
logout()
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
@@ -223,8 +239,15 @@ export default function MainLayout() {
|
|||||||
trigger={null}
|
trigger={null}
|
||||||
collapsible
|
collapsible
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
width={230}
|
width={240}
|
||||||
collapsedWidth={74}
|
collapsedWidth={74}
|
||||||
|
style={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
height: '100vh',
|
||||||
|
left: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="app-sidebar__brand">
|
<div className="app-sidebar__brand">
|
||||||
<span className="app-sidebar__brand-mark">S</span>
|
<span className="app-sidebar__brand-mark">S</span>
|
||||||
@@ -270,27 +293,61 @@ export default function MainLayout() {
|
|||||||
</nav>
|
</nav>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout className={`app-main${isDocsPage ? ' app-main--flush' : ''}`}>
|
<Layout className={`app-main${isDocsPage ? ' app-main--flush' : ''}`}>
|
||||||
<Header
|
<Header className="app-header">
|
||||||
className="app-header"
|
<div className="app-header__left">
|
||||||
>
|
<button
|
||||||
<button
|
className="app-header__collapse"
|
||||||
className="app-header__collapse"
|
type="button"
|
||||||
type="button"
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'}
|
||||||
aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'}
|
title={collapsed ? '展开侧边栏' : '收起侧边栏'}
|
||||||
title={collapsed ? '展开侧边栏' : '收起侧边栏'}
|
>
|
||||||
>
|
<MenuOutlined />
|
||||||
<MenuOutlined />
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<Dropdown menu={{ items: userMenu }}>
|
<div className="app-header__right">
|
||||||
<Space style={{ cursor: 'pointer' }}>
|
{isAdmin && (
|
||||||
<Avatar size="small" icon={<UserOutlined />} />
|
<Space size="small">
|
||||||
<Typography.Text>
|
<Tooltip title="开放 API 接口文档">
|
||||||
{user?.nickname || user?.username}
|
<Button
|
||||||
{isAdmin ? '(平台管理员)' : '(商户账号)'}
|
type="text"
|
||||||
</Typography.Text>
|
size="small"
|
||||||
</Space>
|
icon={<FileTextOutlined />}
|
||||||
</Dropdown>
|
onClick={() => navigate('/open-api')}
|
||||||
|
>
|
||||||
|
API文档
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="API 签名与请求在线调试器">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CodeOutlined />}
|
||||||
|
onClick={() => navigate('/api-debug')}
|
||||||
|
>
|
||||||
|
接口调试
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
<Dropdown menu={{ items: userMenu }} placement="bottomRight">
|
||||||
|
<div className="app-header__user">
|
||||||
|
<Avatar className="app-header__user-avatar" size="small" icon={<UserOutlined />} />
|
||||||
|
<Typography.Text style={{ fontWeight: 600, fontSize: 13.5, color: '#1e293b' }}>
|
||||||
|
{user?.nickname || user?.username}
|
||||||
|
</Typography.Text>
|
||||||
|
{isAdmin ? (
|
||||||
|
<Tag color="blue" bordered={false} style={{ margin: 0, fontSize: 11, fontWeight: 600 }}>
|
||||||
|
管理员
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag color="cyan" bordered={false} style={{ margin: 0, fontSize: 11, fontWeight: 600 }}>
|
||||||
|
商户
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
<Content className="app-content">
|
<Content className="app-content">
|
||||||
<div className="app-content__body">
|
<div className="app-content__body">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from 'react'
|
import { useState, type ReactNode } from 'react'
|
||||||
import { Space, Table, Tag, Typography } from 'antd'
|
import { Space, Table, Tag, Typography } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import type { EndpointSpec, ParamSpec } from './types'
|
import type { EndpointSpec, ParamSpec } from './types'
|
||||||
@@ -53,17 +53,35 @@ function highlightJson(json: string): string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function JsonBlock({ code }: { code: string }) {
|
function JsonBlock({ code, title = 'JSON Response' }: { code: string; title?: string }) {
|
||||||
return <pre className="api-docs__code" dangerouslySetInnerHTML={{ __html: highlightJson(code) }} />
|
const [copied, setCopied] = useState(false)
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="doc-code-card">
|
||||||
|
<div className="doc-code-header">
|
||||||
|
<span className="doc-code-title">{title}</span>
|
||||||
|
<button className="doc-code-copy" type="button" onClick={handleCopy}>
|
||||||
|
{copied ? '已复制 ✓' : '复制 JSON'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="api-docs__code" dangerouslySetInnerHTML={{ __html: highlightJson(code) }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MethodPathBlock({ method, path }: { method: string; path: string }) {
|
function MethodPathBlock({ method, path }: { method: string; path: string }) {
|
||||||
return (
|
return (
|
||||||
<pre className="api-docs__endpoint-line">
|
<div className="api-docs__endpoint-line">
|
||||||
<span className="api-docs__method" style={{ color: methodText[method] }}>{method}</span>
|
<span className={`api-docs__method-badge api-docs__method-badge--${method.toLowerCase()}`}>
|
||||||
{' '}
|
{method}
|
||||||
<span className="api-docs__path">{path}</span>
|
</span>
|
||||||
</pre>
|
<span className="api-docs__path-text">{path}</span>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { ClearOutlined, SendOutlined } from '@ant-design/icons'
|
import { ClearOutlined, SendOutlined } from '@ant-design/icons'
|
||||||
import { endpoints } from '../openapi/endpoints'
|
import { endpoints } from '../openapi/endpoints'
|
||||||
|
import { PageHeader } from '../components/PageHeader'
|
||||||
|
|
||||||
const STORAGE_KEY_APP = 'api_debug_app_key'
|
const STORAGE_KEY_APP = 'api_debug_app_key'
|
||||||
const STORAGE_KEY_SECRET = 'api_debug_secret'
|
const STORAGE_KEY_SECRET = 'api_debug_secret'
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography
|
const { Text } = Typography
|
||||||
const { TextArea } = Input
|
const { TextArea } = Input
|
||||||
|
|
||||||
const methodColor: Record<string, string> = {
|
const methodColor: Record<string, string> = {
|
||||||
@@ -228,12 +229,11 @@ export default function ApiDebugger() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title level={4} style={{ marginTop: 0 }}>
|
<PageHeader
|
||||||
API 调试工具
|
title="开放 API 在线调试器"
|
||||||
</Title>
|
subtitle="使用 AppKey 与 Secret 自动完成 HMAC-SHA256 签名计算,在线调用商户侧 /api/client/v1 接口"
|
||||||
<Paragraph type="secondary">
|
breadcrumbs={[{ title: '开放 API', path: '/open-api' }, { title: '调用调试' }]}
|
||||||
填写商户 API 密钥的 AppKey 与 Secret,选择接口并发送请求。签名在浏览器端使用 Web Crypto API 计算。
|
/>
|
||||||
</Paragraph>
|
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title="凭证"
|
title="凭证"
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
import { useEffect, useState, type ReactNode } from 'react'
|
import { useEffect, useState, type ReactNode } from 'react'
|
||||||
import { Card, Col, Progress, Row, Space, Statistic, Typography, Spin, message } from 'antd'
|
import { Card, Col, Progress, Row, Space, Statistic, Typography, Spin, message, Tag } from 'antd'
|
||||||
import {
|
import {
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
|
CodeOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
DollarOutlined,
|
DollarOutlined,
|
||||||
PercentageOutlined,
|
FileTextOutlined,
|
||||||
|
PlusOutlined,
|
||||||
ShopOutlined,
|
ShopOutlined,
|
||||||
ShoppingOutlined,
|
ShoppingOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { dashboardApi } from '../api'
|
import { dashboardApi } from '../api'
|
||||||
import type { DashboardStats } from '../types'
|
import type { DashboardStats } from '../types'
|
||||||
|
import { PageHeader } from '../components/PageHeader'
|
||||||
|
import { useAuth } from '../store/auth'
|
||||||
|
|
||||||
const numberText = (value?: number) => (value ?? 0).toLocaleString('zh-CN')
|
const numberText = (value?: number) => (value ?? 0).toLocaleString('zh-CN')
|
||||||
|
|
||||||
@@ -23,51 +28,74 @@ function ratio(part: number, total: number) {
|
|||||||
return Math.round((part / total) * 100)
|
return Math.round((part / total) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetricCard({
|
function ModernMetricCard({
|
||||||
title,
|
title,
|
||||||
value,
|
value,
|
||||||
icon,
|
icon,
|
||||||
suffix,
|
unit = '',
|
||||||
color,
|
iconTheme = '',
|
||||||
|
footerText,
|
||||||
|
footerValue,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
value: number
|
value: number
|
||||||
icon: ReactNode
|
icon: ReactNode
|
||||||
suffix?: string
|
unit?: string
|
||||||
color?: string
|
iconTheme?: string
|
||||||
|
footerText?: string
|
||||||
|
footerValue?: string | number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="metric-card-box">
|
||||||
<Statistic
|
<div className="metric-card-header">
|
||||||
title={title}
|
<span className="metric-card-title">{title}</span>
|
||||||
value={value}
|
<div className={`metric-card-icon ${iconTheme}`}>{icon}</div>
|
||||||
formatter={() => numberText(value)}
|
</div>
|
||||||
prefix={icon}
|
<div className="metric-card-value">
|
||||||
suffix={suffix}
|
{numberText(value)}
|
||||||
valueStyle={color ? { color } : undefined}
|
{unit && <span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>{unit}</span>}
|
||||||
/>
|
</div>
|
||||||
</Card>
|
{footerText && (
|
||||||
|
<div className="metric-card-footer">
|
||||||
|
<span>{footerText}</span>
|
||||||
|
<span style={{ fontWeight: 600, color: '#0f172a' }}>{footerValue}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusLine({
|
function CustomStatusLine({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
total,
|
total,
|
||||||
color,
|
color,
|
||||||
|
tagClass,
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
value: number
|
value: number
|
||||||
total: number
|
total: number
|
||||||
color: string
|
color: string
|
||||||
|
tagClass: string
|
||||||
}) {
|
}) {
|
||||||
|
const percent = ratio(value, total)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||||
<Typography.Text>{label}</Typography.Text>
|
<div className={`status-tag ${tagClass}`}>
|
||||||
<Typography.Text strong>{numberText(value)}</Typography.Text>
|
<span className="status-dot"></span>
|
||||||
</Space>
|
{label}
|
||||||
<Progress percent={ratio(value, total)} strokeColor={color} showInfo={false} />
|
</div>
|
||||||
|
<Space size="small">
|
||||||
|
<Typography.Text strong style={{ fontSize: 15, color: '#0f172a' }}>
|
||||||
|
{numberText(value)} 单
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
({percent}%)
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Progress percent={percent} strokeColor={color} showInfo={false} size="small" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -75,6 +103,8 @@ function StatusLine({
|
|||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const { user, isAdmin } = useAuth()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dashboardApi
|
dashboardApi
|
||||||
@@ -86,137 +116,238 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', padding: 80 }}>
|
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||||
<Spin size="large" />
|
<Spin size="large" tip="正在加载仪表盘数据..." />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }} align="start">
|
<PageHeader
|
||||||
<div>
|
title="运营工作台"
|
||||||
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 4 }}>
|
subtitle={stats?.scope === 'platform' ? '平台全局运营数据与发货概览' : '当前商户运营数据与极速发货概览'}
|
||||||
数据概览
|
breadcrumbs={[{ title: '工作台' }]}
|
||||||
</Typography.Title>
|
extra={
|
||||||
<Typography.Text type="secondary">
|
<Tag color="blue" style={{ padding: '4px 12px', borderRadius: 20, fontSize: 12, fontWeight: 600 }}>
|
||||||
{stats?.scope === 'platform' ? '平台全局运营数据' : '当前商户运营数据'}
|
{stats?.scope === 'platform' ? '全局视图' : '商户视图'}
|
||||||
</Typography.Text>
|
</Tag>
|
||||||
</div>
|
}
|
||||||
</Space>
|
/>
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
{/* 顶部欢迎卡片 & 快捷 LaunchPad */}
|
||||||
|
<div className="dash-welcome-card">
|
||||||
|
<h2 className="dash-welcome-title">
|
||||||
|
你好,{user?.nickname || user?.username} 👋
|
||||||
|
</h2>
|
||||||
|
<p className="dash-welcome-sub">
|
||||||
|
欢迎回到 Skin Hub 游戏皮肤分销管理平台。以下是系统的最新业务状态与发货订单追踪数据。
|
||||||
|
</p>
|
||||||
|
<div className="dash-quick-launch">
|
||||||
|
<button className="dash-quick-btn" type="button" onClick={() => navigate('/merchant-orders')}>
|
||||||
|
<PlusOutlined /> 发货订单管理
|
||||||
|
</button>
|
||||||
|
<button className="dash-quick-btn" type="button" onClick={() => navigate('/merchant-wallet')}>
|
||||||
|
<WalletOutlined /> 积分流水查看
|
||||||
|
</button>
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<button className="dash-quick-btn" type="button" onClick={() => navigate('/platform-merchants')}>
|
||||||
|
<ShopOutlined /> 平台商户管理
|
||||||
|
</button>
|
||||||
|
<button className="dash-quick-btn" type="button" onClick={() => navigate('/open-api')}>
|
||||||
|
<FileTextOutlined /> 开放接口文档
|
||||||
|
</button>
|
||||||
|
<button className="dash-quick-btn" type="button" onClick={() => navigate('/api-debug')}>
|
||||||
|
<CodeOutlined /> 在线 API 调试
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第一组:核心业务指标 */}
|
||||||
|
<Typography.Title level={5} style={{ marginBottom: 14, fontWeight: 700, color: '#1e293b' }}>
|
||||||
|
基础数据指标
|
||||||
|
</Typography.Title>
|
||||||
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<MetricCard
|
<ModernMetricCard
|
||||||
title={stats?.scope === 'platform' ? '可售商品' : '商户商品'}
|
title={stats?.scope === 'platform' ? '平台可售商品' : '商户上架商品'}
|
||||||
value={stats?.active_product_count ?? 0}
|
value={stats?.active_product_count ?? 0}
|
||||||
icon={<ShoppingOutlined />}
|
icon={<ShoppingOutlined />}
|
||||||
|
iconTheme=""
|
||||||
|
footerText="活跃商品库"
|
||||||
|
footerValue="已上线"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<MetricCard
|
<ModernMetricCard
|
||||||
title={stats?.scope === 'platform' ? '启用商户' : '当前商户'}
|
title={stats?.scope === 'platform' ? '平台启用商户' : '当前合作商户'}
|
||||||
value={stats?.active_merchant_count ?? 0}
|
value={stats?.active_merchant_count ?? 0}
|
||||||
icon={<ShopOutlined />}
|
icon={<ShopOutlined />}
|
||||||
|
iconTheme="metric-card-icon--purple"
|
||||||
|
footerText="商户状态"
|
||||||
|
footerValue="正常运行"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<MetricCard title="成员账号" value={stats?.user_count ?? 0} icon={<TeamOutlined />} />
|
<ModernMetricCard
|
||||||
|
title="平台成员账号"
|
||||||
|
value={stats?.user_count ?? 0}
|
||||||
|
icon={<TeamOutlined />}
|
||||||
|
iconTheme="metric-card-icon--emerald"
|
||||||
|
footerText="团队成员"
|
||||||
|
footerValue="活跃"
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<MetricCard title="订单总数" value={stats?.order_count ?? 0} icon={<DatabaseOutlined />} />
|
<ModernMetricCard
|
||||||
|
title="累计订单总数"
|
||||||
|
value={stats?.order_count ?? 0}
|
||||||
|
icon={<DatabaseOutlined />}
|
||||||
|
iconTheme="metric-card-icon--amber"
|
||||||
|
footerText="今日新增订单"
|
||||||
|
footerValue={`${stats?.today_order_count ?? 0} 单`}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<MetricCard title="今日订单" value={stats?.today_order_count ?? 0} icon={<ClockCircleOutlined />} />
|
<ModernMetricCard
|
||||||
|
title="今日订单总量"
|
||||||
|
value={stats?.today_order_count ?? 0}
|
||||||
|
icon={<ClockCircleOutlined />}
|
||||||
|
iconTheme=""
|
||||||
|
footerText="占比全量"
|
||||||
|
footerValue={`${ratio(stats?.today_order_count ?? 0, stats?.order_count ?? 0)}%`}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<MetricCard
|
<ModernMetricCard
|
||||||
title="待发货订单"
|
title="待处理/待发货订单"
|
||||||
value={stats?.paid_order_count ?? 0}
|
value={stats?.paid_order_count ?? 0}
|
||||||
icon={<ClockCircleOutlined />}
|
icon={<ClockCircleOutlined />}
|
||||||
color={(stats?.paid_order_count ?? 0) > 0 ? '#cf1322' : undefined}
|
iconTheme={(stats?.paid_order_count ?? 0) > 0 ? 'metric-card-icon--rose' : ''}
|
||||||
|
footerText="需及时发货"
|
||||||
|
footerValue={(stats?.paid_order_count ?? 0) > 0 ? '需处理' : '无积压'}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Typography.Title level={5} style={{ marginTop: 24 }}>
|
{/* 第二组:积分与资金监控 */}
|
||||||
积分与费用
|
<Typography.Title level={5} style={{ marginBottom: 14, fontWeight: 700, color: '#1e293b' }}>
|
||||||
|
积分变动与账户额度
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<MetricCard title="成交积分" value={stats?.total_sales ?? 0} icon={<DollarOutlined />} suffix="积分" />
|
<ModernMetricCard
|
||||||
|
title="累计成交积分"
|
||||||
|
value={stats?.total_sales ?? 0}
|
||||||
|
unit="积分"
|
||||||
|
icon={<DollarOutlined />}
|
||||||
|
iconTheme="metric-card-icon--emerald"
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<MetricCard title="今日成交" value={stats?.today_sales ?? 0} icon={<DollarOutlined />} suffix="积分" />
|
<ModernMetricCard
|
||||||
|
title="今日成交积分"
|
||||||
|
value={stats?.today_sales ?? 0}
|
||||||
|
unit="积分"
|
||||||
|
icon={<DollarOutlined />}
|
||||||
|
iconTheme="metric-card-icon--amber"
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<MetricCard title="平台手续费" value={stats?.total_fees ?? 0} icon={<PercentageOutlined />} suffix="积分" />
|
<ModernMetricCard
|
||||||
|
title="平台扣费/手续费"
|
||||||
|
value={stats?.total_fees ?? 0}
|
||||||
|
unit="积分"
|
||||||
|
icon={<WalletOutlined />}
|
||||||
|
iconTheme="metric-card-icon--purple"
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<MetricCard title="可用余额" value={stats?.wallet_available_balance ?? 0} icon={<WalletOutlined />} suffix="积分" />
|
<ModernMetricCard
|
||||||
|
title="账户可用余额"
|
||||||
|
value={stats?.wallet_available_balance ?? 0}
|
||||||
|
unit="积分"
|
||||||
|
icon={<WalletOutlined />}
|
||||||
|
iconTheme=""
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
{/* 第三组:订单发货分布与 API 监控 */}
|
||||||
|
<Row gutter={[20, 20]}>
|
||||||
<Col xs={24} lg={14}>
|
<Col xs={24} lg={14}>
|
||||||
<Card title="订单状态">
|
<Card title="发货订单状态占比分布" style={{ height: '100%' }}>
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<CustomStatusLine
|
||||||
<StatusLine
|
label="待发货 (paid)"
|
||||||
label="待发货"
|
value={stats?.paid_order_count ?? 0}
|
||||||
value={stats?.paid_order_count ?? 0}
|
total={stats?.order_count ?? 0}
|
||||||
total={stats?.order_count ?? 0}
|
color="#2563eb"
|
||||||
color="#1677ff"
|
tagClass="status-tag--blue"
|
||||||
/>
|
/>
|
||||||
<StatusLine
|
<CustomStatusLine
|
||||||
label="发货中"
|
label="发货中 (delivering)"
|
||||||
value={stats?.delivering_order_count ?? 0}
|
value={stats?.delivering_order_count ?? 0}
|
||||||
total={stats?.order_count ?? 0}
|
total={stats?.order_count ?? 0}
|
||||||
color="#13c2c2"
|
color="#0891b2"
|
||||||
/>
|
tagClass="status-tag--cyan"
|
||||||
<StatusLine
|
/>
|
||||||
label="已交付"
|
<CustomStatusLine
|
||||||
value={stats?.delivered_order_count ?? 0}
|
label="已交付 (delivered)"
|
||||||
total={stats?.order_count ?? 0}
|
value={stats?.delivered_order_count ?? 0}
|
||||||
color="#52c41a"
|
total={stats?.order_count ?? 0}
|
||||||
/>
|
color="#16a34a"
|
||||||
<StatusLine
|
tagClass="status-tag--green"
|
||||||
label="发货失败"
|
/>
|
||||||
value={stats?.ship_failed_order_count ?? 0}
|
<CustomStatusLine
|
||||||
total={stats?.order_count ?? 0}
|
label="发货失败 (ship_failed)"
|
||||||
color="#ff4d4f"
|
value={stats?.ship_failed_order_count ?? 0}
|
||||||
/>
|
total={stats?.order_count ?? 0}
|
||||||
<StatusLine
|
color="#dc2626"
|
||||||
label="已取消"
|
tagClass="status-tag--red"
|
||||||
value={stats?.cancelled_order_count ?? 0}
|
/>
|
||||||
total={stats?.order_count ?? 0}
|
<CustomStatusLine
|
||||||
color="#8c8c8c"
|
label="已取消 (cancelled)"
|
||||||
/>
|
value={stats?.cancelled_order_count ?? 0}
|
||||||
</Space>
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#64748b"
|
||||||
|
tagClass="status-tag--gray"
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} lg={10}>
|
<Col xs={24} lg={10}>
|
||||||
<Card title="接口与回调">
|
<Card title="接口密钥与 Webhook 回调监控" style={{ height: '100%' }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 20]}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="API 密钥"
|
title="API 密钥 (活跃/总量)"
|
||||||
value={stats?.active_api_client_count ?? 0}
|
value={stats?.active_api_client_count ?? 0}
|
||||||
prefix={<ApiOutlined />}
|
prefix={<ApiOutlined style={{ color: '#2563eb' }} />}
|
||||||
suffix={`/ ${numberText(stats?.api_client_count)}`}
|
suffix={`/ ${numberText(stats?.api_client_count)}`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Statistic title="回调订阅" value={stats?.callback_subscription_count ?? 0} prefix={<CheckCircleOutlined />} />
|
<Statistic
|
||||||
</Col>
|
title="Webhook 回调订阅"
|
||||||
<Col span={12}>
|
value={stats?.callback_subscription_count ?? 0}
|
||||||
<Statistic title="待投递回调" value={stats?.pending_callback_count ?? 0} prefix={<ClockCircleOutlined />} />
|
prefix={<CheckCircleOutlined style={{ color: '#16a34a' }} />}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="失败回调"
|
title="待投递回调队列"
|
||||||
|
value={stats?.pending_callback_count ?? 0}
|
||||||
|
prefix={<ClockCircleOutlined style={{ color: '#d97706' }} />}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Statistic
|
||||||
|
title="投递失败回调"
|
||||||
value={stats?.failed_callback_count ?? 0}
|
value={stats?.failed_callback_count ?? 0}
|
||||||
prefix={<CloseCircleOutlined />}
|
prefix={<CloseCircleOutlined style={{ color: '#dc2626' }} />}
|
||||||
valueStyle={(stats?.failed_callback_count ?? 0) > 0 ? { color: '#cf1322' } : undefined}
|
valueStyle={(stats?.failed_callback_count ?? 0) > 0 ? { color: '#dc2626' } : undefined}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,66 +1,132 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Card, Form, Input, Button, Typography, message, Space } from 'antd'
|
import { Form, Input, Button, Typography, message, Space, Tag } from 'antd'
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
import { UserOutlined, LockOutlined, RightOutlined } from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/auth'
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { login } = useAuth()
|
const { login } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
const onFinish = async (values: { username: string; password: string }) => {
|
const onFinish = async (values: { username: string; password: string }) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await login(values.username, values.password)
|
await login(values.username, values.password)
|
||||||
message.success('登录成功')
|
message.success('登录成功,欢迎回来!')
|
||||||
navigate('/')
|
navigate('/')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e instanceof Error ? e.message : '登录失败')
|
message.error(e instanceof Error ? e.message : '登录失败,请检查账号密码')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fillCredentials = (user: string, pass: string) => {
|
||||||
|
form.setFieldsValue({ username: user, password: pass })
|
||||||
|
message.info(`已填入账号: ${user}`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="login-wrapper">
|
||||||
style={{
|
<div className="login-card">
|
||||||
minHeight: '100vh',
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card style={{ width: 400, boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
|
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
<div className="login-logo">S</div>
|
||||||
游戏皮肤供货平台
|
<Typography.Title level={3} style={{ margin: '0 0 6px 0', color: '#ffffff', fontWeight: 700 }}>
|
||||||
|
Skin Hub 游戏皮肤分销平台
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="secondary">登录后台管理</Typography.Text>
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>
|
||||||
|
基于 Go + React 的高并发皮肤分发管理系统
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Form layout="vertical" onFinish={onFinish} initialValues={{ username: 'admin', password: 'admin123' }}>
|
|
||||||
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
<Form
|
||||||
<Input prefix={<UserOutlined />} placeholder="用户名" size="large" />
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
initialValues={{ username: 'admin', password: 'admin123' }}
|
||||||
|
requiredMark={false}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
style={{ marginBottom: 18 }}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined style={{ color: '#64748b' }} />}
|
||||||
|
placeholder="用户名 / 手机号"
|
||||||
|
size="large"
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
|
||||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" size="large" />
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined style={{ color: '#64748b' }} />}
|
||||||
|
placeholder="密码"
|
||||||
|
size="large"
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" loading={loading} block size="large">
|
<Form.Item style={{ marginBottom: 12 }}>
|
||||||
登录
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
icon={<RightOutlined />}
|
||||||
|
style={{
|
||||||
|
height: 46,
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 600,
|
||||||
|
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
|
||||||
|
boxShadow: '0 4px 14px rgba(37, 99, 235, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
安全登录
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0, textAlign: 'center' }}>
|
|
||||||
商户账号请联系平台管理员创建
|
{/* 快捷测试账号填充 */}
|
||||||
</Typography.Paragraph>
|
<div
|
||||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0, textAlign: 'center' }}>
|
style={{
|
||||||
默认管理员:admin / admin123
|
padding: '12px 14px',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: 'rgba(255, 255, 255, 0.04)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 8, textAlign: 'center' }}>
|
||||||
|
快速体验(演示账号列表):
|
||||||
|
</div>
|
||||||
|
<Space style={{ width: '100%', justifyContent: 'center' }} wrap>
|
||||||
|
<Tag
|
||||||
|
color="blue"
|
||||||
|
style={{ cursor: 'pointer', padding: '4px 10px', borderRadius: 4 }}
|
||||||
|
onClick={() => fillCredentials('admin', 'admin123')}
|
||||||
|
>
|
||||||
|
管理员 (admin)
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography.Paragraph
|
||||||
|
style={{ fontSize: 12, margin: 0, textAlign: 'center', color: '#64748b' }}
|
||||||
|
>
|
||||||
|
商户账号与成员密钥请联系平台管理员统一配置与创建
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1096,11 +1096,31 @@ function moneyWithSign(value?: number | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function productStatusTag(value: string) {
|
function productStatusTag(value: string) {
|
||||||
return value === 'active' ? <Tag color="green">上架</Tag> : <Tag>下架</Tag>
|
return value === 'active' ? (
|
||||||
|
<span className="status-tag status-tag--green">
|
||||||
|
<span className="status-dot"></span>
|
||||||
|
已上架
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="status-tag status-tag--gray">
|
||||||
|
<span className="status-dot"></span>
|
||||||
|
已下架
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function activeStatusTag(value: string) {
|
function activeStatusTag(value: string) {
|
||||||
return value === 'active' ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag>
|
return value === 'active' ? (
|
||||||
|
<span className="status-tag status-tag--green">
|
||||||
|
<span className="status-dot"></span>
|
||||||
|
已启用
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="status-tag status-tag--gray">
|
||||||
|
<span className="status-dot"></span>
|
||||||
|
已禁用
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function productOptionLabel(item: MerchantProduct) {
|
function productOptionLabel(item: MerchantProduct) {
|
||||||
@@ -1110,7 +1130,20 @@ function productOptionLabel(item: MerchantProduct) {
|
|||||||
|
|
||||||
function orderStatusTag(value: string) {
|
function orderStatusTag(value: string) {
|
||||||
const item = orderStatusMap[value] || { color: 'default', text: value }
|
const item = orderStatusMap[value] || { color: 'default', text: value }
|
||||||
return <Tag color={item.color}>{item.text}</Tag>
|
const tagClassMap: Record<string, string> = {
|
||||||
|
paid: 'status-tag--blue',
|
||||||
|
delivering: 'status-tag--cyan',
|
||||||
|
delivered: 'status-tag--green',
|
||||||
|
ship_failed: 'status-tag--red',
|
||||||
|
cancelled: 'status-tag--gray',
|
||||||
|
}
|
||||||
|
const tagClass = tagClassMap[value] || 'status-tag--gray'
|
||||||
|
return (
|
||||||
|
<span className={`status-tag ${tagClass}`}>
|
||||||
|
<span className="status-dot"></span>
|
||||||
|
{item.text}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ledgerTypeTag(value: string) {
|
function ledgerTypeTag(value: string) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { Alert, Card, Descriptions, Table, Tag, Typography } from 'antd'
|
import { Alert, Card, Descriptions, Table, Tag, Typography } from 'antd'
|
||||||
|
import { BookOutlined } from '@ant-design/icons'
|
||||||
import EndpointDoc, { CommonResponseDoc } from '../openapi/EndpointDoc'
|
import EndpointDoc, { CommonResponseDoc } from '../openapi/EndpointDoc'
|
||||||
import {
|
import {
|
||||||
commonResponseFields,
|
commonResponseFields,
|
||||||
@@ -180,6 +181,10 @@ export default function OpenApiDocs() {
|
|||||||
<div className="api-docs">
|
<div className="api-docs">
|
||||||
{/* 左侧目录 */}
|
{/* 左侧目录 */}
|
||||||
<aside className="api-docs__toc">
|
<aside className="api-docs__toc">
|
||||||
|
<div className="api-docs__toc-header">
|
||||||
|
<BookOutlined style={{ color: '#2563eb', fontSize: 16 }} />
|
||||||
|
<span>文档章节目录</span>
|
||||||
|
</div>
|
||||||
<nav className="api-docs__nav">
|
<nav className="api-docs__nav">
|
||||||
{toc.map((group) => (
|
{toc.map((group) => (
|
||||||
<div className="api-docs__nav-group" key={group.title}>
|
<div className="api-docs__nav-group" key={group.title}>
|
||||||
@@ -330,21 +335,38 @@ function OverviewSection() {
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="mode"
|
rowKey="mode"
|
||||||
dataSource={deliveryModes}
|
dataSource={deliveryModes}
|
||||||
|
scroll={{ x: 1080 }}
|
||||||
columns={[
|
columns={[
|
||||||
{ title: '方式', dataIndex: 'mode', width: 130 },
|
{
|
||||||
{ title: '适用场景', dataIndex: 'scene' },
|
title: '方式',
|
||||||
|
dataIndex: 'mode',
|
||||||
|
width: 140,
|
||||||
|
render: (v: string) => <span style={{ fontWeight: 700, whiteSpace: 'nowrap', color: '#0f172a' }}>{v}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '适用场景',
|
||||||
|
dataIndex: 'scene',
|
||||||
|
width: 280,
|
||||||
|
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.6, display: 'block', color: '#334155' }}>{v}</span>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '接口顺序',
|
title: '接口顺序',
|
||||||
dataIndex: 'steps',
|
dataIndex: 'steps',
|
||||||
width: 250,
|
width: 240,
|
||||||
render: (v: string) => <pre className="api-docs__pre">{v}</pre>,
|
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '相关接口',
|
title: '相关接口',
|
||||||
dataIndex: 'interfaces',
|
dataIndex: 'interfaces',
|
||||||
render: (v: string) => <pre className="api-docs__pre">{v}</pre>,
|
width: 260,
|
||||||
|
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '建议',
|
||||||
|
dataIndex: 'recommend',
|
||||||
|
width: 180,
|
||||||
|
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.5, display: 'block', color: '#475569' }}>{v}</span>,
|
||||||
},
|
},
|
||||||
{ title: '建议', dataIndex: 'recommend', width: 170 },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { platformApi } from '../api'
|
import { platformApi } from '../api'
|
||||||
import type { Merchant, MerchantMember, PageResult, ProductCatalogItem } from '../types'
|
import type { Merchant, MerchantMember, PageResult, ProductCatalogItem } from '../types'
|
||||||
import { formatDateTime } from '../utils/time'
|
import { formatDateTime } from '../utils/time'
|
||||||
|
import { PageHeader } from '../components/PageHeader'
|
||||||
|
|
||||||
const memberRoleOptions = [
|
const memberRoleOptions = [
|
||||||
{ value: 'owner', label: '负责人' },
|
{ value: 'owner', label: '负责人' },
|
||||||
@@ -194,7 +195,23 @@ export default function PlatformMerchants() {
|
|||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ title: '状态', dataIndex: 'status', width: 90, render: (v) => v === 'active' ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag> },
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (v) =>
|
||||||
|
v === 'active' ? (
|
||||||
|
<span className="status-tag status-tag--green">
|
||||||
|
<span className="status-dot"></span>
|
||||||
|
已启用
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="status-tag status-tag--gray">
|
||||||
|
<span className="status-dot"></span>
|
||||||
|
已禁用
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
{ title: '创建时间', dataIndex: 'created_at', width: 180, render: formatDateTime },
|
{ title: '创建时间', dataIndex: 'created_at', width: 180, render: formatDateTime },
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -257,35 +274,34 @@ export default function PlatformMerchants() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
<PageHeader
|
||||||
<div>
|
title="平台商户管理"
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
subtitle="平台管理员维护各分销商户租户、分配商品库、配置手续费费率与授权成员"
|
||||||
商户管理
|
breadcrumbs={[{ title: '商户管理' }]}
|
||||||
</Typography.Title>
|
extra={
|
||||||
<Typography.Text type="secondary">平台管理员维护租户与授权关系。</Typography.Text>
|
<Space>
|
||||||
</div>
|
<Button icon={<ReloadOutlined />} onClick={() => load()}>
|
||||||
<Space>
|
刷新
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => load()}>
|
</Button>
|
||||||
刷新
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
<Button
|
icon={<PlusOutlined />}
|
||||||
type="primary"
|
onClick={() => {
|
||||||
icon={<PlusOutlined />}
|
createForm.resetFields()
|
||||||
onClick={() => {
|
createForm.setFieldsValue({
|
||||||
createForm.resetFields()
|
features: featureOptions.map((item) => item.value),
|
||||||
createForm.setFieldsValue({
|
fee_type: 'rate',
|
||||||
features: featureOptions.map((item) => item.value),
|
fee_rate_bp: 0,
|
||||||
fee_type: 'rate',
|
fee_fixed_amount: 0,
|
||||||
fee_rate_bp: 0,
|
})
|
||||||
fee_fixed_amount: 0,
|
setCreateOpen(true)
|
||||||
})
|
}}
|
||||||
setCreateOpen(true)
|
>
|
||||||
}}
|
新增商户
|
||||||
>
|
</Button>
|
||||||
新增商户
|
</Space>
|
||||||
</Button>
|
}
|
||||||
</Space>
|
/>
|
||||||
</Space>
|
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
Reference in New Issue
Block a user