安装

先安装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
/** @type {import('tailwindcss').Config} */
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);
}

/* 响应式设计 - 假设我们想要在屏幕宽度小于768px时改变样式 */
@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的请求转发到另一个服务器。下面是各部分的具体解释:

  • devServer: 这是webpack开发服务器的配置对象。在这里,我们定义了开发服务器的额外配置,比如代理设置。
  • proxy: proxy属性用于定义一个或多个代理规则。这些规则定义了如何将某些API请求从开发服务器转发到实际的后端API服务器。
  • ‘/api’: 这是一个路径匹配器。它告诉开发服务器:任何URL路径以/api开始的请求都应该被代理到定义在target属性中的服务器。
  • target: 这个属性定义了要代理到的目标服务器的地址。在这个例子中,目标服务器是http://127.0.0.1:8000/api。这意味着所有匹配/api的请求都会被转发到这个地址。
  • changeOrigin: 设置为true时,开发服务器会在代理请求时修改HTTP请求头中的host值为目标地址的host值。这通常用于绕过由于host不匹配而引起的一些安全限制。
  • pathRewrite: 这是路径重写规则。’^/api’: ‘’表示将匹配到的路径中的/api替换为空字符串。举个例子,如果你的前端代码中发出一个请求到/api/user,那么请求会被转发到http://127.0.0.1:8000/api/user,并且因为pathRewrite的设置,最终请求的路径会变成http://127.0.0.1:8000/user。

综上所述,这段代码的主要作用是在本地开发时,将对本地/api路径的请求代理到另一个服务器(在这个例子中是本机的8000端口),解决因跨域限制而无法直接访问后端API的问题。

发送axios请求

安装axios

1
cnpm install 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请求
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
# DRF设置
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' // /api/movie/?page=3&movie_name=我
// 获取page参数值
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
<!-- MovieList.vue -->
<template>
<Page :info="info">
</template>
1
2
3
4
5
6
// Page.vue
<script>
export default {
props: ["info"],
};
</script>

上一页下一页功能

通过info里的previousnext属性的值判断是否是第一页或最后一页
通过计算属性计算页码

  • 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"; // /api/movie/?page=3&movie_name=我
// 获取page参数值
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',
]


# DRF设置
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_fields = ('movie_name',)
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"; // /api/movie/?page=3&movie_name=我
// 获取page参数值
const page = Number(this.$route.query.page);
// 获取search参数
const search = this.$route.query.search;
const params = new URLSearchParams();

// if (!isNaN(page) && page !== 0) {
// url = url + "/?page=" + page;
// }
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>