Open chenxiaochun opened 6 years ago
最近开发京东旅行的一个买票选择座位功能,当你订票时选择的是送票上门,可以在线选择座位位置,因为每个订单最多只能预订5位乘客(包括儿童)的票,因此每种座位类型的选择逻辑如下:
一排显示三个座位,乘客人数大于3时,需要显示两排座位
一排显示五个座位,可以任意选择
只允许乘客选择靠窗位置的座位数
只允许乘客选择下铺的数目,并且乘客数大于等于2时,不能保证会在同一包厢(不是车厢)
需求已经写清楚了,我先模拟一个高铁在线选座的界面出来:
座位类型: <select id="ticketType"> <option value="P">商务座/一等座</option> <option value="M">二等座</option> <option value="1">硬座</option> <option value="2">硬卧/软卧</option> </select> 乘客数: <input type="number" step=0 min=0 max=5 step=1 value=0 id="passenger"> 选择数: <span id="ticketInfo"></span> <div class="seat-box"></div>
#passenger{ width: 80px; } .seat-row{ padding: 10px; } .seat{ display: inline-block; width: 42px; height: 36px; line-height: 33px; text-align: center; color: #666; background-image: url(//img20.360buyimg.com/uba/jfs/t7282/74/1658256725/1195/7e270fe2/599e6afbN59d51b35.png); background-repeat: no-repeat; margin-left: 10px; cursor: pointer; } .seat-chosen{ background-image: url(//img30.360buyimg.com/uba/jfs/t7588/281/1633648098/1268/b1d85178/599e6bbcNfb1b597a.png) }
我首先创建了一个类ChooseSeat,init方法用来担任路由的角色,根据传入的seatType调用相应的功能方法,将计算完之后的座位模版插入到页面中;bind方法用来给页面中的所有的dom操作绑定事件;setTicketInfo方法用来更新页面中展示的已选座位数与乘客数。
ChooseSeat
init
seatType
bind
dom
setTicketInfo
class ChooseSeat{ constructor(seatType, totalNum){ this.seatType = seatType //座位类型 this.totalNum = totalNum //乘客人数 this.seatBox = document.querySelector('.seat-box') this.initSeat() this.bind() this.setTicketInfo() } initSeat(){ var tpl = '' switch(this.seatType){ case 'P': tpl = this.shangWuZuo() break case 'M': tpl = this.erDengZuo() break case '1': tpl = this.yingZuo() break case '2': tpl = this.woPu() break } this.seatBox.innerHTML = tpl } bind(){ } setTicketInfo(){ } shangWuZuo(){ } erDengZuo(){ } yingZuo(){ } woPu(){ } } new ChooseSeat('M', 0)
通过需求描述可以发现,不管你预订的是高铁几等座,共同点都是:点击座位预订,再次点击就是取消。所以,我决定先从这个点入手。
class ChooseSeat{ constructor(seatType, totalNum){ this.hasChosen = [] this.seatBox = document.querySelector('.seat-box') } bind(){ var self = this self.seatBox.onclick = (event) => { var target = event.target if(target.classList.contains('seat')){ let dataSeat = target.getAttribute('data-seat') //如果当前选择的座位不在数组中,而且乘客数(非零)小于等于已选择座位数,则将最早选择的座位删掉,然后再把当前选择的座位push到数组中 if(!self.hasChosen.includes(dataSeat)){ if(self.totalNum <= self.hasChosen.length){ self.hasChosen.splice(0, 1) } if(self.totalNum != 0){ self.hasChosen.push(dataSeat) } } //如果当前选择的座位存在于数组中,则找出来,将其删掉 else{ let index = self.hasChosen.findIndex(value => { return value == dataSeat }) self.hasChosen.splice(index, 1) } self.initSeat() self.setTicketInfo() } } } } new ChooseSeat('M', 0)
后选择的座位会顶替掉最早选择的座位;删除时,也是优先删除最早选择的座位。这明显是一个先进先出的队列操作,聪明的你一定想到了可以用数组来模拟这个数据结构。所以我在constructor中定义一个hasChosen数组,用来存放当前所有已经选择的座位。
constructor
hasChosen
利用事件委托在seat-box上监听onclick事件,保证每次座位类型改变之后,座位依然可以进行点击。每次触发click事件,我都使用classList对象的contains方法来确定点击的是否为seat,再进行下一步处理。
seat-box
onclick
click
classList
contains
seat
选择一个座位无非两种情况:
如果当前选择的座位不在hasChosen中,而且乘客数(非零)小于等于已选择座位数,则将最早选择的座位删掉,然后再把当前选择的座位push到数组中。
如果当前选择的座位存在于hasChosen中,使用findIndex方法定位其索引,再使用splice方法将其删除。
findIndex
splice
贯穿整个代码结构的思想逻辑是:每次根据用户的选择情况,处理完hasChosen中的数据之后,再去重新调用initSeat方法渲染页面上的模版。以达到数据处理与dom操作分离的目的。
initSeat
第二个比较容易实现的功能是选择高铁二等座位,因为它只有一排,模版每次只需要根据hasChosen中的数据进行渲染即可。
row数组代表座位编号,每次erDengZuo方法被调用时,都会遍历此数组,判断每一个座位编号是否存在于hasChosen中。如果存在,就给其加上seat-chosen样式,否则,就按默认样式渲染。
row
erDengZuo
seat-chosen
class ChooseSeat{ constructor(seatType, totalNum){ this.hasChosen = [] this.seatBox = document.querySelector('.seat-box') } bind(){ self.seatBox.onclick = (event) => { var target = event.target if(target.classList.contains('seat')){ let dataSeat = target.getAttribute('data-seat') //如果当前选择的座位不在数组中,而且乘客数(非零)小于等于已选择座位数,则将最早选择的座位删掉,然后再把当前选择的座位push到数组中 if(!self.hasChosen.includes(dataSeat)){ if(self.totalNum <= self.hasChosen.length){ self.hasChosen.splice(0, 1) } if(self.totalNum != 0){ self.hasChosen.push(dataSeat) } } //如果当前选择的座位存在于数组中,则找出来,将其删掉 else{ let index = self.hasChosen.findIndex(value => { return value == dataSeat }) self.hasChosen.splice(index, 1) } self.initSeat() self.setTicketInfo() } } } erDengZuo(){ var tpl = '' var row = ['A', 'B', 'C', 'D', 'E'] for(let r of row){ if(this.hasChosen.includes(r)){ tpl += `<span class="seat seat-chosen" data-seat="${r}">${r}</span>` }else{ tpl += `<span class="seat" data-seat="${r}">${r}</span>` } } return `<div class="seat-row">${tpl}</div>` } } new ChooseSeat('M', 0)
SWRow1表示商务座位/一等座位的第一排,SWRow2表示商务座位/一等座位的第二排。ticketType表示选择的座位类型,passenger表示乘客数目。
SWRow1
SWRow2
ticketType
passenger
ticketType的切换说明用户改变了座位的类型,需要重新订票选择,所以我需要把已经选择的座位清空、重新初始化座位模版、更新显示的乘客与已选择座位数目:
self.hasChosen = [] self.initSeat() self.setTicketInfo()
passenger的切换说明用户改变了需要订票的乘客数。那首先应该处理的是,如果当前乘客数小于已选择座位数的时候,需要将最早选择的座位删掉。
这里需要解释一下为什么是“小于”,比如场景如下:
接下来处理棘手的显示两排座位的场景,这又分两种情况:
我定义了一个totalNum变量用来存放上一次的乘客数,this.value存放的是当前乘客数,所以当totalNum=4,并且this.value=3的时候,用户肯定是减少了乘客数目。(ps:4和3是乘客数目的一个临界点,只有这个时候才会有一排和两排切换的问题)。
totalNum
this.value
totalNum=4
this.value=3
取出hasChosen[0]的值,也就是最早选择的那个座位和C进行比较,小于等于C的肯定是A, B, C,否则就是D, E, F。这样就能知道是需要删除哪一排了,这也是为什么上面会把两排座位分开存放。
hasChosen[0]
C
A, B, C
D, E, F
class ChooseSeat{ constructor(seatType, totalNum){ this.seatType = seatType this.totalNum = totalNum this.hasChosen = [] this.SWRow1 = ['A', 'B', 'C'] //商务座位/一等座位的第一排 this.SWRow2 = ['D', 'E', 'F'] //商务座位/一等座位的第二排 this.seatBox = document.querySelector('.seat-box') this.ticketInfo = document.getElementById('ticketInfo') this.initSeat() this.bind() this.setTickeInfo() } initSeat(){ ... } bind(){ var self = this var seats = document.querySelectorAll('.seat') var ticketType = document.getElementById('ticketType') var passenger = document.getElementById('passenger') ticketType.onchange = function(){ self.seatType = this.value self.hasChosen = [] self.initSeat() self.setTickeInfo() } passenger.onchange = function(){ var seatEl = '' //当前乘客数小于已选择座位数的时候,将最早选择的座位删掉 if(this.value < self.hasChosen.length){ seatEl = self.hasChosen.splice(0, 1) } /** * 因为只有商务座/一等座可能会显示两排座位,所以需要特殊处理 * 当前乘客数小于等于3时,并且座位显示了两排时,必须删除一排 */ if(this.value == 3 && self.totalNum == 4 && self.seatType == 'P'){ if(!seatEl){ seatEl = self.hasChosen[0] } if(seatEl <= 'C'){ removeSeat(self.SWRow1) }else{ removeSeat(self.SWRow2) } } function removeSeat(seats){ for(let s of seats){ let index = self.hasChosen.findIndex(value => { return value == s }) if(index != -1){ self.hasChosen.splice(index, 1) } } } self.totalNum = this.value self.initSeat() self.setTicketInfo() } } setTicketInfo(){ this.ticketInfo.innerHTML = `${this.hasChosen.length}/${this.totalNum}` } } new ChooseSeat('P', 0)
处理完数据,接下来就是使用数据来渲染模版了。
hasChosen.length = 0
hasChosen.length != 0
class ChooseSeat{ shangWuZuo(){ var self = this var tpl = '' function seatGen(row){ var seats = '' for(let r of row){ if(self.hasChosen.includes(r)){ seats += `<span class="seat seat-chosen" data-seat="${r}">${r}</span>` }else{ seats += `<span class="seat" data-seat="${r}">${r}</span>` } } return `<div class="seat-row">${seats}</div>` } if(this.hasChosen.length == 0){ if(this.totalNum <= 3){ tpl = seatGen(this.SWRow1) } else{ tpl = seatGen(this.SWRow1) tpl += seatGen(this.SWRow2) } } else{ if(this.totalNum <= 3){ let seatEl = this.hasChosen[0] if(this.SWRow1.includes(seatEl)){ tpl = seatGen(this.SWRow1) } else{ tpl = seatGen(this.SWRow2) } } else{ tpl = seatGen(this.SWRow1) tpl += seatGen(this.SWRow2) } } return tpl } } new ChooseSeat('P', 0)
对于这两种座位类型,只能是选择一个数量:上限是不能大于乘客数目,也不能大于5;下限是不能小于0即可。所以,它的操作界面是这样的:
class ChooseSeat{ constructor(seatType, totalNum){ ... } initSeat(){ ... } bind(){ var self = this var seats = document.querySelectorAll('.seat') var ticketType = document.getElementById('ticketType') var passenger = document.getElementById('passenger') self.seatBox.onclick = (event) => { var target = event.target ... //处理硬座、硬卧、软卧 if(this.seatType == '1' || this.seatType == '2'){ let ticketNum = document.getElementById('ticketNum') let n = parseInt(ticketNum.value) if(target.classList.contains('increase')){ if(n < 5 && n < self.totalNum){ ticketNum.value = ++n } } if(target.classList.contains('reduce')){ if(n > 0){ ticketNum.value = --n } } } } ... } yingZuo(){ var tpl = ` <button class="reduce">-</button> <input id="ticketNum" type="number" value=0 min=0 max=5 step=1 /> <button class="increase">+</button> ` return tpl } } new ChooseSeat('1', 0)
好了,终于写完了。这几种座位类型的选择功能,最容易实现的是硬座/卧铺,其次是高铁二等座,最痛苦的就是高铁商务座/一等座。因为它涉及到了两排座位的各种增删,曾经思考这个逻辑感觉都要精神分裂了。
不过,任何复杂的逻辑都是由多个简单的小逻辑组成。所以,学会能够清晰的把各个小逻辑拆分出来、实现它,并组合到一起才是最重要的。
https://codepen.io/sjzcxc/pen/MvXNrG
需求描述
最近开发京东旅行的一个买票选择座位功能,当你订票时选择的是送票上门,可以在线选择座位位置,因为每个订单最多只能预订5位乘客(包括儿童)的票,因此每种座位类型的选择逻辑如下:
一排显示三个座位,乘客人数大于3时,需要显示两排座位
一排显示五个座位,可以任意选择
只允许乘客选择靠窗位置的座位数
只允许乘客选择下铺的数目,并且乘客数大于等于2时,不能保证会在同一包厢(不是车厢)
这几种座位之间的共同点是:
功能实现
需求已经写清楚了,我先模拟一个高铁在线选座的界面出来:
初始化代码结构
我首先创建了一个类
ChooseSeat
,init
方法用来担任路由的角色,根据传入的seatType
调用相应的功能方法,将计算完之后的座位模版插入到页面中;bind
方法用来给页面中的所有的dom
操作绑定事件;setTicketInfo
方法用来更新页面中展示的已选座位数与乘客数。从共同点入手
通过需求描述可以发现,不管你预订的是高铁几等座,共同点都是:点击座位预订,再次点击就是取消。所以,我决定先从这个点入手。
后选择的座位会顶替掉最早选择的座位;删除时,也是优先删除最早选择的座位。这明显是一个先进先出的队列操作,聪明的你一定想到了可以用数组来模拟这个数据结构。所以我在
constructor
中定义一个hasChosen
数组,用来存放当前所有已经选择的座位。利用事件委托在
seat-box
上监听onclick
事件,保证每次座位类型改变之后,座位依然可以进行点击。每次触发click
事件,我都使用classList
对象的contains
方法来确定点击的是否为seat
,再进行下一步处理。选择一个座位无非两种情况:
如果当前选择的座位不在
hasChosen
中,而且乘客数(非零)小于等于已选择座位数,则将最早选择的座位删掉,然后再把当前选择的座位push到数组中。如果当前选择的座位存在于
hasChosen
中,使用findIndex
方法定位其索引,再使用splice
方法将其删除。贯穿整个代码结构的思想逻辑是:每次根据用户的选择情况,处理完
hasChosen
中的数据之后,再去重新调用initSeat
方法渲染页面上的模版。以达到数据处理与dom
操作分离的目的。高铁二等座位
第二个比较容易实现的功能是选择高铁二等座位,因为它只有一排,模版每次只需要根据
hasChosen
中的数据进行渲染即可。row
数组代表座位编号,每次erDengZuo
方法被调用时,都会遍历此数组,判断每一个座位编号是否存在于hasChosen
中。如果存在,就给其加上seat-chosen
样式,否则,就按默认样式渲染。高铁商务座位/一等座位
SWRow1
表示商务座位/一等座位的第一排,SWRow2
表示商务座位/一等座位的第二排。ticketType
表示选择的座位类型,passenger
表示乘客数目。ticketType
的切换说明用户改变了座位的类型,需要重新订票选择,所以我需要把已经选择的座位清空、重新初始化座位模版、更新显示的乘客与已选择座位数目:passenger
的切换说明用户改变了需要订票的乘客数。那首先应该处理的是,如果当前乘客数小于已选择座位数的时候,需要将最早选择的座位删掉。这里需要解释一下为什么是“小于”,比如场景如下:
接下来处理棘手的显示两排座位的场景,这又分两种情况:
我定义了一个
totalNum
变量用来存放上一次的乘客数,this.value
存放的是当前乘客数,所以当totalNum=4
,并且this.value=3
的时候,用户肯定是减少了乘客数目。(ps:4和3是乘客数目的一个临界点,只有这个时候才会有一排和两排切换的问题)。取出
hasChosen[0]
的值,也就是最早选择的那个座位和C
进行比较,小于等于C
的肯定是A, B, C
,否则就是D, E, F
。这样就能知道是需要删除哪一排了,这也是为什么上面会把两排座位分开存放。处理完数据,接下来就是使用数据来渲染模版了。
hasChosen.length = 0
,也就是用户没有选择座位的时候。如果乘客数目小于等于3,直接显示第一排;否则,就是把两排都显示出来。hasChosen.length != 0
,也就是用户选择了若干座位的时候。如果乘客数目小于等3,那肯定就是显示一排座位,但是,显示哪一排取决于hasChosen[0]
取出来的值属于哪一排。否则,也是直接将两排都显示出来。硬座/卧铺
对于这两种座位类型,只能是选择一个数量:上限是不能大于乘客数目,也不能大于5;下限是不能小于0即可。所以,它的操作界面是这样的:
总结
好了,终于写完了。这几种座位类型的选择功能,最容易实现的是硬座/卧铺,其次是高铁二等座,最痛苦的就是高铁商务座/一等座。因为它涉及到了两排座位的各种增删,曾经思考这个逻辑感觉都要精神分裂了。
不过,任何复杂的逻辑都是由多个简单的小逻辑组成。所以,学会能够清晰的把各个小逻辑拆分出来、实现它,并组合到一起才是最重要的。
完整示例
https://codepen.io/sjzcxc/pen/MvXNrG