这一篇讲解系统的逻辑代码部分,下面是ai的讲解,也可以直接跳到代码部分进行浏览。
一、整体功能概述
这个Vue组件构建了一个完整的影院座位选择系统,涵盖从座位数据初始化、视图渲染到交互处理以及业务逻辑的整个流程。它遵循响应式编程模式,数据的变化能够及时反映在视图上,反之亦然。
二、核心数据结构剖析
- seatData二维数组
- 组件利用Vue的响应式数据模型,定义了
seatData
这个二维数组,用来表示9x14的影院座位布局。每个元素代表一个座位对象,包含seatState
(座位状态,取值为不可选、可选、被选中、空白)、rowIndex
(排号 + 1)和colIndex
(列号 + 1)属性。通过行列索引,能够以O(1)的复杂度查询座位状态,这与影院座位的物理布局特征相匹配。
- 组件利用Vue的响应式数据模型,定义了
- selectedSeats数组
- 用于记录用户选择的座位坐标。在用户操作过程中,这个数组会不断更新,以反映用户的选择状态。
- seatPrice和totalPrice
seatPrice
存储座位的单价,并且保持两位小数精度。totalPrice
则实时根据selectedSeats
数组的长度和seatPrice
计算订单总额。
三、生命周期初始化 - onLoad函数
- 座位矩阵构建
- 在
onLoad
生命周期钩子函数中,通过双重循环构建9行14列的座位矩阵。这种方式能够清晰地初始化座位数据结构。
- 在
- 状态判定 - decideState方法
- 采用坐标匹配算法对座位状态进行初始化。例如,对于过道等特殊区域(如
i = 7
且j∈[2,13]
)标记为空白,模拟已售座位(如i = 6
的特定列)标记为不可选,其余位置默认为可选状态。不过,目前这种硬编码的方式虽然直观,但可维护性较差。建议将特殊坐标等配置参数化,以便在需要调整布局或座位状态时能够更轻松地进行修改。
- 采用坐标匹配算法对座位状态进行初始化。例如,对于过道等特殊区域(如
四、视图交互方法解析
- 视图渲染 - getSeatImage方法
- 此方法遵循状态驱动视图模式,通过座位的
seatState
属性映射到相应的静态资源路径。例如,不可选状态返回不可选座位图标,可选状态返回可选座位图标,被选中状态返回选中状态图标,空白状态返回透明占位图。这种方式有效地实现了视图层与数据层的解耦,符合MVVM设计原则,并且使用uni - app的图片组件能够实现跨平台渲染。
- 此方法遵循状态驱动视图模式,通过座位的
- 选择逻辑 - selectSeat方法
- 该方法实现了一个状态切换的三态机逻辑。
- 当座位处于不可选状态时,触发
uni.showToast
提示用户,这是一种很好的异常反馈封装方式。 - 对于空白区域,建立了防误触机制,避免不必要的操作。
- 可选与被选中状态之间通过状态切换算法完成状态变更。当座位从被选中变为可选时,从
selectedSeats
数组中移除相应记录,并且总价减42;当座位从可选变为被选中时,添加选中记录到selectedSeats
数组,总价加42。然而,在定位已选座位索引时,当前采用线性查找(O(n)复杂度),这在性能上存在一定的瓶颈。建议改用哈希表或者Map结构存储行列组合键(如${row}-${col}
)来实现O(1)的查找效率。
- 当座位处于不可选状态时,触发
- 该方法实现了一个状态切换的三态机逻辑。
五、业务逻辑封装解析
- 下单验证 - clickOrder方法
- 该方法通过
uni.showModal
模态对话框实现二次确认,这符合金融交易安全规范。在成功下单后,执行一系列状态持久化操作,如将选中座位标记为不可选,并清空临时数据。不过,目前在清空selectedSeats
数组和重置totalPrice
时存在一些问题。- 清空
selectedSeats
数组时,不应采用循环splice
的方式,直接赋值为空数组更为简洁高效。 - 重置
totalPrice
时,应该使用this.totalPrice = 0
,以避免变量作用域错误。
- 清空
- 该方法通过
- 异常处理
- 在用户没有选择座位就下单时,采用防御性编程策略,通过
uni.showToast
组件阻断无效操作,并且通过image
参数实现图形化的错误提示信息反馈,提供了较好的用户体验。
- 在用户没有选择座位就下单时,采用防御性编程策略,通过
六、性能优化建议
- 计算属性优化
- 目前
totalPrice
采用显式加减运算,这可能会导致金额累加误差。建议改用computed
属性,基于selectedSeats.length
动态计算totalPrice
,以确保金额计算的准确性。
- 目前
- 响应式更新
- 直接通过索引修改
seatData
数组元素可能会丢失响应性。建议使用Vue.set
或数组splice
方法触发视图更新,以确保视图能够正确反映数据的变化。
- 直接通过索引修改
- 状态管理优化
- 整个组件存在状态管理颗粒度过粗的问题。建议引入
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>