• 售前

  • 售后

热门帖子
入门百科

微信小步伐canvas实现签名功能

[复制链接]
yfyffuuy 显示全部楼层 发表于 2021-10-25 19:50:34 |阅读模式 打印 上一主题 下一主题
在微信小程序项目中,开发模块涉及得手写签名功能,微信小程序canvas闪亮登场
媒介
微信小程序canvas实现签名功能
焦点内容简介:
(1)签名实现,开始,移动,结束
(2)重写
(3)完成
(4)上传
一、微信小程序canvas实现签名功能

效果演示:
(1)签名实现

(2)重写

(3)完成

完成后将图片展示在相应的位置
(4)根据业务需求,可以将图片上传到配景,在必要的地方展示
二、上代码

1.全部演示
wxml
  1. <!--pages/canvas-test/canvas-test.wxml-->
  2. <view class="handCenter">
  3. <canvas class="handWriting" disable-scroll="true" bindtouchstart="uploadScaleStart" bindtouchmove="uploadScaleMove"
  4. bindtouchend="uploadScaleEnd" bindtap="mouseDown" canvas-id="handWriting">
  5. </canvas>
  6. </view>
  7. <view class="handBtn">
  8. <button catchtap="retDraw" class="delBtn">重写</button>
  9. <button catchtap="subCanvas" class="subBtn">完成</button>
  10. </view>
  11. <view class="preview">
  12. <image wx:if="{{tmpPath}}" style="width:100%;height:100%;" src="{{tmpPath}}"></image>
  13. </view>
复制代码
js
  1. const app = getApp()
  2. const api = require('../../utils/request.js'); //相对路径
  3. const apiEev = require('../../config/config');
  4. Page({
  5. data: {
  6. canvasName: 'handWriting',
  7. ctx: '',
  8. canvasWidth: 0,
  9. canvasHeight: 0,
  10. transparent: 1, // 透明度
  11. selectColor: 'black',
  12. lineColor: '#1A1A1A', // 颜色
  13. lineSize: 1.5, // 笔记倍数
  14. lineMin: 0.5, // 最小笔画半径
  15. lineMax: 4, // 最大笔画半径
  16. pressure: 1, // 默认压力
  17. smoothness: 60, //顺滑度,用60的距离来计算速度
  18. currentPoint: {},
  19. currentLine: [], // 当前线条
  20. firstTouch: true, // 第一次触发
  21. radius: 1, //画圆的半径
  22. cutArea: { top: 0, right: 0, bottom: 0, left: 0 }, //裁剪区域
  23. bethelPoint: [], //保存所有线条 生成的贝塞尔点;
  24. lastPoint: 0,
  25. chirography: [], //笔迹
  26. currentChirography: {}, //当前笔迹
  27. linePrack: [], //划线轨迹 , 生成线条的实际点
  28. tmpPath:''
  29. },
  30. // 笔迹开始
  31. uploadScaleStart (e) {
  32. if (e.type != 'touchstart') return false;
  33. let ctx = this.data.ctx;
  34. ctx.setFillStyle(this.data.lineColor); // 初始线条设置颜色
  35. ctx.setGlobalAlpha(this.data.transparent); // 设置半透明
  36. let currentPoint = {
  37. x: e.touches[0].x,
  38. y: e.touches[0].y
  39. }
  40. let currentLine = this.data.currentLine;
  41. currentLine.unshift({
  42. time: new Date().getTime(),
  43. dis: 0,
  44. x: currentPoint.x,
  45. y: currentPoint.y
  46. })
  47. this.setData({
  48. currentPoint,
  49. // currentLine
  50. })
  51. if (this.data.firstTouch) {
  52. this.setData({
  53. cutArea: { top: currentPoint.y, right: currentPoint.x, bottom: currentPoint.y, left: currentPoint.x },
  54. firstTouch: false
  55. })
  56. }
  57. this.pointToLine(currentLine);
  58. },
  59. // 笔迹移动
  60. uploadScaleMove (e) {
  61. if (e.type != 'touchmove') return false;
  62. if (e.cancelable) {
  63. // 判断默认行为是否已经被禁用
  64. if (!e.defaultPrevented) {
  65. e.preventDefault();
  66. }
  67. }
  68. let point = {
  69. x: e.touches[0].x,
  70. y: e.touches[0].y
  71. }
  72. //测试裁剪
  73. if (point.y < this.data.cutArea.top) {
  74. this.data.cutArea.top = point.y;
  75. }
  76. if (point.y < 0) this.data.cutArea.top = 0;
  77. if (point.x > this.data.cutArea.right) {
  78. this.data.cutArea.right = point.x;
  79. }
  80. if (this.data.canvasWidth - point.x <= 0) {
  81. this.data.cutArea.right = this.data.canvasWidth;
  82. }
  83. if (point.y > this.data.cutArea.bottom) {
  84. this.data.cutArea.bottom = point.y;
  85. }
  86. if (this.data.canvasHeight - point.y <= 0) {
  87. this.data.cutArea.bottom = this.data.canvasHeight;
  88. }
  89. if (point.x < this.data.cutArea.left) {
  90. this.data.cutArea.left = point.x;
  91. }
  92. if (point.x < 0) this.data.cutArea.left = 0;
  93. this.setData({
  94. lastPoint: this.data.currentPoint,
  95. currentPoint: point
  96. })
  97. let currentLine = this.data.currentLine
  98. currentLine.unshift({
  99. time: new Date().getTime(),
  100. dis: this.distance(this.data.currentPoint, this.data.lastPoint),
  101. x: point.x,
  102. y: point.y
  103. })
  104. // this.setData({
  105. // currentLine
  106. // })
  107. this.pointToLine(currentLine);
  108. },
  109. // 笔迹结束
  110. uploadScaleEnd (e) {
  111. if (e.type != 'touchend') return 0;
  112. let point = {
  113. x: e.changedTouches[0].x,
  114. y: e.changedTouches[0].y
  115. }
  116. this.setData({
  117. lastPoint: this.data.currentPoint,
  118. currentPoint: point
  119. })
  120. let currentLine = this.data.currentLine
  121. currentLine.unshift({
  122. time: new Date().getTime(),
  123. dis: this.distance(this.data.currentPoint, this.data.lastPoint),
  124. x: point.x,
  125. y: point.y
  126. })
  127. // this.setData({
  128. // currentLine
  129. // })
  130. if (currentLine.length > 2) {
  131. var info = (currentLine[0].time - currentLine[currentLine.length - 1].time) / currentLine.length;
  132. //$("#info").text(info.toFixed(2));
  133. }
  134. //一笔结束,保存笔迹的坐标点,清空,当前笔迹
  135. //增加判断是否在手写区域;
  136. this.pointToLine(currentLine);
  137. var currentChirography = {
  138. lineSize: this.data.lineSize,
  139. lineColor: this.data.lineColor
  140. };
  141. var chirography = this.data.chirography
  142. chirography.unshift(currentChirography);
  143. this.setData({
  144. chirography
  145. })
  146. var linePrack = this.data.linePrack
  147. linePrack.unshift(this.data.currentLine);
  148. this.setData({
  149. linePrack,
  150. currentLine: []
  151. })
  152. },
  153. onLoad () {
  154. let canvasName = this.data.canvasName
  155. let ctx = wx.createCanvasContext(canvasName)
  156. this.setData({
  157. ctx: ctx
  158. })
  159. var query = wx.createSelectorQuery();
  160. query.select('.handCenter').boundingClientRect(rect => {
  161. this.setData({
  162. canvasWidth: rect.width,
  163. canvasHeight: rect.height
  164. })
  165. }).exec();
  166. },
  167. subCanvas(){
  168. // 新增我的
  169. let that = this
  170. let ctx = this.data.ctx;
  171. ctx.draw(true,setTimeout(function(){ //我的新增定时器及回调
  172. wx.canvasToTempFilePath({
  173. x: 0,
  174. y: 0,
  175. width: 375,
  176. height: 152,
  177. canvasId: 'handWriting',
  178. fileType: 'png',
  179. success: function(res) {
  180. that.setData({
  181. tmpPath:res.tempFilePath
  182. })
  183. console.log(that.data.tmpPath,'看下是个啥玩意')
  184. that.upImgs(that.data.tmpPath,0)
  185. }
  186. }, ctx)
  187. },1000))
  188. },
  189. // 新增将保存的图片路径上传到文件服务器
  190. upImgs: function (imgurl, index) {
  191. console.log(imgurl,'看下路径是多少')
  192. var that = this;
  193. wx.uploadFile({
  194. url: apiEev.api + 'xxxx',//后台上传路径
  195. filePath: imgurl,
  196. name: 'file',
  197. header: {
  198. 'content-type': 'multipart/form-data'
  199. },
  200. formData: null,
  201. success: function (res) {
  202. console.log(res) //接口返回网络路径
  203. var data = JSON.parse(res.data)
  204. console.log(data,'看下data是个啥')
  205. if (data.code == "success") {
  206. console.log('成功')
  207. }
  208. }
  209. })
  210. },
  211. retDraw () {
  212. this.data.ctx.clearRect(0, 0, 700, 730)
  213. this.data.ctx.draw()
  214. this.setData({
  215. tmpPath:''
  216. })
  217. },
  218. //画两点之间的线条;参数为:line,会绘制最近的开始的两个点;
  219. pointToLine (line) {
  220. this.calcBethelLine(line);
  221. return;
  222. },
  223. //计算插值的方式;
  224. calcBethelLine (line) {
  225. if (line.length <= 1) {
  226. line[0].r = this.data.radius;
  227. return;
  228. }
  229. let x0, x1, x2, y0, y1, y2, r0, r1, r2, len, lastRadius, dis = 0, time = 0, curveValue = 0.5;
  230. if (line.length <= 2) {
  231. x0 = line[1].x
  232. y0 = line[1].y
  233. x2 = line[1].x + (line[0].x - line[1].x) * curveValue;
  234. y2 = line[1].y + (line[0].y - line[1].y) * curveValue;
  235. //x2 = line[1].x;
  236. //y2 = line[1].y;
  237. x1 = x0 + (x2 - x0) * curveValue;
  238. y1 = y0 + (y2 - y0) * curveValue;;
  239. } else {
  240. x0 = line[2].x + (line[1].x - line[2].x) * curveValue;
  241. y0 = line[2].y + (line[1].y - line[2].y) * curveValue;
  242. x1 = line[1].x;
  243. y1 = line[1].y;
  244. x2 = x1 + (line[0].x - x1) * curveValue;
  245. y2 = y1 + (line[0].y - y1) * curveValue;
  246. }
  247. //从计算公式看,三个点分别是(x0,y0),(x1,y1),(x2,y2) ;(x1,y1)这个是控制点,控制点不会落在曲线上;实际上,这个点还会手写获取的实际点,却落在曲线上
  248. len = this.distance({ x: x2, y: y2 }, { x: x0, y: y0 });
  249. lastRadius = this.data.radius;
  250. for (let n = 0; n < line.length - 1; n++) {
  251. dis += line[n].dis;
  252. time += line[n].time - line[n + 1].time;
  253. if (dis > this.data.smoothness) break;
  254. }
  255. this.setData({
  256. radius: Math.min(time / len * this.data.pressure + this.data.lineMin, this.data.lineMax) * this.data.lineSize
  257. });
  258. line[0].r = this.data.radius;
  259. //计算笔迹半径;
  260. if (line.length <= 2) {
  261. r0 = (lastRadius + this.data.radius) / 2;
  262. r1 = r0;
  263. r2 = r1;
  264. //return;
  265. } else {
  266. r0 = (line[2].r + line[1].r) / 2;
  267. r1 = line[1].r;
  268. r2 = (line[1].r + line[0].r) / 2;
  269. }
  270. let n = 5;
  271. let point = [];
  272. for (let i = 0; i < n; i++) {
  273. let t = i / (n - 1);
  274. let x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2;
  275. let y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2;
  276. let r = lastRadius + (this.data.radius - lastRadius) / n * i;
  277. point.push({ x: x, y: y, r: r });
  278. if (point.length == 3) {
  279. let a = this.ctaCalc(point[0].x, point[0].y, point[0].r, point[1].x, point[1].y, point[1].r, point[2].x, point[2].y, point[2].r);
  280. a[0].color = this.data.lineColor;
  281. // let bethelPoint = this.data.bethelPoint;
  282. // console.log(a)
  283. // console.log(this.data.bethelPoint)
  284. // bethelPoint = bethelPoint.push(a);
  285. this.bethelDraw(a, 1);
  286. point = [{ x: x, y: y, r: r }];
  287. }
  288. }
  289. this.setData({
  290. currentLine: line
  291. })
  292. },
  293. //求两点之间距离
  294. distance (a, b) {
  295. let x = b.x - a.x;
  296. let y = b.y - a.y;
  297. return Math.sqrt(x * x + y * y);
  298. },
  299. ctaCalc (x0, y0, r0, x1, y1, r1, x2, y2, r2) {
  300. let a = [], vx01, vy01, norm, n_x0, n_y0, vx21, vy21, n_x2, n_y2;
  301. vx01 = x1 - x0;
  302. vy01 = y1 - y0;
  303. norm = Math.sqrt(vx01 * vx01 + vy01 * vy01 + 0.0001) * 2;
  304. vx01 = vx01 / norm * r0;
  305. vy01 = vy01 / norm * r0;
  306. n_x0 = vy01;
  307. n_y0 = -vx01;
  308. vx21 = x1 - x2;
  309. vy21 = y1 - y2;
  310. norm = Math.sqrt(vx21 * vx21 + vy21 * vy21 + 0.0001) * 2;
  311. vx21 = vx21 / norm * r2;
  312. vy21 = vy21 / norm * r2;
  313. n_x2 = -vy21;
  314. n_y2 = vx21;
  315. a.push({ mx: x0 + n_x0, my: y0 + n_y0, color: "#1A1A1A" });
  316. a.push({ c1x: x1 + n_x0, c1y: y1 + n_y0, c2x: x1 + n_x2, c2y: y1 + n_y2, ex: x2 + n_x2, ey: y2 + n_y2 });
  317. a.push({ c1x: x2 + n_x2 - vx21, c1y: y2 + n_y2 - vy21, c2x: x2 - n_x2 - vx21, c2y: y2 - n_y2 - vy21, ex: x2 - n_x2, ey: y2 - n_y2 });
  318. a.push({ c1x: x1 - n_x2, c1y: y1 - n_y2, c2x: x1 - n_x0, c2y: y1 - n_y0, ex: x0 - n_x0, ey: y0 - n_y0 });
  319. a.push({ c1x: x0 - n_x0 - vx01, c1y: y0 - n_y0 - vy01, c2x: x0 + n_x0 - vx01, c2y: y0 + n_y0 - vy01, ex: x0 + n_x0, ey: y0 + n_y0 });
  320. a[0].mx = a[0].mx.toFixed(1);
  321. a[0].mx = parseFloat(a[0].mx);
  322. a[0].my = a[0].my.toFixed(1);
  323. a[0].my = parseFloat(a[0].my);
  324. for (let i = 1; i < a.length; i++) {
  325. a[i].c1x = a[i].c1x.toFixed(1);
  326. a[i].c1x = parseFloat(a[i].c1x);
  327. a[i].c1y = a[i].c1y.toFixed(1);
  328. a[i].c1y = parseFloat(a[i].c1y);
  329. a[i].c2x = a[i].c2x.toFixed(1);
  330. a[i].c2x = parseFloat(a[i].c2x);
  331. a[i].c2y = a[i].c2y.toFixed(1);
  332. a[i].c2y = parseFloat(a[i].c2y);
  333. a[i].ex = a[i].ex.toFixed(1);
  334. a[i].ex = parseFloat(a[i].ex);
  335. a[i].ey = a[i].ey.toFixed(1);
  336. a[i].ey = parseFloat(a[i].ey);
  337. }
  338. return a;
  339. },
  340. bethelDraw (point, is_fill, color) {
  341. // 新增我的
  342. let that = this
  343. let ctx = this.data.ctx;
  344. ctx.beginPath();
  345. ctx.moveTo(point[0].mx, point[0].my);
  346. if (undefined != color) {
  347. ctx.setFillStyle(color);
  348. ctx.setStrokeStyle(color);
  349. } else {
  350. ctx.setFillStyle(point[0].color);
  351. ctx.setStrokeStyle(point[0].color);
  352. }
  353. for (let i = 1; i < point.length; i++) {
  354. ctx.bezierCurveTo(point[i].c1x, point[i].c1y, point[i].c2x, point[i].c2y, point[i].ex, point[i].ey);
  355. }
  356. ctx.stroke();
  357. if (undefined != is_fill) {
  358. ctx.fill(); //填充图形 ( 后绘制的图形会覆盖前面的图形, 绘制时注意先后顺序 )
  359. }
  360. ctx.draw(true)
  361. },
  362. selectColorEvent (event) {
  363. console.log(event)
  364. var color = event.currentTarget.dataset.colorValue;
  365. var colorSelected = event.currentTarget.dataset.color;
  366. this.setData({
  367. selectColor: colorSelected,
  368. lineColor: color
  369. })
  370. }
  371. })
复制代码
  1. /* pages/canvas-test2/canvas-test2.wxss */
  2. .canvasId {
  3. position: absolute;
  4. left: 50%;
  5. top: 0;
  6. transform: translate(-50%);
  7. z-index: 1;
  8. border: 2px dashed #ccc;
  9. border-radius: 8px;
  10. margin-bottom: 66px;
  11. }
  12. .handCenter {
  13. border: 1px solid red;
  14. }
  15. .handWriting {
  16. width: 100%;
  17. }
  18. .preview {
  19. width: 375px;
  20. height: 152px;
  21. }
复制代码
2.重点部分分析

(1)签名根本实现,开始,移动,结束
  1. // 笔迹开始
  2. uploadScaleStart (e) {
  3. if (e.type != 'touchstart') return false;
  4. let ctx = this.data.ctx;
  5. ctx.setFillStyle(this.data.lineColor); // 初始线条设置颜色
  6. ctx.setGlobalAlpha(this.data.transparent); // 设置半透明
  7. let currentPoint = {
  8. x: e.touches[0].x,
  9. y: e.touches[0].y
  10. }
  11. let currentLine = this.data.currentLine;
  12. currentLine.unshift({
  13. time: new Date().getTime(),
  14. dis: 0,
  15. x: currentPoint.x,
  16. y: currentPoint.y
  17. })
  18. this.setData({
  19. currentPoint,
  20. // currentLine
  21. })
  22. if (this.data.firstTouch) {
  23. this.setData({
  24. cutArea: { top: currentPoint.y, right: currentPoint.x, bottom: currentPoint.y, left: currentPoint.x },
  25. firstTouch: false
  26. })
  27. }
  28. this.pointToLine(currentLine);
  29. },
  30. // 笔迹移动
  31. uploadScaleMove (e) {
  32. if (e.type != 'touchmove') return false;
  33. if (e.cancelable) {
  34. // 判断默认行为是否已经被禁用
  35. if (!e.defaultPrevented) {
  36. e.preventDefault();
  37. }
  38. }
  39. let point = {
  40. x: e.touches[0].x,
  41. y: e.touches[0].y
  42. }
  43. //测试裁剪
  44. if (point.y < this.data.cutArea.top) {
  45. this.data.cutArea.top = point.y;
  46. }
  47. if (point.y < 0) this.data.cutArea.top = 0;
  48. if (point.x > this.data.cutArea.right) {
  49. this.data.cutArea.right = point.x;
  50. }
  51. if (this.data.canvasWidth - point.x <= 0) {
  52. this.data.cutArea.right = this.data.canvasWidth;
  53. }
  54. if (point.y > this.data.cutArea.bottom) {
  55. this.data.cutArea.bottom = point.y;
  56. }
  57. if (this.data.canvasHeight - point.y <= 0) {
  58. this.data.cutArea.bottom = this.data.canvasHeight;
  59. }
  60. if (point.x < this.data.cutArea.left) {
  61. this.data.cutArea.left = point.x;
  62. }
  63. if (point.x < 0) this.data.cutArea.left = 0;
  64. this.setData({
  65. lastPoint: this.data.currentPoint,
  66. currentPoint: point
  67. })
  68. let currentLine = this.data.currentLine
  69. currentLine.unshift({
  70. time: new Date().getTime(),
  71. dis: this.distance(this.data.currentPoint, this.data.lastPoint),
  72. x: point.x,
  73. y: point.y
  74. })
  75. // this.setData({
  76. // currentLine
  77. // })
  78. this.pointToLine(currentLine);
  79. },
  80. // 笔迹结束
  81. uploadScaleEnd (e) {
  82. if (e.type != 'touchend') return 0;
  83. let point = {
  84. x: e.changedTouches[0].x,
  85. y: e.changedTouches[0].y
  86. }
  87. this.setData({
  88. lastPoint: this.data.currentPoint,
  89. currentPoint: point
  90. })
  91. let currentLine = this.data.currentLine
  92. currentLine.unshift({
  93. time: new Date().getTime(),
  94. dis: this.distance(this.data.currentPoint, this.data.lastPoint),
  95. x: point.x,
  96. y: point.y
  97. })
  98. // this.setData({
  99. // currentLine
  100. // })
  101. if (currentLine.length > 2) {
  102. var info = (currentLine[0].time - currentLine[currentLine.length - 1].time) / currentLine.length;
  103. //$("#info").text(info.toFixed(2));
  104. }
  105. //一笔结束,保存笔迹的坐标点,清空,当前笔迹
  106. //增加判断是否在手写区域;
  107. this.pointToLine(currentLine);
  108. var currentChirography = {
  109. lineSize: this.data.lineSize,
  110. lineColor: this.data.lineColor
  111. };
  112. var chirography = this.data.chirography
  113. chirography.unshift(currentChirography);
  114. this.setData({
  115. chirography
  116. })
  117. var linePrack = this.data.linePrack
  118. linePrack.unshift(this.data.currentLine);
  119. this.setData({
  120. linePrack,
  121. currentLine: []
  122. })
  123. },
复制代码
记得要先在onload中初始化
代码拿走直接用
(2)重新签署
明白话就是清空画布
  1. retDraw () {
  2. this.data.ctx.clearRect(0, 0, 700, 730)
  3. this.data.ctx.draw()
  4. this.setData({
  5. tmpPath:''
  6. })
  7. },
复制代码
(3)签署完成
  1. subCanvas(){
  2. // 新增我的
  3. let that = this
  4. let ctx = this.data.ctx;
  5. ctx.draw(true,setTimeout(function(){ //我的新增定时器及回调
  6. wx.canvasToTempFilePath({
  7. x: 0,
  8. y: 0,
  9. width: 375,
  10. height: 152,
  11. canvasId: 'handWriting',
  12. fileType: 'png',
  13. success: function(res) {
  14. that.setData({
  15. tmpPath:res.tempFilePath
  16. })
  17. console.log(that.data.tmpPath,'看下是个啥玩意')
  18. that.upImgs(that.data.tmpPath,0)
  19. }
  20. }, ctx)
  21. },1000))
  22. },
复制代码
里边的回调比较紧张哦:
  1. 防止拿不到画布内容,可以设置延迟;
  2. wx.canvasToTempFilePath方法获取到画布图片内容;
复制代码
(4)根据业务需求,可以将图片上传到配景,在必要的地方展示
重点是怎样上传到配景
  1. // 新增将保存的图片路径上传到文件服务器
  2. upImgs: function (imgurl, index) {
  3. console.log(imgurl,'看下路径是多少')
  4. var that = this;
  5. wx.uploadFile({
  6. url: apiEev.api + 'xxxx',//后台文件上传的路径接口
  7. filePath: imgurl,
  8. name: 'file',
  9. header: {
  10. 'content-type': 'multipart/form-data'
  11. },
  12. formData: null,
  13. success: function (res) {
  14. console.log(res) //接口返回网络路径
  15. var data = JSON.parse(res.data)
  16. console.log(data,'看下data是个啥')
  17. if (data.code == "success") {
  18. console.log('成功')
  19. }
  20. }
  21. })
  22. },
复制代码
总结
微信小程序canvas实现签名功能。
特殊提醒:在真机调试和体验版中大概会出现卡顿环境,有条件要发布至预发布中查看是否影响性能。
以上就是本文的全部内容,盼望对大家的学习有所资助,也盼望大家多多支持草根技术分享。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作