Vue基础教程(218)电影购票APP开发实战之设计项目页面组件及路由配置中的影院页面组件及路由:别让烂片毁约会!三招搞定影院页,Vue小白秒变选座大神

朋友们,有没有过这样的悲惨经历:和心上人约好看电影,结果打开购票APP卡成PPT,选座页面加载半天,最后只能委屈坐在第一排仰头看完3小时?别问我是怎么知道的…

今天,咱们就来彻底解决这个痛点!用Vue打造一个丝滑到像德芙巧克力的电影购票APP,重点攻克其中最关键的影院页面组件路由配置。我保证,就算你是Vue新手,跟着本文操作也能轻松搞定。

一、为什么影院页面是购票APP的灵魂?

想象一下这个场景:用户已经决定看《流浪地球3》,接下来要做什么?选影院啊!这个页面决定了用户能否快速找到合适的影院、合适的时间、合适的座位——直接关系到用户会不会掏钱买单。

在我们的电影购票APP中,影院页面需要承载以下核心功能:

  • 影院列表展示 - 显示附近的影院信息
  • 筛选和排序 - 按距离、价格、设施等筛选
  • 场次选择 - 显示不同时间的排片
  • 路由跳转 - 流畅地跳转到选座页面

看起来复杂?别怕,咱们把它拆解成一个个小组件,就像搭乐高一样简单。

二、项目结构:先把房子盖好

在开始写代码前,得先规划好项目结构。这是我的目录安排,清晰得像整理过的衣柜:

src/
├── components/
│   ├── CinemaList.vue      # 影院列表
│   ├── CinemaFilter.vue    # 筛选器
│   ├── ShowTimeList.vue    # 场次列表
│   └── Common/
│       └── Loading.vue     # 加载动画
├── views/
│   ├── Cinema.vue          # 影院页面主组件
│   └── SeatSelection.vue   # 选座页面
├── router/
│   └── index.js            # 路由配置
└── assets/
    └── data/
        └── cinemas.json    # 模拟数据

三、影院页面组件设计:庖丁解牛式拆分

1. Cinema.vue - 主页面容器

这个组件就像乐高的底板,其他组件都在它上面拼接:

<template>
  <div class="cinema-page">
    <!-- 顶部筛选条 -->
    <cinema-filter 
      :filters="activeFilters"
      @filter-change="handleFilterChange"
    />
    
    <!-- 加载状态 -->
    <loading v-if="loading" />
    
    <!-- 影院列表 -->
    <cinema-list 
      v-else
      :cinemas="filteredCinemas"
      @cinema-click="handleCinemaClick"
    />
  </div>
</template>
<script>
import CinemaFilter from '@/components/CinemaFilter.vue'
import CinemaList from '@/components/CinemaList.vue'
import Loading from '@/components/Common/Loading.vue'
import cinemaData from '@/assets/data/cinemas.json'

export default {
  name: 'CinemaPage',
  components: {
    CinemaFilter,
    CinemaList,
    Loading
  },
  data() {
    return {
      loading: false,
      allCinemas: [],
      activeFilters: {
        region: '',
        service: [],
        sortBy: 'distance'
      }
    }
  },
  computed: {
    // 根据筛选条件过滤影院
    filteredCinemas() {
      let result = [...this.allCinemas]
      
      // 按区域筛选
      if (this.activeFilters.region) {
        result = result.filter(cinema => 
          cinema.region === this.activeFilters.region
        )
      }
      
      // 按服务筛选(IMAX、杜比等)
      if (this.activeFilters.service.length > 0) {
        result = result.filter(cinema =>
          this.activeFilters.service.every(service =>
            cinema.services.includes(service)
          )
        )
      }
      
      // 排序
      if (this.activeFilters.sortBy === 'distance') {
        result.sort((a, b) => a.distance - b.distance)
      } else if (this.activeFilters.sortBy === 'price') {
        result.sort((a, b) => a.minPrice - b.minPrice)
      }
      
      return result
    }
  },
  async created() {
    // 模拟API调用
    this.loading = true
    setTimeout(() => {
      this.allCinemas = cinemaData
      this.loading = false
    }, 800)
  },
  methods: {
    handleFilterChange(newFilters) {
      this.activeFilters = { ...newFilters }
    },
    handleCinemaClick(cinema) {
      // 跳转到该影院的场次页面
      this.$router.push(`/cinema/${cinema.id}/shows`)
    }
  }
}
</script>
<style scoped>
.cinema-page {
  min-height: 100vh;
  background-color: #f5f5f5;
}
</style>
2. CinemaFilter.vue - 智能筛选器

这个组件让用户能精准找到想要的影院,就像给影院装上了GPS:

<template>
  <div class="cinema-filter">
    <!-- 区域选择 -->
    <div class="filter-group">
      <h3>区域</h3>
      <div class="filter-options">
        <button 
          v-for="region in regions"
          :key="region"
          :class="['filter-btn', { active: activeFilters.region === region }]"
          @click="toggleRegion(region)"
        >
          {{ region }}
        </button>
      </div>
    </div>
    
    <!-- 特色服务 -->
    <div class="filter-group">
      <h3>特色服务</h3>
      <div class="filter-options">
        <button 
          v-for="service in services"
          :key="service.value"
          :class="['filter-btn', { active: activeFilters.service.includes(service.value) }]"
          @click="toggleService(service.value)"
        >
          {{ service.label }}
        </button>
      </div>
    </div>
    
    <!-- 排序 -->
    <div class="filter-group">
      <h3>排序</h3>
      <select v-model="activeFilters.sortBy" @change="emitFilterChange">
        <option value="distance">距离最近</option>
        <option value="price">价格最低</option>
      </select>
    </div>
  </div>
</template>
<script>
export default {
  name: 'CinemaFilter',
  props: {
    filters: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      regions: ['全部', '朝阳区', '海淀区', '东城区', '西城区'],
      services: [
        { value: 'imax', label: 'IMAX' },
        { value: 'dolby', label: '杜比影院' },
        { value: '4d', label: '4D厅' },
        { value: 'freePark', label: '免费停车' }
      ],
      activeFilters: { ...this.filters }
    }
  },
  watch: {
    filters: {
      handler(newVal) {
        this.activeFilters = { ...newVal }
      },
      deep: true
    }
  },
  methods: {
    toggleRegion(region) {
      this.activeFilters.region = 
        this.activeFilters.region === region ? '' : region
      this.emitFilterChange()
    },
    
    toggleService(service) {
      const index = this.activeFilters.service.indexOf(service)
      if (index > -1) {
        this.activeFilters.service.splice(index, 1)
      } else {
        this.activeFilters.service.push(service)
      }
      this.emitFilterChange()
    },
    
    emitFilterChange() {
      this.$emit('filter-change', { ...this.activeFilters })
    }
  }
}
</script>
<style scoped>
.cinema-filter {
  background: white;
  padding: 15px;
  border-bottom: 1px solid #eee;
  position: sticky;
  top: 0;
  z-index: 100;
}

.filter-group {
  margin-bottom: 15px;
}

.filter-group h3 {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #666;
}

.filter-options {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.filter-btn {
  padding: 6px 12px;
  border: 1px solid #ddd;
  border-radius: 15px;
  background: white;
  font-size: 12px;
  cursor: pointer;
  transition: all 0.3s;
}

.filter-btn.active {
  background: #ff2d51;
  color: white;
  border-color: #ff2d51;
}

.filter-btn:hover {
  border-color: #ff2d51;
}
</style>
3. CinemaList.vue - 影院列表展示

这个组件负责把影院信息漂亮地展示出来,让用户一眼相中:

<template>
  <div class="cinema-list">
    <div 
      v-for="cinema in cinemas"
      :key="cinema.id"
      class="cinema-item"
      @click="$emit('cinema-click', cinema)"
    >
      <div class="cinema-info">
        <h3 class="cinema-name">{{ cinema.name }}</h3>
        <p class="cinema-address">{{ cinema.address }}</p>
        <p class="cinema-distance">{{ cinema.distance }}km</p>
      </div>
      
      <div class="cinema-price">
        <span class="price">¥{{ cinema.minPrice }}</span>
        <span class="price-desc">起</span>
      </div>
      
      <div class="cinema-tags">
        <span 
          v-for="tag in cinema.tags"
          :key="tag"
          class="tag"
        >
          {{ tag }}
        </span>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'CinemaList',
  props: {
    cinemas: {
      type: Array,
      required: true
    }
  }
}
</script>
<style scoped>
.cinema-list {
  padding: 10px;
}

.cinema-item {
  background: white;
  padding: 15px;
  margin-bottom: 10px;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  cursor: pointer;
  transition: transform 0.2s;
}

.cinema-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

.cinema-info {
  margin-bottom: 10px;
}

.cinema-name {
  margin: 0 0 5px 0;
  font-size: 16px;
  color: #333;
}

.cinema-address {
  margin: 0 0 5px 0;
  font-size: 12px;
  color: #999;
}

.cinema-distance {
  margin: 0;
  font-size: 12px;
  color: #666;
}

.cinema-price {
  text-align: right;
  margin-bottom: 10px;
}

.price {
  font-size: 18px;
  color: #ff2d51;
  font-weight: bold;
}

.price-desc {
  font-size: 12px;
  color: #999;
}

.cinema-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.tag {
  padding: 2px 6px;
  background: #fff0f0;
  color: #ff2d51;
  border-radius: 3px;
  font-size: 10px;
}
</style>

四、路由配置:给页面装上导航系统

光有组件还不够,得让用户能通过URL访问到这些页面。这就轮到Vue Router出场了!

router/index.js - 路由配置中心
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 路由配置
const routes = [
  {
    path: '/',
    redirect: '/cinema'  // 默认跳转到影院页面
  },
  {
    path: '/cinema',
    name: 'Cinema',
    component: () => import('@/views/Cinema.vue'),
    meta: {
      title: '选择影院',
      keepAlive: true  // 缓存页面,提升用户体验
    }
  },
  {
    path: '/cinema/:cinemaId/shows',
    name: 'CinemaShows',
    component: () => import('@/views/CinemaShows.vue'),
    meta: {
      title: '选择场次'
    },
    props: true  // 将路由参数作为props传递
  },
  {
    path: '/cinema/:cinemaId/show/:showId/seat',
    name: 'SeatSelection',
    component: () => import('@/views/SeatSelection.vue'),
    meta: {
      title: '选择座位',
      requireAuth: true  // 需要登录
    }
  }
]

const router = new VueRouter({
  mode: 'history',  // 使用history模式,URL更美观
  base: process.env.BASE_URL,
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 返回页面时保持滚动位置
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  }
})

// 路由守卫 - 登录检查
router.beforeEach((to, from, next) => {
  // 动态设置页面标题
  if (to.meta.title) {
    document.title = `${to.meta.title} - 电影购票`
  }
  
  // 检查是否需要登录
  if (to.meta.requireAuth) {
    const isLoggedIn = localStorage.getItem('userToken')
    if (!isLoggedIn) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
      return
    }
  }
  
  next()
})

export default router

五、模拟数据:让页面活起来

没有真实数据?没关系,咱们先造一些模拟数据:

// assets/data/cinemas.json
[
  {
    "id": 1,
    "name": "万达影城(CBD店)",
    "address": "朝阳区建国路93号万达广场3层",
    "distance": 1.2,
    "minPrice": 39.9,
    "region": "朝阳区",
    "services": ["imax", "dolby", "freePark"],
    "tags": ["IMAX", "杜比全景声", "免费停车"]
  },
  {
    "id": 2, 
    "name": "UME影城(双井店)",
    "address": "朝阳区东三环中路65号富力广场5层",
    "distance": 2.1,
    "minPrice": 45.0,
    "region": "朝阳区", 
    "services": ["4d", "freePark"],
    "tags": ["4D厅", "巨幕", "会员优惠"]
  },
  {
    "id": 3,
    "name": "首都电影院(西单店)", 
    "address": "西城区西单北大街180号西单文化广场B1层",
    "distance": 3.8,
    "minPrice": 42.5,
    "region": "西城区",
    "services": ["dolby"],
    "tags": ["杜比影院", "交通便利"]
  }
]

六、在main.js中挂载路由

最后一步,把路由挂载到Vue实例上:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,  // 注入路由
  render: h => h(App)
}).$mount('#app')

七、进阶技巧:让体验更丝滑

1. 路由懒加载

上面路由配置中我们已经使用了懒加载(() => import()),这样每个页面组件会被打包成独立的chunk,只有访问时才加载,大大提升首屏速度。

2. 页面缓存策略

在App.vue中配置keep-alive:

<template>
  <div id="app">
    <keep-alive :include="cachedPages">
      <router-view />
    </keep-alive>
  </div>
</template>
<script>
export default {
  name: 'App',
  computed: {
    cachedPages() {
      return this.$route.meta.keepAlive ? [this.$route.name] : []
    }
  }
}
</script>
3. 错误处理

在路由配置中添加通配符路由处理404:

// 添加到routes数组的最后
{
  path: '*',
  name: 'NotFound',
  component: () => import('@/views/NotFound.vue')
}

八、避坑指南:我踩过的坑你们别踩了

  1. 路由重复点击警告:在router/index.js中添加以下代码解决:
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}
  1. 动态路由参数更新不触发组件更新:使用watch监听$route,或者在组件内使用beforeRouteUpdate守卫
  2. 筛选状态丢失:使用keep-alive配合滚动行为记忆,或者将筛选状态保存到Vuex

写在最后

看到这里,你已经掌握了Vue影院页面开发的核心技能!从组件拆分到路由配置,从基础功能到进阶优化,这套方案可以直接用到你的项目中。

记住,好的用户体验就像好的服务——用户感觉不到它的存在,但一旦缺少就会立刻察觉。现在就去打造让你的用户赞不绝口的购票体验吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

值引力

持续创作,多谢支持!

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

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

打赏作者

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

抵扣说明:

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

余额充值