Vue基础教程(211)电影购票APP开发实战之设计项目组件:Vue.js暴走电影票:手把手教你拆解组件像搭乐高,附可跑源码!

一、 为什么你的Vue组件总在“拆家”?

很多新手学Vue时最懵的时刻:明明跟着教程写了组件,结果数据像迷路的小孩在父子组件间乱窜。比如电影购票页面,用户选座时座位颜色要变,购票按钮要实时计算价格,这些功能如果全塞在一个.vue文件里,代码会臃肿得像国庆景区的厕所排队——根本动不了。

组件的本质是分而治之

  • 把页面拆成头部导航电影海报轮播座位网格购票面板四个独立组件
  • 组件间用Props/Events通信,就像快递员送包裹一样规范
  • 状态管理用Vuex?不!小项目用EventBus更轻量(文末会教你避坑)

举个栗子:当用户点击座位时,座位组件需要告诉父组件“这个位置被选了”,父组件再通知购票面板更新价格。如果直接操作DOM,代码会变成意大利面条——扯不断理还乱。


二、 购票APP组件设计:把界面变成乐高手册

1. 项目结构(用文件树秒懂)
movie-ticket/  
├─ src/  
│  ├─ components/  
│  │  ├─ Header.vue      # 顶栏:返回键+标题  
│  │  ├─ PosterSwiper.vue # 海报轮播  
│  │  ├─ SeatMap.vue     # 座位网格(核心难点!)  
│  │  └─ TicketPanel.vue # 购票信息面板  
│  └─ App.vue           # 组件组装平台
2. 组件拆解心法

Header组件:简单到笑出声

<template>
  <div class="header">
    <button @click="$router.back()">←</button>
    <h3>{{ title }}</h3>
  </div>
</template>
<script>
export default {
  props: ['title'] // 父组件传影城名称
}
</script>

SeatMap组件:内含二维数组魔法
座位数据用二维数组模拟影厅布局,0=空位 1=已售 2=选中:

seats: [  
  [0,0,1,0,2],  
  [1,0,0,0,0],  
  // ... 更多行
]

每个座位绑定动态class,点击时触发事件:

<template>
  <div class="seat-map">
    <div v-for="(row, i) in seats" :key="i" class="row">
      <div 
        v-for="(seat, j) in row" 
        :key="j"
        :class="['seat', { 
          sold: seat === 1, 
          selected: seat === 2 
        }]"
        @click="selectSeat(i, j)"
      >💺</div>
    </div>
  </div>
</template>

三、 组件通信:用“快递系统”代替“吼叫传话”

父子组件通信最怕变成“洋葱模型”——数据穿过多层组件就像剥洋葱,边写边流泪。

正确姿势

  1. 父传子用Props(像寄明信片)
    在App.vue中:
<SeatMap :seats="cinemaData" @seat-select="onSeatSelect"/>
  1. 子传父用Events(像收快递)
    在SeatMap.vue中点击座位时:
methods: {
  selectSeat(i, j) {
    if (this.seats[i][j] === 1) return // 已售座位不可选
    this.$emit('seat-select', { row: i, col: j }) // 发货!
  }
}
  1. 兄弟组件用EventBus(像小区广播)
    创建event-bus.js:
import Vue from 'vue'
export const EventBus = new Vue()

在TicketPanel中监听:

mounted() {
  EventBus.$on('seat-selected', (seat) => {
    this.selectedSeats.push(seat)
  })
}

四、 状态管理的平替方案

小项目没必要上Vuex,教你用2行代码实现状态共享:

// state.js
export const store = {
  selectedSeats: [],
  ticketPrice: 42
}

在组件中直接引入,注意响应式问题可用Vue.set解决。这招就像用保鲜盒代替冰箱——轻量又够用!


五、 完整可跑示例(复制粘贴即用)

App.vue(组装所有组件)

<template>
  <div id="app">
    <Header title="IMAX影厅 - 《流浪地球3》"/>
    <PosterSwiper :images="['poster1.jpg','poster2.jpg']"/>
    <SeatMap :seats="seatsData" @seat-select="handleSeatSelect"/>
    <TicketPanel :selected="selectedSeats" :price="42"/>
  </div>
</template>
<script>
import Header from './components/Header.vue'
import SeatMap from './components/SeatMap.vue'
import TicketPanel from './components/TicketPanel.vue'

export default {
  components: { Header, SeatMap, TicketPanel },
  data() {
    return {
      seatsData: [[0,0,1], [1,0,0], [0,0,0]],
      selectedSeats: []
    }
  },
  methods: {
    handleSeatSelect(seat) {
      const {row, col} = seat
      // 切换选中状态
      this.seatsData[row][col] = this.seatsData[row][col] === 2 ? 0 : 2
      this.selectedSeats = this.seatsData.flatMap((r, i) => 
        r.map((s, j) => s === 2 ? {row: i, col: j} : null).filter(Boolean)
      )
    }
  }
}
</script>

SeatMap.vue(核心交互组件)

<template>
  <div class="seat-map">
    <div v-for="(row, i) in seats" :key="i" class="row">
      <div 
        v-for="(seat, j) in row" 
        :key="j"
        :class="['seat', 
          seat === 1 ? 'sold' : '',
          seat === 2 ? 'selected' : '']"
        @click="selectSeat(i, j)">
        {{ seat === 1 ? '❌' : '💺' }}
      </div>
    </div>
  </div>
</template>
<script>
export default {
  props: ['seats'],
  methods: {
    selectSeat(i, j) {
      if (this.seats[i][j] === 1) {
        alert('这个座位已售出啦!')
        return
      }
      this.$emit('seat-select', { row: i, col: j })
    }
  }
}
</script>
<style scoped>
.seat-map { margin: 20px; }
.row { display: flex; justify-content: center; }
.seat { 
  width: 30px; height: 30px; margin: 5px; 
  cursor: pointer; border-radius: 5px;
}
.sold { background: #ccc; cursor: not-allowed; }
.selected { background: #4CAF50; }
</style>

(因篇幅限制,TicketPanel等组件代码已省略,完整源码可私信获取)


六、 踩坑预警:这三个雷区千万别踩!

  1. 动态修改Prop:直接改props会报警告,应该用events让父组件改
  2. 事件名大小写:HTML中事件名会自动转小写,建议用kebab-case命名
  3. SCSS作用域:style scoped有时无法覆盖子组件,可用/deep/穿透

七、 总结:组件化是Vue的灵魂乐高

通过电影购票案例,你会发现组件化开发就像拼乐高:

  • 每个组件都是独立的积木块
  • Props和Events是积木的凸起和凹槽
  • 状态管理是拼装说明书

当你能把页面拆成可复用的组件时,就完成了从“切图仔”到“工程师”的蜕变。现在就去GitHub克隆源码,边啃爆米花边敲代码吧——毕竟,没有实战的理论,就像没有电影的影厅,空虚得很!

(注:本文代码适用于Vue 2.x,Vue 3版本需调整组合式API)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值