记录--ThreeJs手搓一个罗盘特效

来源:博客园 时间:2023-05-10 18:07:52

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

先上效果

前言

最近在学Three.js.,对着文档看了一周多,正好赶上码上掘金的活动,就顺便写了一个小demo,手搓一个罗盘特效。

太极

先来看一下太极的实现方式,这里我们使用CircleGeometry,将其分解开来可以看出是由圆形和半圆形组成 。

CircleGeometry

CircleGeometry官网案例
radius半径
segments分段(三角面)的数量
thetaStart第一个分段的起始角度
thetaLength圆形扇区的中心角

这里不需要用到segments,但是需要颜色,所以定义一个函数传入半径、颜色、起始角度、中心角。

const createCircle = (r, color, thetaStart, thetaLength) => {    const material = new THREE.MeshBasicMaterial({    color: color,    side: THREE.DoubleSide    });    const geometry = new THREE.CircleGeometry(r, 64, thetaStart, thetaLength);    const circle = new THREE.Mesh(geometry, material);    return circle;  };

我们只需要通过传参生产不同大小的圆或半圆,再进行位移就可以实现其效果。

参考代码/73-96行还有一些需要注意的地方写在注释里了。

罗盘

接下来看罗盘的实现,罗盘由一个个圆环组成,一个圆环又由内圈、外圈、分隔线、文字、八卦构成。

内外圈

内外圈我们使用两个RingGeometry

RingGeometry官网案例
innerRadius内部半径
outerRadius外部半径
thetaSegments圆环的分段数
phiSegments圆环的分段数
thetaStart起始角度
thetaLength圆心角

通过circle控制内外圆圈的尺寸,circleWidth控制圆圈的线宽

const circleWidth = [0.1, 0.1]  const circle = [0, 1];  circle.forEach((i, j) => {    const RingGeo = new THREE.RingGeometry(      innerRing + i,      innerRing + i + circleWidth[j],      64,      1    );    const Ring = new THREE.Mesh(RingGeo, material);    RingGroup.add(Ring);  });

分隔线

分隔线使用的是PlaneGeometry

PlaneGeometry官网案例
width宽度
height高度
widthSegments宽度分段数
heightSegments高度分段数

关于分隔线,它的长度就是内外圈的差值,所以这里使用外圈的数值,确定与圆心的距离就要使用内圈的数值加上自身长度除2。除此之外,还需要计算分隔线与圆心的夹角。

for (let i = 0; i < lineNum; i++) {    const r = innerRing + circle[1] / 2;    const rad = ((2 * Math.PI) / lineNum) * i;    const x = Math.cos(rad) * r;    const y = Math.sin(rad) * r;    const planeGeo = new THREE.PlaneGeometry(lineWidth, circle[1]);    const line = new THREE.Mesh(planeGeo, material);    line.position.set(x, y, 0);    line.rotation.set(0, 0, rad + Math.PI / 2);    RingGroup.add(line);  }

文字

文字使用的是TextGeometry,定位与分隔线一致,只需要交错开来。

for (let i = 0; i < lineNum; i++) {      const r = innerRing + circle[1] / 2;      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;      const x = Math.cos(rad) * r;      const y = Math.sin(rad) * r;      var txtGeo = new THREE.TextGeometry(text[i % text.length], {        font: font,        size: size,        height: 0.001,        curveSegments: 12,      });      txtGeo.translate(offsetX, offsetY, 0);      var txt = new THREE.Mesh(txtGeo, material);      txt.position.set(x, y, 0);      txt.rotation.set(0, 0, rad + -Math.PI / 2);      RingGroup.add(txtMesh);

不过TextGeometry的使用有一个得注意得前提,我们需要引入字体文件。

const fontLoader = new THREE.FontLoader();const fontUrl =  "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/fonts.json";let font;const loadFont = new Promise((resolve, reject) => {  fontLoader.load(    fontUrl,    function (loadedFont) {      font = loadedFont;      resolve();    },    undefined,    function (err) {      reject(err);    }  );});

八卦

圆环中除了文字之外,还能展示八卦,通过传递baguaData给createBagua生成每一个符号。

const baguaData = [      [1, 1, 1],      [0, 0, 0],      [0, 0, 1],      [0, 1, 0],      [0, 1, 1],      [1, 0, 0],      [1, 0, 1],      [1, 1, 0],    ];    for (let i = 0; i < lineNum; i++) {      const r = innerRing + circle[1] / 2;      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;      const x = Math.cos(rad) * r;      const y = Math.sin(rad) * r;      RingGroup.add(        createBagua(baguaData[i % 8], x, y, 0 , rad + Math.PI / 2, text[0]),      );    }

createBagua参考代码/114-146行 ,和分隔线是一样的,使用了PlaneGeometry只是做了一些位置的设置。

视频贴图

在罗盘外,还有一圈视频,这里是用到了VideoTexture,实现也很简单。唯一得注意的是视频的跨域问题,需要配置video.crossOrigin = "anonymous"

const videoSrc = [    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/yAC65vN6.mp4",    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/6Z5VZdZM.mp4",  ];  video.src = videoSrc[Math.floor(Math.random() * 2)];  video.crossOrigin = "anonymous";  const texture = new THREE.VideoTexture(video);  ...        const material = new THREE.MeshBasicMaterial({      color: 0xffffff,      side: THREE.DoubleSide,      map: texture,    });

动画

动画总共分为三个部分,一块是旋转动画,一块是分解动画和入场动画,我们使用gsap实现。

旋转动画

gsap.to(videoGroup.rotation, {    duration: 30,    y: -Math.PI * 2,    repeat: -1,    ease: "none",  });

分解动画

.to(RingGroup.position, {        duration: 1,        ease: "ease.inOut",        y: Math.random() * 10 - 5,        delay: 5,      })      .to(RingGroup.position, {        duration: 1,        ease: "ease.inOut",        delay: 5,        y: 0,      })  }

入场动画

item.scale.set(1.2, 1.2, 1.2);    gsap.to(item.scale, {      duration: 0.8,      x: 1,      y: 1,      repeat: 0,      ease: "easeInOut",    });

旋转动画与分解动画可以写在生成函数内,也可以写在添加scene时,但是入场动画只能写到scene后,因为在生成时,动画就添加上了,当我们点击开始的时候才会将其加入场景中,而这时动画可能已经执行了。

总代码

html

                
大道五十,天衍四九,人遁其一
推衍中...

style

*{  margin: 0;  padding: 0;}body {  background-color: #3d3f42;}.box{  width: 350px;  height: 250px;  background-color: #000;  position:absolute;  top: calc(50% - 75px);  left: calc(50% - 150px);  border-radius: 10px;  font-size: 16px;  color: #fff;  display: flex;  flex-direction: column;  justify-content: space-evenly;  align-items: center;}.btn {  width: 120px;  height: 35px;  line-height: 35px;  color: #fff;  border: 2px solid #fff;  border-radius: 10px;  font-size: 20px;  transition: 0.5s;  text-align: center;  cursor:default;  opacity: 0.5;}img{  width: 200px;  height: 150px;}

js

import * as THREE from "three@0.125.1";import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";import { gsap } from "gsap@3.5.1";// Canvasconst canvas = document.querySelector("canvas.webgl");const box = document.querySelector(".box");const btn = document.querySelector(".btn");const video = document.createElement("video");// Sceneconst scene = new THREE.Scene(); //----------------------const fontLoader = new THREE.FontLoader();const fontUrl =  "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/fonts.json";let font;const loadFont = new Promise((resolve, reject) => {  fontLoader.load(    fontUrl,    function (loadedFont) {      font = loadedFont;      resolve();    },    undefined,    function (err) {      reject(err);    }  );});const text = {  五行: ["金", "木", "水", "火", "土"],  八卦: ["乾", "坤", "震", "巽", "坎", "艮", "离", "兑"],  数字: ["壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖", "拾"],  天干: ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"],  地支: [    "子",    "丑",    "寅",    "卯",    "辰",    "巳",    "午",    "未",    "申",    "酉",    "戌",    "亥",  ],  方位: [    "甲",    "卯",    "乙",    "辰",    "巽",    "巳",    "丙",    "午",    "丁",    "未",    "坤",    "申",    "庚",    "酉",    "辛",    "戍",    "干",    "亥",    "壬",    "子",    "癸",    "丑",    "艮",    "寅",  ],  节气: [    "立  春",    "雨  水",    "惊  蛰",    "春  分",    "清  明",    "谷  雨",    "立  夏",    "小  满",    "芒  种",    "夏  至",    "小  暑",    "大  暑",    "立  秋",    "处  暑",    "白  露",    "秋  分",    "寒  露",    "霜  降",    "立  冬",    "小  雪",    "大  雪",    "冬  至",    "小  寒",    "大  寒",  ],  天星: [    "天辅",    "天垒",    "天汉",    "天厨",    "天市",    "天掊",    "天苑",    "天衡",    "天官",    "天罡",    "太乙",    "天屏",    "太微",    "天马",    "南极",    "天常",    "天钺",    "天关",    "天潢",    "少微",    "天乙",    "天魁",    "天厩",    "天皇",  ],  天干1: [    "甲",    " ",    "乙",    " ",    "丙",    " ",    "丁",    " ",    "戊",    " ",    "己",    " ",    "庚",    " ",    "辛",    " ",    "壬",    " ",    "癸",    " ",    "甲",    " ",    "乙",    " ",  ],  地支1: [    "子",    " ",    "丑",    " ",    "寅",    " ",    "卯",    " ",    "辰",    " ",    "巳",    " ",    "午",    " ",    "未",    " ",    "申",    " ",    "酉",    " ",    "戌",    " ",    "亥",    " ",  ],};const data = [  {    innerRing: 2,    outerRing: 1.5,    lineWidth: 0.1,    circleWidth: [0.1, 0.1],    lineNum: 8,    text: [0xffffff],    offsetX: 0,    offsetY: 0,    size: 0.3,    direction: -1,    duration: 40,  },  {    innerRing: 3.5,    outerRing: 0.7,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 24,    text: text["方位"],    offsetX: -0.2,    offsetY: -0.08,    size: 0.3,    direction: 1,    duration: 10,  },  {    innerRing: 4.2,    outerRing: 0.7,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 24,    text: text["八卦"],    offsetX: -0.2,    offsetY: -0.08,    size: 0.3,    direction: -1,    duration: 20,  },  {    innerRing: 4.9,    outerRing: 1.3,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 24,    text: text["方位"],    offsetX: -0.4,    offsetY: -0.2,    size: 0.6,    direction: 1,    duration: 30,  },  {    innerRing: 6.2,    outerRing: 0.4,    lineWidth: 0.15,    circleWidth: [0, 0],    lineNum: 60,    text: text["地支"],    offsetX: -0.13,    offsetY: 0.01,    size: 0.2,    direction: 1,    duration: 25,  },  {    innerRing: 6.6,    outerRing: 0.4,    lineWidth: 0.15,    circleWidth: [0, 0],    lineNum: 60,    text: text["天干"],    offsetX: -0.13,    offsetY: -0.07,    size: 0.2,    direction: 1,    duration: 25,  },  {    innerRing: 7,    outerRing: 0.5,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 36,    text: text["天星"],    offsetX: -0.27,    offsetY: -0.03,    size: 0.2,    direction: -1,    duration: 20,  },  {    innerRing: 7.5,    outerRing: 0.5,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 24,    text: text["节气"],    offsetX: -0.36,    offsetY: -0.03,    size: 0.2,    direction: 1,    duration: 30,  },  {    innerRing: 8,    outerRing: 0.8,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 48,    text: text["方位"],    offsetX: -0.3,    offsetY: -0.1,    size: 0.4,    direction: 1,    duration: 35,  },  {    innerRing: 8.8,    outerRing: 0.8,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 32,    text: text["八卦"],    offsetX: -0.3,    offsetY: -0.1,    size: 0.4,    direction: -1,    duration: 60,  },  {    innerRing: 9.6,    outerRing: 0.4,    lineWidth: 0.18,    circleWidth: [0, 0],    lineNum: 120,    text: text["地支1"],    offsetX: -0.13,    offsetY: 0.01,    size: 0.2,    direction: 1,    duration: 30,  },  {    innerRing: 10,    outerRing: 0.4,    lineWidth: 0.18,    circleWidth: [0, 0],    lineNum: 120,    text: text["天干1"],    offsetX: -0.13,    offsetY: -0.07,    size: 0.2,    direction: 1,    duration: 30,  },  {    innerRing: 10.4,    outerRing: 0.5,    lineWidth: 0.1,    circleWidth: [0.1, 0.1],    lineNum: 60,    text: text["数字"],    offsetX: -0.13,    offsetY: -0.02,    size: 0.2,    direction: 1,    duration: 25,  },  {    innerRing: 10.9,    outerRing: 0.5,    lineWidth: 0.15,    circleWidth: [0.1, 0.1],    lineNum: 50,    text: text["五行"],    offsetX: -0.13,    offsetY: -0.02,    size: 0.2,    direction: 1,    duration: 35,  },  {    innerRing: 11.7,    outerRing: 1,    lineWidth: 0.1,    circleWidth: [1, 0],    lineNum: 64,    text: [0x000000],    offsetX: 0,    offsetY: 0,    size: 0.3,    direction: 1,    duration: 30,  },];const Rings = [];const duration = [  0, 0.7, 0.7, 0.7, 0.7, 0, 0.7, 0.7, 0.7, 0.7, 0.7, 0, 0.7, 0.7, 0.7,];//Ringconst Ring = ({  innerRing,  outerRing,  lineWidth,  circleWidth,  lineNum,  offsetX,  offsetY,  text,  size,  direction,  duration,}) => {  const RingGroup = new THREE.Group();  const circle = [0, outerRing];  const material = new THREE.MeshStandardMaterial({    color: 0xffffff,    side: THREE.DoubleSide,  });  // create ring  circle.forEach((i, j) => {    const RingGeo = new THREE.RingGeometry(      innerRing + i,      innerRing + circleWidth[j] + i,      64,      1    );    const Ring = new THREE.Mesh(RingGeo, material);    RingGroup.add(Ring);  });  // create line  for (let i = 0; i < lineNum; i++) {    const r = innerRing + circle[1] / 2;    const rad = ((2 * Math.PI) / lineNum) * i;    const x = Math.cos(rad) * r;    const y = Math.sin(rad) * r;    const planeGeo = new THREE.PlaneGeometry(lineWidth, circle[1]);    const line = new THREE.Mesh(planeGeo, material);    line.position.set(x, y, 0);    line.rotation.set(0, 0, rad + Math.PI / 2);    RingGroup.add(line);  }  // create text  if (text.length > 1) {    for (let i = 0; i < lineNum; i++) {      const r = innerRing + circle[1] / 2;      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;      const x = Math.cos(rad) * r;      const y = Math.sin(rad) * r;      var txtGeo = new THREE.TextGeometry(text[i % text.length], {        font: font,        size: size,        height: 0.001,        curveSegments: 12,      });      txtGeo.translate(offsetX, offsetY, 0);      var txtMater = new THREE.MeshStandardMaterial({ color: 0xffffff });      var txtMesh = new THREE.Mesh(txtGeo, txtMater);      txtMesh.position.set(x, y, 0);      txtMesh.rotation.set(0, 0, rad + -Math.PI / 2);      RingGroup.add(txtMesh);    }  }  // create bagua  if (text.length == 1) {    const baguaData = [      [1, 1, 1],      [0, 0, 0],      [0, 0, 1],      [0, 1, 0],      [0, 1, 1],      [1, 0, 0],      [1, 0, 1],      [1, 1, 0],    ];    for (let i = 0; i < lineNum; i++) {      const r = innerRing + circle[1] / 2;      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;      const x = Math.cos(rad) * r;      const y = Math.sin(rad) * r;      RingGroup.add(        createBagua(baguaData[i % 8], x, y, 0.0001, rad + Math.PI / 2, text[0]),        createBagua(baguaData[i % 8], x, y, -0.0001, rad + Math.PI / 2, text[0])      );    }  }  // animation  {    gsap.to(RingGroup.rotation, {      duration: duration,      z: Math.PI * 2 * direction,      repeat: -1,      ease: "none",    });        const amColor = { r: 1, g: 1, b: 1 };    const explode = gsap.timeline({ repeat: -1, delay: 5 });    explode      .to(RingGroup.position, {        duration: 1,        ease: "ease.inOut",        y: Math.random() * 10 - 5,        delay: 5,      })      .to(amColor, {        r: 133 / 255,        g: 193 / 255,        b: 255 / 255,        duration: 2,        onUpdate: () =>          ambientLight.color.setRGB(amColor.r, amColor.g, amColor.b),      })      .to(RingGroup.position, {        duration: 1,        ease: "ease.inOut",        delay: 5,        y: 0,      })      .to(amColor, {        r: 1,        g: 1,        b: 1,        duration: 3,        onUpdate: () =>          ambientLight.color.setRGB(amColor.r, amColor.g, amColor.b),      });  }  // rotate  RingGroup.rotateX(-Math.PI / 2);  return RingGroup;};//taijiconst createTaiji = (position, scale) => {  const taiji = new THREE.Group();  const createCircle = (r, color, thetaStart, thetaLength) => {    const material = new THREE.MeshBasicMaterial({      color: color,      side: THREE.DoubleSide,    });    const geometry = new THREE.CircleGeometry(r, 64, thetaStart, thetaLength);    const circle = new THREE.Mesh(geometry, material);    return circle;  };  const ying = createCircle(1.8, 0x000000, 0, Math.PI);  const yang = createCircle(1.8, 0xffffff, Math.PI, Math.PI);  const Lblack = createCircle(0.9, 0x000000, 0, Math.PI * 2);  const Lwhite = createCircle(0.9, 0xffffff, 0, Math.PI * 2);  const Sblack = createCircle(0.25, 0x000000, 0, Math.PI * 2);  const Swhite = createCircle(0.25, 0xffffff, 0, Math.PI * 2);  const Lblack1 = createCircle(0.9, 0x000000, 0, Math.PI * 2);  const Lwhite1 = createCircle(0.9, 0xffffff, 0, Math.PI * 2);  const Sblack1 = createCircle(0.25, 0x000000, 0, Math.PI * 2);  const Swhite1 = createCircle(0.25, 0xffffff, 0, Math.PI * 2);  Lblack.position.set(-0.9, 0, 0.001);  Lwhite.position.set(0.9, 0, 0.001);  Swhite.position.set(-0.9, 0, 0.002);  Sblack.position.set(0.9, 0, 0.002);  Lblack1.position.set(-0.9, 0, -0.001);  Lwhite1.position.set(0.9, 0, -0.001);  Swhite1.position.set(-0.9, 0, -0.002);  Sblack1.position.set(0.9, 0, -0.002);  taiji.add(    ying,    yang,    Lblack,    Lwhite,    Swhite,    Sblack,    Lblack1,    Lwhite1,    Swhite1,    Sblack1  );  gsap.to(taiji.rotation, {    duration: 30,    z: Math.PI * 2,    repeat: -1,    ease: "none",  });  taiji.rotateX(-Math.PI / 2);  taiji.position.set(...position);  taiji.scale.set(...scale);  return taiji;};scene.add(createTaiji([0, 0, 0], [1, 1, 1]));// baguaconst createBagua = (data, x, y, z, deg, color) => {  const idx = [-0.32, 0, 0.32];  const bagua = new THREE.Group();  const material = new THREE.MeshStandardMaterial({    color: color,    side: THREE.DoubleSide,  });  data.forEach((i, j) => {    if (i == 1) {      const yang = new THREE.Mesh(new THREE.PlaneGeometry(1, 0.2), material);      yang.position.set(0, idx[j], 0);      bagua.add(yang);    }    if (i == 0) {      const ying1 = new THREE.Mesh(        new THREE.PlaneGeometry(0.45, 0.2),        material      );      const ying2 = new THREE.Mesh(        new THREE.PlaneGeometry(0.45, 0.2),        material      );      ying1.position.set(-0.275, idx[j], 0);      ying2.position.set(0.275, idx[j], 0);      bagua.add(ying1, ying2);    }  });  bagua.position.set(x, y, z);  bagua.rotation.set(0, 0, deg);  return bagua;};const showVideo = () => {  const videoSrc = [    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/yAC65vN6.mp4",    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/6Z5VZdZM.mp4",  ];  video.src = videoSrc[Math.floor(Math.random() * 2)];  video.crossOrigin = "anonymous";  const texture = new THREE.VideoTexture(video);  const videoGroup = new THREE.Group();  for (let i = 0; i < 8; i++) {    const r = 25;    const rad = ((2 * Math.PI) / 8) * i;    const x = Math.cos(rad) * r;    const y = Math.sin(rad) * r;    const planeGeo = new THREE.PlaneGeometry(16, 9);    const material = new THREE.MeshBasicMaterial({      color: 0xffffff,      side: THREE.DoubleSide,      map: texture,    });    const plane = new THREE.Mesh(planeGeo, material);    plane.position.set(x, 4.5, y);    if (i % 2 == 0) plane.rotation.set(0, rad + Math.PI / 2, 0);    else plane.rotation.set(0, rad, 0);    videoGroup.add(plane);  }  gsap.to(videoGroup.rotation, {    duration: 30,    y: -Math.PI * 2,    repeat: -1,    ease: "none",  });  scene.add(videoGroup);};//loadFont, RingsloadFont.then(() => {  data.forEach((item) => {    Rings.push(Ring(item));  });  btn.innerText = "入 局";  btn.style.opacity = 1;  btn.style.cursor = "pointer";});//startconst start = function () {  const showRing = (item) => {    scene.add(item);    item.scale.set(1.2, 1.2, 1.2);    gsap.to(item.scale, {      duration: 0.8,      x: 1,      y: 1,      repeat: 0,      ease: "easeInOut",    });  };  const tl = gsap.timeline();  Rings.forEach((item, idx) => {    tl.to(".webgl", { duration: duration[idx] }).call(() => {      showRing(item);    });  });};btn.addEventListener("click", () => {  box.style.display = "none";  start();  showVideo();  video.play();  video.loop = true;});//---------------------- //Lightconst ambientLight = new THREE.AmbientLight(0xffffff, 1);scene.add(ambientLight); //Sizesconst sizes = {  width: window.innerWidth,  height: window.innerHeight,};// Cameraconst camera = new THREE.PerspectiveCamera(  75,  sizes.width / sizes.height,  1,  1000);camera.position.y = 10;camera.position.x = 10;camera.position.z = 10;camera.lookAt(scene.position);scene.add(camera);//Rendererconst renderer = new THREE.WebGLRenderer({  canvas: canvas,  antialias: true,  alpha: true,});renderer.setSize(sizes.width, sizes.height);//controlsconst controls = new OrbitControls(camera, canvas);controls.enableDamping = true;controls.maxDistance = 50;controls.enablePan = false;const tick = () => {  renderer.render(scene, camera);  controls.update();  window.requestAnimationFrame(tick);};tick();window.addEventListener("resize", () => {  sizes.height = window.innerHeight;  sizes.width = window.innerWidth;  camera.aspect = sizes.width / sizes.height;  camera.updateProjectionMatrix();  renderer.setSize(sizes.width, sizes.height);  renderer.setPixelRatio(window.devicePixelRatio);});

本文转载于:

https://juejin.cn/post/7220629398965108794

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

  • 热门推荐

Copyright   2015-2022 南非日报网 版权所有  备案号:沪ICP备2022005074号-13   联系邮箱:58 55 97 3@qq.com