实现分销网后台基础功能与独立前端

This commit is contained in:
yml2213
2026-09-05 10:52:36 +08:00
parent 85332df2bd
commit 2ffa7a89b1
24 changed files with 6122 additions and 25 deletions
+40
View File
@@ -0,0 +1,40 @@
import React, { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { App, Card, ConfigProvider, Layout, Menu, Space, Statistic, Table, Tag, Typography, message } from 'antd'
import { AccountBookOutlined, ApiOutlined, DashboardOutlined, FileSearchOutlined, SettingOutlined, ShopOutlined } from '@ant-design/icons'
import axios from 'axios'
import './style.css'
const api = axios.create({ baseURL: '/api' })
type Site = { id: number; site_name: string; site_code: string; domain: string; status: string; display_account_enabled: boolean; tax_enabled: boolean; tax_rate_bps: number }
type Order = { order_id: number; order_no: string; site_id: number; status: string; settlement_status: string; net_paid_amount_cent: number; payment_fee_cent: number; service_fee_cent: number; tax_amount_cent: number; created_at: string }
function Dashboard() {
const [sites, setSites] = useState<Site[]>([])
const [orders, setOrders] = useState<Order[]>([])
useEffect(() => { Promise.all([api.get('/admin/distribution/sites'), api.get('/admin/distribution/orders?kind=all')]).then(([a, b]) => { setSites(a.data.data.items ?? []); setOrders(b.data.data.items ?? []) }).catch(() => message.error('数据加载失败')) }, [])
const total = orders.reduce((sum, item) => sum + item.net_paid_amount_cent, 0)
return <Space direction="vertical" size="large" className="page"><Typography.Title level={3}>分销运营概览</Typography.Title><div className="stats"><Card><Statistic title="分销网站" value={sites.length} /></Card><Card><Statistic title="正常订单" value={orders.length} /></Card><Card><Statistic title="净支付金额" value={(total / 100).toFixed(2)} suffix="元" /></Card></div><Card title="最近订单"><Table rowKey="order_id" dataSource={orders.slice(0, 10)} pagination={false} columns={[{ title: '订单号', dataIndex: 'order_no' }, { title: '状态', dataIndex: 'status', render: (v) => <Tag color={v === 'completed' ? 'green' : 'blue'}>{v}</Tag> }, { title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v) => `¥${(v / 100).toFixed(2)}` }, { title: '创建时间', dataIndex: 'created_at' }]} /></Card></Space>
}
function Sites() {
const [items, setItems] = useState<Site[]>([])
useEffect(() => { api.get('/admin/distribution/sites').then((r) => setItems(r.data.data.items ?? [])).catch(() => message.error('站点加载失败')) }, [])
return <Space direction="vertical" className="page"><Typography.Title level={3}>分销网站</Typography.Title><Card><Table rowKey="id" dataSource={items} columns={[{ title: '名称', dataIndex: 'site_name' }, { title: '编码', dataIndex: 'site_code' }, { title: '域名', dataIndex: 'domain' }, { title: '状态', dataIndex: 'status' }, { title: '账号展示', dataIndex: 'display_account_enabled', render: (v) => v ? <Tag color="green">开启</Tag> : <Tag>关闭</Tag> }, { title: '税费', render: (_, r) => r.tax_enabled ? `${r.tax_rate_bps / 100}%` : '关闭' }]} /></Card></Space>
}
function Orders({ kind }: { kind: 'sold' | 'receive' }) {
const [items, setItems] = useState<Order[]>([])
useEffect(() => { api.get(`/admin/distribution/orders?kind=${kind}`).then((r) => setItems(r.data.data.items ?? [])).catch(() => message.error('订单加载失败')) }, [kind])
return <Space direction="vertical" className="page"><Typography.Title level={3}>{kind === 'sold' ? '售出账号订单' : '分销收号订单'}</Typography.Title><Card><Table rowKey="order_id" dataSource={items} columns={[{ title: '订单号', dataIndex: 'order_no' }, { title: '订单状态', dataIndex: 'status' }, { title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v) => `¥${(v / 100).toFixed(2)}` }, { title: '手续费', dataIndex: 'payment_fee_cent', render: (v) => `¥${(v / 100).toFixed(2)}` }, { title: '服务费', dataIndex: 'service_fee_cent', render: (v) => `¥${(v / 100).toFixed(2)}` }, { title: '结算', dataIndex: 'settlement_status' }]} /></Card></Space>
}
function Placeholder({ title }: { title: string }) { return <Space direction="vertical" className="page"><Typography.Title level={3}>{title}</Typography.Title><Card><Typography.Text type="secondary">该模块已预留接口入口,可在后台继续配置。</Typography.Text></Card></Space> }
function Shell() {
const [key, setKey] = useState('dashboard')
const content = key === 'dashboard' ? <Dashboard /> : key === 'sites' ? <Sites /> : key === 'sold' ? <Orders kind="sold" /> : key === 'receive' ? <Orders kind="receive" /> : <Placeholder title={({ settlements: '结算管理', payments: '支付通道', accounts: '账号展示', rules: '比例与税费规则', qrcodes: '客服组与二维码', chats: '客服消息' } as Record<string, string>)[key] ?? '分销后台'} />
return <Layout className="app"><Layout.Sider breakpoint="lg" collapsedWidth="0"><div className="brand">HFB 分销后台</div><Menu theme="dark" selectedKeys={[key]} onClick={(e) => setKey(e.key)} items={[{ key: 'dashboard', icon: <DashboardOutlined />, label: '仪表盘' }, { key: 'sites', icon: <ShopOutlined />, label: '分销网站' }, { key: 'sold', icon: <FileSearchOutlined />, label: '售出账号' }, { key: 'receive', icon: <AccountBookOutlined />, label: '分销收号' }, { key: 'settlements', icon: <AccountBookOutlined />, label: '结算管理' }, { key: 'payments', icon: <ApiOutlined />, label: '支付通道' }, { key: 'accounts', icon: <SettingOutlined />, label: '账号展示' }, { key: 'rules', icon: <SettingOutlined />, label: '比例与税费' }, { key: 'qrcodes', icon: <SettingOutlined />, label: '客服二维码' }, { key: 'chats', icon: <SettingOutlined />, label: '客服消息' }]} /></Layout.Sider><Layout><Layout.Header className="header"><Typography.Text>主站 hao858.com · 分销子域名 distribution.hao858.com</Typography.Text></Layout.Header><Layout.Content>{content}</Layout.Content></Layout></Layout>
}
createRoot(document.getElementById('root')!).render(<React.StrictMode><ConfigProvider theme={{ token: { colorPrimary: '#1677ff', borderRadius: 6 } }}><App><Shell /></App></ConfigProvider></React.StrictMode>)
+9
View File
@@ -0,0 +1,9 @@
:root { font-family: Inter, "PingFang SC", sans-serif; color: #1f2937; background: #f5f7fa; }
* { box-sizing: border-box; }
body { margin: 0; }
.app { min-height: 100vh; }
.brand { height: 64px; padding: 20px 18px; color: #fff; font-weight: 600; letter-spacing: 0; }
.header { display: flex; align-items: center; background: #fff; padding: 0 24px; border-bottom: 1px solid #edf0f5; }
.page { width: 100%; padding: 24px; }
.stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; width: 100%; }
@media (max-width: 720px) { .page { padding: 16px; } .stats { grid-template-columns: 1fr; } }