【基于 Vue3+Pinia 实现纯前端仿有票网项目实战】
基于 Vue3+Pinia 实现纯前端仿有票网项目实战
在前端技术飞速发展的当下,Vue3 凭借其更优的性能、更灵活的组合式 API 成为主流框架,而 Pinia 作为 Vue 官方推荐的状态管理库,替代了传统的 Vuex,简化了状态管理逻辑。本文将带大家从零开始,使用 Vue3+Pinia 打造一个纯前端的仿有票网项目,涵盖首页展示、电影列表、购票选座等核心功能,帮助大家巩固 Vue3 生态的实战应用能力。
一、项目背景与技术选型
1. 项目背景
有票网是国内知名的票务平台,主要提供电影票、演出票等购票服务,其核心交互场景包括 “热门影片推荐”“影片详情查看”“场次选择”“座位预订” 等。本次项目为纯前端模拟实现,重点聚焦前端页面开发与状态管理逻辑,数据层面使用 Mock 模拟,不涉及后端接口联调。
2. 技术选型理由
- Vue3:相比 Vue2,Vue3 的组合式 API(Composition API)更适合复杂项目的逻辑拆分,同时支持 Tree-Shaking 减小打包体积,配合<script setup>语法糖能大幅简化代码。
- Pinia:作为 Vue 官方状态管理库,Pinia 移除了 Vuex 中的 Mutation 概念,直接通过 Action 修改 State,语法更简洁;同时支持 TypeScript 类型推导,开发体验更优,且无需手动注册,开箱即用。
- Vant 4:移动端 UI 组件库,提供了 “轮播图”“卡片”“日期选择器” 等高频组件,适配移动端场景,减少重复开发。
- Mock.js:用于生成模拟数据(如电影列表、影院信息、座位数据),避免依赖后端接口,实现纯前端开发。
- Vue Router 4:Vue3 配套的路由库,实现页面间的路由跳转,支持动态路由、嵌套路由等功能。
二、项目环境搭建
1. 初始化 Vue3 项目
首先确保本地安装了 Node.js(建议 v14+),通过 Vue CLI 创建项目:
# 安装Vue CLI(若未安装)
npm install -g @vue/cli
# 创建Vue3项目
vue create vue3-pinia-ticket
# 选择配置:Manually select features -> 勾选Babel、TypeScript、Router、Pinia、CSS Pre-processors
# 后续步骤按默认选项即可
2. 安装依赖
进入项目目录,安装 Vant 4 和 Mock.js:
cd vue3-pinia-ticket
npm install vant@next mockjs
3. 项目目录结构
初始化后的核心目录结构如下(重点关注标记部分):
vue3-pinia-ticket/
├── src/
│ ├── api/ # 接口请求(Mock请求封装)
│ ├── components/ # 公共组件(如Header、Footer、FilmCard)
│ ├── pages/ # 页面组件(首页、电影详情页、购票页)
│ ├── pinia/ # Pinia状态管理(电影、购物车、用户状态)
│ ├── router/ # 路由配置
│ ├── mock/ # Mock数据配置
│ ├── styles/ # 全局样式
│ ├── utils/ # 工具函数(如日期格式化)
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
└── package.json
三、核心功能实现(附关键代码)
1. Mock 数据配置(模拟后端接口)
在src/mock/index.ts中配置模拟数据,以 “电影列表” 和 “座位数据” 为例:
import Mock from 'mockjs';
// 模拟电影列表接口
Mock.mock('/api/films', 'get', () => {
return Mock.mock({
"code": 200,
"data|5-8": [ // 生成5-8条电影数据
{
"id|+1": 1, // 自增ID
"name": "@ctitle(4,8)", // 随机中文标题(4-8字)
"poster": "@image('200x300', '#f0f0f0', '#333', '电影海报')", // 模拟海报图
"score|7.0-9.9": 1, // 评分(7.0-9.9)
"type": "@cword(2,4)/@cword(2,4)", // 类型(如“喜剧/爱情”)
"duration": "@integer(90, 150)", // 时长(分钟)
"showDate": "@date('yyyy-MM-dd')" // 上映日期
}
]
});
});
// 模拟座位数据接口(参数:影院ID、场次ID)
Mock.mock(/\/api\/seats/, 'get', (options) => {
const { cinemaId, showId } = JSON.parse(options.body);
// 生成10行*15列座位,状态:0-可选,1-已售,2-锁定
const seats = Array(10).fill(0).map((_, row) => {
return Array(15).fill(0).map((_, col) => {
const seatNo = `${String.fromCharCode(65 + row)}${col + 1}`; // 座位号(如A1、B2)
return {
seatNo,
status: Mock.mock('@integer(0,1)') // 随机状态(0或1)
};
});
});
return { code: 200, data: { seats, cinemaId, showId } };
});
在main.ts中引入 Mock 配置,确保 Mock 生效:
import './mock';
2. Pinia 状态管理(核心状态设计)
本次项目需要管理 “电影列表”“已选座位”“购物车” 三个核心状态,分别创建对应的 Pinia Store。
(1)电影状态管理(src/pinia/filmStore.ts)
负责获取电影列表、存储当前选中的电影信息:
import { defineStore } from 'pinia';
import axios from 'axios';
export const useFilmStore = defineStore('film', {
state: () => ({
filmList: [] as any[], // 电影列表
currentFilm: null as any, // 当前选中的电影
loading: false // 加载状态
}),
actions: {
// 获取电影列表
async getFilmList() {
this.loading = true;
try {
const res = await axios.get('/api/films');
if (res.data.code === 200) {
this.filmList = res.data.data;
}
} catch (err) {
console.error('获取电影列表失败:', err);
} finally {
this.loading = false;
}
},
// 设置当前选中的电影
setCurrentFilm(film: any) {
this.currentFilm = film;
}
}
});
(2)购票状态管理(src/pinia/ticketStore.ts)
负责管理座位选择、场次信息、购票数量:
import { defineStore } from 'pinia';
import axios from 'axios';
export const useTicketStore = defineStore('ticket', {
state: () => ({
seats: [] as any[], // 座位列表
selectedSeats: [] as any[], // 已选座位
currentShow: null as any, // 当前选中的场次
ticketCount: 0 // 购票数量
}),
getters: {
// 计算已选座位的总价格(假设单张票价35元)
totalPrice(): number {
return this.selectedSeats.length * 35;
}
},
actions: {
// 获取座位数据
async getSeats(cinemaId: number, showId: number) {
try {
const res = await axios.post('/api/seats', { cinemaId, showId });
if (res.data.code === 200) {
this.seats = res.data.data.seats;
}
} catch (err) {
console.error('获取座位失败:', err);
}
},
// 切换座位选择(选中/取消)
toggleSeat(seat: any) {
const isSelected = this.selectedSeats.some((s: any) => s.seatNo === seat.seatNo);
if (isSelected) {
// 取消选中
this.selectedSeats = this.selectedSeats.filter((s: any) => s.seatNo !== seat.seatNo);
} else {
// 选中(仅允许选择可选状态的座位)
if (seat.status === 0) {
this.selectedSeats.push(seat);
}
}
this.ticketCount = this.selectedSeats.length;
},
// 重置购票状态(下单后)
resetTicketState() {
this.selectedSeats = [];
this.currentShow = null;
this.ticketCount = 0;
}
}
});
3. 核心页面实现
(1)首页(src/pages/Home.vue)
展示热门电影轮播图、电影列表,点击电影卡片跳转至详情页:
<template>
<div class="home">
<!-- 轮播图(使用Vant的Swipe组件) -->
<van-swipe class="swipe" :autoplay="3000">
<van-swipe-item v-for="(item, index) in filmList.slice(0,3)" :key="index">
<img :src="item.poster" alt="电影海报" class="swipe-img" />
</van-swipe-item>
</van-swipe>
<!-- 电影列表 -->
<div class="film-list">
<h2 class="title">热门影片</h2>
<div class="film-grid">
<div
class="film-card"
v-for="film in filmList"
:key="film.id"
@click="goToFilmDetail(film)"
>
<img :src="film.poster" alt="电影海报" class="film-img" />
<div class="film-info">
<h3 class="film-name">{{ film.name }}</h3>
<div class="film-meta">
<span class="score">评分:{{ film.score.toFixed(1) }}</span>
<span class="type">{{ film.type }}</span>
</div>
<div class="film-time">时长:{{ film.duration }}分钟</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useFilmStore } from '@/pinia/filmStore';
import { VanSwipe, VanSwipeItem } from 'vant';
const filmStore = useFilmStore();
const router = useRouter();
const filmList = ref([]);
// 页面挂载时获取电影列表
onMounted(() => {
filmStore.getFilmList().then(() => {
filmList.value = filmStore.filmList;
});
});
// 跳转至电影详情页
const goToFilmDetail = (film: any) => {
filmStore.setCurrentFilm(film);
router.push(`/film-detail/${film.id}`);
};
</script>
<style scoped>
/* 样式省略,可根据需求自定义,重点实现响应式布局 */
.swipe { height: 200px; }
.swipe-img { width: 100%; height: 100%; object-fit: cover; }
.film-list { padding: 16px; }
.film-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-top: 12px; }
.film-card { cursor: pointer; }
.film-img { width: 100%; height: 220px; border-radius: 8px; }
</style>
(2)购票页(src/pages/TicketPage.vue)
实现场次选择、座位预订功能,使用 Pinia 管理选中状态:
<template>
<div class="ticket-page">
<!-- 场次选择 -->
<div class="show-list">
<h3 class="cinema-name">{{ currentCinema.name }}</h3>
<div class="show-times">
<div
class="show-item"
v-for="show in showList"
:key="show.id"
@click="selectShow(show)"
:class="{ active: currentShow?.id === show.id }"
>
<div class="show-time">{{ show.time }}</div>
<div class="show-hall">影厅:{{ show.hall }}</div>
<div class="show-price">¥{{ show.price }}/张</div>
</div>
</div>
</div>
<!-- 座位选择 -->
<div class="seat-section" v-if="currentShow">
<h3 class="section-title">选择座位(已选:{{ selectedSeats.length }}个)</h3>
<!-- 座位图 -->
<div class="seat-grid">
<div class="seat-row" v-for="(row, rowIndex) in seats" :key="rowIndex">
<div
class="seat"
v-for="(seat, colIndex) in row"
:key="colIndex"
@click="toggleSeat(seat)"
:class="{
'seat-available': seat.status === 0,
'seat-sold': seat.status === 1,
'seat-selected': selectedSeats.some(s => s.seatNo === seat.seatNo)
}"
>
{{ seat.seatNo }}
</div>
</div>
</div>
<!-- 已选座位与价格 -->
<div class="ticket-summary" v-if="selectedSeats.length > 0">
<div class="selected-seats">
已选座位:<span v-for="(seat, idx) in selectedSeats" :key="idx">{{ seat.seatNo }} </span>
</div>
<div class="total-price">
总价:<span class="price">¥{{ totalPrice }}</span>
</div>
<button class="submit-btn" @click="submitOrder">确认下单</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useTicketStore } from '@/pinia/ticketStore';
import { useFilmStore } from '@/pinia/filmStore';
const ticketStore = useTicketStore();
const filmStore = useFilmStore();
const route = useRoute();
// 从路由参数获取影院ID
const cinemaId = ref(Number(route.params.cinemaId));
const currentCinema = ref({ name: '万达影城(CBD店)' }); // 模拟影院信息
const showList = ref([ // 模拟场次列表
{ id: 1, time: '10:30', hall: '3号厅', price: 35 },
{ id: 2, time: '14:00', hall: '5号厅(IMAX)', price: 45 },
{ id: 3, time: '18:30', hall: '2号厅', price: 35 }
]);
// 页面挂载时获取座位数据(默认取第一个场次)
onMounted(() => {
if (showList.value.length > 0) {
ticketStore.setCurrentShow(showList.value[0]);
ticketStore.getSeats(cinemaId.value, showList.value[0].id);
}
});
// 选择场次
const selectShow = (show: any) => {
ticketStore.setCurrentShow(show);
ticketStore.getSeats(cinemaId.value, show.id);
// 切换场次时重置已选座位
ticketStore.resetSelectedSeats();
};
// 切换座位选择
const toggleSeat = (seat: any) => {
ticketStore.toggleSeat(seat);
};
// 确认下单(模拟跳转至订单页)
const submitOrder = () => {
alert(`下单成功!座位:${ticketStore.selectedSeats.map(s => s.seatNo).join(', ')},总价:${ticketStore.totalPrice}元`);
ticketStore.reset</doubaocanvas>
更多推荐


所有评论(0)