express+vue 在线五子棋(一)

示例

在线体验地址五子棋,记得一定要再拉个人才能对战

本期难点

1、完成了五子棋的布局,判断游戏结束

2、基本的在线对战

3、游戏配套im(这个im的实现,请移步在线im)

下期安排

1、每步的倒计时设置

2、黑白棋分配由玩家自定义

3、新增旁观者

4、悔棋

5、自行创建一盘棋局

感兴趣的,还请点个免费的收藏与关注,后续会一直跟进这个系列

前端部分五子棋布局与游戏输赢判断

html 复制代码
<template>
    <div class="flex-wrap flex-justify-between">
        <div class="gobang-main">
            <!-- {{ userdata._id }}
            {{ gameBase }} -->
            步数: {{ currentStep }} 我方:{{ getQi }}
            <el-button v-if="isAdmin" @click="clearGame"> 清空游戏 </el-button>
            <!-- 下棋区 -->
            <game :list="gobangList" :config="config" @clickHandle="clickHandle"></game>

            <!-- <el-button v-if="isGameOver && isAdmin" @click="reloadGame"> 重新开始 </el-button> -->
        </div>
        <!-- im区 -->
        <Im
            ref="imRef"
            v-if="game_im_id"
            :style="{ width: '440px' }"
            :isShowLeft="false"
            :gobang_id="gobang_id"
            :game_im_id="game_im_id"
            @room_baseinfo="room_baseinfo"
            @get_game_content="get_game_content"
            @get_gb_info="get_gb_info"
            @del_gobang="del_gobang"
        ></Im>
    </div>
</template>

<script>
import Im from '@/views/blog/im/index.vue'
import { baseURL } from '@/plugins/config.js'
import { get_gobang_list, post_gobang, get_gobang, del_gobang } from '@/api/data.js'
import game from './game.vue'
export default {
    components: {
        Im,
        game,
    },
    data() {
        return {
            isGameOver: false,
            page: 'list',
            gameBase: {
                _id:'',
                status: '',
                max: 10,
                max_move_time: 0,
                all_time: 0,
                hei_user_id: '',
                bai_user_id: '',
                im_romm_id:''
            },
            gobangMembers: [], // 五子棋游戏成员列表

            room_id: '',
            gobangList: [],
            config: {
                type: 1, // 1为白棋 2为黑棋
                line: 15, // 棋盘线条数
                width: 36,
            },
            first: true,
        }
    },
    computed: {
        ...Vuex.mapState(['userdata']),
        ...Vuex.mapGetters(['isAdmin']),
        gobang_id() {
            return this.gameBase._id
        },
        game_im_id() {
            return this.gameBase.im_romm_id
        },

        // 当前步数
        currentStep() {
            let result = 0
            this.gobangList.forEach((item) => {
                item.forEach((itey) => {
                    if (itey.step_number > 0) {
                        result += 1
                    }
                })
            })
            return result
        },
        getQi() {
            // 自己是黑棋还是白棋
            let { _id } = this.userdata
            if (_id == this.gameBase.bai_user_id) {
                return '白棋'
            }

            if (_id == this.gameBase.hei_user_id) {
                return '黑棋'
            }

            return '观众'
        },
        step_content() {
            // 具体下的内容 1白字 2黑子
            const obj = {
                白棋: 1,
                黑棋: 2,
            }

            return obj[this.getQi] || 0
        },
    },
    created() {
        this.init()
    },

    methods: {
        del_gobang() {
            // 清空数据
            // this.gobangList = []
            // this.isGameOver = false
            // this.gameBase = {
            //     status: '',
            //     max: 10,
            //     max_move_time: 0,
            //     all_time: 0,
            //     hei_user_id: '',
            //     bai_user_id: '',
            // }

            location.reload()
        },
        clearGame() {
            this.$confirm('确定要清空游戏吗?')
                .then((res) => {
                    this.$refs.imRef.send_msg({
                        room_id: this.room_id,
                        specialType: 3,
                        gobang_id: this.gobang_id,
                    })
                })
                .catch(() => {})
        },
        // 黑棋先行,一人一只下
        isShould() {
            let { currentStep, gobangList, step_content } = this
            // 黑棋个数
            let heiNumber = 0

            // 白棋个数
            let baiNumber = 0

            // 遍历棋盘
            gobangList.forEach((aaa) => {
                aaa.forEach((item) => {
                    if (item.step_content == 1) {
                        baiNumber += 1
                    }

                    if (item.step_content == 2) {
                        heiNumber += 1
                    }
                })
            })

            // 判断现在下的步数的奇数还是偶数
            let isOdd = currentStep % 2

            if (step_content === 1) {
                // 白棋
                return isOdd === 1
            }

            if (step_content === 2) {
                // 黑棋
                return isOdd === 0
            }
        },

        clickHandle({ x, y }) {
            let {
                step_content,
                room_id,
                gobang_id,
                userdata: { _id: author_id },
                currentStep,
                isGameOver,
            } = this

            if (isGameOver) {
                return this.$message.warning('游戏已结束')
            }

            // 只有棋手才能下棋
            if (![1, 2].includes(+step_content)) return

            // 判断是否该下子
            if (!this.isShould()) {
                return this.$message.warning('请等待对方落子')
            }

            let obj = {
                room_id,
                specialType: 2,
                gobang_id,
                gobang_member_id: author_id,
                step_number: currentStep + 1,
                step_content,
                x,
                y,
                author_id,
            }
            this.$refs.imRef.send_msg(obj, () => {})
        },
        room_baseinfo({ gobangMembers, room_id }) {
            this.gobangMembers = Array.isArray(gobangMembers) ? gobangMembers : []
            this.room_id = room_id || ''
        },
        initConfigList() {
            // 生成一个二维数组
            let { line } = this.config
            for (let i = 0; i < line; i++) {
                this.gobangList.push([])
                for (let j = 0; j < line; j++) {
                    this.gobangList[i].push({
                        x: i,
                        y: j,
                        step_content: 0, // 0: 空 1: 白 2: 黑
                    })
                }
            }
        },
        async init() {
            this.initConfigList()
            let res = null
            try {
                res = await get_gobang_list()
            } catch (err) {
                return
            }
            if (res.data || this.isArray(res.data.data)) {
                if (res.data.data.length === 0) {
                    let res = await post_gobang({
                        game_name: '五子棋',
                    }).catch(() => {
                        return {}
                    })

                    if (!res.data || !this.isObject(res.data.data)) return
                    Object.assign(this.gameBase,res.data.data)
                }
                if (res.data.data.length) {
                    Object.assign(this.gameBase,res.data.data[0])
                }
            }

            // 获取现有的对局信息
            get_gobang({ gobang_id: this.gobang_id })
                .then((res) => {
                    if (res.data && this.isArrayLength(res.data.data)) {
                        let arr = res.data.data
                        this.gobangList = this.gobangList.map((aaa) => {
                            aaa = aaa.map((item) => {
                                let { x, y } = item
                                arr.find((itey) => {
                                    let { step_content, step_number, gobang_member_id } = itey
                                    if (itey.x == x + 1 && itey.y == y + 1 && step_content) {
                                        Object.assign(item, {
                                            step_content,
                                            step_number,
                                            gobang_member_id,
                                        })

                                        return true
                                    }
                                })

                                return item
                            })

                            return aaa
                        })

                        console.log(this.gobangList)
                    }
                })
                .catch(() => {})
        },

        get_game_content(row) {
            this.gobangList = this.gobangList.map((aaa) => {
                aaa = aaa.map((item) => {
                    let { x, y } = item
                    let { step_content, step_number, gobang_member_id } = row
                    if (row.x == x + 1 && row.y == y + 1 && step_content) {
                        Object.assign(item, {
                            step_content,
                            step_number,
                            gobang_member_id,
                        })
                    }

                    return item
                })

                return aaa
            })

            this.checkWin()
        },

        get_gb_info(row) {
            this.gameBase = Object.assign(this.gameBase, row)
            console.log('get_gb_info',this.gameBase)
        },
        // 判断是否胜利 需要知道哪种棋子胜利
        checkWin() {
            // if(this.first) return
            this.first = false
            // 判断当前棋盘是否有五子连珠
            let { line } = this.config
            let { gobangList: list } = this
            let type = 0 // 0: 没有胜利 1: 白棋胜利 2: 黑棋胜利 3: 平局
            // 判断横向
            for (let i = 0; i < line; i++) {
                for (let j = 0; j < line - 4; j++) {
                    if (
                        list[i][j].step_content !== 0 &&
                        list[i][j].step_content === list[i][j + 1].step_content &&
                        list[i][j].step_content === list[i][j + 2].step_content &&
                        list[i][j].step_content === list[i][j + 3].step_content &&
                        list[i][j].step_content === list[i][j + 4].step_content
                    ) {
                        type = list[i][j].step_content
                        break
                    }
                }
            }
            // 判断纵向
            for (let i = 0; i < line; i++) {
                for (let j = 0; j < line - 4; j++) {
                    if (
                        list[j][i].step_content !== 0 &&
                        list[j][i].step_content === list[j + 1][i].step_content &&
                        list[j][i].step_content === list[j + 2][i].step_content &&
                        list[j][i].step_content === list[j + 3][i].step_content &&
                        list[j][i].step_content === list[j + 4][i].step_content
                    ) {
                        type = list[j][i].step_content

                        break
                    }
                }
            }
            // 判断左斜
            for (let i = 0; i < line - 4; i++) {
                for (let j = 0; j < line - 4; j++) {
                    if (
                        list[i][j].step_content !== 0 &&
                        list[i][j].step_content === list[i + 1][j + 1].step_content &&
                        list[i][j].step_content === list[i + 2][j + 2].step_content &&
                        list[i][j].step_content === list[i + 3][j + 3].step_content &&
                        list[i][j].step_content === list[i + 4][j + 4].step_content
                    ) {
                        type = list[i][j].step_content
                        break
                    }
                }
            }

            // 判断右斜
            for (let i = 0; i < line - 4; i++) {
                for (let j = 4; j < line; j++) {
                    if (
                        list[i][j].step_content !== 0 &&
                        list[i][j].step_content === list[i + 1][j - 1].step_content &&
                        list[i][j].step_content === list[i + 2][j - 2].step_content &&
                        list[i][j].step_content === list[i + 3][j - 3].step_content &&
                        list[i][j].step_content === list[i + 4][j - 4].step_content
                    ) {
                        type = list[i][j].step_content
                        break
                    }
                }
            }

            // 判断是否平局
            let flag = true
            for (let i = 0; i < line; i++) {
                for (let j = 0; j < line; j++) {
                    if (list[i][j].step_content == 0) {
                        flag = false
                        break
                    }
                }
            }

            // 如果是平局
            if (flag) {
                type = 3
            }

            const obj = {
                1: '白棋胜利',
                2: '黑棋胜利',
                3: '平局',
            }

            if (obj[type]) {
                this.$message.success(obj[type])
                this.isGameOver = true
            }
        },
    },
}
</script>

<style lang="scss" scoped></style>

express代码

Gobang 为五子棋基本设置表

GobangMember 五子棋对战与观战人表

GobangItem 每一步的对战信息表

js 复制代码
const { io } = require("../../tool/socket.js");
const { AuthorInfo } = require("../../mod/author/author_info");
const { ImRoom } = require("../../mod/game/im_room.js");
const { ImRoomSys } = require("../../mod/game/im_room_sys.js");
const { ImRoomMember } = require("../../mod/game/im_room_member.js");
const { Game } = require("../../mod/game/game.js");
const { GameList } = require("../../mod/game/game_list.js");
const { Gobang } = require("../../mod/game/gobang.js");
const { GobangMember } = require("../../mod/game/gobang_member.js");
const { GobangItem } = require("../../mod/game/gobang_item.js");

let allSocket = {};

// 监听客户端的连接
io.on("connection", function (socket) {
  allSocket[socket.id] = socket;
  // 监听用户掉线
  socket.on("disconnect", async () => {
    // 更新用户状态
    let user = await ImRoomMember.findOneAndUpdate(
      { socket_id: socket.id },
      { status: "2" }
    );
    if (user) {
      delete allSocket[user.im_room_id];
      // 这是触发的方法数组,默认只有im的人员信息变化
      const funStatus = ["members_change"];
      // 对于五子棋游戏相关退出房间操作
      try {
        let res = await GobangMember.findOneAndUpdate(
          { socket_id: socket.id },
          { status: "2" }
        );
        // TODO: 这儿存在性能问题

        if (res.n == 1) {
          funStatus.push("gobang_members_change");
        }
      } catch (err) {
        console.log(err);
      }

      // 向房间的用户同步信息
      sendMsgToRoom(user.im_room_id, null, funStatus);
    }
  });

  // 监听加入房间
  socket.on("join_room", async (data) => {
    if (!global.isObject(data)) {
      resMsg("加入房间参数错误", 400);
      return;
    }
    // game_id 是游戏id,只有游戏才需要传入
    let { user_id, room_id, gobang_id } = data;
    if (!user_id) {
      resMsg("用户id不能为空", 400);
      return;
    }
    let user = await AuthorInfo.findOne({ _id: user_id });
    if (!user) {
      resMsg("用户不存在", 400);
      return;
    }

    if (!room_id) {
      resMsg("房间id不能为空", 400);
      return;
    }
    let room = await ImRoom.findOne({ _id: room_id, status: "1" });
    if (!room) {
      resMsg("房间不存在", 400);
      return;
    }

    let { max, status } = room;
    if (+status !== 1) {
      resMsg("房间未开放", 300);
      return;
    }

    // 查找所有加入该房间,并且状态为在线的用户
    let members = await ImRoomMember.find({
      im_room_id: room_id,
      status: 1,
    }).countDocuments();

    if (members >= max) {
      resMsg("房间已满", 300);
      return;
    }

    // 查找用户是否已经加入过该房间
    let oldUser = await ImRoomMember.findOne({
      im_room_id: room_id,
      author_id: user_id,
    });
    if (!oldUser) {
      let res = await new ImRoomMember({
        im_room_id: room_id,
        author_id: user_id,
        author_type: 2,
        created_time: getCurrentTimer(),
        updated_time: getCurrentTimer(),
        status: 1,
        socket_id: socket.id,
      }).save();

      if (!res) {
        resMsg("加入房间失败", 400);
        return;
      }
    } else {
      await ImRoomMember.updateOne(
        { im_room_id: room_id, author_id: user_id },
        { socket_id: socket.id, status: 1 }
      );
    }

    // 这是触发的方法数组,默认只有im的人员信息变化
    const funStatus = ["members_change"];

    // 对于五子棋游戏相关加入房间操作
    if (gobang_id) {
      let game = await Gobang.findOne({ _id: gobang_id });
      if (!game) {
        resMsg("游戏不存在", 400);
        return;
      }

      // 查找用户是否已经加入过该游戏
      let oldUser = await GobangMember.findOne({
        gobang_id,
        author_id: user_id,
      });

      if (!oldUser) {
        let res = await new GobangMember({
          gobang_id,
          author_id: user_id,
          created_time: getCurrentTimer(),
          updated_time: getCurrentTimer(),
          status: 1,
          socket_id: socket.id,
          user_type: "3",
        }).save();

        if (!res) {
          resMsg("加入游戏失败", 400);
          return;
        }
      } else {
        try {
          await GobangMember.updateOne(
            { gobang_id, author_id: user_id },
            { socket_id: socket.id, status: 1 }
          );
        } catch (error) {
          console.log("err", err);
        }
      }

      // 查看是否需要更新游戏基本信息-黑棋与白棋
      let gameInfo = await Gobang.findOne({ _id: gobang_id });
      if (gameInfo) {
        let { bai_user_id, hei_user_id } = gameInfo;
        // 查看用户是否在线
        let baiUser = await GobangMember.findOne({
          author_id: bai_user_id,
          gobang_id,
        });
        let heiUser = await GobangMember.findOne({
          author_id: hei_user_id,
          gobang_id,
        });
        console.log(111,heiUser,baiUser)
        if (!heiUser) {
          await Gobang.updateOne({ _id: gobang_id }, { hei_user_id: user_id });
        } else if (!baiUser) {
          await Gobang.updateOne({ _id: gobang_id }, { bai_user_id: user_id });
        }
        
      }
      funStatus.push("get_gb");
      funStatus.push("gobang_members_change");
    }

    // 房间信息改变,向房间内所有在线用户推送房间信息
    sendMsgToRoom(room_id, null, funStatus, gobang_id);
  });

  // 主动推出登录
  socket.on("live_room", async (data) => {
    let { room_id, user_id, gobang_id } = data;

    // 更新用户状态
    let user = await ImRoomMember.findOneAndUpdate(
      { im_room_id: room_id, author_id: user_id },
      { status: "2" }
    );
    if (user) {
      delete allSocket[user.socket_id];

      // 这是触发的方法数组,默认只有im的人员信息变化
      const funStatus = ["members_change"];
      if (gobang_id) {
        // 对于五子棋游戏相关退出房间操作
        try {
          await GobangMember.findOneAndUpdate(
            { gobang_id, author_id: user_id },
            { status: "2" }
          );
        } catch (err) {
          console.log(err);
        }

        funStatus.push("gobang_members_change");
      }

      // 向房间的用户同步信息
      sendMsgToRoom(room_id, null, funStatus, gobang_id);
    }
  });

  // 发送消息
  socket.on("send_msg", async (data) => {
    if (!global.isObject(data)) return;
    // time是时长
    // specialType 默认1 im的发送消息;2 五子棋的发送下棋消息 3 五子棋发送清空数据消息
    let {
      room_id,
      author_id,
      content,
      msg_type = "1",
      time = 0,
      poster = "",
      video_width = "",
      video_height = "",
      specialType = 1,
      gobang_id,
      gobang_member_id,
      step_number,
      step_content = 0,
      x,
      y,
    } = data;
    if (specialType == 3) {
      // 有关五子棋的消息
      if (!gobang_id || !room_id) {
        resMsg("清空数据消息有字段缺失", 400, "err", socket);
        return;
      }
      let gobang = await Gobang.findOneAndDelete({ _id: gobang_id });
      if (!gobang) {
        resMsg("删除失败", 400, "err", socket);
        return;
      }

      let { im_romm_id } = gobang;

      // 删除人员
      await ImRoomMember.deleteMany({ gobang_id: gobang_id });

      // 删除对局信息
      await GobangItem.deleteMany({ gobang_id: gobang_id });

      // 删除聊天室
      await ImRoom.deleteOne({ _id: im_romm_id });

      // 删除聊天记录
      await ImRoomSys.deleteMany({ im_romm_id });
      // 删除聊天成员信息
      await ImRoomMember.deleteMany({ im_romm_id });

      console.log(111)
      sendMsgToRoom(room_id, null, ["del_gobang"], gobang_id);
      return
    }

    if (specialType == 2) {
      // 有关五子棋的消息
      console.log(data);
      if (
        !room_id ||
        !gobang_id ||
        !gobang_member_id ||
        !gobang_member_id ||
        !step_number ||
        !x ||
        !y ||
        !author_id
      ) {
        resMsg("下棋消息有字段缺失", 400, "err", socket);
        return;
      }

      if (![1, 2].includes(+step_content)) {
        resMsg("观众不能下棋", 400, "err", socket);
        return;
      }
      let oldGb = await GobangItem.findOne({
        gobang_id,
        step_number,
      });

      if (oldGb) {
        resMsg("您已经下过棋了", 400);
        return;
      }

      try {
        let newGb = await new GobangItem({
          gobang_id,
          gobang_member_id: author_id,
          step_number,
          created_time: getCurrentTimer(),
          updated_time: getCurrentTimer(),
          step_content,
          x,
          y,
        }).save();

        sendMsgToRoom(room_id, newGb, [], gobang_id);
      } catch (err) {
        console.log(err);
        resMsg("保存步数失败", 400);
        return;
      }

      return;
    }
    if (!content) {
      resMsg("请输入内容", 400);
      return;
    }
    // 判断用户是否存在
    if (!author_id) {
      resMsg("用户id不能为空", 400);
      return;
    }

    let user = await AuthorInfo.findOne({ _id: author_id });
    if (!user) {
      resMsg("用户id不能为空", 400);
      return;
    }

    // 判断房间是否存在
    if (!room_id) {
      resMsg("房间id不能为空", 400);
      return;
    }

    let room = await ImRoom({ _id: room_id, status: "1" });
    if (!room) {
      resMsg("房间未开放", 400);
      return;
    }

    if (!content) {
      resMsg("消息内容不能为空", 400);
      return;
    }

    // 保存消息
    let params = {
      im_room_id: room_id,
      author_id: author_id,
      content: content,
      msg_type,
      created_time: getCurrentTimer(),
      updated_time: getCurrentTimer(),
    };

    if (time) {
      params.time = time;
    }

    if (msg_type == 4) {
      if (poster) {
        params.poster = poster;
      }
      if (video_width) {
        params.video_width = video_width;
      }
      if (video_height) {
        params.video_height = video_height;
      }
    }

    let room_sys = await new ImRoomSys(params).save();
    if (!room_sys) {
      resMsg("保存消息失败", 400);
      return;
    }
    // 找出对应的成员信息
    let userinfo = await AuthorInfo.findOne(
      { _id: author_id },
      {
        username: 1,
        header_img: 1,
      }
    );
    if (!userinfo) {
      resMsg("用户信息不存在", 400);
      return;
    }
    room_sys.author_id = userinfo;
    sendMsgToRoom(room_id, room_sys);
  });

  /**
   * 向一个房间内的所有在线用户推送房间的基本信息
   * row 本次聊天信息
   * name 触发事件的小别名
   * **/
  async function sendMsgToRoom(room_id, row = null, names = [], game_id = "") {
    if (!room_id) return;

    // 找出全部在线的成员
    let members = await ImRoomMember.find(
      {
        im_room_id: room_id,
        status: 1,
      },
      { socket_id: 1 }
    );

    if (!members || members.length === 0) return;
    let sockets = members.map((item) => item.socket_id);

    // 找出房间信息
    let room = (await ImRoom.findOne({ _id: room_id, status: "1" })) || {};

    // 查找出当前房间的总消息数
    let roomSysCount = await ImRoomSys.find({
      im_room_id: room_id,
    }).countDocuments();

    let res = {
      data: room,
      roomSysCount,
      msg: "房间信息已更新",
    };

    if (names.length > 0) {
      // 去重
      names = [...new Set(names)];
      for (let i = 0; i < names.length; i++) {
        const item = names[i];
        switch (item) {
          case "members_change":
            // 人员状态有变化
            let roomMembers = await ImRoomMember.find(
              { im_room_id: room_id },
              { author_id: 1, status: 1, room_username: 1 }
            )
              .populate("author_id", "username header_img")
              .exec();
            res.roomMembers = roomMembers;
            break;

          case "gobang_members_change":
            // 五子棋人员状态有变化
            let gobangMembers = await GobangMember.find({ gobang_id: game_id })
              .populate("author_id", "username header_img")
              .exec();
            Object.assign(res, {
              gobangMembers,
            });
            break;

          case "get_gb":
            // 获取游戏的基本信息
            let gameInfo = await Gobang.findOne({ _id: game_id });
            if (gameInfo) {
              res.gb_info = gameInfo;
            }
            break;

          case "del_gobang":
            res.is_del_gobang = true;
            break;
        }
      }
    }

    if (global.isObject(row)) {
      if (game_id) {
        // 五子棋消息
        res.game_content = row;
      } else {
        // im有新消息
        res.content = row;
      }
    }

    sockets.forEach((item) => {
      let socket = allSocket[item];

      if (socket) {
        resMsg(res, 200, "room_baseinfo", socket);
      }
    });
  }

  // 获取当前时间戳
  function getCurrentTimer() {
    return Date.now();
  }

  // 统一返回消息
  function resMsg(msg, code = 400, name = "err", _socket) {
    let obj = {
      code,
    };
    if (code === 200) {
      obj.msg = "操作成功";
      obj.data = msg;
    } else {
      obj.msg = msg;
    }

    socket = _socket ? _socket : socket;
    socket.emit(name, obj);
  }
});
相关推荐
生椰拿铁You2 分钟前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生13 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
baiduopenmap27 分钟前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish35 分钟前
小程序webview我爱死你了 小程序webview和H5通讯
前端
广煜永不挂科1 小时前
Devexpress.Dashboard的调用二义性
c#·express
请叫我欧皇i1 小时前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_1 小时前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
guokanglun1 小时前
空间数据存储格式GeoJSON
前端
zhang-zan1 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
ZBY520312 小时前
【Vue】 npm install amap-js-api-loader指南
javascript·vue.js·npm