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

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

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