网页五子棋——对战前端

打印 上一主题 下一主题

主题 880|帖子 880|积分 2640

目录
对战时序图
约定前后端交互接口
客户端实现
game_room.html
 game_room.css
board.js
setScreenText
initGame()
初始化 WebSocket
发送落子请求
处置惩罚落子响应


在本篇文章中,我们就来实现网页五子棋的末了一个模块,也是最重要的一个模块——对战模块
在举行对战时,也必要使用到 WebSocket

对战时序图


我们来理解一下 对战过程:
   1. 玩家进入游戏房间后,客户端与服务器建立连接
  2. 当两个玩家都进入游戏房间时,服务器推送数据准备就绪响应
  3. 先手方玩家开始落子,客户端发送落子请求
  4. 服务器举行相干处置惩罚,并返回落子响应
  5. 客户端吸收到落子响应,显示对应棋子,并判定是否有玩家胜利,若无,互换落子玩家
  6. 玩家落子,客户端发送落子响应
  ......
  7. 双方玩家持续落子,直到一方玩家胜利(假设 player1 胜利),服务器推送落子响应,并举行游戏房间销毁,修改玩家对应分数等操作
  8. 客户端接吸收到落子响应,显示对应棋子,此时有玩家胜利,显示游戏胜利 / 游戏失败,并显示 返回游戏大厅 按钮
  
约定前后端交互接口

我们起首来分析可能会用到的接口:
一个玩家进入游戏房间时,此时显示等待对手进入游戏房间
直到两个玩家都成功进入游戏房间,返回 游戏准备就绪响应,表示可以开始下棋了
当玩家点击对应位置时,发送落子请求,表示在对应位置落子,服务器举行处置惩罚后,返回落子响应
因此,我们约定连接 url 为:ws://127.0.0.1:8080/game
[连接响应]
  1. {
  2.     "code": 200,
  3.     "data": {
  4.         "roomId": "2c7446d1-5105-49e6-8b6b-c935120c25de", // 房间号
  5.         "thisUserId": 1, // 玩家自己的 id
  6.         "thatUserId": 2, // 对手的 id
  7.         "whiteUserId": 1 // 先手方 id
  8.     },
  9.     "errorMessage": ""
  10. }
复制代码
[落子请求]
  1. {
  2.     "userId": 1, // 玩家自己的 id
  3.     "row": 6, // 落子位置——行
  4.     "col": 5 // 落子位置——列
  5. }
复制代码
[落子响应]
  1. {
  2.     "code": 200,
  3.     "data": {
  4.         "userId": 1, // 落子玩家 id
  5.         "row": 1, // 落子位置——行
  6.         "col": 2, // 落子位置——列
  7.         "winner": 0 // 获胜玩家 id
  8.     },
  9.     "errorMessage": ""
  10. }
复制代码
接着,我们先来实现客户端相干逻辑 

客户端实现

game_room.html

我们起首创建 game_room.html 表示对战页面
game_room.html 页面主要包含两个部分:棋盘显示当前状态的 div

其中,棋盘我们使用 canvas 来举行绘制:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>游戏房间</title>
  7. </head>
  8. <body>
  9.     <div class="container">
  10.         <div>
  11.             <div>
  12.                 <canvas id="chess" width="450px" height="450px"></canvas>
  13.             </div>
  14.             <div id="screen">等待对手玩家进入游戏房间...</div>
  15.         </div>        
  16.     </div>
  17. </body>
  18. </html>
复制代码

 game_room.css

创建 game_room.css,先对 screen 举行设置:
  1. #screen {
  2.     width: 450px;
  3.     height: 50px;
  4.     margin-top: 10px;
  5.     background-color: antiquewhite;
  6.     font-size: 22px;
  7.     line-height: 50px;
  8.     text-align: center;
  9.     border-radius: 10px;
  10. }
复制代码
而对于棋盘和棋子的绘制,我们同一在 board.js 中实现

board.js

起首,我们创建一个全局变量 gameInfo,表示游戏和玩家相干信息:
  1. gameInfo = {
  2.     roomId: null, // 房间 id
  3.     thisUserId: null, // 当前玩家id
  4.     thatUserId: null, // 对手玩家 id
  5.     isWhite: true, // 是否是先手
  6. }
复制代码
setScreenText

接着,我们界说一个 setScreenText 方法,由于在落子时显示提示信息:
  1. function setScreenText(me) {
  2.     let screen = document.querySelector('#screen');
  3.     if (me) {
  4.         screen.innerHTML = "轮到你落子了!";
  5.     } else {
  6.         screen.innerHTML = "轮到对方落子了!";
  7.     }
  8. }
复制代码
传递一个 布尔类型 的参数,若为 true,则表示当前轮到玩家落子,显示对应提示信息;若为 false,则表示当前轮到对手落子,也显示对应提示信息
initGame()

接着,我们必要对游戏举行初始化,因此,我们界说一个 initGame() 方法,来举行相干逻辑判定和棋盘/棋子的绘制操作:
  1. function initGame() {
  2.     // 是我下还是对方下. 根据服务器分配的先后手情况决定
  3.     let me = gameInfo.isWhite;
  4.     // 游戏是否结束
  5.     let over = false;
  6.     let chessBoard = [];
  7.     //初始化chessBord数组(表示棋盘的数组)
  8.     for (let i = 0; i < 15; i++) {
  9.         chessBoard[i] = [];
  10.         for (let j = 0; j < 15; j++) {
  11.             chessBoard[i][j] = 0;
  12.         }
  13.     }
  14. }
复制代码
界说相干变量: 
   me 表示当前是轮到自己落子
  over 表示游戏是否结束
  chessBoard 表示游戏棋盘
  接着对棋盘举行初始化,界说一个 15 * 15 大小的棋盘,并将其中元素都填充为 0,这样方便我们后续举行判定当前位置是否有子(若为 0,则未落子;若不为 0,则表示当前位置已有玩家落子)
接着,为棋盘添加背景图片:
  1.     let chess = document.querySelector('#chess');
  2.     let context = chess.getContext('2d');
  3.     context.strokeStyle = "#BFBFBF";
  4.     // 背景图片
  5.     let logo = new Image();
  6.     logo.src = "img/chessboard.jpg";
  7.     logo.onload = function () {
  8.         context.drawImage(logo, 0, 0, 450, 450);
  9.         initChessBoard();
  10.     }
复制代码
其中,getContext() 方法用于获取一个 画图的上下文对象,获取到的对象(context)是用来举行画图操作的工具,其中必要吸收一个参数,用于指定所需的画图上下文类型
在这里,我们传递 2d,表示 二维画图 上下文,常用于平面图形的绘制、动画等
然后使用 strokeStyle 指定路径的颜色(棋盘网络的颜色)
然后界说一个 Image 对象,通过 src 属性指定图像的来源(图像可自行指定)
onload 是图像加载完毕时调用的方法
当图像加载完毕后,就使用 drawImage(img, dx, dy, dw, dh) 方法将绘制到 canvas,
其中:
   dx 和 dy 是图像 x 坐标 和 y 坐标的起始位置
  dw 和 dh 是图像绘制在 Canvas 上的宽度和高度
  我们让图片充满整个画布:
  1.     logo.onload = function () {
  2.         context.drawImage(logo, 0, 0, 450, 450);
  3.         initChessBoard();
  4.     }
复制代码
接着,我们在 initChessBoard 方法中,绘制棋盘网络:
  1.     function initChessBoard() {
  2.         for (let i = 0; i < 15; i++) {
  3.             context.moveTo(15 + i * 30, 15);
  4.             context.lineTo(15 + i * 30, 430);
  5.             context.stroke();
  6.             context.moveTo(15, 15 + i * 30);
  7.             context.lineTo(430, 15 + i * 30);
  8.             context.stroke();
  9.         }
  10.     }
复制代码
通过一个 for 循环,在画布上循环画线:
其中:
   context.moveTo(15 + i * 30, 15);
  context.lineTo(15 + i * 30, 430);
  context.stroke();
  用于绘制从 (15 + i * 30, 15)  (15 + i * 30, 430) 的竖线
通过 15 + i * 30 盘算出每个竖线的 x 坐标
15 和 430 分别表示竖线的 起始 和 结束 的 y 坐标
例如,i = 1,此时 x = 45:
moveTo(45, 15) 表示将画笔移动到 (45, 15)的坐标位置,不绘制任何内容:

接着 lineTo(45, 430) 表示从当前位置画一条直线到 (45, 430)坐标:

末了 stroke() 方法会让这条线见效
接着,绘制横线:
   context.moveTo(15, 15 + i * 30);
  context.lineTo(430, 15 + i * 30);
  context.stroke();
  i 控制横线的 y 坐标,通过 15 + i * 30 盘算出每条横线的 y 坐标,15 和 430 是横线的 x 坐标的起始值和结束值

接着,我们通过 onStep 方法,对棋子举行绘制:
  1.     function oneStep(row, col, isWhite) {
  2.         context.beginPath();
  3.         context.arc(15 + col * 30, 15 + row * 30, 13, 0, 2 * Math.PI);
  4.         context.closePath();
  5.         var gradient = context.createRadialGradient(15 + col * 30 + 2, 15 + row * 30 - 2, 13, 15 + col * 30 + 2, 15 + row * 30 - 2, 0);
  6.         if (!isWhite) {
  7.             gradient.addColorStop(0, "#0A0A0A");
  8.             gradient.addColorStop(1, "#636766");
  9.         } else {
  10.             gradient.addColorStop(0, "#D1D1D1");
  11.             gradient.addColorStop(1, "#F9F9F9");
  12.         }
  13.         context.fillStyle = gradient;
  14.         context.fill();
  15.     }
复制代码
 该方法必要传递 3 个参数:
   row:棋子所在的行,在棋盘上对应 y 坐标
  col:棋子所在的列,在棋盘上对应 x 坐标
  isWhite:决定棋子是白色(true)还是黑色(false)
  起首 context.beginPath() 表示开始绘制一个新的路径,全部接下来的画图操作都会被视为这个路径的一部分,直到路径被关闭或填充
接着调用 arc 方法,绘制一个圆形:
context.arc(15 + col * 30, 15 + row * 30, 13, 0, 2 * Math.PI)
   15 + col * 30 和 15 + row * 30 :圆心的x 坐标 和 y 坐标,15 为偏移量,col * 30 表示每列之间的程度间距是 30 px,通过这个盘算方式,每个圆会程度间隔 30 像素(同样的,row * 30 表示每行之间的垂直间距是 30 像素。每个圆会垂直间隔 30 像素)
  13 :表示圆的半径为 13 px
  0 和  2 * Math.PI:分别表示圆弧的的起始角度和结束角度。由于起始角度为 0,结束角度为  2 * Math.PI(360 度),这意味着这个弧线将覆盖完整的圆
  接着,创建渐变色
   createRadialGradient(x1, y1, r1, x2, y2, r2)
  var gradient = context.createRadialGradient(15 + col * 30 + 2, 15 + row * 30 - 2, 13, 15 + col * 30 + 2, 15 + row * 30 - 2, 0)
    (15 + col * 30 + 2, 15 + row * 30 - 2):渐变的起始点坐标,也就是渐变的中心位置
  13:起始半径
  (15 + col * 30 + 2, 15 + row * 30 - 2):渐变的结束点坐标,也就是渐变的结束位置,终止点的坐标和起始点的坐标雷同,也就是渐变的止境和起点位于同一位置
  0:结束半径
  由于 13 是起始半径,0 为结束半径,这样就形成了一个从圆心到边缘渐变的效果,即渐变从一个较大的半径逐渐过渡到一个完全没有半径的点
创建径向渐变是为了在圆中实现颜色从中心向外扩展的效果。将这个渐变应用到棋子的填充,从而创造出从圆心逐渐改变颜色的视觉效果
接着,根据 isWhite 添加渐变色的颜色停顿点:
  1. if (!isWhite) {
  2.     gradient.addColorStop(0, "#0A0A0A");
  3.     gradient.addColorStop(1, "#636766");
  4. } else {
  5.     gradient.addColorStop(0, "#D1D1D1");
  6.     gradient.addColorStop(1, "#F9F9F9");
  7. }
复制代码
  如果黑色棋子(!isWhite)),则从深色 #0A0A0A(黑色)到浅灰色 #636766
  如果是白色棋子(isWhite),则从浅灰色 #D1D1D1 到接近白色的 #F9F9F9
  末了,设置填充样式并填充棋子:
           context.fillStyle = gradient;
          context.fill();
  
当玩家点击棋盘对应位置时,就必要绘制对应棋子(先手为白子,后手为黑子)
接下来,我们就来添加对应的点击变乱:
  1.     chess.onclick = function (e) {
  2.         // 判断游戏是否结束
  3.         if (over) {
  4.             return;
  5.         }
  6.         // 当前是否轮到 我 落子
  7.         if (!me) {
  8.             return;
  9.         }
  10.         // 获取棋子的 x 和 y 坐标
  11.         let x = e.offsetX;
  12.         let y = e.offsetY;
  13.         // 注意, 横坐标是列, 纵坐标是行
  14.         let col = Math.floor(x / 30);
  15.         let row = Math.floor(y / 30);
  16.         if (chessBoard[row][col] == 0) {
  17.             // TODO 发送坐标给服务器
  18.         }
  19.     }
复制代码
其中,使用 Math.floor 向下取整,让每次点击操尴尬刁难应到网格线上
   为什么是 / 30 ?
  这是由于整个棋盘的大小是 450 * 450,而棋盘上是 15 行,15 列
因此,每行每列占用 30 px
当玩家未点击到网格线上时:

此时就必要将其对应到对应的网格线上:

点击之后,若当前位置没有玩家落子,就可以将坐标发送给服务器,服务器举行对应处置惩罚(后续实现)
再绘制棋子,并将当前位置标志为 1 (表示这个位置已经有棋子了)
initGame() 方法完整代码:
  1. function initGame() {    // 是我下还是对方下. 根据服务器分配的先后手情况决定    let me = gameInfo.isWhite;    // 游戏是否结束    let over = false;    let chessBoard = [];    //初始化chessBord数组(表示棋盘的数组)    for (let i = 0; i < 15; i++) {        chessBoard[i] = [];        for (let j = 0; j < 15; j++) {            chessBoard[i][j] = 0;        }    }    let chess = document.querySelector('#chess');
  2.     let context = chess.getContext('2d');
  3.     context.strokeStyle = "#BFBFBF";
  4.     // 背景图片
  5.     let logo = new Image();
  6.     logo.src = "img/chessboard.jpg";
  7.     logo.onload = function () {
  8.         context.drawImage(logo, 0, 0, 450, 450);
  9.         initChessBoard();
  10.     }
  11.     // 绘制棋盘网格    function initChessBoard() {
  12.         for (let i = 0; i < 15; i++) {
  13.             context.moveTo(15 + i * 30, 15);
  14.             context.lineTo(15 + i * 30, 430);
  15.             context.stroke();
  16.             context.moveTo(15, 15 + i * 30);
  17.             context.lineTo(430, 15 + i * 30);
  18.             context.stroke();
  19.         }
  20.     }    // 绘制一个棋子    /**     *      * @param {*} row 行 对应 y 坐标     * @param {*} col 列 对应 x 坐标     * @param {*} isWhite     */    function oneStep(row, col, isWhite) {
  21.         context.beginPath();
  22.         context.arc(15 + col * 30, 15 + row * 30, 13, 0, 2 * Math.PI);
  23.         context.closePath();
  24.         var gradient = context.createRadialGradient(15 + col * 30 + 2, 15 + row * 30 - 2, 13, 15 + col * 30 + 2, 15 + row * 30 - 2, 0);
  25.         if (!isWhite) {
  26.             gradient.addColorStop(0, "#0A0A0A");
  27.             gradient.addColorStop(1, "#636766");
  28.         } else {
  29.             gradient.addColorStop(0, "#D1D1D1");
  30.             gradient.addColorStop(1, "#F9F9F9");
  31.         }
  32.         context.fillStyle = gradient;
  33.         context.fill();
  34.     }    chess.onclick = function (e) {        // 判定游戏是否结束        if (over) {            return;        }        // 当前是否轮到 我 落子        if (!me) {            return;        }        // 获取棋子的 x 和 y 坐标        let x = e.offsetX;        let y = e.offsetY;        // 注意, 横坐标是列, 纵坐标是行        let col = Math.floor(x / 30);        let row = Math.floor(y / 30);        if (chessBoard[row][col] == 0) {            // TODO 发送坐标给服务器, 服务器要返回结果            oneStep(row, col, gameInfo.isWhite);            chessBoard[row][col] = 1;        }    }}
复制代码
初始化 WebSocket

接着,我们在 board.js 中加入 WebSocket 连接代码,实现前后端交互:
创建 WebSocket 对象,并挂载回调函数:
  1. // 初始化 websocket
  2. let webSocketUrl = "ws://127.0.0.1:8080/game";
  3. let webSocket = new WebSocket(webSocketUrl);
  4. // 处理游戏就绪
  5. webSocket.onmessage = function(e) {
  6.     console.log("游戏就绪");
  7. }
  8. // 监听页面关闭事件,在页面关闭之前,手动调用 webSocket 的 close 方法
  9. // 防止连接还没断开就关闭窗口
  10. window.onbeforeunload = function() {
  11.     webSocket.close();
  12. }
  13. // 连接发生错误时,回到游戏大厅
  14. webSocket.onerror = function() {
  15.     alert("连接异常!即将回到游戏大厅!");
  16.     location.replace("/game_hall.html");
  17. }
复制代码
当连接异常时,跳转到游戏大厅页面
在页面关闭之前,调用 close 方法关闭 WebSocket 连接
接着,我们实现 onmessage 方法,处置惩罚游戏就绪响应:
   若 code != 200,则响应出现异常,我们先不举行处置惩罚,后续再举行补充
  若 code = 200,则响应成功,对 gameInfo 和 棋盘举行初始化,并设置显示内容
  1. webSocket.onmessage = function(e) {
  2.     console.log("游戏就绪");
  3.     let resp = JSON.parse(e.data);
  4.     if (resp.code != 200) {
  5.         alert("异常情况:" + resp.errorMessage);
  6.         return;
  7.     }
  8.     let readyResult = resp.data;
  9.     gameInfo.roomId =  readyResult.roomId;
  10.     gameInfo.thisUserId = readyResult.thisUserId;
  11.     gameInfo.thatUserId = readyResult.thatUserId;
  12.     gameInfo.isWhite = (readyResult.whiteUserId == gameInfo.thisUserId);
  13.     // 初始化棋盘
  14.     initGame();
  15.     // 设置显示区域内容
  16.     setScreenText(gameInfo.isWhite);
  17. }
复制代码

发送落子请求

我们修改 onclick 函数,在点击落子时发送落子响应

而对于 绘制棋子 和 修改 chessBoard 操作,我们则将其放到吸收到落子响应时举行处置惩罚
实现 send 方法,用于发送落子请求:
  1.     function send(row, col) {
  2.         let req = {
  3.             userId: gameInfo.thisUserId,
  4.             row: row,
  5.             col: col
  6.         }
  7.         webSocket.send(JSON.stringify(req));
  8.     }
复制代码
 若当前位置未落子,发送落子请求:
  1.         if (chessBoard[row][col] == 0) {
  2.             send(row, col);
  3.         }
复制代码

处置惩罚落子响应

在前面的 onmessage 方法中,主要是针对 游戏就绪响应 来举行处置惩罚的,而在初始化之后,就只必要对落子响应举行处置惩罚了
因此,我们可以直接在 initGame 方法中修改 webSocket.onmessage 方法,让方法内部主要是针对落子响应举行处置惩罚:
同样的:
   若 code != 200,则响应出现异常,我们先不举行处置惩罚,后续再举行补充
  若 code = 200,则响应成功,举行后续逻辑处置惩罚
  落子响应处置惩罚:
   1. 判定 code 是否为 200,若不为,则打印异常情况,并返回
  2. 判定落的子是自己的还是对手的
  3. 绘制对应颜色的棋子
  4. 标志当前位置有子
  5. 互换落子轮次
  6. 判定胜败 (winner 不为 0 时,游戏结束)
  1.     webSocket.onmessage = function(e) {
  2.         let resp = JSON.parse(e.data);
  3.         if (resp.code != 200) {
  4.             alert("异常情况:" + resp.errorMessage);
  5.             return;
  6.         }
  7.         let gameRes = resp.data;
  8.         if (gameInfo.thisUserId == gameRes.userId) {
  9.             // 自己落的子,绘制自己颜色的子
  10.             oneStep(gameRes.row, gameRes.col, gameInfo.isWhite);
  11.         } else if (gameInfo.thatUserId == gameRes.userId) {
  12.             // 对手的子,绘制对手颜色的子
  13.             oneStep(gameRes.row, gameRes.col, !gameInfo.isWhite);
  14.         } else {
  15.             console.log("落子异常");
  16.             return;
  17.         }
  18.         // 标记此处有棋子
  19.         chessBoard[gameRes.row][gameRes.col] = 1;
  20.         // 交换轮次
  21.         me = !me;
  22.         let screenDiv = document.querySelector('#screen');
  23.         // 判定胜负
  24.         if(gameRes.winner != 0) {
  25.             if(gameRes.winner == gameInfo.thisUserId) {
  26.                 screenDiv.innerHTML = "恭喜你!你赢了!";
  27.             } else if(gameRes.winner == gameInfo.thatUserId) {
  28.                 screenDiv.innerHTML = "很遗憾!你输了..."
  29.             } else if(gameRes.winner == -1) {
  30.                 screenDiv.innerHTML = "平局";
  31.             } else {
  32.                 console.log("字段错误!" + gameRes.winner);
  33.             }
  34.         } else {
  35.             setScreenText(me);
  36.         }
  37.     }
复制代码
其中,无论是自己落子还是对手落子,我们都将落子位置标志为1,这是由于在客户端不必要举行得胜的相干业务逻辑判定,也就不必要区分棋子
当游戏结束时,我们为其添加一个按钮,能够返回游戏大厅:
  1.             // 增加一个按钮,让玩家点击按钮后再返回到游戏大厅
  2.             let backBtn = document.createElement('button');
  3.             backBtn.id = "back-button";
  4.             backBtn.innerHTML = '回到大厅';
  5.             backBtn.onclick = function() {
  6.                 location.replace("/game_hall.html");
  7.             }
  8.             let fatherDiv = document.querySelector('.container>div');
  9.             fatherDiv.appendChild(backBtn);
复制代码
在 game_room.css 中添加对应样式: 
  1. #back-button {
  2.     width: 450px;
  3.     height: 50px;
  4.     background-color: rgb(237, 169, 79);
  5.     font-size: 20px;
  6.     color: gray;
  7.     border-radius: 10px;
  8.     text-align: center;
  9.     line-height: 50px;
  10.     margin-top: 10px;
  11.     border: none;
  12. }
复制代码
处置惩罚落子响应完整代码:
  1.     webSocket.onmessage = function(e) {        let resp = JSON.parse(e.data);        if (resp.code != 200) {            alert("异常情况:" + resp.errorMessage);            return;        }        let gameRes = resp.data;        if (gameInfo.thisUserId == gameRes.userId) {            // 自己落的子,绘制自己颜色的子            oneStep(gameRes.row, gameRes.col, gameInfo.isWhite);        } else if (gameInfo.thatUserId == gameRes.userId) {            // 对手的子,绘制对手颜色的子            oneStep(gameRes.row, gameRes.col, !gameInfo.isWhite);        } else {            console.log("落子异常");            return;        }        // 标志此处有棋子        chessBoard[gameRes.row][gameRes.col] = 1;        // 互换轮次        me = !me;        let screenDiv = document.querySelector('#screen');        // 判定胜败        if(gameRes.winner != 0) {            if(gameRes.winner == gameInfo.thisUserId) {                screenDiv.innerHTML = "恭喜你!你赢了!";            } else if(gameRes.winner == gameInfo.thatUserId) {                screenDiv.innerHTML = "很遗憾!你输了..."            } else if(gameRes.winner == -1) {                screenDiv.innerHTML = "平局";            } else {                console.log("字段错误!" + gameRes.winner);            }            // 增加一个按钮,让玩家点击按钮后再返回到游戏大厅
  2.             let backBtn = document.createElement('button');
  3.             backBtn.id = "back-button";
  4.             backBtn.innerHTML = '回到大厅';
  5.             backBtn.onclick = function() {
  6.                 location.replace("/game_hall.html");
  7.             }
  8.             let fatherDiv = document.querySelector('.container>div');
  9.             fatherDiv.appendChild(backBtn);        } else {            setScreenText(me);        }    }
复制代码
至此,关于对战模块的前端代码我们就根本实现完毕了,更多的内容我们在实现服务器端逻辑时继续补充

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

羊蹓狼

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表