chore(init): 初始化解忧商行项目并完成 Phase 0 基座
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
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,
|
||||
DollarCircleOutlined,
|
||||
EyeOutlined,
|
||||
FileSearchOutlined,
|
||||
LogoutOutlined,
|
||||
OrderedListOutlined,
|
||||
SettingOutlined,
|
||||
ShopOutlined,
|
||||
SoundOutlined,
|
||||
SkinOutlined,
|
||||
UserOutlined,
|
||||
WalletOutlined,
|
||||
GiftOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { getMe, logout, type CurrentMember, getStoredToken } from './api'
|
||||
import { MemberContext } from './MemberContext'
|
||||
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 Wallet from './pages/Wallet'
|
||||
import Announcements from './pages/Announcements'
|
||||
import SiteSettings from './pages/SiteSettings'
|
||||
import Invitations from './pages/Invitations'
|
||||
import Users from './pages/Users'
|
||||
import Login from './pages/Login'
|
||||
import Storefront from './pages/Storefront'
|
||||
import './style.css'
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number]
|
||||
|
||||
function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: () => void }) {
|
||||
const location = useLocation()
|
||||
const menuItems: MenuItem[] = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/">仪表盘</Link> },
|
||||
...(member.is_superadmin
|
||||
? [{ key: '/sites', icon: <ShopOutlined />, label: <Link to="/sites">分销站点管理</Link> }]
|
||||
: []),
|
||||
{ key: '/users', icon: <TeamOutlined />, label: <Link to="/users">客户与用户</Link> },
|
||||
{ key: '/orders', icon: <OrderedListOutlined />, label: <Link to="/orders">全部分销订单</Link> },
|
||||
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号订单</Link> },
|
||||
{ key: '/receive', icon: <AccountBookOutlined />, label: <Link to="/receive">分销收号管理</Link> },
|
||||
{ key: '/accounts', icon: <ShopOutlined />, label: <Link to="/accounts">商品货架与加价</Link> },
|
||||
{ key: '/announcements', icon: <SoundOutlined />, label: <Link to="/announcements">公告中心</Link> },
|
||||
{ key: '/wallet', icon: <WalletOutlined />, label: <Link to="/wallet">资金与提现</Link> },
|
||||
{
|
||||
key: '/settlements',
|
||||
icon: <DollarCircleOutlined />,
|
||||
label: <Link to="/settlements">{member.is_superadmin ? '结算批次管理' : '周期对账单'}</Link>,
|
||||
},
|
||||
{ key: '/settings', icon: <SkinOutlined />, label: <Link to="/settings">品牌装修</Link> },
|
||||
...(member.is_superadmin
|
||||
? [{ key: '/payments', icon: <ApiOutlined />, label: <Link to="/payments">支付通道配置</Link> }]
|
||||
: []),
|
||||
{
|
||||
key: '/rules',
|
||||
icon: <SettingOutlined />,
|
||||
label: <Link to="/rules">{member.is_superadmin ? '比例与税费规则' : '合作政策与权益'}</Link>,
|
||||
},
|
||||
{
|
||||
key: '/invitations',
|
||||
icon: <GiftOutlined />,
|
||||
label: <Link to="/invitations">邀请返利设置</Link>,
|
||||
},
|
||||
]
|
||||
|
||||
const selectedKey = menuItems.some((item) => item && 'key' in item && item.key === location.pathname)
|
||||
? location.pathname
|
||||
: '/'
|
||||
|
||||
const roleLabel = member.is_superadmin
|
||||
? '平台超级管理员'
|
||||
: member.member_type === 'operator'
|
||||
? '分销运营'
|
||||
: member.member_type === 'finance'
|
||||
? '分销财务'
|
||||
: '分销成员'
|
||||
|
||||
return (
|
||||
<MemberContext.Provider value={member}>
|
||||
<Layout className="app">
|
||||
<Layout.Sider breakpoint="lg" collapsedWidth="0" className="app-sider" width={220}>
|
||||
<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 className="app-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 }}>
|
||||
主站 zuhaoc.com · 分销管理系统
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<Space size="middle">
|
||||
{member.site_code && (
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<EyeOutlined />}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
const targetUrl = isLocal
|
||||
? `http://${window.location.hostname}:5189/?site=${member.site_code}`
|
||||
: member.domain
|
||||
? `https://${member.domain}`
|
||||
: `/?site=${member.site_code}`
|
||||
window.open(targetUrl, '_blank')
|
||||
}}
|
||||
style={{ borderColor: '#1677ff', color: '#1677ff' }}
|
||||
>
|
||||
预览分销前台商城 ↗
|
||||
</Button>
|
||||
)}
|
||||
<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 className="app-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="/wallet" element={<Wallet />} />
|
||||
<Route path="/announcements" element={<Announcements />} />
|
||||
<Route path="/settings" element={<SiteSettings />} />
|
||||
<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="/users" element={<Users />} />
|
||||
<Route path="/invitations" element={<Invitations />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</MemberContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const [member, setMember] = useState<CurrentMember | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const isPublicStorefront = location.pathname.startsWith('/site')
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
const checkAuth = async () => {
|
||||
// For public storefront, we do not force login
|
||||
if (isPublicStorefront) {
|
||||
try {
|
||||
const profile = await getMe()
|
||||
if (active) setMember(profile)
|
||||
} catch {
|
||||
// ignore auth error on public storefront
|
||||
} finally {
|
||||
if (active) setLoading(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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, isPublicStorefront])
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
setMember(null)
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
const handleLoginSuccess = (current: CurrentMember) => {
|
||||
setMember(current)
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
|
||||
if (isPublicStorefront) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/site" element={<Navigate to="/site/demo" replace />} />
|
||||
<Route path="/site/:siteCode" element={<Storefront />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user