Umi4 从零开始实现动态路由、动态菜单_umi 动态路由_绿胡子大叔的博客-CSDN博客


本站和网页 https://blog.csdn.net/m0_52761633/article/details/127167701 的作者无关,不对其内容负责。快照谨为网络故障时之索引,不代表被搜索网站的即时页面。

Umi4 从零开始实现动态路由、动态菜单_umi 动态路由_绿胡子大叔的博客-CSDN博客
Umi4 从零开始实现动态路由、动态菜单
绿胡子大叔
已于 2023-02-25 23:35:15 修改
3609
收藏
14
文章标签:
前端
react.js
typescript
于 2022-10-04 22:56:28 首次发布
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_52761633/article/details/127167701
版权
Umi4 从零开始实现动态路由、动态菜单
🍕 前言🍔 前期准备📃 数据表🤗 Mock数据🔗 定义类型
🎈 开始🎃 获取路由信息🧵 patchRoutes({ routes, routeComponents})📸 生成动态路由所需的数据formattedRoutePathroutePathcomponentPathfilePath
🍖 生成动态路由数据及组件😋 完成
✨ 踩坑
🍕 前言
近期在写 Umi4 的练习项目,计划实现一个从服务器获取路由信息并动态生成前端路由和导航菜单的功能。本文记录了相关知识、思路以及开发过程中踩到的坑。
🍔 前期准备
📃 数据表
后端同学可以参考
CREATE TABLE `menus` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`menu_id` VARCHAR(128) NOT NULL,
`parent_id` VARCHAR(128) NULL DEFAULT NULL,
`enable` TINYINT(1) NOT NULL,
`name` VARCHAR(64) NOT NULL,
`sort` SMALLINT(5) NOT NULL DEFAULT '0',
`path` VARCHAR(512) NOT NULL,
`direct` TINYINT(1) NULL DEFAULT '0',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `menu_id` (`menu_id`) USING BTREE,
UNIQUE INDEX `sort` (`sort`) USING BTREE,
UNIQUE INDEX `path` (`path`) USING BTREE,
INDEX `FK_menus_menus` (`parent_id`) USING BTREE,
CONSTRAINT `FK_menus_menus` FOREIGN KEY (`parent_id`) REFERENCES `menus` (`menu_id`) ON UPDATE CASCADE ON DELETE CASCADE
COLLATE='utf8mb4_0900_ai_ci'
ENGINE=InnoDB
id 记录ID
menu_id 菜单的唯一ID
parent_id 父级菜单的ID
enable 是否启用菜单(后端或查询时进行过滤)
name 路由名称、菜单名称、页面标题
sort 菜单排序(后端或查询时进行排序)(xxxx 代表:一级菜单序号 子菜单序号)
path 前端页面访问路径(同location.pathname)
direct 是否为直接访问的菜单(即不存在子菜单和子路由,为顶级项目)
created_at 记录创建时间
🤗 Mock数据
// ./mock/dynamicRoutes.ts
export default {
'POST /api/system/routes': {
"code": 200,
"msg": "请求成功",
"data": [
"id": 1,
"menuId": "dashboard",
"parentId": "",
"enable": true,
"name": "仪表盘",
"sort": 1000,
"path": "/dashboard",
"direct": true,
"createdAt": "1992-08-17 07:29:03"
},
"id": 2,
"menuId": "system_management",
"parentId": "",
"enable": true,
"name": "系统管理",
"sort": 2000,
"path": "/system",
"direct": false,
"createdAt": "2011-01-21 09:25:49"
},
"id": 3,
"menuId": "user_management",
"parentId": "system_management",
"enable": true,
"name": "用户管理",
"sort": 2001,
"path": "/system/user",
"direct": false,
"createdAt": "1986-06-03 02:38:12"
},
"id": 4,
"menuId": "role_management",
"parentId": "system_management",
"enable": true,
"name": "角色管理",
"sort": 2002,
"path": "/system/role",
"direct": false,
"createdAt": "1986-06-03 02:38:12"
},
"id": 5,
"menuId": "permission_management",
"parentId": "system_management",
"enable": true,
"name": "权限管理",
"sort": 2003,
"path": "/system/permission",
"direct": false,
"createdAt": "1986-06-03 02:38:12"
},
"id": 6,
"menuId": "app_management",
"parentId": "system_management",
"enable": true,
"name": "应用管理",
"sort": 2004,
"path": "/system/app",
"direct": false,
"createdAt": "1986-06-03 02:38:12"
🔗 定义类型
// @/utils/dynamicRoutes/typing.d.ts
import type { LazyExoticComponent, ComponentType } from 'react';
import type { Outlet } from '@umijs/max';
declare namespace DynamicRoutes {
// 后端返回的路由数据为 RouteRaw[]
interface RouteRaw {
menuId: string;
parentId: string;
enable: boolean;
name: string;
sort: number;
path: string;
direct: boolean;
createdAt: string;
// 前端根据后端返回数据生成的路由数据
interface Route {
id: string;
parentId: 'ant-design-pro-layout' | string;
name: string;
path: string;
file?: string;
children?: Route[];
// 前端根据后端返回数据生成的React.lazy懒加载组件或Outlet(一级路由)
type RouteComponent = LazyExoticComponent<ComponentType<any>> | typeof Outlet;
// patchRoutes 函数的参数可以解构出 { routes, routeComponents }
// 此类型用于 Object.assign(routes, parsedRoutes),合并路由数据
interface ParsedRoutes {
[key: number]: Route;
// 此类型用于 Object.assign(routeComponents, parsedRoutes),合并路由组件
interface ParsedRouteComponent {
[key: number]: RouteComponent;
// parseRoutes 函数的返回值
interface ParseRoutesReturnType {
routes: DynamicRoutes.ParsedRoutes;
routeComponents: DynamicRoutes.ParsedRouteComponent;
// ./typing.d.ts
import type { DynamicRoutes } from '@/utils/dynamicRoutes/typing';
import '@umijs/max/typings';
declare global {
interface Window {
dynamicRoutes: DynamicRoutes.RouteRaw[];
🎈 开始
🎃 获取路由信息
// @/global.ts
import { message } from 'antd';
try {
const { data: routesData } = await fetch('/api/system/routes', {
method: 'POST',
}).then((res) => res.json());
if (routesData) {
window.dynamicRoutes = routesData;
} catch {
message.error('路由加载失败');
export {};
在 umi v4.0.24中 patchRoutes方法早于 render方法执行,所以 umi v3中在 render函数中获取路由数据的方法目前不可用。不清楚这个行为属于bug还是 umi 4的特性
我在Github提的issue: [Bug] umi 4 运行时配置中 patchRoutes 早于 render 执行 #9486
经过测试,global.tsx中的代码早于 patchRoutes执行,所以在此文件中获取数据。
由于执行 global.tsx时,app.tsx中的运行时响应/请求拦截器还未生效,使用 @umijs/max提供的 request会报错,所以这里使用 fetch获取数据,并写入 window.dynamicRoutes。
🧵 patchRoutes({ routes, routeComponents})
此函数为 umi v4提供的合并路由数据的方法,其参数可以解构出 routes、routeCompoents对象。routes对象为打平到对象中的路由数据(类型详见DynamicRoutes.Route),routeComponents对象存储routes对象中对应(属性名对应)的组件(类型详见DynamicRoutes.RouteComponent)
动态更新路由需要直接修改由参数解构出的 routes和 routeComponents对象,使用 Object.assign(routes, newRoutes)将他们与新数据合并
📸 生成动态路由所需的数据
以下三处需要使用DynamicRoutes.RouteRaw.path经过格式化后的路径:
DynamicRoutes.Route.file在路由信息中记录组件文件位置DynamciRoutes.Route.path在路由信息中记录组件的路由路径React.lazy(() => import(path))懒加载组件所需的文件路径
要生成的路径:
formattedRoutePathroutePathcomponentPathfilePath
formattedRoutePath
// @/utils/dynamicRoutes/index.ts
export function formatRoutePath(path: string) {
const words = path.replace(/^\//, '').split(/(?<=\w+)\//); // 提取路径单词
return `/${words
.map((word: string) =>
word.toLowerCase().replace(word[0], word[0].toUpperCase()),
.join('/')}`;
约定使用@/pages/Aaaa/pages/Bbbb文件夹结构存储组件
DynamicRoutes.RouteRaw.path中,路径字母大小写可能是不同的,首先使用此方法将大小写不一的路径转换为单词首字母大写的路径,供其他方法进行下一步转换。
转换前:/SYSTEM/user
转换后:/System/User
routePath
// @/utils/dynamicRoutes/index.ts
export function generateRoutePath(path: string) {
return path.toLowerCase();
此函数将使用formatRoutePath转换为全小写字母的路径并提供给DynamciRoutes.Route.path这个函数根据实际业务需求修改,不必和我一样
转换前:/System/User
转换后:/system/user
componentPath
// @/utils/dynamicRoutes/index.ts
export function generateComponentPath(path: string) {
const words = path.replace(/^\//, '').split(/(?<=\w+)\//); // 提取路径单词
return `${words.join('/pages/')}/index`;
此函数生成React.lazy(() => import(path))所需路径,用于懒加载组件。但此方法生成的不是完整组件路径,由于webpack alias处理机制,需要在() => import(path)的参数中编写一个模板字符串 @/pages/${componentPath},直接传递将导致@别名失效无法正常加载组件
// 转换前:/System/User
// 转换后:/System/pages/User/index
React.lazy(() => import(`@/pages/${componentPath}`)) // 使用时
filePath
// @/utils/dynamicRoutes/index.ts
export function generateFilePath(path: string) {
const words = path.replace(/^\//, '').split(/(?<=\w+)\//);
return `@/pages/${words.join('/pages/')}/index.tsx`;
此函数生成DynamicRoutes.Route.file所需的完整组件路径
转换前:/System/User
转换后:@/pages/System/pages/User/index.tsx
🍖 生成动态路由数据及组件
首先,在app.tsx中生成patchRoutes方法,并获取已在.umirc.ts中配置的路由数目
// @/app.tsx
// @ts-ignore
export function patchRoutes({ routes, routeComponents }) {
if (window.dynamicRoutes) {
// 存在 & 成功获取动态路由数据
const currentRouteIndex = Object.keys(routes).length; // 获取已在.umirc.ts 中配置的路由数目
const parsedRoutes = parseRoutes(window.dynamicRoutes, currentRouteIndex);
传入parseRoutes函数,生成路由数据
// @/utils/dynamicRoutes/index.ts
import type { DynamicRoutes } from './typing';
import { lazy } from 'react';
import { Outlet } from '@umijs/max';
export function parseRoutes(
routesRaw: DynamicRoutes.RouteRaw[],
beginIdx: number,
): DynamicRoutes.ParseRoutesReturnType {
const routes: DynamicRoutes.ParsedRoutes = {}; // 转换后的路由信息
const routeComponents: DynamicRoutes.ParsedRouteComponent = {}; // 生成的React.lazy组件
const routeParentMap = new Map<string, number>(); // menuId 与路由记录在 routes 中的键 的映射。如:'role_management' -> 7
let currentIdx = beginIdx; // 当前处理的路由项的键。把 patchRoutes 传进来的 routes 看作一个数组,这里就是元素的下标。
routesRaw.forEach((route) => {
let effectiveRoute = true; // 当前处理中的路由是否有效
const formattedRoutePath = formatRoutePath(route.path); // 将服务器返回的路由路径中的单词转换为首字母大写其余小写
const routePath = generateRoutePath(formattedRoutePath); // 全小写的路由路径
const componentPath = generateComponentPath(formattedRoutePath); // 组件路径 不含 @/pages/
const filePath = generateFilePath(formattedRoutePath); // 路由信息中的组件文件路径
// 是否为直接显示(不含子路由)的路由记录,如:/home; /Dashboard
if (route.direct) {
// 生成路由信息
const tempRoute: DynamicRoutes.Route = {
id: currentIdx.toString(),
parentId: 'ant-design-pro-layout',
name: route.name,
path: routePath,
file: filePath,
};
// 存储路由信息
routes[currentIdx] = tempRoute;
// 生成组件
const tempComponent = lazy(() => import(`@/pages/${componentPath}`));
// 存储组件
routeComponents[currentIdx] = tempComponent;
} else {
// 判断是否非一级路由
if (!route.parentId) {
// 正在处理的项为一级路由
// 生成路由信息
const tempRoute: DynamicRoutes.Route = {
id: currentIdx.toString(),
parentId: 'ant-design-pro-layout',
name: route.name,
path: routePath,
};
// 存储路由信息
routes[currentIdx] = tempRoute;
// 一级路由没有它自己的页面,这里生成一个Outlet用于显示子路由页面
const tempComponent = Outlet;
// 存储Outlet
routeComponents[currentIdx] = tempComponent;
// 记录菜单ID与当前项下标的映射
routeParentMap.set(route.menuId, currentIdx);
} else {
// 非一级路由
// 获取父级路由ID
const realParentId = routeParentMap.get(route.parentId);
if (realParentId) {
// 生成路由信息
const tempRoute: DynamicRoutes.Route = {
id: currentIdx.toString(),
parentId: realParentId.toString(),
name: route.name,
path: routePath,
file: filePath,
};
// 存储路由信息
routes[currentIdx] = tempRoute;
// 生成组件
const tempComponent = lazy(() => import(`@/pages/${componentPath}`));
// 存储组件
routeComponents[currentIdx] = tempComponent;
} else {
// 找不到父级路由,路由无效,workingIdx不自增
effectiveRoute = false;
if (effectiveRoute) {
// 当路由有效时,将workingIdx加一
currentIdx += 1;
});
return {
routes,
routeComponents,
};
在app.tsx中合并处理后的路由数据
// @ts-ignore
export function patchRoutes({ routes, routeComponents }) {
if (window.dynamicRoutes) {
const currentRouteIndex = Object.keys(routes).length;
const parsedRoutes = parseRoutes(window.dynamicRoutes, currentRouteIndex);
Object.assign(routes, parsedRoutes.routes); // 参数传递的为引用类型,直接操作原对象,合并路由数据
Object.assign(routeComponents, parsedRoutes.routeComponents); // 合并组件
😋 完成
✨ 踩坑
目前需要在global.tsx中获取路由数据,因为patchRoutes发生于render之前patchRoutes的原始路由数据与新数据需要使用Object.assign合并,不能直接赋值使用React.lazy生成懒加载组件时,不能直接传入完整路径。传入完整路径使webpack无法处理alias,导致组件路径错误
关注博主即可阅读全文
绿胡子大叔
关注
关注
点赞
14
收藏
打赏
知道了
评论
Umi4 从零开始实现动态路由、动态菜单
近期在写 Umi4 的练习项目,计划实现一个从服务器获取路由信息并动态生成前端路由和导航菜单的功能。本文记录了相关知识、思路以及开发过程中踩到的坑。
复制链接
扫一扫
React路由鉴权的实现方法
11-30
前言 上一篇文章中有同学提到路由鉴权,由于...在正式开始 react 路由鉴权之前我们先看一下vue的路由鉴权是如何工作的: 一、vue之beforeEach路由鉴权 一般我们会相应的把路由表角色菜单配置在后端,当用户未通过页面菜
020 Umi@4 中如何实现动态菜单
梅花寺
07-26
1097
以上操作,看起来比较繁琐,但是如果你对各个概念都有了一定了解,那阅读起来就会很轻松,觉得逻辑非常的清晰。如果你有任何疑问,可以去看看前面的课程,也可以在评论区和我互动。你应该可以从我的行文内容看出来,我是没有任何“存稿”的,跟这个系列文章,有点类似半直播的方式。我觉得这比我自己“埋头苦干”,要有趣的多,也希望你会喜欢。...
评论 8
您还未登录,请先
登录
后发表或查看评论
react+umi+antd pro动态路由/动态菜单的配置
热门推荐
eisha2015的博客
03-15
1万+
公司项目需要配置动态菜单,即从后端获取权限菜单,然后显示在左边菜单栏里。而我们现在的菜单是从静态配置文件config/routes.js中获得的。我一开始试过用umi封装的方法patchRoutes和render这两个方法结合来改变routes.js中的routes:
routes.js中留下必要的几个路由:
export default [
path: '/user',
component: '@/layouts/UserLayout',
routes: [
umi学习(umi4)
最新发布
weixin_60968779的博客
03-03
635
umi学习
ant design pro v5 - 03 动态菜单 动态路由(配置路由 动态登录路由 登录菜单)
haiyabtx
10-17
1364
React + TypeScript + Ant Design Pro V5.2
开箱即用的中台前端/设计解决方案。动态菜单 动态路由 (配置路由+动态登录路由)
umi 实现动态路由(非动态菜单)
qq_38506368的博客
07-05
7400
背景
复杂的权限在前端不方便控制,将路由保存到后端,经过后端鉴权后返回相应的路由。
umi 版本: 2.x
注意
该方法适用于 umi 3.x,但是需要注意,patchRoutes 的参数与 2.x 不同。
方案
使用 umi 提供的 运行时配置 功能,结合 patchRoutes 和 render 接口,动态修改路由。
在项目根目录创建 app.js 文件,该文件为 umi 的运行时配置文件。
config 目录下以及 .umirc.js 文件都是编译时配置。
编写 patchRoutes
两个umijs/max项目使用微前端简单示例
LRQQHM的博客
12-20
517
a href=“http://localhost:8000/app1”>跳转到子路由本人使用umijs/max搭建项目(内置了qiankun插件)写子应用的id并且规定子应用打开的端口。package.json中。.umirc.ts中。.umirc.ts中。
Umi 4 快速搭建项目
qq_38629292的博客
08-02
3573
Umi4已于今年6月份发布,同时支持 Vite 和 Webpack 两种构建方式,对诟病已久的mufs做了优化,另外还支持vue。那我们就来上手搭建下吧。
Umi从服务端动态获取路由
魔仙女王的魔法杖
05-11
2996
Umi从服务端动态获取路由背景1、前提2、过程
背景
一开始我是在route.ts文件下将路由写死,需求方提出要从服务端动态获取左侧菜单,实现动态路由
1、前提
根据UmiJs官方文档中,我们可以看到运行时配置,提供了配置项,patchRoutes({ routes })和render(oldRender: Function)
patchRoutes({ routes }) ,官方demo中介绍的清清楚楚,在这我们可以获取已经配置好的路由,也可以进行修改;
render(oldRender: Functi
umi4+antDesignPro实现多tabs
放羊的小孩
07-06
2467
um4+antdesign实现多tabs
安装umi4阻碍一天的问题解决了
xiyangbaixue的博客
07-26
2364
umi4已经发布一段时间了,现在项目中还是用的umi3版本,umi3最坑的就是mfsu不会热更新,而且umi4新增了一些功能,所以打算把umi版本升级下。
使用Umi 遇到的错误
qq_53280805的博客
11-23
1023
UmiJS
ant design pro动态菜单
10-08
自己写的ant design pro动态菜单,不足请指正,互相学习借鉴!
ant design pro新版本动态菜单
12-26
基于12月版本的ant design pro开源项目的动态菜单生成的完整方法附代码,粘贴就可使用,简单方便。内含解说。直接能用,本人亲测,非诚勿扰!!!
umi v4加密狗驱动 官方版_支持64位系统
07-14
umi v4加密狗驱动是微狗最新驱动程序,当遇到部分umi v4加密狗无法使用的时候可以尝试一下此驱动,本驱动支持以下操作系统:Windows9X/ME/NT/2K/XP/WS2003/Vista/XP64/WS2003x64/Vista64,需要的拿走吧。驱动介绍微...
TigerHee#shareJS#14.umi2+Antd实现动态主题色切换实践1
07-25
前言:新项目设计稿评审后准备开工,老大说:考虑一下需要支持主题切换,我说:好的开始:算是比较成熟的套餐了,主题切换应该是小case的需求,于是:去antd De
Umi4 集成阿里低代码框架lowcode-engine
wangqiaojie的博客
08-04
3193
最近准备研究下阿里低代码框架lowcode-engine, 官方Demo是提供好的脚手架,由于我们的框架使用的是umi,官方文档提供了一些教程,在此记录下在umi4集成lowcode-engine.
react-umi-3-1.动态路由实现并实现路由权限
echo_Ae的博客
12-08
3982
效果图:
这时候让我们先把 pages 下面的 index.ts、index.less 删除掉
这样看起来就整洁了很多
好了开始正文。
其实动态路由非常简单~实用umi框架之后,小伙伴们先来到umi文档运行时配置
来让我们来实现
.umirc.ts
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
umi 路由配置
大龙帅的博客
04-05
6197
一、基础路由
项目根目录下新建文件夹:config/routes.tsx (umi约定,位置不能错,不然引入会报错)
routes.tsx
import type { IRoute } from 'umi';
const routes: IRoute[] = [
{ path: '/', component: '@/pages/index' }
];
export default routes;
.umirc.ts这个文件内引入
简单介绍下这个文件:umi的配置文件,参考官方文档
impo
umi4 定义ErrorComponent没有起效,错误边界配置出错的解决方案
瑾白芨的博客
01-28
194
umi4 antD ErrorComponent 错误边界
“相关推荐”对你有帮助么?
非常没帮助
没帮助
一般
有帮助
非常有帮助
提交
绿胡子大叔
CSDN认证博客专家
CSDN认证企业博客
码龄2年
暂无认证
22
原创
7972
周排名
6万+
总排名
1万+
访问
等级
253
积分
513
粉丝
14
获赞
评论
40
收藏
私信
关注
热门文章
Umi4 从零开始实现动态路由、动态菜单
3603
React 学习笔记 - create-react-app踩坑 & eslint
1849
React 学习笔记 - react router v6 hooks
1687
React 学习笔记 - 利用高阶组件和React Hooks实现权限拦截
1513
React 学习笔记 - 使用React Hooks进行简单的表单封装
673
分类专栏
React 学习笔记
9篇
React Native 学习笔记
1篇
JavaScript 学习笔记
8篇
CSS 学习笔记
1篇
最新评论
Umi4 从零开始实现动态路由、动态菜单
qq_16084435:
能分享下源码吗
Umi4 从零开始实现动态路由、动态菜单
绿胡子大叔:
最近又看了一遍发现关于根据用户权限生成路由列表的问题:生产环境实际使用了qiankun微前端,在访问global.ts(umi初始化)前已经通过登录模块获取到了登录态token并写入到localStorage。在global.ts中向后端请求路由列表时实际携带了localStorage中的token。每次页面刷新时同样也会触发global.ts中获取路由的方法刷新路由列表
Umi4 从零开始实现动态路由、动态菜单
zyntm:
登录后不能获取动态菜单是因为history.push 跳转并不会重新加载global.ts,所以登录成功后我不使用history.push,而选择用location.href 修改页面路由,这样跳转后就会重新global.ts,在获取角色菜单前先请求/api/currentUser,获取当前角色,然后通过/api/menu/:usrId获取用户个性化路由
Umi4 从零开始实现动态路由、动态菜单
江雪松:
我好像有这个问题,请问有解决方案吗
Umi4 从零开始实现动态路由、动态菜单
绿胡子大叔:
前端代码中只写了 /home 和 /login,其他路由是根据获取到的 menu 数据使用 React.lazy 动态加载的页面组件并在 umi 的 patchRoutes 方法中合并路由数据
您愿意向朋友推荐“博客详情页”吗?
强烈不推荐
不推荐
一般般
推荐
强烈推荐
提交
最新文章
彻底解决 Delete `␍`
React Native 安卓全面屏状态栏和底部导航栏透明适配
React 学习笔记 - Redux 中间件的使用方法及原理
2023年2篇
2022年10篇
2021年10篇
目录
目录
分类专栏
React 学习笔记
9篇
React Native 学习笔记
1篇
JavaScript 学习笔记
8篇
CSS 学习笔记
1篇
目录
评论 8
被折叠的 条评论
为什么被折叠?
到【灌水乐园】发言
查看更多评论
添加红包
祝福语
请填写红包祝福语或标题
红包数量
红包个数最小为10个
红包总金额
红包金额最低5元
余额支付
当前余额3.43元
前往充值 >
需支付:10.00元
取消
确定
下一步
知道了
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝
规则
hope_wisdom 发出的红包
打赏作者
绿胡子大叔
你的鼓励将是我创作的最大动力
¥2
¥4
¥6
¥10
¥20
输入1-500的整数
余额支付
(余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付
您的余额不足,请更换扫码支付或充值
打赏作者
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额
抵扣说明:
1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。 2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。
余额充值