
tetris.vue
html
<template>
<view class="container">
<!-- 开始弹窗 -->
<view class="modal" v-if="showStartModal">
<view class="modal-content">
<text class="title">俄罗斯方块</text>
<view class="speed-box">
<text>速度:{{ speed }}</text>
<slider :value="speed" min="1" max="10" activeColor="#42b983" @change="onSpeedChange" />
</view>
<button class="start-btn" @click="startGame">开始游戏</button>
</view>
</view>
<!-- 游戏画布 -->
<canvas id="game" canvas-id="game" class="game-canvas"></canvas>
<!-- 分数 -->
<view class="score" v-if="gameStarted">分数:{{ score }}</view>
<!-- 控制按钮 -->
<view class="control-group" v-if="gameStarted">
<button @click="moveLeft">◀</button>
<button @click="rotate">⟳</button>
<button @click="moveRight">▶</button>
<button @click="dropDown">▼</button>
<button @click="pauseGame">⏸</button>
</view>
<!-- 暂停弹窗 -->
<view class="modal" v-if="showPauseModal">
<view class="modal-content">
<text class="title">{{ gameOver ? '游戏结束' : '暂停' }}</text>
<button @click="resumeGame" v-if="!gameOver">继续</button>
<button @click="restartGame">重新开始</button>
<button @click="exitGame">退出</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
ctx: null,
width: 300,
height: 600,
cols: 10,
rows: 20,
block: 30,
board: [],
current: null,
score: 0,
gameStarted: false,
gameOver: false,
paused: false,
showStartModal: true,
showPauseModal: false,
speed: 2,
timer: null,
shapes: [
[[1, 1, 1, 1]],
[[1, 1], [1, 1]],
[[1, 1, 1], [0, 1, 0]],
[[1, 1, 1], [1, 0, 0]],
[[1, 1, 1], [0, 0, 1]],
[[1, 1, 0], [0, 1, 1]],
[[0, 1, 1], [1, 1, 0]]
],
colors: [
[0, 255, 255], [255, 255, 0], [128, 0, 128],
[255, 165, 0], [0, 0, 255], [0, 255, 0], [255, 0, 0]
]
}
},
onReady() {
this.ctx = uni.createCanvasContext('game', this)
this.drawBg()
},
onUnload() {
clearInterval(this.timer)
},
methods: {
onSpeedChange(e) {
this.speed = e.detail.value
},
initBoard() {
let board = []
for (let y = 0; y < this.rows; y++) {
board[y] = []
for (let x = 0; x < this.cols; x++) {
board[y][x] = 0
}
}
this.board = board
},
newPiece() {
let i = Math.floor(Math.random() * this.shapes.length)
let shape = this.shapes[i]
this.current = {
shape,
color: this.colors[i],
x: Math.floor(this.cols / 2) - Math.floor(shape[0].length / 2),
y: 0
}
},
startGame() {
this.showStartModal = false
this.gameStarted = true
this.gameOver = false
this.paused = false
this.score = 0
this.initBoard()
this.newPiece()
this.startLoop()
},
startLoop() {
clearInterval(this.timer)
this.timer = setInterval(() => {
if (this.paused || this.gameOver) return
this.tick()
}, 1000 / this.speed)
},
tick() {
this.current.y++
if (this.collide()) {
this.current.y--
this.merge()
this.clearLines()
this.newPiece()
if (this.collide()) {
this.gameOver = true
this.showPauseModal = true
}
}
this.drawAll()
},
collide() {
let p = this.current
for (let y = 0; y < p.shape.length; y++) {
for (let x = 0; x < p.shape[y].length; x++) {
if (p.shape[y][x]) {
let nx = p.x + x
let ny = p.y + y
if (
nx < 0 ||
nx >= this.cols ||
ny >= this.rows ||
(ny >= 0 && this.board[ny][nx])
) return true
}
}
}
return false
},
merge() {
let p = this.current
for (let y = 0; y < p.shape.length; y++) {
for (let x = 0; x < p.shape[y].length; x++) {
if (p.shape[y][x]) {
let ny = p.y + y
let nx = p.x + x
if (ny >= 0) this.board[ny][nx] = 1
}
}
}
},
clearLines() {
let lines = 0
for (let y = this.rows - 1; y >= 0; y--) {
if (this.board[y].every(v => v === 1)) {
this.board.splice(y, 1)
this.board.unshift(new Array(this.cols).fill(0))
lines++
y++
}
}
this.score += lines * 100
},
drawAll() {
this.drawBg()
this.drawBoard()
this.drawPiece()
this.ctx.draw()
},
drawBg() {
this.ctx.setFillStyle('#000')
this.ctx.fillRect(0, 0, this.width, this.height)
},
drawBoard() {
for (let y = 0; y < this.rows; y++) {
for (let x = 0; x < this.cols; x++) {
if (this.board[y][x]) {
this.ctx.setFillStyle('#0ff')
this.ctx.fillRect(x * this.block, y * this.block, this.block - 2, this.block - 2)
}
}
}
},
drawPiece() {
let p = this.current
this.ctx.setFillStyle(`rgb(${p.color[0]},${p.color[1]},${p.color[2]})`)
for (let y = 0; y < p.shape.length; y++) {
for (let x = 0; x < p.shape[y].length; x++) {
if (p.shape[y][x]) {
let nx = p.x + x
let ny = p.y + y
this.ctx.fillRect(
nx * this.block,
ny * this.block,
this.block - 2,
this.block - 2
)
}
}
}
},
moveLeft() {
this.current.x--
if (this.collide()) this.current.x++
},
moveRight() {
this.current.x++
if (this.collide()) this.current.x--
},
rotate() {
let shape = this.current.shape
let rotated = shape[0].map((_, i) =>
shape.map(row => row[i]).reverse()
)
let old = this.current.shape
this.current.shape = rotated
if (this.collide()) this.current.shape = old
},
dropDown() {
while (!this.collide()) this.current.y++
this.current.y--
},
pauseGame() {
this.paused = true
this.showPauseModal = true
},
resumeGame() {
this.paused = false
this.showPauseModal = false
},
restartGame() {
clearInterval(this.timer)
this.startGame()
this.showPauseModal = false
},
exitGame() {
clearInterval(this.timer)
uni.navigateBack()
}
}
}
</script>
<style scoped>
.container {
width: 100%;
min-height: 100vh;
background: #222;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
}
.game-canvas {
width: 300px;
height: 600px;
background: #000;
border: 2px solid #fff;
}
.score {
color: #fff;
font-size: 32rpx;
margin: 20rpx 0;
}
.control-group {
display: flex;
gap: 16rpx;
margin-top: 20rpx;
}
.control-group button {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #42b983;
color: #fff;
font-size: 28rpx;
}
.modal {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
background: #fff;
padding: 40rpx;
border-radius: 20rpx;
text-align: center;
}
.title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 30rpx;
display: block;
}
.speed-box {
margin: 20rpx 0;
}
.start-btn {
background: #42b983;
color: #fff;
padding: 20rpx 40rpx;
border-radius: 10rpx;
margin-top: 20rpx;
}
</style>