This commit is contained in:
sunmeng 2025-06-26 18:15:12 +08:00
parent 8cba464fc0
commit a2f3daa2ce
25 changed files with 1518 additions and 58 deletions

View File

@ -7,7 +7,13 @@
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {
"usingComponents" : true, "canvas2d": true,
"optimization": {
"subPackages": true
},
"usingComponents": {
"canvas": true
},
"nvueStyleCompiler" : "uni-app", "nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3, "compilerVersion" : 3,
"splashscreen" : { "splashscreen" : {

View File

@ -1,51 +1,310 @@
<template> <template>
<view> <view class="container">
<!-- 定义canvas画布 // canvas200x200 --> <view class="screen">银幕</view>
<canvas canvas-id="canvas" id="canvas" style="width: 200px; height: 200px;border: 1px solid red;" :width="canvasWidth" :height="canvasHeight"></canvas> <canvas
canvas-id="seatCanvas"
<!-- 添加一个按钮用于触发绘图 --> type="2d"
<button @click="handleDraw">点击绘制圆形</button> class="seat-canvas"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<view class="legend">
<view class="legend-item">
<view class="seat-icon available"></view>
<text>可选</text>
</view>
<view class="legend-item">
<view class="seat-icon selected"></view>
<text>已选</text>
</view>
<view class="legend-item">
<view class="seat-icon sold"></view>
<text>已售</text>
</view>
</view>
<view class="selected-seats">
<text>已选座位{{ selectedSeats.join(', ') || '无' }}</text>
</view>
</view> </view>
</template> </template>
<script>
<script setup> export default {
import { ref, onMounted ,getCurrentInstance } from 'vue' data() {
return {
const canvasWidth = ref(200) // Canvas ctx: null,
const canvasHeight = ref(200) // Canvas canvasWidth: 0,
const instance = getCurrentInstance() canvasHeight: 0,
// seatSize: 30,
function draw() { gap: 10,
const ctx = uni.createCanvasContext('canvas',instance) // canvas rows: 8,
console.log(ctx,'12'); cols: 10,
// seats: [],
ctx.beginPath() // selectedSeats: [],
ctx.arc(100, 100, 80, 0, Math.PI * 2) // (100, 100)80 touchStartX: 0,
ctx.setFillStyle('rgba(0, 0, 0, 1)') // touchStartY: 0,
ctx.fill() // offsetX: 0,
offsetY: 0,
// scale: 1
ctx.beginPath() //
ctx.arc(100, 100, 75, 0, Math.PI * 2) // (100, 100)60
ctx.setFillStyle('white') //
ctx.fill() //
ctx.draw()
} }
},
// onReady() {
const handleDraw = () => { this.initSeats()
console.log('111111'); this.initCanvas()
draw(); // },
console.log('222222222'); methods: {
initSeats() {
//
for (let i = 0; i < this.rows; i++) {
this.seats[i] = []
for (let j = 0; j < this.cols; j++) {
//
const isSold = Math.random() < 0.2
this.seats[i][j] = {
row: i,
col: j,
status: isSold ? 'sold' : 'available', // available, selected, sold
x: 0,
y: 0
} }
}
}
},
async initCanvas() {
// canvas
const { canvas, width, height } = await this.getCanvasNode('seatCanvas')
this.canvasWidth = width
this.canvasHeight = height
this.ctx = canvas.getContext('2d')
onMounted(() => { // 使
// Canvas const totalWidth = this.cols * (this.seatSize + this.gap) - this.gap
const totalHeight = this.rows * (this.seatSize + this.gap) - this.gap
this.offsetX = (this.canvasWidth - totalWidth) / 2
this.offsetY = (this.canvasHeight - totalHeight) / 2
this.drawSeats()
},
getCanvasNode(id) {
return new Promise((resolve) => {
const query = uni.createSelectorQuery().in(this)
query.select(`#${id}`)
.fields({ node: true, size: true })
.exec((res) => {
console.log('给我看看',res)
console.log('给我看看',res)
console.log('给我看看',res)
console.log('给我看看',res)
console.log('给我看看',res)
console.log('给我看看',res)
const canvas = res[0].node
const width = res[0].width
const height = res[0].height
// canvas
canvas.width = width * uni.getSystemInfoSync().pixelRatio
canvas.height = height * uni.getSystemInfoSync().pixelRatio
resolve({ canvas, width, height })
}) })
</script> })
},
drawSeats() {
if (!this.ctx) return
<style scoped> //
button { this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
margin-top: 10px;
//
this.ctx.save()
//
this.ctx.translate(this.offsetX, this.offsetY)
this.ctx.scale(this.scale, this.scale)
//
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j]
const x = j * (this.seatSize + this.gap)
const y = i * (this.seatSize + this.gap)
//
seat.x = x
seat.y = y
//
switch (seat.status) {
case 'available':
this.ctx.fillStyle = '#4CAF50' // 绿
break
case 'selected':
this.ctx.fillStyle = '#2196F3' //
break
case 'sold':
this.ctx.fillStyle = '#9E9E9E' //
break
} }
//
this.ctx.beginPath()
this.ctx.roundRect(x, y, this.seatSize, this.seatSize, 4)
this.ctx.fill()
//
this.ctx.fillStyle = '#FFFFFF'
this.ctx.font = '12px Arial'
this.ctx.textAlign = 'center'
this.ctx.textBaseline = 'middle'
this.ctx.fillText(
`${String.fromCharCode(65 + i)}${j + 1}`,
x + this.seatSize / 2,
y + this.seatSize / 2
)
}
}
//
this.ctx.restore()
},
handleTouchStart(e) {
this.touchStartX = e.touches[0].x
this.touchStartY = e.touches[0].y
},
handleTouchMove(e) {
const touchX = e.touches[0].x
const touchY = e.touches[0].y
//
const dx = touchX - this.touchStartX
const dy = touchY - this.touchStartY
//
this.offsetX += dx
this.offsetY += dy
//
const minX = this.canvasWidth - (this.cols * (this.seatSize + this.gap) - this.gap) * this.scale
const minY = this.canvasHeight - (this.rows * (this.seatSize + this.gap) - this.gap) * this.scale
this.offsetX = Math.max(minX, Math.min(0, this.offsetX))
this.offsetY = Math.max(minY, Math.min(0, this.offsetY))
//
this.touchStartX = touchX
this.touchStartY = touchY
this.drawSeats()
},
handleTouchEnd(e) {
//
const touchX = e.changedTouches[0].x
const touchY = e.changedTouches[0].y
//
const canvasX = (touchX - this.offsetX) / this.scale
const canvasY = (touchY - this.offsetY) / this.scale
//
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j]
if (
canvasX >= seat.x &&
canvasX <= seat.x + this.seatSize &&
canvasY >= seat.y &&
canvasY <= seat.y + this.seatSize
) {
this.toggleSeatSelection(seat)
return
}
}
}
},
toggleSeatSelection(seat) {
if (seat.status === 'sold') return
if (seat.status === 'available') {
seat.status = 'selected'
this.selectedSeats.push(`${String.fromCharCode(65 + seat.row)}${seat.col + 1}`)
} else {
seat.status = 'available'
this.selectedSeats = this.selectedSeats.filter(
s => s !== `${String.fromCharCode(65 + seat.row)}${seat.col + 1}`
)
}
this.drawSeats()
}
}
}
</script>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
height: 100vh;
background-color: #f5f5f5;
}
.screen {
width: 80%;
height: 40rpx;
background: linear-gradient(to bottom, #ccc, #fff);
margin-bottom: 40rpx;
text-align: center;
font-size: 28rpx;
color: #333;
border-radius: 8rpx;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
.seat-canvas {
width: 100%;
height: 500rpx;
background-color: #fff;
margin-bottom: 20rpx;
border-radius: 8rpx;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
.legend {
display: flex;
justify-content: center;
margin-bottom: 20rpx;
}
.legend-item {
display: flex;
align-items: center;
margin: 0 20rpx;
}
.seat-icon {
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
border-radius: 4rpx;
}
.available {
background-color: #4CAF50;
}
.selected {
background-color: #2196F3;
}
.sold {
background-color: #9E9E9E;
}
.selected-seats {
padding: 20rpx;
background-color: #fff;
border-radius: 8rpx;
width: 90%;
text-align: center;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
</style> </style>

View File

@ -1,17 +1,294 @@
<template> <template>
<view> <view class="container">
<indexstep2></indexstep2> <view class="screen">银幕</view>
<canvas
canvas-id="seatCanvas"
type="2d"
class="seat-canvas"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<view class="legend">
<view class="legend-item">
<view class="seat-icon available"></view>
<text>可选</text>
</view>
<view class="legend-item">
<view class="seat-icon selected"></view>
<text>已选</text>
</view>
<view class="legend-item">
<view class="seat-icon sold"></view>
<text>已售</text>
</view>
</view>
<view class="selected-seats">
<text>已选座位{{ selectedSeats.join(', ') || '无' }}</text>
</view>
</view> </view>
</template> </template>
<script>
export default {
data() {
return {
ctx: null,
canvasWidth: 300,
canvasHeight: 500,
seatSize: 30,
gap: 10,
rows: 8,
cols: 10,
seats: [],
selectedSeats: [],
touchStartX: 0,
touchStartY: 0,
offsetX: 0,
offsetY: 0
}
},
onReady() {
this.initSeats();
this.initAndroidCanvas();
},
methods: {
initSeats() {
//
for (let i = 0; i < this.rows; i++) {
this.seats[i] = [];
for (let j = 0; j < this.cols; j++) {
//
const isSold = Math.random() < 0.2;
this.seats[i][j] = {
row: i,
col: j,
status: isSold ? 'sold' : 'available',
x: 0,
y: 0
};
}
}
},
// initAndroidCanvas
initAndroidCanvas() {
return new Promise((resolve) => {
// 使 uni.createCanvasContext
this.ctx = uni.createCanvasContext('seatCanvas', this);
console.log(this.ctx,'ctx123123123')
// Canvas
const query = uni.createSelectorQuery().in(this);
query.select('.seat-canvas')
.boundingClientRect(rect => {
if (!rect) {
console.error('获取Canvas尺寸失败');
return resolve(false);
}
<script setup> this.canvasWidth = rect.width;
import indexstep2 from "./index.vue" this.canvasHeight = rect.height;
import {
ref
} from 'vue'
// 使
const totalWidth = this.cols * (this.seatSize + this.gap) - this.gap;
const totalHeight = this.rows * (this.seatSize + this.gap) - this.gap;
this.offsetX = (this.canvasWidth - totalWidth) / 2;
this.offsetY = (this.canvasHeight - totalHeight) / 2;
this.drawSeats();
resolve(true);
})
.exec();
});
},
drawSeats() {
if (!this.ctx) return;
//
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
//
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j];
const x = j * (this.seatSize + this.gap) + this.offsetX;
const y = i * (this.seatSize + this.gap) + this.offsetY;
//
seat.x = x;
seat.y = y;
//
switch (seat.status) {
case 'available':
this.ctx.setFillStyle('#4CAF50');
break;
case 'selected':
this.ctx.setFillStyle('#2196F3');
break;
case 'sold':
this.ctx.setFillStyle('#9E9E9E');
break;
}
// API
this.ctx.fillRect(x, y, this.seatSize, this.seatSize);
//
this.ctx.setFillStyle('#FFFFFF');
this.ctx.setFontSize(12);
this.ctx.setTextAlign('center');
this.ctx.fillText(
`${String.fromCharCode(65 + i)}${j + 1}`,
x + this.seatSize / 2,
y + this.seatSize / 2 + 4 //
);
}
}
// draw
this.ctx.draw();
},
//
handleTouchStart(e) {
this.touchStartX = e.touches[0].x;
this.touchStartY = e.touches[0].y;
},
handleTouchMove(e) {
const touchX = e.touches[0].x;
const touchY = e.touches[0].y;
//
const dx = touchX - this.touchStartX;
const dy = touchY - this.touchStartY;
//
this.offsetX += dx;
this.offsetY += dy;
//
const minX = this.canvasWidth - (this.cols * (this.seatSize + this.gap) - this.gap);
const minY = this.canvasHeight - (this.rows * (this.seatSize + this.gap) - this.gap);
this.offsetX = Math.max(minX, Math.min(0, this.offsetX));
this.offsetY = Math.max(minY, Math.min(0, this.offsetY));
//
this.touchStartX = touchX;
this.touchStartY = touchY;
this.drawSeats();
},
handleTouchEnd(e) {
//
const touchX = e.changedTouches[0].x;
const touchY = e.changedTouches[0].y;
//
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j];
if (
touchX >= seat.x &&
touchX <= seat.x + this.seatSize &&
touchY >= seat.y &&
touchY <= seat.y + this.seatSize
) {
this.toggleSeatSelection(seat);
return;
}
}
}
},
toggleSeatSelection(seat) {
if (seat.status === 'sold') return;
if (seat.status === 'available') {
seat.status = 'selected';
this.selectedSeats.push(`${String.fromCharCode(65 + seat.row)}${seat.col + 1}`);
} else {
seat.status = 'available';
this.selectedSeats = this.selectedSeats.filter(
s => s !== `${String.fromCharCode(65 + seat.row)}${seat.col + 1}`
);
}
this.drawSeats();
}
}
}
</script> </script>
<style scoped> <style>
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
height: 100vh;
background-color: #f5f5f5;
}
.screen {
width: 80%;
height: 40rpx;
background: linear-gradient(to bottom, #ccc, #fff);
margin-bottom: 40rpx;
text-align: center;
font-size: 28rpx;
color: #333;
border-radius: 8rpx;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
.seat-canvas {
width: 100%;
height: 500px; /* 安卓使用固定像素单位更可靠 */
background-color: #fff;
margin-bottom: 20rpx;
border-radius: 8rpx;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
.legend {
display: flex;
justify-content: center;
margin-bottom: 20rpx;
}
.legend-item {
display: flex;
align-items: center;
margin: 0 20rpx;
}
.seat-icon {
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
border-radius: 4rpx;
}
.available {
background-color: #4CAF50;
}
.selected {
background-color: #2196F3;
}
.sold {
background-color: #9E9E9E;
}
.selected-seats {
padding: 20rpx;
background-color: #fff;
border-radius: 8rpx;
width: 90%;
text-align: center;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
</style> </style>

11
unpackage/dist/dev/.nvue/app.css.js vendored Normal file
View File

@ -0,0 +1,11 @@
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var require_app_css = __commonJS({
"app.css.js"(exports) {
const _style_0 = {};
exports.styles = [_style_0];
}
});
export default require_app_css();

2
unpackage/dist/dev/.nvue/app.js vendored Normal file
View File

@ -0,0 +1,2 @@
Promise.resolve("./app.css.js").then(() => {
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>View</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="app.css" />
<script>var __uniConfig = {"globalStyle":{},"darkmode":false}</script>
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
</head>
<body>
<div id="app"></div>
<script src="uni-app-view.umd.js"></script>
</body>
</html>

View File

@ -0,0 +1,11 @@
;(function(){
let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[];
const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"canvas","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"4.57","entryPagePath":"pages/index/step2","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}};
const __uniRoutes = [{"path":"pages/index/step2","meta":{"isQuit":true,"isEntry":true,"navigationBar":{"titleText":"fu","type":"default"},"isNVue":false}},{"path":"pages/index/index","meta":{"navigationBar":{"titleText":"uni-app","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute));
__uniConfig.styles=[];//styles
__uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
__uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
service.register("uni-app-config",{create(a,b,c){if(!__uniConfig.viewport){var d=b.weex.config.env.scale,e=b.weex.config.env.deviceWidth,f=Math.ceil(e/d);Object.assign(__uniConfig,{viewport:f,defaultFontSize:16})}return{instance:{__uniConfig:__uniConfig,__uniRoutes:__uniRoutes,global:u,window:u,document:u,frames:u,self:u,location:u,navigator:u,localStorage:u,history:u,Caches:u,screen:u,alert:u,confirm:u,prompt:u,fetch:u,XMLHttpRequest:u,WebSocket:u,webkit:u,print:u}}}});
})();

View File

@ -0,0 +1 @@
(function(){})();

View File

@ -0,0 +1,468 @@
if (typeof Promise !== "undefined" && !Promise.prototype.finally) {
Promise.prototype.finally = function(callback) {
const promise = this.constructor;
return this.then(
(value) => promise.resolve(callback()).then(() => value),
(reason) => promise.resolve(callback()).then(() => {
throw reason;
})
);
};
}
;
if (typeof uni !== "undefined" && uni && uni.requireGlobal) {
const global = uni.requireGlobal();
ArrayBuffer = global.ArrayBuffer;
Int8Array = global.Int8Array;
Uint8Array = global.Uint8Array;
Uint8ClampedArray = global.Uint8ClampedArray;
Int16Array = global.Int16Array;
Uint16Array = global.Uint16Array;
Int32Array = global.Int32Array;
Uint32Array = global.Uint32Array;
Float32Array = global.Float32Array;
Float64Array = global.Float64Array;
BigInt64Array = global.BigInt64Array;
BigUint64Array = global.BigUint64Array;
}
;
if (uni.restoreGlobal) {
uni.restoreGlobal(Vue, weex, plus, setTimeout, clearTimeout, setInterval, clearInterval);
}
(function(vue) {
"use strict";
function formatAppLog(type, filename, ...args) {
if (uni.__log__) {
uni.__log__(type, filename, ...args);
} else {
console[type].apply(console, [...args, filename]);
}
}
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
const _sfc_main$2 = {
data() {
return {
ctx: null,
canvasWidth: 300,
canvasHeight: 500,
seatSize: 30,
gap: 10,
rows: 8,
cols: 10,
seats: [],
selectedSeats: [],
touchStartX: 0,
touchStartY: 0,
offsetX: 0,
offsetY: 0
};
},
onReady() {
this.initSeats();
this.initAndroidCanvas();
},
methods: {
initSeats() {
for (let i = 0; i < this.rows; i++) {
this.seats[i] = [];
for (let j = 0; j < this.cols; j++) {
const isSold = Math.random() < 0.2;
this.seats[i][j] = {
row: i,
col: j,
status: isSold ? "sold" : "available",
x: 0,
y: 0
};
}
}
},
// 替换原来的 initAndroidCanvas 方法
initAndroidCanvas() {
return new Promise((resolve) => {
this.ctx = uni.createCanvasContext("seatCanvas", this);
formatAppLog("log", "at pages/index/step2.vue:77", this.ctx, "ctx123123123");
const query = uni.createSelectorQuery().in(this);
query.select(".seat-canvas").boundingClientRect((rect) => {
if (!rect) {
formatAppLog("error", "at pages/index/step2.vue:83", "获取Canvas尺寸失败");
return resolve(false);
}
this.canvasWidth = rect.width;
this.canvasHeight = rect.height;
const totalWidth = this.cols * (this.seatSize + this.gap) - this.gap;
const totalHeight = this.rows * (this.seatSize + this.gap) - this.gap;
this.offsetX = (this.canvasWidth - totalWidth) / 2;
this.offsetY = (this.canvasHeight - totalHeight) / 2;
this.drawSeats();
resolve(true);
}).exec();
});
},
drawSeats() {
if (!this.ctx)
return;
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j];
const x = j * (this.seatSize + this.gap) + this.offsetX;
const y = i * (this.seatSize + this.gap) + this.offsetY;
seat.x = x;
seat.y = y;
switch (seat.status) {
case "available":
this.ctx.setFillStyle("#4CAF50");
break;
case "selected":
this.ctx.setFillStyle("#2196F3");
break;
case "sold":
this.ctx.setFillStyle("#9E9E9E");
break;
}
this.ctx.fillRect(x, y, this.seatSize, this.seatSize);
this.ctx.setFillStyle("#FFFFFF");
this.ctx.setFontSize(12);
this.ctx.setTextAlign("center");
this.ctx.fillText(
`${String.fromCharCode(65 + i)}${j + 1}`,
x + this.seatSize / 2,
y + this.seatSize / 2 + 4
// 安卓平台文字垂直对齐需要微调
);
}
}
this.ctx.draw();
},
// 触摸事件处理(安卓专用优化)
handleTouchStart(e) {
this.touchStartX = e.touches[0].x;
this.touchStartY = e.touches[0].y;
},
handleTouchMove(e) {
const touchX = e.touches[0].x;
const touchY = e.touches[0].y;
const dx = touchX - this.touchStartX;
const dy = touchY - this.touchStartY;
this.offsetX += dx;
this.offsetY += dy;
const minX = this.canvasWidth - (this.cols * (this.seatSize + this.gap) - this.gap);
const minY = this.canvasHeight - (this.rows * (this.seatSize + this.gap) - this.gap);
this.offsetX = Math.max(minX, Math.min(0, this.offsetX));
this.offsetY = Math.max(minY, Math.min(0, this.offsetY));
this.touchStartX = touchX;
this.touchStartY = touchY;
this.drawSeats();
},
handleTouchEnd(e) {
const touchX = e.changedTouches[0].x;
const touchY = e.changedTouches[0].y;
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j];
if (touchX >= seat.x && touchX <= seat.x + this.seatSize && touchY >= seat.y && touchY <= seat.y + this.seatSize) {
this.toggleSeatSelection(seat);
return;
}
}
}
},
toggleSeatSelection(seat) {
if (seat.status === "sold")
return;
if (seat.status === "available") {
seat.status = "selected";
this.selectedSeats.push(`${String.fromCharCode(65 + seat.row)}${seat.col + 1}`);
} else {
seat.status = "available";
this.selectedSeats = this.selectedSeats.filter(
(s) => s !== `${String.fromCharCode(65 + seat.row)}${seat.col + 1}`
);
}
this.drawSeats();
}
}
};
function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [
vue.createElementVNode("view", { class: "screen" }, "银幕"),
vue.createElementVNode(
"canvas",
{
"canvas-id": "seatCanvas",
type: "2d",
class: "seat-canvas",
onTouchstart: _cache[0] || (_cache[0] = (...args) => $options.handleTouchStart && $options.handleTouchStart(...args)),
onTouchmove: _cache[1] || (_cache[1] = (...args) => $options.handleTouchMove && $options.handleTouchMove(...args)),
onTouchend: _cache[2] || (_cache[2] = (...args) => $options.handleTouchEnd && $options.handleTouchEnd(...args))
},
null,
32
/* NEED_HYDRATION */
),
vue.createElementVNode("view", { class: "legend" }, [
vue.createElementVNode("view", { class: "legend-item" }, [
vue.createElementVNode("view", { class: "seat-icon available" }),
vue.createElementVNode("text", null, "可选")
]),
vue.createElementVNode("view", { class: "legend-item" }, [
vue.createElementVNode("view", { class: "seat-icon selected" }),
vue.createElementVNode("text", null, "已选")
]),
vue.createElementVNode("view", { class: "legend-item" }, [
vue.createElementVNode("view", { class: "seat-icon sold" }),
vue.createElementVNode("text", null, "已售")
])
]),
vue.createElementVNode("view", { class: "selected-seats" }, [
vue.createElementVNode(
"text",
null,
"已选座位:" + vue.toDisplayString($data.selectedSeats.join(", ") || "无"),
1
/* TEXT */
)
])
]);
}
const PagesIndexStep2 = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$1], ["__file", "/Users/sunmeng/Desktop/wx/canvas/pages/index/step2.vue"]]);
const _sfc_main$1 = {
data() {
return {
ctx: null,
canvasWidth: 0,
canvasHeight: 0,
seatSize: 30,
gap: 10,
rows: 8,
cols: 10,
seats: [],
selectedSeats: [],
touchStartX: 0,
touchStartY: 0,
offsetX: 0,
offsetY: 0,
scale: 1
};
},
onReady() {
this.initSeats();
this.initCanvas();
},
methods: {
initSeats() {
for (let i = 0; i < this.rows; i++) {
this.seats[i] = [];
for (let j = 0; j < this.cols; j++) {
const isSold = Math.random() < 0.2;
this.seats[i][j] = {
row: i,
col: j,
status: isSold ? "sold" : "available",
// available, selected, sold
x: 0,
y: 0
};
}
}
},
async initCanvas() {
const { canvas, width, height } = await this.getCanvasNode("seatCanvas");
this.canvasWidth = width;
this.canvasHeight = height;
this.ctx = canvas.getContext("2d");
const totalWidth = this.cols * (this.seatSize + this.gap) - this.gap;
const totalHeight = this.rows * (this.seatSize + this.gap) - this.gap;
this.offsetX = (this.canvasWidth - totalWidth) / 2;
this.offsetY = (this.canvasHeight - totalHeight) / 2;
this.drawSeats();
},
getCanvasNode(id) {
return new Promise((resolve) => {
const query = uni.createSelectorQuery().in(this);
query.select(`#${id}`).fields({ node: true, size: true }).exec((res) => {
formatAppLog("log", "at pages/index/index.vue:94", "给我看看", res);
formatAppLog("log", "at pages/index/index.vue:95", "给我看看", res);
formatAppLog("log", "at pages/index/index.vue:96", "给我看看", res);
formatAppLog("log", "at pages/index/index.vue:97", "给我看看", res);
formatAppLog("log", "at pages/index/index.vue:98", "给我看看", res);
formatAppLog("log", "at pages/index/index.vue:99", "给我看看", res);
const canvas = res[0].node;
const width = res[0].width;
const height = res[0].height;
canvas.width = width * uni.getSystemInfoSync().pixelRatio;
canvas.height = height * uni.getSystemInfoSync().pixelRatio;
resolve({ canvas, width, height });
});
});
},
drawSeats() {
if (!this.ctx)
return;
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.save();
this.ctx.translate(this.offsetX, this.offsetY);
this.ctx.scale(this.scale, this.scale);
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j];
const x = j * (this.seatSize + this.gap);
const y = i * (this.seatSize + this.gap);
seat.x = x;
seat.y = y;
switch (seat.status) {
case "available":
this.ctx.fillStyle = "#4CAF50";
break;
case "selected":
this.ctx.fillStyle = "#2196F3";
break;
case "sold":
this.ctx.fillStyle = "#9E9E9E";
break;
}
this.ctx.beginPath();
this.ctx.roundRect(x, y, this.seatSize, this.seatSize, 4);
this.ctx.fill();
this.ctx.fillStyle = "#FFFFFF";
this.ctx.font = "12px Arial";
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.fillText(
`${String.fromCharCode(65 + i)}${j + 1}`,
x + this.seatSize / 2,
y + this.seatSize / 2
);
}
}
this.ctx.restore();
},
handleTouchStart(e) {
this.touchStartX = e.touches[0].x;
this.touchStartY = e.touches[0].y;
},
handleTouchMove(e) {
const touchX = e.touches[0].x;
const touchY = e.touches[0].y;
const dx = touchX - this.touchStartX;
const dy = touchY - this.touchStartY;
this.offsetX += dx;
this.offsetY += dy;
const minX = this.canvasWidth - (this.cols * (this.seatSize + this.gap) - this.gap) * this.scale;
const minY = this.canvasHeight - (this.rows * (this.seatSize + this.gap) - this.gap) * this.scale;
this.offsetX = Math.max(minX, Math.min(0, this.offsetX));
this.offsetY = Math.max(minY, Math.min(0, this.offsetY));
this.touchStartX = touchX;
this.touchStartY = touchY;
this.drawSeats();
},
handleTouchEnd(e) {
const touchX = e.changedTouches[0].x;
const touchY = e.changedTouches[0].y;
const canvasX = (touchX - this.offsetX) / this.scale;
const canvasY = (touchY - this.offsetY) / this.scale;
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
const seat = this.seats[i][j];
if (canvasX >= seat.x && canvasX <= seat.x + this.seatSize && canvasY >= seat.y && canvasY <= seat.y + this.seatSize) {
this.toggleSeatSelection(seat);
return;
}
}
}
},
toggleSeatSelection(seat) {
if (seat.status === "sold")
return;
if (seat.status === "available") {
seat.status = "selected";
this.selectedSeats.push(`${String.fromCharCode(65 + seat.row)}${seat.col + 1}`);
} else {
seat.status = "available";
this.selectedSeats = this.selectedSeats.filter(
(s) => s !== `${String.fromCharCode(65 + seat.row)}${seat.col + 1}`
);
}
this.drawSeats();
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return vue.openBlock(), vue.createElementBlock("view", { class: "container" }, [
vue.createElementVNode("view", { class: "screen" }, "银幕"),
vue.createElementVNode(
"canvas",
{
"canvas-id": "seatCanvas",
type: "2d",
class: "seat-canvas",
onTouchstart: _cache[0] || (_cache[0] = (...args) => $options.handleTouchStart && $options.handleTouchStart(...args)),
onTouchmove: _cache[1] || (_cache[1] = (...args) => $options.handleTouchMove && $options.handleTouchMove(...args)),
onTouchend: _cache[2] || (_cache[2] = (...args) => $options.handleTouchEnd && $options.handleTouchEnd(...args))
},
null,
32
/* NEED_HYDRATION */
),
vue.createElementVNode("view", { class: "legend" }, [
vue.createElementVNode("view", { class: "legend-item" }, [
vue.createElementVNode("view", { class: "seat-icon available" }),
vue.createElementVNode("text", null, "可选")
]),
vue.createElementVNode("view", { class: "legend-item" }, [
vue.createElementVNode("view", { class: "seat-icon selected" }),
vue.createElementVNode("text", null, "已选")
]),
vue.createElementVNode("view", { class: "legend-item" }, [
vue.createElementVNode("view", { class: "seat-icon sold" }),
vue.createElementVNode("text", null, "已售")
])
]),
vue.createElementVNode("view", { class: "selected-seats" }, [
vue.createElementVNode(
"text",
null,
"已选座位:" + vue.toDisplayString($data.selectedSeats.join(", ") || "无"),
1
/* TEXT */
)
])
]);
}
const PagesIndexIndex = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render], ["__file", "/Users/sunmeng/Desktop/wx/canvas/pages/index/index.vue"]]);
__definePage("pages/index/step2", PagesIndexStep2);
__definePage("pages/index/index", PagesIndexIndex);
const _sfc_main = {
onLaunch: function() {
formatAppLog("log", "at App.vue:4", "App Launch");
},
onShow: function() {
formatAppLog("log", "at App.vue:7", "App Show");
},
onHide: function() {
formatAppLog("log", "at App.vue:10", "App Hide");
}
};
const App = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "/Users/sunmeng/Desktop/wx/canvas/App.vue"]]);
function createApp() {
const app = vue.createVueApp(App);
return {
app
};
}
const { app: __app__, Vuex: __Vuex__, Pinia: __Pinia__ } = createApp();
uni.Vuex = __Vuex__;
uni.Pinia = __Pinia__;
__app__.provide("__globalStyles", __uniConfig.styles);
__app__._component.mpType = "app";
__app__._component.render = () => {
};
__app__.mount("#app");
})(Vue);

4
unpackage/dist/dev/app-plus/app.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,113 @@
{
"@platforms": [
"android",
"iPhone",
"iPad"
],
"id": "__UNI__EBE2302",
"name": "canvas",
"version": {
"name": "1.0.0",
"code": "100"
},
"description": "",
"developer": {
"name": "",
"email": "",
"url": ""
},
"permissions": {
"UniNView": {
"description": "UniNView原生渲染"
}
},
"plus": {
"useragent": {
"value": "uni-app",
"concatenate": true
},
"splashscreen": {
"target": "id:1",
"autoclose": true,
"waiting": true,
"delay": 0
},
"popGesture": "close",
"launchwebview": {
"render": "always",
"id": "1",
"kernel": "WKWebview"
},
"canvas2d": true,
"optimization": {
"subPackages": true
},
"usingComponents": {
"canvas": true
},
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"distribute": {
"google": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"apple": {},
"plugins": {
"audio": {
"mp3": {
"description": "Android平台录音支持MP3格式文件"
}
}
}
},
"statusbar": {
"immersed": "supportedDevice",
"style": "dark",
"background": "#F8F8F8"
},
"uniStatistics": {
"enable": false
},
"allowsInlineMediaPlayback": true,
"uni-app": {
"control": "uni-v3",
"vueVersion": "3",
"compilerVersion": "4.57",
"nvueCompiler": "uni-app",
"renderer": "auto",
"nvue": {
"flex-direction": "column"
},
"nvueLaunchMode": "normal",
"webView": {
"minUserAgentVersion": "49.0"
}
}
},
"app-harmony": {
"useragent": {
"value": "uni-app",
"concatenate": true
},
"uniStatistics": {
"enable": false
}
},
"launch_path": "__uniappview.html"
}

View File

@ -0,0 +1,61 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.625rem;
height: 100vh;
background-color: #f5f5f5;
}
.screen {
width: 80%;
height: 1.25rem;
background: linear-gradient(to bottom, #ccc, #fff);
margin-bottom: 1.25rem;
text-align: center;
font-size: 0.875rem;
color: #333;
border-radius: 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
}
.seat-canvas {
width: 100%;
height: 15.625rem;
background-color: #fff;
margin-bottom: 0.625rem;
border-radius: 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
}
.legend {
display: flex;
justify-content: center;
margin-bottom: 0.625rem;
}
.legend-item {
display: flex;
align-items: center;
margin: 0 0.625rem;
}
.seat-icon {
width: 0.9375rem;
height: 0.9375rem;
margin-right: 0.3125rem;
border-radius: 0.125rem;
}
.available {
background-color: #4CAF50;
}
.selected {
background-color: #2196F3;
}
.sold {
background-color: #9E9E9E;
}
.selected-seats {
padding: 0.625rem;
background-color: #fff;
border-radius: 0.25rem;
width: 90%;
text-align: center;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
}

View File

@ -0,0 +1,61 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.625rem;
height: 100vh;
background-color: #f5f5f5;
}
.screen {
width: 80%;
height: 1.25rem;
background: linear-gradient(to bottom, #ccc, #fff);
margin-bottom: 1.25rem;
text-align: center;
font-size: 0.875rem;
color: #333;
border-radius: 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
}
.seat-canvas {
width: 100%;
height: 500px; /* 安卓使用固定像素单位更可靠 */
background-color: #fff;
margin-bottom: 0.625rem;
border-radius: 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
}
.legend {
display: flex;
justify-content: center;
margin-bottom: 0.625rem;
}
.legend-item {
display: flex;
align-items: center;
margin: 0 0.625rem;
}
.seat-icon {
width: 0.9375rem;
height: 0.9375rem;
margin-right: 0.3125rem;
border-radius: 0.125rem;
}
.available {
background-color: #4CAF50;
}
.selected {
background-color: #2196F3;
}
.sold {
background-color: #9E9E9E;
}
.selected-seats {
padding: 0.625rem;
background-color: #fff;
border-radius: 0.25rem;
width: 90%;
text-align: center;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long