不多说,直接上代码

<template>
  <div class="gameMain">
    <div class="gameName">2048小游戏</div>
    <div class="maxScore">
      最高分:<span id="maxScore">{{ maxScore }}</span>
    </div>
    <div class="col-sm-3 col-md-4"></div>
    <div class="gameBody col-sm-6 col-md-4" id="gameBody" v-touch:left="move" v-touch:right="move" v-touch:up="move" v-touch:down="move">
      <div class="row" v-for="(row, index) in gameList" :key="index">
        <div
          class="item"
          :style="{ background: refreshColorData[item.num] }"
          v-for="(item, idx) in row"
          :key="idx"
        >
          {{ item ? item.num : null }}
        </div>
      </div>
    </div>
    <div class="col-sm-4 col-md-4 gameDirection">
      <span @click="move('up')">上</span>
      <span @click="move('down')">下</span>
      <span @click="move('left')">左</span>
      <span @click="move('right')">右</span>
    </div>
    <div class="scoreAndRefresh col-sm-6 col-md-6">
      <div class="gameScore ">
        得分:<span id="gameScore">{{ gameScore }}</span> 分
      </div>
      <div class="btn btn-danger refreshBtn" @click="refreshGame">刷新</div>
    </div>

   <div class="gameOver" v-if="gameOver">游戏结束</div>
  </div>
</template>

<script>
import touch from './directives.js'
export default {
  name: 'Games',
  directives: { touch },
  data () {
    return {
      refreshColorData: {
        2: 'rgb(250, 225, 188)',
        4: 'rgb(202, 240, 240)',
        8: 'rgb(117, 231, 193)',
        16: 'rgb(240, 132, 132)',
        32: 'rgb(181, 240, 181)',
        64: 'rgb(182, 210, 246)',
        128: 'rgb(255, 207, 126)',
        256: 'rgb(250, 216, 216)',
        521: 'rgb(124, 183, 231)',
        1024: 'rgb(225, 219, 215)',
        2048: 'rgb(221, 160, 221)',
        4096: 'rgb(250, 139, 176)'
      },
      gameOver: false,
      gameScore: 0,
      maxScore: 0, // 最高分
      gameList: null,
      isNewRndItem: false //   // 是否产生新元素
    }
  },
  created () {
    this.gameList = this.matrix(4, 4, null)
    // 游戏初始化
    this.gameInit()
  },
  methods: {
    gameInit () {
      // 初始化分数
      this.gameScore = 0
      this.gameScore = 0
      // 最大分值
      if (localStorage.getItem('maxScore')) {
        this.maxScore = localStorage.getItem('maxScore') - 0
      } else {
        this.maxScore = 0
      }
      // 随机生成两个新元素
      this.newRndItem()
      this.newRndItem()
    },
    move (direction) {
      if (this.gameOver) return false
      // 获取所有非空元素
      let nonEmptyItems = [].concat
        .apply([], this.gameList)
        .filter(item => item.num !== null)
      // 如果按下的方向是左或上,则正向遍历非空元素
      if (direction === 'left' || direction === 'up') {
        for (let i = 0; i < nonEmptyItems.length; i++) {
          let currentItem = nonEmptyItems[i]
          this.itemMove(currentItem, direction)
        }
      } else if (direction === 'right' || direction === 'down') {
        // 如果按下的方向是右或下,则反向遍历非空元素
        for (let i = nonEmptyItems.length - 1; i >= 0; i--) {
          let currentItem = nonEmptyItems[i]
          this.itemMove(currentItem, direction)
        }
      }
      // 是否产生新元素
      if (this.isNewRndItem && !this.gameOver) {
        this.newRndItem()
      }
      this.isGameOver()
    },
    getSideItem (current, direction) {
      let sideItemX = current.id.substr(0, 1)
      let sideItemY = current.id.slice(1, 2)
      let falg
      switch (direction) {
        case 'left':
          falg = sideItemX > 0
          sideItemX = falg ? Number(sideItemX) - 1 : sideItemX
          break
        case 'right':
          falg = sideItemX < 3
          sideItemX = falg ? Number(sideItemX) + 1 : sideItemX
          break
        case 'up':
          falg = sideItemY > 0
          sideItemY = falg ? Number(sideItemY) - 1 : sideItemY
          break
        case 'down':
          falg = sideItemY < 3
          sideItemY = falg ? Number(sideItemY) + 1 : sideItemY
          break
      }
      let currentId = sideItemX + sideItemY
      let currentItem = falg
        ? [].concat(...this.gameList).filter(item => item.id === currentId)[0]
        : null
      // 判断移动方向是否有空位
      return currentItem
    },
    itemMove (currentItem, direction) {
      var sideItem = this.getSideItem(currentItem, direction)
      // 当前元素在最边上
      if (sideItem === null) return false
      // 当前元素不在最后一个且左(右、上、下)侧元素是空元素
      if (sideItem.num === null) {
        this.setGameList(sideItem, currentItem.num)
        sideItem.num = currentItem.num
        currentItem.num = null
        this.itemMove(sideItem, direction)
        this.isNewRndItem = true
      } else if (sideItem.num === currentItem.num) {
        sideItem.num = Number(currentItem.num) * 2
        currentItem.num = null
        this.gameScore += Number(sideItem.num) * 10
        this.maxScore =
          this.maxScore < this.gameScore ? this.gameScore : this.maxScore
        localStorage.setItem('maxScore', this.maxScore)
        this.itemMove(sideItem, direction)
        this.isNewRndItem = true
      }
    },
    // 游戏是否结束
    isGameOver () {
      let nonEmptyItems = [].concat
        .apply([], this.gameList)
        .filter(item => item.num !== null)
      let Items = [].concat
        .apply([], this.gameList)
      let gameOver = true
      if (Items.length === nonEmptyItems.length) { // 所有元素的个数 == 所有非空元素的个数  即没有空元素
        nonEmptyItems.forEach(currentItem => {
          // let up = this.getSideItem(currentItem, 'up') && this.getSideItem(currentItem, 'up').num
          // let down = this.getSideItem(currentItem, 'down') && this.getSideItem(currentItem, 'down').num
          // let left = this.getSideItem(currentItem, 'left') && this.getSideItem(currentItem, 'left').num
          // let right = this.getSideItem(currentItem, 'right') && this.getSideItem(currentItem, 'right').num
          // console.log(up + 'up' + down + 'down' + left + 'left' + right + 'right')
          // alert(currentItem.num + 'up' + this.getSideItem(currentItem, 'up').num + 'down' + this.getSideItem(currentItem, 'down').num + 'left' + this.getSideItem(currentItem, 'left').num + 'right' + this.getSideItem(currentItem, 'right').num)
          if (this.getSideItem(currentItem, 'up') && currentItem.num === this.getSideItem(currentItem, 'up').num) {
            gameOver = false
          } else if (this.getSideItem(currentItem, 'down') && currentItem.num === this.getSideItem(currentItem, 'down').num) {
            gameOver = false
          } else if (this.getSideItem(currentItem, 'left') && currentItem.num === this.getSideItem(currentItem, 'left').num) {
            gameOver = false
          } else if (this.getSideItem(currentItem, 'right') && currentItem.num === this.getSideItem(currentItem, 'right').num) {
            gameOver = false
          }
        })
      } else {
        gameOver = false
      }
      this.gameOver = gameOver
    },
    // 随机生成新数字
    newRndItem () {
      var newRndArr = [2, 2, 4]
      var newRndNum = newRndArr[this.getRandom(0, 2)]
      let emptyItemList = [].concat
        .apply([], this.gameList)
        .filter(item => item.num === null)
      var newRndSite = this.getRandom(0, emptyItemList.length - 1)
      var emptyItem = emptyItemList[newRndSite]
      this.setGameList(emptyItem, newRndNum)
    },
    // 设置数字
    setGameList (item, num) {
      if (!item) return false
      for (var row = 0; row < this.gameList.length; ++row) {
        for (var col = 0; col < this.gameList[row].length; ++col) {
          if (this.gameList[row][col].id === item.id) {
            this.gameList[row][col].num = num
          }
        }
      }
    },
    // 产生随机数,包括min、max
    getRandom (min, max) {
      return min + Math.floor(Math.random() * (max - min + 1))
    },
    // 刷新操作
    refreshGame () {
      this.gameList = this.matrix(4, 4, null)
      this.gameOver = false
      // 游戏初始化
      this.gameInit()
    },
    // 随机生成一个两位数组
    matrix (numrows, numcols, initial) {
      var arr = []
      for (var i = 0; i < numrows; ++i) {
        var columns = []
        for (var j = 0; j < numcols; ++j) {
          columns[j] = { id: j + '' + i, num: initial }
          // columns[j] = initial
        }
        arr[i] = columns
      }
      return arr
    }
  }
}
</script>

<style scoped lang="scss">
// @import "./index.scss";
.gameMain {
  height: calc(100vh - 88px);
  font-size: 28px;
  background: #d7d3b6;
  .gameName {
    font-size: 28px;
    font-weight: bold;
    padding-top: 20px;
  }
  .maxScore {
    font-size: 38px;
    margin: 20px auto;
    span {
      color: red;
      font-weight: bold;
    }
  }
  .gameBody {
    width: 80%;
    height: 50%;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 10px;
    background: #999;
    border-radius: 8px;
    padding-top: 5px;
    padding-bottom: 5px;
    .row {
      display: flex;
      justify-content: space-between;
      .item {
        width: 100px;
        height: 100px;
        border-radius: 10px;
        background: #fff;
        text-align: center;
        line-height: 100px;
        font-size: 30px;
        font-weight: bold;
        margin: 5px;
        color: #666;
      }
    }
  }
  .gameDirection {
    margin: 50px auto;
    font-size: 26px;
    font-weight: bold;
    span {
      width: 100px;
      display: inline-block;
    }
  }
  .gameRule {
    font-size: 26px;
    font-weight: bold;
    margin-top: 5px;
  }
  .gameScore {
    font-size: 20px;
    font-weight: bold;
    line-height: 40px;
    span {
      color: red;
      font-size: 30px;
    }
  }
  .scoreAndRefresh {
    display: flex;
    justify-content: space-around;
        align-items: center;
    width: 280px;
    margin: 20px auto;
    .refreshBtn {
      padding:10px 20px;
    line-height: 40px;
      margin-top: 8px;
      background: #093233;
      color: #fff;
      border-radius: 6px;
    }
  }

  .gameOver{
      color: red;
      font-size: 40px;
  }
}
</style>

 

注释

v-touch:left,v-touch:right,v-touch:up,v-touch:down
使用vue自定义指令

const touch = {
  bind (el, binding, vnode) {
    console.log(binding)
    // 滑动指令
    var touchType = binding.arg // 传入的模式 press swipeRight swipeLeft swipeTop swipeDowm Tap
    var timeOutEvent = 0
    var direction = ''
    // 滑动处理
    var startX, startY

    // 返回角度
    function GetSlideAngle (dx, dy) {
      return Math.atan2(dy, dx) * 180 / Math.PI
    }

    // 根据起点和终点返回方向 1:向上,2:向下,3:向左,4:向右,0:未滑动
    function GetSlideDirection (startX, startY, endX, endY) {
      var dy = startY - endY
      var dx = endX - startX
      var result = 0

      // 如果滑动距离太短
      if (Math.abs(dx) < 2 && Math.abs(dy) < 2) {
        return result
      }

      var angle = GetSlideAngle(dx, dy)
      if (angle >= -45 && angle < 45) {
        result = 'right'
      } else if (angle >= 45 && angle < 135) {
        result = 'up'
      } else if (angle >= -135 && angle < -45) {
        result = 'down'
      } else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) {
        result = 'left'
      }
      return result
    }

    el.addEventListener('touchstart', function (ev) {
      startX = ev.touches[0].pageX
      startY = ev.touches[0].pageY

      // 判断长按
      timeOutEvent = setTimeout(() => {
        timeOutEvent = 0
        if (touchType === 'press') {
          binding.value()
        }
      }, 500)
    }, false)

    el.addEventListener('touchmove', function (ev) {
      clearTimeout(timeOutEvent)
      timeOutEvent = 0
    })

    el.addEventListener('touchend', function (ev) {
      var endX, endY
      endX = ev.changedTouches[0].pageX
      endY = ev.changedTouches[0].pageY
      direction = GetSlideDirection(startX, startY, endX, endY)

      clearTimeout(timeOutEvent)
      switch (direction) {
        case 0:
          break
        case 'up':
          if (touchType === 'up') {
            binding.value(direction)
          }
          break
        case 'down':
          if (touchType === 'down') {
            binding.value(direction)
          }
          break
        case 'left':
          if (touchType === 'left') {
            binding.value(direction)
          }
          break
        case 'right':
          if (touchType === 'right') {
            binding.value(direction)
          }
          break
        default:
      }
    }, false)
  }
}

export default touch