qs_xinchun2026_h5/pages/canvas2.vue

792 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="canvas-container">
<view class="header">
<text class="title">Canvas图片拖拽示例</text>
<text class="subtitle">点击并拖动图片到目标区域</text>
</view>
<view class="main-content">
<view class="canvas-wrapper">
<canvas
canvas-id="imageCanvas"
id="imageCanvas"
class="canvas-element"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<view class="canvas-info">
<text>Canvas尺寸: {{ canvasWidth }} × {{ canvasHeight }} px</text>
<text>图片状态: {{ imageStatus }}</text>
</view>
</view>
<view class="controls">
<view class="control-card">
<text class="control-title">操作说明</text>
<view class="instructions">
<text>1. 点击并按住图片开始拖拽</text>
<text>2. 拖动图片到蓝色目标区域</text>
<text>3. 释放手指完成拖放</text>
</view>
<view class="image-url">
<text>图片URL:</text>
<text class="url-text">{{ imageUrl }}</text>
</view>
</view>
<view class="control-card">
<text class="control-title">目标区域</text>
<view :class="['target-area', { 'target-hit': isInTarget }]">
<text>目标位置: ({{ target.x }}, {{ target.y }})</text>
<text>尺寸: {{ target.width }} × {{ target.height }}</text>
<text v-if="isInTarget" class="success-text">✅ 图片已在目标区域内</text>
</view>
</view>
<view class="control-card">
<text class="control-title">状态信息</text>
<view class="status">
<text :class="['status-text', { 'dragging': isDragging }, { 'success': isInTarget }]">
{{ statusText }}
</text>
</view>
<view class="stats">
<view class="stat-item">
<text class="stat-value">{{ dragCount }}</text>
<text class="stat-label">拖拽次数</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ targetHits }}</text>
<text class="stat-label">命中目标</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ Math.round(dragDistance) }}</text>
<text class="stat-label">移动距离</text>
</view>
</view>
</view>
<view class="control-card">
<text class="control-title">图片位置</text>
<view class="position-info">
<text>X坐标: {{ image.x.toFixed(0) }} px</text>
<text>Y坐标: {{ image.y.toFixed(0) }} px</text>
<text>宽度: {{ image.width }} px</text>
<text>高度: {{ image.height }} px</text>
</view>
</view>
<button class="reset-btn" @tap="resetImage">重置图片位置</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// Canvas尺寸
canvasWidth: 350,
canvasHeight: 500,
// Canvas上下文
ctx: null,
// 图片URL
imageUrl: 'https://static.vecteezy.com/system/resources/previews/026/105/544/non_2x/example-icon-design-free-vector.jpg',
// 图片对象
image: {
x: 50,
y: 50,
width: 120,
height: 120,
isDragging: false,
dragOffsetX: 0,
dragOffsetY: 0,
startX: 0,
startY: 0,
img: null,
loaded: false
},
// 目标区域
target: {
x: 150,
y: 200,
width: 140,
height: 100
},
// Canvas位置信息
canvasRect: {
left: 0,
top: 0,
width: 0,
height: 0
},
// 状态
isDragging: false,
isInTarget: false,
dragCount: 0,
targetHits: 0,
dragDistance: 0,
statusText: '点击图片开始拖拽',
imageStatus: '加载中...',
// 动画帧ID
animationFrameId: null,
lastRenderTime: 0,
renderInterval: 16 // ~60fps
}
},
onLoad() {
// 页面加载后初始化Canvas
this.$nextTick(() => {
setTimeout(() => {
this.initCanvas()
}, 100)
})
},
onUnload() {
// 页面卸载时停止动画
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
}
},
onShow() {
// 页面显示时开始动画
this.startAnimation()
},
onHide() {
// 页面隐藏时停止动画
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
},
methods: {
// 初始化Canvas
initCanvas() {
// 创建Canvas上下文
this.ctx = uni.createCanvasContext('imageCanvas', this)
// 获取Canvas位置信息
this.getCanvasPosition()
// 加载图片
this.loadImage()
// 开始动画循环
this.startAnimation()
},
// 获取Canvas位置信息
getCanvasPosition() {
const query = uni.createSelectorQuery().in(this)
query.select('#imageCanvas').boundingClientRect(res => {
if (res) {
this.canvasRect = {
left: res.left,
top: res.top,
width: res.width,
height: res.height
}
}
}).exec()
},
// 加载图片
loadImage() {
const img = new Image()
img.onload = () => {
console.log('图片加载成功')
this.image.img = img
this.image.loaded = true
this.imageStatus = '已加载'
this.statusText = '点击图片开始拖拽'
// 如果图片尺寸太大,调整显示尺寸
const maxWidth = 150
const maxHeight = 150
if (img.width > maxWidth || img.height > maxHeight) {
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height)
this.image.width = img.width * ratio
this.image.height = img.height * ratio
}
}
img.onerror = () => {
console.error('图片加载失败,使用备选方案')
this.imageStatus = '加载失败,使用备选图片'
this.createFallbackImage()
}
this.createFallbackImage()
// img.src = '/static/dzm/img_duck.png'
},
// 创建备选图片(如果网络图片加载失败)
createFallbackImage() {
// 创建一个备用的Canvas来绘制图片
const fallbackCanvas = document.createElement('canvas')
const fallbackCtx = fallbackCanvas.getContext('2d')
fallbackCanvas.width = this.image.width
fallbackCanvas.height = this.image.height
// 绘制橙色背景
fallbackCtx.fillStyle = '#FF8C00'
fallbackCtx.fillRect(0, 0, this.image.width, this.image.height)
// 绘制边框
fallbackCtx.strokeStyle = '#333'
fallbackCtx.lineWidth = 2
fallbackCtx.strokeRect(0, 0, this.image.width, this.image.height)
// 绘制示例图标
fallbackCtx.fillStyle = 'white'
fallbackCtx.font = 'bold 14px Arial'
fallbackCtx.textAlign = 'center'
fallbackCtx.textBaseline = 'middle'
fallbackCtx.fillText('示例', this.image.width / 2, this.image.height / 2 - 10)
fallbackCtx.font = '12px Arial'
fallbackCtx.fillText('图片', this.image.width / 2, this.image.height / 2 + 10)
// 转换为图片
const img = new Image()
img.src = fallbackCanvas.toDataURL('image/png')
img.onload = () => {
this.image.img = img
this.image.loaded = true
}
},
// 开始动画循环
startAnimation() {
const animate = (timestamp) => {
if (timestamp - this.lastRenderTime >= this.renderInterval) {
this.drawCanvas()
this.lastRenderTime = timestamp
}
this.animationFrameId = requestAnimationFrame(animate)
}
this.animationFrameId = requestAnimationFrame(animate)
},
// 绘制Canvas内容
drawCanvas() {
if (!this.ctx) return
// 清除Canvas
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制目标区域
this.drawTargetArea()
// 绘制可拖拽图片
this.drawImage()
// 绘制坐标信息
this.drawInfo()
// 执行绘制
this.ctx.draw(true)
},
// 绘制目标区域
drawTargetArea() {
this.ctx.setFillStyle(this.isInTarget ? 'rgba(46, 204, 113, 0.2)' : 'rgba(52, 152, 219, 0.1)')
this.ctx.setStrokeStyle(this.isInTarget ? '#2ecc71' : '#3498db')
this.ctx.setLineWidth(2)
this.ctx.setLineDash([5, 5])
this.ctx.fillRect(this.target.x, this.target.y, this.target.width, this.target.height)
this.ctx.strokeRect(this.target.x, this.target.y, this.target.width, this.target.height)
this.ctx.setLineDash([])
// 绘制目标区域文字
this.ctx.setFontSize(12)
this.ctx.setFillStyle('#3498db')
this.ctx.setTextAlign('center')
this.ctx.fillText(
'目标区域',
this.target.x + this.target.width / 2,
this.target.y + this.target.height / 2
)
},
// 绘制图片
drawImage() {
if (this.image.loaded && this.image.img) {
// 绘制图片
this.ctx.drawImage(
this.image.img,
this.image.x,
this.image.y,
this.image.width,
this.image.height
)
// 如果正在拖拽,绘制高亮边框
if (this.image.isDragging) {
this.ctx.setStrokeStyle('#e74c3c')
this.ctx.setLineWidth(3)
this.ctx.setLineDash([5, 5])
this.ctx.strokeRect(
this.image.x - 5,
this.image.y - 5,
this.image.width + 10,
this.image.height + 10
)
this.ctx.setLineDash([])
}
} else {
// 图片未加载时绘制占位符
this.ctx.setFillStyle('#FF8C00')
this.ctx.fillRect(this.image.x, this.image.y, this.image.width, this.image.height)
this.ctx.setStrokeStyle('#333')
this.ctx.setLineWidth(2)
this.ctx.strokeRect(this.image.x, this.image.y, this.image.width, this.image.height)
this.ctx.setFontSize(14)
this.ctx.setFillStyle('#FFFFFF')
this.ctx.setTextAlign('center')
this.ctx.fillText(
'加载中...',
this.image.x + this.image.width / 2,
this.image.y + this.image.height / 2
)
}
},
// 绘制信息
drawInfo() {
this.ctx.setFontSize(10)
this.ctx.setFillStyle('#666')
this.ctx.setTextAlign('left')
this.ctx.setTextBaseline('top')
// 图片位置信息
this.ctx.fillText(
`图片位置: (${Math.round(this.image.x)}, ${Math.round(this.image.y)})`,
10, 10
)
// 目标位置信息
this.ctx.fillText(
`目标位置: (${this.target.x}, ${this.target.y})`,
10, 25
)
// 拖拽次数信息
this.ctx.fillText(
`拖拽次数: ${this.dragCount}`,
10, 40
)
},
// 检查触摸点是否在图片内
isPointInImage(x, y) {
return x >= this.image.x &&
x <= this.image.x + this.image.width &&
y >= this.image.y &&
y <= this.image.y + this.image.height
},
// 检查图片是否在目标区域内
checkImageInTarget() {
const img = this.image
const imgCenterX = img.x + img.width / 2
const imgCenterY = img.y + img.height / 2
const hit =
imgCenterX >= this.target.x &&
imgCenterX <= this.target.x + this.target.width &&
imgCenterY >= this.target.y &&
imgCenterY <= this.target.y + this.target.height
this.isInTarget = hit
return hit
},
// 计算两点距离
calculateDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
},
// 触摸开始事件
handleTouchStart(e) {
const touch = e.touches[0]
if (!touch) return
// 更新Canvas位置信息
this.getCanvasPosition()
// 异步执行确保Canvas位置信息已更新
setTimeout(() => {
const touchX = touch.clientX - this.canvasRect.left
const touchY = touch.clientY - this.canvasRect.top
if (this.isPointInImage(touchX, touchY)) {
this.image.isDragging = true
this.image.dragOffsetX = touchX - this.image.x
this.image.dragOffsetY = touchY - this.image.y
this.image.startX = this.image.x
this.image.startY = this.image.y
this.isDragging = true
this.statusText = '正在拖拽图片...'
}
}, 50)
},
// 触摸移动事件
handleTouchMove(e) {
if (!this.image.isDragging) return
e.preventDefault()
const touch = e.touches[0]
if (!touch) return
const touchX = touch.clientX - this.canvasRect.left
const touchY = touch.clientY - this.canvasRect.top
// 计算移动距离
const distance = this.calculateDistance(
this.image.x,
this.image.y,
touchX - this.image.dragOffsetX,
touchY - this.image.dragOffsetY
)
this.dragDistance += distance
// 更新图片位置
this.image.x = touchX - this.image.dragOffsetX
this.image.y = touchY - this.image.dragOffsetY
// 限制边界
this.image.x = Math.max(0, Math.min(
this.canvasWidth - this.image.width,
this.image.x
))
this.image.y = Math.max(0, Math.min(
this.canvasHeight - this.image.height,
this.image.y
))
// 检查是否进入目标区域
this.checkImageInTarget()
},
// 触摸结束事件
handleTouchEnd() {
if (this.image.isDragging) {
this.image.isDragging = false
this.isDragging = false
this.dragCount++
if (this.checkImageInTarget()) {
this.targetHits++
this.statusText = '图片已在目标区域内'
} else {
this.statusText = '拖拽完成'
}
}
},
// 重置图片位置
resetImage() {
this.image.x = 50
this.image.y = 50
this.image.isDragging = false
this.isDragging = false
this.isInTarget = false
this.statusText = '位置已重置,点击图片开始拖拽'
}
}
}
</script>
<style>
.canvas-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
}
.header {
text-align: center;
margin-bottom: 20px;
padding: 20px;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.title {
display: block;
font-size: 22px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 8px;
}
.subtitle {
display: block;
font-size: 16px;
color: #7f8c8d;
}
.main-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.canvas-wrapper {
background-color: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
}
.canvas-element {
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
touch-action: none;
}
.canvas-info {
margin-top: 10px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 8px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.canvas-info text {
font-size: 12px;
color: #666;
text-align: center;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
}
.control-card {
background-color: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.control-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 12px;
display: block;
padding-bottom: 8px;
border-bottom: 2px solid #f0f0f0;
}
.instructions {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
}
.instructions text {
display: block;
color: #555;
font-size: 14px;
line-height: 1.5;
}
.image-url {
background-color: #f8f9fa;
padding: 10px;
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 5px;
}
.image-url text {
font-size: 12px;
color: #666;
}
.url-text {
word-break: break-all;
color: #3498db !important;
font-family: monospace;
font-size: 11px !important;
}
.target-area {
margin-top: 10px;
padding: 12px;
border: 2px dashed #3498db;
border-radius: 8px;
background-color: #e8f4fc;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
transition: all 0.3s;
}
.target-area text {
display: block;
color: #3498db;
font-size: 13px;
text-align: center;
}
.target-hit {
background-color: #d1f2eb;
border-color: #2ecc71;
border-style: solid;
}
.success-text {
color: #27ae60 !important;
font-weight: bold;
margin-top: 5px !important;
}
.status {
padding: 12px;
border-radius: 8px;
background-color: #f8f9fa;
margin-bottom: 15px;
}
.status-text {
display: block;
text-align: center;
font-weight: 500;
color: #666;
}
.status-text.dragging {
color: #e67e22;
background-color: #fff9e6;
padding: 8px;
border-radius: 6px;
}
.status-text.success {
color: #27ae60;
background-color: #d4f8e8;
padding: 8px;
border-radius: 6px;
}
.stats {
display: flex;
justify-content: space-between;
background-color: #f8f9fa;
padding: 12px;
border-radius: 8px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: #3498db;
margin-bottom: 4px;
}
.stat-label {
font-size: 11px;
color: #7f8c8d;
}
.position-info {
display: flex;
flex-direction: column;
gap: 8px;
background-color: #f8f9fa;
padding: 12px;
border-radius: 8px;
}
.position-info text {
display: block;
font-size: 13px;
color: #666;
}
.reset-btn {
background-color: #3498db;
color: white;
border: none;
padding: 15px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
text-align: center;
transition: all 0.3s;
margin-top: 10px;
}
.reset-btn:active {
background-color: #2980b9;
transform: scale(0.98);
}
@media (min-width: 768px) {
.main-content {
flex-direction: row;
}
.canvas-wrapper {
flex: 1;
}
.controls {
flex: 1;
min-width: 300px;
}
}
</style>