拖动代码
/**
* 获取点击或触摸事件对应的座位位置
* 通过事件对象获取座位的行列信息
* @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;
// 如果不是目标座位,计数器加1
count++;
}
}
}
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,取消选中时 -1
this.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;
// 如果不是目标座位,计数器加1
count++;
}
}
}
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>