朋友们,有没有过这样的悲惨经历:和心上人约好看电影,结果打开购票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')
}
八、避坑指南:我踩过的坑你们别踩了
- 路由重复点击警告:在router/index.js中添加以下代码解决:
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
- 动态路由参数更新不触发组件更新:使用watch监听$route,或者在组件内使用beforeRouteUpdate守卫
- 筛选状态丢失:使用keep-alive配合滚动行为记忆,或者将筛选状态保存到Vuex
写在最后
看到这里,你已经掌握了Vue影院页面开发的核心技能!从组件拆分到路由配置,从基础功能到进阶优化,这套方案可以直接用到你的项目中。
记住,好的用户体验就像好的服务——用户感觉不到它的存在,但一旦缺少就会立刻察觉。现在就去打造让你的用户赞不绝口的购票体验吧!
840

被折叠的 条评论
为什么被折叠?



