
文章目录前言座位模型要先想清楚二维数组更贴近真实座位图选座逻辑的边界比 UI 更重要颜色只是状态的另一种表达已选座位不要手动维护第二份列表底部结算区只是状态展示完整代码最后总结前言影院选座页最容易写乱的地方不是座位画出来而是座位状态怎么维护。一个座位可能可选、已售、已选、不可用用户点一下底部票数、价格、已选座位文案都要同步变化。这个案例刚好把这些关系压在一个比较小的页面里适合拆开看。当前页面是一个简化版购票选座上方显示屏幕方向中间是 8 行 10 列的座位矩阵底部展示电影、场次、已选座位和总价。确认按钮只是展示状态没有真正提交订单或锁座逻辑。座位模型要先想清楚每个座位用Seat表示里面同时保存了位置、状态和展示标签。interfaceSeat{row:numbercol:numberstatus:available|taken|selected|disabledlabel:string}这里的row和col是给程序用的点击座位时要靠它们回到二维数组里更新状态label是给用户看的比如A1、B6。status则是整个页面的核心字段颜色、是否可点击、是否计入价格都由它决定。二维数组更贴近真实座位图源码没有把座位摊平成一维数组而是用Seat[][]保存。外层数组表示行内层数组表示这一行里的座位。这样写ForEach的时候很自然先遍历每一排再遍历这一排的每个座位。Stateseats:Seat[][]this.initSeats()StateselectedCount:number0StatemaxSelect:number4initSeats()负责初始化 8 行 10 列座位。它用idx r * cols c把二维坐标转换成一个连续编号再通过takenSet和disabledSet标记已售、不可用座位。initSeats():Seat[][]{constrows8constcols10consttakenSetnewSet([3,4,12,23,34,45,56,67,22,33,44,55,66,77])constdisabledSetnewSet([0,9,70,79])constresult:Seat[][][]for(letr0;rrows;r){constrow:Seat[][]for(letc0;ccols;c){constidxr*colscletstatus:available|taken|selected|disabledavailableif(takenSet.has(idx))statustakenif(disabledSet.has(idx))statusdisabledrow.push({row:r,col:c,status,label:${String.fromCharCode(65r)}${c1}})}result.push(row)}returnresult}这个初始化方式很适合教学因为它把座位图生成过程写得很透明。真实项目里taken和disabled多半来自接口而且还要考虑临时锁座、超时释放、多人同时抢座这些问题不能只靠前端状态判断。选座逻辑的边界比 UI 更重要selectSeat()是这页最值得看的函数。它先拿到被点击的座位然后处理三类情况已售和不可用直接返回已选座位再次点击会取消可选座位只有在没超过maxSelect时才能变成已选。selectSeat(row:number,col:number){constseatthis.seats[row][col]if(seat.statustaken||seat.statusdisabled)returnif(seat.statusselected){seat.statusavailable;this.seats[row][col]seatthis.selectedCount--}elseif(this.selectedCountthis.maxSelect){seat.statusselected;this.seats[row][col]seatthis.selectedCount}this.seatsthis.seats.slice()}最后一行this.seats this.seats.slice()很关键。前面虽然改了某个座位对象的status但二维数组本身的引用没有变。重新赋值一个浅拷贝可以让状态变更更明确地触发 UI 刷新。这个写法不算完美因为内层数组和座位对象仍然是原引用如果页面变复杂我会改成更彻底的不可变更新比如按行和列重新map()出新数组。这里还有一个产品边界超过 4 张时点击其他可选座位不会有任何提示只是静默不选。教学案例这样写足够简洁真实购票页最好给一个 Toast 或底部提示不然用户可能以为点按失效。颜色只是状态的另一种表达seatColor()把座位状态映射成颜色。这个函数虽然短但能避免在座位组件里堆一串三元表达式。seatColor(status:string):string{if(statusavailable)return#E0E0E0if(statustaken)return#757575if(statusselected)return#007AFFreturn#F5F5F5}中间座位矩阵和底部图例用的是同一套颜色语义灰色可选、深灰已售、蓝色已选。只要这个映射不变用户看到图例后就能理解座位图。后续如果要支持情侣座、VIP 座、无障碍座也应该先扩展status或增加座位类型而不是在 UI 里硬写多个特殊判断。已选座位不要手动维护第二份列表底部的“已选A5、B3”并没有单独保存一个数组而是通过 getter 从seats里扫出来。getselectedLabels():string[]{constlabels:string[][]this.seats.forEach(rowrow.forEach(seat{if(seat.statusselected)labels.push(seat.label)}))returnlabels}这和selectedCount的处理形成了一个对比。selectedCount是单独维护的计数selectedLabels是实时推导出来的列表。小页面里这样写没问题但如果担心selectedCount和真实选中座位不同步也可以把票数改成 getter直接根据selectedLabels.length或遍历结果计算。这样状态源会更少出错概率也更低。底部结算区只是状态展示底部区域用selectedCount决定票价、按钮文案和按钮颜色Text(¥${this.selectedCount*68})Text(${this.selectedCount}张票)Button(this.selectedCount0?确认选座:最多选${this.maxSelect}张)这段代码能把选座反馈做出来但还没有真正的“购票”。按钮没有绑定提交事件也没有票价模型价格68是直接写在视图里的。正式项目里至少应该把单价放进场次数据确认按钮接入锁座接口并在提交前重新校验座位是否仍然可用。这个案例最有价值的地方是把选座页的状态关系讲清楚了座位矩阵是主状态点击事件只修改座位状态和数量底部文案、价格、颜色都从这些状态派生出来。只要这条线不乱页面再加放大缩小、最佳座位推荐、多人连座选择也不会失控。完整代码下面保留案例完整代码方便你直接对照学习、复制和重构。import{router}fromkit.ArkUIinterfaceSeat{row:numbercol:numberstatus:available|taken|selected|disabledlabel:string}EntryComponentstruct CinemaSeatSelector{Stateseats:Seat[][]this.initSeats()StateselectedCount:number0StatemaxSelect:number4StatemovieName:string星际穿越·重映版StateshowTime:string今天 19:30Statehall:string3号厅 IMAXinitSeats():Seat[][]{constrows8constcols10consttakenSetnewSet([3,4,12,23,34,45,56,67,22,33,44,55,66,77])constdisabledSetnewSet([0,9,70,79])constresult:Seat[][][]for(letr0;rrows;r){constrow:Seat[][]for(letc0;ccols;c){constidxr*colscletstatus:available|taken|selected|disabledavailableif(takenSet.has(idx))statustakenif(disabledSet.has(idx))statusdisabledrow.push({row:r,col:c,status,label:\\${String.fromCharCode(65r)}\${c1}\})}result.push(row)}returnresult}selectSeat(row:number,col:number){constseatthis.seats[row][col]if(seat.statustaken||seat.statusdisabled)returnif(seat.statusselected){seat.statusavailable;this.seats[row][col]seatthis.selectedCount--}elseif(this.selectedCountthis.maxSelect){seat.statusselected;this.seats[row][col]seatthis.selectedCount}this.seatsthis.seats.slice()}seatColor(status:string):string{if(statusavailable)return#E0E0E0if(statustaken)return#757575if(statusselected)return#007AFFreturn#F5F5F5}getselectedLabels():string[]{constlabels:string[][]this.seats.forEach(rowrow.forEach(seat{if(seat.statusselected)labels.push(seat.label)}))returnlabels}build(){Column(){Row(){Image($r(app.media.ic_back)).width(24).height(24).onClick(()router.back())Text(选座购票).fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text( ).width(24)}.width(100%).padding({left:16,right:16,top:12,bottom:8})// Movie infoColumn(){Text( 屏幕).fontSize(12).fontColor(#AAA).margin({bottom:4})Column().width(60%).height(4).backgroundColor(#CCC).borderRadius(2).margin({bottom:16})}.alignItems(HorizontalAlign.Center).width(100%)// Seat gridScroll(){Column(){ForEach(this.seats,(row:Seat[],rIdx:number){Row(){Text(String.fromCharCode(65rIdx)).fontSize(11).fontColor(#AAA).width(16).textAlign(TextAlign.Center)ForEach(row,(seat:Seat){Column().width(28).height(24).borderRadius({topLeft:4,topRight:4}).backgroundColor(this.seatColor(seat.status)).margin(3).onClick(()this.selectSeat(seat.row,seat.col))})}.margin({bottom:4}).justifyContent(FlexAlign.Center)})}.width(100%).padding({top:8,bottom:16})}.layoutWeight(1).width(100%)// LegendRow(){Row(){Column().width(16).height(14).backgroundColor(#E0E0E0).borderRadius(2).margin({right:4})Text(可选).fontSize(11).fontColor(#666)}.margin({right:16})Row(){Column().width(16).height(14).backgroundColor(#757575).borderRadius(2).margin({right:4})Text(已售).fontSize(11).fontColor(#666)}.margin({right:16})Row(){Column().width(16).height(14).backgroundColor(#007AFF).borderRadius(2).margin({right:4})Text(已选).fontSize(11).fontColor(#666)}}.justifyContent(FlexAlign.Center).width(100%).padding({bottom:12})// BottomColumn(){Text(this.movieName).fontSize(15).fontWeight(FontWeight.Bold).fontColor(#333)Text(\\${this.showTime}· \${this.hall}\).fontSize(13).fontColor(#888).margin({top:4})if(this.selectedCount0){Text(\已选\${this.selectedLabels.join(、)}\).fontSize(13).fontColor(#007AFF).margin({top:4})}Row(){Column(){Text(\¥\${this.selectedCount*68}\).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#FF4757)Text(\\${this.selectedCount}张票\).fontSize(12).fontColor(#888)}.layoutWeight(1)Button(this.selectedCount0?确认选座:\最多选\${this.maxSelect}张\).backgroundColor(this.selectedCount0?#FF4757:#CCC).fontColor(#FFF).borderRadius(24).padding({left:28,right:28,top:12,bottom:12})}.width(100%).margin({top:12})}.width(100%).backgroundColor(#FFF).padding({left:16,right:16,top:12,bottom:20}).shadow({radius:8,color:#0000001A,offsetY:-4})}.width(100%).height(100%).backgroundColor(#1A1A2E)}}最后总结影院选座购票页 这个案例并不靠复杂技巧取胜它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后你再去写同类型功能速度会明显快很多。