使用了组件开发,在seat页面里引入seatList组件。因为不想再去看怎么从组件中获取数据,因此这整个页面就是一个组件。。。
本文前端部分参考于:微信小程序组件开发——可视化电影选座 - 掘金,在其基础上增加了行标,座位状态数组从一维换成二维,添加了底部信息和按钮。初次开发小程序,错误不妥之处望多指正。
seat_state:座位状态信息,用一个二维数组表示,-1表示没有座位,0表示有且暂未出售,1表示已出售
film:电影信息,包含电影名称等电影基本信息
arrangement:排片信息,包含电影放映时间、地点等信息。其实也包含了seat_state信息,第一个参数seat_state就是从arrangement里取出来的,但是由于在arrangement里,seat_state是String类型的(因为这样方便mybatis给seat_state赋值,不知道mybatis能不能直接赋值复杂类型的?不能的话,怎么让他把数据库里存的String映射成Array呢),所以单独把seat_state拿出来方便一些。
组件的 js文件中需声明传过来的数据,与页面声明方式不同,组件的properties中声明数据需要写出数据类型。
充分利用了wx:if、wx:for、block、view的特点
seatings为座位区;bear为小熊图标区;other为其他信息区
可移动,可双指缩放,包含屏幕、电影厅介绍和座位区,结构如下:
通过遮盖法,实现荧幕弧形效果:长方形盒子构建荧幕长宽,再用一个椭圆形大饼的边缘显示在长方形盒子里,其他部分用overflow: hidden属性遮盖,再调整背景颜色和边框颜色即可实现荧幕效果
较为简单,微调样式即可
基础结构:
基础单位 :设定座位宽度单位为vw,为了便于统一单位和机型配适,将高度也以vw为单位
座位样式 : 基础样式相同,再单独写各自独特的样式。空白座位只要设置边框颜色为透明就可以达到效果。
推荐 :我们用到了一个非常好用的样式 “box-sizing: border-box;”,它为元素设定的宽度和高度决定了元素的边框盒。就是说,为元素指定的任何内边距和边框都将在已设定的宽度和高度内进行绘制,通过从已设定的宽度和高度分别减去边框和内边距才能得到内容的宽度和高度,便于控制元素大小。
.seatNormal, .seatNone, .seatChosen {
height: 4vw;
width: 4vw;
margin: 1vw;
border-radius: 8rpx;
box-sizing: border-box;
}
.seatNormal {
border: 1rpx solid #63c0c0;
}
.seatChosen {
border: 1rpx solid red;
background-color: red;
}
.selected {
border: 1rpx solid #05ca90;
background-color: #05ca90;
}
.seatNone {
border: 1rpx solid rgba(0, 0, 0, 0);
}
/* 1.3 座位区 */
.visual_seatings {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: fit-content;
}
/* 1.3.1 座位行标 */
.row_num_box {
width: fit-content;
height: fit-content;
background-color: #777B88;
display: flex;
flex-direction: column;
justify-content: center;
display: inline-block;
border-radius: 20rpx;
}
.row_num{
/*行序号的一些样式需要与座位相同才能对齐*/
display: flex;
border: 1rpx solid rgba(0, 0, 0, 0);
height: 4vw;
width: 4vw;
margin-top: 33%;
border-radius: 8rpx;
}
/*1.3.2 遍历二维数组*/
.visual_seating {
width: 90%;
height: fit-content;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
/* 设置每行内的座位水平排布 */
.row {
display: flex;
flex-direction: row;
}
包括座位标志、选座排片信息、确认按钮。该区域固定在屏幕底部,距离底部1vh(1%屏幕高度)
/* 3. 其他信息区*/
.other{
width: 100%;
position: fixed;
bottom: 1vh;
justify-content: center;
}
3.1 标识区
样式与座位区域中的座位相同,类名为signs_normal和signs_chosen
/*3.1 座位标志区 */
.seatings_signs {
height: fit-content;
display: flex;
justify-content: center;
}
.signs_normal, .signs_chosen {
height: fit-content;
display: flex;
flex-direction: row;
align-items: center;
margin-right: 20rpx;
}
3.2 排片选座信息区
{{film.film_name}}
今天 {{arrangement.broadcast_time_m_d}} {{arrangement.broadcast_time_h_m}} {{film.duration}}分钟 {{arrangement.hall_type}}
{{item+1}}排
{{item+1}}座
/* 3.2.1 排片信息 */
.info{
width: 100%;
}
.arrangSeat{
width: 96%;
margin-left: 2%;
background-color: white;
border-radius: 30rpx;
}
.arrangementInfoBox{
height: 120rpx;
width: 100%;
margin-top: 10rpx;
border-radius: 30rpx;
display: flex;
flex-direction: column;
}
.filmName{
margin-top: 10rpx;
margin-left: 5%;
}
.arrangementInfo{
margin-top: 10rpx;
margin-left: 5%;
display: flex;
flex-direction: row;
align-items: center;
}
/*3.2.2 选座信息*/
.selectedSeatBox {
width: fit-content;
display: flex;
flex-direction: row;
border-radius: 8rpx;
box-sizing: border-box;
margin-left: 1%;
}
.selectedSeat{
font-size: 25rpx;
background-color: #c9cdd3;
margin: 10rpx;
border-radius: 10rpx;
}
3.3 按钮区
选座数量为0时,显示请先选座按钮,大于0时显示确认选座按钮。确认选座按钮有hover样式。
<view class="buttonBox">
<block wx:if="{{selectedNum>0}}">
<button hover-class="hover" bindtap="toOrderDetails" style="width: 96%;">
¥{{selectedNum*arrangement.real_price}} 确认选座button>
block>
<block wx:else>
<button class="pleaseSelect" style="width: 96%;">请先选座button>
block>
view>
/* 按钮区 */
.buttonBox{
width: 100%;
margin-top: 20rpx;
bottom: 1vh;
display: flex;
justify-content: center;
}
button {
font-size: 35rpx;
background-color: rgb(252, 126, 67);
color: white;
border-radius: 98rpx;
}
/* 按下变颜色 */
.hover {
top: 3rpx;
background: rgb(236, 179, 156);
}
.pleaseSelect {
color:rgb(230, 188, 106);
}
遍历座位状态数组第一层即可。
通过block标签对数组数据双层循环输出, 判断数组数据, 输出不同的格式。注意在第一层循环后再加一个view,第二层循环套在view里面,这样相当于每一行座位都被套在了一个view里面,从而实现换行。
<view class="visual_seatings">
<view class="row_num">
<view class="seatNormals" wx:for="{{seat_state}}" wx:for-index="line_index">
<text style="font-size: 20rpx;margin-left: 3rpx;">{{line_index+1}}text>
view>
view>
<view class="visual_seating">
<block wx:for="{{seat_state}}" wx:key="Index"
wx:for-index="line_index" wx:for-item="line">
<view class="row">
<block wx:for="{{line}}" wx:key="Index"
wx:for-index="column_index" wx:for-item="item">
<view wx:if="{{item == 0}}"
class="seatNormal {{tools.indexOf(selectedIndex, line_index,column_index)?'selected': ''}}"
data-line-index="{{line_index}}"
data-column-index="{{column_index}}"
bindtap="select">
view>
<view wx:elif="{{item == 1}}" class="seatChosen">view>
<view wx:else>
<view class="seatNone">view>
view>
block>
view>
block>
view>
view>
用户点击可选座位, 改变其样式并更新已选座位坐标数组selectedIndex内容
外层循环,利用wx:for-index="line_index"和wx:for-item=“line”,分别记录 行坐标为line_index,外循环item为line;
内层循环,利用wx:for-index="column_index"和wx:for-item=“item”,分别记录 列坐标为column_index,内循环item为item,
则对于每一个可选座位item,通过三元运算符判断tools.indexOf(selectedIndex, line_index,column_index)?‘selected’: ‘’,如果该item坐标已经在selectedIndex里面,则添加类名为空字符串“”,使其样式为未选中状态,否则添加类名为selected,使其样式为选中状态。
每个可选座位item绑定了一个点击事件select,通过data-line-index="{{line_index}}“和 data-column-index=”{{column_index}}",把坐标作为参数传递到js处理
<block wx:for="{{seat_state}}" wx:key="Index"
wx:for-index="line_index" wx:for-item="line">
<block wx:for="{{line}}" wx:key="Index"
wx:for-index="column_index" wx:for-item="item">
<view wx:if="{{item == 0}}"
class="seatNormal {{tools.indexOf(selectedIndex, line_index,column_index)?'selected': ''}}"
data-line-index="{{line_index}}"
data-column-index="{{column_index}}"
bindtap="select">
view>
block>
block>
判断坐标是否在selectedIndex中有些复杂, 所以我们需要在pages同级目录下util文件夹中声明一个indexOf函数供三元运算调用。使用时只需通过进行声明, 就可使用 tools.indexOf 方法。
function indexOf(arr,line_index ,column_index) {
if(arr.length===0) {
return false;
}
for (var index = 0; index < arr.length; index++) {
if((arr[index][0]===line_index)&&(arr[index][1]===column_index)) {
return true;
}
}
return false;
/* 一维数组直接用这个
if (arr.indexOf([line_index,column_index]) < 0) {
return false;
} else {
return true;
}*/
}
module.exports.indexOf = indexOf;
// 1. 添加或减少选中元素数量和下标
select(e) {
console.log(e);
let column_index = e.currentTarget.dataset.columnIndex;
let line_index = e.currentTarget.dataset.lineIndex;
let arr = [line_index,column_index]
//console.log(line_index+":"+column_index)
// let item = "["+line_index+","+column_index+"]";
if(this.indexOf(this.data.selectedIndex,line_index,column_index)!=-1){
let selectedIndex = this.remove(this.data.selectedIndex, arr);
let selectedNum = this.data.selectedNum - 1;
this.setData({
selectedIndex,
selectedNum
})
}else if(this.data.selectedNum<6){
let selectedNum = this.data.selectedNum + 1;
let seatList = this.data.selectedIndex;
let array = [seatList.length+1];
for (let index = 0; index < seatList.length; index++) {
array[index] = seatList[index];
}
array[seatList.length] = arr;
//let selectedIndex = this.data.selectedIndex.concat(arr);//增加元素
this.setData({
selectedIndex : array,
selectedNum
})
} else {
wx.showToast({
title: '一次最多购买6张票',
icon: "none"
})
}
console.log(this.data.selectedIndex)
},
//2. 从二维数组中删除元素
remove(arr, ele) {
var index = this.indexOf(arr,ele[0],ele[1]);
if (index > -1) {
arr.splice(index, 1);
}
return arr;
},
//3. 获取二维数组selectedIndex中元素[line_index,column_index]的下标,若不存在则返回-1
indexOf(arr,line_index ,column_index) {
if(arr.length===0) {
return -1;
}
for (var index = 0; index < arr.length; index++) {
if((arr[index][0]===line_index)&&(arr[index][1]===column_index)) {
return index;
}
}
return -1;
},
点击确认选座,将选座信息提交后台,检查座位是否已经被占。
toOrderDetails(e) {
//发送请求的参数
var user_id = app.globalData.userInfo.id;
var film_id = this.properties.arrangement.film_id;
var hall_id = this.properties.arrangement.hall_id;
var arrangement_id = this.properties.arrangement.id;
var seatSelectedArr = this.data.selectedIndex;
var that = this;
wx.request({
url: app.globalData.server+'/order/addOrder',
//服务器的地址,现在微信小程序只支持https请求,所以调试的时候请勾选不校监安全域名
data: {
user_id: user_id,
film_id: film_id,
hall_id: hall_id,
arrangement_id:arrangement_id,
seatArr: seatSelectedArr
},
header: {
'content-type': 'application/json'
},
//请求成功,响应处理
success: function (res) {
var orderInfo = res.data.orderInfo
var available = res.data.available
var occupied = res.data.occupied
var arrangement = that.properties.arrangement
var json = JSON.stringify(arrangement)
var orderInfoJson = JSON.stringify(orderInfo)
console.log(res.data);
if(occupied!==-1) {//有座被占
wx.showModal({
title: '提示',
duration: 2000,
content: occupied+"已被购买,请重新选择"
})
that.refresh()
}else{//没有座位被占
//跳转
that.goToOrderdetails(orderInfoJson,json);
}
},
fail:(error)=>{
console.log("wx.request失败==>"+error)
}
})//wx.request的结尾
},
//跳转到订单详情界面
goToOrderdetails(orderInfoJson,arrangementInfoJson){
wx.navigateTo({
url: '../../pages/orderdetails/orderdetails?orderInfo=' + orderInfoJson + '&arrangement=' + arrangementInfoJson
})
},
后端判断座位是否有被占的,有则直接返回,没有则继续生成订单
//下单,该方法只用了synchronized来处理同步问题,需要改进
@ResponseBody
@RequestMapping(value = "/addOrder",produces = "application/json;charset=UTF-8")
public synchronized String addOrder(HttpServletRequest request) {
int user_id = Integer.parseInt(request.getParameter("user_id"));
int film_id = Integer.parseInt(request.getParameter("film_id"));
int arrangement_id = Integer.parseInt(request.getParameter("arrangement_id"));
int hall_id = Integer.parseInt(request.getParameter("hall_id"));
String seatSelectedArr = request.getParameter("seatArr");//[[2,3],[1,3]]
//将要返回的信息初始化
Map<String,Object> map = new HashMap<>();
String occupied = "";//被占座位
String not_exist = "";//不存在或者暂停使用的座位
try{
Arrangement arrangement = arrangementService.queryArrangementById(arrangement_id);
//如果剩余座位数量大于0再进一步操作和判断
if(arrangement.getSeat_remain()>0) {
int ticket_num = 0;//票数
int seatRemain = arrangement.getSeat_remain();//剩余座位数
//json字符串转二维数组
int[][] select = StringUtil.jsonToTwoArr(seatSelectedArr);
int[][] state = StringUtil.jsonToTwoArr(arrangement.getSeat_state());
//用于记录空余座位
List<String> available = new ArrayList<>();
//检查座位状态,有空余则更新一些信息
for (int[] ints : select) {
//注意:select存储的是数组的下标
int line = ints[0];
int column = ints[1];
int theState = state[line][column];
if (theState == Constant.seat_struct_OCCUPIED) {
occupied = occupied + "," + (line+1) + "排" + (column+1) + "座";
} else if (theState == Constant.seat_struct_DOSE_NOT_EXIST) {
not_exist = not_exist + "," + (line+1) + "排" + (column+1) + "座";
} else if (theState == Constant.seat_struct_UNOCCUPIED) {
//记录空余座位
available.add((line+1) + "排" + (column+1) + "座");
//剩余座位-1
seatRemain--;
//座位状态更新
state[line][column] = Constant.seat_struct_OCCUPIED;
//累计票数
ticket_num++;
}
}
//座位State解析结果存入map
if (not_exist.length()>0)
map.put("not_exist",not_exist.substring(1));
else map.put("not_exist",-1);
if (occupied.length()>0) {
map.put("occupied",occupied.substring(1));
return JSONObject.toJSONString(map);
}
else map.put("occupied",-1);
//ticket_num>0说明有座位是空的
if (ticket_num>0) {
//更新arrangement信息
film.setId(film_id);
arrangement.setFilm(film);
hall.setId(hall_id);
arrangement.setHall(hall);
arrangement.setSeat_remain(seatRemain);
arrangement.setSeat_state(Arrays.deepToString(state));
arrangementService.updateArrangement(arrangement);
//添加订单
//获取当前表里最大订单号,返回为0表示当前表里没有记录
int next_id = orderService.getMaxOrderId()+1;
order.setId(next_id);
user.setId(user_id);
order.setUser(user);
order.setArrangement(arrangement);
order.setTicket_num(ticket_num);
order.setPrice(ticket_num*arrangement.getReal_price());
order.setSeat(available.toString());
order.setIs_out_of_time(Constant.is_out_of_date_NO);
order.setPay_state(Constant.pay_state_NO);
Timestamp d = new Timestamp(System.currentTimeMillis());
order.setCreate_time(d.toString());
System.out.println("order=" + order.toString());
orderService.addOrder(order);
//存入返回信息
map.put("available",available);
orderInfo = orderInfoService.queryOrderInfoByOrderId(order.getId());
System.out.println(orderInfo);
map.put("orderInfo",orderInfo);//这里要返回orderinfo对象才是
}else{
map.put("available",-1);
map.put("orderInfo",-1);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return JSONObject.toJSONString(map);
}
有被占的,就提示用户重新选座,并且执行refresh方法,从后台取出新的座位状态信息,从而刷新页面。
tips: 注意这里不能直接用上一次检查是否被占的结果里被占的座位更新状态,因为可能其他的座位也被占了,只不过用户没有选到,而后台只会检查用户选择的座位是否已经被占。如果在此时检查其他没有选择的座位是否被占的话,逻辑相对复杂,在后面直接再查一次简单直接。(缺点是要多查一次数据库,还要多发一次请求,诶,是不是可以在检查用户选择座位是否被占的时候,使如果有被占,那么直接返回新的座位状态呢?这样能少发一次请求)
刷新页面的这里踩坑了。首先我试了下,直接对组件seatList.js里直接对其参数重新赋值,
var seat_state = JSON.parse(res.data.arrangement.seat_state)
//因为seat_state是个字符串,如果不把seat_state转成JSON的话,
//遍历的时候不对,比如本来只有12个行序号,结果变成300多个了(12*15+逗号+括号)
that.properties.arrangement = res.data.arrangement
that.properties.seat_state = seat_state
结果是,只有第一次提示重新选择时,页面刷新了。后面再试,页面没有刷新,但是arrangement和seat_state的值是改变了的,不知道什么原因。
因此我干了这个,把seat_state赋值给变量seatState,用seatState渲染页面
然而这里如果我不加下面这句话,那么虽然输出显示已经seatState已经更新了,但是页面还是没有重新渲染。而用setData给seatState赋值后才能刷新页面。不知道什么原因。
最终refresh方法为:
//刷新界面信息
refresh(){
//1. 请求新的arrangement信息,覆盖组件的arrangement,seat_state
var that = this;
wx.request({
url: app.globalData.server+'/arrangement/queryArrangementById',
data: {
arrangement_id:that.properties.arrangement.id,
},
header: {
'content-type': 'application/json'
},
//请求成功,响应处理
success: function (res) {
console.log(res.data)
console.log(res.data.arrangement)
console.log(res.data.arrangement.seat_state)
that.properties.arrangement = res.data.arrangement
var seat_state = JSON.parse(res.data.arrangement.seat_state)
//因为seat_state是个字符串,如果不把seat_state转成JSON的话,遍历的时候不对,比如本来只有12个行序号,结果变成300多个了(12*15+逗号+括号)
that.properties.seat_state = seat_state
console.log("seat_state:"+seat_state)
that.setData({
selectedIndex: [],
selectedNum: 0,
seatState:seat_state
})
console.log("seatState:"+that.data.seat_state)
},
fail:(error)=>{
console.log("刷新失败==>"+error)
}
})//wx.request的结尾
}
没有被占的,直接生成订单,将订单信息和排片信息作为参数传递到订单详情页面,等待用户确认付款