React之布局菜单

这个教程的最终效果如下:
在这里插入图片描述
特点:

  • 支持小红点,
  • 支持菜单互斥,
  • 支持消息数量显示、
  • 支持暗黑暗模式
    废话少说,下面开撸

环境设置

基于这篇文章,你应该首先已经了解了如何创建react项目,用create-react-app也好,用vite也罢。首先你要会创建项目。我的组件UI采用的是MUI, 所以要安装MUI,当然你也可以使用其它的UI组件。当你学习完这个小节并且充分理解后,你就可以随心所欲的做出修改。

安装环境
## npm安装方式
## MUI
npm install @mui/material @emotion/react @emotion/styled
## 字体
npm install @fontsource/roboto
## 图标
npm install @mui/icons-material
## 动画
npm install react-transition-group --save

## yarn安装方式
yarn add @mui/material @emotion/react @emotion/styled
yarn add @fontsource/roboto
yarn add @mui/icons-material
yarn add react-transition-group

布局样式部份使用了bootstrap中的布局样式。所以还要导入bootstrap的样式。

npm i bootstrap@5.3.2

或者你直接把bootstrap布局包放到项目文件下直接引用也是一样的。其实我就是这么干的。
我写了一个CssBaseLine组件,把要引用的样式在这个组件里引用进来就OK了。

import CssBaseline from '@mui/material/CssBaseline';
import '../SCSS/public.css';
import '../SCSS/components.css';
import '../SCSS/bootstrap5.3.0/css/bootstrap-utilities.min.css';
import '../SCSS/bootstrap5.3.0/css/bootstrap-grid.min.css';

export default function AdapterCss() {
  return <CssBaseline />
}

这样,你只要在你的App进口组件中引用这个CSSBaseline就OK了

数据准备

为了减少对第三方组件的依赖,这个部件的内部状态我采用useReact来进行管理。这省去了写一大堆代码的麻烦。
菜单的配置当然越简单越好,能用Json的格式最好,万一你想把配置信息放在服务器上呢,这就方便许多了。我们以JSON数组的方式来进行配置管理。菜单最多可以配置二级菜单。也就是二级菜单以上的菜单就没必要支持了,如果你有二级以的需要,通过这个篇幅学习以后我想你弄个三级四级的支持也不是什么难事。

示例

const sideMenuDataTest = [
    { id: "init", title: "系统初始化", icon: DataUsageIcon },
    { id: "management", title: "用户管理", icon: GroupAddIcon },

    {
        id: "userMsg", title: "角色管理", icon: PersonIcon, children: [
            { id: "", title: "权限管理", icon: VerifiedUserIcon },
            { id: "pwdMsg", title: "密码管理", icon: PasswordIcon },
            { id: "keyMsg", title: "私钥管理", icon: VpnKeyIcon },
            { id: "agentMsg", title: "权限管理", icon: HealthAndSafetyIcon },
        ]
    },

    { id: "advMsg", title: "广告管理", icon: FeaturedVideoIcon },
    { id: "plyMsg", title: "评论管理", icon: ReplyAllIcon },

    {
        id: "title", title: "文章管理", icon: null, children: [
            { id: "caogaoMsg", title: "草稿件", icon: null },
            { id: "newFile", title: "新建文章", icon: null },
            { id: "firstMsg", title: "置顶管理", icon: null },
            { id: "recMsg", title: "推荐管理", icon: null },
            { id: "classMsg", title: "类型管理", icon: null },
            { id: "emailMsg", title: "邮箱管理", icon: null },
        ]
    },

    { id: "system", title: "系统设置", icon: null },
    { id: "userCenter", title: "个人中心", icon: null }
];

通过对上面的数据分析,我想你应该不难看出其数据结构的逻辑。有二级菜单的就包含children项,反之则没有,就这么简单。

状态管理器

这么个相对较复杂组件,还要有高度自定义的特性,我们肯定不能直接封装在一个组件文件里面,肯定要解耦。那么各级组件的状态的流通就直接关系到封装的成败及性能高低了。通过启篇的Gif图可以看到,菜单组成可能分成以下几个大的部分:
1. 主菜单头,里面包含logo 及 项目标题
2. 一菜单栏,包含功能图标及菜单功能标题
3. 二级菜单栏,也称组菜单。含有子菜单
4. 收缩按钮
以上各个组件里还是若干小组件,而菜单的实时状态也是要求无障碍贯穿的。这就需要一系列的状态提供器(provider),但这之前我们要理解useReducer的相关使用方法:

使用Reducer

useReducer是一个 React Hook 语法如下:

const [state, dispatch] = useReducer(reducer, initialArg, init?)

具体的用法及介绍请参考我的下一篇技术文章:React之useReducer

定义SideMenuProvider.jsx文件

import { useReducer, createContext, useState } from 'react';
import React from 'react';

/**
 * 获取菜单项的id集合, 菜单项的ID必须唯一,用于初始化菜单项的徽章,本菜单的每个Item都有一个id属性,用于唯一标识菜单项。
 * @param menuConfig 
 * @returns 
 */
function getIdSet(menuConfig){
    let ids = {};
    menuConfig.forEach((element) => {
        const name = element.id;
        ids = { ...ids, [name]: 0 };

        if (element.children) {
            const children = element.children;
            children.forEach(el => {
                const name1 = el.id;
                ids = { ...ids, [name1]: 0 };
            })
        }
    });

    return ids;
}

//菜单的内部状态的初始值,用react的reducer来管理, 用context来向子组件传递通信。
const initState = {
    activeItemId: null, //当点击一个菜单项时记录活动菜单项
    hoverItemId: null, //当点击一个菜单项组标题时,记录打开的GroupMenu的名称。
    open: true, //菜单项的展开模式,true为展开,false为折叠
    showDivider: true, //菜单项的分割线模式,true为显示,false为不显示
}

const reducer = (state, action) => {
    return {
        ...state,
        ...action
    }
}

export const SideMenuState = createContext(initState); //菜单的内部状态
export const SideMenuBadge = createContext(null); //菜单的徽章
export const DispatchMenuState = createContext(null); //菜单的内部状态的更新函数
export const DispatchMenuBadge = createContext(null); //菜单的徽章的更新函数
export const SideMenuData = createContext([]); //菜单的数据

/**
 * 菜单的上下文Context
 * @param children 
 * @param menuData 
 * @returns 
 */
function SideMenuProvider({ children, menuData }) {
    const [badge, updateBadge] = useState(getIdSet(menuData));
    const [menuState, updateMenuState] = useReducer(reducer, initState);

    const updateBadgeHandler = (id, count) => {
        updateBadge((state) => {
            return {
                ...state,
                [id]: count
            }
        })
    }
    return (
        <SideMenuState.Provider value={ menuState }>
            <SideMenuBadge.Provider value={badge}>
                <DispatchMenuState.Provider value={updateMenuState}>
                    <DispatchMenuBadge.Provider value={updateBadgeHandler}>
                        <SideMenuData.Provider value={menuData}>
                            {
                                children
                            }
                        </SideMenuData.Provider>
                    </DispatchMenuBadge.Provider>
                </DispatchMenuState.Provider>
            </SideMenuBadge.Provider>
        </SideMenuState.Provider>
    )
}

export default SideMenuProvider;

规定每个菜单项的id必须唯一,这个应该不难理解吧。
getIdSet()函数的功能是根据配置菜单数据返回所有菜单项的ID集合,用于确定你单击了哪一项菜单。
initState是reducer的初始状态。 其内部数据项在上面代码的备注中我已经写的很清楚了。
状态定义好了,那么Hook当然也要提供上,要不然有什么意义呢。

定义_SMenuHooks.jsx文件

import { useContext } from 'react';
import { DispatchMenuBadge, DispatchMenuState, SideMenuBadge, SideMenuData, SideMenuState } from './SideMenuProvider'; 

//获取边栏菜单的状态
export function useSideMenuState() {
    return useContext(SideMenuState);
}

//获取边栏菜单的小红点状态
export function useSideMenuBadge() {
    return useContext(SideMenuBadge);
}

//更新边栏菜单小红点的工具,用法:
// const update = useSideMenuBadgeUpdate();
// update("menuItemId", 50)
export function useSideMenuBadgeUpdate() {
    return useContext(DispatchMenuBadge);
}

//更新边栏菜单工具
export function useSideMenuStateUpdate() {
    return useContext(DispatchMenuState);
}

//获取菜单配置项
export function useSideMenuData() {
    return useContext(SideMenuData);
}

现在状态有了,引用状态的Hooks也有了,其它的就是如何定义组件了,是不是容易了许多。

菜单头的定义

直接上代码:_SideMenuHeader.jsx

import Box from '@mui/system/Box';
import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/Typography';
import Stack from '@mui/system/Stack';
import { useSideMenuState } from './_SMenuHooks';

//菜单头
const SideMenuHeader = ({
    logo, //图标
    title, //标题
    onClick //单击事件
}) => {
    const { open } = useSideMenuState();
    const clickEvent = () => {
        onClick && onClick();
    }
    return (
        <Box
            onClick={clickEvent}
            className="p-3">
            <Stack
                spacing={2}
                direction={"row"}
                justifyContent="start"
                alignItems={"center"}
                className="w-100"
            >
                <Avatar
                    sx={{
                        width: 35,
                        height: 35,
                        cursor: "pointer",
                        transition: '0.2s',
                        transform: open ? 'scale(1)' : 'scale(1.2)',
                    }}
                    src={logo}
                    variant="rounded"
                    alt={title}
                >
                    {
                        title && title.substring(0, 1).toUpperCase()
                    }
                </Avatar>

                <Typography className="text-truncate" variant="h5" sx={{pl: 0.5}} > {title} </Typography>
            </Stack>
        </Box>
    )
};

export default SideMenuHeader;
菜单项

_SideMenuItem.jsx

import PropTypes from 'prop-types';
import Tooltip from '@mui/material/Tooltip';
import Badge from '@mui/material/Badge';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import Avatar from '@mui/material/Avatar';
import SvgIcon from '@mui/material/SvgIcon';
import { useSideMenuBadge, useSideMenuState, useSideMenuStateUpdate } from './_SMenuHooks';

/**
 * 主菜单项组件
 * @param title: 菜单项标题
 * @param id: 菜单项ID
 * @param icon: 菜单项图标
 * @param onClick: 菜单项单击事件 
 * @returns 
 */
const SideMenuItem = ({
    title, 
    id,
    icon = null,
    onClick,
}) => {
    const {activeItemId, open} = useSideMenuState();
    const badgeCount = useSideMenuBadge();
    const updateMenuState = useSideMenuStateUpdate();

    //单击事件
    const itemClickeEvent = () => {
        updateMenuState({ activeItemId: id });
        onClick(id, title, [id], [title]);
    }
 
    return (
        <ListItemButton
            selected={ activeItemId == id }
            onClick={itemClickeEvent}
        >
            <Tooltip title={open ? null : title} arrow placement="right">
                <Badge badgeContent={badgeCount[id]} color="error">
                    <ListItemIcon
                        sx={{
                            '& svg': {
                                transition: '0.2s',
                                transform: open ? 'scale(1)' : 'scale(1.2)',
                            },

                            '&:hover, &:focus': {
                                '& svg:first-of-type': {
                                    transform: open ? 'scale(1)' : 'scale(1.3)',
                                }
                            },
                        }}>
                        {
                            icon == null ? 
                                <Avatar
                                    sx={{
                                        width: 30,
                                        height: 30,
                                        fontSize: 18,
                                        transition: '0.2s',
                                        transform: open ? 'scale(1)' : 'scale(1.2)'
                                    }}
                                    variant="rounded">
                                    {title.substring(0, 1).toUpperCase()}
                                </Avatar> :
                                <SvgIcon component={icon} />
                        }
                    </ListItemIcon>
                </Badge>
            </Tooltip>

            <ListItemText primary={title} />
        </ListItemButton>
    );
};
export default SideMenuItem;
子菜单项

_SideMenuSubItem.jsx

import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import Avatar from '@mui/material/Avatar';
import Tooltip from '@mui/material/Tooltip';
import Badge from '@mui/material/Badge';
import SvgIcon from '@mui/material/SvgIcon';
import CssBaseline from '@mui/material/CssBaseline';

import { useSideMenuBadge, useSideMenuState, useSideMenuStateUpdate } from './_SMenuHooks';

/**
 * 子菜单项组件
 * @param icon: 菜单项图标
 * @param title: 菜单项标题
 * @param id: 菜单项ID
 * @param groupId: 菜单项组ID
 * @param groupTitle: 菜单项组标题
 * @param onClick: 菜单项单击事件 
 * @returns 
 */
function SideMenuSubItem({
    icon = null,
    title,
    id,
    groupId,
    groupTitle,
    onClick
}) {
    const { activeItemId, open } = useSideMenuState();
    const updateMenuState = useSideMenuStateUpdate();
    const badgeCount = useSideMenuBadge();

    const handleClick = () => {
        updateMenuState({ activeItemId: id });
        onClick(id, title, [groupId, id], [groupTitle, title])
    };

    return (
        <ListItemButton
            onClick={handleClick}
            selected={ activeItemId == id }
            sx={{
                transition: "padding 0.3s",
                pl: open ? 5 : 2.5,
            }}>
            <CssBaseline />
            <Tooltip title={open ? null : title} arrow placement="right">
                <Badge badgeContent={badgeCount[id]} color="error">
                    <ListItemIcon
                        sx={{
                            '& svg': {
                                transition: '0.2s',
                                transform: open ? 'scale(1)' : 'scale(1.2)',
                            },

                            '&:hover, &:focus': {
                                '& svg:first-of-type': {
                                    transform: open ? 'scale(1)' : 'scale(1.3)',
                                }
                            },
                        }}>
                        {
                            icon == null ?
                                <Avatar
                                    sx={{
                                        width: 24,
                                        height: 24,
                                        fontSize: 16,
                                        transition: '0.2s',
                                        transform: open ? 'scale(1)' : 'scale(1.2)',
                                    }}
                                variant="rounded"
                                > {title.substring(0, 1).toUpperCase()} </Avatar> :
                            <SvgIcon component={icon} sx={{ fontSize: 16 }} />
                            
                        }
                    </ListItemIcon>
                </Badge>
            </Tooltip>
            <ListItemText
                primary={
                    <Typography
                        sx={{ display: 'inline' }}
                        component="span"
                        variant="body1"
                        color="text.secondary"
                    >
                        { title }
                    </Typography>
                }
            />
        </ListItemButton>
    );
}

export default SideMenuSubItem;
菜单组项

_SideMenuGroup.jsx

import React from 'react';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Collapse from '@mui/material/Collapse';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import Avatar from '@mui/material/Avatar';
import Badge from '@mui/material/Badge';
import Tooltip from '@mui/material/Tooltip';
import SvgIcon from '@mui/material/SvgIcon';

import { useSideMenuState, useSideMenuStateUpdate, useSideMenuBadge } from './_SMenuHooks';
import SMenuSubItem from './_SideMenuSubItem';

/**
 * 含有子菜单的菜单项
 * @param props 
 * @returns 
 */
function SideMenuGroup({
    id, //菜单项的ID名称
    icon = null, //图标
    title, //标题
    childrenData, //子菜单 
    onClick, //单击事件
}) {
    const { hoverItemId, open } = useSideMenuState();
    const updateMenuState = useSideMenuStateUpdate();
    const badgeCount = useSideMenuBadge();
    const handleClick = () => {
        updateMenuState({hoverItemId: hoverItemId === id ? null : id})
    };

    return (
        <React.Fragment>
            <ListItemButton onClick={handleClick}>
                <Tooltip title={open ? null : title} arrow placement="right">
                    <Badge badgeContent={badgeCount[id]} color="error">
                        <ListItemIcon
                            sx={{
                                '& svg': {
                                    transition: '0.2s',
                                    transform: open ? 'scale(1)' : 'scale(1.2)',
                                },

                                '&:hover, &:focus': {
                                    '& svg:first-of-type': {
                                        transform: open ? 'scale(1)' : 'scale(1.3)',
                                    }
                                },
                            }}>
                            {
                                icon == null ?
                                    <Avatar
                                        sx={{
                                            width: 30,
                                            height: 30,
                                            fontSize: 18,
                                            transition: '0.2s',
                                            transform: open ? 'scale(1)' : 'scale(1.2)'
                                        }}
                                    variant="rounded"
                                    >
                                        {title.substring(0, 1).toUpperCase()}
                                    </Avatar> :
                                    <SvgIcon component={icon} />
                            }
                        </ListItemIcon>
                    </Badge>
                </Tooltip>

                <ListItemText primary={title} />
                {hoverItemId === id ? <ExpandLess /> : <ExpandMore />}
            </ListItemButton>
            <Collapse in={ hoverItemId === id } timeout="auto" unmountOnExit>
                <List component="div" dense={true} disablePadding>
                    {
                        childrenData === undefined ? null :
                        childrenData.map(function (itemData, index) {
                            return <SMenuSubItem
                                icon = { itemData.icon }
                                title = { itemData.title }
                                id = {itemData.id}
                                groupId = {id}
                                groupTitle={title}
                                onClick={onClick}
                                key={index} />
                        })
                    }
                </List>
            </Collapse>
        </React.Fragment>
    );
}

export default SideMenuGroup;
菜单收缩按钮

_SToggleButton.jsx

import IconButton from '@mui/material/IconButton';
import { useSideMenuState, useSideMenuStateUpdate } from './_SMenuHooks';

/**
 * 菜单的展开/收起按钮
 * @param {*} param0 
 * @returns 
 */
function SToggleButton({icon}) {
    const menuState = useSideMenuState();
    const updateMenuState = useSideMenuStateUpdate();
    const clickHandler = () => {
        updateMenuState({ open: !menuState.open})
    }
    return (
        <IconButton onClick={clickHandler}>
            { icon }
        </IconButton>
    )
}

export default SToggleButton;

一口气所有组件就撸完了。也不是太难对不对。接下来就是组装了。把上面的状态及组件有机的组件起来就完美了。注意看我的手势,我是有手势的。

菜单组件

SideMenu.jsx

import { ReactNode } from 'react';
import Box from '@mui/system/Box';
import SideMenuItem from './_SideMenuItem';
import Divider from '@mui/material/Divider';
import SideMenuGroup from './_SideMenuGroup';
import { useSideMenuData, useSideMenuState } from './_SMenuHooks';
import { List } from '@mui/material';
import Paper from '@mui/material/Paper';

/**
 * 菜单的主体组件
 * @returns 
 */
function SideMenu({
    header,
    footer,
    onClick
}) {
    const menuData = useSideMenuData();
    const { open } = useSideMenuState();
    const openWidth = 300;
    const minWidth = 65;
    return (
        <Paper
            className="d-flex overflow-hidden h-100"
            elevation={1}
            sx={{
                transition: "width 0.3s",
                width: open ? openWidth : minWidth,
            }}
        >        
            <Box className='d-flex flex-column'>
                {
                    header == null ?
                        null : 
                        <>
                            { header }
                            <Divider />
                        </>
                }

                <Box sx={{ flex: 1, overflowY: "auto",  overflowX:"hidden", width: open ? openWidth : minWidth}}>
                    <List sx={{width: openWidth}}>                    
                        { 
                            menuData.map((item, index) => {
                                const subItemsData = item.children || null;
                                if (subItemsData == null) {
                                    return <SideMenuItem
                                        id={item.id}
                                        title={item.title}
                                        icon={item.icon}
                                        onClick={onClick}
                                        key={index}
                                    />
                                }

                                return <SideMenuGroup
                                    icon={item.icon}
                                    id={item.id}
                                    title={item.title}
                                    childrenData={item.children}
                                    onClick={onClick}
                                    key={index} />
                            })
                        }
                    </List>
                </Box>

                {
                    footer == null ?
                        null :
                        <>
                            <Divider />
                            {footer}
                        </>
                }
            </Box>
        </Paper>
    );
}

export default SideMenu;

好了,我们刚撸了一个相当棒的布局菜单。撸得我一手的油。怎么用呢,简单:

菜单项测试

SideMenuTest.jsx

import DataUsageIcon from '@mui/icons-material/DataUsage';
import PersonIcon from '@mui/icons-material/Person';
import GroupAddIcon from '@mui/icons-material/GroupAdd';
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
import FeaturedVideoIcon from '@mui/icons-material/FeaturedVideo';
import PasswordIcon from '@mui/icons-material/Password';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
import HealthAndSafetyIcon from '@mui/icons-material/HealthAndSafety';
import ReplyAllIcon from '@mui/icons-material/ReplyAll';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import Box from '@mui/material/Box';
import SideMenuHeader from "../framework-kakaer/SMenu/_SideMenuHeader";
import SideMenu from "../framework-kakaer/SMenu/SideMenu";
import SideMenuProvider from "../framework-kakaer/SMenu/SideMenuProvider";
import SToggleButton from '../framework-kakaer/SMenu/_SToggleButton';

//菜单的测试数据
const sideMenuDataTest = [
    { id: "init", title: "系统初始化", icon: DataUsageIcon },
    { id: "management", title: "用户管理", icon: GroupAddIcon },

    {
        id: "userMsg", title: "角色管理", icon: PersonIcon, children: [
            { id: "", title: "权限管理", icon: VerifiedUserIcon },
            { id: "pwdMsg", title: "密码管理", icon: PasswordIcon },
            { id: "keyMsg", title: "私钥管理", icon: VpnKeyIcon },
            { id: "agentMsg", title: "权限管理", icon: HealthAndSafetyIcon },
        ]
    },

    { id: "advMsg", title: "广告管理", icon: FeaturedVideoIcon },
    { id: "plyMsg", title: "评论管理", icon: ReplyAllIcon },

    {
        id: "title", title: "文章管理", icon: null, children: [
            { id: "caogaoMsg", title: "草稿件", icon: null },
            { id: "newFile", title: "新建文章", icon: null },
            { id: "firstMsg", title: "置顶管理", icon: null },
            { id: "recMsg", title: "推荐管理", icon: null },
            { id: "classMsg", title: "类型管理", icon: null },
            { id: "emailMsg", title: "邮箱管理", icon: null },
        ]
    },

    { id: "system", title: "系统设置", icon: null },
    { id: "userCenter", title: "个人中心", icon: null }
];


function SideMenuTest() {
    const onClickHandler = (id, title, idPath, titlePath) => {
        console.log("ClickedItem => ", idPath);
    }

    return (
        <SideMenuProvider menuData={sideMenuDataTest}>
            <Box
                className='d-flex overflow-hidden position-absolute w-100 h-100 p-0 m-0'
                sx={{ top: 0, left: 0, bottom: 0, right: 0 }}>
                <Box className='h-100'>
                    <SideMenu
                        header={<SideMenuHeader title="侧边菜单测试系统" />}
                        onClick={onClickHandler} />
                </Box>

                <Box className='d-flex overflow-hidden position-relative flex-grow-1'>
                    <Box className="d-flex flex-column overflow-auto p-0 m-0 position-relative flex-grow-1 flex-nowrap">
                        <Box className="p-0 w-100 h-100">
                            <SToggleButton icon={<VerticalSplitIcon />} />
                        </Box>
                    </Box>
                </Box>
            </Box>

        </SideMenuProvider>
    )
}

export default SideMenuTest;

这下总会了吧。什么,还不会?不知道怎么进?我擦!!!我这都一件不剩了,你还不知道下一步:

入口

_SideMenuItem

// import SideMenuProvider from "../kakaer/SMenu/SideMenuProvider";
import SideMenuTest from "./SideMenuTest";

function App() {
    return <SideMenuTest />
}

export default App;

完美!!
大家还可以加入一个暗模式功能,想想看,构思一下怎么实现,看下图:这个就当是留给自己的作业吧。
带有暗模式的布局界面

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码蚁先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值