vue2 + element-ui 开发网站拼图小游戏-前端项目

我们在学习前端开发的时候,除了要学习基础编程知识,也需要项目来练习我们所学的编程知识,让我们学的知识有更深的体会,这样才可以做到学以致用。 最近在学习前端开发,除了学习 html css js 这些基础知识以为 ,也开始学习 vue2 的相关知识,今天就分享一个 使用 vue2 和 element-ui 组件 开发的一个网站拼图小游戏项目。我觉得这个项目是非常有意思的,但是我发现 这个小游戏 能过到第10关 还是有一定难度的。 先给大家分享一下使用的技术: 前端框架:Vue2 UI 组件库:Element-UI(提供交互组件,如按钮、弹窗等) 编程语言:HTML、CSS、JavaScript node版本: 16.20 构建与部署:支持 Nginx 或静态服务器部署

接下来给大家看一下 这个拼图游戏的样子

先简单的介绍一下 这个游戏的规则 :

拼图游戏共 十关,每一关的拼图难度逐步增加:

第 1 关:2×2 拼图

第 2 关:3×3 拼图

第 3 关:4×4 拼图

第 4 关:5×5 拼图

第 5 关:6×6 拼图

第 6 关:7×7 拼图

第 7 关:8×8 拼图

第 8 关:9×9 拼图

第 9 关:10×10 拼图

第 10 关:11×11 拼图

游戏通过 打乱图片顺序 让玩家进行拼接,玩家需要拖拽或点击来移动拼图块,直到完整还原原始图片。随着关卡的提升,拼图块的数量增加,挑战难度逐步加大。

功能特点:

逐级解锁:必须完成当前关卡,才能进入下一关。

随机打乱拼图:每次挑战都充满新鲜感。

计时功能:可统计玩家完成每一关所用时间,增加挑战性。

好了,大概的网站样子已经分享完了。接下来 看一下项目的目录结构

项目目录结构还是比较简单的,实际一共做了两个页面 ,

一个开始页面:

xml 复制代码
<template>
  <div class="home">
    <div class="container">
      <h1 class="title">拼图十关</h1>
      <div class="card game-levels">
        <h2>关卡选择</h2>
        <p class="level-description">从简单到困难,挑战10个精彩关卡!</p>
        
        <div class="levels-grid">
          <div 
            v-for="level in 10" 
            :key="level"
            class="level-item"
            :class="{ 
              active: level === currentLevel,
              locked: level > highestLevel
            }"
            @click="selectLevel(level)"
          >
            <div class="level-content">
              <span class="level-number">{{ level }}</span>
              <div class="level-preview">
                <img :src="getImageForLevel(level)" alt="关卡预览" />
                <div class="level-difficulty">{{ getDifficultyText(level) }}</div>
              </div>
              <div class="level-status">
                <i v-if="level > highestLevel" class="el-icon-lock"></i>
                <i v-else-if="level < currentLevel" class="el-icon-check"></i>
                <i v-else class="el-icon-right"></i>
              </div>
            </div>
          </div>
        </div>
        
        <div class="level-actions">
          <el-button type="primary" @click="startGame" :disabled="currentLevel > highestLevel">
            开始游戏
          </el-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Home',
  data() {
    return {
      currentLevel: 1,
      gameImages: [
        require('@/assets/images/1.jpeg'),
        require('@/assets/images/2.jpeg'),
        require('@/assets/images/3.jpeg'),
        require('@/assets/images/4.jpeg'),
        require('@/assets/images/5.jpeg'),
        require('@/assets/images/6.jpeg'),
        require('@/assets/images/7.jpeg'),
        require('@/assets/images/8.jpeg'),
        require('@/assets/images/9.jpeg'),
        require('@/assets/images/10.jpeg')
      ]
    }
  },
  computed: {
    ...mapGetters([
      'getHighestLevel'
    ]),
    highestLevel() {
      return this.getHighestLevel
    }
  },
  created() {
    // 初始化应用,加载最高关卡
    this.$store.dispatch('initApp')
  },
  methods: {
    selectLevel(level) {
      // 只能选择已解锁的关卡
      if (level <= this.highestLevel) {
        this.currentLevel = level
      }
    },
    startGame() {
      // 初始化游戏,设置当前关卡
      this.$store.dispatch('initGame', {
        level: this.currentLevel
      })
      
      // 跳转到游戏页面
      this.$router.push('/game')
    },
    getImageForLevel(level) {
      return this.gameImages[level - 1]
    },
    getDifficultyText(level) {
      const difficulty = level + 1
      return `${difficulty}×${difficulty}`
    }
  }
}
</script>

<style lang="scss" scoped>
.home {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f5f7fa;
}

.container {
  width: 100%;
  max-width: 900px;
  padding: 20px;
}

.title {
  font-size: 36px;
  margin-bottom: 30px;
  color: #303133;
  text-align: center;
}

.game-levels {
  width: 100%;
  
  h2 {
    margin-bottom: 10px;
    color: #303133;
    text-align: center;
  }
  
  .level-description {
    text-align: center;
    color: #606266;
    margin-bottom: 30px;
  }
}

.levels-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 15px;
  margin-bottom: 30px;
  
  @media (max-width: 768px) {
    grid-template-columns: repeat(3, 1fr);
  }
  
  @media (max-width: 480px) {
    grid-template-columns: repeat(2, 1fr);
  }
}

.level-item {
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
  border: 2px solid #dcdfe6;
  transition: all 0.3s ease;
  
  &.active {
    border-color: #409EFF;
    transform: scale(1.05);
    box-shadow: 0 0 10px rgba(64, 158, 255, 0.5);
  }
  
  &.locked {
    opacity: 0.7;
    cursor: not-allowed;
    filter: grayscale(50%);
  }
  
  .level-content {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100%;
  }
  
  .level-number {
    position: absolute;
    top: 5px;
    left: 5px;
    background-color: rgba(0, 0, 0, 0.7);
    color: white;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    z-index: 1;
  }
  
  .level-preview {
    position: relative;
    width: 100%;
    padding-bottom: 100%;
    
    img {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    
    .level-difficulty {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      background-color: rgba(0, 0, 0, 0.7);
      color: white;
      padding: 4px;
      font-size: 12px;
      text-align: center;
    }
  }
  
  .level-status {
    padding: 8px;
    text-align: center;
    background-color: #f5f7fa;
    
    i {
      font-size: 16px;
    }
    
    .el-icon-check {
      color: #67C23A;
    }
    
    .el-icon-lock {
      color: #909399;
    }
    
    .el-icon-right {
      color: #409EFF;
    }
  }
}

.level-actions {
  display: flex;
  justify-content: center;
  
  .el-button {
    width: 200px;
  }
}
</style> 

一个玩游戏的页面:

xml 复制代码
<template>
  <div class="game-page">
    <!-- 左侧区域 -->
    <div class="left-section">
      <!-- 原图预览 -->
      <div class="preview-container">
        <h3>原图预览</h3>
        <div class="preview-image">
          <img :src="imageUrl" alt="拼图预览" />
        </div>
      </div>
      
      <!-- 碎片区域 -->
      <div class="pieces-container">
        <h3>拼图碎片</h3>
        <div class="pieces-area" ref="piecesArea"></div>
      </div>
    </div>
    
    <!-- 右侧区域 -->
    <div class="right-section">
      <!-- 游戏信息和控制 -->
      <div class="game-header">
        <h2>拼图游戏</h2>
        <div class="level-badge">第 {{ currentLevel }} 关</div>
        <div class="game-info">
          <div class="info-item">
            <span class="info-label">关卡:</span>
            <span class="info-value">{{ currentLevel }}/10</span>
          </div>
          <div class="info-item">
            <span class="info-label">难度:</span>
            <span class="info-value">{{ difficulty }}×{{ difficulty }}</span>
          </div>
          <div class="info-item">
            <span class="info-label">时间:</span>
            <span class="info-value">{{ formatTime }}</span>
          </div>
          <div class="info-item">
            <span class="info-label">步数:</span>
            <span class="info-value">{{ moves }}</span>
          </div>
        </div>
        <div class="game-controls">
          <el-button 
            type="warning" 
            @click="togglePause"
            v-if="gameStatus === 'playing' || gameStatus === 'paused'"
          >
            {{ gameStatus === 'playing' ? '暂停' : '继续' }}
          </el-button>
          <el-button type="danger" @click="resetGame">重置</el-button>
          <el-button @click="backToHome">返回首页</el-button>
        </div>
        <div class="game-instructions">
          <p>游戏说明:将左侧的拼图碎片拖放到右侧的目标区域中,完成拼图。</p>
          <p class="tip"><i class="el-icon-back"></i> <strong>重要提示:</strong>已放置的碎片<span style="color: #F56C6C; font-weight: bold;">只能通过点击取回</span>,不能拖动到其他位置</p>
        </div>
      </div>
      
      <!-- 目标区域(凹槽) -->
      <div class="target-container">
        <div class="target-area" ref="targetArea">
          <template v-if="gameStatus === 'idle'">
            <div class="loading-overlay">
              <i class="el-icon-loading"></i>
              <p>正在加载游戏...</p>
            </div>
          </template>
          
          <template v-else-if="gameStatus === 'paused'">
            <div class="paused-overlay">
              <i class="el-icon-video-pause"></i>
              <p>游戏已暂停</p>
              <el-button type="primary" @click="togglePause">继续游戏</el-button>
            </div>
          </template>
        </div>
        
        <!-- 游戏完成覆盖层 -->
        <div class="completed-overlay" v-if="gameStatus === 'completed'">
          <i :class="isLastLevel && currentLevel === 10 ? 'el-icon-medal' : 'el-icon-trophy'"></i>
          <h2 v-if="isLastLevel && currentLevel === 10">恭喜你通关成功!</h2>
          <h2 v-else>恭喜你完成了第{{ currentLevel }}关!</h2>
          <p class="level-message">{{ levelCompleteMessage }}</p>
          <p>用时: {{ formatTime }}</p>
          <p>步数: {{ moves }}</p>
          <div class="action-buttons">
            <el-button type="primary" @click="nextLevel" v-if="!isLastLevel">下一关</el-button>
            <el-button type="success" @click="resetGame" v-if="isLastLevel">再玩一次</el-button>
            <el-button @click="backToHome">返回首页</el-button>
          </div>
        </div>
        
        <!-- 拼图错误覆盖层 -->
        <div class="incorrect-overlay" v-if="gameStatus === 'incorrect'">
          <i class="el-icon-warning-outline"></i>
          <h2>拼图似乎有些问题...</h2>
          <p class="error-message">{{ errorMessage }}</p>
          <div class="action-buttons">
            <el-button type="primary" @click="continueGame">继续调整当前拼图</el-button>
            <el-button type="info" @click="showHint">查看提示</el-button>
            <el-button type="danger" @click="resetGame">重新开始本关</el-button>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 隐藏的拼图组件,用于处理游戏逻辑 -->
    <puzzle-board 
      v-show="false"
      ref="puzzleBoard"
      :difficulty="difficulty"
      :image-url="imageUrl"
      @move="handleMove"
      @complete="handleComplete"
      @incorrect="handleIncorrect"
      @pieces-initialized="handlePiecesInitialized"
    />
  </div>
</template>

<script>
import PuzzleBoard from '@/components/PuzzleBoard.vue'
import { mapGetters } from 'vuex'

export default {
  name: 'Game',
  components: {
    PuzzleBoard
  },
  data() {
    return {
      timer: null,
      placedCount: 0,
      totalPieces: 0
    }
  },
  computed: {
    ...mapGetters([
      'getGameStatus',
      'getDifficulty',
      'getTime',
      'getMoves',
      'getImageUrl',
      'getCurrentLevel',
      'getLevelCompleteMessage',
      'isLastLevel',
      'getErrorMessage'
    ]),
    gameStatus() {
      return this.getGameStatus
    },
    difficulty() {
      return this.getDifficulty
    },
    time() {
      return this.getTime
    },
    moves() {
      return this.getMoves
    },
    imageUrl() {
      return this.getImageUrl
    },
    currentLevel() {
      return this.getCurrentLevel
    },
    levelCompleteMessage() {
      return this.getLevelCompleteMessage
    },
    errorMessage() {
      return this.getErrorMessage
    },
    formatTime() {
      const minutes = Math.floor(this.time / 60)
      const seconds = this.time % 60
      return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
    },
    remainingPieces() {
      return Math.max(0, this.totalPieces - this.placedCount)
    }
  },
  created() {
    // 如果没有设置图片URL,返回首页
    if (!this.imageUrl) {
      this.$router.push('/')
      return
    }
  },
  mounted() {
    // 开始游戏
    this.startGame()
    
    // 监听窗口大小变化,调整布局
    window.addEventListener('resize', this.adjustLayout)
    this.adjustLayout()
  },
  beforeDestroy() {
    // 清除计时器
    this.clearTimer()
    
    // 移除事件监听
    window.removeEventListener('resize', this.adjustLayout)
  },
  methods: {
    startGame() {
      this.$store.dispatch('startGame')
      this.startTimer()
    },
    togglePause() {
      if (this.gameStatus === 'playing') {
        this.$store.dispatch('pauseGame')
        this.clearTimer()
      } else if (this.gameStatus === 'paused') {
        this.$store.dispatch('startGame')
        this.startTimer()
      }
    },
    resetGame() {
      this.clearTimer()
      
      // 重新初始化游戏
      this.$store.dispatch('initGame', {
        level: this.currentLevel
      })
      
      // 开始游戏
      this.startGame()
      
      // 重新获取拼图碎片
      this.$nextTick(() => {
        this.$refs.puzzleBoard.initializePuzzle()
      })
    },
    nextLevel() {
      this.clearTimer()
      
      // 进入下一关
      this.$store.dispatch('nextLevel')
      
      // 开始游戏
      this.startGame()
      
      // 重新获取拼图碎片
      this.$nextTick(() => {
        this.$refs.puzzleBoard.initializePuzzle()
      })
    },
    backToHome() {
      this.clearTimer()
      this.$router.push('/')
    },
    startTimer() {
      this.clearTimer()
      this.timer = setInterval(() => {
        this.$store.commit('incrementTime')
      }, 1000)
    },
    clearTimer() {
      if (this.timer) {
        clearInterval(this.timer)
        this.timer = null
      }
    },
    handleMove() {
      this.$store.commit('incrementMoves')
    },
    handleComplete() {
      console.log('游戏完成事件被触发')
      this.clearTimer()
      this.$store.dispatch('completeGame')
    },
    handlePiecesInitialized(pieces, targetCells) {
      this.$nextTick(() => {
        // 重置已放置计数
        this.placedCount = 0
        this.totalPieces = pieces.length
        
        // 清空现有内容
        this.$refs.piecesArea.innerHTML = ''
        this.$refs.targetArea.innerHTML = ''
        
        // 创建目标区域(凹槽)
        const targetGrid = document.createElement('div')
        targetGrid.className = 'puzzle-target-grid'
        targetGrid.style.display = 'grid'
        targetGrid.style.gridTemplateColumns = `repeat(${this.difficulty}, 1fr)`
        targetGrid.style.gridTemplateRows = `repeat(${this.difficulty}, 1fr)`
        targetGrid.style.gap = '1px'
        targetGrid.style.width = '100%'
        targetGrid.style.height = '100%'
        targetGrid.style.border = '2px solid #ddd'
        
        // 添加目标单元格
        targetCells.forEach(cell => {
          const cellElement = document.createElement('div')
          cellElement.className = 'puzzle-cell'
          cellElement.dataset.index = cell.index
          cellElement.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'
          cellElement.style.borderRadius = '2px'
          cellElement.style.border = '1px dashed rgba(0, 0, 0, 0.2)'
          cellElement.style.position = 'relative'
          
          // 添加序号以显示位置(难度大于6时不显示序号)
          if (this.difficulty <= 6) {
            const cellNumber = document.createElement('div')
            cellNumber.className = 'cell-number'
            cellNumber.textContent = cell.index + 1
            cellNumber.style.position = 'absolute'
            cellNumber.style.top = '50%'
            cellNumber.style.left = '50%'
            cellNumber.style.transform = 'translate(-50%, -50%)'
            cellNumber.style.fontSize = this.difficulty <= 4 ? '14px' : '10px'
            cellNumber.style.color = 'rgba(0, 0, 0, 0.3)'
            cellNumber.style.pointerEvents = 'none'
            cellElement.appendChild(cellNumber)
          }
          
          // 添加拖拽事件
          cellElement.addEventListener('dragover', (e) => e.preventDefault())
          cellElement.addEventListener('drop', (e) => {
            e.preventDefault()
            const pieceId = e.dataTransfer.getData('text/plain')
            this.$refs.puzzleBoard.handleExternalDrop(parseInt(pieceId), cell.index)
          })
          
          targetGrid.appendChild(cellElement)
        })
        
        this.$refs.targetArea.appendChild(targetGrid)
        
        // 计算碎片大小,根据难度调整
        const pieceSize = this.calculatePieceSize(this.difficulty)
        
        // 创建拼图碎片
        pieces.forEach(piece => {
          const pieceElement = document.createElement('div')
          pieceElement.className = 'puzzle-piece'
          pieceElement.dataset.id = piece.id
          pieceElement.draggable = true
          
          // 设置样式
          pieceElement.style.width = `${pieceSize}px`
          pieceElement.style.height = `${pieceSize}px`
          pieceElement.style.borderRadius = '2px'
          pieceElement.style.cursor = 'grab'
          pieceElement.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)'
          pieceElement.style.transition = 'transform 0.2s ease'
          pieceElement.style.margin = '3px'
          
          // 设置背景图片
          const pieceWidth = 100 / this.difficulty
          const pieceHeight = 100 / this.difficulty
          pieceElement.style.backgroundImage = `url(${this.imageUrl})`
          pieceElement.style.backgroundSize = `${this.difficulty * 100}% ${this.difficulty * 100}%`
          
          // 使用原始索引(originalIndex)来确定背景位置,而不是当前的row和col
          const originalRow = Math.floor(piece.originalIndex / this.difficulty)
          const originalCol = piece.originalIndex % this.difficulty
          pieceElement.style.backgroundPosition = `-${originalCol * 100}% -${originalRow * 100}%`
          
          // 添加拖拽事件
          pieceElement.addEventListener('dragstart', (e) => {
            e.dataTransfer.setData('text/plain', piece.id)
            e.dataTransfer.effectAllowed = 'move'
          })
          
          // 添加到碎片区域
          this.$refs.piecesArea.appendChild(pieceElement)
          
          // 监听拼图碎片状态变化
          this.$watch(
            () => this.$refs.puzzleBoard.availablePieces.find(p => p.id === piece.id).isPlaced,
            (isPlaced) => {
              if (isPlaced) {
                pieceElement.style.opacity = '0.3'
                pieceElement.style.pointerEvents = 'none'
                pieceElement.style.filter = 'grayscale(100%)'
                pieceElement.style.transform = 'scale(0.85)'
                pieceElement.style.border = '2px dashed #999'
              } else {
                pieceElement.style.opacity = '1'
                pieceElement.style.pointerEvents = 'auto'
                pieceElement.style.filter = 'none'
                pieceElement.style.transform = 'scale(1)'
                pieceElement.style.border = 'none'
              }
            }
          )
        })
        
        // 监听目标单元格状态变化
        targetCells.forEach(cell => {
          this.$watch(
            () => this.$refs.puzzleBoard.targetCells[cell.index].pieceId,
            (pieceId, oldPieceId) => {
              // 更新计数
              if (pieceId !== null && oldPieceId === null) {
                this.placedCount++;
              } else if (pieceId === null && oldPieceId !== null) {
                this.placedCount--;
              }
              
              const cellElement = targetGrid.children[cell.index]
              
              // 清空单元格
              while (cellElement.firstChild) {
                cellElement.removeChild(cellElement.firstChild)
              }
              
              // 如果有拼图碎片,添加到单元格
              if (pieceId !== null) {
                const piece = this.$refs.puzzleBoard.availablePieces.find(p => p.id === pieceId)
                const placedPiece = document.createElement('div')
                placedPiece.className = 'puzzle-piece-placed'
                placedPiece.style.width = '100%'
                placedPiece.style.height = '100%'
                placedPiece.style.borderRadius = '2px'
                placedPiece.style.cursor = 'pointer'
                
                // 设置背景图片
                const pieceWidth = 100 / this.difficulty
                const pieceHeight = 100 / this.difficulty
                placedPiece.style.backgroundImage = `url(${this.imageUrl})`
                placedPiece.style.backgroundSize = `${this.difficulty * 100}% ${this.difficulty * 100}%`
                
                // 使用原始索引(originalIndex)来确定背景位置,而不是当前的row和col
                const originalRow = Math.floor(piece.originalIndex / this.difficulty)
                const originalCol = piece.originalIndex % this.difficulty
                placedPiece.style.backgroundPosition = `-${originalCol * 100}% -${originalRow * 100}%`
                
                // 明确设置为不可拖动
                placedPiece.draggable = false
                
                // 添加阻止拖动的事件处理
                placedPiece.addEventListener('dragstart', (e) => {
                  e.preventDefault()
                  e.stopPropagation()
                  return false
                })
                
                // 添加点击事件,允许取回碎片
                placedPiece.addEventListener('click', () => {
                  this.$refs.puzzleBoard.retrievePiece(cell.index)
                })
                
                // 添加悬停效果
                placedPiece.addEventListener('mouseover', () => {
                  placedPiece.style.boxShadow = '0 0 8px rgba(0, 0, 0, 0.5)'
                  placedPiece.style.transform = 'scale(0.95)'
                  
                  // 添加提示文本
                  const tooltip = document.createElement('div')
                  tooltip.className = 'piece-tooltip'
                  tooltip.textContent = '点击取回'
                  tooltip.style.position = 'absolute'
                  tooltip.style.top = '50%'
                  tooltip.style.left = '50%'
                  tooltip.style.transform = 'translate(-50%, -50%)'
                  tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'
                  tooltip.style.color = 'white'
                  tooltip.style.padding = '2px 6px'
                  tooltip.style.borderRadius = '4px'
                  tooltip.style.fontSize = '12px'
                  tooltip.style.zIndex = '10'
                  placedPiece.appendChild(tooltip)
                })
                
                placedPiece.addEventListener('mouseout', () => {
                  placedPiece.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)'
                  placedPiece.style.transform = 'scale(1)'
                  
                  // 移除提示文本
                  const tooltip = placedPiece.querySelector('.piece-tooltip')
                  if (tooltip) {
                    placedPiece.removeChild(tooltip)
                  }
                })
                
                placedPiece.style.userSelect = 'none'
                placedPiece.style.webkitUserDrag = 'none'
                placedPiece.style.MozUserSelect = 'none'
                placedPiece.style.msUserSelect = 'none'
                
                cellElement.appendChild(placedPiece)
              } else {
                // 如果没有拼图碎片,添加序号(难度大于6时不显示序号)
                if (this.difficulty <= 6) {
                  const cellNumber = document.createElement('div')
                  cellNumber.className = 'cell-number'
                  cellNumber.textContent = cell.index + 1
                  cellNumber.style.position = 'absolute'
                  cellNumber.style.top = '50%'
                  cellNumber.style.left = '50%'
                  cellNumber.style.transform = 'translate(-50%, -50%)'
                  cellNumber.style.fontSize = this.difficulty <= 4 ? '14px' : '10px'
                  cellNumber.style.color = 'rgba(0, 0, 0, 0.3)'
                  cellNumber.style.pointerEvents = 'none'
                  cellElement.appendChild(cellNumber)
                }
              }
            }
          )
        })
      })
    },
    calculatePieceSize(difficulty) {
      // 根据难度调整碎片大小
      if (difficulty <= 3) return 60;
      if (difficulty <= 5) return 50;
      if (difficulty <= 7) return 40;
      if (difficulty <= 9) return 30;
      return 25; // 对于10×10和11×11的难度
    },
    adjustLayout() {
      // 根据窗口大小调整布局
      const targetArea = this.$refs.targetArea
      if (targetArea) {
        const width = targetArea.clientWidth
        const height = targetArea.clientHeight
        const size = Math.min(width, height) * 0.9
        
        const targetGrid = targetArea.querySelector('.puzzle-target-grid')
        if (targetGrid) {
          targetGrid.style.width = `${size}px`
          targetGrid.style.height = `${size}px`
          targetGrid.style.margin = 'auto'
        }
      }
    },
    handleIncorrect() {
      console.log('拼图错误事件被触发')
      this.$store.dispatch('incorrectGame')
    },
    continueGame() {
      // 不重置游戏,只将状态改回 playing,让用户继续尝试调整已放置的拼图
      console.log('继续尝试,保留当前拼图状态')
      this.$store.dispatch('startGame') // 将状态从 incorrect 改回 playing
    },
    showHint() {
      // 显示提示
      this.$alert('提示:仔细对照原图,从边缘开始拼起。特别注意图片的颜色和纹理的连续性。', '拼图提示', {
        confirmButtonText: '知道了',
        type: 'info',
        callback: action => {
          this.$message({
            type: 'info',
            message: '加油!你一定能完成的!'
          });
        }
      });
    }
  }
}
</script>

<style lang="scss" scoped>
.game-page {
  width: 100vw;
  height: 100vh;
  display: flex;
  overflow: hidden;
}

.left-section {
  width: 40%;
  height: 100%;
  display: flex;
  flex-direction: column;
  background-color: #f0f0f0;
  border-right: 1px solid #ddd;
}

.right-section {
  width: 60%;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.preview-container {
  height: 40%;
  padding: 20px;
  display: flex;
  flex-direction: column;
  
  h3 {
    margin-bottom: 10px;
    text-align: center;
    font-size: 18px;
  }
  
  .preview-image {
    flex: 1;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    
    img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
  }
}

.pieces-container {
  height: 60%;
  padding: 20px;
  display: flex;
  flex-direction: column;
  
  h3 {
    margin-bottom: 10px;
    text-align: center;
    font-size: 18px;
  }
  
  .pieces-area {
    flex: 1;
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    padding: 10px;
    overflow-y: auto;
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    justify-content: center;
  }
}

.game-header {
  padding: 20px;
  background-color: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  position: relative;
  
  h2 {
    margin-bottom: 15px;
    text-align: center;
    color: #303133;
  }
  
  .level-badge {
    position: absolute;
    top: 20px;
    right: 20px;
    padding: 5px 10px;
    background-color: #409EFF;
    color: #fff;
    border-radius: 4px;
    font-size: 14px;
    font-weight: bold;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  }
  
  .game-info {
    display: flex;
    justify-content: space-around;
    margin-bottom: 15px;
    
    .info-item {
      text-align: center;
      
      .info-label {
        font-weight: bold;
        margin-right: 5px;
      }
      
      .info-value {
        font-size: 18px;
      }
    }
  }
  
  .game-controls {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-bottom: 15px;
  }
  
  .game-instructions {
    text-align: center;
    font-size: 14px;
    color: #606266;
    
    p {
      margin-bottom: 5px;
    }
    
    .tip {
      font-size: 12px;
      color: #909399;
      font-style: italic;
    }
  }
}

.target-container {
  flex: 1;
  padding: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  
  .target-area {
    width: 100%;
    height: 100%;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #f8f8f8;
    border-radius: 8px;
    box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
  }
}

.loading-overlay,
.paused-overlay,
.completed-overlay,
.incorrect-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(255, 255, 255, 0.95);
  z-index: 100;
  
  i {
    font-size: 48px;
    margin-bottom: 20px;
  }
  
  p {
    font-size: 18px;
    margin-bottom: 20px;
  }
}

.completed-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(255, 255, 255, 0.95);
  z-index: 100;
  
  i {
    font-size: 48px;
    margin-bottom: 20px;
    color: #E6A23C;
    
    &.el-icon-medal {
      color: #F56C6C;
      animation: pulse 1.5s infinite;
    }
  }
  
  h2 {
    margin-bottom: 10px;
    color: #67C23A;
  }
  
  .level-message {
    font-size: 18px;
    color: #409EFF;
    margin-bottom: 20px;
    text-align: center;
    font-weight: bold;
  }
  
  p {
    font-size: 18px;
    margin-bottom: 10px;
  }
  
  .action-buttons {
    margin-top: 20px;
    display: flex;
    gap: 10px;
  }
}

.incorrect-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(255, 255, 255, 0.95);
  z-index: 100;
  
  i {
    font-size: 48px;
    margin-bottom: 20px;
    color: #E6A23C;
  }
  
  h2 {
    margin-bottom: 10px;
    color: #E6A23C;
  }
  
  .error-message {
    font-size: 18px;
    color: #F56C6C;
    margin-bottom: 20px;
    text-align: center;
    max-width: 80%;
  }
  
  .action-buttons {
    margin-top: 20px;
    display: flex;
    gap: 10px;
  }
}

@keyframes pulse {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
  }
}
</style> 

代码内容过多,这边文章里就简单的分享一部分代码了。

如果我们想快速掌握一门编程语言,最好的办法就是多写代码,多练,然后多去看一些完整的项目,看看别人在开发项目的时候 都使用了什么技术,怎么设计一个软件项目,让项目更健壮稳定。

这个拼图小游戏前端基本上算是完成了。bug 也简单的测了一下,但是不保证项目里是否还存在bug。 如果分享的这个拼图小游戏对你有所帮助,源码也已经整理好了,需要的可以去看看。 www.wwwoop.com/home/Index/...

相关推荐
兰德里的折磨55015 分钟前
基于若依和elementui实现文件上传(导入Excel表)
前端·elementui·excel
喝拿铁写前端18 分钟前
一个列表页面,初级中级高级前端之间的鸿沟就显出来了
前端·架构·代码规范
编程毕设1 小时前
【开题报告+论文+源码】基于SpringBoot+Vue的招聘管理系统的设计与实现
vue.js·spring boot·后端
magic 2451 小时前
ES6变量声明:let、var、const全面解析
前端·javascript·ecmascript·es6
M_chen_M1 小时前
es6学习02-let命令和const命令
前端·学习·es6
好_快1 小时前
Lodash源码阅读-dropWhile
前端·javascript·源码阅读
M_chen_M2 小时前
JS6(ES6)学习01-babel转码器
前端·学习·es6
好_快2 小时前
Lodash源码阅读-dropRightWhile
前端·javascript·源码阅读
二川bro2 小时前
Vue 项目中 package.json 文件的深度解析
前端·vue.js·json
寰宇视讯2 小时前
铼赛智能Edge mini斩获2025法国设计大奖 | 重新定义数字化齿科美学
前端·数据库·edge