基于 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>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐