总页面
二次封装Response模块
# drf提供的Response,前端想接收到的格式 {code:xx,msg:xx}
后端返回,前端收到:APIResponse(tokne='asdfa.asdfas.asdf')---->{code:100,msg:成功,token:asdfa.asdfas.asdf} APIResponse(code=101,msg='用户不存在') ---->{code:101,msg:用户不存在} APIResponse(results=[{},{}])---->{code:100,msg:成功,results:[{},{}]} APIResponse(msg='创建成功')---->{code:100,msg:创建成功} APIResponse(token=sd.11.22,icon='用户头像')---->{code:100,msg:创建成功,token:sd.11.22,icon:'用户头像'} APIResponse(msg='创建成功',headers={'xx':xxx})---->{code:100,msg:创建成功}
# 开始封装:
# utills/common_response.py from rest_framework.response import Response class APIResponse(Response): def __init__(self, code=100, msg='成功', status=None, headers=None, **kwargs): data = {'code': code, 'msg': msg} if kwargs: # kwargs={token:xx,icon:zz} data.update(kwargs) # super().__init__()---等同于 -->Response(data=data) super().__init__(data=data, headers=headers, status=status)
# views.py from utils.common_response import APIResponse class TestResponseView(APIView): def get(self, request): # retur n APIResponse(token='ss.ee.ss',icon='/media/icon/default.png') # return APIResponse(msg='创建成功') # return APIResponse(msg='用户不存在',code=101) # return APIResponse(results=[{},{}]) return APIResponse(headers={'xx': 'yy'}) # raise APIException(detail='用户名或密码错误') # {code:999,msg:用户名或密码错误}
admin和国际化问题
# admin 注释掉,重新启动
* url中注册app
* 重新迁移
# 国际化LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False
5个视图扩展类封装
# utills/mixins.py from rest_framework.mixins import ListModelMixin, CreateModelMixin, DestroyModelMixin, UpdateModelMixin, \ RetrieveModelMixin from .common_response import APIResponse class CommonListModelMixin(ListModelMixin): def list(self, request, *args, **kwargs): # Response 的对象---》res.data res = super().list(request, *args, **kwargs) return APIResponse(results=res.data) # {code:100,msg:成功,results:[{},{},{}]} class CommonCreateModelMixin(CreateModelMixin): def create(self, request, *args, **kwargs): res = super().create(request, *args, **kwargs) return APIResponse(msg='新增成功', result=res.data) # {code:100,msg:新增成功,result:{}} class CommonDestroyModelMixin(DestroyModelMixin): def destroy(self, request, *args, **kwargs): super().destroy(request, *args, **kwargs) return APIResponse(msg='删除成功') # {code:100,msg:删除成功} class CommonUpdateModelMixin(UpdateModelMixin): def update(self, request, *args, **kwargs): super().update(request, *args, **kwargs) return APIResponse(msg='修改成功') # {code:100,msg:修改成功} class CommonRetrieveModelMixin(RetrieveModelMixin): def retrieve(self, request, *args, **kwargs): res = super().retrieve(request, *args, **kwargs) return APIResponse(result=res.data) # {code:100,msg:成功,result:{}}
开启media访问
# 上传文件( ImageField,FileField ),会自动传到 media文件夹下的 to 的路径
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = 'media/'
# 在总路由中配置:
from django.conf import settings from django.views.static import serve path('media/<path:path>', serve, kwargs={'document_root': settings.MEDIA_ROOT}), # http://127.0.0.1:8000/media/icon/2.png
前端创建--vue2
前端使用vue2搭建,使用pycharm打开运行,删除不需要的样式和vue文件
vue create luffy_city
前端配置
1、全局样式:
# 在main.js里引入:
//在这里导入即可--全局样式生效 import '@/assets/css/global.css'
/* assets/css/global.css */ /* 声明全局样式和项目的初始化样式 */ body, h1, h2, h3, h4, h5, h6, p, table, tr, td, ul, li, a, form, input, select, option, textarea { margin: 0; padding: 0; font-size: 15px; } a { text-decoration: none; color: #333; } ul { list-style: none; } table { border-collapse: collapse; /* 合并边框 */ }
2、配置文件(路由):
# 在main.js中注册
以后再任意组件中,this.$settings.BASE_URL # 拿到基地址
// main.js import settings from "@/assets/js/settings"; Vue.prototype.$settings = settings /* asesst/ js/ settings.jss */ export default { BASE_URL: 'http://127.0.0.1:8000/api/v1/' }
3、axios:
# 安装:cnpm install -S axios
// main.js import axios from "axios"; Vue.prototype.$axios = axios // 以后再任意组件中直接使用 this.$axios.get(this.$settings.BASE_URL+'user/user/loign/')
4、使用elementui:
# 安装: cnpm install element-ui -S
# 网址:组件 | Element
// main.js import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI);
5、操作cookie:
# 安装:cnpm install vue-cookies
# 以后任意组件直接使用:this.$cookies.set / this.$cookies.get
// main.js import cookies from 'vue-cookies' Vue.prototype.$cookies = cookies;
6、使用bootstrap:
# 安装:cnpm install bootstrap@5卸载:cnpm remove bootstrap@4
# 在组件中使用: <button class="btn btn-danger">点我看美女</button>
// main.js import 'bootstrap/dist/css/bootstrap.min.css'
后端之轮播图
首页home---轮播图接口
轮播图表:#utils / common_model.py from django.db import models class BaseModel(models.Model): created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间') is_delete = models.BooleanField(default=False, verbose_name='是否删除') is_show = models.BooleanField(default=True, verbose_name='是否上架') orders = models.IntegerField(verbose_name='优先级') class Meta: abstract = True # 这样写了,这张表是个虚拟的,不会在数据库创建,只用来继承
# home/models 继承common_models.py from utills.common_model import BaseModel # 通过写一个BaseModel 实现,以后如果其他表中有对应字段,直接继承即可 class Banner(BaseModel): title = models.CharField(max_length=16, unique=True, verbose_name='名称') image = models.ImageField(upload_to='banner', verbose_name='图片') link = models.CharField(max_length=64, verbose_name='跳转链接') info = models.TextField(verbose_name='详情')
轮播图接口:这里运用了自定义配置common_settings,参考下列知识
# home/serializers.py from rest_framework import serializers from .models import Banner class BannerSerializer(serializers.ModelSerializer): class Meta: model = Banner fields = ['id', 'title', 'image', 'link']
# home/views.py from .models import Banner from rest_framework.viewsets import GenericViewSet from utills.mixins import CommonListModelMixin from .serializers import BannerSerializer from django.conf import settings # 查询所有轮播图 class BannerView(GenericViewSet, CommonListModelMixin): # qs对象可以切片----》 limit 2 queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT] serializer_class = BannerSerializer
# urls.py from django.urls import path,include urlpatterns = [ path('api/v1/home/', include('home.urls')), ] # home/urls.py from rest_framework.routers import SimpleRouter from .views import BannerView router=SimpleRouter() router.register('banner',BannerView,'banner') urlpatterns = [ ] urlpatterns += router.urls
自定义配置
1、以后咱们会有自定义的配置 common_settings.py
# settings/common_settings # 自定义配置 BANNER_COUNT=3 # 还有自己其他的配置,都写在这里
2、将自定义配置引入总 settings/dev.py 中
# 导入common_settings.py from .common_settings import *
3、只需要在配置文件中导入:from .common_settings import *
前端页面
vue里面都是首页+组件
唯一首页:
//luffy_city //views/HomeView.vue <template> <div class="home"> <Header></Header> <Banner></Banner> <div class="course"> <el-row> <el-col :span="6" v-for="(o, index) in 8" :key="o" class="course_detail"> <el-card :body-style="{ padding: '0px' }"> <img src="http://photo.liuqingzheng.top/2023%2002%2022%2021%2057%2011%20/image-20230222215707795.png" class="image"> <div style="padding: 14px;"> <span>推荐课程</span> <div class="bottom clearfix"> <time class="time">价格:999</time> <el-button type="text" class="button">查看详情</el-button> </div> </div> </el-card> </el-col> </el-row> </div> <img src="http://photo.liuqingzheng.top/2023%2003%2001%2016%2010%2034%20/1.png" alt="" width="100%" height="500px"> <Footer></Footer> </div> </template> <script> import Footer from '@/components/Footer' import Header from '@/components/Header' import Banner from "@/components/Banner"; export default { name: 'HomeView', components: { Footer, Header, Banner } } </script> <style scoped> .time { font-size: 13px; color: #999; } .bottom { margin-top: 13px; line-height: 12px; } .button { padding: 0; float: right; } .image { width: 100%; display: block; } .clearfix:before, .clearfix:after { display: table; content: ""; } .clearfix:after { clear: both } .course_detail { padding: 50px; } </style>
三个组件:
// components/Banner.vue <template> <div class="banner"> <el-carousel height="400px"> <el-carousel-item v-for="banner in bannerList" :key="banner.id"> <div v-if="banner.link.startsWith('http:')"> <a :href="banner.link"><img :src="banner.image" alt=""></a> </div> <div v-else> <router-link :to="banner.link"><img :src="banner.image" alt=""></router-link> </div> </el-carousel-item> </el-carousel> </div> </template> <script> export default { name: "Banner", data() { return { bannerList: [] } }, created() { this.$axios.get(this.$settings.BASE_URL + 'home/banner/').then(res => { if (res.data.code == 100) { this.bannerList = res.data.results } else { this.$message.error(res.data.msg); } }).catch(res => { this.$message.error('系统异常,请稍后再试'); }) } } </script> <style scoped> .el-carousel__item { height: 400px; min-width: 1200px; } .el-carousel__item img { height: 400px; margin-left: calc(50% - 1920px / 2); } </style>
// components/Footer.vue <template> <div class="footer"> <ul> <li>关于我们</li> <li>联系我们</li> <li>商务合作</li> <li>帮助中心</li> <li>意见反馈</li> <li>新手指南</li> </ul> <p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p> </div> </template> <script> export default { name: "Footer" } </script> <style scoped> .footer { width: 100%; height: 128px; background: #25292e; color: #fff; } .footer ul { margin: 0 auto 16px; padding-top: 38px; width: 810px; } .footer ul li { float: left; width: 112px; margin: 0 10px; text-align: center; font-size: 14px; } .footer ul::after { content: ""; display: block; clear: both; } .footer p { text-align: center; font-size: 12px; } </style>
// components/Header.vue <template> <div class="header"> <div class="slogan"> <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p> </div> <div class="nav"> <ul class="left-part"> <li class="logo"> <router-link to="/"> <img src="../assets/img/head-logo.svg" alt=""> </router-link> </li> <li class="ele"> <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span> </li> <li class="ele"> <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span> </li> <li class="ele"> <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span> </li> </ul> <div class="right-part"> <span>登录</span> <span class="line">|</span> <span>注册</span> </div> </div> </div> </template> <script> export default { name: "Header", data() { return { url_path: sessionStorage.url_path || '/', } }, methods: { goPage(url_path) { if (this.url_path !== url_path) { this.$router.push(url_path); } sessionStorage.url_path = url_path; }, }, created() { sessionStorage.url_path = this.$route.path; this.url_path = this.$route.path; }, } </script> <style scoped> .header { background-color: white; box-shadow: 0 0 5px 0 #aaa; } .header:after { content: ""; display: block; clear: both; } .slogan { background-color: #eee; height: 40px; } .slogan p { width: 1200px; margin: 0 auto; color: #aaa; font-size: 13px; line-height: 40px; } .nav { background-color: white; user-select: none; width: 1200px; margin: 0 auto; } .nav ul { padding: 15px 0; float: left; } .nav ul:after { clear: both; content: ''; display: block; } .nav ul li { float: left; } .logo { margin-right: 20px; } .ele { margin: 0 20px; } .ele span { display: block; font: 15px/36px '微软雅黑'; border-bottom: 2px solid transparent; cursor: pointer; } .ele span:hover { border-bottom-color: orange; } .ele span.active { color: orange; border-bottom-color: orange; } .right-part { float: right; } .right-part .line { margin: 0 10px; } .right-part span { line-height: 68px; cursor: pointer; } .search { float: right; position: relative; margin-top: 22px; margin-right: 10px; } .search input, .search button { border: none; outline: none; background-color: white; } .search input { border-bottom: 1px solid #eeeeee; } .search input:focus { border-bottom-color: orange; } .search input:focus + button { color: orange; } .search .tips { position: absolute; bottom: 3px; left: 0; } .search .tips span { border-radius: 11px; background-color: #eee; line-height: 22px; display: inline-block; padding: 0 7px; margin-right: 3px; cursor: pointer; color: #aaa; font-size: 14px; } .search .tips span:hover { color: orange; } </style>
前后端打通
前后端打通:轮播图需在后端数据库中取
后端:表模型、接口、路由
前端:在banner组件中creat()中发送axios在响应路由地方拿到轮播图数据
再展示在轮播图里
created() { this.$axios.get(this.$settings.BASE_URL + 'home/banner/').then(res => { if (res.data.code == 100) { this.bannerList = res.data.results } else { this.$message.error(res.data.msg); } }).catch(res => { this.$message.error('系统异常,请稍后再试'); }) }
跨域问题详解
# 同源策略(Same origin policy)是一种约定,它规定了 请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名,端口,协议相同,如果不一致,请求会发送成功,后端会正常响应,但是浏览器对非同源请求返回的结果做了拦截
只要做前后端分离,就会出跨域
# 解决跨域问题:
CORS :跨域资源共享, 向响应头中加数据,允许跨域
后端代码处理
nginx代理
JSONP :利用有的标签没有跨域问题 script img
websocket:长链接,不存在跨域
前端代理:开发阶段用,上线不用
# CORS请求分成两类:
简单请求---只发送一次:
非简单请求---发送两次,第一次是OPTIONS预检请求,第二次是真正的请求
#请求方法是以下三种方法之一: HEAD、GET、 POST
# HTTP的头信息不超出以下几种字段:Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
# 解决跨域: 统一写个中间件,处理所有跨域
# utills/common_cors.py from django.utils.deprecation import MiddlewareMixin class CorsMiddleWare(MiddlewareMixin): def process_response(self,request,response): if request.method=="OPTIONS": #可以加* response["Access-Control-Allow-Headers"]="*" res['Access-Control-Allow-Methods'] = '*' response["Access-Control-Allow-Origin"] = "*" return response
# 第三方解决方案:
使用pip安装:pip install django-cors-headers
配置文件settings/dev.py# setting的app中 INSTALLED_APPS = [ ... 'corsheaders', ... ] # 添加中间件 MIDDLEWARE = [ ... 'corsheaders.middleware.CorsMiddleware', ... ] # 底部添加 CORS_ORIGIN_ALLOW_ALL = True CORS_ALLOW_METHODS = ( 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'VIEW', ) CORS_ALLOW_HEADERS = ( 'XMLHttpRequest', 'X_FILENAME', 'accept-encoding', 'authorization', 'content-type', 'dnt', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with', 'Pragma', 'token' )