uniapp 实现微信小程序电影选座功能
拖动代码
/*** 获取点击或触摸事件对应的座位位置* 通过事件对象获取座位的行列信息* @param {Event|TouchEvent} event - 点击或触摸事件对象* @returns {Object} 返回座位位置对象,包含行(row)和列(col)信息,若未找到有效位置则返回 {row: -1, col: -1}*/getSeatPosition(event) {// 统一处理触摸事件和点击事件// 触摸事件时从 touches 数组获取第一个触摸点// 点击事件时直接使用事件对象const touch = event.touches ? event.touches[0] : event;// 获取触摸/点击的坐标位置// clientX/Y 用于标准事件,x/y 用于某些特殊环境const x = touch.clientX || touch.x;const y = touch.clientY || touch.y;// 创建查询对象,用于获取 DOM 信息(当前未使用)const query = uni.createSelectorQuery();// 从事件目标的数据集中获取座位信息// 使用 HTML5 data-* 属性存储的行列信息if (event.target && event.target.dataset) {const dataset = event.target.dataset;// 检查数据集中是否包含有效的行列信息if (dataset.row !== undefined && dataset.col !== undefined) {// 返回解析后的座位位置// parseInt 确保返回数值类型return {row: parseInt(dataset.row),col: parseInt(dataset.col)};}}// 如果无法获取有效的座位信息// 返回表示无效位置的对象return { row: -1, col: -1 };},/*** 处理触摸开始事件* 用于初始化拖拽和缩放的起始状态* @param {TouchEvent} event - 触摸事件对象,包含触摸点信息*/onTouchStart(event) {// 记录触摸开始的时间戳,用于后续判断是点击还是拖动this.touchStartTime = Date.now();// 重置移动标志,初始状态下未发生移动this.isMoved = false;// 单指触摸 - 处理拖动初始化if (event.touches.length === 1) {const touch = event.touches[0];// 记录当前触摸点作为上一次触摸位置,用于计算移动距离this.lastTouch = { x: touch.clientX, y: touch.clientY };// 记录触摸起始位置,用于计算总移动距离this.touchStartPos = { x: touch.clientX, y: touch.clientY };}// 双指触摸 - 处理缩放初始化else if (event.touches.length === 2) {// 计算两个触摸点之间的初始距离,用于后续计算缩放比例this.startDistance = this.getDistance(event.touches[0], event.touches[1]);}},// 处理触摸移动事件onTouchMove(event) {// 标记已经发生移动,用于区分点击和拖动this.isMoved = true;// 单指触摸 - 处理拖动if (event.touches.length === 1) {const touch = event.touches[0];// 计算相对于上一次触摸位置的偏移量const deltaX = touch.clientX - this.lastTouch.x;const deltaY = touch.clientY - this.lastTouch.y;// 根据当前缩放比例调整位移距离// 缩放比例越大,移动距离越小,保证移动体验一致this.position.x += deltaX / this.scale;this.position.y += deltaY / this.scale;// 更新最后一次触摸位置this.lastTouch = { x: touch.clientX, y: touch.clientY };}// 双指触摸 - 处理缩放else if (event.touches.length === 2) {// 计算当前两个触摸点之间的距离const currentDistance = this.getDistance(event.touches[0], event.touches[1]);// 根据距离变化计算新的缩放比例let newScale = this.scale * (currentDistance / this.startDistance);// 限制缩放范围在 minScale 和 maxScale 之间newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));this.scale = newScale;// 更新起始距离,用于下一次计算this.startDistance = currentDistance;}// 检查并限制移动边界,防止内容移出可视区域this.checkBoundaries();},// 处理手势结束onTouchEnd() {// 可以在这里处理手势结束后的逻辑},/*** 计算两个触摸点之间的距离* @param {Object} touch1 - 第一个触摸点,包含 clientX 和 clientY 坐标* @param {Object} touch2 - 第二个触摸点,包含 clientX 和 clientY 坐标* @returns {number} 两点之间的欧几里得距离*/getDistance(touch1, touch2) {// 计算 X 轴方向的距离差const dx = touch1.clientX - touch2.clientX;// 计算 Y 轴方向的距离差const dy = touch1.clientY - touch2.clientY;// 使用勾股定理计算两点之间的直线距离// distance = √(dx² + dy²)return Math.sqrt(dx * dx + dy * dy);},/*** 检查并限制座位区域的移动边界* 防止用户将座位区域拖动到视图之外*/checkBoundaries() {// 定义最大可移动距离(像素)const maxX = 200; // X轴最大移动距离,可根据实际座位区域大小调整const maxY = 200; // Y轴最大移动距离,可根据实际座位区域大小调整// 限制X轴移动范围:[-maxX, maxX]// Math.min 确保不会超过右边界// Math.max 确保不会超过左边界this.position.x = Math.max(-maxX, Math.min(maxX, this.position.x));// 限制Y轴移动范围:[-maxY, maxY]// Math.min 确保不会超过下边界// Math.max 确保不会超过上边界this.position.y = Math.max(-maxY, Math.min(maxY, this.position.y));},
计算总价代码
/*** 获取指定座位在所有已选座位中的序号* @param {number} row - 要查询的座位行号* @param {number} col - 要查询的座位列号* @returns {number} 返回该座位是第几个被选中的座位(从1开始计数)* * 使用场景:* 1. 用于确定座位的选中顺序* 2. 可用于显示座位的选中序号* 3. 帮助用户了解座位的选择顺序*/getSelectedIndex(row, col) {// 初始化计数器,从1开始计数let count = 1;// 遍历所有座位for (let i = 0; i < this.seatMap.length; i++) {for (let j = 0; j < this.seatMap[i].length; j++) {// 检查当前遍历到的座位是否被选中if (this.seatMap[i][j].selected) {// 如果找到目标座位,返回当前计数if (i === row && j === col) return count;// 如果不是目标座位,计数器加1count++;}}}return count;},// 获取已选座位列表getSelectedSeats() {const selectedSeats = [];this.seatMap.forEach((row, rowIndex) => {row.forEach((seat, colIndex) => {if (seat.selected) {selectedSeats.push({row: rowIndex,col: colIndex,type: seat.type});}});});return selectedSeats;},/*** 计算所有已选座位的总价* @returns {string} 返回格式化后的总价字符串,保留两位小数* * 使用场景:* 1. 显示确认选座按钮上的总价* 2. 提交订单时计算支付金额* 3. 更新用户选座时实时显示价格*/getTotalPrice() {// 定义不同类型座位的价格映射const prices = {pink: 40, // 粉色座位(VIP座)价格orange: 38, // 橙色座位(情侣座)价格blue: 35 // 蓝色座位(普通座)价格};// 使用 reduce 方法计算总价// 1. 获取所有已选座位列表// 2. 根据每个座位的类型获取对应价格// 3. 累加所有座位的价格return this.getSelectedSeats().reduce((total, seat) => {// total: 累计总价// seat: 当前座位信息,包含 type 属性return total + prices[seat.type];}, 0).toFixed(2); // 初始值为0,结果保留两位小数},/*** 获取指定类型座位的单价* @param {string} type - 座位类型('pink'|'orange'|'blue')* @returns {string} 返回格式化后的价格字符串,保留两位小数* * 使用场景:* 1. 显示单个座位的价格* 2. 在已选座位列表中显示每个座位的单价*/getSeatPrice(type) {// 定义不同类型座位的价格映射const prices = {pink: 40, // 粉色座位(VIP座)价格orange: 38, // 橙色座位(情侣座)价格blue: 35 // 蓝色座位(普通座)价格};// 返回格式化后的价格,保留两位小数return prices[type].toFixed(2);},/*** 处理确认选座操作* 验证选座状态并进行后续处理* * 使用场景:* 1. 用户点击确认选座按钮时触发* 2. 验证是否已选择座位* 3. 进行下一步订单处理*/confirmSeats() {// 检查是否有选中的座位if (this.selectedSeatsCount === 0) {// 如果没有选择座位,显示提示信息uni.showToast({title: '请先选择座位',icon: 'none'});return;}// TODO: 处理确认选座逻辑// 可以添加以下操作:// 1. 获取选中的座位信息// 2. 调用后端API锁定座位// 3. 跳转到订单确认页面// 4. 处理支付流程等console.log('确认选座', this.getSelectedSeats());}
完整代码
<template><view class="chooseSeat"><!-- 价格说明 --><view class="price-info"><view class="price-item"><view class="price-box pink"></view><text>¥40.00</text></view><view class="price-item"><view class="price-box orange"></view><text>¥38.00</text></view><view class="price-item"><view class="price-box blue"></view><text>¥35.00</text></view></view><!-- 银幕 --><view class="screen"><image class="screen-image" src="https://s.xitupt.com/tsimgs/949558333604714792/20250318/h5_mng_1742302489261"mode="aspectFit"></image><!-- <view class="screen-text"><text>IMAX</text><text>4DX</text></view> --></view><!-- 座位区域 --><view class="seat-container"><!-- 修改行号部分,让它和座位区域一起缩放移动 --><view class="seat-area-wrapper" :style="{transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)`,transformOrigin: '0 0'}" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"><!-- 行号 --><view class="row-numbers"><view v-for="i in 6" :key="i" class="row-number">{{ i }}</view></view><!-- 座位图 --><view class="seats-area"><view v-for="(row, rowIndex) in seatMap" :key="rowIndex" class="seat-row"><view v-for="(seat, colIndex) in row" :key="colIndex" class="seat" :class="[seat.type,{'selected': seat.selected,'sold': seat.sold}]" :data-row="rowIndex" :data-col="colIndex" @tap.stop="selectSeat(rowIndex, colIndex)"><image v-if="seat.selected" class="seat-selected-image"src="https://s.xitupt.com/tsimgs/949558333604714792/20250318/h5_mng_1742303462702" mode="aspectFit"></image></view></view></view></view></view><!-- 底部固定区域 --><view class="bottom-fixed"><!-- 卡片部分 --><view class="info-card"><view class="movie-info"><view class="movie-title"><text class="title">初步举证</text></view><view class="movie-time"><text class="today">今天</text><text class="time">14:10-16:25</text></view><!-- 已选座位区域 --><view class="selected-seats" v-if="selectedSeatsCount > 0"><!-- 已选标签和数量 --><view class="selected-header"><text class="selected-label">已选:</text><text class="selected-count">{{ selectedSeatsCount }}个座位</text></view><!-- 座位详情列表 --><scroll-view class="seats-scroll" scroll-x show-scrollbar="false"><view class="seats-container"><view class="seat-tag" v-for="(seat, index) in getSelectedSeats()" :key="index"><view class="seat-info"><view class="seat-position">{{ `${seat.row + 1}排${seat.col + 1}座` }}</view><view class="seat-price">¥{{ getSeatPrice(seat.type) }}</view></view><text class="close" @tap.stop="selectSeat(seat.row, seat.col)">×</text></view></view></scroll-view></view></view></view><!-- 按钮部分 --><view class="button-wrapper"><view class="confirm-button" :class="{ 'disabled': selectedSeatsCount === 0 }" @tap="confirmSeats"><text>¥{{ getTotalPrice() }} 确认选座</text></view></view></view></view>
</template><script>
export default {data() {return {seatMap: [], // 座位数据isDragging: false, // 是否正在拖动中dragAction: false, // 拖动动作(true:选择, false:取消选择)lastDragPosition: { row: -1, col: -1 }, // 上一次拖动的位置scale: 1, // 当前缩放比例startDistance: 0, // 开始的手势距离position: { x: 0, y: 0 }, // 添加位置信息lastTouch: { x: 0, y: 0 }, // 记录上次触摸位置minScale: 0.5, // 最小缩放比例maxScale: 2, // 最大缩放比例maxSelectedSeats: 40, // 最多可选座位数selectedSeatsCount: 0, // 当前已选座位数touchStartTime: 0, // 触摸开始的时间戳,用于区分点击和拖动touchStartPos: { x: 0, y: 0 }, // 触摸开始的位置,用于计算移动距离isMoved: false, // 是否发生了移动,用于区分点击和拖动事件}},created() {this.initSeatMap()},methods: {/*** 初始化座位图数据* 创建一个二维数组来表示影院座位布局*/initSeatMap() {// 定义座位图的尺寸const rows = 6 // 总行数const cols = 8 // 每行座位数// 创建二维数组并初始化每个座位的属性this.seatMap = Array(rows).fill().map((_, rowIndex) =>Array(cols).fill().map((_, colIndex) => ({// 根据位置确定座位类型(粉色/橙色/蓝色)type: this.getSeatType(rowIndex, colIndex),// 初始状态为未选中selected: false,// 随机设置座位是否已售出sold: this.getRandomSoldStatus(rowIndex, colIndex)})))},getSeatType(row, col) {// 第一列和第二列、倒数第二列、倒数第一列是蓝色if (col < 2 || col > 5) {return 'blue'}// 第三列和倒数第三列是橙色if (col === 2 || col === 5) {return 'orange'}// 第一行的第四第五列是橙色if (row === 0 && (col === 3 || col === 4)) {return 'orange'}// 第五行的第四第五列是橙色if (row === 5 && (col === 3 || col === 4)) {return 'orange'}// 第四第五列的第二到第四行是粉色if ((col === 3 || col === 4) && (row >= 1 && row <= 4)) {return 'pink'}return 'blue' // 默认蓝色},/*** 处理座位选择事件* 用于切换座位的选中状态,并管理已选座位数量* @param {number} row - 座位所在行号* @param {number} col - 座位所在列号*/selectSeat(row, col) {// 防止拖动操作触发选座// 当用户拖动查看座位时,不应触发选座操作if (this.isMoved) return;// 检查座位是否可选// 验证座位是否存在且未售出if (!this.isSeatSelectable(row, col)) return;// 获取目标座位对象const seat = this.seatMap[row][col];// 检查是否超出最大可选座位数// 仅在要选中新座位时进行检查if (!seat.selected && this.selectedSeatsCount >= this.maxSelectedSeats) {// 显示提示信息uni.showToast({title: `最多只能选择${this.maxSelectedSeats}个座位`,icon: 'none'});return;}// 切换座位选中状态seat.selected = !seat.selected;// 更新已选座位计数// 选中时 +1,取消选中时 -1this.selectedSeatsCount += seat.selected ? 1 : -1;},// 随机设置部分座位为已售getRandomSoldStatus(row, col) {// 约20%的概率将座位标记为已售return Math.random() < 0.2;},// 检查座位是否可选择isSeatSelectable(row, col) {// 确保座位存在且未售出return this.seatMap[row] &&this.seatMap[row][col] &&!this.seatMap[row][col].sold;},/*** 获取点击或触摸事件对应的座位位置* 通过事件对象获取座位的行列信息* @param {Event|TouchEvent} event - 点击或触摸事件对象* @returns {Object} 返回座位位置对象,包含行(row)和列(col)信息,若未找到有效位置则返回 {row: -1, col: -1}*/getSeatPosition(event) {// 统一处理触摸事件和点击事件// 触摸事件时从 touches 数组获取第一个触摸点// 点击事件时直接使用事件对象const touch = event.touches ? event.touches[0] : event;// 获取触摸/点击的坐标位置// clientX/Y 用于标准事件,x/y 用于某些特殊环境const x = touch.clientX || touch.x;const y = touch.clientY || touch.y;// 创建查询对象,用于获取 DOM 信息(当前未使用)const query = uni.createSelectorQuery();// 从事件目标的数据集中获取座位信息// 使用 HTML5 data-* 属性存储的行列信息if (event.target && event.target.dataset) {const dataset = event.target.dataset;// 检查数据集中是否包含有效的行列信息if (dataset.row !== undefined && dataset.col !== undefined) {// 返回解析后的座位位置// parseInt 确保返回数值类型return {row: parseInt(dataset.row),col: parseInt(dataset.col)};}}// 如果无法获取有效的座位信息// 返回表示无效位置的对象return { row: -1, col: -1 };},/*** 处理触摸开始事件* 用于初始化拖拽和缩放的起始状态* @param {TouchEvent} event - 触摸事件对象,包含触摸点信息*/onTouchStart(event) {// 记录触摸开始的时间戳,用于后续判断是点击还是拖动this.touchStartTime = Date.now();// 重置移动标志,初始状态下未发生移动this.isMoved = false;// 单指触摸 - 处理拖动初始化if (event.touches.length === 1) {const touch = event.touches[0];// 记录当前触摸点作为上一次触摸位置,用于计算移动距离this.lastTouch = { x: touch.clientX, y: touch.clientY };// 记录触摸起始位置,用于计算总移动距离this.touchStartPos = { x: touch.clientX, y: touch.clientY };}// 双指触摸 - 处理缩放初始化else if (event.touches.length === 2) {// 计算两个触摸点之间的初始距离,用于后续计算缩放比例this.startDistance = this.getDistance(event.touches[0], event.touches[1]);}},// 处理触摸移动事件onTouchMove(event) {// 标记已经发生移动,用于区分点击和拖动this.isMoved = true;// 单指触摸 - 处理拖动if (event.touches.length === 1) {const touch = event.touches[0];// 计算相对于上一次触摸位置的偏移量const deltaX = touch.clientX - this.lastTouch.x;const deltaY = touch.clientY - this.lastTouch.y;// 根据当前缩放比例调整位移距离// 缩放比例越大,移动距离越小,保证移动体验一致this.position.x += deltaX / this.scale;this.position.y += deltaY / this.scale;// 更新最后一次触摸位置this.lastTouch = { x: touch.clientX, y: touch.clientY };}// 双指触摸 - 处理缩放else if (event.touches.length === 2) {// 计算当前两个触摸点之间的距离const currentDistance = this.getDistance(event.touches[0], event.touches[1]);// 根据距离变化计算新的缩放比例let newScale = this.scale * (currentDistance / this.startDistance);// 限制缩放范围在 minScale 和 maxScale 之间newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));this.scale = newScale;// 更新起始距离,用于下一次计算this.startDistance = currentDistance;}// 检查并限制移动边界,防止内容移出可视区域this.checkBoundaries();},// 处理手势结束onTouchEnd() {// 可以在这里处理手势结束后的逻辑},/*** 计算两个触摸点之间的距离* @param {Object} touch1 - 第一个触摸点,包含 clientX 和 clientY 坐标* @param {Object} touch2 - 第二个触摸点,包含 clientX 和 clientY 坐标* @returns {number} 两点之间的欧几里得距离*/getDistance(touch1, touch2) {// 计算 X 轴方向的距离差const dx = touch1.clientX - touch2.clientX;// 计算 Y 轴方向的距离差const dy = touch1.clientY - touch2.clientY;// 使用勾股定理计算两点之间的直线距离// distance = √(dx² + dy²)return Math.sqrt(dx * dx + dy * dy);},/*** 检查并限制座位区域的移动边界* 防止用户将座位区域拖动到视图之外*/checkBoundaries() {// 定义最大可移动距离(像素)const maxX = 200; // X轴最大移动距离,可根据实际座位区域大小调整const maxY = 200; // Y轴最大移动距离,可根据实际座位区域大小调整// 限制X轴移动范围:[-maxX, maxX]// Math.min 确保不会超过右边界// Math.max 确保不会超过左边界this.position.x = Math.max(-maxX, Math.min(maxX, this.position.x));// 限制Y轴移动范围:[-maxY, maxY]// Math.min 确保不会超过下边界// Math.max 确保不会超过上边界this.position.y = Math.max(-maxY, Math.min(maxY, this.position.y));},/*** 获取指定座位在所有已选座位中的序号* @param {number} row - 要查询的座位行号* @param {number} col - 要查询的座位列号* @returns {number} 返回该座位是第几个被选中的座位(从1开始计数)* * 使用场景:* 1. 用于确定座位的选中顺序* 2. 可用于显示座位的选中序号* 3. 帮助用户了解座位的选择顺序*/getSelectedIndex(row, col) {// 初始化计数器,从1开始计数let count = 1;// 遍历所有座位for (let i = 0; i < this.seatMap.length; i++) {for (let j = 0; j < this.seatMap[i].length; j++) {// 检查当前遍历到的座位是否被选中if (this.seatMap[i][j].selected) {// 如果找到目标座位,返回当前计数if (i === row && j === col) return count;// 如果不是目标座位,计数器加1count++;}}}return count;},// 获取已选座位列表getSelectedSeats() {const selectedSeats = [];this.seatMap.forEach((row, rowIndex) => {row.forEach((seat, colIndex) => {if (seat.selected) {selectedSeats.push({row: rowIndex,col: colIndex,type: seat.type});}});});return selectedSeats;},/*** 计算所有已选座位的总价* @returns {string} 返回格式化后的总价字符串,保留两位小数* * 使用场景:* 1. 显示确认选座按钮上的总价* 2. 提交订单时计算支付金额* 3. 更新用户选座时实时显示价格*/getTotalPrice() {// 定义不同类型座位的价格映射const prices = {pink: 40, // 粉色座位(VIP座)价格orange: 38, // 橙色座位(情侣座)价格blue: 35 // 蓝色座位(普通座)价格};// 使用 reduce 方法计算总价// 1. 获取所有已选座位列表// 2. 根据每个座位的类型获取对应价格// 3. 累加所有座位的价格return this.getSelectedSeats().reduce((total, seat) => {// total: 累计总价// seat: 当前座位信息,包含 type 属性return total + prices[seat.type];}, 0).toFixed(2); // 初始值为0,结果保留两位小数},/*** 获取指定类型座位的单价* @param {string} type - 座位类型('pink'|'orange'|'blue')* @returns {string} 返回格式化后的价格字符串,保留两位小数* * 使用场景:* 1. 显示单个座位的价格* 2. 在已选座位列表中显示每个座位的单价*/getSeatPrice(type) {// 定义不同类型座位的价格映射const prices = {pink: 40, // 粉色座位(VIP座)价格orange: 38, // 橙色座位(情侣座)价格blue: 35 // 蓝色座位(普通座)价格};// 返回格式化后的价格,保留两位小数return prices[type].toFixed(2);},/*** 处理确认选座操作* 验证选座状态并进行后续处理* * 使用场景:* 1. 用户点击确认选座按钮时触发* 2. 验证是否已选择座位* 3. 进行下一步订单处理*/confirmSeats() {// 检查是否有选中的座位if (this.selectedSeatsCount === 0) {// 如果没有选择座位,显示提示信息uni.showToast({title: '请先选择座位',icon: 'none'});return;}// TODO: 处理确认选座逻辑// 可以添加以下操作:// 1. 获取选中的座位信息// 2. 调用后端API锁定座位// 3. 跳转到订单确认页面// 4. 处理支付流程等console.log('确认选座', this.getSelectedSeats());}}
}
</script><style scoped>
.chooseSeat {width: 100%;min-height: 100vh;padding: 20rpx;box-sizing: border-box;
}.chooseSeat-header {padding: 20rpx 0;text-align: center;font-size: 32rpx;font-weight: bold;
}.price-info {display: flex;justify-content: space-around;margin: 20rpx 0;
}.price-item {display: flex;align-items: center;
}.price-box {width: 30rpx;height: 30rpx;margin-right: 10rpx;border-radius: 4rpx;
}.price-box.pink {background-color: #FF3162;
}.price-box.orange {background-color: #F6BB7F;
}.price-box.blue {background-color: #8BBFF0;
}.screen {margin: 40rpx 0;text-align: center;
}.screen-image {width: 90%;height: 60rpx;margin: 0 auto 10rpx;
}.seat-container {width: 100%;height: 100%;display: flex;margin-top: 40rpx;user-select: none;touch-action: none;/* overflow: hidden; *//* 防止缩放时溢出 */
}/* 新增包装器样式 */
.seat-area-wrapper {display: flex;will-change: transform;touch-action: none;
}/* 修改行号样式 */
.row-numbers {width: 35rpx;margin-right: 75rpx;background: rgba(0, 0, 0, 0.3);border-radius: 36rpx;display: flex;flex-direction: column;
}.row-number {height: 50rpx;/* 与座位高度一致 */line-height: 50rpx;text-align: center;font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 24rpx;color: #FFFFFF;margin-bottom: 20rpx;/* 与座位间距一致 */
}.row-number:last-child {margin-bottom: 0;/* 最后一个行号不需要底部间距 */
}.seat-row {display: flex;justify-content: space-between;margin-bottom: 20rpx;/* 修改座位行间距为20rpx */
}.seat-row:last-child {margin-bottom: 0;/* 最后一行不需要底部间距 */
}.seat {width: 50rpx;height: 50rpx;margin-right: 20rpx;background-color: #fff;border-radius: 8rpx;position: relative;transition: transform 0.3s ease;/* 添加过渡效果 */
}.seat:last-child {margin-right: 0;/* 最后一个座位不需要右边距 */
}.seats-area {flex: 1;will-change: transform;touch-action: none;padding: 0;/* 移除内边距 */
}/* 修改选中状态的样式 */
.seat.selected {transform: scale(1.05);/* 恢复放大效果 */
}/* 选中图片样式 */
.seat-selected-image {position: absolute;top: 0;left: 0;width: 100%;height: 100%;z-index: 1;pointer-events: none;
}/* 修改选中状态的边框样式 */
.seat.selected.pink {border: 2rpx solid #FF3162;
}.seat.selected.orange {border: 2rpx solid #F6BB7F;
}.seat.selected.blue {border: 2rpx solid #8BBFF0;
}/* 未选中状态样式 */
.seat.pink {border: 2rpx solid #FF3162;
}.seat.orange {border: 2rpx solid #F6BB7F;
}.seat.blue {border: 2rpx solid #8BBFF0;
}/* 已售出座位的样式 */
.seat.sold {background-color: #F5F5F5;border-color: #E0E0E0;opacity: 0.6;cursor: not-allowed;transition: all 0.2s ease;filter: grayscale(100%);
}.seat.sold::after {content: "×";position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 24rpx;color: #999;
}/* 添加hover效果 */
.seat:active:not(.sold) {opacity: 0.8;transform: scale(0.95);
}/* 底部固定区域 */
.bottom-fixed {position: fixed;left: 0;bottom: 0;width: 100%;z-index: 100;display: flex;flex-direction: column;
}/* 卡片样式 */
.info-card {margin: 20rpx;padding: 20rpx;background-color: #fff;border-radius: 16rpx;box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}.movie-title {margin-bottom: 16rpx;
}.movie-title .title {font-size: 32rpx;font-weight: bold;margin-right: 20rpx;
}.movie-time {display: flex;align-items: center;margin-bottom: 16rpx;
}.movie-time .today {font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 24rpx;color: #FF3162;margin-right: 8rpx;
}.movie-time .time {font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 24rpx;color: #666666;
}.selected-seats {display: flex;flex-direction: column;gap: 16rpx;
}.selected-header {display: flex;align-items: center;
}.selected-label {font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 24rpx;color: #666666;
}.selected-count {font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 24rpx;color: #FF3162;margin-left: 8rpx;
}/* 滚动区域样式 */
.seats-scroll {width: 100%;white-space: nowrap;overflow: hidden;
}/* 座位容器样式 */
.seats-container {display: inline-flex;align-items: center;
}.seat-tag {width: 147rpx;height: 64rpx;background: #F4F5F7;border-radius: 10rpx;display: inline-flex;align-items: center;justify-content: space-between;padding: 0 16rpx;margin-right: 10rpx;flex-shrink: 0;
}.seat-info {display: flex;flex-direction: column;align-items: flex-start;
}.seat-position {font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 24rpx;color: #666666;
}.seat-price {font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 20rpx;color: #FF3162;
}.seat-tag .close {color: #999;font-size: 28rpx;
}.seat-tag:last-child {margin-right: 0;
}/* 按钮容器 */
.button-wrapper {padding: 20rpx;background-color: #fff;
}/* 确认按钮样式 */
.confirm-button {width: 100%;height: 88rpx;background: linear-gradient(to right, #ff3162, #ff6c89);border-radius: 44rpx;display: flex;justify-content: center;align-items: center;color: #fff;font-size: 32rpx;font-weight: 500;margin-bottom: constant(safe-area-inset-bottom);/* iOS 11.2+ */margin-bottom: env(safe-area-inset-bottom);/* iOS 11.2+ */
}.confirm-button.disabled {background: #ccc;opacity: 0.8;
}
</style>
相关文章:
uniapp 实现微信小程序电影选座功能
拖动代码 /*** 获取点击或触摸事件对应的座位位置* 通过事件对象获取座位的行列信息* param {Event|TouchEvent} event - 点击或触摸事件对象* returns {Object} 返回座位位置对象,包含行(row)和列(col)信息,若未找到有效位置则返回 {row: -1, col: -1}*…...
python+flask实现360全景图和stl等多种格式模型浏览
1. 安装依赖 pip install flask 2. 创建Flask应用 创建一个基本的Flask应用,并设置路由来处理不同的文件类型。 from flask import Flask, render_template, send_from_directory app Flask(__name__) # 设置静态文件路径 app.static_folder static app.r…...
IntelliJ 配置文件plugin.xml
在 IntelliJ IDEA 插件开发中,plugin.xml 是插件的配置文件,它包含了关于插件的所有基本信息、扩展点、依赖关系等。该文件使用 XML 格式进行定义。以下是 plugin.xml 中常见的元素及其用途: <idea-plugin><!-- 插件的基本信息 --&…...
C# Unity 唐老狮 No.10 模拟面试题
本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: Unity课程 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho C# 1. 内存中,堆和…...
数据库系统——规范化1NF~BCNF
数据库规范化完全指南:从零到BCNF,中学生也能秒懂!📚✨ 一、什么是数据库规范化? 科学定义 🔍 数据库规范化是通过一系列规则(范式)将数据库表结构分解为更小、更高效、无冗余的表…...
第十五届蓝桥杯2024JavaB组省赛试题A:报数游戏
简单的找规律题目。题目给得数列,第奇数项是20的倍数,第偶数项时24的倍数。题目要求第n 202420242024 项是多少。这一项是偶数,所以答案一定是24的倍数,并且偶数项的个数和奇数项的个数各占一半,所以最终的答案ans( n…...
Matlab 汽车二自由度转弯模型
1、内容简介 Matlab 187-汽车二自由度转弯模型 可以交流、咨询、答疑 2、内容说明 略 摘 要 本文前一部分提出了侧偏角和横摆角速度作为参数。描述了车辆运动的运动状态,其中文中使用的参考模型是二自由度汽车模型。汽车速度被认为是建立基于H.B.Pacejka的轮胎模…...
关于 2>/dev/null 的作用以及机理
每个进程都有三个标准文件描述符:stdin(标准输入)、stdout(标准输出)和stderr(标准错误)。默认情况下,stderr会输出到终端。使用2>可以将stderr重定向到其他地方,比如…...
学c++的人可以几天速通python?
学了俩天啊,文章写纸上了 还是蛮有趣的...
HTML,CSS,JavaScript
HTML:负责网页的结构(页面元素和内容)。 CSS:负责网页的表现(页面元素的外观、位置等页面样式,如:颜色、大小等)。 Javascript:负责网页的行为(交互效果)。 MDN前端开发文档(MDN Web Docs) HTML HTML(HyperText Markup Language):超文本标记语言超文本:超越了文本的…...
微信小程序面试内容整理-图片优化
在微信小程序中,图片优化是提升加载速度、节省网络带宽和提高用户体验的重要步骤。图片通常是小程序页面中的主要资源,合理的图片优化能显著提高小程序的性能,尤其是在用户网络状况较差的情况下。 1. 选择合适的图片格式 不同的图片格式有不同的特点,选择合适的格式能够有效…...
Rocky Linux 9.x 基于 kubeadm部署k8s 1.32
一、部署说明 1、主机操作系统说明 序号操作系统及版本备注1Rocky Linux release 9下载链接:https://mirrors.163.com/rocky/9.5/isos/x86_64/Rocky-9.5-x86_64-minimal.iso 2、主机硬件配置说明 作用IP地址操作系统配置关键组件k8s-master01192.168.234.51Rocky…...
【每日学点HarmonyOS Next知识】上下拉列表、停止无限循环动画、页面列表跟随列表滑动、otf字体、日期选择
1、HarmonyOS 实现只需要保留上拉加载更多,但是不需要下拉刷新? Refresh通过参数refreshing判断当前组件是否正在刷新,可以控制该参数变化来触发下拉刷新:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5…...
解决git init 命令不显示.git
首先在自己的项目代码右击 打开git bash here 输入git init 之后自己的项目没有.git文件,有可能是因为.git文件隐藏了,下面是解决办法...
利用AI让数据可视化
1. 从问卷星上下载一份答题结果。 序号用户ID提交答卷时间所用时间来源来源详情来自IP总分1、《中华人民共和国电子商务法》正式实施的时间是()。2、()可以判断企业在行业中所处的地位。3、()是指店铺内有…...
神经网络微调技术解析
神经网络微调技术 微调(Fine-tuning)是迁移学习的核心技术,通过在预训练模型基础上调整参数,使其适应特定任务或领域。以下从传统方法、参数高效微调(PEFT)、新兴技术三个维度展开,覆盖主流技术…...
WebLogic XMLDecoder反序列化漏洞(CVE-2017-10271)深度解析与实战复现
0x00 漏洞概述 CVE-2017-10271 是Oracle WebLogic Server WLS Security组件中的远程代码执行漏洞。攻击者通过构造恶意XML请求,利用XMLDecoder反序列化机制绕过安全验证,最终实现服务器权限接管。 影响版本 WebLogic 10.3.6.0WebLogic 12.1.3.0WebLog…...
解决qt中自定插件加载失败,不显示问题。
这个问题断断续续搞了一天多,主要是版本不匹配问题。 我们先来看下 Based on Qt 6.6.0 → 说明 Qt Creator 本身 是基于 Qt 6.6.0 框架构建的。MSVC 2019, 64-bit → 说明 Qt Creator 是使用 Microsoft Visual C 2019 编译器(64 位) 编译的。…...
Git 面试问题,解决冲突
1.问题描述 在多人协作开发中,当多个开发者在同一文件的同一部分进行修改并提交时,Git 无法自动合并这些更改,从而产生代码冲突(Conflict)。冲突的代码会被 Git 标记出来,需要开发者手动解决。 冲突原因 多…...
Apache Shiro 使用教程
Apache Shiro 使用教程 Apache Shiro是一个强大且灵活的开源安全框架,主要用于处理身份验证(Authentication)、授权(Authorization)、加密(Cryptography)和会话管理(Session Manage…...
用maven生成springboot多模块项目
用Maven生成Spring Boot多模块项目,可以按照以下步骤操作: 1. 创建父项目 首先,使用Maven的archetype插件创建一个空的Maven项目作为父项目。打开终端,执行以下命令: mvn archetype:generate -DgroupIdcom.example -…...
【最佳实践】Go 状态模式
设计思路 状态模式的核心在于将对象的行为封装在特定的状态类中,使得对象在不同的状态下表现出不同的行为。每个状态实现同一个接口,允许对象在运行时通过改变其内部状态对象来改变其行为。状态模式使得状态转换更加明确,并且易于扩展新的状…...
智慧社区3.0
项目介绍: 此项目旨在推动成都市探索**超大城市社区发展治理新路**,由三个实验室负责三大内容 1、**研发社区阵地空间管理模块**:AI算法实现态势感知(如通过社区图片和视频、文本,对环境 空间质量、绿视率、安全感分…...
C#语法基础总结
输入和输出 输入 Console.Read(); 从屏幕读取一个字符,并返回该字符所对应的整型数字 Console.ReadLine(); 从屏幕读取一串字符,并返回该字符串 输出 Console.WriteLine(); 输出内容,并换行 Console.Write(); 输出内容,不换行…...
Springboot+Vue登录、注册功能(含验证码)(后端!)
我们首先写一个接口,叫login!然后对传入一个user,因为我们前端肯定是要传过来一个user,然后我们后端返回一个user,因为我们要根据这个去校验!我们还引入了一个hutool的一个东西,在pom文件里面引…...
深入理解 HTML 中的<div>和元素:构建网页结构与样式的基石
一、引言 在 HTML 的世界里,<div>和元素虽看似普通,却扮演着极为关键的角色。它们就像网页搭建过程中的万能积木,能够将各种 HTML 元素巧妙地组合起来,无论是构建页面布局,还是对局部内容进行样式调整ÿ…...
搞定python之八----操作mysql
本文是《搞定python》系列文章的第八篇,讲述利用python操作mysql数据库。相对来说,本文的综合性比较强,包含了操作数据库、异常处理、元组等内容,需要结合前面的知识点。 1、安装mysql模块 PyMySql模块相当于数据库的驱动&#…...
游戏立项时期随笔记录(1)
模拟经营的项目还没有完全结束,这几天又有可能涉及到一个新项目。感想随笔记录一下,防止忘记。今天一天整理这个,搞得今天没时间看数学和AI。 在 Unity3D 游戏前端主程序的立项时期,核心目标是明确技术方向、评估可行性、搭建基础…...
LVGL 中设置 UI 层局部透明,显示下方视频层
LVGL层次 LVGL自上而下分别是layer_sys > layer_top > lv_sreen_active > layer_bottom 即 系统层、顶层、活动屏幕、底层 原理 如果将UI设置为局部透明,显示下方的视频层,不仅仅需要将当前活动屏幕的背景设置为透明,还需要将底层…...
Debezium日常分享系列之:Debezium 3.1.0.Beta1发布
Debezium日常分享系列之:Debezium 3.1.0.Beta1发布 新特性和改进Debezium 平台的首次发布Percona 的最小锁定新的 Oracle 源信息 SCN 和时间戳字段Vitess Epoch/零日期列解析的变化Vitess 二进制排序的 tiny、medium 和 long 文本列的变化CloudEvent traceparent 支…...
