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

news2025/3/19 5:30:15

拖动代码

  /**
     * 获取点击或触摸事件对应的座位位置
     * 通过事件对象获取座位的行列信息
     * @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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2317590.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C# Unity 唐老狮 No.10 模拟面试题

本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: Unity课程 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho C# 1. 内存中&#xff0c;堆和…

第十五届蓝桥杯2024JavaB组省赛试题A:报数游戏

简单的找规律题目。题目给得数列&#xff0c;第奇数项是20的倍数&#xff0c;第偶数项时24的倍数。题目要求第n 202420242024 项是多少。这一项是偶数&#xff0c;所以答案一定是24的倍数&#xff0c;并且偶数项的个数和奇数项的个数各占一半&#xff0c;所以最终的答案ans( n…

Matlab 汽车二自由度转弯模型

1、内容简介 Matlab 187-汽车二自由度转弯模型 可以交流、咨询、答疑 2、内容说明 略 摘 要 本文前一部分提出了侧偏角和横摆角速度作为参数。描述了车辆运动的运动状态&#xff0c;其中文中使用的参考模型是二自由度汽车模型。汽车速度被认为是建立基于H.B.Pacejka的轮胎模…

学c++的人可以几天速通python?

学了俩天啊&#xff0c;文章写纸上了 还是蛮有趣的

Rocky Linux 9.x 基于 kubeadm部署k8s 1.32

一、部署说明 1、主机操作系统说明 序号操作系统及版本备注1Rocky Linux release 9下载链接&#xff1a;https://mirrors.163.com/rocky/9.5/isos/x86_64/Rocky-9.5-x86_64-minimal.iso 2、主机硬件配置说明 作用IP地址操作系统配置关键组件k8s-master01192.168.234.51Rocky…

解决git init 命令不显示.git

首先在自己的项目代码右击 打开git bash here 输入git init 之后自己的项目没有.git文件&#xff0c;有可能是因为.git文件隐藏了&#xff0c;下面是解决办法

利用AI让数据可视化

1. 从问卷星上下载一份答题结果。 序号用户ID提交答卷时间所用时间来源来源详情来自IP总分1、《中华人民共和国电子商务法》正式实施的时间是&#xff08;&#xff09;。2、&#xff08;&#xff09;可以判断企业在行业中所处的地位。3、&#xff08;&#xff09;是指店铺内有…

解决qt中自定插件加载失败,不显示问题。

这个问题断断续续搞了一天多&#xff0c;主要是版本不匹配问题。 我们先来看下 Based on Qt 6.6.0 → 说明 Qt Creator 本身 是基于 Qt 6.6.0 框架构建的。MSVC 2019, 64-bit → 说明 Qt Creator 是使用 Microsoft Visual C 2019 编译器&#xff08;64 位&#xff09; 编译的。…

智慧社区3.0

项目介绍&#xff1a; 此项目旨在推动成都市探索**超大城市社区发展治理新路**&#xff0c;由三个实验室负责三大内容 1、**研发社区阵地空间管理模块**&#xff1a;AI算法实现态势感知&#xff08;如通过社区图片和视频、文本&#xff0c;对环境 空间质量、绿视率、安全感分…

Springboot+Vue登录、注册功能(含验证码)(后端!)

我们首先写一个接口&#xff0c;叫login&#xff01;然后对传入一个user&#xff0c;因为我们前端肯定是要传过来一个user&#xff0c;然后我们后端返回一个user&#xff0c;因为我们要根据这个去校验&#xff01;我们还引入了一个hutool的一个东西&#xff0c;在pom文件里面引…

搞定python之八----操作mysql

本文是《搞定python》系列文章的第八篇&#xff0c;讲述利用python操作mysql数据库。相对来说&#xff0c;本文的综合性比较强&#xff0c;包含了操作数据库、异常处理、元组等内容&#xff0c;需要结合前面的知识点。 1、安装mysql模块 PyMySql模块相当于数据库的驱动&#…

LVGL 中设置 UI 层局部透明,显示下方视频层

LVGL层次 LVGL自上而下分别是layer_sys > layer_top > lv_sreen_active > layer_bottom 即 系统层、顶层、活动屏幕、底层 原理 如果将UI设置为局部透明&#xff0c;显示下方的视频层&#xff0c;不仅仅需要将当前活动屏幕的背景设置为透明&#xff0c;还需要将底层…

21.多态

一、多态概念 多种形态。 静态多态&#xff1a;编译时多态。&#xff08;函数重载&#xff09; 动态多态&#xff1a;运行时多态。&#xff08;继承关系下&#xff0c;调用父类指针或引用&#xff0c;对于不同的对象有不同的行为&#xff09; 二、多态的定义及实现 1&#xff…

【蓝桥杯】第十三届C++B组省赛

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;蓝桥杯 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 试题A&#xff1a;九进制转十进制试题B&#xff1a;顺子日期试题C&#xff1a;刷题统计试题D&#xff1a;修剪灌木试题E&#xf…

C# PaddleOCR字符识别

1 安装Nuget 2 C# using System; using OpenCvSharp; using Sdcb.PaddleOCR; using Sdcb.PaddleOCR.Models.Local; using Sdcb.PaddleOCR.Models; using Sdcb.PaddleInference;namespace ConsoleApp1 {public class MichaelOCR{string imagePath "D:\\BUFFER\\VS\\Text\…

多环境开发-Profiles

在实际的项目开发中&#xff0c;我们通常会涉及多个环境&#xff0c;如开发环境&#xff08;dev&#xff09;、测试环境&#xff08;test&#xff09;和生产环境&#xff08;pro&#xff09;。在不同的环境下&#xff0c;程序的配置信息会有所不同&#xff0c;例如连接的数据库…

《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现

《TCP/IP网络编程》学习笔记 | Chapter 18&#xff1a;多线程服务器端的实现 《TCP/IP网络编程》学习笔记 | Chapter 18&#xff1a;多线程服务器端的实现线程的概念引入线程的背景线程与进程的区别 线程创建与运行pthread_createpthread_join可在临界区内调用的函数工作&#…

MambaVision:一种Mamba-Transformer混合视觉骨干网络

摘要 我们提出了一种新型混合Mamba-Transformer主干网络&#xff0c;称为MambaVision&#xff0c;该网络专为视觉应用而设计。我们的核心贡献包括重新设计Mamba公式&#xff0c;以增强其对视觉特征的高效建模能力。此外&#xff0c;我们还对将视觉Transformer&#xff08;ViT&…

深度学习-服务器训练SparseDrive过程记录

1、cuda安装 1.1 卸载安装失败的cuda 参考&#xff1a;https://blog.csdn.net/weixin_40826634/article/details/127493809 注意&#xff1a;因为/usr/local/cuda-xx.x/bin/下没有卸载脚本&#xff0c;很可能是apt安装的&#xff0c;所以通过执行下面的命令删除&#xff1a; a…

学习单片机需要多长时间才能进行简单的项目开发?

之前有老铁问我&#xff0c;学单片机到底要多久&#xff0c;才能进行简单的项目开发&#xff1f;是三个月速成&#xff0c;还是三年磨一剑&#xff1f; 今天咱们就来聊聊这个话题&#xff0c;我不是什么高高在上的专家&#xff0c;就是个踩过无数坑、烧过几块板子的“技术老友”…