微信小程序影院选座界面前后端

选座界面

使用了组件开发,在seat页面里引入seatList组件。因为不想再去看怎么从组件中获取数据,因此这整个页面就是一个组件。。。

本文前端部分参考于:微信小程序组件开发——可视化电影选座 - 掘金,在其基础上增加了行标,座位状态数组从一维换成二维,添加了底部信息和按钮。初次开发小程序,错误不妥之处望多指正。

一、效果图

微信小程序影院选座界面前后端_第1张图片

二、组件参数

image-20220306202458759

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中声明数据需要写出数据类型。

微信小程序影院选座界面前后端_第2张图片

三、 组件页面布局

充分利用了wx:if、wx:for、block、view的特点

1.总体布局

微信小程序影院选座界面前后端_第3张图片
seatings为座位区;bear为小熊图标区;other为其他信息区

2.座位区

可移动,可双指缩放,包含屏幕、电影厅介绍和座位区,结构如下:
微信小程序影院选座界面前后端_第4张图片

微信小程序影院选座界面前后端_第5张图片
2.1 电影屏幕:

通过遮盖法,实现荧幕弧形效果:长方形盒子构建荧幕长宽,再用一个椭圆形大饼的边缘显示在长方形盒子里,其他部分用overflow: hidden属性遮盖,再调整背景颜色和边框颜色即可实现荧幕效果

img
2.2 电影厅介绍:

较为简单,微调样式即可

2.3 座位区域:

基础结构:

微信小程序影院选座界面前后端_第6张图片
  1. 基础单位 :设定座位宽度单位为vw,为了便于统一单位和机型配适,将高度也以vw为单位

  2. 座位样式 : 基础样式相同,再单独写各自独特的样式。空白座位只要设置边框颜色为透明就可以达到效果。

    推荐 :我们用到了一个非常好用的样式 “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. 可移动缩放区域: 由于方便座位选择,我们设定座位区域可移动,并可通过双指缩放的,所以我们需要用到微信小程序的一个API:movable-area和 movable-view。
  • movable-area: 这个区域必须设置width和height属性,不设置则默认为10px,同时,当movable-view小于movable-area时,movable-view的移动范围是在movable-area内, 当movable-view大于movable-area时,movable-view的移动范围必须包含movable-area(x轴方向和y轴方向分开考虑)。
  • movable-view: 标签属性设定移动方向全方位direction=“all”;支持双指缩放scale="{undefined{true}}";最大最小缩放倍数scale-min="0.3"和scale-max=“1.5”;如果想绑定触发条件还可以添加绑定方法,拖动绑定事件:bindchange,缩放绑定事件bindscale等等
  • 注意: 由于movable-view区域在放大时, 所处的x, y坐标不变会导致view区域会超出area区域. 为了view可移动区域不遮挡上方元素, 所以可采取减少放大倍数上限并可在上方添加一些空白区域, 增加页面美观性.
    详细用法参考微信小程序官网手册: developers.weixin.qq.com/miniprogram
  1. 座位区域 :座位区域使用的是弹性布局display:flex,大盒子包住一个盒子,让里面的盒子弹性居中,达到整体居中的效果。在第一层循环后又加了一个view,第二层循环套在view里面,这样相当于每一行座位都被套在了一个view里面,从而实现了换行(wxml代码见3组件业务逻辑)。行序号的一些样式需要与座位相同才能对齐
/* 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;  
}

3.其他信息区

包括座位标志、选座排片信息、确认按钮。该区域固定在屏幕底部,距离底部1vh(1%屏幕高度)

微信小程序影院选座界面前后端_第7张图片
微信小程序影院选座界面前后端_第8张图片
/* 3. 其他信息区*/
.other{
  width: 100%;
  position: fixed;
  bottom: 1vh;
  justify-content: center;
}

3.1 标识区

样式与座位区域中的座位相同,类名为signs_normal和signs_chosen

image-20220307111833248
/*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);
}

四、 组件业务逻辑

1. 座位行标

​ 遍历座位状态数组第一层即可。

2. 座位数据输出

​ 通过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>

3. 选择座位

​ 用户点击可选座位, 改变其样式并更新已选座位坐标数组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>
3.1 已选样式改变

判断坐标是否在selectedIndex中有些复杂, 所以我们需要在pages同级目录下util文件夹中声明一个indexOf函数供三元运算调用。使用时只需通过进行声明, 就可使用 tools.indexOf 方法。

微信小程序影院选座界面前后端_第9张图片

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;
3.2 selected方法
  1. 判断下标数组selectedIndex中是否存在该元素,
  2. 存在则说明此前座位是选中状态,现在再次点击即是取消选中, 则将其从selectedIndex中删除,选中座位数selectedNum减一 ; 不存在则说明用户想要再选一个座位,判断selectedNum是否小于6,小于则将坐标存入selectedIndex中, selectedNum加一, 不小于则提示最多选择六张票;
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DUfsrPnV-1646794511652)(#pic_center)]
微信小程序影院选座界面前后端_第10张图片
// 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;
    },

4. 提交选座信息,准备生成订单

​ 点击确认选座,将选座信息提交后台,检查座位是否已经被占。

4.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);
}
4.2 有被占的

​ 有被占的,就提示用户重新选座,并且执行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渲染页面

微信小程序影院选座界面前后端_第11张图片

然而这里如果我不加下面这句话,那么虽然输出显示已经seatState已经更新了,但是页面还是没有重新渲染。而用setData给seatState赋值后才能刷新页面。不知道什么原因

微信小程序影院选座界面前后端_第12张图片
微信小程序影院选座界面前后端_第13张图片

最终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的结尾
    }
4.3 没有被占的

​ 没有被占的,直接生成订单,将订单信息和排片信息作为参数传递到订单详情页面,等待用户确认付款

你可能感兴趣的:(微信小程序,微信小程序,前端,小程序)