优化后台UI与开放文档排版设计

This commit is contained in:
yml2213
2026-08-04 11:01:52 +08:00
parent ae9804e158
commit c015def4cd
11 changed files with 1422 additions and 646 deletions
+26 -2
View File
@@ -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,
},
}, },
}} }}
> >
+62
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+90 -33
View File
@@ -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">
+26 -8
View File
@@ -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>
) )
} }
+7 -7
View File
@@ -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="凭证"
+230 -99
View File
@@ -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>
+98 -32
View File
@@ -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>
) )
} }
+36 -3
View File
@@ -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) {
+28 -6
View File
@@ -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>
+46 -30
View File
@@ -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"