实战项目二:交互式电影信息与评论看板 (MovieDash)
这个项目更能深刻理解Vue.js
- 数据异步加载与状态管理: 需要处理更复杂的加载状态、错误状态。
- 组件化思想的深度应用: 我们会将页面拆分为多个可复用的组件(如电影卡片、评分组件、评论列表)。
- 动态路由与参数传递: 通过路由传递电影ID,展示不同电影的详情页。
- 更丰富的用户交互: 实现打分、发表评论、实时搜索等功能。
- 数据驱动视图的完美体现: 用户的每一个操作都会动态地、响应式地更新页面视图。
项目目标: 创建一个单页面应用,用户可以浏览热门电影列表,点击查看电影详情,并对其进行评分和发表评论。
技术栈: Vue 3 + Vite + Pinia (状态管理) + Element Plus (UI库) + Axios
第一步:项目初始化与环境准备
【操作】: 这部分与上一个项目类似,但我们会加入Pinia来进行状态管理。
-
创建Vue项目:
在你的工作目录下,打开命令行/终端,运行:npm create vue@latest根据提示操作:
Project name:movie-dashSelect a framework:VueSelect a variant:Customize with create-vue...Add TypeScript?:NoAdd JSX Support?:NoAdd Vue Router for Single Page Application development?:Yes<-- 必须Add Pinia for state management?:Yes<-- 必须Add Vitest for Unit Testing?:NoAdd an End-to-End Testing Solution?:NoAdd ESLint for code quality?:Yes选上Add Prettier for code formatting?:Yes选上- 试验特性两个都不选
-
安装依赖:
cd movie-dash npm install npm install element-plus axios -
配置Element Plus:
打开src/main.js,引入Element Plus并全局注册。// src/main.js import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount('#app') -
启动开发服务器:
npm run dev你的新项目已经在本地运行起来了。
如果打开一些文件发现有报错,就把主目录下的eslin那个文件重新命名.eslintrc.cjs
第二步:后端API模拟
为了让前端能独立开发,我们先不急着连接真实后端,而是使用一个API模拟工具。json-server是一个绝佳的选择,它能让你用一个json文件快速启动一个功能完备的RESTful API。
【操作】:
- 全局安装
json-server:npm install -g json-server - 创建数据文件: 在你的项目根目录(
movie-dash/)下,创建一个db.json文件,并粘贴以下内容。{ "movies": [ { "id": 1, "title": "肖申克的救赎", "director": "弗兰克·德拉邦特", "year": 1994, "poster": "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp", "rating": 9.7, "plot": "20世纪40年代末,小有成就的青年银行家安迪因涉嫌杀害妻子及她的情人而锒铛入狱..." }, { "id": 2, "title": "霸王别姬", "director": "陈凯歌", "year": 1993, "poster": "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2561716440.webp", "rating": 9.6, "plot": "段小楼与程蝶衣是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合天衣无缝..." }, { "id": 3, "title": "阿甘正传", "director": "罗伯特·泽米吉斯", "year": 1994, "poster": "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2372307693.webp", "rating": 9.5, "plot": "阿甘于二战结束后不久出生在美国南方阿拉巴马州一个闭塞的小镇,他先天弱智,智商只有75..." } ], "comments": [ { "id": 1, "movieId": 1, "author": "影迷张三", "content": "永恒的经典,每次看都有新的感动!", "rating": 5, "createdAt": "2023-10-28T10:00:00Z" }, { "id": 2, "movieId": 1, "author": "观众李四", "content": "希望与自由,永远是人类最光辉的主题。", "rating": 5, "createdAt": "2023-10-28T11:30:00Z" } ] } - 启动API服务器: 在项目根目录打开新的命令行窗口,运行:
现在,你就拥有了一个运行在json-server --watch db.jsonhttp://localhost:3000的API服务器!你可以通过以下地址访问数据:http://localhost:3000/movies(获取所有电影)http://localhost:3000/movies/1(获取ID为1的电影)http://localhost:3000/comments?movieId=1(获取ID为1的电影的所有评论)
第三步:页面与组件设计
我们将构建两个主要页面:电影列表页 (HomeView) 和 电影详情页 (MovieDetailView)。
3.1 改造主布局 App.vue
【操作】: 打开src/App.vue,将其修改为一个经典的后台布局,包含头部和主内容区。
<template>
<el-container class="app-container">
<el-header class="app-header">
<div class="logo">
<img src="@/assets/logo.svg" alt="logo" />
<span>MovieDash 电影看板</span>
</div>
</el-header>
<el-main class="app-main">
<!-- 路由视图:所有页面将在这里被渲染 -->
<router-view />
</el-main>
</el-container>
</template>
<script setup>
import { RouterView } from 'vue-router';
</script>
<style scoped>
.app-container {
height: 100vh;
}
.app-header {
background-color: #2c3e50;
color: white;
display: flex;
align-items: center;
padding: 0 20px;
}
.logo {
display: flex;
align-items: center;
}
.logo img {
height: 40px;
margin-right: 10px;
}
.logo span {
font-size: 20px;
font-weight: bold;
}
.app-main {
padding: 20px;
background-color: #f4f7f6;
}
</style>
3.2 创建电影卡片组件 MovieCard.vue
这是一个可复用的组件,用于在列表页展示单个电影的信息。
【操作】: 在src/components目录下创建MovieCard.vue。
<template>
<el-card class="movie-card" shadow="hover">
<div class="poster-wrapper">
<img :src="movie.poster" class="poster" alt="poster" />
</div>
<div class="info">
<h3 class="title">{{ movie.title }} ({{ movie.year }})</h3>
<p class="director">导演: {{ movie.director }}</p>
<div class="rating">
<el-rate
:model-value="movie.rating"
disabled
show-score
text-color="#ff9900"
:score-template="`${movie.rating} 分`"
/>
</div>
</div>
</el-card>
</template>
<script setup>
// 使用defineProps来接收从父组件传递过来的数据
defineProps({
movie: {
type: Object,
required: true,
},
});
</script>
<style scoped>
.movie-card {
cursor: pointer;
transition: all 0.3s;
}
.movie-card:hover {
transform: translateY(-5px);
}
.poster-wrapper {
width: 100%;
padding-top: 140%; /* 保持图片比例 */
position: relative;
}
.poster {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.info {
padding: 14px;
}
.title {
margin: 0 0 8px 0;
font-size: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.director {
color: #606266;
font-size: 14px;
margin: 0 0 8px 0;
}
</style>
3.3 构建电影列表页 HomeView.vue
这个页面将获取所有电影数据,并使用MovieCard组件来展示它们。
【操作】: 打开src/views/HomeView.vue,用以下内容覆盖。
<template>
<div v-loading="loading" class="home-view">
<el-row :gutter="20">
<el-col
v-for="movie in movies"
:key="movie.id"
:xs="24" :sm="12" :md="8" :lg="6" :xl="4"
>
<!-- 使用v-for循环渲染MovieCard组件,并通过props传递movie数据 -->
<router-link :to="`/movie/${movie.id}`">
<movie-card :movie="movie" />
</router-link>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import MovieCard from '../components/MovieCard.vue';
import { RouterLink } from 'vue-router';
const movies = ref([]);
const loading = ref(true);
const fetchMovies = async () => {
try {
const response = await axios.get('http://localhost:3000/movies');
movies.value = response.data;
} catch (error) {
console.error('获取电影列表失败', error);
} finally {
loading.value = false;
}
};
// 在组件挂载后执行数据获取
onMounted(fetchMovies);
</script>
<style scoped>
.home-view .el-col {
margin-bottom: 20px;
}
a {
text-decoration: none;
}
</style>
第四步:构建电影详情页与路由
4.1 创建电影详情页 MovieDetailView.vue
这个页面将根据URL中的电影ID,获取并展示特定电影的详细信息和评论。
【操作】: 在src/views目录下创建MovieDetailView.vue。
<template>
<div v-loading="loading" class="movie-detail-view">
<el-page-header @back="goBack" title="返回列表" />
<el-card v-if="movie" class="box-card">
<el-row :gutter="20">
<el-col :span="8">
<img :src="movie.poster" class="detail-poster" />
</el-col>
<el-col :span="16">
<h1>{{ movie.title }} ({{ movie.year }})</h1>
<p><strong>导演:</strong> {{ movie.director }}</p>
<div class="rating-section">
<strong>豆瓣评分:</strong>
<el-rate v-model="movie.rating" disabled size="large" show-score score-template="{value} 分" />
</div>
<p><strong>剧情简介:</strong></p>
<p class="plot">{{ movie.plot }}</p>
</el-col>
</el-row>
</el-card>
<!-- 评论区 (后续可以添加更多交互) -->
<el-card v-if="comments.length > 0" class="box-card">
<template #header><h3>观众评论</h3></template>
<div v-for="comment in comments" :key="comment.id" class="comment-item">
<p><strong>{{ comment.author }}</strong> <el-rate :model-value="comment.rating" disabled size="small" /></p>
<p>{{ comment.content }}</p>
<el-divider />
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
const route = useRoute();
const router = useRouter();
const movie = ref(null);
const comments = ref([]);
const loading = ref(true);
const movieId = route.params.id; // 从路由中获取ID
const fetchData = async () => {
try {
// 并行获取电影详情和评论
const [movieRes, commentsRes] = await Promise.all([
axios.get(`http://localhost:3000/movies/${movieId}`),
axios.get(`http://localhost:3000/comments?movieId=${movieId}`)
]);
movie.value = movieRes.data;
comments.value = commentsRes.data;
} catch (error) {
console.error('获取详情失败', error);
} finally {
loading.value = false;
}
};
const goBack = () => {
router.back();
};
onMounted(fetchData);
</script>
<style scoped>
.box-card {
margin-top: 20px;
}
.detail-poster {
width: 100%;
}
.rating-section {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
}
.plot {
line-height: 1.8;
color: #606266;
}
.comment-item {
margin-bottom: 15px;
}
</style>
4.2 配置动态路由
我们需要在路由配置中,告诉Vue Router如何处理像/movie/1, /movie/2这样的动态URL。
【操作】: 打开src/router/index.js,添加详情页的路由规则。
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
// 【新增】动态路由规则
// :id 是一个动态段,它会匹配/movie/后面的任何字符串,并作为参数存储在路由中
{
path: '/movie/:id',
name: 'movie-detail',
// 路由懒加载,只有当访问这个页面时,才会加载对应的组件文件
component: () => import('../views/MovieDetailView.vue')
}
]
})
export default router
第五步:运行与验证
现在,你的交互式电影看板项目已经基本完成了!
- 确保你的
json-server仍在运行。 - 确保你的Vue开发服务器(
npm run dev)也在运行。 - 打开浏览器:
- 访问
http://localhost:5173/(或你的端口),你应该能看到电影列表。 - 点击任意一张电影卡片,页面应该会跳转到详情页,URL会变成类似
/movie/1的形式,并展示该电影的详细信息和评论。 - 点击详情页的“返回列表”按钮,可以回到主页。
- 访问

总结与下一步
恭喜你!你刚刚从零开始,完整地构建了一个比CRUD更复杂的、交互性更强的Vue单页面应用。
通过这个项目,你深刻实践了:
- 组件化开发: 将UI拆分为可复用的
MovieCard组件。 - 父子组件通信: 通过
props将电影数据传递给卡片组件。 - 异步数据获取: 使用
axios和async/await从模拟API加载数据,并处理加载状态。 - 动态路由: 使用Vue Router处理带参数的URL,实现页面间的跳转和数据传递。
- Vue响应式系统: 你所有的操作都是在修改JS数据(如
movies.value = ...),而Vue自动帮你更新了整个复杂的DOM视图。
下一步你可以挑战的功能:
- 在详情页添加一个“发表评论”的表单,通过
POST请求将新评论提交到json-server。 - 引入Pinia,创建一个
movieStore来集中管理电影列表的状态,避免在多个组件中重复请求。 - 在列表页添加搜索框和排序功能,通过修改API请求参数来实现动态筛选和排序。
更多推荐
所有评论(0)