Django+Vue开发生鲜电商平台之6.使用Vue实现商品类别和商品数据前台显示
商品分类分为一级、二级和三级,需要实现视图、配置路由,还需要利用反向引用实现Serializer的三层嵌套引用,实现分类详情需要继承自RetrieveModelMixin。在将后端的数据显示到前端时需要根据Vue定义的接口实现,遇到禁止跨域访问可以通过对服务端进行设置或前端代理设置来解决。进一步实现点击某一个商品分类下面显示出商品详情,具体包括分类显示、价格筛选、分页和排序等功能,搜索和点击导航栏
什么是胸怀?胸怀是人生的志向和抱负,胸怀是人格的品位和质量,胸怀是人对待世界万物气量和风度的定位。胸怀,能使弱者走过别人不敢走的路,攀上别人难以达到的高峰;胸怀,可以使先天低矮的人在别人眼里变得挺拔高大;胸怀,能使一名柔弱的女子充满大丈夫的英雄气概;胸怀,也使一个弱质变得体格健壮。
——马云
Github和Gitee代码同步更新:
https://github.com/PythonWebProject/Django_Fresh_Ecommerce;
https://gitee.com/Python_Web_Project/Django_Fresh_Ecommerce。
现在将DRF数据接口与前端显示相结合。
一、商品类别数据接口
由之前的效果图和需求分析可知,首页全部商品分类需要展示一级、二级和三级分类,而在搜索结果页只展示一级和二级分类,分类有两个Vue组件,即Header中的全部商品分类和左侧的某以及类别对应的分类导航栏,也对应两个数据接口。
先在apps/goods/views.py中定义商品类别数据View如下:
from rest_framework import mixins, viewsets, filters
from rest_framework.pagination import PageNumberPagination
from django_filters.rest_framework import DjangoFilterBackend
from .models import Goods, GoodsCategory
from .serializers import GoodsSerializer, CategorySerializer
from .filters import GoodsFilter
# Create your views here.
class GoodsPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
page_query_param = 'p'
max_page_size = 100
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
'''商品列表页,并实现分页、搜索、过滤、排序'''
queryset = Goods.objects.all()
serializer_class = GoodsSerializer
pagination_class = GoodsPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filter_class = GoodsFilter
search_fields = ['name', 'goods_brief', 'goods_desc']
ordering_fields = ['sold_num', 'market_price']
class CategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
'''商品分类列表数据'''
queryset = GoodsCategory.objects.all()
serializer_class = CategorySerializer
再在urls.py中配置路由如下:
from django.conf.urls import url, include
from django.views.static import serve
from rest_framework.documentation import include_docs_urls
from rest_framework.routers import DefaultRouter
import xadmin
from .settings import MEDIA_ROOT
from goods.views import GoodsListViewSet, CategoryViewSet
# Create a router and register our viewsets with it.
router = DefaultRouter()
# 配置goods的路由
router.register(r'goods', GoodsListViewSet, basename='goods')
# 配置categories的路由
router.register(r'categories', CategoryViewSet, basename='categories')
访问http://127.0.0.1:8000/categories/,显示:
显然,将所有的数据都发送到前端,但是根据前端的要求,不同级之间的类别具有附属和依赖的关系,而不是平级的关系,显然还没有达到效果,需要进行改进。
此时需要用到才定义模型GoodsCategory的字段parent_category时指定的related_name属性,即related_name='sub_cat'
,此属性表示可以反向引用,即通过夫类别可以通过该属性查询子类别,利用该属性实现Serializer的三层嵌套引用,从而实现类别的嵌套显示,serializers.py 如下:
from rest_framework import serializers
from .models import Goods, GoodsCategory
class TerCategorySerializer(serializers.ModelSerializer):
'''三级商品子类别序列化'''
class Meta:
model = GoodsCategory
fields = '__all__'
class SecCategorySerializer(serializers.ModelSerializer):
'''二级商品子类别序列化'''
sub_cat = TerCategorySerializer(many=True)
class Meta:
model = GoodsCategory
fields = '__all__'
class CategorySerializer(serializers.ModelSerializer):
'''一级商品类别序列化'''
sub_cat = SecCategorySerializer(many=True)
class Meta:
model = GoodsCategory
fields = '__all__'
class GoodsSerializer(serializers.ModelSerializer):
'''商品序列化'''
category = CategorySerializer()
class Meta:
model = Goods
fields = '__all__'
此时再访问http://127.0.0.1:8000/categories/,显示:
此时,以嵌套的形式在父类别中显示出子类别,并且属于三层嵌套。
现在需要实现获取某一个具体类别的详情(包括其基本信息和子类别),此时需要在路由中加入商品对应的id,只要使CategoryViewSet继承自mixins.RetrieveModelMixin,即可自动配置路由,无需再额外配置,即views.py如下:
class CategoryViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
'''商品分类列表数据'''
queryset = GoodsCategory.objects.all()
serializer_class = CategorySerializer
显示:
显然,此时地址中传入指定的id,只显示该id对应的类别的信息和其子类别的信息。
二、Vue展示商品分类
在测试前需要先在fresh_online目录下执行命令cnpm run dev
启动前端项目,启动后访问http://127.0.0.1:8080,可以看到:
显然,商品分类展示完整。
为了只是单独测试某一类数据而不影响其他数据的显示,因此其他数据都通过线上接口显示、待测试的数据通过本地接口测试。
在前端项目fresh_online中,在src/api/api.js中定义了数据API接口:
import axios from 'axios';
let host = 'http://shop.projectsedu.com';
let local_host = 'http://127.0.0.1:8000';
//获取商品类别信息
export const queryCategorygoods = params => { return axios.get(`${host}/indexgoods/`) }
//获取首页中的新品
export const newGoods = params => { return axios.get(`${host}/newgoods/`) }
//获取轮播图
export const bannerGoods = params => { return axios.get(`${host}/banners/`) }
//获取商品类别信息
export const getCategory = params => {
if('id' in params){
return axios.get(`${local_host}/categorys/`+params.id+'/');
}
else {
return axios.get(`${local_host}/categorys/`, params);
}
};
//获取热门搜索关键词
export const getHotSearch = params => { return axios.get(`${host}/hotsearchs/`) }
//获取商品列表
export const getGoods = params => { return axios.get(`${host}/goods/`, { params: params }) }
//商品详情
export const getGoodsDetail = goodId => { return axios.get(`${host}/goods/${goodId}`+'/') }
//获取购物车商品
export const getShopCarts = params => { return axios.get(`${host}/shopcarts/`) }
// 添加商品到购物车
export const addShopCart = params => { return axios.post(`${host}/shopcarts/`, params) }
//更新购物车商品信息
export const updateShopCart = (goodsId, params) => { return axios.patch(`${host}/shopcarts/`+goodsId+'/', params) }
//删除某个商品的购物记录
export const deleteShopCart = goodsId => { return axios.delete(`${host}/shopcarts/`+goodsId+'/') }
//收藏
export const addFav = params => { return axios.post(`${host}/userfavs/`, params) }
//取消收藏
export const delFav = goodsId => { return axios.delete(`${host}/userfavs/`+goodsId+'/') }
export const getAllFavs = () => { return axios.get(`${host}/userfavs/`) }
//判断是否收藏
export const getFav = goodsId => { return axios.get(`${host}/userfavs/`+goodsId+'/') }
//登录
export const login = params => {
return axios.post(`${host}/login/`, params)
}
//注册
export const register = parmas => { return axios.post(`${host}/users/`, parmas) }
//短信
export const getMessage = parmas => { return axios.post(`${host}/code/`, parmas) }
//获取用户信息
export const getUserDetail = () => { return axios.get(`${host}/users/1/`) }
//修改用户信息
export const updateUserInfo = params => { return axios.patch(`${host}/users/1/`, params) }
//获取订单
export const getOrders = () => { return axios.get(`${host}/orders/`) }
//删除订单
export const delOrder = orderId => { return axios.delete(`${host}/orders/`+orderId+'/') }
//添加订单
export const createOrder = params => {return axios.post(`${host}/orders/`, params)}
//获取订单详情
export const getOrderDetail = orderId => {return axios.get(`${host}/orders/`+orderId+'/')}
//获取留言
export const getMessages = () => {return axios.get(`${host}/messages/`)}
//添加留言
export const addMessage = params => {return axios.post(`${host}/messages/`, params, {headers:{ 'Content-Type': 'multipart/form-data' }})}
//删除留言
export const delMessages = messageId => {return axios.delete(`${host}/messages/`+messageId+'/')}
//添加收货地址
export const addAddress = params => {return axios.post(`${host}/address/`, params)}
//删除收货地址
export const delAddress = addressId => {return axios.delete(`${host}/address/`+addressId+'/')}
//修改收货地址
export const updateAddress = (addressId, params) => {return axios.patch(`${host}/address/`+addressId+'/', params)}
//获取收货地址
export const getAddress = () => {return axios.get(`${host}/address/`)}
其中,获取商品分类的接口为:
export const getCategory = params => {
if('id' in params){
return axios.get(`${host}/categorys/`+params.id+'/');
}
else {
return axios.get(`${host}/categorys/`, params);
}
};
显然,可以看到,如果参数中传入了id,则返回单个类别,否则返回所有类别。
而负责将类别数据显示到前端的是head.vue组件,位于src/views/head目录下,其通过赋值和循环将类别遍历出来:
<div class="main_cata" id="J_mainCata" v-show="showAllmenu">
<ul>
<li class="first" v-for="(item,index) in allMenuLabel" @mouseover="overChildrenmenu(index)" @mouseout="outChildrenmenu(index)">
<h3 style="background:url(../images/1449088788518670880.png) 20px center no-repeat;">
<router-link :to="'/app/home/list/'+item.id">{{item.name}}</router-link> </h3>
<div class="J_subCata" id="J_subCata" v-show="showChildrenMenu ===index" style=" left: 215px; top: 0px;">
<div class="J_subView" >
<div v-for="list in item.sub_cat">
<dl>
<dt>
<router-link :to="'/app/home/list/'+list.id">{{list.name}}</router-link>
</dt>
<dd>
<router-link v-for="childrenList in list.sub_cat" :key="childrenList.id" :to="'/app/home/list/'+childrenList.id">{{childrenList.name}}</router-link>
</dd>
</dl>
<div class="clear"></div>
</div>
</div>
</div>
</li>
</ul>
</div>
// ...
getMenu(){//获取菜单
getCategory({
params:{}
}).then((response)=> {
console.log(response)
this.allMenuLabel = response.data
})
.catch(function (error) {
console.log(error);
});
},
为了让数据在前端正常显示,还需要过滤类别数据、只显示一级类别,修改views.py如下:
class CategoryViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
'''
List: 显示商品所有分类列表数据
Retrieve: 获取单个商品分类详情
'''
queryset = GoodsCategory.objects.filter(category_type=1)
serializer_class = CategorySerializer
此时再访问http://127.0.0.1:8080,显示:
此时再访问,却访问不到商品分类,查看控制台提示很明显,同源策略禁止读取位于 http://127.0.0.1:8000/categorys/ 的远程资源。
,即禁止跨域访问,当前端口是 8080,而数据接口端口是8000,因此被浏览器自动拒绝,一种方式是对服务器进行设置,还有一种是通过前端代理解决,这里采用第一种方式:
首先在虚拟环境中执行pip install django-cors-headers
命令安装库,然后在后端settings.py中配置Cors:
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'apps.users.apps.UsersConfig',
'goods.apps.GoodsConfig',
'trade.apps.TradeConfig',
'user_operation.apps.UserOperationConfig',
'DjangoUeditor',
'xadmin',
'crispy_forms',
'django.contrib.admin',
'rest_framework',
'django_filters',
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 跨域访问设置
CORS_ORIGIN_ALLOW_ALL = True
此时再访问,显示:
此时不再报错,商品的各级分类也显示出来,但是可以看出来,全部分类右侧并未显示商品分类,这是因为一级分类的is_tab属性默认为False,改为True即可,可以在数据库中修改,也可以直接在后台管理系统修改。
head.vue中显示商品分类的逻辑也是通过循环实现,如下:
<template v-for="(item,index) in allMenuLabel">
<li>
<div v-if="item.is_tab">
<router-link :to="'/app/home/list/'+item.id" >{{item.name}}</router-link>
</div>
</li>
</template>
后台修改商品分类is_tab属性并刷新页面如下:
此时已经显示出商品分类。
三、Vue展示商品列表页数据和搜索
现在进一步实现点击某一个商品分类下面显示出商品详情,具体包括分类显示、价格筛选、分页和排序等功能。
通过项目可以看到,通过搜索和点击Tab页左侧显示的导航栏是不同的,其数据接口也不一样,head.vue如下:
<div class="head_search_hot">
<span>热搜榜:</span>
<router-link v-for="item in hotSearch" :to="'/app/home/search/'+item.keywords" :key="item.keywords">
{{item.keywords}}
</router-link>
</div>
// ...
<div class="main_nav_link" @mouseover="overAllmenu" @mouseout="outAllmenu">
<a class="meunAll">全部商品分类
<i class="iconfont"></i>
</a>
<div class="main_cata" id="J_mainCata" v-show="showAllmenu">
<ul>
<li class="first" v-for="(item,index) in allMenuLabel" @mouseover="overChildrenmenu(index)" @mouseout="outChildrenmenu(index)">
<h3 style="background:url(../images/1449088788518670880.png) 20px center no-repeat;">
<router-link :to="'/app/home/list/'+item.id">{{item.name}}</router-link> </h3>
<div class="J_subCata" id="J_subCata" v-show="showChildrenMenu ===index" style=" left: 215px; top: 0px;">
<div class="J_subView" >
<div v-for="list in item.sub_cat">
<dl>
<dt>
<router-link :to="'/app/home/list/'+list.id">{{list.name}}</router-link>
</dt>
<dd>
<router-link v-for="childrenList in list.sub_cat" :key="childrenList.id" :to="'/app/home/list/'+childrenList.id">{{childrenList.name}}</router-link>
</dd>
</dl>
<div class="clear"></div>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
可以看到,跳转路由不同,搜索跳转到/app/home/search/
,而点击导航栏跳转到/app/home/list/
。
再查看src/router/index.js如下:
{
path: 'list/:id',
name: 'list',
component: list,
meta: {
title: '列表',
need_log: false
}
},
{
path: 'search/:keyword',
name: 'search',
component: list,
meta: {
title: '搜索',
need_log: false
}
},
可以看到,两个路由绑定的组件是相同的,都是list,而在src/views/list/list.vue中:
getAllData () {
console.log(this.$route.params)
if(this.$route.params.id){
this.top_category = this.$route.params.id;
this.getMenu(this.top_category); // 获取左侧菜单列表
}else{
this.getMenu(null); // 获取左侧菜单列表
this.pageType = 'search'
this.searchWord = this.$route.params.keyword;
}
this.getCurLoc(); // 获取当前位置
this.getListData(); //获取产品列表
this.getPriceRange(); // 获取价格区间
},
getListData() {
if(this.pageType=='search'){
getGoods({
search: this.searchWord, //搜索关键词
}).then((response)=> {
this.listData = response.data.results;
this.proNum = response.data.count;
}).catch(function (error) {
console.log(error);
});
}else {
getGoods({
page: this.curPage, //当前页码
top_category: this.top_category, //商品类型
ordering: this.ordering, //排序类型
pricemin: this.pricemin, //价格最低 默认为‘’ 即为不选价格区间
pricemax: this.pricemax // 价格最高 默认为‘’
}).then((response)=> {
this.listData = response.data.results;
this.proNum = response.data.count;
}).catch(function (error) {
console.log(error);
});
}
},
getMenu(id) {
if(id != null){
getCategory({
id:this.$route.params.id
}).then((response)=> {
this.cateMenu = response.data.sub_cat;
this.currentCategoryName = response.data.name
}).catch(function (error) {
console.log(error);
});
}else {
getCategory({}).then((response)=> {
this.cateMenu = response.data;
this.isObject = false
}).catch(function (error) {
console.log(error);
});
}
},
可以看到,针对不同的参数有不同的请求方法和参数。
请求商品列表数据使用的是getListData()
方法,调用了getGoods()
方法,为了测试获取商品,将getGoods
API进行修改如下:
//获取商品列表
export const getGoods = params => { return axios.get(`${local_host}/goods/`, { params: params }) }
同时,向后端请求的参数有一个为top_category,即表示一级类别,请求该参数则返回这一类别下的所有类别,需要在后端定义一个过滤器,需要找到该一级分类下的所有二级分及其对应的商品,后端apps/goods/filters.py如下:
import django_filters
from django.db.models import Q
from .models import Goods
class GoodsFilter(django_filters.rest_framework.FilterSet):
'''商品过滤类'''
name = django_filters.CharFilter(field_name="name", lookup_expr='contains')
pricemin = django_filters.NumberFilter(field_name="market_price", lookup_expr='gte')
pricemax = django_filters.NumberFilter(field_name="market_price", lookup_expr='lte')
top_category = django_filters.NumberFilter(method='top_category_filter')
def top_category_filter(self, queryset, name, value):
'''自定义过滤'''
return queryset.filter(Q(category_id=value)|Q(category__parent_category_id=value)|Q(category__parent_category__parent_category_id=value))
class Meta:
model = Goods
fields = ['name', 'pricemin', 'pricemax']
显示:
此时,可以根据top_category进行筛选,再查看前端:
可以看到,已经实现了价格筛选、排序、分页等功能。
从之前的代码还可以看到搜索功能的实现:
if(this.pageType=='search'){
getGoods({
search: this.searchWord, //搜索关键词
}).then((response)=> {
this.listData = response.data.results;
this.proNum = response.data.count;
}).catch(function (error) {
console.log(error);
});
}
再测试搜索功能如下:
显然,已经实现了搜索功能。
更多推荐
所有评论(0)