uniapp 实现微信小程序电影选座功能

打印 上一主题 下一主题

主题 1006|帖子 1006|积分 3018

拖动代码
  1.   /**
  2.      * 获取点击或触摸事件对应的座位位置
  3.      * 通过事件对象获取座位的行列信息
  4.      * @param {Event|TouchEvent} event - 点击或触摸事件对象
  5.      * @returns {Object} 返回座位位置对象,包含行(row)和列(col)信息,若未找到有效位置则返回 {row: -1, col: -1}
  6.      */
  7.     getSeatPosition(event) {
  8.       // 统一处理触摸事件和点击事件
  9.       // 触摸事件时从 touches 数组获取第一个触摸点
  10.       // 点击事件时直接使用事件对象
  11.       const touch = event.touches ? event.touches[0] : event;
  12.       // 获取触摸/点击的坐标位置
  13.       // clientX/Y 用于标准事件,x/y 用于某些特殊环境
  14.       const x = touch.clientX || touch.x;
  15.       const y = touch.clientY || touch.y;
  16.       // 创建查询对象,用于获取 DOM 信息(当前未使用)
  17.       const query = uni.createSelectorQuery();
  18.       // 从事件目标的数据集中获取座位信息
  19.       // 使用 HTML5 data-* 属性存储的行列信息
  20.       if (event.target && event.target.dataset) {
  21.         const dataset = event.target.dataset;
  22.         // 检查数据集中是否包含有效的行列信息
  23.         if (dataset.row !== undefined && dataset.col !== undefined) {
  24.           // 返回解析后的座位位置
  25.           // parseInt 确保返回数值类型
  26.           return {
  27.             row: parseInt(dataset.row),
  28.             col: parseInt(dataset.col)
  29.           };
  30.         }
  31.       }
  32.       // 如果无法获取有效的座位信息
  33.       // 返回表示无效位置的对象
  34.       return { row: -1, col: -1 };
  35.     },
  36.     /**
  37.      * 处理触摸开始事件
  38.      * 用于初始化拖拽和缩放的起始状态
  39.      * @param {TouchEvent} event - 触摸事件对象,包含触摸点信息
  40.      */
  41.     onTouchStart(event) {
  42.       // 记录触摸开始的时间戳,用于后续判断是点击还是拖动
  43.       this.touchStartTime = Date.now();
  44.       // 重置移动标志,初始状态下未发生移动
  45.       this.isMoved = false;
  46.       // 单指触摸 - 处理拖动初始化
  47.       if (event.touches.length === 1) {
  48.         const touch = event.touches[0];
  49.         // 记录当前触摸点作为上一次触摸位置,用于计算移动距离
  50.         this.lastTouch = { x: touch.clientX, y: touch.clientY };
  51.         // 记录触摸起始位置,用于计算总移动距离
  52.         this.touchStartPos = { x: touch.clientX, y: touch.clientY };
  53.       }
  54.       // 双指触摸 - 处理缩放初始化
  55.       else if (event.touches.length === 2) {
  56.         // 计算两个触摸点之间的初始距离,用于后续计算缩放比例
  57.         this.startDistance = this.getDistance(event.touches[0], event.touches[1]);
  58.       }
  59.     },
  60.     // 处理触摸移动事件
  61.     onTouchMove(event) {
  62.       // 标记已经发生移动,用于区分点击和拖动
  63.       this.isMoved = true;
  64.       // 单指触摸 - 处理拖动
  65.       if (event.touches.length === 1) {
  66.         const touch = event.touches[0];
  67.         // 计算相对于上一次触摸位置的偏移量
  68.         const deltaX = touch.clientX - this.lastTouch.x;
  69.         const deltaY = touch.clientY - this.lastTouch.y;
  70.         // 根据当前缩放比例调整位移距离
  71.         // 缩放比例越大,移动距离越小,保证移动体验一致
  72.         this.position.x += deltaX / this.scale;
  73.         this.position.y += deltaY / this.scale;
  74.         // 更新最后一次触摸位置
  75.         this.lastTouch = { x: touch.clientX, y: touch.clientY };
  76.       }
  77.       // 双指触摸 - 处理缩放
  78.       else if (event.touches.length === 2) {
  79.         // 计算当前两个触摸点之间的距离
  80.         const currentDistance = this.getDistance(event.touches[0], event.touches[1]);
  81.         // 根据距离变化计算新的缩放比例
  82.         let newScale = this.scale * (currentDistance / this.startDistance);
  83.         // 限制缩放范围在 minScale 和 maxScale 之间
  84.         newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
  85.         this.scale = newScale;
  86.         // 更新起始距离,用于下一次计算
  87.         this.startDistance = currentDistance;
  88.       }
  89.       // 检查并限制移动边界,防止内容移出可视区域
  90.       this.checkBoundaries();
  91.     },
  92.     // 处理手势结束
  93.     onTouchEnd() {
  94.       // 可以在这里处理手势结束后的逻辑
  95.     },
  96.     /**
  97.      * 计算两个触摸点之间的距离
  98.      * @param {Object} touch1 - 第一个触摸点,包含 clientX 和 clientY 坐标
  99.      * @param {Object} touch2 - 第二个触摸点,包含 clientX 和 clientY 坐标
  100.      * @returns {number} 两点之间的欧几里得距离
  101.      */
  102.     getDistance(touch1, touch2) {
  103.       // 计算 X 轴方向的距离差
  104.       const dx = touch1.clientX - touch2.clientX;
  105.       // 计算 Y 轴方向的距离差
  106.       const dy = touch1.clientY - touch2.clientY;
  107.       // 使用勾股定理计算两点之间的直线距离
  108.       // distance = √(dx² + dy²)
  109.       return Math.sqrt(dx * dx + dy * dy);
  110.     },
  111.     /**
  112.      * 检查并限制座位区域的移动边界
  113.      * 防止用户将座位区域拖动到视图之外
  114.      */
  115.     checkBoundaries() {
  116.       // 定义最大可移动距离(像素)
  117.       const maxX = 200; // X轴最大移动距离,可根据实际座位区域大小调整
  118.       const maxY = 200; // Y轴最大移动距离,可根据实际座位区域大小调整
  119.       // 限制X轴移动范围:[-maxX, maxX]
  120.       // Math.min 确保不会超过右边界
  121.       // Math.max 确保不会超过左边界
  122.       this.position.x = Math.max(-maxX, Math.min(maxX, this.position.x));
  123.       // 限制Y轴移动范围:[-maxY, maxY]
  124.       // Math.min 确保不会超过下边界
  125.       // Math.max 确保不会超过上边界
  126.       this.position.y = Math.max(-maxY, Math.min(maxY, this.position.y));
  127.     },
复制代码
盘算总价代码
 
  1.     /**
  2.      * 获取指定座位在所有已选座位中的序号
  3.      * @param {number} row - 要查询的座位行号
  4.      * @param {number} col - 要查询的座位列号
  5.      * @returns {number} 返回该座位是第几个被选中的座位(从1开始计数)
  6.      *
  7.      * 使用场景:
  8.      * 1. 用于确定座位的选中顺序
  9.      * 2. 可用于显示座位的选中序号
  10.      * 3. 帮助用户了解座位的选择顺序
  11.      */
  12.     getSelectedIndex(row, col) {
  13.       // 初始化计数器,从1开始计数
  14.       let count = 1;
  15.       // 遍历所有座位
  16.       for (let i = 0; i < this.seatMap.length; i++) {
  17.         for (let j = 0; j < this.seatMap[i].length; j++) {
  18.           // 检查当前遍历到的座位是否被选中
  19.           if (this.seatMap[i][j].selected) {
  20.             // 如果找到目标座位,返回当前计数
  21.             if (i === row && j === col) return count;
  22.             // 如果不是目标座位,计数器加1
  23.             count++;
  24.           }
  25.         }
  26.       }
  27.       return count;
  28.     },
  29.     // 获取已选座位列表
  30.     getSelectedSeats() {
  31.       const selectedSeats = [];
  32.       this.seatMap.forEach((row, rowIndex) => {
  33.         row.forEach((seat, colIndex) => {
  34.           if (seat.selected) {
  35.             selectedSeats.push({
  36.               row: rowIndex,
  37.               col: colIndex,
  38.               type: seat.type
  39.             });
  40.           }
  41.         });
  42.       });
  43.       return selectedSeats;
  44.     },
  45.     /**
  46.      * 计算所有已选座位的总价
  47.      * @returns {string} 返回格式化后的总价字符串,保留两位小数
  48.      *
  49.      * 使用场景:
  50.      * 1. 显示确认选座按钮上的总价
  51.      * 2. 提交订单时计算支付金额
  52.      * 3. 更新用户选座时实时显示价格
  53.      */
  54.     getTotalPrice() {
  55.       // 定义不同类型座位的价格映射
  56.       const prices = {
  57.         pink: 40,   // 粉色座位(VIP座)价格
  58.         orange: 38, // 橙色座位(情侣座)价格
  59.         blue: 35    // 蓝色座位(普通座)价格
  60.       };
  61.       // 使用 reduce 方法计算总价
  62.       // 1. 获取所有已选座位列表
  63.       // 2. 根据每个座位的类型获取对应价格
  64.       // 3. 累加所有座位的价格
  65.       return this.getSelectedSeats().reduce((total, seat) => {
  66.         // total: 累计总价
  67.         // seat: 当前座位信息,包含 type 属性
  68.         return total + prices[seat.type];
  69.       }, 0).toFixed(2); // 初始值为0,结果保留两位小数
  70.     },
  71.     /**
  72.      * 获取指定类型座位的单价
  73.      * @param {string} type - 座位类型('pink'|'orange'|'blue')
  74.      * @returns {string} 返回格式化后的价格字符串,保留两位小数
  75.      *
  76.      * 使用场景:
  77.      * 1. 显示单个座位的价格
  78.      * 2. 在已选座位列表中显示每个座位的单价
  79.      */
  80.     getSeatPrice(type) {
  81.       // 定义不同类型座位的价格映射
  82.       const prices = {
  83.         pink: 40,   // 粉色座位(VIP座)价格
  84.         orange: 38, // 橙色座位(情侣座)价格
  85.         blue: 35    // 蓝色座位(普通座)价格
  86.       };
  87.       // 返回格式化后的价格,保留两位小数
  88.       return prices[type].toFixed(2);
  89.     },
  90.     /**
  91.      * 处理确认选座操作
  92.      * 验证选座状态并进行后续处理
  93.      *
  94.      * 使用场景:
  95.      * 1. 用户点击确认选座按钮时触发
  96.      * 2. 验证是否已选择座位
  97.      * 3. 进行下一步订单处理
  98.      */
  99.     confirmSeats() {
  100.       // 检查是否有选中的座位
  101.       if (this.selectedSeatsCount === 0) {
  102.         // 如果没有选择座位,显示提示信息
  103.         uni.showToast({
  104.           title: '请先选择座位',
  105.           icon: 'none'
  106.         });
  107.         return;
  108.       }
  109.       // TODO: 处理确认选座逻辑
  110.       // 可以添加以下操作:
  111.       // 1. 获取选中的座位信息
  112.       // 2. 调用后端API锁定座位
  113.       // 3. 跳转到订单确认页面
  114.       // 4. 处理支付流程等
  115.       console.log('确认选座', this.getSelectedSeats());
  116.     }
复制代码
完备代码
  1. <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;    },    /**
  2.      * 获取点击或触摸事件对应的座位位置
  3.      * 通过事件对象获取座位的行列信息
  4.      * @param {Event|TouchEvent} event - 点击或触摸事件对象
  5.      * @returns {Object} 返回座位位置对象,包含行(row)和列(col)信息,若未找到有效位置则返回 {row: -1, col: -1}
  6.      */
  7.     getSeatPosition(event) {
  8.       // 统一处理触摸事件和点击事件
  9.       // 触摸事件时从 touches 数组获取第一个触摸点
  10.       // 点击事件时直接使用事件对象
  11.       const touch = event.touches ? event.touches[0] : event;
  12.       // 获取触摸/点击的坐标位置
  13.       // clientX/Y 用于标准事件,x/y 用于某些特殊环境
  14.       const x = touch.clientX || touch.x;
  15.       const y = touch.clientY || touch.y;
  16.       // 创建查询对象,用于获取 DOM 信息(当前未使用)
  17.       const query = uni.createSelectorQuery();
  18.       // 从事件目标的数据集中获取座位信息
  19.       // 使用 HTML5 data-* 属性存储的行列信息
  20.       if (event.target && event.target.dataset) {
  21.         const dataset = event.target.dataset;
  22.         // 检查数据集中是否包含有效的行列信息
  23.         if (dataset.row !== undefined && dataset.col !== undefined) {
  24.           // 返回解析后的座位位置
  25.           // parseInt 确保返回数值类型
  26.           return {
  27.             row: parseInt(dataset.row),
  28.             col: parseInt(dataset.col)
  29.           };
  30.         }
  31.       }
  32.       // 如果无法获取有效的座位信息
  33.       // 返回表示无效位置的对象
  34.       return { row: -1, col: -1 };
  35.     },
  36.     /**
  37.      * 处理触摸开始事件
  38.      * 用于初始化拖拽和缩放的起始状态
  39.      * @param {TouchEvent} event - 触摸事件对象,包含触摸点信息
  40.      */
  41.     onTouchStart(event) {
  42.       // 记录触摸开始的时间戳,用于后续判断是点击还是拖动
  43.       this.touchStartTime = Date.now();
  44.       // 重置移动标志,初始状态下未发生移动
  45.       this.isMoved = false;
  46.       // 单指触摸 - 处理拖动初始化
  47.       if (event.touches.length === 1) {
  48.         const touch = event.touches[0];
  49.         // 记录当前触摸点作为上一次触摸位置,用于计算移动距离
  50.         this.lastTouch = { x: touch.clientX, y: touch.clientY };
  51.         // 记录触摸起始位置,用于计算总移动距离
  52.         this.touchStartPos = { x: touch.clientX, y: touch.clientY };
  53.       }
  54.       // 双指触摸 - 处理缩放初始化
  55.       else if (event.touches.length === 2) {
  56.         // 计算两个触摸点之间的初始距离,用于后续计算缩放比例
  57.         this.startDistance = this.getDistance(event.touches[0], event.touches[1]);
  58.       }
  59.     },
  60.     // 处理触摸移动事件
  61.     onTouchMove(event) {
  62.       // 标记已经发生移动,用于区分点击和拖动
  63.       this.isMoved = true;
  64.       // 单指触摸 - 处理拖动
  65.       if (event.touches.length === 1) {
  66.         const touch = event.touches[0];
  67.         // 计算相对于上一次触摸位置的偏移量
  68.         const deltaX = touch.clientX - this.lastTouch.x;
  69.         const deltaY = touch.clientY - this.lastTouch.y;
  70.         // 根据当前缩放比例调整位移距离
  71.         // 缩放比例越大,移动距离越小,保证移动体验一致
  72.         this.position.x += deltaX / this.scale;
  73.         this.position.y += deltaY / this.scale;
  74.         // 更新最后一次触摸位置
  75.         this.lastTouch = { x: touch.clientX, y: touch.clientY };
  76.       }
  77.       // 双指触摸 - 处理缩放
  78.       else if (event.touches.length === 2) {
  79.         // 计算当前两个触摸点之间的距离
  80.         const currentDistance = this.getDistance(event.touches[0], event.touches[1]);
  81.         // 根据距离变化计算新的缩放比例
  82.         let newScale = this.scale * (currentDistance / this.startDistance);
  83.         // 限制缩放范围在 minScale 和 maxScale 之间
  84.         newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
  85.         this.scale = newScale;
  86.         // 更新起始距离,用于下一次计算
  87.         this.startDistance = currentDistance;
  88.       }
  89.       // 检查并限制移动边界,防止内容移出可视区域
  90.       this.checkBoundaries();
  91.     },
  92.     // 处理手势结束
  93.     onTouchEnd() {
  94.       // 可以在这里处理手势结束后的逻辑
  95.     },
  96.     /**
  97.      * 计算两个触摸点之间的距离
  98.      * @param {Object} touch1 - 第一个触摸点,包含 clientX 和 clientY 坐标
  99.      * @param {Object} touch2 - 第二个触摸点,包含 clientX 和 clientY 坐标
  100.      * @returns {number} 两点之间的欧几里得距离
  101.      */
  102.     getDistance(touch1, touch2) {
  103.       // 计算 X 轴方向的距离差
  104.       const dx = touch1.clientX - touch2.clientX;
  105.       // 计算 Y 轴方向的距离差
  106.       const dy = touch1.clientY - touch2.clientY;
  107.       // 使用勾股定理计算两点之间的直线距离
  108.       // distance = √(dx² + dy²)
  109.       return Math.sqrt(dx * dx + dy * dy);
  110.     },
  111.     /**
  112.      * 检查并限制座位区域的移动边界
  113.      * 防止用户将座位区域拖动到视图之外
  114.      */
  115.     checkBoundaries() {
  116.       // 定义最大可移动距离(像素)
  117.       const maxX = 200; // X轴最大移动距离,可根据实际座位区域大小调整
  118.       const maxY = 200; // Y轴最大移动距离,可根据实际座位区域大小调整
  119.       // 限制X轴移动范围:[-maxX, maxX]
  120.       // Math.min 确保不会超过右边界
  121.       // Math.max 确保不会超过左边界
  122.       this.position.x = Math.max(-maxX, Math.min(maxX, this.position.x));
  123.       // 限制Y轴移动范围:[-maxY, maxY]
  124.       // Math.min 确保不会超过下边界
  125.       // Math.max 确保不会超过上边界
  126.       this.position.y = Math.max(-maxY, Math.min(maxY, this.position.y));
  127.     },    /**
  128.      * 获取指定座位在所有已选座位中的序号
  129.      * @param {number} row - 要查询的座位行号
  130.      * @param {number} col - 要查询的座位列号
  131.      * @returns {number} 返回该座位是第几个被选中的座位(从1开始计数)
  132.      *
  133.      * 使用场景:
  134.      * 1. 用于确定座位的选中顺序
  135.      * 2. 可用于显示座位的选中序号
  136.      * 3. 帮助用户了解座位的选择顺序
  137.      */
  138.     getSelectedIndex(row, col) {
  139.       // 初始化计数器,从1开始计数
  140.       let count = 1;
  141.       // 遍历所有座位
  142.       for (let i = 0; i < this.seatMap.length; i++) {
  143.         for (let j = 0; j < this.seatMap[i].length; j++) {
  144.           // 检查当前遍历到的座位是否被选中
  145.           if (this.seatMap[i][j].selected) {
  146.             // 如果找到目标座位,返回当前计数
  147.             if (i === row && j === col) return count;
  148.             // 如果不是目标座位,计数器加1
  149.             count++;
  150.           }
  151.         }
  152.       }
  153.       return count;
  154.     },
  155.     // 获取已选座位列表
  156.     getSelectedSeats() {
  157.       const selectedSeats = [];
  158.       this.seatMap.forEach((row, rowIndex) => {
  159.         row.forEach((seat, colIndex) => {
  160.           if (seat.selected) {
  161.             selectedSeats.push({
  162.               row: rowIndex,
  163.               col: colIndex,
  164.               type: seat.type
  165.             });
  166.           }
  167.         });
  168.       });
  169.       return selectedSeats;
  170.     },
  171.     /**
  172.      * 计算所有已选座位的总价
  173.      * @returns {string} 返回格式化后的总价字符串,保留两位小数
  174.      *
  175.      * 使用场景:
  176.      * 1. 显示确认选座按钮上的总价
  177.      * 2. 提交订单时计算支付金额
  178.      * 3. 更新用户选座时实时显示价格
  179.      */
  180.     getTotalPrice() {
  181.       // 定义不同类型座位的价格映射
  182.       const prices = {
  183.         pink: 40,   // 粉色座位(VIP座)价格
  184.         orange: 38, // 橙色座位(情侣座)价格
  185.         blue: 35    // 蓝色座位(普通座)价格
  186.       };
  187.       // 使用 reduce 方法计算总价
  188.       // 1. 获取所有已选座位列表
  189.       // 2. 根据每个座位的类型获取对应价格
  190.       // 3. 累加所有座位的价格
  191.       return this.getSelectedSeats().reduce((total, seat) => {
  192.         // total: 累计总价
  193.         // seat: 当前座位信息,包含 type 属性
  194.         return total + prices[seat.type];
  195.       }, 0).toFixed(2); // 初始值为0,结果保留两位小数
  196.     },
  197.     /**
  198.      * 获取指定类型座位的单价
  199.      * @param {string} type - 座位类型('pink'|'orange'|'blue')
  200.      * @returns {string} 返回格式化后的价格字符串,保留两位小数
  201.      *
  202.      * 使用场景:
  203.      * 1. 显示单个座位的价格
  204.      * 2. 在已选座位列表中显示每个座位的单价
  205.      */
  206.     getSeatPrice(type) {
  207.       // 定义不同类型座位的价格映射
  208.       const prices = {
  209.         pink: 40,   // 粉色座位(VIP座)价格
  210.         orange: 38, // 橙色座位(情侣座)价格
  211.         blue: 35    // 蓝色座位(普通座)价格
  212.       };
  213.       // 返回格式化后的价格,保留两位小数
  214.       return prices[type].toFixed(2);
  215.     },
  216.     /**
  217.      * 处理确认选座操作
  218.      * 验证选座状态并进行后续处理
  219.      *
  220.      * 使用场景:
  221.      * 1. 用户点击确认选座按钮时触发
  222.      * 2. 验证是否已选择座位
  223.      * 3. 进行下一步订单处理
  224.      */
  225.     confirmSeats() {
  226.       // 检查是否有选中的座位
  227.       if (this.selectedSeatsCount === 0) {
  228.         // 如果没有选择座位,显示提示信息
  229.         uni.showToast({
  230.           title: '请先选择座位',
  231.           icon: 'none'
  232.         });
  233.         return;
  234.       }
  235.       // TODO: 处理确认选座逻辑
  236.       // 可以添加以下操作:
  237.       // 1. 获取选中的座位信息
  238.       // 2. 调用后端API锁定座位
  239.       // 3. 跳转到订单确认页面
  240.       // 4. 处理支付流程等
  241.       console.log('确认选座', this.getSelectedSeats());
  242.     }  }}</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>
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

去皮卡多

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表