朋友们,最近我接了个私活——用Vue搞个电影购票APP。本以为凭我三年摸鱼经验,这不是手到擒来?结果刚设计影院页面就差点阵亡。这玩意儿可比写TodoList刺激多了!
一、为什么影院页面是“组件设计修罗场”?
刚开始,我天真的以为影院页面不就显示个影院列表嘛!上手才发现,这里面的水比《泰坦尼克号》还深。
首先,用户在这页面要完成“选影院-选电影-选时间-选座位”的完整决策。光是状态管理就够喝一壶的:当前选中影院、日期筛选、影片类型、场次时间...这哪是影院页面,这分明是Vue版《密室逃脱》!
更别提那些细碎需求:要显示距离排序、价格筛选、特色服务(IMAX/4DX)、会员优惠...产品经理笑眯眯地说:“这个很简单吧?”我默默看了眼需求文档,感觉头发又少了几根。
二、影院页面组件拆解:把大象装进冰箱
经过一番挣扎,我把影院页面拆成了三个核心组件:
- 影院筛选栏 (CinemaFilter) - 负责各种筛选条件
- 影院列表 (CinemaList) - 展示影院卡片信息
- 影院卡片 (CinemaCard) - 单个影院的详细信息
这就好比把大象装冰箱——分三步!每个组件各司其职,代码瞬间清爽多了。
三、手把手 coding:从零搭建影院页面
来吧,展示完整代码!咱们用Vue3的Composition API来写,毕竟这年头不用Composition API,出门都不好意思跟人打招呼。
<template>
<div class="cinema-page">
<!-- 筛选组件 -->
<CinemaFilter
:filters="activeFilters"
@update:filters="handleFilterChange"
/>
<!-- 影院列表 -->
<CinemaList
:cinemas="filteredCinemas"
:loading="loading"
@cinema-click="handleCinemaSelect"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import CinemaFilter from './components/CinemaFilter.vue'
import CinemaList from './components/CinemaList.vue'
// 状态管理 - 这才是重头戏!
const cinemaList = ref([]) // 影院列表
const loading = ref(false) // 加载状态
const activeFilters = ref({ // 当前筛选条件
area: '',
movieType: '',
service: [],
date: ''
})
// 模拟API请求
const fetchCinemas = async () => {
loading.value = true
try {
// 这里假装调用了接口
const response = await mockApiGetCinemas()
cinemaList.value = response
} catch (error) {
console.error('获取影院列表失败:', error)
} finally {
loading.value = false
}
}
// 计算属性 - 过滤后的影院列表
const filteredCinemas = computed(() => {
let result = cinemaList.value
// 区域筛选
if (activeFilters.value.area) {
result = result.filter(cinema => cinema.area === activeFilters.value.area)
}
// 服务类型筛选
if (activeFilters.value.service.length > 0) {
result = result.filter(cinema =>
activeFilters.value.service.every(service =>
cinema.services.includes(service)
)
)
}
return result
})
// 事件处理
const handleFilterChange = (newFilters) => {
activeFilters.value = { ...activeFilters.value, ...newFilters }
}
const handleCinemaSelect = (cinema) => {
// 跳转到场次选择页面
router.push(`/schedule/${cinema.id}`)
}
onMounted(() => {
fetchCinemas()
})
</script>
四、核心组件深度剖析
1. CinemaFilter组件 - 筛选界的瑞士军刀
这个组件负责所有的筛选逻辑,我把它设计得足够灵活:
<template>
<div class="cinema-filter">
<!-- 区域选择 -->
<div class="filter-section">
<h4>区域</h4>
<div class="filter-options">
<button
v-for="area in areas"
:key="area"
:class="{ active: filters.area === area }"
@click="updateFilter('area', area)"
>
{{ area }}
</button>
</div>
</div>
<!-- 特色服务 -->
<div class="filter-section">
<h4>特色服务</h4>
<div class="filter-options">
<button
v-for="service in services"
:key="service.value"
:class="{ active: filters.service.includes(service.value) }"
@click="toggleService(service.value)"
>
{{ service.label }}
</button>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
filters: Object
})
const emit = defineEmits(['update:filters'])
const areas = ['全部', '朝阳区', '海淀区', '东城区', '西城区']
const services = [
{ label: 'IMAX', value: 'imax' },
{ label: '杜比影院', value: 'dolby' },
{ label: '4DX', value: '4dx' }
]
const updateFilter = (key, value) => {
emit('update:filters', { [key]: value })
}
const toggleService = (service) => {
const currentServices = [...props.filters.service]
const index = currentServices.indexOf(service)
if (index > -1) {
currentServices.splice(index, 1)
} else {
currentServices.push(service)
}
emit('update:filters', { service: currentServices })
}
</script>
2. CinemaCard组件 - 颜值担当的影院名片
影院卡片要展示的信息最多,设计时我特别注意了信息层级:
<template>
<div class="cinema-card" @click="$emit('cinema-click', cinema)">
<div class="cinema-header">
<h3 class="cinema-name">{{ cinema.name }}</h3>
<span class="cinema-distance">{{ cinema.distance }}</span>
</div>
<div class="cinema-location">
<span class="area">{{ cinema.area }}</span>
<span class="address">{{ cinema.address }}</span>
</div>
<!-- 服务标签 -->
<div class="service-tags">
<span
v-for="service in cinema.services"
:key="service"
class="service-tag"
:class="getServiceClass(service)"
>
{{ getServiceLabel(service) }}
</span>
</div>
<!-- 价格信息 -->
<div class="price-info">
<span class="current-price">¥{{ cinema.lowestPrice }}</span>
<span class="original-price">¥{{ cinema.originalPrice }}</span>
</div>
</div>
</template>
<script setup>
defineProps({
cinema: {
type: Object,
required: true
}
})
defineEmits(['cinema-click'])
// 服务类型映射
const serviceMap = {
imax: { label: 'IMAX', class: 'imax' },
dolby: { label: '杜比', class: 'dolby' },
'4dx': { label: '4DX', class: 'four-dx' }
}
const getServiceLabel = (service) => serviceMap[service]?.label || service
const getServiceClass = (service) => serviceMap[service]?.class || 'default'
</script>
五、状态管理:Vuex还是Composition API?
这是个经典问题。对于这种规模的项目,我选择直接用Composition API的自定义hook:
// hooks/useCinemaStore.js
import { ref, computed } from 'vue'
export function useCinemaStore() {
const cinemas = ref([])
const selectedCinema = ref(null)
const filters = ref({})
const filteredCinemas = computed(() => {
// 复杂的筛选逻辑...
return applyFilters(cinemas.value, filters.value)
})
const selectCinema = (cinema) => {
selectedCinema.value = cinema
}
const updateFilters = (newFilters) => {
filters.value = { ...filters.value, ...newFilters }
}
return {
cinemas,
selectedCinema,
filters,
filteredCinemas,
selectCinema,
updateFilters
}
}
六、性能优化:让你的APP飞起来
在实际测试中,我发现当影院数据超过100条时,筛选会出现明显卡顿。解决方案:
- 虚拟滚动 - 只渲染可见区域的影院卡片
- 防抖搜索 - 用户输入时延迟执行筛选
- 计算属性缓存 - 合理使用computed
// 防抖实现
import { ref } from 'vue'
export function useDebounce(fn, delay) {
const timeout = ref(null)
return (...args) => {
clearTimeout(timeout.value)
timeout.value = setTimeout(() => fn(...args), delay)
}
}
七、踩坑记录:那些年我交过的“学费”
- 响应式数据陷阱:直接修改props里的数组导致诡异bug
- 事件总线滥用:组件间通信混乱,后期难以维护
- CSS作用域:样式污染其他组件,建议使用scoped或CSS Modules
八、总结
通过这个影院页面组件的实战,我深刻体会到:好的组件设计就像搭乐高——每个零件独立且可组合。Vue的响应式系统让状态管理变得直观,而Composition API则提供了更好的逻辑复用。
现在我的电影APP已经上线,虽然用户还不多,但至少组件代码看起来挺专业的——毕竟,程序员最大的成就感不就是写出优雅的代码吗?
好了,我要去测试新功能了——顺便看场电影,这应该算工作吧?
Vue影院页面组件设计实战

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



