提交 088eacea authored 作者: 王鹏飞's avatar 王鹏飞

feat: 添加菜单路径处理和高级筛选功能,优化布局样式

上级 12993d55
...@@ -42,3 +42,21 @@ export function getOpenMenuKeys(menuList, pathname) { ...@@ -42,3 +42,21 @@ export function getOpenMenuKeys(menuList, pathname) {
visit(menuList) visit(menuList)
return openKeys return openKeys
} }
export function getFirstMenuPath(menuList = []) {
for (const item of menuList) {
if (item.children?.length) {
const childPath = getFirstMenuPath(item.children)
if (childPath) return childPath
}
if (item.path && item.path !== '/') return item.path
}
return ''
}
export function hasMenuPath(menuList = [], pathname) {
return menuList.some(
(item) =>
item.path === pathname || (item.children?.length && hasMenuPath(item.children, pathname)),
)
}
import test from 'node:test' import test from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { getOpenMenuKeys, getSelectedMenuKeys } from './menu.utils.js' import {
getFirstMenuPath,
getOpenMenuKeys,
getSelectedMenuKeys,
hasMenuPath,
} from './menu.utils.js'
test('highlights the current nested menu item', () => { test('highlights the current nested menu item', () => {
const menuList = [ const menuList = [
...@@ -28,3 +33,17 @@ test('opens a parent with no route when one of its children matches', () => { ...@@ -28,3 +33,17 @@ test('opens a parent with no route when one of its children matches', () => {
assert.deepEqual(getOpenMenuKeys(menuList, '/settings/roles'), ['settings']) assert.deepEqual(getOpenMenuKeys(menuList, '/settings/roles'), ['settings'])
}) })
test('finds the first accessible leaf menu path', () => {
const menuList = [
{
key: 'books',
path: '/books',
children: [{ key: 'orders', path: '/books/order-management' }],
},
]
assert.equal(getFirstMenuPath(menuList), '/books/order-management')
assert.equal(hasMenuPath(menuList, '/books/management'), false)
assert.equal(hasMenuPath(menuList, '/books/order-management'), true)
})
import { useState, useEffect, Suspense } from 'react' import { useState, useEffect, Suspense } from 'react'
import { Button, Layout, Flex, Tooltip } from 'antd' import { Button, Layout, Flex, Tooltip } from 'antd'
import { Outlet, Link, useLocation } from 'react-router' import { Outlet, Link, useLocation, useNavigate } from 'react-router'
import { import {
RiAdvertisementLine, RiAdvertisementLine,
RiAppStoreLine, RiAppStoreLine,
...@@ -46,6 +46,7 @@ import Message from './components/message' ...@@ -46,6 +46,7 @@ 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 { getFirstMenuPath, hasMenuPath } from './components/menu.utils'
import './index.less' import './index.less'
import logo from '@/assets/images/logo.png' import logo from '@/assets/images/logo.png'
...@@ -170,6 +171,7 @@ const pageHeaderOverrides = { ...@@ -170,6 +171,7 @@ const pageHeaderOverrides = {
export default function AppLayout() { export default function AppLayout() {
const setOperationPermissionsList = useUserStore((state) => state.setOperationPermissionsList) const setOperationPermissionsList = useUserStore((state) => state.setOperationPermissionsList)
const { pathname } = useLocation() const { pathname } = useLocation()
const navigate = useNavigate()
const [menuList, setMenuList] = useState([]) const [menuList, setMenuList] = useState([])
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(false)
const headerOverride = pageHeaderOverrides[pathname] const headerOverride = pageHeaderOverrides[pathname]
...@@ -206,6 +208,21 @@ export default function AppLayout() { ...@@ -206,6 +208,21 @@ export default function AppLayout() {
})() })()
}, []) }, [])
useEffect(() => {
if (menuList.length) setCollapsed(menuList.length < 3)
}, [menuList.length])
useEffect(() => {
if (pathname !== '/books/management' || !menuList.length || hasMenuPath(menuList, pathname)) {
return
}
const firstMenuPath = getFirstMenuPath(menuList)
if (firstMenuPath && firstMenuPath !== pathname) {
navigate(firstMenuPath, { replace: true })
}
}, [menuList, navigate, pathname])
return ( return (
<Layout className='layout-container'> <Layout className='layout-container'>
<EditorIconSprite /> <EditorIconSprite />
......
.layout-container { .layout-container {
height: 100vh;
min-height: 100vh; min-height: 100vh;
overflow: hidden;
background: #fff; background: #fff;
} }
...@@ -242,6 +244,9 @@ ...@@ -242,6 +244,9 @@
.layout-main { .layout-main {
min-width: 0; min-width: 0;
min-height: 0;
height: 100vh;
overflow: hidden;
background: #fff; background: #fff;
} }
...@@ -320,7 +325,9 @@ ...@@ -320,7 +325,9 @@
} }
.layout-content { .layout-content {
min-height: calc(100vh - 84px); min-height: 0;
height: calc(100vh - 84px);
overflow-y: auto;
margin: 0; margin: 0;
padding: 24px 32px; padding: 24px 32px;
background: #fff; background: #fff;
......
...@@ -27,7 +27,14 @@ export function LibraryToolbar({ ...@@ -27,7 +27,14 @@ export function LibraryToolbar({
onReset, onReset,
}: LibraryToolbarProps) { }: LibraryToolbarProps) {
const [keyword, setKeyword] = useState(state.keyword) const [keyword, setKeyword] = useState(state.keyword)
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
const isSearching = Boolean(state.keyword) const isSearching = Boolean(state.keyword)
const hasAdvancedFilters =
state.category !== 0 ||
state.label !== 0 ||
state.price !== 'all' ||
state.sortField !== 'none' ||
state.sortOrder !== 'desc'
const withAllOption = (filters: LibraryFilter[]) => const withAllOption = (filters: LibraryFilter[]) =>
filters.some((item) => item.id === 0) ? filters : [{ id: 0, name: '全部' }, ...filters] filters.some((item) => item.id === 0) ? filters : [{ id: 0, name: '全部' }, ...filters]
const categoryOptions = withAllOption(categories).map((item) => ({ const categoryOptions = withAllOption(categories).map((item) => ({
...@@ -38,6 +45,10 @@ export function LibraryToolbar({ ...@@ -38,6 +45,10 @@ export function LibraryToolbar({
useEffect(() => setKeyword(state.keyword), [state.keyword]) useEffect(() => setKeyword(state.keyword), [state.keyword])
useEffect(() => {
if (hasAdvancedFilters) setShowAdvancedFilters(true)
}, [hasAdvancedFilters])
return ( return (
<Form component={false} aria-label='图书馆筛选'> <Form component={false} aria-label='图书馆筛选'>
<div className='library-toolbar__controls'> <div className='library-toolbar__controls'>
...@@ -57,70 +68,10 @@ export function LibraryToolbar({ ...@@ -57,70 +68,10 @@ export function LibraryToolbar({
onSearch={onKeywordSearch} onSearch={onKeywordSearch}
/> />
</Form.Item> </Form.Item>
<Form.Item label='分类'>
<Select
aria-label='分类'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.category}
options={categoryOptions}
onChange={(category) => onFiltersChange({ category })}
/>
</Form.Item>
<Form.Item label='标签'>
<Select
aria-label='标签'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.label}
options={labelOptions}
onChange={(label) => onFiltersChange({ label })}
/>
</Form.Item>
</Space> </Space>
<Space className='library-toolbar__secondary' size='small' aria-label='显示与排序'> <Space className='library-toolbar__secondary' size='small' aria-label='显示与筛选'>
<Form.Item label='价格'> {/* 暂时隐藏筛选入口,筛选逻辑保留 */}
<Select
aria-label='价格'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.price}
options={[
{ value: 'all', label: '全部' },
{ value: 'paid', label: '收费' },
{ value: 'free', label: '免费' },
]}
onChange={(price) => onFiltersChange({ price })}
/>
</Form.Item>
<Form.Item label='排序'>
<Space.Compact>
<Select
aria-label='排序'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.sortField}
options={[
{ value: 'none', label: '默认' },
{ value: 'read_num', label: '阅读人数' },
{ value: 'rating', label: '评分' },
]}
onChange={(sortField) => onSortChange({ sortField })}
/>
<Select
aria-label='排序方向'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.sortOrder}
options={[
{ value: 'desc', label: '降序' },
{ value: 'asc', label: '升序' },
]}
onChange={(sortOrder) => onSortChange({ sortOrder })}
/>
</Space.Compact>
</Form.Item>
<Segmented <Segmented
aria-label='图书馆视图' aria-label='图书馆视图'
value={state.view} value={state.view}
...@@ -130,9 +81,77 @@ export function LibraryToolbar({ ...@@ -130,9 +81,77 @@ export function LibraryToolbar({
]} ]}
onChange={(value) => onViewChange(value as LibrarySearchState['view'])} onChange={(value) => onViewChange(value as LibrarySearchState['view'])}
/> />
<Button onClick={onReset}>重置</Button>
</Space> </Space>
</div> </div>
{showAdvancedFilters ? (
<div className='library-toolbar__advanced'>
<Space wrap size='middle'>
<Form.Item label='分类'>
<Select
aria-label='分类'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.category}
options={categoryOptions}
onChange={(category) => onFiltersChange({ category })}
/>
</Form.Item>
<Form.Item label='标签'>
<Select
aria-label='标签'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.label}
options={labelOptions}
onChange={(label) => onFiltersChange({ label })}
/>
</Form.Item>
<Form.Item label='价格'>
<Select
aria-label='价格'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.price}
options={[
{ value: 'all', label: '全部' },
{ value: 'paid', label: '收费' },
{ value: 'free', label: '免费' },
]}
onChange={(price) => onFiltersChange({ price })}
/>
</Form.Item>
<Form.Item label='排序'>
<Space.Compact>
<Select
aria-label='排序'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.sortField}
options={[
{ value: 'none', label: '默认' },
{ value: 'read_num', label: '阅读人数' },
{ value: 'rating', label: '评分' },
]}
onChange={(sortField) => onSortChange({ sortField })}
/>
<Select
aria-label='排序方向'
disabled={isSearching}
popupMatchSelectWidth={false}
value={state.sortOrder}
options={[
{ value: 'desc', label: '降序' },
{ value: 'asc', label: '升序' },
]}
onChange={(sortOrder) => onSortChange({ sortOrder })}
/>
</Space.Compact>
</Form.Item>
<Button onClick={onReset}>重置</Button>
</Space>
</div>
) : null}
</Form> </Form>
) )
} }
...@@ -98,6 +98,17 @@ ...@@ -98,6 +98,17 @@
background: #fafbfc; background: #fafbfc;
} }
.library-toolbar__advanced {
padding: 14px 16px;
border: 1px solid #edf0f4;
border-radius: 12px;
background: #fafbfc;
}
.library-toolbar__advanced .ant-form-item {
margin-bottom: 0;
}
.library-toolbar__controls .ant-form-item { .library-toolbar__controls .ant-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -253,6 +264,11 @@ ...@@ -253,6 +264,11 @@
border: 0; border: 0;
background: transparent; background: transparent;
} }
.library-toolbar__advanced {
padding: 0;
border: 0;
background: transparent;
}
.library-books--grid { .library-books--grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
......
...@@ -114,6 +114,7 @@ export function LibraryView() { ...@@ -114,6 +114,7 @@ export function LibraryView() {
current={state.page} current={state.page}
pageSize={state.pageSize} pageSize={state.pageSize}
total={booksQuery.data?.total ?? 0} total={booksQuery.data?.total ?? 0}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
showTotal={(total) => `共 ${total} 本`} showTotal={(total) => `共 ${total} 本`}
onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)} onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)}
......
...@@ -87,6 +87,7 @@ export function MyBooksView() { ...@@ -87,6 +87,7 @@ export function MyBooksView() {
coursesQuery.data.items.length, coursesQuery.data.items.length,
coursesQuery.data.total, coursesQuery.data.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPage(page, pageSize)} onChange={(page, pageSize) => void queryState.setPage(page, pageSize)}
/> />
......
...@@ -193,6 +193,7 @@ export function MyBookshelfView() { ...@@ -193,6 +193,7 @@ export function MyBookshelfView() {
items.length, items.length,
bookshelfQuery.data?.total, bookshelfQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => { onChange={(page, pageSize) => {
setSelectedCartIds(new Set()) setSelectedCartIds(new Set())
......
...@@ -56,6 +56,7 @@ export function MyCouponsView() { ...@@ -56,6 +56,7 @@ export function MyCouponsView() {
items.length, items.length,
couponsQuery.data?.total, couponsQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)} onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)}
/> />
......
...@@ -74,6 +74,7 @@ export function MyDiscussionsView() { ...@@ -74,6 +74,7 @@ export function MyDiscussionsView() {
books.length, books.length,
discussionsQuery.data?.total, discussionsQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPage(page, pageSize)} onChange={(page, pageSize) => void queryState.setPage(page, pageSize)}
/> />
......
...@@ -111,6 +111,7 @@ export function MyFavoritesView() { ...@@ -111,6 +111,7 @@ export function MyFavoritesView() {
favorites.length, favorites.length,
favoritesQuery.data?.total, favoritesQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPage(page, pageSize)} onChange={(page, pageSize) => void queryState.setPage(page, pageSize)}
/> />
......
...@@ -83,6 +83,7 @@ export function MyMessagesView() { ...@@ -83,6 +83,7 @@ export function MyMessagesView() {
items.length, items.length,
messagesQuery.data?.total, messagesQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)} onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)}
/> />
......
...@@ -83,6 +83,7 @@ export function MyNotesView() { ...@@ -83,6 +83,7 @@ export function MyNotesView() {
books.length, books.length,
notesQuery.data?.total, notesQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPage(page, pageSize)} onChange={(page, pageSize) => void queryState.setPage(page, pageSize)}
/> />
......
...@@ -88,6 +88,7 @@ export function MyOrdersView() { ...@@ -88,6 +88,7 @@ export function MyOrdersView() {
items.length, items.length,
ordersQuery.data?.total, ordersQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)} onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)}
/> />
......
...@@ -63,6 +63,7 @@ export function MyWalletView() { ...@@ -63,6 +63,7 @@ export function MyWalletView() {
records.length, records.length,
query.data?.total, query.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)} onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)}
/> />
......
...@@ -74,6 +74,7 @@ export function MyWrongQuestionsView() { ...@@ -74,6 +74,7 @@ export function MyWrongQuestionsView() {
books.length, books.length,
wrongQuestionsQuery.data?.total, wrongQuestionsQuery.data?.total,
)} )}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
onChange={(page, pageSize) => void queryState.setPage(page, pageSize)} onChange={(page, pageSize) => void queryState.setPage(page, pageSize)}
/> />
......
...@@ -154,6 +154,7 @@ export function SearchView() { ...@@ -154,6 +154,7 @@ export function SearchView() {
current={state.page} current={state.page}
pageSize={state.pageSize} pageSize={state.pageSize}
total={booksQuery.data.total} total={booksQuery.data.total}
hideOnSinglePage
showSizeChanger={false} showSizeChanger={false}
showTotal={(total) => `共 ${total} 本`} showTotal={(total) => `共 ${total} 本`}
onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)} onChange={(page, pageSize) => void queryState.setPagination(page, pageSize)}
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论