使用corsheaders包跨域
Django后端,rest_framework+token登录验证,使用默认用户表
建立模型models.py
from django.db import models
from io import BytesIO
from PIL import Image
from django.core.files import File
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField() # 仅包含数字字母下划线,常用于网址
class Meta:
ordering = ('name',)
def __str__(self):
return self.name
def get_absolute_url(self):
return f'/{self.slug}/'
class Product(models.Model):
category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE)
name = models.CharField(max_length=255)
slug = models.SlugField()
description = models.TextField(blank=True, null=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
image = models.ImageField(upload_to='uploads/', blank=True, null=True)
thumbnail = models.ImageField(upload_to='uploads/', blank=True, null=True)
date_add = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('-date_add',)
def __str__(self):
return self.name
def get_absolute_url(self):
return f'/{self.category.slug}/{self.slug}/'
def get_image(self):
if self.image:
return 'http://127.0.0.1:8000' + self.image.url
return ''
def get_thumbnail(self):
if self.thumbnail:
return 'http://127.0.0.1:8000' + self.thumbnail.url
else:
if self.image:
self.thumbnail = self.make_thumbnail(self.image)
self.save()
return 'http://127.0.0.1:8000' + self.thumbnail.url
else:
return ''
def make_thumbnail(self, image, size=(300, 200)):
img = Image.open(image)
img.convert('RGB')
img.thumbnail(size)
thumb_io = BytesIO()
img.save(thumb_io, 'JPEG', quality=85)
thumbnail = File(thumb_io, name=image.name)
return thumbnail
模型序列化serializers.py
from rest_framework import serializers
from .models import *
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = (
'id',
'name',
'get_absolute_url',
'description',
'price',
'get_image',
'get_thumbnail',
)
class CategorySerializer(serializers.ModelSerializer):
# Category是Product的外键,这样可以调用Product序列化的数据
products = ProductSerializer(many=True)
class Meta:
model = Category
fields = (
'id',
'name',
'get_absolute_url',
'products',
)
视图函数view.py
from django.db.models import Q
from django.http import Http404
from django.shortcuts import render
from .serializers import *
from rest_framework.views import APIView
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import *
# Create your views here.
class LatestProductList(APIView):
def get(self, request, format=None):
products = Product.objects.all()
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)
class ProductDetail(APIView):
def get_object(self, category_slug, product_slug):
try:
return Product.objects.filter(category__slug=category_slug).get(slug=product_slug)
except Product.DoesNotExist:
raise Http404
def get(self, request, category_slug, product_slug, format=None):
product = self.get_object(category_slug, product_slug)
serializer = ProductSerializer(product)
return Response(serializer.data)
class CategoryDetail(APIView):
def get_object(self, category_slug):
try:
return Category.objects.get(slug=category_slug)
except Product.DoesNotExist:
raise Http404
def get(self, request, category_slug, format=None):
category = self.get_object(category_slug)
serializer = CategorySerializer(category)
return Response(serializer.data)
@api_view(['POST'])
def search(request):
query = request.data.get('query', '')
if query:
products = Product.objects.filter(Q(name__icontains=query) | Q(description__icontains=query))
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)
else:
return Response({'products': []})
主urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static # static函数添加静态文件
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('djoser.urls')),
path('api/v1/', include('djoser.urls.authtoken')),
path('api/v1/', include('products.urls'), name='products'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
应用urls.py
from django.urls import path, include
from products import views
urlpatterns = [
path('latest-products/', views.LatestProductList.as_view(), name='latest-products'),
path('products/search/', views.search),
path('products/<slug:category_slug>/<slug:product_slug>/', views.ProductDetail.as_view()),
path('products/<slug:category_slug>/', views.CategoryDetail.as_view()),
]
Vue3:
图标引入:public->index.html
<!-- Font Awesome CSS-->
<link href="https://cdn.bootcss.com/font-awesome/5.13.0/css/all.css" rel="stylesheet">
main.js设置默认访问url:axios.defaults.baseURL
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import axios from "axios"
axios.defaults.baseURL = `http://127.0.0.1:8000`
createApp(App).use(store).use(router, axios).mount('#app')
在views创建视图HomeView.vue ProductView.vue......
router->index.js加入设置路由,导入视图,默认进入home,对某个视图进行权限认证增加
meta:{ requireLogin:true } router.beforeEach对每个视图检验权限,无权限的自动进入home,有权限的放行
import HomeView from '../views/HomeView.vue'
import ProductView from "../views/ProductView.vue";
import Category from "@/views/Category";
import Search from "@/views/Search";
import SignUp from "@/views/SignUp";
import Login from "@/views/Login";
import MyAccount from "@/views/MyAccount";
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/:category_slug/:product_slug/',
name: 'productview',
component: ProductView
},
{
path: '/:category_slug',
name: 'Category',
component: Category
},
{
path: '/search',
name: 'Search',
component: Search
},
{
path: '/sign-up',
name: 'SignUp',
component: SignUp
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/myaccount',
name: 'MyAccount',
component: MyAccount,
meta:{
requireLogin:true
}
},]
router.beforeEach((to, from, next)=>{
if (to.matched.some(record=>record.meta.requireLogin) && !store.state.isAuthenticated) {
next({name:'Login', query:{to:to.path}});
}else {
next()
}
})
store->index.js保存状态加入通用方法
state: {
isAuthenticated:false,
token:'',
isLoading:false,
},
getters: {
},
mutations: {
initializeStore(state){
if (localStorage.getItem('token')){
state.token = localStorage.getItem('token')
state.isAuthenticated = true
}else {
state.token= ''
state.isAuthenticated = false
}
},
//设置加载框
setIsLoading(state,status){
state.isLoading = status
},
setToken(state, token){
state.token = token
state.isAuthenticated = true
},
removeToken(state, token){
state.token = ''
state.isAuthenticated = false
},
},
App.vue主模块,类似模板,可当做模板页面使用,其他页面都是在它基础上添加组件,需要返回渲染页面的数据在data->return声明,调用 store->index.js的初始化方法,设置ajax请求头,在'Token '+ token拼接时一定要加一个空格否则验证不通过
axios.defaults.headers.common['Authorization'] = 'Token '+ token
<template>
<div id="wrapper">
<nav class="navbar is-dark">
<div class="navbar-brand">
<router-link to="/" class="navbar-item"><strong>JACKETS</strong></router-link>
<a class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbar-menu" @click="showMobileMenu = !showMobileMenu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navbar-menu" v-bind:class="{'is-active':showMobileMenu}">
<div class="navbar-start">
<div class="navbar-item">
<form action="/search" method="get">
<div class="field has-addons">
<div class="control">
<input type="text" class="input" placeholder="What are you looking for?" name="query">
</div>
<div class="control">
<button class="button is-success">
<span class="icon">
<i class="fas fa-search"></i>
</span>
</button>
</div>
</div>
</form>
</div>
</div>
<div class="navbar-end">
<router-link to="/summer" class="navbar-item">Summer</router-link>
<router-link to="/winter" class="navbar-item">Winter</router-link>
<div class="navbar-item">
<div class="buttons">
<template v-if="$store.state.isAuthenticated">
<router-link to="/myaccount" class="button is-light">My Account</router-link>
<button @click="logout()" class="button is-danger">Logout</button>
</template>
<template v-else>
<router-link to="/login" class="button is-light" >Login</router-link>
<router-link to="/sign-up" class="button is-light" >SignUp</router-link>
</template>
<router-link to="/cart" class="button is-success">
<span class="icon"><i class="fas fa-shopping-cart"></i></span>
<span>Cart ({{cartTotalLength}}) </span>
</router-link>
</div>
</div>
</div>
</div>
</nav>
<div class="is-loading-bar has-text-centered" v-bind:class="{'is-loading': $store.state.isLoading}">
<div class="lds-dual-ring"></div>
</div>
<section class="section">
<router-view/>
</section>
<footer class="footer">
<p class="has-text-centered">Copyright(c)2022</p>
</footer>
</div>
</template>
<script>
import axios from "axios";
export default {
data() {
return{
showMobileMenu: false,
}
},
beforeCreate() {
this.$store.commit('initializeStore')
const token = this.$store.state.token
if (token){
axios.defaults.headers.common['Authorization'] = 'Token '+ token
}else {
axios.defaults.headers.common['Authorization'] = ''
}
},
mounted() {
},
computed:{
//cartTotalLength(){
let totalLength = 0
for (let i=0;i<this.cart.items.length;i++){
totalLength += this.cart.items[i].quantity
}
return totalLength
}
},
methods:{
logout(){
axios.defaults.headers.common['Authorization'] = ''
localStorage.removeItem('token')
localStorage.removeItem('username')
localStorage.removeItem('userid')
this.$store.commit('removeToken')
this.$router.push('/')
}
}
}
</script>
<style lang="scss">
@import '../node_modules/bulma';
.lds-dual-ring{
display: inline-block;
width: 80px;
height: 80px;
}
.lds-dual-ring:after{
content: " ";
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: 6px solid #ccc;
border-color: #ccc transparent #ccc transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.is-loading-bar{
height: 0;
overflow: hidden;
-webkit-transition: all 0.3s;
transition: all 0.3s;
&.is-loading{
height: 80px;
}
}
</style>
HomeView.vue,mounted部分在网页初始化调用,ajax的get请求及返回数据接收,url使用反引号(英文状态下的1左边的~)。一些自定义组件(例如商品介绍卡片)可以在components部分调用,但是需要在components文件夹自定义(如ProductBox组件)
HomeView.vue
<template>
<div class="home">
<section class="hero is-medium is-dark mb-6">
<div class="hero-body has-text-centered">
<p class="title mb-6">
Welcome to jacket
</p>
<p class="subtitle">
The best jacket store online
</p>
</div>
</section>
<div class="columns is-multiline">
<div class="column is-12">
<h2 class="is-size-2 has-text-centered"> Latest products</h2>
</div>
<!-- <div class="column is-3" v-for="product in latestProducts"-->
<!-- v-bind:key="product.id">-->
<!-- <div class="box">-->
<!-- <figure class="image mb-4">-->
<!-- <img v-bind:src="product.get_thumbnail">-->
<!-- </figure>-->
<!-- <h3 class="is-size-6">{{product.name}}</h3>-->
<!-- <p class="is-size-6 has-text-grey">${{product.price}}</p>-->
<!-- <router-link v-bind:to="product.get_absolute_url" class="button is-dark mt-4"> View details</router-link>-->
<!-- </div>-->
<!-- </div>-->
<!-- 调用自定义组件-->
<ProductBox
v-for="product in latestProducts"
v-bind:key="product.id"
v-bind:product="product"/>
</div>
</div>
</template>
<script>
import axios from 'axios'
//导入自己封装的组件
import ProductBox from "@/components/ProductBox";
export default {
name: 'HomeView',
data(){
return{
latestProducts:[]
}
},
components: {
//调用自定义组件
ProductBox
},
mounted() {
this.getLatestProducts()
//网页修改标题
document.title = 'Home | JACKETS-SHOP'
},
methods:{
async getLatestProducts(){
//添加加载框
this.$store.commit('setIsLoading',true)
await axios
.get(`/api/v1/latest-products/`)
.then(response=>{
this.latestProducts=response.data
})
.catch(error=>{
console.log(error)
})
//移除加载框
this.$store.commit('setIsLoading',false)
}
}
}
</script>
components->ProductBox.vue,组件声明,
props:{ product:Object }
然后在其他vue里导入,就可以在页面这样调用
<ProductBox
v-for="product in latestProducts"//操作
v-bind:key="product.id"
v-bind:product="product"/>//绑定
<template>
<div class="column is-3">
<div class="box">
<figure class="image mb-4">
<img v-bind:src="product.get_thumbnail">
</figure>
<h3 class="is-size-6">{{product.name}}</h3>
<p class="is-size-6 has-text-grey">${{product.price}}</p>
<router-link v-bind:to="product.get_absolute_url" class="button is-dark mt-4"> View details</router-link>
</div>
</div>
</template>
<script>
export default {
name: "ProductBox",
//将一个通用组件封装在需要的地方调用
props:{
product:Object
}
}
</script>
<style scoped>
.image{
margin-top: -1.25rem;
margin-left: -1.25rem;
margin-right: -1.25rem;
}
</style>
Search.vue,post方法举例
<template>
<div class="page-search">
<div class="columns is-multiline">
<div class="column is-12">
<h1 class="title">Search</h1>
<h2 class="is-size-5 has-text-grey">Search term:"{{query}}"</h2>
</div>
<!-- 调用自定义组件-->
<ProductBox
v-for="product in products"
v-bind:key="product.id"
v-bind:product="product"/>
</div>
</div>
</template>
<script>
import axios from "axios";
import ProductBox from "@/components/ProductBox";
export default {
name: "Search",
components:{
ProductBox
},
data(){
return{
products:[],
query:''
}
},
mounted() {
document .title = 'Search | JACKETS-SHOP'
let url = window.location.search.substring(1)
let params = new URLSearchParams(url)
if (params.get('query')){
this.query = params.get('query')
this.performSearch()
}
},
methods:{
async performSearch(){
//添加加载框
this.$store.commit('setIsLoading',true)
await axios
.post(`/api/v1/products/search/`, {'query':this.query})
.then(response=>{
this.products=response.data
})
.catch(error=>{
console.log(error)
})
//移除加载框
this.$store.commit('setIsLoading',false)
}
}
}
</script>
<style scoped>
</style>