react最流行的生态替代antdpro搭建轻量级后台管理

目录
  • 前言
  • 项目初始化
  • 数据请求 + mock
    • 配置 axios
    • 配置 react-query
    • mock
  • 路由权限配置
    • 路由文件
    • main.tsx
    • App.tsx
  • 页面编写
    • login 页面
    • BasicLayout
    • 动态菜单栏
    • 封装页面通用面包屑
  • 总结

前言

你是否经历过公司的产品和 ui 要求左侧菜单栏要改成设计图上的样子? 苦恼 antd-pro 强绑定的 pro-layout 菜单栏不能自定义?你可以使用 umi,但是就要根据它的约定来开发,捆绑全家桶等等。手把手教你搭一个轻量级的后台模版,包括路由的权限、动态菜单等等。

为方便使用 antd 组件库,你可以改成任意你喜欢的。数据请求的管理使用 react-query,类似 useRequest,但是更加将大。样式使用 tailwindcssstyled-components,因为 antd v5 将使用 css in js。路由的权限和菜单管理使用 react-router-auth-plus。。。

仓库地址

项目初始化

vite

# npm 7+
npm create vite spirit-admin -- --template react-ts

antd

tailwindcss

styled-components

react-query

axios

react-router

react-router-auth-plus (权限路由、动态菜单解决方案) 仓库地址 文章地址

等等...

数据请求 + mock

配置 axios

设置拦截器,并在 main.ts 入口文件中引入这个文件,使其在全局生效

// src/axios.ts
import axios, { AxiosError } from "axios";
import { history } from "./main";
// 设置 response 拦截器,状态码为 401 清除 token,并返回 login 页面。
axios.interceptors.response.use(
  function (response) {
    return response;
  },
  function (error: AxiosError) {
    if (error.response?.status === 401) {
      localStorage.removeItem("token");
      // 在 react 组件外使用路由方法, 使用方式会在之后路由配置时讲到
      history.push("/login");
    }
    return Promise.reject(error);
  }
);
// 设置 request 拦截器,请求中的 headers 带上 token
axios.interceptors.request.use(function (request) {
  request.headers = {
    authorization: localStorage.getItem("token") || "",
  };
  return request;
});

配置 react-query

在 App 外层包裹 QueryClientProvider,设置默认选项,窗口重新聚焦时和失败时不重新请求。

// App.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      retry: false,
    },
  },
});
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>
  </React.StrictMode>
);

我们只有两个请求,登录和获取当前用户,src 下新建 hooks 文件夹,再分别建 query、mutation 文件夹,query 是请求数据用的,mutation 是发起数据操作的请求用的。具体可以看 react-query 文档

获取当前用户接口

// src/hooks/query/useCurrentUserQuery.ts
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { queryClient } from "../../main";
// useQuery 需要唯一的 key,react-query v4 是数组格式
const currentUserQueryKey = ["currentUser"];
// 查询当前用户,如果 localStorage 里没有 token,则不请求
export const useCurrentUserQuery = () =>
  useQuery(currentUserQueryKey, () => axios.get("/api/me"), {
    enabled: !!localStorage.getItem("token"),
  });
// 可以在其它页面获取 useCurrentUserQuery 的数据
export const getCurrentUser = () => {
  const data: any = queryClient.getQueryData(currentUserQueryKey);
  return {
    username: data?.data.data.username,
  };
};

登录接口

// src/hooks/mutation/useLoginMutation.ts
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
export const useLoginMutation = () =>
  useMutation((data) => axios.post("/api/login", data));

mock

数据请求使用 react-query + axios, 因为只有两个请求,/login(登录) 和 /me(当前用户),直接使用 express 本地 mock 一下数据。新建 mock 文件夹,分别建立 index.jsusers.js

// users.js 存放两种类型的用户
export const users = [
  { username: "admin", password: "admin" },
  { username: "employee", password: "employee" },
];
// index.js 主文件
import express from "express";
import { users } from "./users.js";
const app = express();
const port = 3000;
const router = express.Router();
// 登录接口,若成功返回 token,这里模拟 token 只有两种情况
router.post("/login", (req, res) => {
  setTimeout(() => {
    const username = req.body.username;
    const password = req.body.password;
    const user = users.find((user) => user.username === username);
    if (user && password === user.password) {
      res.status(200).json({
        code: 0,
        token: user.username === "admin" ? "admin-token" : "employee-token",
      });
    } else {
      res.status(200).json({ code: -1, message: "用户名或密码错误" });
    }
  }, 2000);
});
// 当前用户接口,请求时需在 headers 中带上 authorization,若不正确返回 401 状态码。根据用户类型返回权限和用户名
router.get("/me", (req, res) => {
  setTimeout(() => {
    const token = req.headers.authorization;
    if (!["admin-token", "employee-token"].includes(token)) {
      res.status(401).json({ code: -1, message: "请登录" });
    } else {
      const auth = token === "admin-token" ? ["application", "setting"] : [];
      const username = token === "admin-token" ? "admin" : "employee";
      res.status(200).json({ code: 0, data: { auth, username } });
    }
  }, 2000);
});
app.use(express.json());
// 接口前缀统一加上 /api
app.use("/api", router);
// 禁用 304 缓存
app.disable("etag");
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

package.json 中的 scripts 添加一条 mock 命令,需安装 nodemon,用来热更新 mock 文件的。npm run mock 启动 express 服务。

"scripts": {
  ...
  "mock": "nodemon mock/index.js"
}

现在在项目中还不能使用,需要在 vite 中配置 proxy 代理

// vite.config.ts
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
});

路由权限配置

路由和权限这块使用的方案是 react-router-auth-plus,具体介绍见上篇

路由文件

新建一个 router.tsx,引入页面文件,配置项目所用到的所有路由,配置上权限。这里我们扩展一下 AuthRouterObject 类型,自定义一些参数,例如左侧菜单的 icon、name 等。设置上 /account/center/application 路由需要对应的权限。

import {
  AppstoreOutlined,
  HomeOutlined,
  UserOutlined,
} from "@ant-design/icons";
import React from "react";
import { AuthRouterObject } from "react-router-auth-plus";
import { Navigate } from "react-router-dom";
import BasicLayout from "./layouts/BasicLayout";
import Application from "./pages/application";
import Home from "./pages/home";
import Login from "./pages/login";
import NotFound from "./pages/404";
import Setting from "./pages/account/setting";
import Center from "./pages/account/center";
export interface MetaRouterObject extends AuthRouterObject {
  name?: string;
  icon?: React.ReactNode;
  hideInMenu?: boolean;
  hideChildrenInMenu?: boolean;
  children?: MetaRouterObject[];
}
// 只需在需要权限的路由配置 auth 即可
export const routers: MetaRouterObject[] = [
  { path: "/", element: <Navigate to="/home" replace /> },
  { path: "/login", element: <Login /> },
  {
    element: <BasicLayout />,
    children: [
      {
        path: "/home",
        element: <Home />,
        name: "主页",
        icon: <HomeOutlined />,
      },
      {
        path: "/account",
        name: "个人",
        icon: <UserOutlined />,
        children: [
          {
            path: "/account",
            element: <Navigate to="/account/center" replace />,
          },
          {
            path: "/account/center",
            name: "个人中心",
            element: <Center />,
          },
          {
            path: "/account/setting",
            name: "个人设置",
            element: <Setting />,
            // 权限
            auth: ["setting"],
          },
        ],
      },
      {
        path: "/application",
        element: <Application />,
        // 权限
        auth: ["application"],
        name: "应用",
        icon: <AppstoreOutlined />,
      },
    ],
  },
  { path: "*", element: <NotFound /> },
];

main.tsx

使用 HistoryRouter,在组件外可以路由跳转,这样就可以在 axios 拦截器中引入 history 跳转路由了。

import { createBrowserHistory } from "history";
import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";
export const history = createBrowserHistory({ window });
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <HistoryRouter history={history}>
        <App />
      </HistoryRouter>
    </QueryClientProvider>
  </React.StrictMode>
);

App.tsx

import { useAuthRouters } from "react-router-auth-plus";
import { routers } from "./router";
import NotAuth from "./pages/403";
import { Spin } from "antd";
import { useEffect, useLayoutEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useCurrentUserQuery } from "./hooks/query";
function App() {
  const navigate = useNavigate();
  const location = useLocation();
  // 获取当前用户,localStorage 里没 token 时不请求
  const { data, isFetching } = useCurrentUserQuery();
  // 第一次进入程序,不是 login 页面且没有 token,跳转到 login 页面
  useEffect(() => {
    if (!localStorage.getItem("token") && location.pathname !== "/login") {
      navigate("/login");
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  // 第一次进入程序,若是 login 页面,且 token 没过期(code 为 0),自动登录进入 home 页面。使用 useLayoutEffect 可以避免看到先闪一下 login 页面,再跳到 home 页面。
  useLayoutEffect(() => {
    if (location.pathname === "/login" && data?.data.code === 0) {
      navigate("/home");
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data?.data.code]);
  return useAuthRouters({
    // 传入当前用户的权限
    auth: data?.data.data.auth || [],
    // 若正在获取当前用户,展示 loading
    render: (element) =>
      isFetching ? (
        <div className="flex justify-center items-center h-full">
          <Spin size="large" />
        </div>
      ) : (
        element
      ),
    // 若进入没权限的页面,显示 403 页面
    noAuthElement: () => <NotAuth />,
    routers,
  });
}
export default App;

页面编写

login 页面

html 省略,antd Form 表单账号密码输入框和一个登录按钮

// src/pages/login/index.tsx
const Login: FC = () => {
  const navigate = useNavigate();
  const { mutateAsync: login, isLoading } = useLoginMutation();
  // Form 提交
  const handleFinish = async (values: any) => {
    const { data } = await login(values);
    if (data.code === 0) {
      localStorage.setItem("token", data.token);
      // 请求当前用户
      await queryClient.refetchQueries(currentUserQueryKey);
      navigate("/home")
      message.success("登录成功");
    } else {
      message.error(data.message);
    }
  };
  return ...
};

BasicLayout

BasicLayout 这里简写一下,具体可以看源码。BasicLayout 会接收到 routers,在 routers.tsx 配置的 children 会自动传入 routers,不需要像这样手动传入<BasicLayout routers={[]} />Outlet 相当于 children,是 react-router v6 新增的。

将 routers 传入到 Outlet 的 context 中。之后就可以在页面中用 useOutletContext 获取到 routers 了。

// src/layouts
import { Layout } from "antd";
import { Outlet } from "react-router-dom";
import styled from "styled-components";
// 使用 styled-components 覆盖样式
const Header = styled(Layout.Header)`
  height: 48px;
  line-height: 48px;
  padding: 0 16px;
`;
// 同上
const Slider = styled(Layout.Sider)`
  .ant-layout-sider-children {
    display: flex;
    flex-direction: column;
  }
`;
interface BasicLayoutProps {
  routers?: MetaRouterObject[];
}
const BasicLayout: FC<BasicLayoutProps> = ({ routers }) => {
  // 样式省略简写
  return (
    <Layout>
      <Header>
        ...顶部
      </Header>
      <Layout hasSider>
        <Slider>
          ...左侧菜单
        </Slider>
        <Layout>
          <Layout.Content>
            <Outlet context={{ routers }} />
          </Layout.Content>
        </Layout>
      </Layout>
    </Layout>
  );
};

动态菜单栏

把左侧菜单栏单独拆分成一个组件,在 BasicLayout 中引入,传入 routers 参数。

// src/layouts/BasicLayout/components/SliderMenu.tsx
import { Menu } from "antd";
import { FC, useEffect, useState } from "react";
import { useAuthMenus } from "react-router-auth-plus";
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { MetaRouterObject } from "../../../router";
import { ItemType } from "antd/lib/menu/hooks/useItems";
// 转化成 antd Menu 组件需要的格式。只有配置了 name 和不隐藏的才展示
const getMenuItems = (routers: MetaRouterObject[]): ItemType[] => {
  const menuItems = routers.reduce((total: ItemType[], router) => {
    if (router.name && !router.hideInMenu) {
      total?.push({
        key: router.path as string,
        icon: router.icon,
        label: router.name,
        children:
          router.children &&
          router.children.length > 0 &&
          !router.hideChildrenInMenu
            ? getMenuItems(router.children)
            : undefined,
      });
    }
    return total;
  }, []);
  return menuItems;
};
interface SlideMenuProps {
  routers: MetaRouterObject[];
}
const SlideMenu: FC<SlideMenuProps> = ({ routers }) => {
  const location = useLocation();
  const navigate = useNavigate();
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  // useAuthMenus 先过滤掉没有权限的路由。再通过 getMenuItems 获得 antd Menu组件需要的格式
  const menuItems = getMenuItems(useAuthMenus(routers));
  // 默认打开的下拉菜单
  const defaultOpenKey = menuItems.find((i) =>
    location.pathname.startsWith(i?.key as string)
  )?.key as string;
  // 选中菜单
  useEffect(() => {
    setSelectedKeys([location.pathname]);
  }, [location.pathname]);
  return (
    <Menu
      style={{ borderRightColor: "white" }}
      className="h-full"
      mode="inline"
      selectedKeys={selectedKeys}
      defaultOpenKeys={defaultOpenKey ? [defaultOpenKey] : []}
      items={menuItems}
      {/* 选中菜单回调,导航到其路由 */}
      onSelect={({ key }) => navigate(key)}
    />
  );
};
export default SlideMenu;

封装页面通用面包屑

封装一个在 BasicLayout 下全局通用的面包屑。

// src/components/PageBreadcrumb.tsx
import { Breadcrumb } from "antd";
import { FC } from "react";
import {
  Link,
  matchRoutes,
  useLocation,
  useOutletContext,
} from "react-router-dom";
import { MetaRouterObject } from "../router";
const PageBreadcrumb: FC = () => {
  const location = useLocation();
  // 获取在 BasicLayout 中传入的 routers
  const { routers } = useOutletContext<{ routers: MetaRouterObject[] }>();
  // 使用 react-router 的 matchRoutes 方法匹配路由数组
  const match = matchRoutes(routers, location);
  // 处理一下生成面包屑数组
  const breadcrumbs =
    (match || []).reduce((total: MetaRouterObject[], current) => {
      if ((current.route as MetaRouterObject).name) {
        total.push(current.route);
      }
      return total;
    }, []);
  // 最后一个面包屑不能点击,前面的都能点击跳转
  return (
    <Breadcrumb>
      {breadcrumbs.map((i, index) => (
        <Breadcrumb.Item key={i.path}>
          {index === breadcrumbs.length - 1 ? (
            i.name
          ) : (
            <Link to={i.path as string}>{i.name}</Link>
          )}
        </Breadcrumb.Item>
      ))}
    </Breadcrumb>
  );
};
export default PageBreadcrumb;

这样就能在页面中引入这个组件使用了,如果你想在每个页面中都使用,可以写在 BasicLayout 的 Content 中,并在 routers 配置中加一个 hideBreadcrumb 选项,通过配置来控制是否在当前路由页面显示面包屑。

function Home() {
  return (
    <div>
      <PageBreadcrumb />
    </div>
  );
}

总结

react 的生态是越来越多样化了,学的东西也越来越多(太卷了)。总的来说,上面所使用的一些库,或多或少都要有所了解。应该都要锻炼自己有具备能搭建一个简易版的后台管理模版的能力 github 地址

以上就是react最流行的生态替代antdpro搭建轻量级后台管理的详细内容,更多关于react生态轻量级后台管理的资料请关注我们其它相关文章!

(0)

相关推荐

  • 教你使用vscode 搭建react-native开发环境

    问题 代码没有提示: 许多刚接触RN开发的非前端同学,都会问"哪个编辑器有智能提示?"...而对于前端同学来说,现在的日子已经好很多了,要什么自行车. 低级代码错误: 这里的错误是指类似拼写错误,符号错误等.写完代码,跑起来各种报错,有时候费死劲的找,最后发现是个中文的分号问题. 解决办法 可选的方案大概有: 使用typescript: 直接使用有静态类型支持的js版本,but要再学习一套语法,而且我的代码都是ts写的,但很多好的公共库不是啊. 使用flow: 由于网络的原因,这个环境

  • React+Ant Design开发环境搭建的实现步骤

    基础知识 1.使用脚手架创建项目并启动 1.1 安装脚手架: npm install -g create-react-app 1.2 使用脚手架创建项目: create-react-app antd-start-demo antd-start-demo为项目名. 1.3 启动 npm start 2.npm转换为yarn 2.1 安装yarn: npm install -g yarn ​ 2.2 获取yarn当前的镜像源: yarn config get registry ​ 2.3 设置为淘宝

  • 详解使用React全家桶搭建一个后台管理系统

    引子 学生时代为了掌握某个知识点会不断地做习题,做总结,步入岗位之后何尝不是一样呢?做业务就如同做习题,如果'课后'适当地进行总结,必然更快地提升自己的水平. 由于公司采用的react+node的技术栈,于是就完成了一个reactSPA小项目,计划日后把平时工作中遇到的业务以及学习中遇到有趣的东西给抽象成demo展示出来.目前该项目只是把雏形搭好,效果如下.在此文的基础上,写了篇新文章使用React全家桶搭建一个后台管理系统,欢迎围观.(附注:因为项目不时更新,文章不一定会即时更新,所以以实际的

  • 基于visual studio code + react 开发环境搭建过程

    开发环境 windows 开发工具 visual studio code node 安装和 npm windows 安装node 可以直接在 node官网直接下载直接当作普通软件安装即可. 安装完成可以在控制台中运行node测试是否安装成功 win + r 输入 cmd ,直接在终端输入node -v 输出版本号及已经成功安装. 目前新版本的node自带npm(npm 是随同 node 一起安装的包管理工具).这里安装好了 node并且测试安装成功之后,可以继续在控制台输入 npm -v 检查是

  • 手把手教你从零开始react+antd搭建项目

    之前的文章都是自己的学习日志,主要是防止自己遗忘之前遇到的坑.这次将从最基础的项目搭建开始讲起,做一个基于react和antd的后台管理系统.我会一步步进行下去,所以看完本文你哪怕不了解react,应该也会使用react做一个简单的项目.话不多少,直接开始.完整项目请前往GitHub查看,体验请点击这里.如果觉得可以请给一颗star,谢谢各位. 1.开发环境: node.js -v 12.16.3 create-react-app -v 3.4.1 antd -v 4.3.3 项目开始前请自行全

  • React引入antd-mobile+postcss搭建移动端

    安装antd-mobile 全局引入 npm install antd-mobile --save 在App.js引入css import 'antd-mobile/dist/antd-mobile.css'; 在jsx使用antd组件 import React from 'react'; import { Button } from 'antd-mobile'; const index = () => { return ( <div> <Button type="pri

  • react最流行的生态替代antdpro搭建轻量级后台管理

    目录 前言 项目初始化 数据请求 + mock 配置 axios 配置 react-query mock 路由权限配置 路由文件 main.tsx App.tsx 页面编写 login 页面 BasicLayout 动态菜单栏 封装页面通用面包屑 总结 前言 你是否经历过公司的产品和 ui 要求左侧菜单栏要改成设计图上的样子? 苦恼 antd-pro 强绑定的 pro-layout 菜单栏不能自定义?你可以使用 umi,但是就要根据它的约定来开发,捆绑全家桶等等.手把手教你搭一个轻量级的后台模版

  • 快速搭建python爬虫管理平台

    爬虫有多重要 对搜索引擎来说,爬虫不可或缺:对舆情公司来说,爬虫是基础:对 NLP来说,爬虫可以获取语料:对初创公司来说,爬虫可以获取初始内容.但是爬虫技术纷繁复杂,不同类型的抓取场景会运用到不同的技术.例如,简单的静态页面可以用 HTTP 请求+HTML 解析器直接搞定:一个动态页面需要用 Puppeteer 或 Selenium等自动化测试工具:有反爬的网站需要用到代理.打码等技术:等等.那么此时就需要一个成熟的爬虫管理平台,帮助企业或个人处理大量的爬虫类别. 理解什么是爬虫管理平台 定义

  • VUE搭建分布式医疗挂号系统后台管理页面示例步骤

    目录 前言 一.搭建前端环境 (1)引入项目到项目工作区 (2)根据package.json下载依赖 (3)启动创建好的前端项目 二.前端环境目录结构 (1)总体目录概览 (2)关键文件 package.js build/webpack.dev.conf.js index.html src/main.js config/dev.env.js src/utils/request.js src/api/login.js 三.登录改造 登入方法改造 获取用户信息方法改造 登出方法改造 前端登出方法改造

  • Python3 venv搭建轻量级虚拟环境的步骤(图文)

    今天先聊一聊在windows/mac iOS系统下用venv搭建python轻量级虚拟环境的问题. 使用venv搭建的虚拟环境同virtualenv搭建的虚拟环境,即venv可替代virtualenv 1. 虚拟环境的作用是:搭建独立的python运行环境,不与其他产生冲突 就是说,安装在虚拟环境里的所有包,均不会对环境外的其他包产生影响,反之,在虚拟环境下运行时只能调用虚拟环境中安装的包,不会调用外部的包.比如,mac系统自带python2,我搭建了虚拟环境安装python3,那么我在虚拟环境

  • React项目中axios的封装与API接口的管理详解

    目录 前言 安装 引入 环境的切换 请求拦截 响应拦截 api的统一管理 总结 前言 在react项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中.他有很多优秀的特性,例如拦截请求和响应.取消请求.转换json.客户端防御XSRF等.如果还对axios不了解的,可以移步axios文档. 安装 //使用npm安装 npm install axios; //使用yarn安装 yarn add axios 引入 在项目

  • 从0到1搭建element后台框架优化篇(打包优化)

    前言 hello,咱又见了~~嘻嘻.本次主要来说说这个打包优化的问题.一个vue项目从开发到上线必须得经历打包过程,一个项目的打包优化与否都决定了你这个项目的运行速度以及用户体验.本次主要是针对vue.config,js的配置进行优化.项目地址 开发环境与生产环境 开发环境与生产环境的配置也是开发中的必不可少的一环.本项目是由vue-cli3开发,vue-cli3深度集成了webpack,如果不熟悉vue-cli3可以先去官网看看相关配置. 开发环境 在项目根目录下新建.env.developm

  • 使用Layui搭建后台管理界面的操作方法

    Layui是一个样式比较丰富艳丽的UI框架,通过自学了相关api文档,自己手工搭建了一个后台管理页面.页面布局是常用的一套,上面是一个logo,左边区域是一个导航,中间是内容tab页,底部是版权信息,好了,看代码吧! <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" cont

  • 详解微信小程序的 request 封装示例

    背景 之前小程序代码混乱,所以新项目一开始就准备弄个微信小程序的 request 的封装 httpUtils.js const request = function (path, method, data, header) { let user_id = ""; let token = ""; try { user_id = wx.getStorageSync(USER_ID_KEY); token = wx.getStorageSync(TOKEN_KEY); }

  • 简单谈谈React中的路由系统

    React中的路由系统 提起路由,首先想到的就是 ASPNET MVC 里面的路由系统--通过事先定义一组路由规则,程序运行时就能自动根据我们输入的URL来返回相对应的页面.前端中的路由与之类似,前端中的路由是根据你定义的路由规则来渲染不同的页面/组件,同时也会更新地址栏的URL.本篇文章要介绍的是React中经常使用到的路由,react-router主要使用HTML5的history API来同步你的UI和URL. react-router的最新版本是v4.1.1,由于4.0版本和之间的版本A

随机推荐