ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ArcGIS JS API 4.x双屏联动:MapView与SceneView状态同步实战

ArcGIS JS API 4.x双屏联动:MapView与SceneView状态同步实战 二三维联动双屏这个需求我在好几个项目里都碰到过这阵子又用ArcGIS JavaScript API 4.x做了一版踩了不少坑干脆把实现思路和关键代码整理出来。如果你手上正好接到类似“左边二维地图、右边三维场景操作一边另一边跟着动”的需求这篇文章应该能帮你省下不少调试时间。先说明一点这里用的是ArcGIS JavaScript API 4.x版本不是3.x那套老接口。4.x里MapView和SceneView这套设计天生就适合做双屏关键是搞清楚两个视图之间到底该同步哪些状态、怎么同步才不会互相打架。1. 双屏联动到底在解决什么问题先说需求场景做功能之前一定要先搞明白业务方到底想要什么不然很容易做成“看起来在联动实际没法用”的玩具。我最近做的是一个城市片区展示项目。业务方希望在系统首页左右分屏展示同一片区域的整体面貌左边用二维地图看路网、用地范围、建筑基底方便做宏观分析和标注右边用三维场景看楼宇高度、地形起伏、环境关系让不懂GIS的领导也能直观感受到规划效果。两边要能联动左边拖动地图右边跟着转右边旋转视角飞到某栋楼左边也自动把中心点移过去。这种需求背后的核心诉求其实很简单——所见即所指。用户在任一视图里关注某个位置时另一个视图能立刻跟上帮助用户在宏观和微观、整体和局部之间来回切换。听起来不复杂但涉及到的技术点一点都不少。1.1 典型应用场景有哪些除了我做的城市规划展示二三维联动的应用场景远比想象中广片区规划汇报展示二维图上画个范围或者点个POI三维场景自动飞过去非专业人员也能快速理解方案。应急指挥调度二维视图掌握全局态势和资源分布三维视图看重点建筑、道路、避难场所的立体关系。地下管线与设施管理二维视图管管线拓扑、检修记录三维视图快速定位井盖、阀门、管段的具体空间位置。园区或校园可视化二维看全园布局三维看某一栋楼的楼层结构联动后鼠标点哪里都能对应上。科研教学演示同一套专题数据在两个视图下对照讲解比如地质构造、水文分析结果。这些场景的共性都一样单靠二维或者单靠三维都无法完整满足需求必须两个视图协作而协作的前提就是状态同步。1.2 这个功能真正的难点在哪难点不在于“把两个视图放在同一个页面上”只要是WebGIS开发者布局这种事十分钟就能搞定。难点在于两个视图的状态模型不一致。二维MapView的核心状态是center中心点、zoom缩放级别、rotation旋转角度三维SceneView的核心状态是camera相机这个相机对象里面又包含position位置、heading航向角、tilt俯仰角好几个子属性。二维没有相机概念三维没有单纯的zoom概念即使两个地方都有zoom数值含义和使用方式也有细微差别。如果你只是简单地“二维变了就把center赋给三维三维变了就把position赋给二维”大概率会得到一种体验很差的联动三维视角总是被重置、二维旋转方向不对、两边比例尺对不上、甚至还可能出现两个视图互相触发导致无限刷新的情况。所以这篇文章会围绕四个部分展开两个视图的状态模型到底怎么对应、完整的双屏实现代码长什么样、我在实际调试中踩到的那些关键坑、以及从“能联动”到“联动得好用”需要做哪些优化。2. 视图状态同步的核心逻辑MapView与SceneView的对应关系要写出靠谱的联动先得把MapView和SceneView这两个视图类的状态模型摸清楚。2.1 两个视图各自的关键状态属性MapView二维的关键状态属性类型说明centerPoint二维地图中心点包含经纬度坐标zoomnumber缩放级别整数或小数scalenumber比例尺值与zoom相关但不完全相同rotationnumber地图旋转角度正北为0单位度stationaryboolean视图是否停止交互/动画判断是否完成更新很有用SceneView三维的关键状态属性类型说明cameraCamera相机对象包含位置和朝向信息camera.positionPoint相机所在空间位置有x/y/z坐标camera.headingnumber相机朝向正北为0顺时针为正单位度camera.tiltnumber相机俯仰角0度代表垂直向下看camera.fovnumber视野范围zoomnumber三维视图的缩放级别与相机高度关联scalenumber三维视图的比例尺值viewingModestringglobal全球场景或local局部场景从表格能看出来二维和三维的状态并不是一一对应的。最直接的对应该关系是view2D.center↔view3D.camera.position的经度和纬度view2D.zoom↔view3D.zoom两者都能表示视野范围但不是严格的线性关系view2D.rotation↔view3D.camera.heading旋转方向一致性还要看具体表现view3D.camera.tilt→ 二维没有对应属性通常忽略不同步这个映射关系是整个联动代码的地基。我见过很多新手一上来就用goTo把整个视图对象丢过去比如view3D.goTo(view2D.center)看起来能跑但真正交互起来就会露馅。原因就在于goTo接收的参数在不同视图里默认行为不一样比如你只传一个Point给SceneView它可能会把相机直接拉到一个默认俯视角把用户正在看的倾斜角度给重置掉。2.2 同步中心点时为什么不能直接赋值整个对象先说一个血泪教训如果直接把view2D.center整个对象赋给view3D.camera.position会出问题。// 错误示范直接赋值对象 view3D.camera.position view2D.center;这段代码不一定报错但隐患很大。view2D.center返回的是一个Point对象直接赋值相当于把对象的引用或者属性拷贝过去position内部可能还关联着空间参考、z值等二维视图中压根不存在的属性。更重要的是这种写法不受控你没法在同步时保留三维相机的tilt、fov等用户自定义状态。二维的中心点本身没有z值直接赋值后三维相机的z高度可能直接被覆盖成0或者undefined画面瞬间就乱了。正确的思路是每次同步时只提取需要的标量值重新组装成目标视图能接受的对象。比如从二维同步到三维正确做法是const center view2D.center; const camera view3D.camera.clone(); camera.position { x: center.longitude, y: center.latitude, z: camera.position.z // 保留原来的高度 }; camera.heading view2D.rotation; view3D.goTo(camera);这样三维相机的经纬度跟二维中心点一致但高度和俯仰角保持用户原来设置的样子体验要自然得多。2.3 为什么用中间变量而不是让两个view互相直接监听实现联动最直觉的做法是双向监听view2D的center/zoom/rotation变化时更新view3Dview3D的camera变化时更新view2D但真这么做会踩到“事件风暴”的坑二维一变三维跟着变三维一变又触发二维的监听二维又把值赋给三维……两个视图会无限互相触发页面卡死。所以必须引入一个同步状态锁也就是用isUpdating这样的布尔变量标记“正在同步中禁止再次触发”。在看代码之前我们先明确一下“中间变量”的思路。我在项目里通常不直接在监听回调里写同步逻辑而是维护一个统一的状态对象const syncState { center: [116.39, 39.9], zoom: 12, heading: 0 };哪个视图发生变化先更新这个中间变量再由这个变量去驱动另一个视图。这样能理清数据流向避免两个视图之间直接互相依赖排查问题也方便。3. 完整实现从页面布局到联动逻辑这里直接给一个能跑通的完整页面基于ArcGIS JavaScript API 4.28版本。如果使用其他4.x版本接口基本通用。3.1 HTML页面骨架与布局左右分屏布局用flex即可每个视图容器各占50%宽度。注意ArcGIS JS API的视图容器必须有确定的宽高否则会渲染失败或者出现空白。!DOCTYPE html html langzh-CN head meta charsetutf-8 / meta nameviewport contentinitial-scale1, maximum-scale1, user-scalableno / titleArcGIS JS API 二三维联动双屏/title link relstylesheet hrefhttps://js.arcgis.com/4.28/esri/themes/dark/main.css / style html, body, #viewDiv { width: 100%; height: 100%; margin: 0; padding: 0; } #viewDiv { display: flex; } .view-item { width: 50%; height: 100%; position: relative; } #toggleBtn { position: absolute; top: 15px; left: 50%; transform: translateX(-50%); z-index: 999; padding: 8px 18px; border: none; background-color: #007ac2; color: #fff; font-size: 14px; cursor: pointer; border-radius: 4px; box-shadow: 0 1px 6px rgba(0, 0, 0, 0.3); } /style /head body div idviewDiv div idviewDiv2D classview-item/div div idviewDiv3D classview-item/div /div button idtoggleBtn开启联动/button /body /html这里有个布局细节要注意#toggleBtn悬浮在整个页面上方层级用z-index保证在ArcGIS视图的控制按钮之上。ArcGIS视图容器内部是动态DOM按钮如果放在视图容器内部有可能会被ArcGIS的UI组件覆盖掉。3.2 引入ArcGIS JS API并初始化两个视图ArcGIS JS API 4.x的常规引入方式是通过CDN的script标签加require去加载模块。如果你的项目用了npm和打包工具也可以使用arcgis/core那种ESM方式但这里先用CDN方式方便快速验证。require([ esri/Map, esri/views/MapView, esri/views/SceneView ], function (Map, MapView, SceneView) { // 共享同一个map实例 const map new Map({ basemap: topographic }); // 二维视图 const view2D new MapView({ container: viewDiv2D, map: map, center: [116.39, 39.9], zoom: 12, constraints: { rotationEnabled: true } }); // 三维视图 const view3D new SceneView({ container: viewDiv3D, map: map, center: [116.39, 39.9], zoom: 12 }); });初始化时让两个视图的中心点和缩放级别保持一致这样用户第一眼看到的是同一位置的画面。3.3 联动同步核心逻辑重点来了。我这里的实现思路是监听二维视图的view-change事件这个事件在视图状态变化时触发比单独监听center更跟手监听三维视图的camera属性变化用isUpdating锁防止无限循环用联动开关控制是否执行同步完整代码如下let isLinked false; let isUpdating false; // 二维 - 三维 function sync2DTo3D() { if (isUpdating || !isLinked) return; isUpdating true; const center view2D.center; const camera view3D.camera.clone(); camera.position { x: center.longitude, y: center.latitude, z: camera.position.z }; camera.heading view2D.rotation; view3D.goTo(camera).finally(function () { isUpdating false; }); } // 三维 - 二维 function sync3DTo2D() { if (isUpdating || !isLinked) return; isUpdating true; const camera view3D.camera; view2D.goTo({ center: [camera.position.x, camera.position.y], zoom: view3D.zoom, rotation: camera.heading }).finally(function () { isUpdating false; }); } view2D.on(view-change, function () { if (isLinked !isUpdating) { sync2DTo3D(); } }); view3D.watch(camera, function () { if (isLinked !isUpdating) { sync3DTo2D(); } });这段代码有几点值得展开说第一view2D.on(view-change)vsview2D.watch(center)。watch(center)能监听到中心点的变化但center本身是个对象对象属性变化时的监听行为在ArcGIS里容易产生意想不到的频次或者漏监听。view-change是MapView在每次视图刷新后都会触发的综合性事件包含了中心点、缩放、旋转的全部变化更适合作为联动入口。不过要注意view-change触发的频率非常高拖拽地图时每帧都可能触发如果每次都执行goTo动画性能上会有压力。后面我在优化部分会讲怎么降频。第二为什么三维这边监听camera属性而不是view-change。view3D.watch(camera, handler)是ArcGIS响应式系统提供的监听方式可以监听camera属性的变化。camera是一个对象但这里的watch回调会在camera指向的对象发生改变时触发已经够用了。第三goTo后面的.finally()非常关键。goTo返回一个Promise用finally确保不管动画成功还是失败isUpdating都会被重置。如果你用的是.then()当goTo被新的交互打断时Promise会rejectisUpdating会一直停在true后续所有联动都会失效。这个问题我在第四节会详细讲这里先记住一个原则同步标志位的重置一定要放在finally里。3.4 联动开关按钮逻辑页面上放了一个“开启联动”按钮业务上经常需要让用户自由切换联动状态。代码逻辑非常简单const toggleBtn document.getElementById(toggleBtn); toggleBtn.addEventListener(click, function () { isLinked !isLinked; toggleBtn.textContent isLinked ? 关闭联动 : 开启联动; if (isLinked) { // 开启联动的瞬间主动同步一次让两个视图先对齐 const center3D view3D.camera.position; view2D.goTo({ center: [center3D.x, center3D.y], zoom: view3D.zoom, rotation: view3D.camera.heading }); } });开启联动时主动同步一次是很多实现里容易漏掉的地方。因为用户可能先单独操作了三维视图等想开启联动时二维还停留在老位置如果不主动对齐一次画面已经不一致了联动又从何谈起。3.5 组装完整代码时的几个细节把前面的代码拼在一起基本就能跑起来了。但有几个细节值得留意CSS里两个视图的宽度要写死为50%不能用百分比加paddingArcGIS视图容器尺寸自适应能力有限改动后可能需要调用view.resize()。共享同一个Map实例能保证底图、图层、符号等完全一致用户一眼看上去就是同一个地方的两个维度不会因为底图不同而产生割裂感。如果需要在三维场景中加载自定义模型、切片图层建议在初始化SceneView之前先把图层加到Map上否则三维视图不会自动刷新新增的图层。4. 实测中一定要避开的四个关键坑代码能跑起来只是第一步真实项目里难的是处理边界情况。我把调试过程中踩到的坑整理成四条每条都是实际出现过的问题不分先后但都很致命。4.1 无限回环与同步锁失效这是二三维联动最典型的坑。理论上isUpdating锁能防止无限循环但实际使用中锁的设计如果不严谨还是会有问题。我第一版代码是这样的view2D.on(view-change, function () { if (isLinked !isUpdating) { isUpdating true; view3D.goTo({...}).then(function () { isUpdating false; }); } });试了几下就发现交互几分钟后联动突然失效了。排查了很久才发现是goTo动画在飞行途中被用户下一次操作打断Promise走到了reject分支isUpdating永远留在true锁死。修复很简单把.then换成.finally就行view3D.goTo({...}).finally(function () { isUpdating false; });如果你的项目需要兼容不支持Promise.prototype.finally的老浏览器那就用.then和.catch双重处理view3D.goTo({...}) .then(function () { isUpdating false; }) .catch(function () { isUpdating false; });另一个思路是给goTo传入signal用AbortController取消上一次未完成的动画。这个方式适合交互频繁的场景let syncAbortController null; function sync2DTo3D() { if (syncAbortController) { syncAbortController.abort(); } syncAbortController new AbortController(); view3D.goTo(camera, { signal: syncAbortController.signal }).finally(function () { syncAbortController null; isUpdating false; }); }4.2 zoom数值精度与比例尺不一致在同步三维到二维时我用过view2D.goTo({ zoom: view3D.zoom })。结果发现在二维立体感强的区域操作时两边的视野范围偶尔会轻微对不上。后来查了文档才意识到虽然MapView和SceneView都有zoom属性但两者的zoom计算逻辑并不完全一致。二维的zoom是基于当前坐标系和切片方案直接计算出来的三维的zoom则是根据相机高度、视野范围、屏幕大小反推出来的。你从三维反推出来的zoom值赋给二维二维里会再按自己的规则转换成比例尺这个过程一定有精度损失。解决方案有两个层面常规场景直接用zoom同步因为用户基本感知不到零点几级的缩放差异这是最简单可靠的方式。对精度要求高的场景改用scale去同步。两个视图都支持scale属性且scale是底层的比例尺值语义一致。不过实测中个别情况下scale同步会出现微小抖动所以要看实际效果决定采用哪个。我的经验是zoom同步绝大多数场景够用scale同步在对比例尺要求严格的测绘类项目中更稳。建议你在项目里两个都试一下肉眼观察哪个更符合业务预期就选哪个。4.3 旋转方向不一致rotation与heading的差二维的rotation和三维的heading语义上都表示“正北为0、顺时针为正的角度”但实际项目中我遇到了奇怪的现象在二维视图旋转地图到30度切换到三维时相机heading在有些情况下会变成330度两个视图的北向居然不一致。后来确认是MapView的rotation在跨过南北方向时会自动取负数表达。比如rotation设置为350MapView内部可能表示为-10从-10再赋给三维时camera.heading接收了-10三维视角就偏了20度。解决办法是对角度做归一化统一映射到0-360区间function normalizeAngle(deg) { return ((deg % 360) 360) % 360; } // 同步时 camera.heading normalizeAngle(view2D.rotation); // 反向同步时 view2D.rotation normalizeAngle(view3D.camera.heading);这段函数很简单但值好几千块因为不处理的话用户旋转到某个角度后联动就乱了这种bug特别难排查。4.4 二维和三维的“坐标系”不能想当然ArcGIS JS API 4.x默认使用Web MercatorEPSG:3857和WGS84地理坐标系EPSG:4326之间自动转换center传的是经纬度数组。但是在三维场景中尤其是使用viewingMode: local的时候坐标系可能不是WGS84或者Mercator这时候直接把view3D.camera.position.x和.y当成经纬度赋给二维的center坐标就完全错位了。这里有个简单的验证方法三维视图初始化后在控制台输入view3D.camera.position.spatialReference查看它的wkid。如果是102100或3857说明是Web Mercator如果是4326说明是WGS84。我在某个项目里加载了地方坐标系的服务到三维场景camera.position直接返回的是当地平面坐标不是经纬度二维同步过去就完全偏到海里去了。后来加了坐标转换逻辑用webMercatorUtils把平面坐标转成经纬度再赋给二维视图。const position view3D.camera.position; let lngLat; if (position.spatialReference.isWebMercator) { lngLat webMercatorUtils.webMercatorToGeographic(position); } else { lngLat position; } view2D.goTo({ center: [lngLat.longitude, lngLat.latitude] });所以写联动代码前先确认两个视图的坐标系是否一致不要默认就是经纬度。这句话能帮你省下很多调试时间。5. 从“能联动”到“联动得好用”场景化演进基础版跑通之后如果你直接在真实环境里用大概率还是会被吐槽“太生硬”“不顺滑”。接下来这几个优化点是我在多个项目里不断打磨出来的经验。5.1 如何让三维在联动时保持当前视角基础版代码里从二维同步到三维时我用的是camera.position { x: center.longitude, y: center.latitude, z: camera.position.z };这样保留了原来的高度。同时因为是clone出来的camera对象tilt、fov、heading这些属性也都保留了下来。但如果直接用view3D.goTo({ center: [lng, lat] })这种写法tilt会被重置成默认的俯视角用户会觉得“我明明在看楼宇立面你一动就变成鸟瞰图了”。这里要理解ArcGIS三维相机的行为goTo一个目标对象时如果目标对象不含tilt和heading字段SceneView会沿用当前相机的值但如果用的是goTo(Object)的方式传入了position那tilt的默认值可能会重新生效具体表现因版本和场景而异。为了避免不确定性我习惯显式地从当前view3D.camera中读取所有字段组装成完整的新相机对象再传入goTo。const currentCamera view3D.camera; const newCamera currentCamera.clone(); newCamera.position { x: center.longitude, y: center.latitude, z: currentCamera.position.z }; view3D.goTo(newCamera);5.2 减少同步频率只在交互结束时触发基础版里监听的是view-change这个事件在拖拽过程中每帧都在触发。二维拖拽一下三维就连续飞好几帧动画体验看起来“很跟手”但性能消耗极大特别是三维场景里如果加载了建筑物、点云这类重数据连续飞行会把帧率拖垮。更好的策略是改在交互结束时触发同步。监听指针抬起和双击结束let syncTimer null; function scheduleSync2DTo3D() { if (syncTimer) clearTimeout(syncTimer); syncTimer setTimeout(function () { sync2DTo3D(); }, 100); } view2D.on(pointer-up, scheduleSync2DTo3D); view2D.on(double-click, scheduleSync2DTo3D);用100毫秒的防抖既保证用户操作停止后能同步又不会在连续操作时造成动画风暴。这个时间是我调出来的经验值建议你在真实设备上感受一下太快容易闪烁太慢又显得滞后。实际项目里如果用户拖拽后马上又要看另一边100毫秒基本无感但性能压力小很多。同样的思路也适用于三维到二维的同步因为camera的监听在三维里也极其频繁。5.3 底图选择与视觉一致性共享同一个Map实例是好习惯但底图不一样会导致视觉不一致。我在实际项目中发现业务方经常要求在三维用影像底图加建筑白模在二维用矢量图看起来更专业。这时候联动依然成立因为gps坐标一致但视觉上用户会短暂疑惑“这两个是同一个地方吗”。如果业务没有特殊要求建议两个视图用同一套底图。你可以通过动态切换Map的basemap属性来控制。另外提一个海外底图在国内网络环境下的加载速度问题。ArcGIS Online自带底图在国外访问速度快但在国内业务系统中经常出现加载失败或很慢。如果是给国内用户做系统建议使用天地图、高德、百度这类国内底图服务。ArcGIS JS API加载天地图需要自己写一个BaseTileLayer子类这个临时写起来工作量大网上有现成示例可以直接用。如果底图加载慢会导致联动开启后两个视图画面不同步——三维已经飞过去了二维底图还在转圈。排查的时候先确认底图服务本身可用再考虑代码逻辑。5.4 联动粒度与业务扩展联动不只是位置同步实际项目中我陆续还做了这些扩展图层显隐联动在二维图层列表里关闭了某个图层三维场景里对应的图层同步消失。图形标注联动在二维图上画的点、线、面自动在三维场景中以对应位置展示或者反过来。量测结果联动二维量测的长度面积切到三维后显示同样的量测结果。视点/书签联动点击某个预设书签两个视图同时飞行到对应位置。比例尺和指北针联动两个视图的比例尺、指北针状态保持一致。这些扩展本质上都是在“状态同步”这条线上增加更多同步字段。只要核心联动架构设计得清晰扩展起来并不难。我目前的实现里把同步逻辑抽成了一个独立的SyncManager模块接收两个view实例和一组可选的同步配置项内部统一处理状态搬运。这样每个项目接入时只需要传配置不用重写核心逻辑。// 伪代码展示结构 const syncManager new SyncManager(view2D, view3D, { updateCenter: true, updateZoom: true, updateRotation: true, syncOnInteractionEnd: true, debounceInterval: 100 });这种模块化的设计在第二个项目里就明显省事了新需求只需要加配置项联动核心不会因为业务变化而频繁返工。6. 测试与调试的几个实用技巧最后分享几个调试技巧都是我在项目里真刀真枪用过的。6.1 在控制台快速验证同步状态打开浏览器控制台手动改变二维视图的状态观察三维是否跟着变。这个操作非常直观// 在控制台执行 view2D.goTo({ center: [121.47, 31.23], zoom: 15 });如果三维视角没有跟着飞过去说明二维到三维的同步链路有问题断点打在sync2DTo3D函数第一行去排查。6.2 用view.stationary判断是否完成ArcGIS的视图有一个stationary属性表示视图是否停止所有动画和交互。在联动里可以用它来判断同步是否完成view3D.watch(stationary, function (isStationary) { if (isStationary) { // 三维动画完成此时读取camera值同步给二维更准确 sync3DTo2D(); } });我自己一般用goTo的Promise更直接但如果你遇到同步值“差一点”的问题可以试试等stationary为true时再读取状态。6.3 性能调优图层面数越多越要控制频次项目里三维场景如果加载了倾斜摄影、BIM模型这些大数据量图层联动时的连续动画基本等于自杀。这种情况下我强烈建议使用“交互结束才同步”的方案并且把防抖时间调大一些比如200到300毫秒。同时可以考虑在三维动画飞行过程中隐藏部分重量级图层等动画结束再显示能极大提升流畅度。6.4 多显示器环境下的视野差异双屏联动有一种特殊情况如果两个视图分别在不同显示器上展示大屏项目很常见显示器分辨率和宽高比不一致会导致肉眼可见的视野差异。三维场景尤其明显同样的zoom宽屏显示器看到的范围更广。这种情况不适合靠zoom或scale精确同步可以考虑增加一个“校正因子”。我在一个指挥大厅项目里把三维zoom乘以一个0.98的系数再赋给二维让两边的视野范围肉眼看更接近。这个系数没有标准答案得在目标设备上慢慢调。写在最后的几点体会ArcGIS JavaScript API做二三维联动双屏本质上是在两个视图状态模型之间做翻译和搬运。理解了center/camera/zoom/rotation/heading这些状态之间的关系代码本身并不复杂真正的复杂度全在边界处理上——同步锁、角度归一化、坐标系转换、动画打断、性能控制每一条都能让一个看似正常的联动功能在特定场景下失效。从我做的几个项目来看最稳妥的实施路径是先跑通最简单的双向同步然后根据真实业务场景逐步加上防抖、视角保持、坐标转换这些细节最后再考虑扩展联动字段。不要一上来就追求大而全先把核心链路打通后续的优化才有基础。如果你正在做类似功能照着这篇文章的代码搭一版再把四个坑提前规避掉基本上一天内就能出一个可演示的版本。后面遇到具体问题欢迎一起交流。
返回列表