安装
先安装node.js再安装vue,跟着官方文档来
还需要安装vue cli
Vue 3 在 2022 年 2 月 7 日 成为新的默认版本
tailwindCSS
tailwind CSS可以通过更加简单的CSS样式的名称来表示CSS样式。
其更简单的CSS样式表示方式如 w-24 表示宽度为 24,PT-6 表示 Padding Top 6 等。
官网:www.tailwindCSS.com
安装完后需要再main.js中引入
1
| import './asset/css/tailwind.css'
|
默认背景
在tailwind.config.js中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = { content: ["./src/**/*.{html,js,vue}"], theme: { extend: { colors: { primary: { 100: '#31343d', 200: '#9e9e9e', 300: '#212329', 700: '#101114' }, secondary: { 100: '#E2E2D5', 200: '#888883' } } }, }, plugins: [], }
|
不用tailwind的话这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| :root { --color-primary-100: #31343d; --color-primary-200: #9e9e9e; --color-primary-300: #212329; --color-primary-700: #101114; --color-secondary-100: #E2E2D5; --color-secondary-200: #888883; }
.my-element { background-color: var(--color-primary-300); color: var(--color-secondary-100); }
@media (max-width: 768px) { .my-element { background-color: var(--color-primary-700); color: var(--color-primary-200); } }
.my-element:hover { background-color: var(--color-primary-200); }
|
:root:这是一个CSS伪类
,它代表文档的根元素,通常是元素。
在CSS中,使用:root
可以定义全局样式变量,这些变量可以在文档的任何地方使用。
定义完以后,就可以用var()函数
来使用这些变量,当用的多时,改起来就很方便,可维护性好,同时代码可读性高
引入组件
在views文件夹下的页面引入components文件夹下的组件
1 2
| import Footer from '@/components/Footer.vue' import Header from '@/components/Header.vue'
|
在vue2里还需要在component属性里添上组件名
1 2 3
| components: { Header, Footer, MovieList }
|
跨域
在本项目中,直接使用前端的方式进行解决。通过配置前端的vue.config.js文件来实现设置一个代理,将这里的API请求转发到8000端口去。
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { devServer: { proxy: { '/api': { target: `http://127.0.0.1:8000/api`, changeOrigin: true, pathRewrite: { '^/api': '' } } } } };
|
这段代码是一个Node.js环境中使用的配置对象,通常用于配置开发服务器的代理。它是用来解决前端开发环境中的跨域问题。具体来说,这段代码定义了一个代理规则,用于将对/api的请求转发到另一个服务器。下面是各部分的具体解释:
综上所述,这段代码的主要作用是在本地开发时,将对本地/api路径的请求代理到另一个服务器(在这个例子中是本机的8000端口),解决因跨域限制而无法直接访问后端API的问题。
发送axios请求
安装axios
发送请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <script> import axios from 'axios'
export default { name: 'MovieList', data: function() { return { info: '' } }, mounted() { axios .get('/api/movie') .then( response => (this.info = response.data)) .catch(error => { console.log(error) }) }, }
</script>
|
- import axios from ‘axios’: 这行代码导入了axios库。axios是一个基于Promise的HTTP客户端,用于浏览器和node.js,主要用于发送HTTP请求。
- export default { … }: 这是ES6的模块导出语法,导出了一个Vue组件的配置对象。
- name: ‘MovieList’: 给Vue组件命名为MovieList。
- data: function() { … }: 定义组件的数据对象。这里定义了一个info属性,初始值为空字符串。这个属性将用于存储从API获取的电影数据。
- mounted(): 是Vue组件的生命周期钩子之一,会在组件被挂载到DOM后立即执行。在这个函数中,你可以执行一些初始化任务,比如发送API请求。
- axios.get(‘/api/movie’): 使用axios发送GET请求到/api/movie。这通常意味着你的后端有一个路由来处理/api/movie的GET请求,并返回电影数据。
- .then(response => (this.info = response.data)): 当请求成功时,将响应的数据(response.data)赋值给组件的info属性。这样,你就可以在组件的模板或其他地方使用info来显示电影数据。
- .catch(error => { console.log(error) }): 如果请求失败(比如网络问题或者服务器错误),.catch方法将捕获错误,并通过console.log打印出错误信息。
综合来看,这个组件在被加载时,会通过axios发送一个GET请求到/api/movie,然后将获取的电影数据存储在info属性中,如果请求失败,则在控制台打印错误信息。
使用模板显示首页数据
通过请求’/api/movie/‘接口得到的接口数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| [ { "id": 2, "movie_name": "人生大事", "release_year": 2022, "director": "刘江江", "scriptwriter": "刘江江", "actors": "朱一龙/杨恩又/王戈/刘陆/罗京民", "region": 1, "types": "剧情/家庭", "language": "汉语普通话", "release_date": "2022-06-24", "duration": "112分钟", "alternate_name": "Lighting Up The Stars / Funeral Family", "image_url": "https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2874262709.jpg", "rate": 7.3, "review": "殡葬师莫三妹(朱一龙 饰)在刑满释放不久后的一次出殡中,遇到了孤儿武小文(杨恩又 饰),小文的出现,意外地改变了莫三妹对职业和生活的态度。", "is_hot": false, "is_top": false, "quality": 2, "subtitle": "", "update_info": "", "update_progress": "", "download_info": "百度网盘:http://www.baidu.com 提取码:8888", "is_show": true, "is_free": false, "category": 1 }, ... ]
|
使用v-for遍历电影信息,需要遍历的是info,使用下面的语句:
1
| <div v-for="movie in info" :key="movie.id"/>
|
:key=”movie.id” 意味着每个通过 v-for 创建的元素都会绑定一个唯一的 key,其值为当前迭代的 movie 对象的 id 属性。这有助于Vue进行高效的DOM更新。
页面代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div class="flex items-center justify-center"> <div class="w-full px-2" style="max-width:1440px;"> <div id="movie-list" class="p-2 grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4"> <div class="movie" v-for="movie in info" :key="movie.id"> <a href="#"> <div class="relative"> <div class="" style="min-height: 259px; max-height: 300px;height: 274px;" > <img :src="movie.image_url" alt="" class="rounded h-full w-full" referrerPolicy="no-referrer"> </div> </div> <p> {{ movie.movie_name }} ({{ movie.release_year }})</p> <p class="text-sm text-primary-200"> {{ movie.language}}</p> </a> </div> </div> </div> </div> </template>
|
实现分页请求数据功能
电影列表的所有条目都在同一页面中显示。这样的设计带来了两个明显的问题:
- 首先,每次请求都会加载所有数据,由于图片加载时间较长,导致页面加载缓慢;
- 其次,这种设计并不利于提升用户体验。
实现分页接口
在DRF中,已经内置了分页类。不需要编写代码,只通过配置就能实现对特定页面数据的请求。
例如,通过在请求接口中添加一个page参数(如page=1表示请求第一页的数据),并在全局设置中定义每页显示的数据量,我们可以方便地实现数据的分页显示。
如果我们设置每页显示5条数据,那么当page=1时,接口将返回前5条数据;当page=2时,将返回第6至第10条数据,依此类推。
需在全局配置文件settings.py中设置DRF相关参数
1 2 3 4 5
| REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 12, }
|
更新前端模板
增加分页功能后,数据结构已经发生了变化。
在实现分页之前,数据结构是列表形式,列表中每个元素是一个字典;
现在,数据结构变为包含results的字典,真实的数据被嵌套在results键下。
所以,使用info.results来获取实际的数据列表。
改为
1
| <div class="movie" v-for="movie in info.results" :key="movie.id">
|
更新请求url
根据当前的页面参数动态构建请求URL
,然后发送请求以获取相应页面的数据。
查看第3页电影信息,则需要访问”127.0.0.1:8080?page=3”
如果直接访问”127.0.0.1:8080”, 则等价与”127.0.0.1:8080?page=1”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| methods: { get_movie_data: function() { let url = '/api/movie' const page = Number(this.$route.query.page); if (!isNaN(page) && (page !== 0)) { url = url + '/?page=' + page; }
axios .get(url) .then( response => (this.info = response.data)) .catch(error => { console.log(error) }) } }
|
创建分页组件
传值代替重复请求
将MovieList页面中请求得到的数据info
通过属性传递给分页组件。把MovieList当做父组件,而把Page.vue当做子组件。
1 2 3 4
| <template> <Page :info="info"> </template>
|
1 2 3 4 5 6
| <script> export default { props: ["info"], }; </script>
|
上一页下一页功能
通过info里的previous
和next
属性的值判断是否是第一页或最后一页
通过计算属性计算页码
- lastPage: 计算最后一页的页码。它通过将总记录数this.info.count除以每页的记录数(pageSize,这里设为12),并使用Math.ceil向上取整得到。
- prePage: 计算上一页的页码。如果当前页码current大于1,上一页为current - 1,否则上一页为1。
- nextPage: 计算下一页的页码。如果当前页码current小于最后一页lastPage,下一页为current + 1,否则保持当前页码不变。
通过方法实现分页
- getPageFromUrl: 从
URL
的查询参数中获取page参数
,并将其转换为数字类型。如果page存在且有效,则返回该值,否则返回1(默认页码)。
- goToPage: 负责处理页码的跳转逻辑。
首先,设置current为传入的page参数。
最后,使用Vue Router
的push方法更新URL,从而触发页面跳转。
但url更新了,页面还没更新,需要通过新的url来重新获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| computed: { lastPage() { let pageSize = 12; return Math.ceil(this.info.count / pageSize); },
prePage() { if (this.current > 1) { return this.current - 1; } return 1; },
nextPage() { if (this.current < this.lastPage) { return this.current + 1; } return this.current; }, }, mounted() { this.current = this.getPageFromUrl(); }, methods: { getPageFromUrl() { const page = Number(this.$route.query.page); return page ? page : 1; }, goToPage(page) { this.current = page; this.$router.push({ query: { page } }); }, },
|
使用watch来监听路由变化。当检测到路由变化时,我们可以调用getMovieData方法来重新获取数据
,并将新数据渲染到页面上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <script> import axios from "axios"; import Page from "@/components/Page.vue";
export default { name: "MovieList", data: function () { return { info: "", }; }, components: { Page }, mounted() { this.get_movie_data(); }, methods: { get_movie_data: function () { let url = "/api/movie"; const page = Number(this.$route.query.page); if (!isNaN(page) && page !== 0) { url = url + "/?page=" + page; }
axios .get(url) .then((response) => (this.info = response.data)) .catch((error) => { console.log(error); }); }, }, watch: { $route() { this.get_movie_data(); }, }, }; </script>
|
电影详情页实现
创建路由
1 2 3 4 5
| { path: "/movie/:id", name: "MovieDetail", component: MovieDetail },
|
path: “/movie/:id”: 表示匹配的URL路径。这里的:id是一个动态参数,可以匹配如/movie/1、/movie/123等多种形式的URL。
访问这样的URL时,:id部分会被解析为参数,可以在组件中通过this.$route.params.id来获取这个参数的值。
数据获取
1 2 3 4 5
| get_movie_info: function () { axios .get("/api/movie/" + this.$route.params.id) .then((response) => (this.movie = response.data)); },
|
将响应赋值给 movie 数据属性,
就可以通过如{{movie.movie_name}}
、{{movie.release_year}}
来显示数据了
搜索功能接口实现
搜索是非常常见的功能,django中有很多包,可以拿来即用。在本项目中,使用的是django-filter。
安装和配置django-filter
安装
1
| pip install django-filter
|
在views.py上配置django-filter的相关属性
1 2 3 4 5 6 7
| from django_filters.rest_framework import DjangoFilterBackend
class MovieViewSet(viewsets.ModelViewSet): queryset = Movie.objects.all() serializer_class = MovieSerializer filter_backends = (filters.DjangoFilterBackend,) filterset_fields = ('movie_name',)
|
配置settings.py全局文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| INSTALLED_APPS = [ 'django_filters', ]
REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 12, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), }
|
配置完成后,我们就可以直接请求接口了,请求的url示例如下:
1
| http://127.0.0.1:8000/api/movie/?movie_name=我不是药神
|
模糊查询
在MovieViewSet配置中,设置了filterset_fields, 它只能精确查询,比如查询“我不是药神”可以查到一条结果,但是如果查询“药神”,返回结果就是空。
实现模糊查询
在MovieViewSet中,配置一个filterset_class, 定义一个类,命名“MovieFilter”, 具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from django_filters.rest_framework import DjangoFilterBackend
class MovieFilter(filters.FilterSet): movie_name = filters.CharFilter(lookup_expr='icontains') class Meta: model = Movie fields = ['movie_name']
class MovieViewSet(viewsets.ModelViewSet): queryset = Movie.objects.all() serializer_class = MovieSerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = MovieFilter
|
MovieFilter 类 这是一个使用 django-filter 库定义的过滤器类,它继承自 filters.FilterSet。这个类定义了如何根据不同的字段过滤 Movie 模型的查询结果。
movie_name = filters.CharFilter(lookup_expr=’icontains’): 定义了一个字符过滤器,用于对 movie_name 字段进行过滤。icontains 表示不区分大小写的包含,即模糊查询。
在 Meta 子类中,指定了以下内容:
model = Movie: 指明这个过滤器是用于 Movie 模型的。
fields = [‘movie_name’]: 指定可以用于过滤的字段。
注释掉的行 # filterset_fields = (‘movie_name’,) 是另一种指定过滤字段的方法,它被注释掉了,使用了更灵活的 MovieFilter 类。
实现搜索页面
接收名为search的新参数,其值来源于this.$route.query.search。接收到这个值后,我们将其与其他参数一起加入到一个URL对象中
MovieList.vue代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <script> import axios from "axios"; import Page from "@/components/Page.vue";
export default { name: "MovieList", data: function () { return { info: "", }; }, components: { Page }, mounted() { this.get_movie_data(); }, methods: { get_movie_data: function () { let url = "/api/movie"; const page = Number(this.$route.query.page); const search = this.$route.query.search; const params = new URLSearchParams();
if (page) { params.append("page", page); } if (search) { params.append("movie_name", search); } url = url + "?" + params.toString();
axios .get(url) .then((response) => (this.info = response.data)) .catch((error) => { console.log(error); }); }, }, watch: { $route() { this.get_movie_data(); }, }, }; </script>
|
Page.vue也需要更新,不然搜索后点击跳转,url不包含搜索的参数,会显示所有电影
1 2 3 4 5 6 7
| goToPage(page) { this.current = page; const params = { ...this.$route.query }; params.page = page; this.$router.push({ query: params }); }
|
实现顶部导航栏分类功能
创建分类接口
1 2 3 4 5 6
| from movie.models import Movie, Category from movie.serializers import MovieSerializer, CategorySerializer
class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer
|
在serializers下创建CategorySerializer
1 2 3 4 5 6
| class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = '__all__'
|
在url.py里注册url
1 2 3 4 5 6 7 8 9
| from django.contrib import admin from django.urls import path, include from rest_framework.routers import DefaultRouter from movie import views
router = DefaultRouter() router.register(r'movie', views.MovieViewSet) router.register(r'category', views.CategoryViewSet)
|
分类信息组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template> <div id="nav" class="px-4"> <ul class="md:flex items-center space-x-4 ml-2"> <li> <a href="/">首页</a> </li> <li v-for="category in info.results" v-bind="category.id" class="dropdown-menu flex items-center relative hover: cursor-pointer select-none" > {{ category.category_name }} </li> </ul> </div> </template>
<script> import axios from "axios";
export default { name: "Category", data: function () { return { info: "", }; }, mounted() { this.get_category_info(); }, methods: { get_category_info() { axios.get("/api/category/").then((response) => { this.info = response.data; }); }, }, }; </script>
|
在Header组件中,导入Category组件,实现代码的模块化和易维护性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <div id="header" class="h-12 py-1 bg-primary-100 flex items-center justify-center"> <div class="w-full px-4" style="max-width: 1440px"> <div class="flex justify-between"> <div class="flex items-center"> <a href="/"> <img src="../assets/images/logo.png" style="height: 39px" /> </a> <Category /> </div> </template>
<script> import Category from "./Category.vue"; export default { components: { Category }, name: "Header", data() { return { keyword: "" }; }, methods: { searchMovies() { const keyword = this.keyword.trim(); this.$router.push({ name: "home", query: { search: keyword } }); }, }, }; </script>
|