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 = √(dx2 + dy2)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 = √(dx2 + dy2)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>

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/898244.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/898244.shtml
英文地址,請注明出處:http://en.pswp.cn/news/898244.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Docker - 切換源 (Linux / macOS)

文章目錄 Linux 系統macOS 系統 Linux 系統 修改配置文件&#xff1a;/etc/docker/daemon.json "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn","https://hub-mirror.c.163.com"]驗證是否修改成功&#xff1a; docker info重啟 …

RocketMQ的安裝及配置(windows)

1. 環境準備 JDK需要先安裝好。 1. RocketMQ是用Java語言寫的&#xff0c;所以需要JDK的支持。2. 下載RocketMQ 建議下載這個低版本的 https://rocketmq.apache.org/release-notes/2020/12/21/4.8.0/ 下載之后解壓即可&#xff0c;目錄結構如下&#xff1a; 3. 配置RocketM…

【運維自動化-標準運維】如何實現一個最簡單的流程編排

流程編排是標準運維最核心的功能&#xff0c;通過將不同功能的原子插件在畫布上可視化的拖拽編排&#xff0c;可以實現各種不同場景的跨系統工作流。標準運維流程 根據實際運維操作場景梳理出來的操作步驟&#xff0c;通過不同的流轉邏輯&#xff08;并行、分支、條件并行&…

性能測試之grafana展示jmeter測試指標與主機監控

性能測試之grafana展示jmeter測試指標與主機監控 背景 ? 公司新的項目準備開展性能測試,之前性能監控主要使用的jmeter的插件jpgc-Transactions per Second 與 jpgc- Response Times Over Time 與 jpgc - Active Threads Over Time等等插件監控性能指標結果,PerfMon Metrics…

1~2 課程簡介+ESP32-IDF環境搭建(虛擬機Linux環境下)

嗶站“宸芯IOT”視頻鏈接 一、課程內容介紹 1.什么是ESP32 ESP32是集成2.4GHz Wi-Fi和藍牙雙模的單芯片方案&#xff0c;具有超高的射頻性能、穩定性、通用性和可靠性&#xff0c;以及超低的功耗&#xff0c;滿足不同的功耗需求&#xff0c;適用于各種應用場景。ESP32是ESP8…

Vue3一個組件綁定多個 v-model,自定義 prop 和 event 名稱

Vue3一個組件綁定多個 v-model&#xff0c;自定義 prop 和 event 名稱 Vue3中v-model默認使用modelValue作為prop&#xff0c;update:modelValue作為事件&#xff0c;而Vue2使用的是value和input。此外&#xff0c;Vue3允許通過參數的方式為組件添加多個v-model綁定&#xff0…

YOLOv11小白的進擊之路(九)創新YOLO11損失函數之NWD損失函數源碼解讀

之前的博客也有對YOLO11的損失函數進行過源碼分析&#xff0c;可以參考&#xff1a;YOLOv11小白的進擊之路&#xff08;六&#xff09;創新YOLO的iou及損失函數時的源碼分析_yolov11的損失函數是什么-CSDN博客最近在做小目標檢測的時候注意到了NWD損失函數&#xff0c;這里對其…

VLN 論文精讀(四)Dynamic Path Navigation for Motion Agents with LLM Reasoning

這篇筆記用來描述2025年發表在arxiv上的一篇有關VLN領域的論文&#xff0c;由港科大和達特茅斯大學聯合發布&#xff0c;其核心思想有以下幾點&#xff1a; 將3D環境轉化為2D平面&#xff1b;2D平面中障礙物分布、機器人起點與終點信息用稀疏矩陣形式進行描述&#xff1b;與LL…

vue3之寫一個aichat ----vite.config.js

vite.config.js的CSS配置 postcss-pxtorem 開發響應式網頁的時候需要用到postcss-pxtorem amfe-flexible amfe-flexible是由阿里團隊開發的一個庫&#xff0c;它可以根據設備的屏幕寬度去動態調整HTML根元素()的字體大小&#xff0c;這意味著無論用戶使用什么尺寸的設備訪問你…

寶石PDF,全新 PC 版本,全部免費

寶石PDF已經運行 3 年時間&#xff0c;有客戶端&#xff0c;小程序&#xff0c;一直未上 PC 版本&#xff0c;隨著客戶端功能升級的不及時&#xff0c;很多用戶建議上 PC 版本。但是飛哥一直忙&#xff0c;這不終于給上了。 同時系統的名稱也從 “PDF云轉換”改為“寶石PDF”&…

.NET8使用EF Core連接SQLite

使用框架 .NET8 在nuget中&#xff0c;需要安裝包&#xff1a; SQLitePCLRaw.bundle_e_sqlite3&#xff0c;版本 2.1.10 Microsoft.EntityFrameworkCore.Sqlite.Core&#xff0c;版本 9.0.0 using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microso…

HTML課后實踐

實驗一 【實驗原理】 在搜索引擎的文本分析中&#xff0c;標題的信息權重要比正文的大&#xff0c;所以標題的樣式非常重要。本實驗通過把標題標記和常規文本進行對比輸出&#xff0c;掌握標題標簽的用法。在網頁中&#xff0c;有時需要為文字設置粗體、斜體或下劃線效果&#…

【紫光同創FPGA開發常用工具】FPGACPLD的下載與固化

文檔內容適配技術問題說明&#xff08;非正文&#xff09;&#xff1a; 1、FPGA&CPLD如何下載位流文件&#xff1b; 2、FPGA外部flash如何固化位流文件&#xff1b; 3、PDS軟件燒錄界面如何新增用戶flash&#xff1b; 4、CPLD內部flash如何固化位流文件&#xff1b; F…

前端傳參+后端接參對照

? Java 后端參數接收注解 & 前端傳參格式對照 后端注解前端 Content-Type前端傳參方式說明RequestParamapplication/x-www-form-urlencodedURL參數 / form表單提交 / Postman form-data常用于 keyvalue 形式的參數&#xff1b;適合少量簡單參數RequestParamURL拼接/api/t…

計算機網絡的框架結構

計算機網絡課程知識體系框架 一、計算機網絡基礎概念 1.1 網絡組成要素 端系統&#xff08;主機、服務器&#xff09;通信鏈路&#xff08;有線/無線介質&#xff09;交換設備&#xff08;路由器、交換機&#xff09;協議體系&#xff08;TCP/IP協議簇&#xff09; 1.2 網絡…

塔能智慧物聯節能方案:點亮城市,賦能工廠

在全球積極倡導節能減排、綠色發展的時代背景下&#xff0c;塔能&#xff08;江蘇&#xff09;科技有限公司憑借其創新的智慧物聯節能一體化解決方案&#xff0c;在城市照明和工廠節能領域取得了顯著成果。該方案不僅為城市的夜晚帶來了明亮且節能的照明&#xff0c;還為工廠的…

Laravel框架下通過DB獲取數據并轉為數組的方法

在Laravel框架中&#xff0c;獲取數據庫信息并將其轉換為數組是一種常見的操作&#xff0c;特別是在處理數據導出、API響應等場景中。Laravel提供了簡潔而強大的數據庫抽象層&#xff0c;旨在簡化這類操作。接下來&#xff0c;我們將探討幾種在Laravel中通過數據庫抽象層&#…

pytorch小記(九):pytorch中創建指定形狀的張量: torch.empty

pytorch小記&#xff08;九&#xff09;&#xff1a;pytorch中創建指定形狀的張量: torch.empty 詳細解釋1. 基本功能2. 語法3. 示例代碼示例 1&#xff1a;創建一個 5 的未初始化張量示例 2&#xff1a;創建一個 23 的未初始化張量示例 3&#xff1a;指定數據類型和設備 4. 注…

Linux cgroup cpuset

cpuset.c 是 Linux cgroup 的 cpuset 子系統的核心實現&#xff0c;這個文件的主要作用是&#xff1a; 實現 cgroup 的 cpuset 子系統管理進程的 CPU 和內存資源訪問權限提供 CPU 和內存節點的獨占功能支持層級化的資源管理提供用戶空間接口來配置和查看資源限制 關鍵數據結構…

Tailwind CSS 學習筆記(一)

一、簡介 Tailwind CSS是一個工具優先的CSS 框架,只需書寫HTML 代碼,無需書寫CSS,即可快速構建美觀的網站。 二、優點 1、簡潔、規整,避免了隨意取類名 Tailwind CSS 的工具類(Utility classes) 能夠為你提供一套約束系統,避免讓你的樣式表中出現隨意的取值。它讓顏色、…