- 独立账号鉴权:分销后台支持独立登录页面、Bearer+Cookie 双模鉴权与租户资料接口 - 多租户隔离:分销商成员强制锁定自身站点数据范围,防止横向越权;平台超管享有全局视界 - 一键开通分销商:平台端支持一键录入站点、管理员账号、分配角色并原子化初始化佣金规则 - 主后台分销分组:在主站后台增加「分销管理」导航分组,提供站点管理、规则配置、订单监控与结算管理 GUI - 门禁与联调:补充完整单测,全量代码门禁 check.sh 100% 通过,dev.sh 支持前后端与分销后台一键同启
211 lines
6.9 KiB
TypeScript
211 lines
6.9 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { BrowserRouter, Link, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
|
import { App, ConfigProvider, Layout, Menu, Typography, Button, Tag, Space, Spin, type MenuProps } from 'antd'
|
|
import {
|
|
AccountBookOutlined,
|
|
ApiOutlined,
|
|
DashboardOutlined,
|
|
FileSearchOutlined,
|
|
LogoutOutlined,
|
|
SettingOutlined,
|
|
ShopOutlined,
|
|
UserOutlined,
|
|
} from '@ant-design/icons'
|
|
import { getMe, logout, type CurrentMember, getStoredToken } from './api'
|
|
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 Login from './pages/Login'
|
|
import './style.css'
|
|
|
|
type MenuItem = Required<MenuProps>['items'][number] & { superadminOnly?: boolean }
|
|
|
|
const ALL_MENU_ITEMS: MenuItem[] = [
|
|
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/">仪表盘</Link> },
|
|
{ key: '/sites', icon: <ShopOutlined />, label: <Link to="/sites">分销网站</Link>, superadminOnly: true },
|
|
{ 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 MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: () => void }) {
|
|
const location = useLocation()
|
|
const menuItems = ALL_MENU_ITEMS.filter((item) => !item.superadminOnly || member.is_superadmin)
|
|
|
|
const selectedKey = menuItems.some((item) => item.key === location.pathname)
|
|
? location.pathname
|
|
: '/'
|
|
|
|
const roleLabel = member.is_superadmin
|
|
? '平台超级管理员'
|
|
: member.member_type === 'operator'
|
|
? '分销运营'
|
|
: member.member_type === 'finance'
|
|
? '分销财务'
|
|
: '分销成员'
|
|
|
|
return (
|
|
<Layout className="app">
|
|
<Layout.Sider breakpoint="lg" collapsedWidth="0">
|
|
<div className="brand">
|
|
<ShopOutlined style={{ fontSize: 20, color: '#1677ff' }} />
|
|
<span>HFB 分销后台</span>
|
|
</div>
|
|
<Menu
|
|
theme="dark"
|
|
mode="inline"
|
|
items={menuItems}
|
|
selectedKeys={[selectedKey]}
|
|
/>
|
|
</Layout.Sider>
|
|
<Layout>
|
|
<Layout.Header className="header">
|
|
<div className="header-left">
|
|
{member.is_superadmin ? (
|
|
<Tag color="gold" style={{ fontSize: 13, padding: '2px 8px' }}>
|
|
平台全局管理模式
|
|
</Tag>
|
|
) : (
|
|
<Tag color="blue" style={{ fontSize: 13, padding: '2px 8px' }}>
|
|
{member.site_name || '当前分销站点'} · {member.domain || member.site_code}
|
|
</Tag>
|
|
)}
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
主站 hao858.com · 分销管理系统
|
|
</Typography.Text>
|
|
</div>
|
|
<div className="header-right">
|
|
<Space size="middle">
|
|
<div className="user-profile">
|
|
<UserOutlined style={{ color: '#1677ff' }} />
|
|
<Typography.Text strong style={{ fontSize: 13 }}>
|
|
{member.nickname || member.username}
|
|
</Typography.Text>
|
|
<Tag color={member.is_superadmin ? 'purple' : 'cyan'}>{roleLabel}</Tag>
|
|
</div>
|
|
<Button
|
|
type="text"
|
|
danger
|
|
icon={<LogoutOutlined />}
|
|
onClick={onLogout}
|
|
size="small"
|
|
>
|
|
退出登录
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
</Layout.Header>
|
|
<Layout.Content>
|
|
<Routes>
|
|
<Route path="/" element={<Dashboard />} />
|
|
{member.is_superadmin && <Route path="/sites" element={<Sites isSuperadmin={true} />} />}
|
|
<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>
|
|
)
|
|
}
|
|
|
|
function AppContent() {
|
|
const [member, setMember] = useState<CurrentMember | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
|
|
useEffect(() => {
|
|
let active = true
|
|
const checkAuth = async () => {
|
|
// If no token exists and no session, skip getMe
|
|
if (!getStoredToken() && location.pathname === '/login') {
|
|
if (active) setLoading(false)
|
|
return
|
|
}
|
|
try {
|
|
const profile = await getMe()
|
|
if (active) {
|
|
setMember(profile)
|
|
if (location.pathname === '/login') {
|
|
navigate('/', { replace: true })
|
|
}
|
|
}
|
|
} catch {
|
|
if (active) {
|
|
setMember(null)
|
|
if (location.pathname !== '/login') {
|
|
navigate('/login', { replace: true })
|
|
}
|
|
}
|
|
} finally {
|
|
if (active) setLoading(false)
|
|
}
|
|
}
|
|
checkAuth()
|
|
return () => {
|
|
active = false
|
|
}
|
|
}, [location.pathname, navigate])
|
|
|
|
const handleLogout = async () => {
|
|
await logout()
|
|
setMember(null)
|
|
navigate('/login', { replace: true })
|
|
}
|
|
|
|
const handleLoginSuccess = (current: CurrentMember) => {
|
|
setMember(current)
|
|
navigate('/', { replace: true })
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Spin size="large" tip="正在加载分销系统..." />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (location.pathname === '/login') {
|
|
return <Login onSuccess={handleLoginSuccess} />
|
|
}
|
|
|
|
if (!member) {
|
|
return <Navigate to="/login" replace />
|
|
}
|
|
|
|
return <MainLayout member={member} onLogout={handleLogout} />
|
|
}
|
|
|
|
export default function AppShell() {
|
|
return (
|
|
<BrowserRouter>
|
|
<ConfigProvider
|
|
theme={{
|
|
token: {
|
|
colorPrimary: '#1677ff',
|
|
borderRadius: 6,
|
|
},
|
|
}}
|
|
>
|
|
<App>
|
|
<AppContent />
|
|
</App>
|
|
</ConfigProvider>
|
|
</BrowserRouter>
|
|
)
|
|
}
|