Vue實(shí)現(xiàn)美團(tuán)app的影院推薦選座功能【推薦】
經(jīng)常用美團(tuán)app買電影票,不禁對(duì)它的推薦選座功能產(chǎn)生了好奇,于是打算自己實(shí)現(xiàn)一個(gè)類似的算法,美團(tuán)app的推薦選座界面如下
最多可以選5個(gè)座位,本demo的選座界面如下圖
上圖是點(diǎn)擊 推薦選座5人
后選出的座位(綠色),這個(gè)demo和美團(tuán)app不同的地方在于可以連續(xù)進(jìn)行推薦選座,美團(tuán)app點(diǎn)擊了推薦選座就必須買票才能繼續(xù)選擇。
本demo采用Vue-cli搭建,github地址點(diǎn)此 ,clone后直接npm start即可進(jìn)行具體操作
算法思考過程
對(duì)于這個(gè)推薦座位算法,我嘗試了不同場(chǎng)次的電影進(jìn)行推薦選座,總結(jié)出以下幾點(diǎn)
(1)推薦算法首先從影院中間排數(shù)的后一排的正中間開始搜索如下圖
可以確定是這個(gè)邏輯,試了其他幾個(gè)場(chǎng)次也是一樣
(2)優(yōu)先向后排方向進(jìn)行搜索,后排搜索完成后再從中間起始位置向前排搜索這個(gè)大多數(shù)情況是對(duì)的,如下圖,偶爾會(huì)出現(xiàn)不同
(3)后排搜索完成后,每一行都會(huì)有一個(gè)結(jié)果(每一行的結(jié)果是最靠近中軸線的那一組座位),取這些結(jié)果中距離中軸線最小的那個(gè)結(jié)果作為最終結(jié)果,而不是距離屏幕越近的
這一點(diǎn)也是大多數(shù)情況是對(duì)的,有些情況不對(duì),很奇怪
(4)只考慮并排且連續(xù)的座位,不能不在一排或者一排中間有分隔,比如過道之類的這一條可以理解,畢竟坐一排肯定觀影體驗(yàn)好得多
影院座位數(shù)據(jù)結(jié)構(gòu)
可以肯定的是用一個(gè)二維數(shù)組 seatArray 代表影院座位,注意到影院座位分布是不規(guī)則的,因此需要確定一個(gè) seatRow 和 seatCol 來確定影院座位的數(shù)組尺寸,分別代表行列數(shù),對(duì)于那些沒有座位的地方, seatArray 對(duì)應(yīng)的位置填-1,下面是座位具體的值和代表的含義
-1 非座位
0 可選座位 (白色)
1 已選座位 (綠色)
2 已購票座位 (紅色)
然后在mounted中初始化座位,初始值都為0(可選座位),如下代碼
//初始座位數(shù)組
initSeatArray: function(){
let seatArray = Array(this.seatRow).fill(0).map(()=>Array(this.seatCol).fill(0));
this.seatArray = seatArray;
//均分父容器寬度作為座位的寬度
this.seatSize = this.$refs.innerSeatWrapper
? parseInt(parseInt(window.getComputedStyle(this.$refs.innerSeatWrapper).width,10) / this.seatCol,10)
:0;
//初始化不是座位的地方
this.initNonSeatPlace();
},
//初始化不是座位的地方
initNonSeatPlace: function(){
for(let i=0;i<9;i++){
this.seatArray[i][0]=-1;
}
for(let i=0;i<8;i++){
this.seatArray[i][this.seatArray[0].length-1]=-1;
this.seatArray[i][this.seatArray[0].length-2]=-1;
}
for(let i=0;i<9;i++){
this.seatArray[i][this.seatArray[0].length-3]=-1;
}
for(let i=0;i<this.seatArray[0].length;i++){
this.seatArray[2][i]=-1;
}
}
初始化好之后用一個(gè)二重循環(huán)來構(gòu)建html結(jié)構(gòu),2個(gè)v-for嵌套循環(huán)出整個(gè)結(jié)構(gòu),如下代碼
<div class="inner-seat-wrapper" ref="innerSeatWrapper" >
<div v-for="row in seatRow">
<!--這里的v-if很重要,如果沒有則會(huì)導(dǎo)致報(bào)錯(cuò),因?yàn)閟eatArray初始狀態(tài)為空-->
<div v-for="col in seatCol"
v-if="seatArray.length>0"
class="seat"
:style="{width:seatSize+'px',height:seatSize+'px'}">
<div class="inner-seat"
@click="handleChooseSeat(row-1,col-1)"
v-if="seatArray[row-1][col-1]!==-1"
:class="seatArray[row-1][col-1]===2?'bought-seat':(seatArray[row-1][col-1]===1?'selected-seat':'unselected-seat')">
</div>
</div>
</div>
</div>
上述的inner-seat類的div就是具體的座位div,v-if說明了如果是-1也就是是過道之類的就不渲染,然后:class一句控制了該座位對(duì)應(yīng)狀態(tài)的類的值,一個(gè)嵌套三目運(yùn)算符來控制,對(duì)于每個(gè)座位綁定點(diǎn)擊事件 handleChooseSeat(row-1,col-1) 進(jìn)行狀態(tài)切換
//處理座位選擇邏輯
handleChooseSeat: function(row,col){
let seatValue = this.seatArray[row][col];
let newArray = this.seatArray;
//如果是已購座位,直接返回
if(seatValue===2) return
//如果是已選座位點(diǎn)擊后變未選
if(seatValue === 1){
newArray[row][col]=0
}else if(seatValue === 0){
newArray[row][col]=1
}
//必須整體更新二維數(shù)組,Vue無法檢測(cè)到數(shù)組某一項(xiàng)更新,必須slice復(fù)制一個(gè)數(shù)組才行
this.seatArray = newArray.slice();
},
這里注意vue中改變data中的二維數(shù)組必須先緩存二維數(shù)組,修改后,最終將二維數(shù)組重新賦值,否則修改不生效,因?yàn)閂ue無法偵測(cè)到數(shù)組內(nèi)的變動(dòng)。
推薦座位的具體代碼
首先給每個(gè)推薦座位的按鈕綁定事件smartChoose
代碼如下
//推薦選座,參數(shù)是推薦座位數(shù)目
smartChoose: function(num){
//找到影院座位水平垂直中間位置的后一排
let rowStart = parseInt((this.seatRow-1)/2,10)+1;
//先從中間排往后排搜索
let backResult = this.searchSeatByDirection(rowStart,this.seatRow-1,num);
if(backResult.length>0){
this.chooseSeat(backResult);
return
}
//再從中間排往前排搜索
let forwardResult = this.searchSeatByDirection(rowStart-1,0,num);
if(forwardResult.length>0) {
this.chooseSeat(forwardResult);
return
}
//提示用戶無合法位置可選
alert('無合法位置可選!')
},
第一步是找到影院座位水平垂直中間位置的后一排,然后調(diào)用 this.searchSeatByDirection 進(jìn)行該方向的搜索,先從中間排往后排搜索,再從中間排往前排搜索。如果任意一個(gè)方向搜索到結(jié)果,直接返回,否則提示用戶無合法位置, chooseSeat 用于改變座位的狀態(tài)
重點(diǎn)就是 searchSeatByDirection 的實(shí)現(xiàn),代碼如下
//向前后某個(gè)方向進(jìn)行搜索的函數(shù),參數(shù)是起始行,終止行,推薦座位個(gè)數(shù)
searchSeatByDirection: function(fromRow,toRow,num){
/*
* 推薦座位規(guī)則
* (1)初始狀態(tài)從座位行數(shù)的一半處的后一排的中間開始向左右分別搜索,取離中間最近的,如果滿足條件,
* 記錄下該結(jié)果離座位中軸線的距離,后排搜索完成后取距離最小的那個(gè)結(jié)果作為最終結(jié)果,優(yōu)先向后排進(jìn)行搜索,
* 后排都沒有才往前排搜,前排邏輯同上
* (2)只考慮并排且連續(xù)的座位,不能不在一排或者一排中間有分隔
* */
/*
* 保存當(dāng)前方向搜索結(jié)果的數(shù)組,元素是對(duì)象,result是結(jié)果數(shù)組,offset代表與中軸線的偏移距離
* {
* result:Array([x,y])
* offset:Number
* }
*/
let currentDirectionSearchResult = [];
//確定行數(shù)的大小關(guān)系,從小到大進(jìn)行遍歷
let largeRow = fromRow>toRow?fromRow:toRow,
smallRow = fromRow>toRow?toRow:fromRow;
//逐行搜索
for(let i=smallRow;i<=largeRow;i++){
//每一排的搜索,找出該排里中軸線最近的一組座位
let tempRowResult = [],
minDistanceToMidLine=Infinity;
for(let j=0;j<=this.seatCol - num;j++){
//如果有合法位置
if(this.checkRowSeatContinusAndEmpty(i,j,j+num-1)){
//計(jì)算該組位置距離中軸線的距離:該組位置的中間位置到中軸線的距離
let resultMidPos = parseInt((j+num/2),10);
let distance = Math.abs(parseInt(this.seatCol/2) - resultMidPos);
//如果距離較短則更新
if(distance<minDistanceToMidLine){
minDistanceToMidLine = distance;
//該行的最終結(jié)果
tempRowResult = this.generateRowResult(i,j,j+num-1)
}
}
}
//保存該行的最終結(jié)果
currentDirectionSearchResult.push({
result:tempRowResult,
offset:minDistanceToMidLine
})
}
//處理后排的搜索結(jié)果:找到距離中軸線最短的一個(gè)
//注意這里的邏輯需要區(qū)分前后排,對(duì)于后排是從前往后,前排則是從后往前找
let isBackDir = fromRow < toRow;
let finalReuslt = [],minDistanceToMid = Infinity;
if(isBackDir){
//后排情況,從前往后
currentDirectionSearchResult.forEach((item)=>{
if(item.offset < minDistanceToMid){
finalReuslt = item.result;
minDistanceToMid = item.offset;
}
});
}else{
//前排情況,從后往前找
currentDirectionSearchResult.reverse().forEach((item)=>{
if(item.offset < minDistanceToMid){
finalReuslt = item.result;
minDistanceToMid = item.offset;
}
})
}
//直接返回結(jié)果
return finalReuslt
},
代碼有點(diǎn)長(zhǎng),不過邏輯不難,就是前面那幾條規(guī)則的實(shí)現(xiàn),對(duì)于每一行的搜索,是可能存在多個(gè)合理的座位結(jié)果的
我這里采用的是從左往右遍歷,如果是推薦5個(gè)座位,先判斷1-5位置是否合理,如果合理則記錄下其中間位置(3號(hào))到中軸線的距離以及座位結(jié)果數(shù)組,然后再右移一位檢查2-6位置是否合理,如果合理則比較2-6位置的中間位置(4號(hào))距離中軸線的距離和之前的距離,取最短的一個(gè),同時(shí)更新座位結(jié)果數(shù)組。 這樣遍歷下來,該行的最終結(jié)果就能確定, 每一行的最佳結(jié)果會(huì)存放在currentDirectionSearchResult數(shù)組中
然后后排方向的所有排遍歷完后,就得到了由每一行最佳結(jié)果組成的數(shù)組currentDirectionSearchResult,再遍歷這個(gè)數(shù)組根據(jù)規(guī)則取距離中軸線最近的一個(gè)作為 最終返回的結(jié)果
這個(gè)算法可以優(yōu)化,直接從中間向2邊找,找到就返回,不過寫起來有點(diǎn)麻煩,但是效率肯定高。需要注意的是前排情況下要 currentDirectionSearchResult.reverse() 反向數(shù)組一下,因?yàn)閷?duì)于前排部分是優(yōu)先選擇前排的后面部分的(誰都不想坐第一排!),同后排相反
最后
這個(gè)算法不過有點(diǎn)問題,如下圖

最左邊的2個(gè)綠色座位是最后一次點(diǎn)擊推薦選座2人的結(jié)果,不過該位置卻不如另外一個(gè)箭頭處那2個(gè)位置合理,說明該算法其實(shí)不完美,可能上面的分析不到位,其實(shí)美團(tuán)的算法也有問題,如下圖

這個(gè)推薦的合理位置應(yīng)該是4個(gè)位置往左移動(dòng)一格,這才是正中央位置,這個(gè)推薦的有偏移量,不知道是為啥,網(wǎng)上也沒有搜到具體的算法邏輯,只能靠猜想和實(shí)驗(yàn)。
總結(jié)
以上所述是小編給大家介紹的Vue實(shí)現(xiàn)一個(gè)美團(tuán)app的影院推薦選座功能,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
vue開發(fā)runtime core中的虛擬節(jié)點(diǎn)示例詳解
這篇文章主要為大家介紹了vue開發(fā)runtime core中的虛擬節(jié)點(diǎn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
vue實(shí)現(xiàn)同時(shí)設(shè)置多個(gè)倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)同時(shí)設(shè)置多個(gè)倒計(jì)時(shí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
Vue路由回退的完美解決方案(vue-route-manager)
最近做了一個(gè)vue項(xiàng)目關(guān)于路由場(chǎng)景的問題,路由如何回退指定頁面,在此做個(gè)記錄,這篇文章主要給大家介紹了關(guān)于Vue路由回退的完美解決方案,主要利用的是vue-route-manager,需要的朋友可以參考下2021-09-09
vue監(jiān)聽滾動(dòng)事件實(shí)現(xiàn)滾動(dòng)監(jiān)聽
本文主要介紹了vue監(jiān)聽滾動(dòng)事件實(shí)現(xiàn)滾動(dòng)監(jiān)聽的相關(guān)資料。具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-04-04
Vue預(yù)覽圖片和視頻的幾種實(shí)現(xiàn)方式
本文主要介紹了Vue預(yù)覽圖片和視頻的幾種實(shí)現(xiàn)方式,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
vue開發(fā)樹形結(jié)構(gòu)組件(組件遞歸)
這篇文章主要為大家詳細(xì)介紹了vue開發(fā)樹形結(jié)構(gòu)組件的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Vue中插槽slot的使用方法與應(yīng)用場(chǎng)景詳析
這篇文章主要給大家介紹了關(guān)于Vue中插槽slot的使用方法與應(yīng)用場(chǎng)景的相關(guān)資料,插槽(Slot)是Vue提出來的一個(gè)概念,正如名字一樣,插槽用于決定將所攜帶的內(nèi)容,插入到指定的某個(gè)位置,從而使模板分塊,具有模塊化的特質(zhì)和更大的重用性,需要的朋友可以參考下2021-06-06
關(guān)于vue 項(xiàng)目中瀏覽器跨域的配置問題
這篇文章主要介紹了vue 項(xiàng)目中瀏覽器跨域的配置問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11

