390 lines
16 KiB
TypeScript
390 lines
16 KiB
TypeScript
import { useState, type ReactNode } from 'react'
|
|
import {
|
|
Avatar,
|
|
Breadcrumb,
|
|
Button,
|
|
Dropdown,
|
|
Layout,
|
|
Menu,
|
|
Space,
|
|
Tag,
|
|
Typography,
|
|
type MenuProps,
|
|
} from 'antd'
|
|
import {
|
|
AccountBookOutlined,
|
|
BellOutlined,
|
|
CustomerServiceOutlined,
|
|
DashboardOutlined,
|
|
FileDoneOutlined,
|
|
LogoutOutlined,
|
|
MenuFoldOutlined,
|
|
MenuUnfoldOutlined,
|
|
SettingOutlined,
|
|
ShareAltOutlined,
|
|
ShopOutlined,
|
|
ShoppingCartOutlined,
|
|
TeamOutlined,
|
|
} from '@ant-design/icons'
|
|
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'
|
|
|
|
import { BrandLogo } from '@/components/BrandLogo'
|
|
import { getAdminBasePath, getAdminLoginPath } from '@/shared/api/adminClient'
|
|
import { useAdminSessionStore } from '@/stores/adminSession'
|
|
|
|
const { Sider, Header, Content } = Layout
|
|
|
|
interface NavItem {
|
|
key: string
|
|
label: string
|
|
permission?: string
|
|
}
|
|
|
|
interface NavGroup {
|
|
key: string
|
|
label: string
|
|
icon: ReactNode
|
|
children: NavItem[]
|
|
}
|
|
|
|
const allNavGroups: NavGroup[] = [
|
|
{
|
|
key: 'workspace',
|
|
label: '工作台',
|
|
icon: <DashboardOutlined />,
|
|
children: [{ key: '/', label: '仪表盘', permission: 'dashboard:view' }],
|
|
},
|
|
{
|
|
key: 'trade',
|
|
label: '交易',
|
|
icon: <ShoppingCartOutlined />,
|
|
children: [
|
|
{ key: '/orders', label: '订单管理', permission: 'order:list' },
|
|
{ key: '/orders/refund-review', label: '退款审核', permission: 'order:close' },
|
|
{ key: '/pickup', label: '线下提号', permission: 'order:pickup' },
|
|
{ key: '/disputes', label: '申诉仲裁', permission: 'dispute:view' },
|
|
],
|
|
},
|
|
{
|
|
key: 'listings',
|
|
label: '商品',
|
|
icon: <ShopOutlined />,
|
|
children: [
|
|
{ key: '/listings', label: '商品管理', permission: 'listing:view' },
|
|
{ key: '/listings/review', label: '商品审核', permission: 'listing:approve' },
|
|
],
|
|
},
|
|
{
|
|
key: 'users',
|
|
label: '用户',
|
|
icon: <TeamOutlined />,
|
|
children: [{ key: '/users', label: '用户管理', permission: 'user:view' }],
|
|
},
|
|
{
|
|
key: 'finance',
|
|
label: '财务',
|
|
icon: <AccountBookOutlined />,
|
|
children: [
|
|
{ key: '/finance', label: '财务看板', permission: 'wallet:view' },
|
|
{ key: '/finance/details', label: '资金明细', permission: 'wallet:view' },
|
|
{ key: '/finance/disbursements', label: '资金出款', permission: 'wallet:view' },
|
|
{ key: '/payments', label: '支付流水', permission: 'wallet:view' },
|
|
{ key: '/wallet-ledger', label: '钱包流水', permission: 'wallet:view' },
|
|
{
|
|
key: '/finance/operating-expenses',
|
|
label: '运营开支',
|
|
permission: 'finance:operating_expense',
|
|
},
|
|
{ key: '/withdrawals', label: '提现审核', permission: 'withdrawal:list' },
|
|
],
|
|
},
|
|
{
|
|
key: 'distribution',
|
|
label: '分销',
|
|
icon: <ShareAltOutlined />,
|
|
children: [
|
|
{ key: '/distribution/dashboard', label: '分销看板', permission: 'distribution:site_view' },
|
|
{ key: '/distribution/sites', label: '站点管理', permission: 'distribution:site_view' },
|
|
{ key: '/distribution/orders', label: '分销订单', permission: 'distribution:order_view' },
|
|
{
|
|
key: '/distribution/settlements',
|
|
label: '结算批次',
|
|
permission: 'distribution:settlement_view',
|
|
},
|
|
{ key: '/distribution/users', label: '分销用户', permission: 'distribution:site_view' },
|
|
{
|
|
key: '/distribution/announcements',
|
|
label: '站点公告',
|
|
permission: 'distribution:announcement_view',
|
|
},
|
|
{ key: '/distribution/listings', label: '分销商品', permission: 'distribution:account_view' },
|
|
{
|
|
key: '/distribution/payment-channels',
|
|
label: '支付通道',
|
|
permission: 'distribution:payment_view',
|
|
},
|
|
{ key: '/distribution/wallet', label: '站点钱包', permission: 'distribution:settlement_view' },
|
|
{ key: '/distribution/rules', label: '规则版本', permission: 'distribution:rule_view' },
|
|
{ key: '/distribution/qrcodes', label: '二维码池', permission: 'qrcode:view' },
|
|
{ key: '/distribution/sms', label: '站点短信', permission: 'distribution:site_view' },
|
|
{ key: '/distribution/invitation', label: '邀请返利', permission: 'distribution:site_view' },
|
|
],
|
|
},
|
|
{
|
|
key: 'operations',
|
|
label: '运营',
|
|
icon: <BellOutlined />,
|
|
children: [{ key: '/announcements', label: '公告管理', permission: 'announcement:view' }, { key: '/tracking-links', label: '推广追踪', permission: 'ad_tracking:manage' }],
|
|
},
|
|
{
|
|
key: 'support',
|
|
label: '客服',
|
|
icon: <CustomerServiceOutlined />,
|
|
children: [
|
|
{ key: '/chat-support-groups', label: '客服群组', permission: 'chat:manage' },
|
|
{ key: '/chats', label: '客服会话', permission: 'chat:view' },
|
|
{ key: '/manual-entries', label: '手工录单', permission: 'cs_manual_entry:view' },
|
|
{ key: '/chat-quick-replies', label: '快捷回复', permission: 'chat:view' },
|
|
],
|
|
},
|
|
{
|
|
key: 'system',
|
|
label: '系统',
|
|
icon: <SettingOutlined />,
|
|
children: [
|
|
{ key: '/system-configs', label: '系统配置', permission: 'system_config:view' },
|
|
{ key: '/roles', label: '角色权限', permission: 'role:manage' },
|
|
{ key: '/admin-users', label: '管理员账号', permission: 'admin_user:manage' },
|
|
{ key: '/payment-configs', label: '支付配置', permission: 'payment_config:list' },
|
|
{ key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const sectionMeta: Record<string, { title: string; description: string }> = {
|
|
'/orders': { title: '订单管理', description: '查看全量订单、交接记录与结账状态。' },
|
|
'/orders/refund-review': { title: '退款审核', description: '审核退款申请并核对资金影响。' },
|
|
'/pickup': { title: '线下提号', description: '管理员线下提号,站内结算或线下打款确认。' },
|
|
'/disputes': { title: '申诉仲裁', description: '处理订单争议、证据与仲裁结果。' },
|
|
'/listings': { title: '商品管理', description: '查看商品、账号资产与上下架状态。' },
|
|
'/listings/review': { title: '商品审核', description: '审核待上架商品与账号截图。' },
|
|
'/users': { title: '用户管理', description: '查看用户、实名状态与交易记录。' },
|
|
'/finance': { title: '财务看板', description: '查看收入、退款、结算与资金趋势。' },
|
|
'/finance/details': { title: '资金明细', description: '按订单核对实付、退款、号主结算与平台净额。' },
|
|
'/payments': { title: '支付流水', description: '核对渠道支付单、回调状态与失败原因。' },
|
|
'/wallet-ledger': { title: '钱包流水', description: '按用户、订单与业务类型核对余额变动。' },
|
|
'/finance/disbursements': { title: '资金出款', description: '查看代管出款、提现与线下出款明细。' },
|
|
'/finance/operating-expenses': { title: '运营开支', description: '登记与核对日常运营支出。' },
|
|
'/withdrawals': { title: '提现审核', description: '审核提现申请与打款确认。' },
|
|
'/distribution/sites': { title: '分销站点', description: '管理分站、规则、结算与站点钱包。' },
|
|
'/distribution/dashboard': { title: '分销看板', description: '查看站点收号、订单、利润与近 7 天趋势。' },
|
|
'/distribution/orders': { title: '分销订单', description: '核对分销订单快照与冷却期释放状态。' },
|
|
'/distribution/settlements': { title: '结算批次', description: '生成人工批次并核对、释放站内份额。' },
|
|
'/distribution/users': { title: '分销用户', description: '查看站点注册客户、下单客户与消费贡献。' },
|
|
'/distribution/announcements': { title: '站点公告', description: '维护分站自己的公告中心与弹窗公告。' },
|
|
'/distribution/listings': { title: '分销商品', description: '查看收货商品并控制账号展示与加价。' },
|
|
'/distribution/payment-channels': { title: '支付通道', description: '为分站绑定主站支付商户配置。' },
|
|
'/distribution/wallet': { title: '站点钱包', description: '查看分站余额、结算进度与提现记录。' },
|
|
'/distribution/rules': { title: '规则版本', description: '维护全局与站点级分账规则版本。' },
|
|
'/distribution/qrcodes': { title: '二维码池', description: '按站点维护发布群二维码池与发放状态。' },
|
|
'/distribution/sms': { title: '站点短信', description: '为分站配置独立短信签名、模板与密钥。' },
|
|
'/distribution/invitation': { title: '邀请返利', description: '配置分站邀请好友的返利比例。' },
|
|
'/announcements': { title: '公告管理', description: '维护主站与分站公告。' },
|
|
'/roles': { title: '角色权限', description: '维护后台角色并分配资源级操作权限。' },
|
|
'/admin-users': { title: '管理员账号', description: '维护后台账号、启用状态与角色绑定。' },
|
|
'/manual-entries': {
|
|
title: '手工录单',
|
|
description: '录入系统外成单编号,用于对账与客服绩效统计。',
|
|
},
|
|
'/chat-quick-replies': {
|
|
title: '快捷回复',
|
|
description: '维护客服会话常用话术与全局可见范围。',
|
|
},
|
|
'/tracking-links': {
|
|
title: '推广追踪',
|
|
description: '管理推广短链、渠道成本与注册下单转化。',
|
|
},
|
|
'/chat-support-groups': {
|
|
title: '客服群组',
|
|
description: '维护客服群与坐席分配,支撑会话分流。',
|
|
},
|
|
'/chats': {
|
|
title: '客服会话',
|
|
description: '按阶段筛选会话、回复用户并转接客服。',
|
|
},
|
|
'/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
|
|
'/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' },
|
|
'/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' },
|
|
}
|
|
|
|
export function AdminLayout() {
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const admin = useAdminSessionStore(state => state.admin)
|
|
const logout = useAdminSessionStore(state => state.logout)
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
|
|
const basePath = getAdminBasePath()
|
|
const relativePath =
|
|
location.pathname.startsWith(basePath) && location.pathname !== basePath
|
|
? location.pathname.slice(basePath.length)
|
|
: '/'
|
|
|
|
const can = (permission?: string) => {
|
|
if (!permission) return true
|
|
const permissions = admin?.permissions ?? []
|
|
return permissions.includes('*') || permissions.includes(permission)
|
|
}
|
|
|
|
const visibleGroups = allNavGroups
|
|
.map(group => ({
|
|
...group,
|
|
children: group.children.filter(item => can(item.permission)),
|
|
}))
|
|
.filter(group => group.children.length > 0)
|
|
|
|
const menuItems: MenuProps['items'] = visibleGroups.map(group => ({
|
|
key: group.key,
|
|
icon: group.icon,
|
|
label: group.label,
|
|
children: group.children.map(item => ({
|
|
key: item.key,
|
|
label: item.label,
|
|
})),
|
|
}))
|
|
|
|
const currentLabel =
|
|
visibleGroups.flatMap(group => group.children).find(item => item.key === relativePath)?.label ||
|
|
sectionMeta[relativePath]?.title ||
|
|
(relativePath === '/' ? '仪表盘' : '功能建设中')
|
|
|
|
const onLogout = async () => {
|
|
await logout()
|
|
navigate(getAdminLoginPath(), { replace: true })
|
|
}
|
|
|
|
return (
|
|
<Layout className="admin-shell">
|
|
<Sider
|
|
className="admin-sider"
|
|
theme="light"
|
|
width={240}
|
|
collapsedWidth={72}
|
|
collapsed={collapsed}
|
|
trigger={null}
|
|
>
|
|
<Link to={basePath} className="admin-sider__brand" aria-label="解忧商行后台首页">
|
|
<BrandLogo height={34} variant="light" />
|
|
{!collapsed ? <span className="admin-sider__phase">OPS · P1</span> : null}
|
|
</Link>
|
|
|
|
<Menu
|
|
className="admin-menu"
|
|
mode="inline"
|
|
theme="light"
|
|
inlineCollapsed={collapsed}
|
|
items={menuItems}
|
|
selectedKeys={[relativePath]}
|
|
defaultOpenKeys={visibleGroups.map(group => group.key)}
|
|
onClick={({ key }) => {
|
|
if (key.startsWith('/')) {
|
|
navigate(key === '/' ? basePath : `${basePath}${key}`)
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{!collapsed ? (
|
|
<div className="admin-sider__foot">
|
|
<Typography.Text type="secondary">解忧商行 · 运营后台</Typography.Text>
|
|
<Typography.Text type="secondary">Phase 2 · 交易与商品</Typography.Text>
|
|
</div>
|
|
) : null}
|
|
</Sider>
|
|
|
|
<Layout className="admin-main">
|
|
<Header className="admin-header">
|
|
<div className="admin-header__left">
|
|
<Button
|
|
type="text"
|
|
className="admin-collapse"
|
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
onClick={() => setCollapsed(value => !value)}
|
|
aria-label={collapsed ? '展开侧栏' : '收起侧栏'}
|
|
/>
|
|
<Breadcrumb
|
|
items={[
|
|
{ title: '解忧商行' },
|
|
{ title: currentLabel },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<Space size={16}>
|
|
{admin?.roles?.[0] ? <Tag className="admin-role-tag">{admin.roles[0].name}</Tag> : null}
|
|
<Dropdown
|
|
menu={{
|
|
items: [
|
|
{
|
|
key: 'account',
|
|
label: `${admin?.nickname || admin?.username || '管理员'} · ${admin?.username || ''}`,
|
|
disabled: true,
|
|
},
|
|
{ type: 'divider' },
|
|
{ key: 'logout', label: '退出后台', icon: <LogoutOutlined />, onClick: onLogout },
|
|
],
|
|
}}
|
|
placement="bottomRight"
|
|
>
|
|
<Button type="text" className="admin-account">
|
|
<Avatar size={30} className="admin-avatar">
|
|
{(admin?.nickname || admin?.username || '管').slice(0, 1)}
|
|
</Avatar>
|
|
<span className="admin-account__name">{admin?.nickname || admin?.username}</span>
|
|
</Button>
|
|
</Dropdown>
|
|
</Space>
|
|
</Header>
|
|
|
|
<Content className="admin-content">
|
|
<Outlet />
|
|
</Content>
|
|
</Layout>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
export function AdminSectionPlaceholder({ title, description }: { title: string; description: string }) {
|
|
return (
|
|
<div className="admin-page">
|
|
<div className="admin-page__head">
|
|
<div>
|
|
<Typography.Text className="admin-page__eyebrow">JIEYOU OPS</Typography.Text>
|
|
<Typography.Title level={2} className="brand-title">
|
|
{title}
|
|
</Typography.Title>
|
|
<Typography.Text type="secondary">{description}</Typography.Text>
|
|
</div>
|
|
<Tag icon={<FileDoneOutlined />} className="admin-phase-tag">
|
|
Phase 1 · 待迁移
|
|
</Tag>
|
|
</div>
|
|
|
|
<div className="admin-placeholder-card">
|
|
<span className="admin-placeholder-card__seal">解忧</span>
|
|
<Typography.Title level={4}>这个页面还没开始迁移</Typography.Title>
|
|
<Typography.Paragraph type="secondary">
|
|
当前批次先把后台登录、权限会话、浅色 Shell 和真实仪表盘跑通。{title}
|
|
会在后续批次按统一列表/详情/表单原型迁移,不会直接复用旧站页面。
|
|
</Typography.Paragraph>
|
|
<Button type="primary" onClick={() => window.history.back()}>
|
|
返回上一页
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function getAdminSectionMeta(key: string) {
|
|
return sectionMeta[key]
|
|
}
|