feat(frontend): 完成品牌 SVG 与后台 Phase 1 基础切片

This commit is contained in:
yml2213
2026-10-30 12:20:01 +08:00
parent 8d9481c251
commit 97167fa482
25 changed files with 2028 additions and 40 deletions
+299
View File
@@ -0,0 +1,299 @@
import { useState, type ReactNode } from 'react'
import {
Avatar,
Breadcrumb,
Button,
Dropdown,
Layout,
Menu,
Space,
Tag,
Typography,
type MenuProps,
} from 'antd'
import {
AccountBookOutlined,
BellOutlined,
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:view' },
{ key: '/orders/refund-review', label: '退款审核', permission: 'order:close' },
{ 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: 'finance:view' },
{ key: '/withdrawals', label: '提现审核', permission: 'withdrawal:view' },
],
},
{
key: 'distribution',
label: '分销',
icon: <ShareAltOutlined />,
children: [
{ key: '/distribution/sites', label: '站点管理', permission: 'distribution:site_view' },
],
},
{
key: 'operations',
label: '运营',
icon: <BellOutlined />,
children: [{ key: '/announcements', label: '公告管理', permission: 'announcement:view' }],
},
{
key: 'system',
label: '系统',
icon: <SettingOutlined />,
children: [
{ key: '/system-configs', label: '系统配置', permission: 'systemconfig:view' },
{ key: '/audit-logs', label: '审计日志', permission: 'audit:view' },
],
},
]
const sectionMeta: Record<string, { title: string; description: string }> = {
'/orders': { title: '订单管理', description: '查看全量订单、交接记录与结账状态。' },
'/orders/refund-review': { title: '退款审核', description: '审核退款申请并核对资金影响。' },
'/disputes': { title: '申诉仲裁', description: '处理订单争议、证据与仲裁结果。' },
'/listings': { title: '商品管理', description: '查看商品、账号资产与上下架状态。' },
'/listings/review': { title: '商品审核', description: '审核待上架商品与账号截图。' },
'/users': { title: '用户管理', description: '查看用户、实名状态与交易记录。' },
'/finance': { title: '财务看板', description: '查看收入、退款、结算与资金趋势。' },
'/withdrawals': { title: '提现审核', description: '审核提现申请与打款确认。' },
'/distribution/sites': { title: '分销站点', description: '管理分站、规则、结算与站点钱包。' },
'/announcements': { title: '公告管理', description: '维护主站与分站公告。' },
'/system-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 1 Shell</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]
}