分销后台前端对接与结算明细接口
- 新增 GET /admin/distribution/settlement-batches/:id/items 明细查询 - distribution-admin 重构为 react-router 多页面:仪表盘/网站/规则(启停)/订单(筛选分页)/结算(创建/明细/结算)/支付通道(绑定解绑)/账号展示(上下架) - axios 统一 401 提示,引导回主站后台登录;金额统一整数分格式化 - npm run build(tsc 严格模式) 通过
This commit is contained in:
@@ -178,3 +178,11 @@ type CreateFinancialSnapshotRequest struct {
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
Parties []SnapshotPartyRequest `json:"parties"`
|
||||
}
|
||||
|
||||
type SettlementItemDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
OrderID uint64 `json:"order_id"`
|
||||
PartyRole string `json:"party_role"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
@@ -322,6 +322,20 @@ func (h *Handler) CreateSettlementBatch(c *gin.Context) {
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) ListSettlementItems(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
items, err := h.service.ListSettlementItems(c.Request.Context(), adminID, id)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) MarkSettlementBatch(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -582,3 +582,33 @@ func (r *Repository) CreateFinancialSnapshot(ctx context.Context, adminID, order
|
||||
}
|
||||
return r.FindOrder(ctx, adminID, orderID)
|
||||
}
|
||||
|
||||
func (r *Repository) ListSettlementItems(ctx context.Context, adminID, batchID uint64) ([]SettlementItemDTO, error) {
|
||||
var batch model.DistributionSettlementBatch
|
||||
if err := r.db.WithContext(ctx).First(&batch, batchID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrSettlementBatchNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, batch.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []struct {
|
||||
model.DistributionSettlementItem
|
||||
PartyRole string
|
||||
}
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("distribution_settlement_items i").
|
||||
Select("i.id, i.order_id, i.amount_cent, i.status, p.party_role").
|
||||
Joins("JOIN distribution_order_parties p ON p.id = i.party_id").
|
||||
Where("i.batch_id = ?", batchID).
|
||||
Order("i.id ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]SettlementItemDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, SettlementItemDTO{ID: row.ID, OrderID: row.OrderID, PartyRole: row.PartyRole, AmountCent: row.AmountCent, Status: row.Status})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -152,6 +152,12 @@ func (s *Service) CreateSettlementBatch(ctx context.Context, adminID uint64, req
|
||||
}
|
||||
return s.repo.CreateSettlementBatch(ctx, adminID, req)
|
||||
}
|
||||
func (s *Service) ListSettlementItems(ctx context.Context, adminID, batchID uint64) ([]SettlementItemDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.ListSettlementItems(ctx, adminID, batchID)
|
||||
}
|
||||
func (s *Service) MarkSettlementBatch(ctx context.Context, adminID, id uint64) (*SettlementBatchDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -612,6 +612,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.DELETE("/distribution/payment-channels/:id", requirePerm("distribution:payment_manage"), distributionHandler.UnbindPaymentChannel)
|
||||
adminRoutes.GET("/distribution/settlement-batches", requirePerm("distribution:settlement_view"), distributionHandler.ListSettlementBatches)
|
||||
adminRoutes.POST("/distribution/settlement-batches", requirePerm("distribution:settlement_manage"), distributionHandler.CreateSettlementBatch)
|
||||
adminRoutes.GET("/distribution/settlement-batches/:id/items", requirePerm("distribution:settlement_view"), distributionHandler.ListSettlementItems)
|
||||
adminRoutes.POST("/distribution/settlement-batches/:id/settle", requirePerm("distribution:settlement_manage"), distributionHandler.MarkSettlementBatch)
|
||||
adminRoutes.GET("/files/object", fileHandler.Object)
|
||||
adminRoutes.GET("/users", requirePerm("user:view"), adminUserHandler.List)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BrowserRouter, Link, Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { App, ConfigProvider, Layout, Menu, Typography } from 'antd'
|
||||
import { AccountBookOutlined, ApiOutlined, DashboardOutlined, FileSearchOutlined, SettingOutlined, ShopOutlined } from '@ant-design/icons'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import Sites from './pages/Sites'
|
||||
import Rules from './pages/Rules'
|
||||
import Payments from './pages/Payments'
|
||||
import Accounts from './pages/Accounts'
|
||||
import Settlements from './pages/Settlements'
|
||||
import Orders from './pages/Orders'
|
||||
import './style.css'
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/">仪表盘</Link> },
|
||||
{ key: '/sites', icon: <ShopOutlined />, label: <Link to="/sites">分销网站</Link> },
|
||||
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号</Link> },
|
||||
{ key: '/receive', icon: <AccountBookOutlined />, label: <Link to="/receive">分销收号</Link> },
|
||||
{ key: '/settlements', icon: <AccountBookOutlined />, label: <Link to="/settlements">结算管理</Link> },
|
||||
{ key: '/payments', icon: <ApiOutlined />, label: <Link to="/payments">支付通道</Link> },
|
||||
{ key: '/accounts', icon: <SettingOutlined />, label: <Link to="/accounts">账号展示</Link> },
|
||||
{ key: '/rules', icon: <SettingOutlined />, label: <Link to="/rules">比例与税费</Link> },
|
||||
]
|
||||
|
||||
function pathToSelectedKey(pathname: string): string {
|
||||
return MENU_ITEMS.some((item) => item.key === pathname) ? pathname : '/'
|
||||
}
|
||||
|
||||
export default function AppShell() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ConfigProvider theme={{ token: { colorPrimary: '#1677ff', borderRadius: 6 } }}>
|
||||
<App>
|
||||
<Layout className="app">
|
||||
<Layout.Sider breakpoint="lg" collapsedWidth="0">
|
||||
<div className="brand">HFB 分销后台</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
items={MENU_ITEMS}
|
||||
defaultSelectedKeys={[pathToSelectedKey(window.location.pathname)]}
|
||||
/>
|
||||
</Layout.Sider>
|
||||
<Layout>
|
||||
<Layout.Header className="header">
|
||||
<Typography.Text>主站 hao858.com · 分销子域名 distribution.hao858.com</Typography.Text>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/sites" element={<Sites />} />
|
||||
<Route path="/rules" element={<Rules />} />
|
||||
<Route path="/payments" element={<Payments />} />
|
||||
<Route path="/accounts" element={<Accounts />} />
|
||||
<Route path="/settlements" element={<Settlements />} />
|
||||
<Route path="/sold" element={<Orders kind="sold" />} />
|
||||
<Route path="/receive" element={<Orders kind="receive" />} />
|
||||
<Route path="/orders" element={<Orders kind="all" />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import axios from 'axios'
|
||||
import { Modal } from 'antd'
|
||||
|
||||
const api = axios.create({ baseURL: '/api', withCredentials: true })
|
||||
|
||||
interface ApiResponse<T> { data: T; code?: number; message?: string }
|
||||
|
||||
let authNoticeShown = false
|
||||
|
||||
api.interceptors.response.use(
|
||||
(resp) => resp,
|
||||
(error) => {
|
||||
// 后台鉴权依赖主站 HttpOnly cookie;401 说明未登录或会话过期。
|
||||
if (error?.response?.status === 401 && !authNoticeShown) {
|
||||
authNoticeShown = true
|
||||
Modal.warning({
|
||||
title: '登录状态失效',
|
||||
content: '请先在主站后台登录管理员账号,再返回本页面。',
|
||||
onOk: () => { authNoticeShown = false },
|
||||
})
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export function extract<T>(resp: { data: ApiResponse<T> }): T {
|
||||
return resp.data.data
|
||||
}
|
||||
|
||||
export function formatMoney(cent: number | undefined | null): string {
|
||||
return `¥${((cent ?? 0) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
export interface Site {
|
||||
id: number
|
||||
site_code: string
|
||||
site_name: string
|
||||
domain: string
|
||||
status: string
|
||||
display_account_enabled: boolean
|
||||
tax_enabled: boolean
|
||||
tax_rate_bps: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
id: number
|
||||
site_id?: number
|
||||
mode: string
|
||||
version: number
|
||||
status: string
|
||||
platform_receive_ratio: number
|
||||
platform_sell_ratio: number
|
||||
distributor_receive_ratio: number
|
||||
distributor_sell_ratio: number
|
||||
distributor_a_ratio: number
|
||||
distributor_b_ratio: number
|
||||
service_fee_value: number
|
||||
service_fee_unit: string
|
||||
payment_fee_bps: number
|
||||
tax_enabled: boolean
|
||||
tax_rate_bps: number
|
||||
effective_from: string
|
||||
effective_to?: string
|
||||
}
|
||||
|
||||
export interface DistributionOrder {
|
||||
order_id: number
|
||||
order_no: string
|
||||
site_id: number
|
||||
status: string
|
||||
refund_status: string
|
||||
settlement_status: string
|
||||
distribution_mode: string
|
||||
net_paid_amount_cent: number
|
||||
payment_fee_cent: number
|
||||
service_fee_cent: number
|
||||
markup_amount_cent: number
|
||||
tax_amount_cent: number
|
||||
split_base_amount_cent: number
|
||||
rounding_adjustment_cent: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface OrderPage {
|
||||
items: DistributionOrder[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AccountDisplay {
|
||||
id: number
|
||||
site_id: number
|
||||
account_id: number
|
||||
listing_id: number
|
||||
display_status: string
|
||||
}
|
||||
|
||||
export interface PaymentChannel {
|
||||
id: number
|
||||
site_id: number
|
||||
config_id: number
|
||||
priority: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface SettlementBatch {
|
||||
id: number
|
||||
site_id: number
|
||||
batch_no: string
|
||||
period_start: string
|
||||
period_end: string
|
||||
total_amount_cent: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface SettlementItem {
|
||||
id: number
|
||||
order_id: number
|
||||
party_role: string
|
||||
amount_cent: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export async function listSites(): Promise<Site[]> {
|
||||
const resp = await api.get<ApiResponse<{ items: Site[] }>>('/admin/distribution/sites')
|
||||
return extract(resp).items ?? []
|
||||
}
|
||||
|
||||
export async function listRules(siteID?: number, mode?: string): Promise<Rule[]> {
|
||||
const resp = await api.get<ApiResponse<{ items: Rule[] }>>('/admin/distribution/rules', {
|
||||
params: { site_id: siteID || undefined, mode: mode || undefined },
|
||||
})
|
||||
return extract(resp).items ?? []
|
||||
}
|
||||
|
||||
export interface CreateRulePayload {
|
||||
site_id?: number
|
||||
mode: string
|
||||
distributor_receive_ratio?: number
|
||||
platform_sell_ratio?: number
|
||||
distributor_sell_ratio?: number
|
||||
platform_receive_ratio?: number
|
||||
distributor_a_ratio?: number
|
||||
distributor_b_ratio?: number
|
||||
service_fee_value: number
|
||||
service_fee_unit: string
|
||||
payment_fee_bps: number
|
||||
tax_enabled: boolean
|
||||
tax_rate_bps: number
|
||||
effective_from?: string
|
||||
}
|
||||
|
||||
export async function createRule(payload: CreateRulePayload): Promise<Rule> {
|
||||
const resp = await api.post<ApiResponse<Rule>>('/admin/distribution/rules', payload)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function updateRuleStatus(id: number, status: string): Promise<Rule> {
|
||||
const resp = await api.put<ApiResponse<Rule>>(`/admin/distribution/rules/${id}/status`, { status })
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function listOrders(params: {
|
||||
kind?: string
|
||||
status?: string
|
||||
siteID?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}): Promise<OrderPage> {
|
||||
const resp = await api.get<ApiResponse<OrderPage>>('/admin/distribution/orders', {
|
||||
params: {
|
||||
kind: params.kind || undefined,
|
||||
status: params.status || undefined,
|
||||
site_id: params.siteID || undefined,
|
||||
page: params.page || 1,
|
||||
page_size: params.pageSize || 20,
|
||||
},
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function listAccountDisplays(siteID: number, status?: string): Promise<AccountDisplay[]> {
|
||||
const resp = await api.get<ApiResponse<{ items: AccountDisplay[] }>>('/admin/distribution/accounts', {
|
||||
params: { site_id: siteID, status: status || undefined },
|
||||
})
|
||||
return extract(resp).items ?? []
|
||||
}
|
||||
|
||||
export async function updateAccountDisplay(id: number, displayStatus: string): Promise<AccountDisplay> {
|
||||
const resp = await api.put<ApiResponse<AccountDisplay>>(`/admin/distribution/accounts/${id}/display`, {
|
||||
display_status: displayStatus,
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function listPaymentChannels(siteID: number): Promise<PaymentChannel[]> {
|
||||
const resp = await api.get<ApiResponse<{ items: PaymentChannel[] }>>('/admin/distribution/payment-channels', {
|
||||
params: { site_id: siteID },
|
||||
})
|
||||
return extract(resp).items ?? []
|
||||
}
|
||||
|
||||
export async function bindPaymentChannel(siteID: number, configID: number, priority: number): Promise<PaymentChannel> {
|
||||
const resp = await api.post<ApiResponse<PaymentChannel>>(
|
||||
'/admin/distribution/payment-channels',
|
||||
{ config_id: configID, priority },
|
||||
{ params: { site_id: siteID } },
|
||||
)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function unbindPaymentChannel(id: number): Promise<void> {
|
||||
await api.delete(`/admin/distribution/payment-channels/${id}`)
|
||||
}
|
||||
|
||||
export async function listSettlementBatches(siteID: number): Promise<SettlementBatch[]> {
|
||||
const resp = await api.get<ApiResponse<{ items: SettlementBatch[] }>>('/admin/distribution/settlement-batches', {
|
||||
params: { site_id: siteID },
|
||||
})
|
||||
return extract(resp).items ?? []
|
||||
}
|
||||
|
||||
export async function createSettlementBatch(siteID: number, periodStart: string, periodEnd: string): Promise<SettlementBatch> {
|
||||
const resp = await api.post<ApiResponse<SettlementBatch>>('/admin/distribution/settlement-batches', {
|
||||
site_id: siteID,
|
||||
period_start: periodStart,
|
||||
period_end: periodEnd,
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function settleBatch(id: number): Promise<SettlementBatch> {
|
||||
const resp = await api.post<ApiResponse<SettlementBatch>>(`/admin/distribution/settlement-batches/${id}/settle`)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function listSettlementItems(batchID: number): Promise<SettlementItem[]> {
|
||||
const resp = await api.get<ApiResponse<{ items: SettlementItem[] }>>(`/admin/distribution/settlement-batches/${batchID}/items`)
|
||||
return extract(resp).items ?? []
|
||||
}
|
||||
@@ -1,40 +1,4 @@
|
||||
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'
|
||||
import AppShell from './AppShell'
|
||||
|
||||
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>)
|
||||
createRoot(document.getElementById('root')!).render(<AppShell />)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, message } from 'antd'
|
||||
import { listAccountDisplays, listSites, updateAccountDisplay, type AccountDisplay, type Site } from '../api'
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
enabled: '展示中',
|
||||
disabled: '已下架',
|
||||
sold: '已售出',
|
||||
offline: '已离线',
|
||||
}
|
||||
|
||||
export default function Accounts() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [status, setStatus] = useState<string>('')
|
||||
const [items, setItems] = useState<AccountDisplay[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function load(site?: number, statusFilter?: string) {
|
||||
if (!site) return
|
||||
setLoading(true)
|
||||
try {
|
||||
setItems(await listAccountDisplays(site, statusFilter))
|
||||
} catch {
|
||||
message.error('账号展示加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void load(rows[0].id)
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
|
||||
async function toggle(item: AccountDisplay) {
|
||||
const next = item.display_status === 'enabled' ? 'disabled' : 'enabled'
|
||||
try {
|
||||
await updateAccountDisplay(item.id, next)
|
||||
message.success(next === 'enabled' ? '已恢复展示' : '已下架')
|
||||
if (siteID) void load(siteID, status)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage ?? '状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>账号展示</Typography.Title>
|
||||
<Space>
|
||||
<Select
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => {
|
||||
setSiteID(value)
|
||||
void load(value, status)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="展示状态"
|
||||
style={{ width: 140 }}
|
||||
options={Object.entries(STATUS_LABELS).map(([value, label]) => ({ label, value }))}
|
||||
onChange={(value) => {
|
||||
setStatus(value ?? '')
|
||||
void load(siteID, value ?? '')
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={items}
|
||||
pagination={{ pageSize: 20 }}
|
||||
columns={[
|
||||
{ title: '账号 ID', dataIndex: 'account_id' },
|
||||
{ title: '商品 ID', dataIndex: 'listing_id' },
|
||||
{
|
||||
title: '展示状态',
|
||||
dataIndex: 'display_status',
|
||||
render: (v: string) => <Tag color={v === 'enabled' ? 'green' : 'default'}>{STATUS_LABELS[v] ?? v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: unknown, row: AccountDisplay) => (
|
||||
<a onClick={() => toggle(row)}>{row.display_status === 'enabled' ? '下架' : '恢复展示'}</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Statistic, Space, message } from 'antd'
|
||||
import { listOrders, listSites, formatMoney, type Site } from '../api'
|
||||
|
||||
export default function Dashboard() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>['items']>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([listSites(), listOrders({ pageSize: 100 })])
|
||||
.then(([siteItems, page]) => {
|
||||
setSites(siteItems)
|
||||
setOrders(page.items)
|
||||
setTotal(page.total)
|
||||
})
|
||||
.catch(() => message.error('数据加载失败'))
|
||||
}, [])
|
||||
|
||||
const netPaid = 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={total} /></Card>
|
||||
<Card><Statistic title="最近净支付" value={(netPaid / 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' },
|
||||
{ title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '结算', dataIndex: 'settlement_status' },
|
||||
{ title: '创建时间', dataIndex: 'created_at' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, message } from 'antd'
|
||||
import { formatMoney, listOrders, listSites, type DistributionOrder, type Site } from '../api'
|
||||
|
||||
const ORDER_STATUSES = [
|
||||
'pending_payment',
|
||||
'pending_handoff',
|
||||
'renting',
|
||||
'overdue',
|
||||
'pending_checkout_confirm',
|
||||
'pending_checkout_accept',
|
||||
'checkout_disputing',
|
||||
'disputing',
|
||||
'abnormal',
|
||||
'completed',
|
||||
'refunded',
|
||||
'cancelled',
|
||||
]
|
||||
|
||||
export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [status, setStatus] = useState<string>('')
|
||||
const [items, setItems] = useState<DistributionOrder[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const pageSize = 20
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const load = useCallback(async (targetPage: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await listOrders({ kind, status, siteID, page: targetPage, pageSize })
|
||||
setItems(result.items)
|
||||
setTotal(result.total)
|
||||
setPage(result.page)
|
||||
} catch {
|
||||
message.error('订单加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [kind, status, siteID])
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) setSiteID(rows[0].id)
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void load(1)
|
||||
}, [load])
|
||||
|
||||
const title = kind === 'sold' ? '售出账号订单' : kind === 'receive' ? '分销收号订单' : '全部分销订单'
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>{title}</Typography.Title>
|
||||
<Space>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => setSiteID(value ?? undefined)}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="订单状态"
|
||||
style={{ width: 200 }}
|
||||
options={ORDER_STATUSES.map((value) => ({ label: value, value }))}
|
||||
onChange={(value) => setStatus(value ?? '')}
|
||||
/>
|
||||
</Space>
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="order_id"
|
||||
loading={loading}
|
||||
dataSource={items}
|
||||
pagination={{ current: page, pageSize, total, onChange: (p) => void load(p) }}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'order_no' },
|
||||
{ title: '订单状态', dataIndex: 'status', render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '退款状态', dataIndex: 'refund_status' },
|
||||
{ title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '服务费', dataIndex: 'service_fee_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '尾差', dataIndex: 'rounding_adjustment_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '结算', dataIndex: 'settlement_status' },
|
||||
{ title: '创建时间', dataIndex: 'created_at' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, InputNumber, Button, message } from 'antd'
|
||||
import { bindPaymentChannel, listPaymentChannels, listSites, unbindPaymentChannel, type PaymentChannel, type Site } from '../api'
|
||||
|
||||
export default function Payments() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [channels, setChannels] = useState<PaymentChannel[]>([])
|
||||
const [configID, setConfigID] = useState<number | null>(null)
|
||||
const [priority, setPriority] = useState<number>(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function load(id: number) {
|
||||
setLoading(true)
|
||||
try {
|
||||
setChannels(await listPaymentChannels(id))
|
||||
} catch {
|
||||
message.error('支付通道加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((items) => {
|
||||
setSites(items)
|
||||
if (items.length > 0) {
|
||||
setSiteID(items[0].id)
|
||||
void load(items[0].id)
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
|
||||
async function handleBind() {
|
||||
if (!siteID || !configID) {
|
||||
message.warning('请选择站点并填写商户配置 ID')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await bindPaymentChannel(siteID, configID, priority)
|
||||
message.success('支付通道已绑定')
|
||||
setConfigID(null)
|
||||
void load(siteID)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage ?? '绑定失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnbind(id: number) {
|
||||
try {
|
||||
await unbindPaymentChannel(id)
|
||||
message.success('已解绑')
|
||||
if (siteID) void load(siteID)
|
||||
} catch {
|
||||
message.error('解绑失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>支付通道</Typography.Title>
|
||||
<Space>
|
||||
<Select
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => {
|
||||
setSiteID(value)
|
||||
void load(value)
|
||||
}}
|
||||
/>
|
||||
<InputNumber min={1} placeholder="商户配置 ID" value={configID ?? undefined} onChange={(v) => setConfigID(v ?? null)} />
|
||||
<InputNumber min={0} placeholder="优先级" value={priority} onChange={(v) => setPriority(v ?? 0)} />
|
||||
<Button type="primary" onClick={handleBind}>绑定</Button>
|
||||
</Space>
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={channels}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '商户配置 ID', dataIndex: 'config_id' },
|
||||
{ title: '优先级', dataIndex: 'priority' },
|
||||
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag color={v === 'active' ? 'green' : 'default'}>{v}</Tag> },
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: unknown, row: PaymentChannel) => <a onClick={() => handleUnbind(row.id)}>解绑</a>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, message } from 'antd'
|
||||
import { listRules, listSites, updateRuleStatus, type Rule, type Site } from '../api'
|
||||
|
||||
const MODE_LABELS: Record<string, string> = {
|
||||
two_party_distributor_receive: '两方-分销收号',
|
||||
two_party_platform_receive: '两方-分销卖出',
|
||||
three_party: '三方',
|
||||
}
|
||||
|
||||
export default function Rules() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number | undefined>(undefined)
|
||||
const [rules, setRules] = useState<Rule[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function load(site?: number) {
|
||||
setLoading(true)
|
||||
try {
|
||||
setRules(await listRules(site))
|
||||
} catch {
|
||||
message.error('规则加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listSites().then(setSites).catch(() => message.error('站点加载失败'))
|
||||
void load(undefined)
|
||||
}, [])
|
||||
|
||||
async function toggleStatus(rule: Rule) {
|
||||
const next = rule.status === 'active' ? 'disabled' : 'active'
|
||||
try {
|
||||
await updateRuleStatus(rule.id, next)
|
||||
message.success(`规则已${next === 'active' ? '启用' : '停用'}`)
|
||||
void load(siteID)
|
||||
} catch {
|
||||
message.error('状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>比例与税费规则</Typography.Title>
|
||||
<Space>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => {
|
||||
setSiteID(value)
|
||||
void load(value)
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={rules}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '模式', dataIndex: 'mode', render: (v: string) => MODE_LABELS[v] ?? v },
|
||||
{ title: '版本', dataIndex: 'version' },
|
||||
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag color={v === 'active' ? 'green' : 'default'}>{v}</Tag> },
|
||||
{
|
||||
title: '比例',
|
||||
render: (_: unknown, r: Rule) => {
|
||||
const parts: string[] = []
|
||||
if (r.distributor_receive_ratio > 0) parts.push(`分销收 ${r.distributor_receive_ratio}`)
|
||||
if (r.platform_sell_ratio > 0) parts.push(`主站卖 ${r.platform_sell_ratio}`)
|
||||
if (r.distributor_sell_ratio > 0) parts.push(`分销卖 ${r.distributor_sell_ratio}`)
|
||||
if (r.platform_receive_ratio > 0) parts.push(`主站收 ${r.platform_receive_ratio}`)
|
||||
if (r.distributor_a_ratio > 0) parts.push(`分销A ${r.distributor_a_ratio}`)
|
||||
if (r.distributor_b_ratio > 0) parts.push(`分销B ${r.distributor_b_ratio}`)
|
||||
return parts.join(' / ') || '-'
|
||||
},
|
||||
},
|
||||
{ title: '服务费', render: (_: unknown, r: Rule) => `${r.service_fee_value} (${r.service_fee_unit})` },
|
||||
{ title: '手续费', dataIndex: 'payment_fee_bps', render: (v: number) => `${v / 100}%` },
|
||||
{ title: '税费', render: (_: unknown, r: Rule) => (r.tax_enabled ? `${r.tax_rate_bps / 100}%` : '关闭') },
|
||||
{ title: '生效时间', dataIndex: 'effective_from' },
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: unknown, r: Rule) => (
|
||||
<a onClick={() => toggleStatus(r)}>{r.status === 'active' ? '停用' : '启用'}</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Select, Tag, DatePicker, Button, message, Modal } from 'antd'
|
||||
import {
|
||||
createSettlementBatch,
|
||||
formatMoney,
|
||||
listSettlementBatches,
|
||||
listSettlementItems,
|
||||
listSites,
|
||||
settleBatch,
|
||||
type SettlementBatch,
|
||||
type SettlementItem,
|
||||
type Site,
|
||||
} from '../api'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
export default function Settlements() {
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number>()
|
||||
const [batches, setBatches] = useState<SettlementBatch[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [range, setRange] = useState<[Dayjs | null, Dayjs | null] | null>(null)
|
||||
const [itemsFor, setItemsFor] = useState<SettlementBatch | null>(null)
|
||||
const [items, setItems] = useState<SettlementItem[]>([])
|
||||
|
||||
async function load(id: number) {
|
||||
setLoading(true)
|
||||
try {
|
||||
setBatches(await listSettlementBatches(id))
|
||||
} catch {
|
||||
message.error('结算批次加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void load(rows[0].id)
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
|
||||
async function handleCreate() {
|
||||
if (!siteID || !range?.[0] || !range?.[1]) {
|
||||
message.warning('请选择站点和结算周期')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const batch = await createSettlementBatch(siteID, range[0].toISOString(), range[1].toISOString())
|
||||
message.success(`批次 ${batch.batch_no} 已创建,合计 ${formatMoney(batch.total_amount_cent)}`)
|
||||
void load(siteID)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage ?? '批次创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSettle(batch: SettlementBatch) {
|
||||
try {
|
||||
await settleBatch(batch.id)
|
||||
message.success(`批次 ${batch.batch_no} 已结算`)
|
||||
if (siteID) void load(siteID)
|
||||
} catch (error: unknown) {
|
||||
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
message.error(respMessage ?? '结算失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function showItems(batch: SettlementBatch) {
|
||||
try {
|
||||
setItemsFor(batch)
|
||||
setItems(await listSettlementItems(batch.id))
|
||||
} catch {
|
||||
message.error('结算明细加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>结算管理</Typography.Title>
|
||||
<Space>
|
||||
<Select
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => {
|
||||
setSiteID(value)
|
||||
void load(value)
|
||||
}}
|
||||
/>
|
||||
<DatePicker.RangePicker showTime onChange={(value) => setRange(value)} />
|
||||
<Button type="primary" onClick={handleCreate}>创建批次</Button>
|
||||
</Space>
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={batches}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '批次号', dataIndex: 'batch_no' },
|
||||
{ title: '周期开始', dataIndex: 'period_start' },
|
||||
{ title: '周期结束', dataIndex: 'period_end' },
|
||||
{ title: '合计金额', dataIndex: 'total_amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (v: string) => <Tag color={v === 'settled' ? 'green' : 'gold'}>{v === 'settled' ? '已结算' : '草稿'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: unknown, row: SettlementBatch) => (
|
||||
<Space>
|
||||
<a onClick={() => showItems(row)}>明细</a>
|
||||
{row.status !== 'settled' && <a onClick={() => handleSettle(row)}>结算</a>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Modal
|
||||
open={itemsFor !== null}
|
||||
title={`结算明细 · ${itemsFor?.batch_no ?? ''}`}
|
||||
footer={null}
|
||||
onCancel={() => setItemsFor(null)}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '订单 ID', dataIndex: 'order_id' },
|
||||
{ title: '参与方', dataIndex: 'party_role' },
|
||||
{ title: '金额', dataIndex: 'amount_cent', render: (v: number) => formatMoney(v) },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Tag, message } from 'antd'
|
||||
import { listSites, type Site } from '../api'
|
||||
|
||||
export default function Sites() {
|
||||
const [items, setItems] = useState<Site[]>([])
|
||||
useEffect(() => {
|
||||
listSites()
|
||||
.then(setItems)
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>分销网站</Typography.Title>
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '名称', dataIndex: 'site_name' },
|
||||
{ title: '编码', dataIndex: 'site_code' },
|
||||
{ title: '域名', dataIndex: 'domain' },
|
||||
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag color={v === 'active' ? 'green' : 'default'}>{v}</Tag> },
|
||||
{
|
||||
title: '账号展示',
|
||||
dataIndex: 'display_account_enabled',
|
||||
render: (v: boolean) => (v ? <Tag color="green">开启</Tag> : <Tag>关闭</Tag>),
|
||||
},
|
||||
{
|
||||
title: '税费',
|
||||
render: (_: unknown, row: Site) => (row.tax_enabled ? `${row.tax_rate_bps / 100}%` : '关闭'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user