0


vue3+three.js实现疫情可视化

前言

自成都九月份以来疫情原因被封了一两周,居家着实无聊,每天都是盯着微信公众号发布的疫情数据看,那种页面,就我一个前端仔来说,看着是真的丑啊!(⊙_⊙)?既然丑,那就自己动手开整!项目是2022.9.5开始的,截止2022.9.12我完成了大概有八成。主要是想让数据更加直观,而且可离线下载(当然还有装逼!┑( ̄Д  ̄)┍)。

项目描述

为证明是有料的,先看效果图(提前装逼!┗|`O′|┛ 嗷~~):请添加图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
项目我是公开了的( ̄m ̄)有兴趣的可以下下来玩玩,这是我第一次使用vue3+ts构建项目,肯定还有不足的地方(比如ts中疯狂的:any,一直any一直爽^o^/)。
这里是在线链接
这里是项目链接(欢迎star!欢迎star!欢迎star!(●’◡’●)嘿嘿嘿~)
项目中使用到的技术有:vue3、TypeScript、Three.js、Echarts、elementPlus。

项目目标

1、以为3D形式展示全球疫情分布。
2、显示实时疫情数值。
3、以图表形式分析疫情数据。
4、允许下载各地疫情excel表格。
5、自动获取用户位置。
6、分析当地疫情数据。
7、生成当地疫情word报告。

api说明

本项目数据来源:新浪公共疫情api(新浪的数据来源于国家卫健委、各省市区卫健委、各省市区政府、港澳台官方渠道等公开数据。这也是够权威官方了)。我主要使用了两个新浪的api和一个太平洋网络ip地址查询web接口。
1、https://news.sina.com.cn/project/fymap/ncp2020_full_data.json
get方式,无入参。该api可获取全球各国大致疫情数据,以及国内的详情疫情数据,这里就api中的字段做一下说明,字段是我自己推测出来的含义,不会100%全而准(→_→):
在这里插入图片描述

{"add_daily(国内今日数据)":{"addcon(今日确诊新增数)":"","addcure(今日治愈新增)":"","adddeath(今日死亡新增)":"","addjwsr(今日境外输入新增)":"","addlocIncrNum(今日本土新增)"},"cachetime(数据缓存时间)":"","curetotal(国内治愈总数)":"","deathtotal(国内死亡总数)":"","gntotal(国内确诊总数)":"","highAndMiddle(中高风险地列表)":[{"allname(全名)":"","list(城市列表)":"","province(省名)":"","province_high_areas(高风险区域)":"","province_middle_areas(中风险区域)":"","province_high_num(高风险区域数)":"","province_middle_num(中风险区域数)":"","province_total(风险地总数)":""}],"historylist(国内疫情历史数据)":[{"cn_conNum(确诊总数)":"","cn_cureNum(治愈总数)":"","cn_deathNum(死亡数)":"","cn_jwsrNum(境外输入)":"","ymd(当前时间)":""}],"jwsrTop(境外数据前10列表)":[],"list(全国各省疫情数据列表)":[{"asymptomNum(较昨日新增数)":"","city(城市列表)":[],"cureNum(治愈数)":"","deathNum(死亡数)":"","econNum(现存确诊数)":"","ename(英文省名)":"","jwsrNum(境外输入数)":"","name(省名)":"","value(累计数)":""}],"locIncrProTop(本土新增前十列表)":[],"othertotal(其他总数)":{"certain(全球现存确诊)":"","certain_inc(今日确诊新增数)":"","die(全球死亡数)":"","die_inc(死亡新增数)":"","ecertain(全球治愈数)":"","ecertain_inc(治愈新增数)":""},"times(数据截止时间)":"","worldlist(世界各国疫情列表)":[{"name(国名)":"","value(累计数)":"","econNum(确诊数)":"","deathNum(死亡数)":"","cureNum(治愈数)":""}]}

2、https://gwpre.sina.cn/interface/news/ncp/data.d.json
get方式,入参:

{mod:"province",province(英文省名):""}

该api可获取国内指定省份的疫情数据,字段我就不推断了,可自行根据上一个api和部分英文单词大概推断出来(没错!就是我不想打字了!太累了!ಥ_ಥ其实这还不算折磨人的,后面使用api的时候那才叫个曲折)。
3、https://whois.pconline.com.cn/ipJson.jsp
get方式,无入参。该api可获取使用者ip地址、省份、城市。返回结果如下:
在这里插入图片描述

数据使用

刚开始开发的时候,我跟以前项目开发一样,跨域嘛,直接整个vue代理(不会vue代理模式的看这儿)不就完事儿了,果然,数据一经过代理,回来是回来了,但汉字全是\n什么什么鬼?乱码?费了一番功夫查了下,不是乱码,是unicode解码的问题。然后我又整了个解码的方法:

//解码返回的unicodefunctiondecodingStr(str: any){letrepStr: any = str.replace(/\\/g,"%");//用%替换\let str1 = repStr.split("jsoncallback(")[1]let str2 = str1.split(");")[0]//截取出需要的字符串let unStr =unescape(str2);//解码出汉字let jsonObj =JSON.parse(unStr);//转换成json对象return jsonObj;};

这下应该可以了吧?一切很顺利,开发差不多了,npm run build、git add . 、git commit -m""、git push,行云流水!直接上gitee Pages部署发布,完成!打开页面一看?嗯?f12。404?直到后面我又在网上扒拉后才明白,vue的代理在打包成dist后会被抽离失效,在gitee Pages中是不能使用vue的代理模式获取数据的!接下来就是各种尝试跨域,直到看到跨域两个字人都麻了。最后发现不同域下,使用jsonp的方式来处理跨域最为简单,jsonp原理和使用方法在这里。

项目开始

项目是vue3的,首先你得创建啊。这里建议使用vue脚手架的图形化界面创建项目,命令为:vue ui
选择手动配置:
在这里插入图片描述
打开TypeScript支持:
在这里插入图片描述
选择vue3选项:
在这里插入图片描述

安装依赖

1、npm install echarts@4.9.0(安装echarts的指定版本,因为项目中需要使用中国地图,在4.9.0之后echarts官方移除了地图支持,之后的版本需要下载chain.js,还得手动下载引入一遍,麻烦,这里直接用老版本)
2、npm i element-plus(vue3对应的element-ui就是element-plus,这是官方使用文档)
3、npm i three(看见首页那个大地球了吧?没错,它就是three.js做的,感兴趣的可以看这儿,还有个太阳系)
4、npm i xlsx(这个是下载excel表格的必备插件,具体使用方法看这里)

首页球体

1、创建宇宙(叼不叼!是不是感觉自己就是创世主!( ̄_, ̄ )):初始化场景时一定记得设置alpha: true。这里创建宇宙我使用了这篇文章创建背景的第三种方法。

import*asTHREEfrom"three";//初始化球体functioninit(data: any){
  dom = document.getElementById("sphereDiv");//获取domlet width = dom.clientWidth;let height = dom.clientHeight;
  scene =newTHREE.Scene();//场景场景
  camera =newTHREE.PerspectiveCamera(45, width / height,1,1000);//创建透视相机(视场、长宽比、近面、远面)
  camera.position.set(0,0,270);//设置相机位置
  camera.lookAt(0,0,0);//创建渲染器
  renderer =newTHREE.WebGLRenderer({antialias:true,//抗锯齿alpha:true,//透明});
  renderer.setClearColor(0x000000,0.1);//设置场景透明度
  renderer.setSize(width, height);//设置渲染区域尺寸
  dom.appendChild(renderer.domElement);//将渲染器添加到dom中形成canvascreateUniverse();//创建宇宙createStars();//创建星辰createLight();//创建光源createSphere(data);//创建球体createOrbitControls();render();};//创建宇宙(球形宇宙)functioncreateUniverse(){let universeGeometry =newTHREE.SphereGeometry(500,100,100);let universeMaterial =newTHREE.MeshLambertMaterial({//高光材质map:newTHREE.TextureLoader().load(universeImg),side:THREE.DoubleSide,//双面显示});//宇宙网格let universeMesh =newTHREE.Mesh(universeGeometry, universeMaterial);
  universeMesh.name ="宇宙";
  scene.add(universeMesh);};

2、创建光源:为了效果,我使用了环境光与平行光源,这两种光都会影响贴图原本颜色,建议光源颜色设置为白色。

//创建光源functioncreateLight(){let lightColor =newTHREE.Color(0xffffff);let ambient =newTHREE.AmbientLight(lightColor);//环境光
  ambient.name ="环境光";
  scene.add(ambient);let directionalLight1 =newTHREE.DirectionalLight(lightColor);
  directionalLight1.position.set(0,0,1000);
  scene.add(directionalLight1);//平行光源添加到场景中let directionalLight2 =newTHREE.DirectionalLight(lightColor);
  directionalLight2.position.set(0,0,-1000);
  scene.add(directionalLight2);//平行光源添加到场景中let directionalLight3 =newTHREE.DirectionalLight(lightColor);
  directionalLight3.position.set(1000,0,0);
  scene.add(directionalLight3);//平行光源添加到场景中let directionalLight4 =newTHREE.DirectionalLight(lightColor);
  directionalLight4.position.set(-1000,0,0);
  scene.add(directionalLight4);//平行光源添加到场景中let directionalLight5 =newTHREE.DirectionalLight(lightColor);
  directionalLight5.position.set(0,1000,0);
  scene.add(directionalLight5);//平行光源添加到场景中let directionalLight6 =newTHREE.DirectionalLight(lightColor);
  directionalLight6.position.set(0,-1000,0);
  scene.add(directionalLight6);//平行光源添加到场景中};

3、创建球体:

//创建球体functioncreateSphere(data: any){let earthSize =100;//地球尺寸let earthGroup =newTHREE.Group();//地球的组let earthGeometry =newTHREE.SphereGeometry(earthSize,100,100);//地球几何体let nightColor =newTHREE.Color(0x999999);let dayColor =newTHREE.Color(0x444444);//地球材质let earthMaterial =newTHREE.MeshPhongMaterial({map:newTHREE.TextureLoader().load(
      isDay ? earthImg : earthNightImg //区分昼夜纹理),color: isDay ? dayColor : nightColor,// metalness: 1, //生锈的金属外观(MeshStandardMaterial材质时使用)// roughness: 0.5, // 材料的粗糙程度(MeshStandardMaterial材质时使用)normalScale:newTHREE.Vector2(0,5),//凹凸深度normalMap:newTHREE.TextureLoader().load(normalImg),//法线贴图});let earthMesh =newTHREE.Mesh(earthGeometry, earthMaterial);//地球网格
  earthMesh.name ="地球";
  earthGroup.add(earthMesh);//将地球网格添加到地球组中
  earthGroup.name ="地球组";
  scene.add(earthGroup);createVirus(data, earthSize);//创建球面病毒};

在这里插入图片描述
放大项目中的地球你会发现球体表面是有凹凸而且反光的(就像稀泥巴一样≡(▔﹏▔)≡),这是因为使用了three中MeshPhongMaterial材质同时设置了属性normalScale与normalMap。
在这里插入图片描述
4、渲染:创建完成后记得渲染,否则是不会生效的。

//渲染functionrender(){
  anId.value =requestAnimationFrame(render);
  document.getElementById("sphereDiv")&&
    document
      .getElementById("sphereDiv")!.addEventListener("mousemove", onMousemove,false);

  orbitControls.update();//鼠标控件实时更新
  renderer.render(scene, camera);};

控制球体

首页的3D球体是可以进行鼠标控制的。
在这里插入图片描述
这是使用的three.js自带的鼠标控件OrbitControls ,它的参数可以自己设置,这是鼠标控制的方法:

import{ OrbitControls }from"three/examples/jsm/controls/OrbitControls";//创建鼠标控件functioncreateOrbitControls(){
  orbitControls =newOrbitControls(camera, renderer.domElement);
  orbitControls.enablePan =false;//右键平移拖拽
  orbitControls.enableZoom =true;//鼠标缩放
  orbitControls.enableDamping =true;//滑动阻尼
  orbitControls.dampingFactor =0.05;//(默认.25)
  orbitControls.minDistance =150;//相机距离目标最小距离
  orbitControls.maxDistance =500;//相机距离目标最大距离
  orbitControls.autoRotate =true;//自转(相机)
  orbitControls.autoRotateSpeed =1;//自转速度
  orbitControls.enableRotate =true;//鼠标左键控制旋转};

创建病毒

被瞎说!covid19可不是我创造的!(;´д`)ゞ

//创建病毒functioncreateVirus(data: any,earthSize: any){let colors =[newTHREE.Color(0xf9b8b8),newTHREE.Color(0xfe4242),newTHREE.Color(0xff0000),];//病毒颜色列表let virSize =4;//病毒大小let pointsGroup =newTHREE.Group();//点的组let list =JSON.parse(JSON.stringify(data));
  list.forEach((e:{value: number; color: any; position: any[];})=>{
    e.value >=10000000&&(e.color = colors[2]);//根据病毒数赋予不同颜色
    e.value >=500000&& e.value <10000000&&(e.color = colors[1]);
    e.value <500000&&(e.color = colors[0]);if(e.position){let virusMaterial =newTHREE.SpriteMaterial({color: e.color,map:newTHREE.TextureLoader().load(virusImg),side:THREE.FrontSide,//只显示前面});//病毒材质let Sprite =newTHREE.Sprite(virusMaterial);//点精灵材质
      Sprite.scale.set(virSize, virSize,1);//点大小let lat = e.position[1];//纬度let lon = e.position[0];//经度let s =latLongToVector3(lat, lon, earthSize,1);//坐标转换
      Sprite.position.set(s.x, s.y, s.z);//设置点的位置
      Sprite.dotData = e;//将点的数据添加到dotData属性中
      Sprite.name ="病毒";
      pointsGroup.add(Sprite);//添加进点的组中}});
  pointsGroup.name ="病毒组";
  scene.add(pointsGroup);//点的组添加到旋转组中};

球面上那些红的、粉的、白的玩意儿就是病毒。其实就是利用three的SpriteMaterial材质加入点精灵Sprite中,再遍历病毒的坐标列表数据,循环设置Sprite的position属性,最后再将创建好的病毒组添加到宇宙场景中,值得注意的是,你一般获取到的数据均为经纬度坐标,需要转换为three能用的三维向量坐标,这是坐标转换的方法:

//经纬度坐标变换(传入e:纬度、a经度、t球半径、o球额外距离)functionlatLongToVector3(e: any,a: any,t: any,o: any){var r =(e * Math.PI)/180,
    i =((a -180)* Math.PI)/180,
    n =-(t + o)* Math.cos(r)* Math.cos(i),
    s =(t + o)* Math.sin(r),
    l =(t + o)* Math.cos(r)* Math.sin(i);returnnewTHREE.Vector3(n, s, l);//计算三维向量};

数值增加动画

在首页右侧和“国内分析”右侧,有一排数字,那个数字在加载时是有数值增加动画的。
在这里插入图片描述

本来想直接使用vue-count-to或者vue-countupjs的,但网上我扒拉了一下,发现原理好像不是很难,无非就是利用vue的数据响应式原理,但我看到的大多封装的组件都是vue2的,vue3好像没有。这里我就整一个vue3+ts的版本:

<template><span :data-time="time":data-value="value">{{addNum}}</span></template><script lang='ts' setup>import{ ref, computed, watch, onMounted }from'vue';let props =defineProps({//动画时间time:{type: Number,default:2},//停止时的值value:{type: Number,default:0},//千位的逗号thousandSign:{type: Boolean,default:()=>false}}),oldValue: any =ref(0),addNum: any =ref(0);//响应式的数值watch(()=> props.value,()=>{startAnimation();//值改变时开始动画})functionstartAnimation(){letvalue: number = props.value - oldValue.value;let step =(value *10)/(props.time *100);let current =0;let start = oldValue.value;//定时器lett: any =setInterval(()=>{
        start += step;if(start > value){clearInterval(t);
            start = value;
            t =null;}if(current === start){return;}
        current = Math.floor(start);//取整
        oldValue.value = current;if(props.thousandSign){
            addNum.value = current.toString().replace(/(\d)(?=(?:\d{3}[+]?)+$)/g,'$1,');//添加千位符}else{
            addNum.value = current.toString();//无千位符}},10)}onMounted(()=>{startAnimation();})</script>

value是动画终止时数值,time是动画时间,thousandSign表示是否添加千分位符,必须保证value与time类型为Number,这是使用方法:

<addNumber class="addcure-div":value="addcure":time="10":thousandSign="true"/>

表格展示

页面上的所有表格都是使用的element-plus的表格组件。这是具体使用方法。当表格做出来之后发现样式颜色之类的并不是自己想要的。其实可以更改css变量。注意,不是全局修改,是局部修改样式。比如你想让表格变成这样透明的:
在这里插入图片描述
那么你可以这样做:

<el-table :data="tabData" style="width:100%;height:calc(100vh - 100px);--el-table-bg-color:rgba(0,0,0,.8);--el-table-tr-bg-color:transparent;--el-table-header-bg-color:#333;--el-table-header-text-color:#fff;--el-table-text-color:#fff;--el-table-row-hover-bg-color:#333;--el-table-border-color:#333"><el-table-column type="index" label="序号" width="100"/><el-table-column prop="name" label="国家"/><el-table-column prop="value" label="累计数" sortable /><el-table-column prop="deathNum" label="死亡数" sortable /><el-table-column prop="cureNum" label="治愈数" sortable /><el-table-column prop="citycode" label="地区代码"/><el-table-column label="坐标"><template #default="scope">{{ scope.row.position ? scope.row.position :"-"}}</template></el-table-column></el-table>

其中的css变量名,你可以f12获取到。

横向柱状图

首页左侧和“国内分析”这样的横向柱状图。
在这里插入图片描述
在echarts里面配置项是这样的:

//柱状图数据letoption: any ={title:{text: titName + sliceNum,left:"center",textStyle:{color:"#fff",},},tooltip:{backgroundColor:"rgba(0,0,0,.5)",borderWidth:"0",trigger:'axis',textStyle:{color:"#fff",fontWeight:"bolder"},},grid:{top:"10%",left:"10%",right:"10%",bottom:"0%",},xAxis:{type:'value',show:false,},yAxis:{type:'category',axisLabel:{color:"#fff",},data:[],},series:[{data:[],type:'bar',showBackground:true,backgroundStyle:{color:'rgba(180, 180, 180, 0.2)'},itemStyle:{color: color,},label:{color:"#fff",fontWeight:"bolder",show:true,align:"left",formatter:"{c}",},}]}

颜色、位置、标题可自己设置。

中国地图

在“国内分析”中有一个中国地图,用来展示国内现存确诊分布,支持缩放拖拽。
在这里插入图片描述

如果你下载的最新的echarts的话,那么你得折腾下,自己另外单独找chain.js下载,然后引入项目,4.9.0是内置地图的,可以直接使用。这是地图的配置项:

letoption: any ={title:{text:'国内各省现存分布',left:"center",top:'1%',textStyle:{color:"#fff",},},visualMap:{min:0,max:500,left:'5%',bottom:'5%',text:['高','低'],textStyle:{color:'#fff',},calculable:true,inRange:{color:['#fff','#f00'],//颜色范围},},tooltip:{padding:10,enterable:true,transitionDuration:0,//动画时间backgroundColor:"rgb(0,0,0,.8)",borderRadius:20,textStyle:{color:'#fff',},formatter:function(params: any){let tipString ="";if(params.data.value){
          tipString ="<div style='font-size:25px;font-weight:900;margin:10px 0px'>"+ params.data.name +"</div>"+"<div style='color:#f00;font-weight:900;'>现存:"+ params.data.value +"</div>"+"<div style='color:#888;font-weight:900;'>累计:"+ params.data.allNum +"</div>"+"<div style='color:#888;font-weight:900;'>死亡:"+ params.data.deathNum +"</div>"+"<div style='color:#888;font-weight:900;'>治愈:"+ params.data.cureNum +"</div>"+"<div style='color:#888;font-weight:900;'>较昨日新增:"+ params.data.asymptomNum +"</div>"+"<div style='color:#888;font-weight:900;'>境外输入:"+ params.data.jwsrNum +"</div>"}return tipString;}},series:[{name:'接入医院数量',type:'map',mapType:'china',zoom:1.2,//缩放roam:true,scaleLimit:{min:1.2,//缩放限制max:2},itemStyle:{normal:{label:{show:true}},emphasis:{show:true,areaColor:'#6eb5ff',//鼠标滑过区域颜色label:{show:true}}},label:{normal:{//静态的时候展示样式show:true,//是否显示地图省份得名称textStyle:{color:"#000",fontSize:12}},emphasis:{//动态展示的样式color:'#fff',},},data:[]}]};

formatter也就是hover出来的信息框,可以自定义。

历史分析

“国内分析”是有个历史数据展示的折线图的,它是允许缩放调节的。
在这里插入图片描述
这是它的echarts配置项:

letoption: any ={// backgroundColor: "",grid:{// top: "15%",// left: "5%",// right: "5%",// bottom: "10%",},title:{text:'国内历史数据',left:"center",top:'5%',textStyle:{color:"#fff",},},tooltip:{backgroundColor:"rgba(0,0,0,.5)",borderWidth:"0",trigger:'axis',textStyle:{color:"#fff",fontWeight:"bolder"},axisPointer:{type:'cross'},},legend:{data:['确诊数','治愈数','死亡数','境外输入'],textStyle:{color:"#fff"},orient:"vertical",top:"15%",right:"2%"},xAxis:{data: lineData.map(function(item: any){return item.ymd;}),textStyle:{color:"#fff"}},yAxis:{textStyle:{color:"#fff",},},dataZoom:[{startValue:''},{type:'inside'}],series:[{name:'确诊数',type:'line',lineStyle:{color:'#f4c25e'},itemStyle:{color:'#f4c25e'},data: lineData.map(function(item: any){return item.cn_conNum;}),zlevel:1,z:1,},{name:'治愈数',type:'line',lineStyle:{color:'#48c56b'},itemStyle:{color:'#48c56b'},data: lineData.map(function(item: any){return item.cn_cureNum;}),zlevel:1,z:1,},{name:'死亡数',type:'line',lineStyle:{color:'#f00'},itemStyle:{color:'#f00'},data: lineData.map(function(item: any){return item.cn_deathNum;}),zlevel:1,z:1},{name:'境外输入',type:'line',lineStyle:{color:'#8903ba'},itemStyle:{color:'#8903ba'},data: lineData.map(function(item: any){return item.cn_jwsrNum;}),zlevel:1,z:1}]};

表格下载

主要是利用xlsx插件,好用得很,直接传入数据就出表格了,这是我的使用方法:

import*asXLSXfrom"xlsx";//入参示例let eg ={fileName:"测试",//文件名tabHead:["国家","人口","测试"],//表头列表keyList:["name","population","test"],//表头对应的属性名,顺序必须与表头对应tabData:[{name:"中国",population:"11",test:"t1"},{name:"美国",population:"22",test:"t2"},{name:"日本",population:"35",test:"t3"}]//对象数组}//导出数据表格exportdefaultasyncfunctiondownloadXlsx(tabObj: any){letaoaList: any =[];
    aoaList[0]= tabObj.tabHead;//赋值表头列表
    tabObj.tabData.forEach((tabItem: any,tabIndex: number)=>{
        aoaList[tabIndex +1]=[];//该二维度数组必须多加一个元素,因为表头占第一个元素
        tabObj.keyList.forEach((keyItem: any,keyIndex: number)=>{let val = tabItem[keyItem];//获取表格属性的值((typeof val =="undefined")||(val ==""))?(aoaList[tabIndex +1][keyIndex]="-")://数据未定义或者为空则用"-"代替(aoaList[tabIndex +1][keyIndex]= val +"");//添加空字符串,防类型为非字符串})});let workSheet =null;
    workSheet =XLSX.utils.aoa_to_sheet(aoaList);//将列表数据添加到工作表let workBook =XLSX.utils.book_new();//创建一个工作薄XLSX.utils.book_append_sheet(workBook, workSheet,"1");//将工作表添加到工作薄中awaitXLSX.writeFile(workBook, tabObj.fileName +".xlsx");//写入文件,下载工作薄};

结语

我做这个其实还是学到了很多东西,主要是vue2与vue3的区别。以前的this算是可以彻底抛弃了,还有就是组合式api配合setup语法糖,爽啊!就连组件引入后都不用注册了,直接使用。不过也有注意点,用ref()声明的响应式变量使用时需要加.vaule。至于ts,我感觉自己还是很菜啊,一直any一直爽。。。。。。ts类型系统直接被我无视了,后面还得观摩观摩其他大佬咋写的。截止到现在2022.9.13成都疫情好转要复工了,项目其实都是没有完成的,主要还差“省内分析”和“下载当地疫情报告”,我做了alert提示,反正数据获取到了,后面在搞,我项目是完全开源了的,有牛逼的可以直接clone下来开发完成(我想要白嫖!(╯▔皿▔)╯)。
在这里插入图片描述
在这里插入图片描述


本文转载自: https://blog.csdn.net/xi1213/article/details/126824752
版权归原作者 xi12 所有, 如有侵权,请联系我们删除。

“vue3+three.js实现疫情可视化”的评论:

还没有评论