提交 6d1921de authored 作者: 王鹏飞's avatar 王鹏飞

chore(admin): 修改页面布局和样式

上级 f0d76e9d
...@@ -5,34 +5,50 @@ ...@@ -5,34 +5,50 @@
&__filters { &__filters {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
align-items: flex-end; align-items: flex-end;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
} }
&__filter-main {
display: flex;
flex: 0 1 auto;
align-items: flex-end;
gap: 16px;
min-width: 0;
}
&__filter-items { &__filter-items {
display: flex; display: flex;
flex: 1; flex: 0 1 auto;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-end; align-items: flex-end;
gap: 16px; gap: 16px;
} }
&__filter-actions {
flex: 0 0 auto;
}
&__filter-item { &__filter-item {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: center;
gap: 8px; gap: 8px;
} }
&__filter-label { &__filter-label {
flex: 0 0 auto;
color: rgb(0 0 0 / 88%); color: rgb(0 0 0 / 88%);
white-space: nowrap;
} }
&__toolbar { &__toolbar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
flex: 0 0 auto;
} }
&__blocking-error { &__blocking-error {
......
...@@ -53,29 +53,32 @@ export function AppList<Row extends object, Filters extends object = Record<stri ...@@ -53,29 +53,32 @@ export function AppList<Row extends object, Filters extends object = Record<stri
<div className="app-list"> <div className="app-list">
{filters ? ( {filters ? (
<section className="app-list__filters" aria-label="筛选条件"> <section className="app-list__filters" aria-label="筛选条件">
<div className="app-list__filter-items"> <div className="app-list__filter-main">
{filters.items.map((item) => ( <div className="app-list__filter-items">
<div className="app-list__filter-item" key={String(item.name)}> {filters.items.map((item) => (
<span className="app-list__filter-label">{item.label}</span> <div className="app-list__filter-item" key={String(item.name)}>
{item.render({ <span className="app-list__filter-label">{item.label}</span>
value: filters.value[item.name], {item.render({
onChange: (value) => updateFilter(item, value), value: filters.value[item.name],
})} onChange: (value) => updateFilter(item, value),
</div> })}
))} </div>
))}
</div>
<Space wrap className="app-list__filter-actions">
<Button type="primary" icon={<SearchOutlined />} onClick={filters.onSearch}>
搜索
</Button>
<Button icon={<ReloadOutlined />} onClick={filters.onReset}>
重置
</Button>
</Space>
</div> </div>
<Space wrap> {toolbar ? <div className="app-list__toolbar">{toolbar}</div> : null}
<Button type="primary" icon={<SearchOutlined />} onClick={filters.onSearch}>
搜索
</Button>
<Button icon={<ReloadOutlined />} onClick={filters.onReset}>
重置
</Button>
</Space>
</section> </section>
) : null} ) : null}
{toolbar ? <div className="app-list__toolbar">{toolbar}</div> : null} {!filters && toolbar ? <div className="app-list__toolbar">{toolbar}</div> : null}
{hasRefreshError ? ( {hasRefreshError ? (
<Alert <Alert
......
import { Button } from "antd"; import { Link } from "react-router";
import { LeftOutlined } from "@ant-design/icons";
import { useLocation, useNavigate } from "react-router";
export default function Breadcrumb() { export default function Breadcrumb({ parent }) {
const { pathname } = useLocation(); if (!parent) return null;
const navigate = useNavigate();
// const hasBack = currentMenusList.find(item => {
// const regExp = new RegExp(`^${item.path}`)
// return regExp.test(pathname) && item.path !== pathname
// })
const hasBack = [
"/books/management/add-edit",
"/setting/help/addedit",
"/books/management/chapter",
"/userinfo",
"/books/audit/detail",
"/books/sale/discussP",
"/books/sale/discuss-detail",
"/member/list/detail",
"/books/sale/detail",
].includes(pathname);
if (!hasBack) return null;
return ( return (
<div className="layout-breadcrumb"> <div className="layout-breadcrumb">
<Button type="text" icon={<LeftOutlined />} onClick={() => navigate(-1)}> <Link to={parent.parent}>{parent.parentTitle}</Link>
返回 <span aria-hidden="true">/</span>
</Button>
</div> </div>
); );
} }
import { useState, useEffect, Suspense } from "react"; import { useState, useEffect, Suspense } from 'react'
import { Button, Layout, Flex } from "antd"; import { Button, Layout, Flex } from 'antd'
import { Outlet, Link, useLocation } from "react-router"; import { Outlet, Link, useLocation } from 'react-router'
import { useDispatch } from "react-redux"; import { useDispatch } from 'react-redux'
import { import {
RiAdvertisementLine, RiAdvertisementLine,
RiAppStoreLine, RiAppStoreLine,
...@@ -38,194 +38,196 @@ import { ...@@ -38,194 +38,196 @@ import {
RiAdminLine, RiAdminLine,
RiLayoutGridLine, RiLayoutGridLine,
RiPaletteLine, RiPaletteLine,
} from "@remixicon/react"; } from '@remixicon/react'
import { setOperationPermissionsList } from "@/store/modules/user"; import { setOperationPermissionsList } from '@/store/modules/user'
import Menu from "./components/menu"; import Menu from './components/menu'
import Message from "./components/message"; import Message from './components/message'
import User from "./components/user"; import User from './components/user'
import Logout from "./components/logout"; import Logout from './components/logout'
import Breadcrumb from "./components/breadcrumb"; import Breadcrumb from './components/breadcrumb'
import "./index.less"; import './index.less'
import logo from "@/assets/images/logo.png"; import logo from '@/assets/images/logo.png'
import { getMenuList, checkSSOLogin } from "./request"; import { getMenuList, checkSSOLogin } from './request'
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout
function getMenuIcon(item, hasChildren) { function getMenuIcon(item, hasChildren) {
const name = item.name || ""; const name = item.name || ''
const path = item.front_url || ""; const path = item.front_url || ''
if (/\/books\/management/.test(path)) return RiQuillPenLine; if (/\/books\/management/.test(path)) return RiQuillPenLine
if (/\/books\/audit/.test(path)) return RiShieldCheckLine; if (/\/books\/audit/.test(path)) return RiShieldCheckLine
if (/\/books\/classify/.test(path)) return RiFolder2Line; if (/\/books\/classify/.test(path)) return RiFolder2Line
if (/\/books\/label/.test(path)) return RiPriceTag3Line; if (/\/books\/label/.test(path)) return RiPriceTag3Line
if (/\/books\/sale/.test(path)) return RiStore2Line; if (/\/books\/sale/.test(path)) return RiStore2Line
if (/\/books\/question-bank/.test(path)) return RiQuestionnaireLine; if (/\/books\/question-bank/.test(path)) return RiQuestionnaireLine
if (/\/books\/discussion/.test(path)) return RiDiscussLine; if (/\/books\/discussion/.test(path)) return RiDiscussLine
if (/\/books\/order-management/.test(path)) return RiShoppingBag3Line; if (/\/books\/order-management/.test(path)) return RiShoppingBag3Line
if (/\/books\/design/.test(path)) return RiPaletteLine; if (/\/books\/design/.test(path)) return RiPaletteLine
if (/\/member\/userlevel/.test(path)) return RiMedal2Line; if (/\/member\/userlevel/.test(path)) return RiMedal2Line
if (/\/member\/list/.test(path)) return RiUser3Line; if (/\/member\/list/.test(path)) return RiUser3Line
if (/\/advertisement\/adsense/.test(path)) return RiLayoutGridLine; if (/\/advertisement\/adsense/.test(path)) return RiLayoutGridLine
if (/\/advertisement\/advert/.test(path)) return RiAdvertisementLine; if (/\/advertisement\/advert/.test(path)) return RiAdvertisementLine
if (/\/setting\/help/.test(path)) return RiCustomerService2Line; if (/\/setting\/help/.test(path)) return RiCustomerService2Line
if (/\/setting\/applied/.test(path)) return RiAppStoreLine; if (/\/setting\/applied/.test(path)) return RiAppStoreLine
if (/\/setting\/personalized-setting/.test(path)) return RiPaletteLine; if (/\/setting\/personalized-setting/.test(path)) return RiPaletteLine
if (/\/setting\/point-management/.test(path)) return RiCoinsLine; if (/\/setting\/point-management/.test(path)) return RiCoinsLine
if (/\/setting\/app-version/.test(path)) return RiSmartphoneLine; if (/\/setting\/app-version/.test(path)) return RiSmartphoneLine
if (/\/setting\/coupon/.test(path)) return RiCoupon3Line; if (/\/setting\/coupon/.test(path)) return RiCoupon3Line
if (/\/setting\/sensitive/.test(path)) return RiSearchEyeLine; if (/\/setting\/sensitive/.test(path)) return RiSearchEyeLine
if (/\/setting\/feedback/.test(path)) return RiFeedbackLine; if (/\/setting\/feedback/.test(path)) return RiFeedbackLine
if (/\/jurisdiction\/admin/.test(path)) return RiAdminLine; if (/\/jurisdiction\/admin/.test(path)) return RiAdminLine
if (/\/jurisdiction\/role/.test(path)) return RiShieldKeyholeLine; if (/\/jurisdiction\/role/.test(path)) return RiShieldKeyholeLine
if (/\/teacher\/list/.test(path)) return RiUserStarLine; if (/\/teacher\/list/.test(path)) return RiUserStarLine
if (/首页|工作台|概览/.test(name)) return RiHome5Line; if (/首页|工作台|概览/.test(name)) return RiHome5Line
if (/图书|教材|章节|书架/.test(name)) return hasChildren ? RiBook2Line : RiBookOpenLine; if (/图书|教材|章节|书架/.test(name)) return hasChildren ? RiBook2Line : RiBookOpenLine
if (/题库|题目|试题|练习/.test(name)) return RiQuestionnaireLine; if (/题库|题目|试题|练习/.test(name)) return RiQuestionnaireLine
if (/分类|标签/.test(name)) return RiPriceTag3Line; if (/分类|标签/.test(name)) return RiPriceTag3Line
if (/会员|用户/.test(name)) return RiUser3Line; if (/会员|用户/.test(name)) return RiUser3Line
if (/教师|老师/.test(name)) return RiUserStarLine; if (/教师|老师/.test(name)) return RiUserStarLine
if (/班级|组织|团队/.test(name)) return RiGroupLine; if (/班级|组织|团队/.test(name)) return RiGroupLine
if (/订单|销售|支付|结算/.test(name)) return RiShoppingBag3Line; if (/订单|销售|支付|结算/.test(name)) return RiShoppingBag3Line
if (/优惠券|卡券/.test(name)) return RiCoupon3Line; if (/优惠券|卡券/.test(name)) return RiCoupon3Line
if (/积分|账单|余额/.test(name)) return RiCoinsLine; if (/积分|账单|余额/.test(name)) return RiCoinsLine
if (/广告|轮播/.test(name)) return RiAdvertisementLine; if (/广告|轮播/.test(name)) return RiAdvertisementLine
if (/讨论|评论/.test(name)) return RiDiscussLine; if (/讨论|评论/.test(name)) return RiDiscussLine
if (/审核|认证/.test(name)) return RiShieldCheckLine; if (/审核|认证/.test(name)) return RiShieldCheckLine
if (/权限|角色/.test(name)) return RiShieldKeyholeLine; if (/权限|角色/.test(name)) return RiShieldKeyholeLine
if (/帮助|客服/.test(name)) return RiCustomerService2Line; if (/帮助|客服/.test(name)) return RiCustomerService2Line
if (/反馈/.test(name)) return RiFeedbackLine; if (/反馈/.test(name)) return RiFeedbackLine
if (/消息|通知/.test(name)) return RiNotification3Line; if (/消息|通知/.test(name)) return RiNotification3Line
if (/应用/.test(name)) return RiAppStoreLine; if (/应用/.test(name)) return RiAppStoreLine
if (/设置|配置/.test(name)) return RiSettings3Line; if (/设置|配置/.test(name)) return RiSettings3Line
if (/内容|编辑/.test(name)) return RiFileSettingsLine; if (/内容|编辑/.test(name)) return RiFileSettingsLine
return hasChildren ? RiApps2Line : RiFileList3Line; return hasChildren ? RiApps2Line : RiFileList3Line
} }
function generateMenus(items = [], maxLevel = 2, level = 1) { function generateMenus(items = [], maxLevel = 2, level = 1) {
return items.map((item) => { return items.map((item) => {
const children = const children = level < maxLevel && item.childs ? generateMenus(item.childs, maxLevel, level + 1) : null
level < maxLevel && item.childs ? generateMenus(item.childs, maxLevel, level + 1) : null; const Icon = getMenuIcon(item, Boolean(children?.length))
const Icon = getMenuIcon(item, Boolean(children?.length));
return { return {
key: item.id + "", key: item.id + '',
path: item.front_url, path: item.front_url,
title: item.name, title: item.name,
icon: <Icon className="layout-menu__icon" aria-hidden="true" />, icon: <Icon className="layout-menu__icon" aria-hidden="true" />,
label: <Link to={item.front_url}>{item.name}</Link>, label: <Link to={item.front_url}>{item.name}</Link>,
children, children,
}; }
}); })
} }
function getCurrentMenuTitle(items, pathname) { function getCurrentMenuTitle(items, pathname) {
let currentMenu; let currentMenu
const visit = (menuItems) => { const visit = (menuItems) => {
menuItems.forEach((item) => { menuItems.forEach((item) => {
const isMatch = const isMatch =
item.path && item.path && (pathname === item.path || (item.path !== '/' && pathname.startsWith(`${item.path}/`)))
(pathname === item.path || (item.path !== "/" && pathname.startsWith(`${item.path}/`))); if (isMatch && (!currentMenu || item.path.length > currentMenu.path.length)) currentMenu = item
if (isMatch && (!currentMenu || item.path.length > currentMenu.path.length)) if (item.children?.length) visit(item.children)
currentMenu = item; })
if (item.children?.length) visit(item.children); }
});
}; visit(items)
return currentMenu?.title || ''
}
visit(items); const pageHeaderOverrides = {
return currentMenu?.title || "工作台"; '/books/management/add-edit': { parent: '/books/management', parentTitle: '写书管理', title: '编辑图书' },
'/books/management/chapter': { parent: '/books/management', parentTitle: '写书管理', title: '章节编辑' },
'/books/audit/detail': { parent: '/books/audit', parentTitle: '书籍审核', title: '审核详情' },
'/books/sale/detail': { parent: '/books/sale', parentTitle: '在售书籍', title: '书籍编辑' },
'/books/sale/discussP': { parent: '/books/sale', parentTitle: '在售书籍', title: '书籍讨论' },
'/books/sale/discuss-detail': { parent: '/books/sale', parentTitle: '在售书籍', title: '讨论详情' },
'/member/list/detail': { parent: '/member/list', parentTitle: '会员管理', title: '会员详情' },
'/setting/help/addedit': { parent: '/setting/help/list', parentTitle: '帮助中心', title: '编辑帮助' },
'/userinfo': { parent: '/', parentTitle: '首页', title: '个人中心' },
} }
export default function AppLayout() { export default function AppLayout() {
const dispatch = useDispatch(); const dispatch = useDispatch()
const { pathname } = useLocation(); const { pathname } = useLocation()
const [menuList, setMenuList] = useState([]); const [menuList, setMenuList] = useState([])
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false)
const pageTitle = getCurrentMenuTitle(menuList, pathname); const headerOverride = pageHeaderOverrides[pathname]
const pageTitle = headerOverride?.title || getCurrentMenuTitle(menuList, pathname)
const getSSOToken = async () => { const getSSOToken = async () => {
const data = await checkSSOLogin(); const data = await checkSSOLogin()
if (data && data.token) { if (data && data.token) {
localStorage.setItem("kiwi.token", data.token); localStorage.setItem('kiwi.token', data.token)
} }
}; }
// 获取菜单 // 获取菜单
const getMenu = async () => { const getMenu = async () => {
// 获取菜单内容 // 获取菜单内容
const { data, operation_permissions_list } = await getMenuList(); const { data, operation_permissions_list } = await getMenuList()
if (data && data.length) { if (data && data.length) {
setMenuList(generateMenus(data)); setMenuList(generateMenus(data))
} }
// 获取操作权限内容 // 获取操作权限内容
if (operation_permissions_list && operation_permissions_list.length) { if (operation_permissions_list && operation_permissions_list.length) {
dispatch(setOperationPermissionsList(operation_permissions_list)); dispatch(setOperationPermissionsList(operation_permissions_list))
} }
}; }
useEffect(() => { useEffect(() => {
(async () => { ;(async () => {
await getSSOToken(); await getSSOToken()
await getMenu(); await getMenu()
})(); })()
}, []); }, [])
return ( return (
<Layout className="layout-container"> <Layout className="layout-container">
<Header className="layout-header"> <Sider
className="layout-sider"
width={240}
collapsedWidth={64}
collapsed={collapsed}
theme="light"
trigger={null}>
<div className="layout-brand"> <div className="layout-brand">
<Link className="layout-brand__logo" to="/" aria-label="返回工作台首页"> <Link className="layout-brand__logo" to="/" aria-label="返回工作台首页">
<img src={logo} alt="清控紫荆数智学堂" /> <img src={logo} alt="清控紫荆数智学堂" />
</Link> </Link>
</div> </div>
{menuList.length ? ( <div className="layout-sider__body">
<Flex align="center" gap="small" className="layout-header__actions"> <Menu menuList={menuList} collapsed={collapsed}></Menu>
<Message></Message> </div>
<User></User> <Button
<Logout></Logout> className="layout-sider-toggle"
</Flex> type="text"
) : null} onClick={() => setCollapsed((value) => !value)}
</Header> aria-label={collapsed ? '展开侧边菜单' : '收起侧边菜单'}>
<Layout className={`layout-body${collapsed ? " layout-body--collapsed" : ""}`}> {collapsed ? <RiMenuUnfoldLine aria-hidden="true" /> : <RiMenuFoldLine aria-hidden="true" />}
<Sider </Button>
className="layout-sider" </Sider>
width={200} <Layout className="layout-main">
collapsedWidth={56} <Header className="layout-header">
collapsed={collapsed} <div className="layout-page-header">
theme="light" <Breadcrumb parent={headerOverride}></Breadcrumb>
trigger={null} <div className="layout-page-title">{pageTitle}</div>
>
<div className="layout-sider__body">
<Menu menuList={menuList} collapsed={collapsed}></Menu>
</div> </div>
<Button {menuList.length ? (
className="layout-sider-toggle" <Flex align="center" gap="small" className="layout-header__actions">
type="text" <Message></Message>
onClick={() => setCollapsed((value) => !value)} <User></User>
aria-label={collapsed ? "展开侧边菜单" : "收起侧边菜单"} <Logout></Logout>
> </Flex>
{collapsed ? ( ) : null}
<RiMenuUnfoldLine aria-hidden="true" /> </Header>
) : ( <Content className="layout-content">
<RiMenuFoldLine aria-hidden="true" /> <main className="layout-page-content">
)} <Suspense>
</Button> <Outlet />
</Sider> </Suspense>
<Layout className="layout-main"> </main>
<Content className="layout-content"> </Content>
<div className="layout-page-header">
<div className="layout-page-title">{pageTitle}</div>
<Breadcrumb></Breadcrumb>
</div>
<main className="layout-page-content">
<Suspense>
<Outlet />
</Suspense>
</main>
</Content>
</Layout>
</Layout> </Layout>
</Layout> </Layout>
); )
} }
.layout-container { .layout-container {
min-height: 100vh; min-height: 100vh;
background: #f6f7f9; background: #fff;
} }
.layout-sider { .layout-sider {
position: fixed !important; position: sticky !important;
top: 84px; top: 0;
bottom: 0; height: 100vh;
left: 0;
height: auto;
overflow: hidden; overflow: hidden;
background: #f6f7f9 !important; background: #f5f6f7 !important;
border-inline-end: 0 !important; border-inline-end: 0 !important;
box-shadow: none; box-shadow: none;
z-index: 100; z-index: 100;
...@@ -28,20 +26,11 @@ ...@@ -28,20 +26,11 @@
overflow-y: auto; overflow-y: auto;
} }
.layout-body {
min-height: calc(100vh - 84px);
padding-left: 200px;
}
.layout-body--collapsed {
padding-left: 56px;
}
.layout-brand { .layout-brand {
display: flex; display: flex;
align-items: center; align-items: center;
height: 84px; height: 84px;
padding: 0; padding: 0 20px;
.layout-brand__logo { .layout-brand__logo {
display: inline-flex; display: inline-flex;
...@@ -68,7 +57,7 @@ ...@@ -68,7 +57,7 @@
margin: 0 10px 10px; margin: 0 10px 10px;
padding: 0; padding: 0;
color: #6f7886; color: #6f7886;
background: #f6f7f9; background: #f5f6f7;
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
...@@ -253,7 +242,7 @@ ...@@ -253,7 +242,7 @@
.layout-main { .layout-main {
min-width: 0; min-width: 0;
background: #f6f7f9; background: #fff;
} }
.layout-header { .layout-header {
...@@ -264,16 +253,16 @@ ...@@ -264,16 +253,16 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: 84px; height: 84px;
padding: 0 32px 0 20px; padding: 0 32px;
line-height: normal; line-height: normal;
background: #f6f7f9; background: #fff;
border-bottom: 0; border-bottom: 1px solid #1f23290a;
} }
.layout-page-title { .layout-page-title {
display: flex; display: flex;
align-items: center; align-items: center;
height: 60px; height: 56px;
color: #252b36; color: #252b36;
font-size: 22px; font-size: 22px;
font-weight: 600; font-weight: 600;
...@@ -281,7 +270,10 @@ ...@@ -281,7 +270,10 @@
} }
.layout-page-header { .layout-page-header {
min-height: 60px; display: flex;
align-items: center;
gap: 8px;
min-width: 0;
} }
.layout-header__actions { .layout-header__actions {
...@@ -307,16 +299,31 @@ ...@@ -307,16 +299,31 @@
.layout-breadcrumb { .layout-breadcrumb {
display: flex; display: flex;
align-items: center; align-items: center;
height: 40px; height: 56px;
margin-top: -8px; margin: 0;
padding: 0; padding: 0;
a {
color: #646a73;
font-size: 14px;
text-decoration: none;
&:hover {
color: #1f2329;
}
}
span {
margin-left: 8px;
color: #b0b5bd;
}
} }
.layout-content { .layout-content {
min-height: calc(100vh - 84px); min-height: calc(100vh - 84px);
margin: 0 32px 40px 20px; margin: 0;
padding: 0; padding: 24px 32px 40px;
background: transparent; background: #fff;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
} }
...@@ -355,9 +362,9 @@ ...@@ -355,9 +362,9 @@
margin-top: 16px; margin-top: 16px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
border: 1px solid #e7eaee; border: 0;
border-radius: 12px; border-radius: 0;
box-shadow: 0 1px 2px rgb(31 41 55 / 2%); box-shadow: none;
} }
.ant-table, .ant-table,
...@@ -377,13 +384,13 @@ ...@@ -377,13 +384,13 @@
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
background: #f8f9fb !important; background: #f8f9fb !important;
border-bottom: 1px solid #edf0f3; border-bottom: 1px solid #1f232926;
} }
.ant-table-tbody > tr > td { .ant-table-tbody > tr > td {
height: 52px; height: 52px;
color: #303641; color: #303641;
border-bottom: 1px solid #f0f2f4; border-bottom: 1px solid #1f232926;
} }
.ant-table-tbody > tr:nth-child(2n) { .ant-table-tbody > tr:nth-child(2n) {
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论