滑动菜单
完成滑动菜单的样式
原先的svgsprites
插件将所有的svg标签的样式属性给清除了,需要保留原有的属性值,需要在option
中配置noOptimizeList
用于存放不需要清除svg标签的文件名称。
tsx
import path from 'path'
import fs from 'fs'
import store from 'svgstore'
import { optimize } from 'svgo'
import type { Plugin, ViteDevServer } from 'vite'
interface Options {
id?: string
inputFolder?: string
inline?: boolean
noOptimizeList?: string[]
}
export const svgsprites = (options: Options = {}): Plugin => {
const virtualModuleId = `virtual:svgsprites${options.id ? `-${options.id}` : ''}`
const resolvedVirtualModuleId = `\0${virtualModuleId}`
const { inputFolder = 'src/assets/icons', inline = false } = options
const generateCode = () => {
const sprites = store(options)
const iconsDir = path.resolve(inputFolder)
for (const file of fs.readdirSync(iconsDir)) {
if (!file.endsWith('.svg'))
continue
const filepath = path.join(iconsDir, file)
const svgId = path.parse(file).name
const code = fs.readFileSync(filepath, { encoding: 'utf-8' })
// noOptimizeList 筛除 不需要清除掉 svgId
const symbol = options.noOptimizeList?.includes(svgId)
? code
: optimize(code, {
plugins: [
'cleanupAttrs', 'removeDoctype', 'removeComments', 'removeTitle', 'removeDesc', 'removeEmptyAttrs',
{ name: 'removeAttrs', params: { attrs: '(data-name|fill)' } },
],
}).data
sprites.add(svgId, symbol)
}
return sprites.toString({ inline })
}
const handleFileCreationOrUpdate = (file: string, server: ViteDevServer) => {
if (!file.includes(inputFolder))
return
const code = generateCode()
server.ws.send('svgsprites:change', { code })
const mod = server.moduleGraph.getModuleById(resolvedVirtualModuleId)
if (!mod)
return
server.moduleGraph.invalidateModule(mod, undefined, Date.now())
}
return {
name: 'svgsprites',
configureServer(server) {
server.watcher.on('add', (file) => {
handleFileCreationOrUpdate(file, server)
})
server.watcher.on('change', (file) => {
handleFileCreationOrUpdate(file, server)
})
},
resolveId(id: string) {
if (id === virtualModuleId)
return resolvedVirtualModuleId
},
load(id: string) {
if (id === resolvedVirtualModuleId) {
const code = generateCode()
return `!function(){
const div = document.createElement('div')
div.innerHTML = \`${code}\`
const svg = div.getElementsByTagName('svg')[0]
const updateSvg = (svg) => {
if (!svg) { return }
svg.style.position = 'absolute'
svg.style.width = 0
svg.style.height = 0
svg.style.overflow = 'hidden'
svg.setAttribute("aria-hidden", "true")
}
const insert = () => {
if (document.body.firstChild) {
document.body.insertBefore(div, document.body.firstChild)
} else {
document.body.appendChild(div)
}
}
updateSvg(svg)
if (document.body){
insert()
} else {
document.addEventListener('DOMContentLoaded', insert)
}
if (import.meta.hot) {
import.meta.hot.on('svgsprites:change', (data) => {
const code = data.code
div.innerHTML = code
const svg = div.getElementsByTagName('svg')[0]
updateSvg(svg)
})
}
}()`
}
},
}
}
创建TopMenu
文件夹存放CurrentUser.tsx
、Menu.tsx
文件。
tsx
interface Props {
className?: string
}
export const CurrentUser: React.FC<Props> = ({ className }) => {
return (
<div className={className} bg="#5C33BE" text-white w="100%" pt-32px pb-44px
px-16px>
<h2 text-24px>未登录用户</h2>
<div text="#CEA1FF">点击这里登录</div>
</div>
)
}
tsx
import styled from 'styled-components'
import { Icon } from '../Icon'
interface Props {
className?: string
}
const MyIcon = styled(Icon)`
width: 32px; height: 32px; margin-right: 16px;
`
export const Menu: React.FC<Props> = ({ className }) => {
return (
<ul className={className} bg-white text-20px py-16px
children-flex children-items-center children-px-16px
children-py-8px children-mb-4px>
<li><MyIcon name="chart" />统计图表</li>
<li><MyIcon name="export" />导出数据</li>
<li><MyIcon name="category" />自定义分类</li>
<li><MyIcon name="noty" />记账提醒</li>
</ul>
)
}
然后在TopMenu.tsx
文件中使用这两个组件,详细代码见链接。
tsx
import { CurrentUser } from './TopMenu/CurrentUser'
import { Menu } from './TopMenu/Menu'
export const TopMenu: React.FC = () => {
return (
<div fixed top-0 left-0 w="70vw" max-w-20em h-screen flex flex-col b-3px b-red>
<CurrentUser className="grow-0 shrink-0" />
<Menu className="grow-1 shrink-1" />
</div>
)
}
点击外部关闭菜单
首先在app.scss
设置几个需要使用的z-index
层级默认值,并在src/shims.d.ts
配置z-index
在标签上的属性type
为z
便于unocss
样式的使用。
css
:root {
--un-shadow-color: red;
--z-default: 128;
--z-menu: 256;
--z-dialog: 512;
}
typescript
import * as React from 'react'
declare module 'react' {
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
flex?: boolean
relative?: boolean
text?: string
grid?: boolean
before?: string
after?: string
shadow?: boolean
w?: string
h?: string
bg?: string
rounded?: string
fixed?: boolean
b?: string
z?: string
block?: boolean
}
interface SVGProps<T> extends SVGAttributes<T>, ClassAttributes<T> {
w?: string
h?: string
}
}
然后修改TopMenu
组件,使用设置好的层级添加遮罩层的div
,点击遮罩层并关闭。
tsx
import { CurrentUser } from './TopMenu/CurrentUser'
import { Menu } from './TopMenu/Menu'
interface Props {
onClickMask?: () => void
}
export const TopMenu: React.FC<Props> = (props) => {
const { onClickMask } = props
return (
<>
<div fixed top-0 left-0 w="100%" h="100%" className="bg-black:75"
z="[calc(var(--z-menu)-1)]" onClick={onClickMask}
/>
<div fixed top-0 left-0 w="70vw" max-w-20em h-screen flex flex-col
z="[var(--z-menu)]">
<CurrentUser className="grow-0 shrink-0" />
<Menu className="grow-1 shrink-1" />
</div>
</>
)
}
并在ItemsPage.tsx
使用useMenuStore
得到的setVisible
来使得TopMenu
的组件接受传入的Props
的onclickMask
方法来控制TopMenu
的显示与否,详情代码见链接。
给菜单添加滑动动画
修改TopMenu
组件,使其直接收Props
值为visible
,并使用useSpring
添加上动画,并设置了动画样式。
maskStyle
(根据visible
来设置opacity
的变化,使得maskVisible
为真为假)menuStyles
(用于菜单栏的显示的时候出现,关闭的时候向左侧滑出)style
(用于遮罩层的动画样式,当maskVisible
为真时展示,假隐藏)
将style
和menuStyles
动画样式放入<animated.div></animated.div>
中,详细代码见链接。
tsx
import { animated, useSpring } from 'react-spring'
import React, { useState } from 'react'
import { CurrentUser } from './TopMenu/CurrentUser'
import { Menu } from './TopMenu/Menu'
interface Props {
onClickMask?: () => void
visible: boolean
}
export const TopMenu: React.FC<Props> = (props) => {
const { onClickMask, visible } = props
const [maskVisible, setMaskVisible] = useState(false)
const maskStyles = useSpring({
opacity: visible ? 1 : 0,
onStart: ({ value }) => {
if (value.opacity < 0.1)
setMaskVisible(true)
},
onRest: ({ value }) => {
if (value.opacity < 0.1)
setMaskVisible(false)
}
})
const menuStyles = useSpring({
opacity: visible ? 1 : 0,
transform: visible ? 'translateX(0%)' : 'translateX(-100%)',
})
const style = {
...maskStyles,
visibility: (maskVisible ? 'visible' : 'hidden') as 'visible' | 'hidden'
}
return (
<>
<animated.div fixed top-0 left-0 w="100%" h="100%" className="bg-black:75"
style={style}
z="[calc(var(--z-menu)-1)]" onClick={onClickMask}
/>
<animated.div fixed top-0 left-0 w="70vw" max-w-20em h-screen flex flex-col
style={menuStyles}
z="[var(--z-menu)]">
<CurrentUser className="grow-0 shrink-0" />
<Menu className="grow-1 shrink-1" />
</animated.div>
</>
)
}
给菜单添加跳转动作
首先在router.tsx
文件中添加要添加的路由,如下所示,详细代码见链接。
tsx
{ path: '/items', element: <ItemsPage /> },
{ path: '/sign_in', element: <div>sign in</div> },
{ path: '/chart', element: <div>图表</div> },
{ path: '/export', element: <div>敬请期待</div> },
{ path: '/tags', element: <div>标签</div> },
{ path: '/noty', element: <div>敬请期待</div> }
修改CurrentUser.tsx
组件,点击未登录的时候可以跳转到登录界面。
tsx
import { Link } from 'react-router-dom'
interface Props {
className?: string
}
export const CurrentUser: React.FC<Props> = ({ className }) => {
return (
<Link to='/sign_in' block className={className} bg="#5C33BE" text-white w="100%" pt-32px pb-44px
px-16px>
<h2 text-24px>未登录用户</h2>
<div text="#CEA1FF">点击这里登录</div>
</Link>
)
}
修改Menu.tsx
组件,点击菜单栏的时候跳转至相对应的界面。
tsx
import { NavLink } from 'react-router-dom'
import styled from 'styled-components'
import { Icon } from '../Icon'
interface Props {
className?: string
}
const MyIcon = styled(Icon)`
width: 32px; height: 32px; margin-right: 16px;
`
const items = [
{ key: 'chart', icon: 'chart', text: '统计图表', to: '/chart' },
{ key: 'export', icon: 'export', text: '导出数据', to: '/export' },
{ key: 'tags', icon: 'category', text: '自定义标签', to: '/tags' },
{ key: 'noty', icon: 'noty', text: '记账提醒', to: '/noty' },
]
export const Menu: React.FC<Props> = ({ className }) => {
return (
<ul className={className} bg-white text-20px py-16px >
{items.map(item =>
<li key={item.key}>
<NavLink flex items-center px-16px py-8px mb-4px to={item.to}>
<MyIcon name={item.icon} />{item.text}
</NavLink>
</li>
)}
</ul>
)
}
修复跳转后遮罩层关闭的bug
这是因为遮罩层的设置的maskVisible
每次都是false
,导致遮罩层直接不显示,并没有维持点击离开菜单来之前的状态来设定。
详细代码见链接。