selectSeat.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <template>
  2. <view class="w-100">
  3. <view class="bg-f1 h-100vh">
  4. <view class="pt-f left-0 w-100 p-0-32 bg-white z1000" :style="'height: 132rpx;top:0'">
  5. <view>
  6. <view class="fz-34 fw-b pt-20">
  7. 三体2:黑暗森林
  8. </view>
  9. <view class="mt-10 fz-28 color-666">
  10. 2021年1月22日 国语3D
  11. </view>
  12. </view>
  13. </view>
  14. <movable-area :style="'height:'+(seatRow*40+350)+'rpx;width: 100vw;top:'+(rpxNum*132)+'px'" class="pt-f left-0">
  15. <movable-view :style="'width: 100vw;height:'+(seatRow*40+350)+'rpx;'" :inertia="true" :scale="true" :scale-min="0.95" :scale-max="2" direction="all" @change="onMove" @scale="onScale">
  16. <view class="Stage dp-f jc-c ai-c fz-22 color-333">5号厅</view>
  17. <view style="width: 100rpx;height: 30rpx;" class="m-0-a mt-48 dp-f jc-c ai-c fz-20 color-999 b-1 br-5">银幕中央</view>
  18. <view class="pt-f pa-v-2 b-d-1" :style="'height:'+seatRow*(20+seatSize*pxNum)+'rpx;top:165rpx;width:0'"></view>
  19. <view v-for="(item,index) in seatArray" :key="index" class="dp-f jc-c mt-20" :style="'width:'+boxWidth+'px;height:'+seatSize+'px'">
  20. <view v-for="(seat,col) in item" :key="col" class="dp-ib" :style="'width:'+seatSize+'px;height:'+seatSize+'px'" @click="handleChooseSeat(index,col)">
  21. <image v-if="seat.type===0" class="w-100 h-100" src="https://test-api.haocha13.cn/static/img/optional.png" mode="aspectFit"></image>
  22. <image v-else-if="seat.type===1" class="w-100 h-100" src="https://test-api.haocha13.cn/static/img/set.png" mode="aspectFit"></image>
  23. <image v-else-if="seat.type===2" class="w-100 h-100" src="https://test-api.haocha13.cn/static/img/sold.png" mode="aspectFit"></image>
  24. </view>
  25. </view>
  26. <view class="pt-f bg-line br-15 over-h" :style="'left: '+(10-moveX/scale)+'px;top:109rpx;width:30rpx;'">
  27. <view class="w-100 dp-f ai-c jc-c fz-22 color-fff" :style="'height:'+seatSize+'px;margin-top:20rpx;'" v-for="(m,mindex) in mArr" :key="mindex">{{m}}</view>
  28. <view :style="'height: 20rpx;'"></view>
  29. </view>
  30. </movable-view>
  31. </movable-area>
  32. <view class="pt-f bottom-bar left-0 dp-f fd-cr z1000">
  33. <view class="bg-white p-all-32">
  34. <view class="dp-f ai-c jc-c fz-28 color-333 mb-20" v-if="SelectNum===0">
  35. 推荐选座
  36. <view style="width: 106rpx;height: 60rpx;" class="b-1 br-5 dp-f ai-c jc-c fz-28 ml-20" v-for="(n,numindex) in 4" :key="n" @click="smartChoose(numindex+1)">
  37. {{numindex+1}}人
  38. </view>
  39. </view>
  40. <view class="dp-f ai-c fw-w fz-28 color-333 mb-20" v-if="SelectNum>0">
  41. <text>已选</text>
  42. <view class="p-all-10 b-1 br-5 dp-f ai-c jc-c fz-28 ml-20" v-for="(optItem,optindex) in optArr" :key="optindex">
  43. {{optItem.RowNum+'排'+optItem.ColumnNum+'座'}}
  44. </view>
  45. </view>
  46. <view style="width: 686rpx;height: 90rpx;" class="dp-f jc-c ai-c br-10 fz-34 color-fff" :class="SelectNum>0?'bg-red-1':'bg-unbtn'" @click="buySeat">
  47. {{SelectNum>0?('¥ '+aPrice+' 确认座位'):'请选座位'}}
  48. </view>
  49. </view>
  50. <view class="dp-f jc-c ai-c mb-20 fz-28" v-if="showTis">
  51. <view class="dp-f jc-c ai-c m-0-10">
  52. <image :style="'width:'+seatSize+'px;height:'+seatSize+'px'" src="https://test-api.haocha13.cn/static/img/optional.png" mode="aspectFit"></image><span class="ml-10">可选</span>
  53. </view>
  54. <view class="dp-f jc-c ai-c m-0-10">
  55. <image :style="'width:'+seatSize+'px;height:'+seatSize+'px'" src="https://test-api.haocha13.cn/static/img/sold.png" mode="aspectFit"></image><span class="ml-10">不可选</span>
  56. </view>
  57. <view class="dp-f jc-c ai-c m-0-10">
  58. <image :style="'width:'+seatSize+'px;height:'+seatSize+'px'" src="https://test-api.haocha13.cn/static/img/set.png" mode="aspectFit"></image><span class="ml-10">选中</span>
  59. </view>
  60. </view>
  61. </view>
  62. </view>
  63. </view>
  64. </template>
  65. <script>
  66. import { post } from "@/request/api.js";
  67. export default {
  68. data() {
  69. return {
  70. scaleMin: 1, //h5端为解决1无法缩小问题,设为0.95
  71. boxWidth: 400, //屏幕宽度px
  72. space: ' ', //空格
  73. seatArray: [], //影院座位的二维数组,-1为非座位,0为未购座位,1为已选座位(绿色),2为已购座位(红色),一维行,二维列
  74. seatRow: 0, //影院座位行数
  75. seatCol: 0, //影院座位列数
  76. seatSize: 0, //座位尺寸
  77. SelectNum: 0, //选择座位数
  78. moveX: 0, //水平移动偏移量
  79. scale: 1, //放大倍数
  80. minRow: 0, //从第几行开始排座位
  81. minCol: 0, //从第几列开始排座位
  82. showTis: true, //显示选座提示
  83. seatList: [], //接口获取的原始位置
  84. mArr: [], //排数提示
  85. optArr: [], //选中的座位数组。
  86. isWXAPP: false,
  87. };
  88. },
  89. computed: {
  90. aPrice() {
  91. return this.SelectNum * 36
  92. },
  93. rpxNum() {
  94. return this.boxWidth / 750
  95. },
  96. pxNum() {
  97. return 750 / this.boxWidth
  98. },
  99. },
  100. onLoad() {
  101. let that = this
  102. //获取宽度
  103. uni.getSystemInfo({
  104. success: function(e) {
  105. that.boxWidth = e.screenWidth
  106. //#ifdef H5
  107. that.scaleMin = 0.95
  108. //#endif
  109. }
  110. })
  111. that.initData()
  112. },
  113. methods: {
  114. async initData() {
  115. let arr = []
  116. let res = await post("local/getSeat",{showId:"bb27ce9b64c86d460007"})
  117. if(res.code == 0){
  118. let da = res.data.data.seats
  119. for (let it of da) {
  120. arr.push({
  121. YCoord: it.rowNo,
  122. XCoord: it.columnNo,
  123. SeatCode: it.seatId,
  124. Status: it.status == "N" ? 0 : it.status == "LK" ? 2 : -1,
  125. RowNum: it.seatNo.slice(0,1),
  126. ColumnNum: it.seatNo.slice(2,3)
  127. })
  128. }
  129. }
  130. //假数据说明:SeatCode座位编号,RowNum-行号,ColumnNum-纵号,YCoord-Y坐标,XCoord-X坐标,Status-状态
  131. let row = 0
  132. let col = 0
  133. let minCol = parseInt(arr[0].XCoord)
  134. let minRow = parseInt(arr[0].YCoord)
  135. for (let i of arr) {
  136. minRow = parseInt(i.YCoord) < minRow ? parseInt(i.YCoord) : minRow
  137. minCol = parseInt(i.XCoord) < minCol ? parseInt(i.XCoord) : minCol
  138. row = parseInt(i.YCoord) > row ? parseInt(i.YCoord) : row
  139. col = parseInt(i.XCoord) > col ? parseInt(i.XCoord) : col
  140. }
  141. this.seatList = arr
  142. this.seatRow = row - minRow + 1
  143. this.seatCol = col - minCol + 3
  144. this.minRow = minRow
  145. this.minCol = minCol - 1
  146. this.initSeatArray()
  147. },
  148. //初始座位数组
  149. initSeatArray: function() {
  150. let seatArray = Array(this.seatRow).fill(0).map(() => Array(this.seatCol).fill({
  151. type: -1,
  152. SeatCode: '',
  153. RowNum: '',
  154. ColumnNum: ''
  155. }));
  156. this.seatArray = seatArray
  157. this.seatSize = this.boxWidth > 0 ?
  158. parseInt(parseInt(this.boxWidth, 10) / (this.seatCol + 1), 10) :
  159. parseInt(parseInt(414, 10) / (this.seatCol + 1), 10)
  160. this.initNonSeatPlace();
  161. },
  162. //初始化是座位的地方
  163. initNonSeatPlace: function() {
  164. let seat = this.seatList.slice()
  165. let arr = this.seatArray.slice()
  166. for (let num in seat) {
  167. let status = 2 //-1为非座位,0为未购座位,1为已选座位(绿色),2为已购座位(红色)
  168. if (seat[num].Status === 0) {
  169. status = 0
  170. } else if (seat[num].Status === -1) {
  171. status = -1
  172. }
  173. if (seat[num].YCoord) {
  174. arr[parseInt(seat[num].YCoord) - this.minRow][parseInt(seat[num].XCoord) - this.minCol] = {
  175. type: status,
  176. SeatCode: seat[num].SeatCode,
  177. RowNum: seat[num].RowNum,
  178. ColumnNum: seat[num].ColumnNum
  179. }
  180. }
  181. }
  182. this.seatArray = arr.slice()
  183. let mArr = []
  184. for (let i in arr) {
  185. let m = ''
  186. if(arr[i][0]){
  187. for (let n of arr[i]) {
  188. if (n.SeatCode) {
  189. m = n.RowNum
  190. }
  191. }
  192. if (m) {
  193. mArr.push(m)
  194. } else {
  195. mArr.push('')
  196. }
  197. }
  198. }
  199. this.mArr = mArr
  200. },
  201. //放大缩小事件
  202. onScale: function(e) {
  203. this.showTis = false
  204. // this.moveX=-e.detail.x
  205. let w = this.boxWidth * 0.5
  206. let s = 1 - e.detail.scale
  207. this.moveX = w * s
  208. this.scale = e.detail.scale
  209. if (s > 0 || s === 0) {
  210. this.showTis = true
  211. }
  212. },
  213. //移动事件
  214. onMove: function(e) {
  215. this.showTis = false
  216. this.moveX = e.detail.x
  217. },
  218. //重置座位
  219. resetSeat: function() {
  220. this.SelectNum = 0
  221. this.optArr = []
  222. //将所有已选座位的值变为0
  223. let oldArray = this.seatArray.slice();
  224. for (let i = 0; i < this.seatRow; i++) {
  225. for (let j = 0; j < this.seatCol; j++) {
  226. if (oldArray[i][j].type === 1) {
  227. oldArray[i][j].type = 0
  228. }
  229. }
  230. }
  231. this.seatArray = oldArray;
  232. },
  233. //选定且购买座位
  234. buySeat: function() {
  235. if (this.SelectNum === 0) {
  236. return
  237. }
  238. let oldArray = [];
  239. for (let i = 0; i < this.seatRow; i++) {
  240. for (let j = 0; j < this.seatCol; j++) {
  241. if (this.seatArray[i][j].type === 1) {
  242. oldArray.push(this.seatArray[i][j])
  243. }
  244. }
  245. }
  246. console.log(oldArray);
  247. },
  248. //处理座位选择逻辑
  249. handleChooseSeat: function(row, col) {
  250. let seatValue = this.seatArray[row][col].type;
  251. let newArray = this.seatArray;
  252. //如果是已购座位,直接返回
  253. if (seatValue === 2 || seatValue === -1) return
  254. //如果是已选座位点击后变未选
  255. if (seatValue === 1) {
  256. newArray[row][col].type = 0
  257. this.SelectNum--
  258. this.getOptArr(newArray[row][col], 0)
  259. } else if (seatValue === 0) {
  260. newArray[row][col].type = 1
  261. this.SelectNum++
  262. this.getOptArr(newArray[row][col], 1)
  263. }
  264. //必须整体更新二维数组,Vue无法检测到数组某一项更新,必须slice复制一个数组才行
  265. this.seatArray = newArray.slice();
  266. },
  267. //处理已选座位数组
  268. getOptArr: function(item, type) {
  269. let optArr = this.optArr
  270. if (type === 1) {
  271. optArr.push(item)
  272. } else if (type === 0) {
  273. let arr = []
  274. optArr.forEach(v => {
  275. if (v.SeatCode !== item.SeatCode) {
  276. arr.push(v)
  277. }
  278. })
  279. optArr = arr
  280. }
  281. this.optArr = optArr.slice()
  282. },
  283. //推荐选座,参数是推荐座位数目,
  284. smartChoose: function(num) {
  285. // 先重置
  286. this.resetSeat()
  287. //找到影院座位水平垂直中间位置的后一排
  288. let rowStart = parseInt((this.seatRow - 1) / 2, 10) + 1;
  289. //先从中间排往后排搜索
  290. let backResult = this.searchSeatByDirection(rowStart, this.seatRow - 1, num);
  291. if (backResult.length > 0) {
  292. this.chooseSeat(backResult);
  293. this.SelectNum += num
  294. return
  295. }
  296. //再从中间排往前排搜索
  297. let forwardResult = this.searchSeatByDirection(rowStart - 1, 0, num);
  298. if (forwardResult.length > 0) {
  299. this.chooseSeat(forwardResult);
  300. this.SelectNum += num
  301. return
  302. }
  303. //提示用户无合法位置可选
  304. alert('无合法位置可选!')
  305. },
  306. //搜索函数,参数:fromRow起始行,toRow终止行,num推荐座位数
  307. searchSeatByDirection: function(fromRow, toRow, num) {
  308. /*
  309. * 推荐座位规则
  310. * (1)初始状态从座位行数的一半处的后一排的中间开始向左右分别搜索,取离中间最近的,如果满足条件,
  311. * 记录下该结果离座位中轴线的距离,后排搜索完成后取距离最小的那个结果座位最终结果,优先向后排进行搜索,
  312. * 后排都没有才往前排搜,前排逻辑同上
  313. *
  314. * (2)只考虑并排且连续的座位,不能不在一排或者一排中间有分隔
  315. *
  316. * */
  317. /*
  318. * 保存当前方向搜索结果的数组,元素是对象,result是结果数组,offset代表与中轴线的偏移距离
  319. * {
  320. * result:Array([x,y])
  321. * offset:Number
  322. * }
  323. *
  324. */
  325. let currentDirectionSearchResult = [];
  326. let largeRow = fromRow > toRow ? fromRow : toRow,
  327. smallRow = fromRow > toRow ? toRow : fromRow;
  328. for (let i = smallRow; i <= largeRow; i++) {
  329. //每一排的搜索,找出该排里中轴线最近的一组座位
  330. let tempRowResult = [],
  331. minDistanceToMidLine = Infinity;
  332. for (let j = 0; j <= this.seatCol - num; j++) {
  333. //如果有合法位置
  334. if (this.checkRowSeatContinusAndEmpty(i, j, j + num - 1)) {
  335. //计算该组位置距离中轴线的距离:该组位置的中间位置到中轴线的距离
  336. let resultMidPos = parseInt((j + num / 2), 10);
  337. let distance = Math.abs(parseInt(this.seatCol / 2) - resultMidPos);
  338. //如果距离较短则更新
  339. if (distance < minDistanceToMidLine) {
  340. minDistanceToMidLine = distance;
  341. //该行的最终结果
  342. tempRowResult = this.generateRowResult(i, j, j + num - 1)
  343. }
  344. }
  345. }
  346. //保存该行的最终结果
  347. currentDirectionSearchResult.push({
  348. result: tempRowResult,
  349. offset: minDistanceToMidLine
  350. })
  351. }
  352. //处理后排的搜索结果:找到距离中轴线最短的一个
  353. //注意这里的逻辑需要区分前后排,对于后排是从前往后,前排则是从后往前找
  354. let isBackDir = fromRow < toRow;
  355. let finalReuslt = [],
  356. minDistanceToMid = Infinity;
  357. if (isBackDir) {
  358. //后排情况,从前往后
  359. currentDirectionSearchResult.forEach((item) => {
  360. if (item.offset < minDistanceToMid) {
  361. finalReuslt = item.result;
  362. minDistanceToMid = item.offset;
  363. }
  364. });
  365. } else {
  366. //前排情况,从后往前找
  367. currentDirectionSearchResult.reverse().forEach((item) => {
  368. if (item.offset < minDistanceToMid) {
  369. finalReuslt = item.result;
  370. minDistanceToMid = item.offset;
  371. }
  372. })
  373. }
  374. //直接返回结果
  375. return finalReuslt
  376. },
  377. /*辅助函数,判断每一行座位从i列到j列是否全部空余且连续
  378. *
  379. */
  380. checkRowSeatContinusAndEmpty: function(rowNum, startPos, endPos) {
  381. let isValid = true;
  382. for (let i = startPos; i <= endPos; i++) {
  383. if (this.seatArray[rowNum][i].type !== 0) {
  384. isValid = false;
  385. break;
  386. }
  387. }
  388. return isValid
  389. },
  390. //辅助函数:返回每一行的某个合理位置的座位数组
  391. generateRowResult: function(row, startPos, endPos) {
  392. let result = [];
  393. for (let i = startPos; i <= endPos; i++) {
  394. result.push([row, i])
  395. }
  396. return result
  397. },
  398. //辅助函数:智能推荐的选座操作
  399. chooseSeat: function(result) {
  400. let opt = this.optArr
  401. let oldArray = this.seatArray.slice();
  402. for (let i = 0; i < result.length; i++) {
  403. //选定座位
  404. oldArray[result[i][0]][result[i][1]].type = 1
  405. this.optArr.push(oldArray[result[i][0]][result[i][1]])
  406. }
  407. this.seatArray = oldArray;
  408. },
  409. }
  410. }
  411. </script>
  412. <style lang="scss" scoped>
  413. .p-all-10 {
  414. padding: 10rpx;
  415. }
  416. .ml-10 {
  417. margin-left: 10rpx;
  418. }
  419. .m-0-10 {
  420. margin: 0 10rpx;
  421. }
  422. .bg-unbtn {
  423. background-color: #f9abb3;
  424. }
  425. .bg-red-1 {
  426. background-color: #F45664;
  427. }
  428. .br-10 {
  429. border-radius: 10rpx;
  430. }
  431. .ml-20 {
  432. margin-left: 20rpx;
  433. }
  434. .mb-20 {
  435. margin-bottom: 20rpx;
  436. }
  437. .p-all-32 {
  438. padding: 32rpx;
  439. }
  440. .fd-cr {
  441. flex-direction: column-reverse;
  442. /* 主轴方向从下到上,默认从左到右 */
  443. }
  444. .bottom-bar {
  445. bottom: var(--window-bottom);
  446. }
  447. .color-fff {
  448. color: #fff
  449. }
  450. .br-15 {
  451. border-radius: 15rpx;
  452. }
  453. .over-h {
  454. overflow: hidden;
  455. }
  456. .dp-ib {
  457. display: inline-block;
  458. }
  459. .mt-20 {
  460. margin-top: 20rpx;
  461. }
  462. .pa-v-2 {
  463. /* 定位垂直对齐 */
  464. left: 50%;
  465. transform: translateX(-50%)
  466. }
  467. .b-d-1 {
  468. border: 1px dashed #e5e5e5;
  469. }
  470. .w-100 {
  471. width: 100%;
  472. }
  473. .h-100 {
  474. height: 100%;
  475. }
  476. .bg-f1 {
  477. background-color: #f1f1f1;
  478. }
  479. .h-100vh {
  480. height: 100vh;
  481. }
  482. .pt-f {
  483. position: fixed;
  484. }
  485. .left-0 {
  486. left: 0;
  487. }
  488. .p-0-32 {
  489. padding: 0 32rpx;
  490. }
  491. .pt-20 {
  492. padding-top: 20rpx;
  493. }
  494. .bg-white {
  495. background-color: #fff;
  496. }
  497. .z1000 {
  498. z-index: 1000;
  499. }
  500. .fz-34 {
  501. font-size: 34rpx;
  502. }
  503. .fw-b {
  504. font-weight: bold;
  505. }
  506. .mt-10 {
  507. margin-top: 10rpx;
  508. }
  509. .fz-28 {
  510. font-size: 28rpx;
  511. }
  512. .color-666 {
  513. color: #666666
  514. }
  515. .dp-f {
  516. display: flex;
  517. }
  518. .jc-c {
  519. justify-content: center;
  520. }
  521. .ai-c {
  522. align-items: center;
  523. }
  524. .fz-22 {
  525. font-size: 22rpx;
  526. }
  527. .color-333 {
  528. color: #333333
  529. }
  530. .m-0-a {
  531. margin: 0 auto;
  532. }
  533. .mt-48 {
  534. margin-top: 48rpx;
  535. }
  536. .fz-20 {
  537. font-size: 20rpx;
  538. }
  539. .color-999 {
  540. color: #999999
  541. }
  542. .b-1 {
  543. border: 1px solid #CCCCCC;
  544. }
  545. .br-5 {
  546. border-radius: 5rpx;
  547. }
  548. .Stage {
  549. background-color: #dddddd;
  550. width: 380rpx;
  551. height: 34rpx;
  552. transform: perspective(34rpx) rotateX(-10deg);
  553. margin: 0 auto;
  554. }
  555. .bg-line {
  556. background-color: rgba(0, 0, 0, 0.3);
  557. }
  558. .sel-seat {
  559. background: url('https://test-api.haocha13.cn/static/img/set.png') center center no-repeat;
  560. background-size: 100% 100%;
  561. }
  562. .unsel-seat {
  563. background: url('https://test-api.haocha13.cn/static/img/optional.png') center center no-repeat;
  564. background-size: 100% 100%;
  565. }
  566. .bought-seat {
  567. background: url('https://test-api.haocha13.cn/static/img/sold.png') center center no-repeat;
  568. background-size: 100% 100%;
  569. }
  570. </style>