影院购票系统(二)——uni-app移动应用开发

发布于:2025-03-06 ⋅ 阅读:(19) ⋅ 点赞:(0)

这一篇讲解系统的逻辑代码部分,下面是ai的讲解,也可以直接跳到代码部分进行浏览。

一、整体功能概述

这个Vue组件构建了一个完整的影院座位选择系统,涵盖从座位数据初始化、视图渲染到交互处理以及业务逻辑的整个流程。它遵循响应式编程模式,数据的变化能够及时反映在视图上,反之亦然。

二、核心数据结构剖析

  1. seatData二维数组
    • 组件利用Vue的响应式数据模型,定义了seatData这个二维数组,用来表示9x14的影院座位布局。每个元素代表一个座位对象,包含seatState(座位状态,取值为不可选、可选、被选中、空白)、rowIndex(排号 + 1)和colIndex(列号 + 1)属性。通过行列索引,能够以O(1)的复杂度查询座位状态,这与影院座位的物理布局特征相匹配。
  2. selectedSeats数组
    • 用于记录用户选择的座位坐标。在用户操作过程中,这个数组会不断更新,以反映用户的选择状态。
  3. seatPrice和totalPrice
    • seatPrice存储座位的单价,并且保持两位小数精度。totalPrice则实时根据selectedSeats数组的长度和seatPrice计算订单总额。

三、生命周期初始化 - onLoad函数

  1. 座位矩阵构建
    • onLoad生命周期钩子函数中,通过双重循环构建9行14列的座位矩阵。这种方式能够清晰地初始化座位数据结构。
  2. 状态判定 - decideState方法
    • 采用坐标匹配算法对座位状态进行初始化。例如,对于过道等特殊区域(如i = 7j∈[2,13])标记为空白,模拟已售座位(如i = 6的特定列)标记为不可选,其余位置默认为可选状态。不过,目前这种硬编码的方式虽然直观,但可维护性较差。建议将特殊坐标等配置参数化,以便在需要调整布局或座位状态时能够更轻松地进行修改。

四、视图交互方法解析

  1. 视图渲染 - getSeatImage方法
    • 此方法遵循状态驱动视图模式,通过座位的seatState属性映射到相应的静态资源路径。例如,不可选状态返回不可选座位图标,可选状态返回可选座位图标,被选中状态返回选中状态图标,空白状态返回透明占位图。这种方式有效地实现了视图层与数据层的解耦,符合MVVM设计原则,并且使用uni - app的图片组件能够实现跨平台渲染。
  2. 选择逻辑 - selectSeat方法
    • 该方法实现了一个状态切换的三态机逻辑。
      • 当座位处于不可选状态时,触发uni.showToast提示用户,这是一种很好的异常反馈封装方式。
      • 对于空白区域,建立了防误触机制,避免不必要的操作。
      • 可选与被选中状态之间通过状态切换算法完成状态变更。当座位从被选中变为可选时,从selectedSeats数组中移除相应记录,并且总价减42;当座位从可选变为被选中时,添加选中记录到selectedSeats数组,总价加42。然而,在定位已选座位索引时,当前采用线性查找(O(n)复杂度),这在性能上存在一定的瓶颈。建议改用哈希表或者Map结构存储行列组合键(如${row}-${col})来实现O(1)的查找效率。

五、业务逻辑封装解析

  1. 下单验证 - clickOrder方法
    • 该方法通过uni.showModal模态对话框实现二次确认,这符合金融交易安全规范。在成功下单后,执行一系列状态持久化操作,如将选中座位标记为不可选,并清空临时数据。不过,目前在清空selectedSeats数组和重置totalPrice时存在一些问题。
      • 清空selectedSeats数组时,不应采用循环splice的方式,直接赋值为空数组更为简洁高效。
      • 重置totalPrice时,应该使用this.totalPrice = 0,以避免变量作用域错误。
  2. 异常处理
    • 在用户没有选择座位就下单时,采用防御性编程策略,通过uni.showToast组件阻断无效操作,并且通过image参数实现图形化的错误提示信息反馈,提供了较好的用户体验。

六、性能优化建议

  1. 计算属性优化
    • 目前totalPrice采用显式加减运算,这可能会导致金额累加误差。建议改用computed属性,基于selectedSeats.length动态计算totalPrice,以确保金额计算的准确性。
  2. 响应式更新
    • 直接通过索引修改seatData数组元素可能会丢失响应性。建议使用Vue.set或数组splice方法触发视图更新,以确保视图能够正确反映数据的变化。
  3. 状态管理优化
    • 整个组件存在状态管理颗粒度过粗的问题。建议引入Vuex进行全局状态管理,解耦座位数据与视图逻辑。同时,对于大数据量下的渲染性能,可以考虑采用Immutable.js进行优化,提高应用的整体性能。

代码

最终完整代码如下:

<!-- 
 小程序的标题在 pages.json中navigationBarTitleText定义,其中page的是启动页标题,而globalStyle是全局的标题
 -->
<template>
	<view class="content">
		<!-- <image src="../../static/可选座位.png" -->
		<text class="title">屏幕</text>
		<!-- 用v-for去渲染数据,这里得对座位对象的属性进行设计
		 1. 状态:未选、不可选、选中
		 2. 根据状态去更换图片路径
		 并且由于是二维座位数组,所以需要用两个v-for进行渲染,其中row为行索引,col为列索引
		 -->
		<view class="container">
			<view v-for="(row, rowIndex) in seatData" :key="rowIndex" class="seat-row">
				<view v-for="(col, colIndex) in row" :key="colIndex" class="" @click="selectSeat(rowIndex, colIndex)">
					<!-- 在methods设定一个函数来获取座位状态,来显示对应的图标,参数应当是col(座位)对象的seatState属性 -->
					<image :src="getSeatImage(col.seatState)"></image>
				</view>
			</view>
		</view>
		<view class="selected-seats-info">
			<view class="seatAddress">
				<view v-for="(seat, index) in selectedSeats" :key="index" class="seatItem">
					{{seat.selectedRow}}排{{seat.selectedCol}}列
				</view>
			</view>
			<view class="seatMoney">
				<view class="text">购票:{{selectedSeats.length}} 张</view>
				<!-- 这里使用toFixed(2)函数让数字显示出小数点后两位 -->
				<view class="text">单价:¥{{seatPrice.toFixed(2)}}</view>
			</view>
			<!-- 设置点击下单函数 -->
			<button @click="clickOrder" class="btn">¥{{totalPrice.toFixed(2)}} 确认下单</button>
		</view>
		<view class="image">
			<image src='../../static/photo.jpg' mode="widthFix" style="width:230px"></image>
		</view>
	</view>

</template>

<script>
	export default {
		data() {
			return {
				// 在data中定义seatData属性
				seatData: [],
				selectedSeats: [],
				seatPrice: 42.00, // 假设单价为 50 元
				totalPrice: 0
			}
		},
		onLoad() {
			// 定义座位行数和列数
			const rows = 9;
			const cols = 14;
			// 创建座位数组
			const seatData = [];
			// 创建被选中座位
			for (let i = 0; i < rows; i++) {
				// 创建单行数组
				const row = [];
				for (let j = 0; j < cols; j++) {
					// 往单行数组里添加一个对象体
					// 先判断现实位置是否是空白,即无座位
					// 在这里设定一个状态枚举变量,来对应座位的状态

					if (this.decideState(i, j) == '空白') {
						row.push({
							seatState: '空白',
							rowIndex: i + 1,
							colIndex: j + 1
						})
					} else if (this.decideState(i, j) == '不可选') {
						row.push({
							seatState: '不可选',
							rowIndex: i + 1,
							colIndex: j + 1
						})
					} else {
						row.push({
							seatState: '可选',
							rowIndex: i + 1,
							colIndex: j + 1
						})
					}
				}
				// 最后往座位数组添加单行数组
				seatData.push(row);
			}
			// 将seatData数组赋值到当前vue实例的seatData属性
			this.seatData = seatData;
		},
		methods: {
			// 返回图片路径函数
			getSeatImage(seatState) {
				if (seatState == '不可选') {
					return '../../static/不可选座位.png';
				} else if (seatState == '可选') {
					return '../../static/可选座位.png';
				} else if (seatState == '被选中') {
					return '../../static/被选中座位.png';
				} else return '../../static/空白.png';
			},
			// 座位点击函数
			selectSeat(rowIndex, colIndex) {
				if (this.seatData[rowIndex][colIndex].seatState == '不可选') {
					uni.showToast({
						title: '该座位已被选用',
						duration: 1500,
						icon: 'none', // 不显示默认图标
						image: '../../static/错误.png', // 使用自定义图标
						mask: true // 显示透明蒙层,防止触摸穿透
					})
					return;
				}
				if (this.seatData[rowIndex][colIndex].seatState == '空白') {
					return;
				}
				if (this.seatData[rowIndex][colIndex].seatState == '被选中') {
					this.seatData[rowIndex][colIndex].seatState = '可选';
					// 获取该位置在数组中的索引
					let index = -1;
					for(index =0; index < this.selectedSeats.length; index++){
						if(this.selectedSeats[index].selectedRow == rowIndex&&this.selectedSeats[index].selectedCol == colIndex){
							break;
						}
					}
					// 所成功找到索引,则溢出该座位
					if (index !== -1) {
						this.selectedSeats.splice(index, 1);
					}
					this.totalPrice -= 42;

				} else {
					this.seatData[rowIndex][colIndex].seatState = '被选中';
					this.selectedSeats.push({
						selectedRow: rowIndex,
						selectedCol: colIndex
					});
					this.totalPrice += 42;
				}
			},
			// 分辨座位类型函数
			decideState(i, j) {
				if ((i == 7 && j == 2) || (i == 7 && j == 11) || (i == 7 && j == 12) || (i == 7 && j == 13) || (i == 8 &&
						j == 0) || (i == 8 && j == 1) || (i == 8 && j == 2) || (i == 8 && j == 9) || (i == 8 && j == 10) ||
					(i == 8 && j == 11) || (i == 8 && j == 12) || (i == 8 && j == 13)) {
					return '空白';
				} else if ((i == 6 && j == 0) || (i == 6 && j == 1) || (i == 6 && j == 9) || (i == 6 && j == 10) || (i ==
						6 && j == 12) || (i == 6 && j == 13)) {
					return '不可选';
				} else return '';
			},
			// 点击下单函数
			clickOrder() {
				// 判断用户是否有选中座位
				if (this.selectedSeats.length == 0) {
					uni.showToast({
						title: '请先选择座位',
						duration: 1500,
						icon: 'none', // 不显示默认图标
						image: '../../static/错误.png', // 使用自定义图标
						mask: true // 显示透明蒙层,防止触摸穿透
					})
					return;
				}
				uni.showModal({
						title: '操作提示',
						content: '确认购买?',
						showCancel: true, // 是否显示取消按钮(默认true)
						cancelText: '取消', // 取消按钮文字(默认"取消")
						cancelColor: '#999', // 取消按钮文字颜色(默认#000)
						confirmText: '确定', // 确认按钮文字(默认"确定")
						confirmColor: '#007AFF', // 确认按钮颜色(默认#3CC51F)
						success: res => {
							if (res.confirm) {
								console.log('用户确认操作');
								// 执行确认逻辑
								uni.showToast({
									title: '下单成功',
									duration: 1500
								});
								// 将被选座位同步到总座位数组里
								this.selectedSeats.forEach(seat=>{
									let row = seat.selectedRow;
									let col = seat.selectedCol;
									this.seatData[row][col].seatState = '不可选'; 
								});
								// 清空被选中座位
								let leng = this.selectedSeats.length;
								for(let i = 0; i < leng; i++) {
									this.selectedSeats.splice(i, 1);
								}
								// 以及对总价重新赋值
								totalPrice = 0;
							} else if (res.cancel) {
								console.log('用户取消操作');
								// 执行取消逻辑
								return;
							}
						}
					});
				}
			}

		}
</script>

<style>
	.content {
		padding: 10px;
		text-align: center;
	}

	/* text-align属性需要作用于父容器,因此要实现屏幕居中,要对它的父容器进行设置。 */
	.title {
		display: inline-block;
		/* 确保行内元素特性 */
		font-size: 20px;
		margin-bottom: 10px;
	}

	.container {
		padding: 0 10px;
	}

	.seat-row {
		display: flex;
		/* 修改为 flex-start 或 space-between 以减少行内元素间的间距 */
		justify-content: flex-start;
		/* 可以根据需要调整行与行之间的间距 */
		margin-bottom: 5px;
		/* 添加负边距来减少行内元素的间距 */
		margin-left: -5px;
	}

	.seat-row view {
		width: 30px;
		height: 30px;
	}

	.seat-row view image {
		width: 100%;
		height: 100%;
	}

	.selected-seats-info {
		border: 2px solid #ccc;
		border-radius: 10px;
		;
	}

	.seatAddress {
		margin: 10px 20px;
		display: flex;
		/* 子元素按横轴方向顺序排列 */
		flex-direction: row;
		/* 设置从开始方向对齐 */
		justify-content: flex-start;
		/* 设置子元素可放置成多行 */
		flex-wrap: wrap;
	}

	.seatItem {
		font-size: 15px;
		background-color: antiquewhite;
		border: 1px solid antiquewhite;
		border-radius: 3px;
		margin: 2px;
		padding: 2px;
	}

	.seatMoney {
		display: flex;
		justify-content: space-evenly;
		padding: 8px;
		font-size: 18px;
	}
	.btn {
		width:90%;
		background-color: antiquewhite;
		border:1px solid antiquewhite;
		border-radius: 4px;
		margin-bottom:10px;
	}
</style>

网站公告

今日签到

点亮在社区的每一天
去签到