vue后台管理系统——添加i18n国际化功能——技能提升
昨天在写后台管理系统时,遇到一个需求就是需要实现国际化功能。
antd
和element-ui
这两个框架其实都是有国际化的。
具体展示形式就是如下:
点击右上角头部的语言,切换语言,然后整个系统的文字都改变成对应的语言展示。
切换成英文的效果如下:
下面对整个系统的国际化进行介绍:
1.安装i18n
插件,如果是使用的vue-admin
的框架,则已经安装过了
具体i18n
插件是否安装过了,可以在package,json
中进行查看。
npm install vue-i18n --save
2.在utils
文件夹中添加i18n.js
文件——路由的国际化需要单独处理,其他的国际化是可以用公用国际化文件的
文件内容如下:
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import routesI18n from '@/router/i18n'
import {getI18nKey} from '@/utils/routerUtil'
import CommonI18n from '@/locales/common.i18n';/*** 创建 i18n 配置* @param locale 本地化语言* @param fallback 回退语言* @returns {VueI18n}*/
function initI18n(locale, fallback) {Vue.use(VueI18n)let i18nOptions = {locale,fallbackLocale: fallback,silentFallbackWarn: true,silentTranslationWarn: true,...CommonI18n,}return new VueI18n(i18nOptions)
}/*** 根据 router options 配置生成 国际化语言* @param lang* @param routes* @param valueKey* @returns {*}*/
function generateI18n(lang, routes, valueKey) {routes.forEach(route => {let keys = getI18nKey(route.fullPath).split('.')let value = valueKey === 'path' ? route[valueKey].split('/').filter(item => !item.startsWith(':') && item != '').join('.') : route[valueKey]lang.assignProps(keys, value)if (route.children) {generateI18n(lang, route.children, valueKey)}})return lang
}/*** 格式化 router.options.routes,生成 fullPath* @param routes* @param parentPath*/
function formatFullPath(routes, parentPath = '') {routes.forEach(route => {let isFullPath = route.path.substring(0, 1) === '/'route.fullPath = isFullPath ? route.path : (parentPath === '/' ? parentPath + route.path : parentPath + '/' + route.path)if (route.children) {formatFullPath(route.children, route.fullPath)}})
}/*** 从路由提取国际化数据* @param i18n* @param routes*/
function mergeI18nFromRoutes(i18n, routes) {formatFullPath(routes)const CN = generateI18n(new Object(), routes, 'name')const US = generateI18n(new Object(), routes, 'path')i18n.mergeLocaleMessage('CN', CN)i18n.mergeLocaleMessage('US', US)const messages = routesI18n.messagesObject.keys(messages).forEach(lang => {i18n.mergeLocaleMessage(lang, messages[lang])})
}export {initI18n,mergeI18nFromRoutes,formatFullPath
}
3.router
中添加i18n.js
文件——路由的国际化需要单独处理,其他的国际化是可以用公用国际化文件的
文件内容如下:
注意:这个文件中的格式要跟路由配置文件中的格式要保持一致。比如user下面的children子页面有userCenter和changePassword两个,则需要像下面的对象一样做嵌套。
messages中的对象,多种语言就需要写多个对象,对象的key命名最好跟语言中的key保持一致。
module.exports = {messages: {CN: {home: { name: '首页' },demo: {name: '演示页',},user: {name: '个人中心',userCenter: { name: '个人信息' },changePassword: { name: '修改账户密码' },},},US: {home: { name: 'home' },demo: {name: 'Demo Page',},user: {name: 'user',userCenter: { name: 'userCenter' },changePassword: { name: 'changePassword' },},},HK: {home: { name: '首頁' },demo: {name: '演示頁',},user: {name: '個人中心',userCenter: { name: '個人信息' },changePassword: { name: '修改賬戶密碼' },},},},
};
4.utils
中添加routerUtil.js
文件
文件内容如下:
import routerMap from '@/router/async/router.map'
import {mergeI18nFromRoutes} from '@/utils/i18n'
import Router from 'vue-router'
import deepMerge from 'deepmerge'
import basicOptions from '@/router/async/config.async'//应用配置
let appOptions = {router: undefined,i18n: undefined,store: undefined
}/*** 设置应用配置* @param options*/
function setAppOptions(options) {const {router, store, i18n} = optionsappOptions.router = routerappOptions.store = storeappOptions.i18n = i18n
}/*** 根据 路由配置 和 路由组件注册 解析路由* @param routesConfig 路由配置* @param routerMap 本地路由组件注册配置*/
function parseRoutes(routesConfig, routerMap) {let routes = []routesConfig.forEach(item => {// 获取注册在 routerMap 中的 router,初始化 routeCfglet router = undefined, routeCfg = {}if (typeof item === 'string') {router = routerMap[item]routeCfg = {path: router.path || item, router: item}} else if (typeof item === 'object') {router = routerMap[item.router]routeCfg = item}if (!router) {console.warn(`can't find register for router ${routeCfg.router}, please register it in advance.`)router = typeof item === 'string' ? {path: item, name: item} : item}// 从 router 和 routeCfg 解析路由const route = {path: routeCfg.path || router.path || routeCfg.router,name: routeCfg.name || router.name,component: router.component,redirect: routeCfg.redirect || router.redirect,meta: {authority: routeCfg.authority || router.authority || routeCfg.meta?.authority || router.meta?.authority || '*',icon: routeCfg.icon || router.icon || routeCfg.meta?.icon || router.meta?.icon,page: routeCfg.page || router.page || routeCfg.meta?.page || router.meta?.page,link: routeCfg.link || router.link || routeCfg.meta?.link || router.meta?.link}}if (routeCfg.invisible || router.invisible) {route.meta.invisible = true}if (routeCfg.children && routeCfg.children.length > 0) {route.children = parseRoutes(routeCfg.children, routerMap)}routes.push(route)})return routes
}/*** 加载路由* @param routesConfig {RouteConfig[]} 路由配置*/
function loadRoutes(routesConfig) {//兼容 0.6.1 以下版本/*************** 兼容 version < v0.6.1 *****************/if (arguments.length > 0) {const arg0 = arguments[0]if (arg0.router || arg0.i18n || arg0.store) {routesConfig = arguments[1]console.error('the usage of signature loadRoutes({router, store, i18n}, routesConfig) is out of date, please use the new signature: loadRoutes(routesConfig).')console.error('方法签名 loadRoutes({router, store, i18n}, routesConfig) 的用法已过时, 请使用新的方法签名 loadRoutes(routesConfig)。')}}/*************** 兼容 version < v0.6.1 *****************/// 应用配置const {router, store, i18n} = appOptions// 如果 routesConfig 有值,则更新到本地,否则从本地获取if (routesConfig) {store.commit('account/setRoutesConfig', routesConfig)} else {routesConfig = store.getters['account/routesConfig']}// 如果开启了异步路由,则加载异步路由配置const asyncRoutes = store.state.setting.asyncRoutesif (asyncRoutes) {if (routesConfig && routesConfig.length > 0) {const routes = parseRoutes(routesConfig, routerMap)const finalRoutes = mergeRoutes(basicOptions.routes, routes)formatRoutes(finalRoutes)router.options = {...router.options, routes: finalRoutes}router.matcher = new Router({...router.options, routes:[]}).matcherrouter.addRoutes(finalRoutes)}}// 提取路由国际化数据mergeI18nFromRoutes(i18n, router.options.routes)// 初始化Admin后台菜单数据const rootRoute = router.options.routes.find(item => item.path === '/')const menuRoutes = rootRoute && rootRoute.childrenif (menuRoutes) {store.commit('setting/setMenuData', menuRoutes)}
}/*** 合并路由* @param target {Route[]}* @param source {Route[]}* @returns {Route[]}*/
function mergeRoutes(target, source) {const routesMap = {}target.forEach(item => routesMap[item.path] = item)source.forEach(item => routesMap[item.path] = item)return Object.values(routesMap)
}/*** 深度合并路由* @param target {Route[]}* @param source {Route[]}* @returns {Route[]}*/
function deepMergeRoutes(target, source) {// 映射路由数组const mapRoutes = routes => {const routesMap = {}routes.forEach(item => {routesMap[item.path] = {...item,children: item.children ? mapRoutes(item.children) : undefined}})return routesMap}const tarMap = mapRoutes(target)const srcMap = mapRoutes(source)// 合并路由const merge = deepMerge(tarMap, srcMap)// 转换为 routes 数组const parseRoutesMap = routesMap => {return Object.values(routesMap).map(item => {if (item.children) {item.children = parseRoutesMap(item.children)} else {delete item.children}return item})}return parseRoutesMap(merge)
}/*** 格式化路由* @param routes 路由配置*/
function formatRoutes(routes) {routes.forEach(route => {const {path} = routeif (!path.startsWith('/') && path !== '*') {route.path = '/' + path}})formatAuthority(routes)
}/*** 格式化路由的权限配置* @param routes 路由* @param pAuthorities 父级路由权限配置集合*/
function formatAuthority(routes, pAuthorities = []) {routes.forEach(route => {const meta = route.metaconst defaultAuthority = pAuthorities[pAuthorities.length - 1] || {permission: '*'}if (meta) {let authority = {}if (!meta.authority) {authority = defaultAuthority}else if (typeof meta.authority === 'string') {authority.permission = meta.authority} else if (typeof meta.authority === 'object') {authority = meta.authorityconst {role} = authorityif (typeof role === 'string') {authority.role = [role]}if (!authority.permission && !authority.role) {authority = defaultAuthority}}meta.authority = authority} else {const authority = defaultAuthorityroute.meta = {authority}}route.meta.pAuthorities = pAuthoritiesif (route.children) {formatAuthority(route.children, [...pAuthorities, route.meta.authority])}})
}/*** 从路由 path 解析 i18n key* @param path* @returns {*}*/
function getI18nKey(path) {const keys = path.split('/').filter(item => !item.startsWith(':') && item != '')keys.push('name')return keys.join('.')
}/*** 加载导航守卫* @param guards* @param options*/
function loadGuards(guards, options) {const {beforeEach, afterEach} = guardsconst {router} = optionsbeforeEach.forEach(guard => {if (guard && typeof guard === 'function') {router.beforeEach((to, from, next) => guard(to, from, next, options))}})afterEach.forEach(guard => {if (guard && typeof guard === 'function') {router.afterEach((to, from) => guard(to, from, options))}})
}export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes, setAppOptions}
5.router
中async
文件夹中添加router.map
文件
里面的内容比较多,有需要的可以留个邮箱给我,我打包发给你。
6.重点是commonI18n
文件,在locales
文件夹中
common.i18n.js
文件中的内容如下:
import CN from './CN';
import US from './US';
import HK from './HK';
//多种语言,则需要有多个文件用于区分// 全局公共的国际化定义
export default {messages: {CN,US,HK,},
};
以CN.js
为例:
// 全局公共的国际化定义 - CN
export default {user:'用户',creator:'创建人',orderNo: '订单编号',search:'搜索',cancel:'取消',CancelEditing:'取消编辑',edit:'编辑',submit:'提交',reset:'重置',....
}
对应的US.js
文件内容如下:
// 全局公共的国际化定义 - US
export default {user:'User',creator:'Creator',orderNo: 'Order No',search: 'Search',cancel:'Cancel',edit:'Edit',CancelEditing:'Cancel Editing',submit:'Submit',reset:'Reset',...
}
这个算是国际化的公共文件,国际化是就近原则,如果是单个页面有单独的i18n
文件,则会从单独的i18n
文件中查找对应的字段,没有找不到,则会从公共的i18n
文件中去查找。
7.以单个文件国际化为例:
页面中使用国际化字段的方式$t(xxx)
8.路由的国际化文件需要跟路由配置文件进行匹配,其他页面的国际化要跟公共国际化文件的格式保持一致即可。
9.通过以上的步骤,菜单+页面中静态的文字都可以实现国际化了,但是接口返回的数据国际化,则需要接口返回不同的文字了。
此时可以在axios
请求时,则请求头上添加当前的语言类型。
9.1 在axios
拦截器文件中的请求部分添加如下的代码
我需要在拦截器.js文件中获取vuex中存储的lang字段的值,此时是拿不到vuex中的数据的,因为this是undefined
因此需要在main.js
文件中添加如下的内容:
将vue挂载到window上
,则其他页面都可以通过window.vm
获取到vue了
...window.vm = new Vue({router,store,i18n,render: (h) => h(App),
}).$mount('#app');
拦截器中的写法:
const reqCommon = {/*** 发送请求之前做些什么* @param config axios config* @param options 应用配置 包含: {router, i18n, store, message}* @returns {*}*/onFulfilled(config, options) {const { message } = options;const { url, xsrfCookieName, headers } = config;// if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {// message.warning('认证 token 已过期,请重新登录')// }if (headers.Authorization &&xsrfCookieName &&!Cookie.get(xsrfCookieName)) {message.warning('认证 token 已过期,请重新登录');}config.headers['Authorization'] = Cookie.get(xsrfHeaderName);window.vm.$store.commit('setting/setLang',localStorage.getItem('language') || 'CN');console.log('语言', window.vm.$store.state.setting.lang);config.headers['language'] = window.vm.$store.state.setting.lang;return config;},.......
上面的代码最重要的就是:
window.vm.$store.commit('setting/setLang',localStorage.getItem('language') || 'CN'
);
console.log('语言', window.vm.$store.state.setting.lang);
config.headers['language'] = window.vm.$store.state.setting.lang;
为什么要存储到localStorage
中?因为,在切换语言时,接口也需要重新请求,则也就是说整个页面全部刷新,此时最简单的方法就是window.vm.$router.go(0)
实现页面的刷新。
页面刷新时,vuex中的setting/lang的默认值是CN简体中文
,为了能够存储上次切换的语言类型,可以存储到本地localStorage,这样浏览器不关闭的时候,这个缓存还是有的。
vuex中的setting文件中的setLang方法也需要改变
setLang(state, lang) {state.lang = lang;if (localStorage.getItem('language') != lang) {window.vm.$router.go(0);}localStorage.setItem('language', lang);console.log('setLang', window.vm.$route);
},
完成!!!
相关文章:

vue后台管理系统——添加i18n国际化功能——技能提升
昨天在写后台管理系统时,遇到一个需求就是需要实现国际化功能。 antd和element-ui这两个框架其实都是有国际化的。 具体展示形式就是如下: 点击右上角头部的语言,切换语言,然后整个系统的文字都改变成对应的语言展示。 切换成…...

理清gcc、g++、libc、glibc、libstdc++的关系
0 理清gcc、g++、libc、glibc、libstdc++的关系 0.1 $ dpkg -L libc6 $ dpkg -L libc6 /lib/x86_64-linux-gnu /lib/x86_64-linux-gnu/ld-2.31.so /lib/x86_64-linux-gnu/libBrokenLocale-2.31.so /lib/x86_64-linux-gnu/libSegFault.so /lib/x86_64-linux-gnu/libanl-2.31.s…...

一、快速入门 MongoDB 数据库
文章目录一、NoSQL 是什么1.1 NoSQL 简史1.2 NoSQL 的种类及其特性1.3 NoSQL 特点1.4 NoSQL 的优缺点1.5 NoSQL 与 SQL 数据库的比较二、MongoDB 基础知识2.1 MongoDB 是什么2.2 MongoDB 的体系结构2.3 MongoDB 的特点2.4 MongoDB 键特性2.5 MongoDB 的核心服务和工具2.6 Mongo…...

PMP第一章到第三章重要知识点
第1章引论 1.1指南概述和目的 PMBOK指南收录项目管理知识体系中被普遍认可为“良好实践”的那一部分: “普遍认可”:大多数时候适用于大多数项目,获得一致认可。 “良好实践”:能提高很多项目成功的可能性。 全球项目管理业界…...

【事务与锁】当Transactional遇上synchronized
事务与锁 - Transactional与Synchronize🥰前言问题回放问题一1、代码与结果复现2、原因分析3、解决方法问题二1、问题复现2、原因分析事务Transactional与锁synchronized1、synchronized与Transactional区别2、可能带来的问题3、针对问题二的解决前言 最近工作中遇…...

Pytorch模型转TensorRT步骤
Pytorch模型转TensorRT步骤 yolov5转TRT 流程 当前项目基于yolov5-6.0版本,如果使用其他版本代码请参考 https://github.com/wang-xinyu/tensorrtx/tree/master/yolov5 获取转换项目: git clone https://github.com/wang-xinyu/tensorrtx.git git …...

产品经理入门——必备技能之【产品运营】
文章目录一、基础介绍1.1 用户生命周期 & 产品生命周期1.2 运营的目的1.3 运营的阶段1.4 运营的主要工作(海盗模型)二、AARRR模型2.1 Acquisition 拉新2.2 Activision 促活2.3 Retention 留存2.4 Revenue 转化2.5 Referral 传播总结产品运营技能是产…...

【Java实现文件上传】java后端+vue前端实现文件上传全过程详解(附源码)
【写在前面】其实这篇文章我早就想写了,只是一直被需求开发耽搁,这不晚上刚好下班后有点时间,记录一下。需求是excel表格的上传,这个是很多业务系统不可或缺的功能点,再此也希望您能够读完我这篇文章对文件上传不再困惑…...

什么是SSD?SSD简述
什么是SSD?SSD简述前言一. SSD组成二. SSD存储介质存储介质按材料不同可分为三大类:光学存储介质、半导体存储介质和磁性存储介质三. SSD接口形态固态硬盘有SATA 3.0接口、MSATA接口、M.2接口、PCI-E接口、U.2接口五种类型。三. SSD闪存颗粒分类闪存颗粒…...

MySQL基础------sql指令1.0(查询操作->select)
目录 前言: 单表查询 1.查询当前所在数据库 2.查询整个表数据 3.查询某字段 4.条件查询 5.单行处理函数(聚合函数) 6.查询时给字段取别名 7.模糊查询 8.查询结果去除重复项 9.排序(升序和降序) 10. 分组查询 1…...

Python数据分析处理报告--实训小案例
目录 1、实验一 1.1、题目总览 1.2、代码解析 2、实现二 2.1、题目总览 2.2、代码解析 3、实验三 3.1、题目总览 3.2、代码解析 4、实验四 3.1、题目总览 3.2、代码解析 哈喽~今天学习记录的是数据分析实训小案例。 就用这个案例来好好巩固一下 python 数据分析三…...

OpenCV入门(十二)快速学会OpenCV 11几何变换
OpenCV入门(十二)快速学会OpenCV 11几何变换1.图像平移2.图像旋转3.仿射变换4.图像缩放我们在处理图像时,往往会遇到需要对图像进行几何变换的问题。图像的几何变换是图像处理和图像分析的基础内容之一,不仅提供了产生某些图像的可…...

小菜鸟Python历险记:(第二集)
今天写的文章是记录我从零开始学习Python的全过程。Python基础语法学习:Python中的数值运算一共有7种,分别是加法()、减法(-)、除法(/)得到的结果是一个浮点数、乘法(*&a…...

ContentProvider程序之间数据的相互调用
1权限的获取和调用 权限分为普通权限和危险权限,除了日历信息,电话,通话记录,相机,通讯录,定位,麦克风,电话,传感器,界面识别(Activity-Recognit…...

金三银四最近一次面试,被阿里P8测开虐惨了...
都说金三银四涨薪季,我是着急忙慌的准备简历——5年软件测试经验,可独立测试大型产品项目,熟悉项目测试流程...薪资要求?5年测试经验起码能要个20K吧 我加班肝了一页半简历,投出去一周,面试电话倒是不少&a…...
算法题——给定一个字符串 s ,请你找出其中不含有重复字符的最长子串 的长度
给定一个字符串 s ,请你找出其中不含有重复字符的最长子串 的长度 示例 1: 输入: s “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例 2: 输入: s “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”&am…...

机器学习中的数学原理——F值与交叉验证
通过这篇博客,你将清晰的明白什么是F值、交叉验证。这个专栏名为白话机器学习中数学学习笔记,主要是用来分享一下我在 机器学习中的学习笔记及一些感悟,也希望对你的学习有帮助哦!感兴趣的小伙伴欢迎私信或者评论区留言࿰…...
vue.js介绍
个人名片: 😊作者简介:一名大一在校生,web前端开发专业 🤡 个人主页:python学不会123 🐼座右铭:懒惰受到的惩罚不仅仅是自己的失败,还有别人的成功。 🎅**学习…...

【设计模式】1、设计模式七大原则
目录一、单一职责二、接口隔离三、依赖倒置(倒转)四、里氏替换五、迪米特法则(Law of Demeter)六、开闭七、合成复用一、单一职责 类(或方法)功能的专一性。一个类(或方法)不应该承担…...
【前端老赵的CSS简明教程】10-1 CSS预处理器和使用方法
大家好,欢迎来到本期前端课程。我是前端老赵,今天的课程将讲解CSS预处理器的概念和使用方法,希望能够帮助大家更好地进行前端开发。 CSS预处理器是什么? CSS预处理器是一种将类似CSS的语言转换为CSS的工具。它们提供了许多额外的功能,如变量、嵌套、混入、函数等等。这些…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...

C++实现分布式网络通信框架RPC(2)——rpc发布端
有了上篇文章的项目的基本知识的了解,现在我们就开始构建项目。 目录 一、构建工程目录 二、本地服务发布成RPC服务 2.1理解RPC发布 2.2实现 三、Mprpc框架的基础类设计 3.1框架的初始化类 MprpcApplication 代码实现 3.2读取配置文件类 MprpcConfig 代码实现…...

使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...
在Spring Boot中集成RabbitMQ的完整指南
前言 在现代微服务架构中,消息队列(Message Queue)是实现异步通信、解耦系统组件的重要工具。RabbitMQ 是一个流行的消息中间件,支持多种消息协议,具有高可靠性和可扩展性。 本博客将详细介绍如何在 Spring Boot 项目…...

可视化预警系统:如何实现生产风险的实时监控?
在生产环境中,风险无处不在,而传统的监控方式往往只能事后补救,难以做到提前预警。但如今,可视化预警系统正在改变这一切!它能够实时收集和分析生产数据,通过直观的图表和警报,让管理者第一时间…...
视觉slam--框架
视觉里程计的框架 传感器 VO--front end VO的缺点 后端--back end 后端对什么数据进行优化 利用什么数据进行优化的 后端是怎么进行优化的 回环检测 建图 建图是指构建地图的过程。 构建的地图是点云地图还是什么信息的地图? 建图并没有一个固定的形式和算法…...