ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Cesium的无人机飞行姿态控制与轨迹定位系统:Vue3封装实践

基于Cesium的无人机飞行姿态控制与轨迹定位系统:Vue3封装实践

简介:面向高校毕业设计、课程设计与科研项目的无人机飞行仿真系统,运用Cesium与Vue3组合式API封装三维地理空间引擎,实现飞行姿态控制、实时状态监测与航迹定位三大核心模块。全键盘交互采用无冲突检测技术,支持多指令并行响应,控制面板可实时调节俯仰角、偏航角和滚转角;动态数据采集机制持续获取空速、航向与高程,仪表化界面直观呈现飞行参数;航迹追踪组件基于Leaflet构建,通过WGS84坐标系转换完成航点精确定位。压缩包共486个文件,以PNG界面素材、JS逻辑脚本、VUE组件、JSON配置及CSS样式为主,另含zbak备份、GLB三维模型、WASM引擎等辅助内容,整体大小51.91MB,目录结构清晰便于按模块查阅。已有53人学习下载,适合需要完成三维GIS仿真项目的开发者作为技术基准,前端工程化与模块化设计支持按需扩展和二次开发,可直接复用核心接口。

1. 基于Cesium的无人机飞行姿态控制与轨迹定位系统:Vue3封装为什么值得自己写一套

做三维GIS的同行应该都有体会:Cesium官方API能力很强,但把它塞进Vue3项目里总有一种“拧巴感” —— 所有viewer实例要挂在window上,组件卸载时destroy不干净,reactive包装后的对象传给Cesium会触发无限刷新,飞控数据接入后姿态角更新与模型旋转互相干扰。这正是我写这套“基于Cesium的无人机飞行姿态控制与轨迹定位系统实现(Vue3封装探索)”的初衷:不是做一个大而全的态势平台,而是把无人机飞控与轨迹定位这一条链路拆成可复用的Vue3组合式函数和组件,让后面接手的人不用再踩一遍我踩过的坑。本文会覆盖坐标系建模、姿态解算、轨迹插值、Vue3响应式封装边界,以及加载模型节点、动态光照、自定义绘制这类常被拿来二次开发的进阶点。适合已经跑过Cesium基础示例、想在Vue3项目里认真落地无人机业务的开发者。

2. 姿态与轨迹的数学基础:从WGS84坐标到东北天坐标系,四元数与欧拉角的转换不能想当然

2.1 两个坐标系必须分开建模,否则无人机在天上“乱转头”

无人机飞行姿态控制系统的第一步不是写代码,而是把坐标系捋清楚。Cesium默认的世界坐标系是WGS84地心地固系(ECEF),无人机飞控输出的定位数据一般是经纬度和高度(LLA),而姿态角(横滚roll、俯仰pitch、偏航yaw)是相对于机体坐标系的。如果直接把飞控输出的经纬度塞进entity.position,姿态角塞进entity.orientation,你的无人机会出现两个经典问题:模型原地疯狂抖动、机体朝向和实际航向偏了九十度。

常见做法是引入一个局部切平面坐标系——东北天(ENU)。以无人机起飞点为原点,X轴指向东、Y轴指向北、Z轴朝天。Cesium提供了Cesium.Transforms.eastNorthUpToFixedFrame(origin)来建立这个局部坐标系到ECEF的变换矩阵。我们的姿态解算全部在ENU系里做,最后用一次矩阵变换转回ECEF给Cesium渲染。这个隔离的意义在于:飞控输出的姿态是相对机体坐标系的,而ENU可以充当无人机的“惯性参考”;

坐标系选型时还有一条经验:短距离(几公里内)飞行用ENU做局部规划足够,但做跨区域长航时巡检,必须回到ECEF或UTM分带处理,否则累积误差会肉眼可见。

2.2 四元数转欧拉角的公式与Cesium的约定:HPR顺序和航向定义是最大的隐性坑

无人机飞控(常见的是PX4或ArduPilot)输出的姿态一般是四元数(q0, q1, q2, q3),而Cesium的Orientation需要的是HeadingPitchRoll对象,其中Heading是绕Z轴(朝下看)的旋转角,正北为0,顺时针为正。很多人直接把飞控的yaw角赋给heading,结果模型指向和实际航迹差了180度——因为飞控的偏航角定义是“机体前方相对正北的夹角”,而Cesium的Heading定义是“模型自身的朝向相对正北的夹角”,两者参考面一致,但在某些飞控固件里yaw范围是-180°到180°,Cesium是0°到360°,不归一化就会跳变。

我一般会在封装层写一个姿态转换函数,统一处理四元数到欧拉角、弧度到角度、角度范围归一化这三件事。核心公式如下:

// 四元数转欧拉角(弧度),Z-Y-X内旋顺序,对应Cesium的HPR export function quaternionToHPR(q) { const { x, y, z, w } = q; // 偏航角 heading:绕Z轴旋转,atan2(y*w + x*z 的分子结构需按坐标系定义小心处理) const heading = Math.atan2( 2 * (w * z + x * y), 1 - 2 * (y * y + z * z) ); // 俯仰角 pitch:绕Y轴,注意asin的值域裁剪,避免超出[-pi/2, pi/2] let pitch = Math.asin(2 * (w * y - z * x)); if (pitch > Math.PI / 2) pitch = Math.PI / 2; if (pitch < -Math.PI / 2) pitch = -Math.PI / 2; // 横滚角 roll:绕X轴 const roll = Math.atan2( 2 * (w * x + y * z), 1 - 2 * (x * x + y * y) ); // Cesium的HPR要求heading从正北顺时针为正,弧度制 return { heading: normalizeAngle(heading), pitch: pitch, roll: roll }; }

这段代码的关键在于asin部分做了值域裁剪。四元数归一化不好或者传输过程中丢精度时,2 * (w * y - z * x)可能略大于1,asin会返回NaN,整个姿态就黑了。我见过真实项目里机载SDK出现尾数误差,导致模型在天上突然翻转90度,就是这个原因。

normalizeAngle函数作用是确保heading落在[-PI, PI]区间,避免轨迹回放时航向角从179°跳到-179°造成旋转动画不连贯。

另一种常见做法是直接使用Cesium.Transforms.headingPitchRollQuaternion(origin, hpr)生成四元数赋给模型,这个函数期望的hpr.heading是相对正北的弧度。注意它内部是“先heading、再pitch、再roll”的旋转顺序,方向是Z-Y-X,和大多数飞控输出约定一致,但一定要确认你的飞控输出是“机体坐标系下绕Z-Y-X的欧拉角”,而不是“固定坐标系下的XYZ欧拉角”。这个搞错,无人机飞控数据接入调试会让你怀疑人生。

3. Vue3封装的核心设计:组合式函数管状态,组件管生命周期,响应式对象别乱传给Cesium

3.1 用Composable把viewer实例、飞控数据和轨迹数据拆成独立模块

把Cesium封装进Vue3,第一原则是:不要在setup()里直接new Cesium.Viewer后塞进ref就完事。Cesium的Viewer是一个巨大的纯JS对象,放进reactive会被代理,性能下降不说,还会触发Cesium内部的属性访问被Vue拦截,导致无限渲染循环。

我的封装结构是三层:useCesiumViewer负责创建和销毁Viewer;useDroneSimulation负责飞控数据的解析、姿态解算和轨迹点累积;useDroneEntity负责把解算后的数据同步到Cesium Entity上。三者都返回独立的响应式状态和操作方法,互不持有对方实例。以下是useCesiumViewer的骨架,展示了如何在Vue3中管理Cesium生命周期:

<script setup> import * as Cesium from 'cesium'; import { onMounted, onBeforeUnmount, shallowRef } from 'vue'; // 用shallowRef持有Viewer,避免深层响应式代理 const viewer = shallowRef(null); const container = ref(null); onMounted(() => { // Cesium.Viewer的配置集中在初始化阶段完成 viewer.value = new Cesium.Viewer(container.value, { animation: false, // 隐藏动画控件 timeline: false, // 隐藏时间轴 baseLayerPicker: false, // 隐藏图层选择器 geocoder: false, // 隐藏地理编码搜索 // 无人机轨迹展示不需要全默认控件,保留一个干净的场景 scene3DOnly: true, // 只启用3D场景,省去2D/Columbus视图切换开销 terrainProvider: Cesium.createWorldTerrainAsync() }); // 场景光照和大气效果对无人机姿态感知很重要 viewer.value.scene.globe.enableLighting = true; }); onBeforeUnmount(() => { if (viewer.value) { // 必须先销毁场景中的primitive和entity,再销毁viewer,否则GPU资源泄漏 viewer.value.entities.removeAll(); viewer.value.scene.primitives.removeAll(); viewer.value.destroy(); viewer.value = null; } }); </script> <template> <div ref="container" class="map-container"></div> </template>

这段代码的核心细节是shallowRef的选型。Viewer内部有几百个属性,ref会递归代理,Cesium内部在渲染循环里大量读取属性,Vue的getter拦截会拖慢帧率。shallowRef只代理最外层.value访问,Cesium实例内部照常运行,这是性能上的关键决策。

scene3DOnly: true是另一个容易被忽略的参数。无人机飞行通常只需要3D视图,关闭2D和Columbus视图可以减少一些底层切换逻辑的开销。动态光照enableLighting在无人机业务里有助于判断模型朝向与太阳方向的关系,对巡检场景尤其明显。

3.2 响应式数据流设计:飞控数据进ref,姿态解算出computed,实体更新用watchEffect

封装无人机模块,最难的不是Cesium API,而是怎么让飞控数据和Vue3的响应式系统优雅共存。我遵循一个很朴素的规则:Cesium需要的数据放在Cesium内部管理,业务态数据(飞行状态、轨迹坐标点列表、姿态角)放在Vue的ref里,只在数据到达的时刻同步一次。

下面是一个useDroneSimulation组合式函数的精简实现,聚焦在飞控数据接收和姿态解算的响应式链路上:

import { ref, computed } from 'vue'; export function useDroneSimulation(originLon, originLat, originAlt) { // 轨迹点累积,只存经纬度和时间戳,不在Vue中存Cesium对象 const trajectory = ref([]); // 当前姿态四元数,来自飞控数据 const attitudeQuaternion = ref({ x: 0, y: 0, z: 0, w: 1 }); // 经度、纬度、高度与姿态的响应式引用,由飞控数据更新 const position = ref({ lon: originLon, lat: originLat, alt: originAlt }); // 通过computed派生HPR,避免每次渲染都重新计算 const hpr = computed(() => { const q = attitudeQuaternion.value; return quaternionToHPR(q); }); // 模拟飞控数据流:实际项目多为WebSocket或UDP接入 function onFlightDataReceived(data) { const { q, lon, lat, alt, timestamp } = data; attitudeQuaternion.value = { x: q[0], y: q[1], z: q[2], w: q[3] }; position.value = { lon, lat, alt }; // 轨迹追加,长度超过阈值时丢弃最老的采样点 trajectory.value.push({ lon, lat, alt, timestamp }); if (trajectory.value.length > 10000) { trajectory.value.shift(); } } return { trajectory, hpr, position, onFlightDataReceived }; }

订阅飞控数据的方式要特别注意:不要在watch里做entity.position.setValue(),也不要让trajectory数组直接绑定到Cesium的PolylineGraphics.positions上——这会强行同步大量坐标点,Vue的差异更新逻辑再快也扛不住每帧几百个点都触发依赖。正确的做法是在watchEffect里手动调用Cesium对象的更新方法,比如:

watchEffect(() => { const pos = position.value; if (!droneEntity.value) return; // 直接调用Cesium的setValue,绕开Vue对Cesium对象的代理 droneEntity.value.position = Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.alt); droneEntity.value.orientation = Cesium.Transforms.headingPitchRollQuaternion( Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.alt), new Cesium.HeadingPitchRoll(hpr.value.heading, hpr.value.pitch, hpr.value.roll) ); });

这里一个隐蔽细节是:watchEffect默认在组件更新前执行,如果position在飞控数据回调里更新频率高于Cesium渲染帧率(飞控一般50Hz,Cesium渲染一般60fps),会出现一次渲染帧内多次位置更新。解决方式是用requestAnimationFrame做节流,或者干脆在viewer.clock.onTick里读取position.value最新值来更新实体。我推荐后者,因为Cesium的onTick天然和渲染帧同步,不需要额外构造函数做时间对齐。

3.3 模型加载与节点控制:glTF模型如何跟着姿态数据动起来

无人机在Cesium中一般用glTF格式的3D模型(如glb),通过ModelGraphics加载。加载时要注意模型自身的坐标系朝向:很多建模软件导出的模型默认机头指向Y轴或Z轴,而Cesium模型默认机头指向X轴(东),所以需要给模型加一个heading偏置。我一般会在模型实体上设置orientation为初始朝向,然后通过heading偏置量来对齐模型本身机头的方向。

模型节点的精细控制(比如旋翼旋转、云台俯仰)属于模型节点层级操作,常见做法是用modelNode遍历模型节点并控制其matrix或show属性。但这里有个粒子:Cesium的ModelGraphics在渲染时才构建节点树,model.readyPromise回调里才有权限遍历节点。无人机旋翼旋转这类效果,性能开销很大,建议只在近距离观察时启用,远距离直接隐藏。

4. 轨迹定位与飞行姿态可视化的完整实现:轨迹插值、姿态锥体、实时线宽与采样频率的取舍

4.1 轨迹定位:从飞控原始点到Cesium轨迹线的四步处理链

轨迹定位不能直接把原始点流全画上去。飞控数据是高频的(常见10Hz-50Hz),原始点画出来的轨迹线锯齿感严重,且数据量大了之后帧率骤降。我的处理链是:接收原始点 → 按距离阈值抽稀 → 平滑插值 → 动态更新Polyline。以下是根据距离阈值抽稀的实现:

function thinningTrajectory(points, minDistance = 2.0) { if (!points || points.length < 2) return points; const result = [points[0]]; let last = points[0]; // 逐点计算与上一个保留点的平面距离(米),小于阈值的丢弃 for (let i = 1; i < points.length; i++) { const p = points[i]; const dist = calcDistanceInMeters(last, p); if (dist >= minDistance) { result.push(p); last = p; } } return result; }

minDistance的取值直接决定轨迹线的细节和性能。低速悬停时无人机几乎不动,2米阈值会造成轨迹点稀疏、轨迹线滞后明显;高速巡航时2米又太密。实践中的做法是根据无人机的实时速度动态调整这个阈值:速度低于5m/s时用0.5米,高于15m/s时用5米。这样既保证了低速段轨迹的平滑,又控制了高速段的数据量。

插值方面,Cesium的Polyline本身支持线型插值,但我们要的是“轨迹线在时间轴上的推进”,所以常见做法是动态更新positions,让轨迹线“生长”。如果使用Cesium.SampledPositionProperty配合Clock,可以实现时间轴回放式的轨迹效果,但需要对Cesium时间机制有充分理解,而且数据量大会影响Clock跳动。我的做法是二选一:实时监控模式直接更新positions,回放模式才用SampledPositionProperty。

4.2 用CustomDataSource组织无人机实体,轨迹线与姿态锥体协同调度

一个无人机场景里往往有多架飞机,每架都有机身模型、轨迹线、姿态指示(机头朝向锥体或箭头)。如果都挂到viewer.entities上,后续筛选、隐藏、删除会非常痛苦。Cesium提供了CustomDataSource来做实体分组,但很多人没用起来。我的封装里,每个无人机实例对应一个CustomDataSource,内部管理四个实体:

const dataSource = new Cesium.CustomDataSource(`drone-${id}`); viewer.dataSources.add(dataSource); // 机身模型 const modelEntity = dataSource.entities.add({ position: Cesium.Cartesian3.fromDegrees(lon, lat, alt), orientation: Cesium.Transforms.headingPitchRollQuaternion(origin, hpr), model: { uri: '/models/drone.glb', scale: 1.0, minimumPixelSize: 32 } }); // 轨迹线,用PolylineGraphics const trailEntity = dataSource.entities.add({ polyline: { positions: new Cesium.CallbackProperty(() => { return tracePositions.value; }, false), width: 2.5, material: new Cesium.PolylineGlowMaterialProperty({ glowPower: 0.2, // 光晕强度,太大会模糊轨迹方向 color: Cesium.Color.YELLOW }), clampToGround: false // 无人机轨迹在空间,不贴地 } }); // 机头朝向指示锥体,用于姿态可视化 const headingCone = dataSource.entities.add({ orientation: new Cesium.VelocityOrientationProperty(modelEntity.position), cylinder: { length: 30.0, // 锥体长度,随速度可调 topRadius: 0.0, bottomRadius: 5.0, material: Cesium.Color.CYAN.withAlpha(0.6) } });

CallbackProperty在这里起到了关键作用。trailEntity的positions用回调函数返回tracePositions.value,Cesium每次渲染时读取最新值,不需要手动更新。注意第二个参数false表示不是常量,但Cesium不会自动检测依赖,所以需要在位置更新时手动调用requestRender或依赖viewer.clock驱动。

姿态指示锥体用VelocityOrientationProperty有个坑:Cesium会根据模型移动速度自动计算朝向,但无人机悬停时速度为零,锥体方向会乱跳。解决方式是不要用velocity,而是直接把hpr对象绑到orientation上,和机身模型一样设定。

4.3 轨迹线的三种常见形态:实时尾迹、历史回放、规划航线对比

实时监控、事后回放和航线规划这三个场景对轨迹可视化要求完全不同。实时尾迹适合用PolylineGlowMaterialProperty,既有科技感又能指示运动方向;历史回放需要时间轴控制,建议用SampledPositionProperty配合viewer.clock;规划航线对比则用普通PolylineGraphics加上线和虚线区分。

要注意轨迹线的高程模式。无人机轨迹是三维空间曲线,clampToGround必须设为false,但也别直接用默认值——如果无人机高度数据有噪声,轨迹线会剧烈抖动。我通常对高度做一阶低通滤波:

function lowPassFilter(current, previous, alpha = 0.3) { return previous + alpha * (current - previous); }

alpha取值0.1-0.3比较合适,太大滤波效果差,太小轨迹高度会滞后几十米。这个滤波要在飞控数据进入Vue状态之前做,不要在渲染时做,否则流畅度会受影响。

5. 避坑指南:Cesium与Vue3联动中的六个常见问题与排查路径

5.1 模型加载黑匣子:glTF模型显示不出来或朝向不对

现象:添加ModelGraphics后模型不显示,或者显示了但机头朝向与轨迹方向不一致。
原因:一是模型文件路径错误或跨域问题,二是模型自身坐标系朝向与Cesium约定不一致,三是模型没有设置minimumPixelSize导致距离远时被Cesium当作“过小像素”剔除。
解决:先检查浏览器Network面板确认模型请求是否200;再用viewer.scene.debugShowFramesPerSecond观察渲染帧率确认场景在刷新;对于朝向问题,最直接的解决方式是给orientation加一个固定偏置,比如heading + Math.PI / 2,把模型机头从X轴拨到Y轴。不要试图在建模软件里翻来覆去改模型坐标轴,那比在Cesium里偏置一次慢得多。

5.2 姿态翻转的玄学:四元数转欧拉角出现NaN或角度跳变

现象:无人机飞行中模型突然翻转180度,或者姿势角度变成NaN。
原因:飞控输出的四元数未归一化,以及asin函数输入超出[-1, 1]区间。另一个常见原因是从飞控数据解析时,四个分量的顺序和文档不一致(比如有的SDK输出[w, x, y, z],有的输出[x, y, z, w])。
解决:每次收到四元数先做归一化——计算模长,各分量除以模长;同时在quaternionToHPR里面对asin结果做钳制。排查顺序从简到难:先打印原始四元数看数值范围,再确认顺序,最后才怀疑公式用错。

5.3 轨迹线“鬼畜”抖动或位移偏差

现象:无人机轨迹线快速抖动,和实际位置明显不贴合。
原因:坐标系不一致。典型场景是飞控输出的是相对起飞点的局部坐标(如NED坐标系下的X/Y/Z偏移),而你把它们当成经纬度来用。
解决:在数据接入的入口处先明确坐标系,如果是局部偏移,需要用起飞点经纬度做一次ENU→LLA的转换。

5.4 Vue3响应式把Cesium对象“套进去”导致性能断崖

现象:接入飞控数据后帧率直接掉到个位数。
原因:把viewer或entity放进了reactive或ref,Vue对Cesium对象做了深层代理。
解决:全面改用shallowRef包裹Cesium实例,Cesium的更新操作一律用viewer.scene.requestRender()或者clock.onTick驱动,不要依赖Vue的响应式去触发Cesium的更新。

5.5 Cesium坐标转换在无人机高度上的精度丢失

现象:无人机在近地面飞行时轨迹线显示出明显的高度偏差。
原因:Cesium默认使用WGS84椭球体,而飞控的GPS高度一般是椭球高(MSL+大地水准面差距)。两者在多数地区有十几米的差值。
解决:接入高度前,先把GPS高度减去该地区的大地水准面差距(EGM96模型可查),或者和Cesium的heightReference设置相互配合。做无人机巡检时这个误差会直接影响云台朝向地面目标区域的精度。

5.6 组件卸载后Cesium资源未清理,导致切换路由时黑屏或报错

现象:在Vue3中切换到另一个路由再返回时,Cesium容器白屏或控制台抛DeveloperError: This object was destroyed。
原因:onBeforeUnmount里没有正确销毁viewer,或者销毁了但其他组件还持有数据源的引用。
解决:销毁顺序有讲究,先entities.removeAll()释放模型和轨迹线的GPU资源,再dataSources.removeAll(),最后viewer.destroy()并把shallowRef置空。另外最好加一个if (!viewer.value.isDestroyed())判断,防止重复销毁。

6. 进阶:多机协同、数据回放与轨迹热度的可视化增强技巧

到了这一步,基础的单机姿态控制和轨迹定位链路已经跑通。现在考虑实际业务中一定会遇到的两个进阶需求:多机协同监控和飞行数据事后回放。多机协同的核心挑战不是渲染,而是数据组织和性能分配。数据层面用CustomDataSource区分机器,性能层面则要关注粒子数量——每架无人机至少2-4个实体,10架无人机就可能有40个实体,如果不做LOD(细节层次)管理,再好的显卡也会卡。

我常用的一个LOD策略是:摄像机距离超过某阈值时隐藏机身模型显示一个简单的点或标签,只有拉近时才加载精细glTF模型。Cesium的distanceDisplayCondition正好可以做到这一点,每个无人机实体设置distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 1500),把模型的显示范围限制在1.5公里以内,远距离只显示轨迹线和标识。

数据回放则是另一个大坑。飞控数据是流式的,回放时要考虑时间轴对齐、姿态和轨迹的同步插值。SampledPositionProperty支持在两个采样点之间按照InterpolationAlgorithm插值,我的做法是设定forwardExtrapolationType为ExtrapolationType.HOLD,避免回放超过最后采样点后模型飞掉。回放速度建议用viewer.clock.multiplier控制,1倍速是实时,2倍速为快进,但插值精度会随速度变化,注意观察回放轨迹是否有尖角。

最后一个让轨迹可视化显得“专业”的技巧:给轨迹线添加速度热度图——用不同颜色表示飞行速度大小。实现方式并不复杂,把轨迹线拆成多个线段,每段颜色由该段的平均速度映射到彩色渐变上。Cesium的PolylineColorAppearance对每个PolylineSegment的顶点颜色支持良好。下面是一个简化的颜色分段示例:

// 按速度映射颜色的轨迹分段 const positions = /* 从抽稀后的轨迹点计算 */; const colors = positions.map((p, i) => { const speed = /* 计算第i段速度 */; // 速度区间映射到蓝→绿→红的渐变 if (speed < 5) return Cesium.Color.BLUE.withAlpha(0.8); if (speed < 15) return Cesium.Color.GREEN.withAlpha(0.8); return Cesium.Color.RED.withAlpha(0.8); }); const polyline = viewer.entities.add({ polyline: { positions, colors, width: 4 } });

注意,给polyline同时设置colors和material会冲突,Cesium会以colors为准。这里的“颜色随速度变化”在排查飞行异常时极其好用——能一眼看出无人机在哪个航段速度骤降,是遇到了侧风还是执行了悬停指令。

做这个系统这么久,我最深的教训是:不要把飞控数据直接接到Cesium上。中间必须隔一层坐标转换、滤波、抽稀的逻辑层,否则排错时你会分不清是数据问题、坐标系问题还是渲染问题。希望这套方案能帮你在Vue3里做无人机三维可视化时少走些弯路,把精力放在业务逻辑和飞行任务本身,而不是和Cesium的坐标系、生命周期搏斗。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表