vue+django实战开发h5旅游网(复习)

需求描述

需要开发一个在线旅游网站,主要在移动端使用,界面美观,体验要好。可以查看景点并在线预订门票信息,游客可以查看景点和门票信息,如果要预订门票需要注册成为会员。为了方便联系,必须收集用户的真实手机号码,景点数据较多,首页数据要快。另外,可以在线维护景点及门票信息,且有统计功能。开发周期有限,希望快速看到效果

需求分析

功能性需求 非功能性需求
登录、注册 首页速度要快
景点、门票展示 界面美观,体验要好
在线预订门票 移动端项目
后台管理 时间有限
统计报表 便于维护,按模块开发

技术栈选择

Django+vue+mysql+redis

移动端前后端完成分离开发

版本迭代计划

v1.0: 首页基本功能

v1.1:景点详情、评论、搜索

v1.2:用户的注册和登录功能

v1.3 : 提交订单、订单管理

v 1.4 :后台管理、报表统计

一、首页拆解

vue+django实战开发h5旅游网(复习)_第1张图片

 拆分为

  • 标题
  • 轮播图
  • 热门推荐
  • 精选景点
  • 底部导航(固定在底部)

可以使用VantUI开发

Vant 2 - Mobile UI Components built on Vue

1.1 轮播图开发

开发步骤:

第一步,查找Vant中可以使用的组件

第二步,实现组件模板部分

第三步,模型准备数据

第四步,模拟数据,实现轮播效果

vue+django实战开发h5旅游网(复习)_第2张图片

1.2 热门推荐组件开发

第一步,查找Vant中可以使用的组件

第二步,完成布局,实现组件模板部分

vue+django实战开发h5旅游网(复习)_第3张图片

拆分为 热门推荐标题,图片,景点名称,价格,是可以左右滑动,所以要加滚动条 overflow-x: scroll;和采用flex布局,flex-direction:column

1.3 精选景点开发

第一步:查找Vant中可以使用的组件  

        精选标题,更多 使用 vant 中cell单元格,评分使用 Rate评分

第二步,完成布局,实现组件模板部分

第三步,模型准备数据

第四步,模拟数据,实现效果

vue+django实战开发h5旅游网(复习)_第4张图片

景点列表组件开发

因为精选景点点击右边菜单还有个列表,对应一个新的页面,但这个页面和当前这个精选页面有重复的代码,所以单独抽取出来

开发步骤:

第一步:分析组件的复用情况

第二步:新建组件文件,实现组件内容

第三步:设置props从父组件传递数据

1.4 页面底部组件开发

第一步:查找Vant中可以使用的组件

        使用Tabbar标签栏

第二步,完成布局,实现组件模板部分

第三步,模型准备数据

第四步,模拟数据,实现效果

底部导航有多个页面用到所以应抽出为公共部分,

二、ORM模型设计

开发步骤:

第一步:分析并设计数据库模型

第二步:完成ORM模型编码

第三步:监测ORM模型

第四步:模型同步

2.1 分析并设计数据库模型

系统模块:轮播图,用户反馈

景点模块:景点、景点详情,景点评论

用户模块:用户,用户详细信息,登录历史

订单模块:订单,订单明细,支付相关

轮播图(system_slider)字段

名称(name),描述(desc),展现位置(types),图片地址(img),排序字段(reorder),生效开始时间(start_time),生效结束时间(end_time),跳转地址(targer_url),是否删除(is_valid),创建时间(created_at),修改时间(update_at)

class Slider(models.Model):
    """轮播图"""
    name = models.CharField('名称',max_length=32)
    desc = models.CharField('描述', max_length=100,null=True,blank=True)
    types = models.SmallIntegerField('展现的位置', default=10)
    img = models.ImageField('图片地址', max_length=255,upload_to='%Y%m/slider')
    reorder = models.SmallIntegerField('排序字段', default=0,help_text='数字越大越靠前')
    start_time = models.DateTimeField('生效开始时间', null=True,blank=True)
    end_time = models.DateTimeField('生效结束时间', null=True,blank=True)
    target_url = models.CharField('跳转的地址', max_length=255,null=True,blank=True)
    is_valid = models.BooleanField('是否有效', default=True)
    created_at = models.DateTimeField('创建时间',auto_now_add=True)
    updated_at = models.DateTimeField('修改时间',auto_now=True)

    class Meta:
        db_table = "system_slider"
        ordering = ['-reorder']

2.2 设计轮播图接口

开发步骤:

  1. 设计接口返回标准 (定义接口返回结构,接口错误信息约定)
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口

接口返回结构

data = {
        "mata":{},
        "objects":[]
    }

def slider_list(request):
    """轮播图接口"""

    data = {
        "mata":{},
        "objects":[]
    }

    queryset = Slider.objects.filter(is_valid=True)
    for item in queryset:
        data['objects'].append({
            'id':item.id,
            'img':item.img.url,
            'target_url':item.target_url,
            'name':item.name
        })
    return http.JsonResponse(data)

2.3 景点ORM模型设计

景点字段名:

vue+django实战开发h5旅游网(复习)_第5张图片

class Sight(models.Model):
    """景点基础信息"""

    name = models.CharField('名称',max_length=64)
    desc = models.CharField('描述',max_length=128)
    main_img = models.ImageField('主图',upload_to='%Y%m/sight',max_length=256)
    banner_img = models.ImageField('详细主图',upload_to='%Y%m/sight',max_length=256)
    content = models.TextField('详细')
    score = models.FloatField('评分',default=5)
    min_price = models.FloatField('最低价格',default=0)
    province = models.CharField('省份',max_length=32)
    city = models.CharField('市区',max_length=32)
    area = models.CharField('区/县',max_length=32,null=True)
    town = models.CharField('乡镇',max_length=32,null=True)

    is_top = models.BooleanField('是否为精选景点',default=False)
    is_hot = models.BooleanField('是否为热门景点',default=False)

    is_valid = models.BooleanField('是否有效',default=True)
    created_at = models.DateTimeField('创建时间',auto_now_add=True)
    updated_at = models.DateTimeField('修改时间',auto_now=True)

    class Meta:
        db_table = 'sight'

 2.4 景点列表API接口

开发步骤

  1. 设计接口返回内容及字段
  2. 编写接口代码(查数据,分页)
  3. 模拟HTTP请求,测试验证接口

接口结构

 data = {
            'meta':{

            },
            'objects':[
                    ]
        }

可能景点有很多,需要用到分页,这里使用Django中的ListView,使用面向对象的方式写视图函数

class SightListView(ListView):
    """景点列表"""
    # 设置每页的数目
    paginate_by = 5

    def get_queryset(self):
        """重写查询方法"""
        query = Q(is_valid=True)
        # 1 热门景点
        is_hot = self.request.GET.get('is_hot', None)
        if is_hot:
            query = query & Q(is_hot=True)

        # 2 精选景点
        is_top = self.request.GET.get('is_top', None)
        if is_top:
            query = query & Q(is_top=True)
        # 3 按景点名称搜索
        queryset = Sight.objects.filter(query)
        return queryset

    def render_to_response(self, context, **response_kwargs):
        page_obj = context['page_obj']
        data = {
            'meta': {
                # 总共多少条记录
                'total_count': page_obj.paginator.count,
                # 总共有多少页
                'page_count': page_obj.paginator.num_pages,
                # 当前是多少页
                'current_page': page_obj.number,
            },
            'objects': [

            ]
        }

        for item in page_obj.object_list:
            data['objects'].append({
                'id': item.id,
                'name': item.name,
                'main_img': item.main_img.url,
                'score': item.score,
                'province': item.province,
                'city': item.city,
                'comment_count': 0

            })
        return http.JsonResponse(data)

2.5 轮播图接口的数据获取(接口联调)

实现步骤

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

建立utils 文件夹,下面新建apis.js,用来存放接口

//接口地址

const apiHost = 'http://localhost:8080/api'


/**
 * 系统模块的接口
 */
const SystemApis = {
    // 轮播图列表
    sliderListUrl: apiHost + '/system/slider/list/'
}

export{
    
    SystemApis,
}

utils下新建ajax.js文件,

import axios from 'axios'

export const ajax = axios.create({
    headers: {
        source: 'h5',
        icode: 'acbd',
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    withCredentials: true
})
ajax.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    console.log('请求拦截到了')
    // window.app.$toast.loading({
    //     message: '加载中...',
    //     forbidClick: true,
    //     loadingType: 'spinner'
    // })
    return config
}, function (error) {
    // 对请求错误做些什么
    // window.app.$toast.clear()
    return Promise.reject(error)
})

ajax.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    console.log('响应拦截到了')
    // window.app.$toast.clear()
    return response
}, function (error) {
    // 对响应错误做点什么
    if (error.response) {
        if (error.response.status === 401) {
            window.alert('未登录,即将跳转到登录页面')
        } else if (error.response.status === 500) {
            window.app.$notify({
                message: '服务器正忙,请稍后重试',
                type: 'danger'
            })
            // window.alert('服务器正忙,请稍后重试')
        }
    }
    // window.app.$toast.clear()
    return Promise.reject(error)
})

根目录新建vue.config.js,在里面解决跨域问题

配置中添加

devServer:{
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000/', //跨域请求的公共地址
        ws: false, //也可以忽略不写,不写不会影响跨域
        changeOrigin: true, //是否开启跨域,值为 true 就是开启, false 不开启
        pathRewrite: {
          '^/api': '' //注册全局路径, 但是在你请求的时候前面需要加上 /api  
        }
      }
    },

  }

在轮播图模块,获取轮播图数据,并按照接口返回的字段在模板中进行绑定

2.6 景点列表接口数据获取

                 

实现步骤

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

三 、景点搜索页面

vue+django实战开发h5旅游网(复习)_第6张图片

实现步骤

  1. 查找Vant中可以使用的组件
  2. 实现组件模板部分
  3. 模型层数据准备

可以将页面拆分为标题,搜索框,景点列表,分页,和底部导航

标题:可以使用Vant中的 navbar导航栏

搜索框:使用Vant中的Search搜索

景点列表:已抽出为公共部分可以导入使用

分页:可以使用Vant中的 pagination分页

底部导航 :已拆分为公共部分,可直接导入使用

新建Search.vue完成上面所拆分的页面

使用VueRouter添加Search搜索页面,在底部导航中将使用:to进行name绑定

在router下面的index.js中添加

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Search from "../views/Search";

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: HomeView
  },
//搜索页面
  {
    path: '/search',
    name: 'Search',
    component: Search
  },
]

const router = new VueRouter({
  routes
})

export default router

底部导航路由绑定

 四、搭建前端详情页

vue+django实战开发h5旅游网(复习)_第7张图片

实现步骤:

  1. 设计URL路由规则
  2. 新建详情页页面
  3. 修改景点列表组件,支持路由跳转
  4. 拆分详情页组件 

详情页可拆分为,图片页,评论页,景点介绍页,门票页(后面做),新建相关页面使用绑定路由链接,

分析首页精选列表和热门榜单,都可以点击更多和单个点击,使用为其绑定相关路由链接

4.1 详情页相关组件

详情页可拆分为 页面头部(固定),大图,评分和景点介绍,地址,门票列表,评论列表

实现步骤:

第一步:查找Vant中可以使用的组件

第二步,完成布局,实现组件模板部分

第三步,模型准备数据

第四步,模拟数据,实现效果

页面头部:NavBar 导航栏

大图:Vant中Image图片

评分和景点介绍:可以从Vant中寻找icon图标

地址:使用Vant中的单元格

门票列表:可以用Vant中的Cell单元格(标题),Tag标记,Button按钮

热门评论:很多地方用到,应单独抽取出来,可以重复用

五、ORM模型设计

开发步骤:

  1. 分析并设计模型
  2. 完成ORM模型编码
  3. 检测ORM模型
  4. 模型同步

景点详细信息、景点评论、景点门票、景点图片、用户账户

景点详细信息

vue+django实战开发h5旅游网(复习)_第8张图片

class Info(models.Model):
    """ 景点详情 """
    sight = models.OneToOneField(Sight, on_delete=models.CASCADE)
    entry_explain = models.CharField('入园参考', max_length=1024, null=True, blank=True)
    play_way = models.TextField('特色玩法',null=True, blank=True)
    tips = models.TextField('温馨提示', null=True, blank=True)
    traffic = models.TextField('交通到达', null=True, blank=True)

    class Meta:
        db_table = 'sight_info'

 景点评论

vue+django实战开发h5旅游网(复习)_第9张图片

class Comment(CommonModel):
    """ 评论及回复 """
    user = models.ForeignKey(User, verbose_name='评论人',
                             related_name='comments',
                             on_delete=models.CASCADE)
    sight = models.ForeignKey(Sight, verbose_name='景点',
                              related_name='comments',
                              on_delete=models.CASCADE)
    content = models.TextField('评论内容', blank=True, null=True)
    is_top = models.BooleanField('是否置顶', default=False)
    love_count = models.IntegerField('点赞次数', default=0)
    score = models.FloatField('评分', default=5)

    ip_address = models.CharField('IP地址', blank=True, null=True, max_length=64)
    is_public = models.SmallIntegerField('是否公开', default=1)
    reply = models.ForeignKey(
        'self', blank=True, null=True,
        related_name='reply_comment',
        verbose_name='回复',
        on_delete=models.CASCADE)

    images = GenericRelation(ImageRelated,
                             verbose_name='关联的图片',
                             related_query_name="rel_comment_images")

    class Meta:
        db_table = 'sight_comment'
        ordering = ['-love_count', '-created_at']

 景点门票

vue+django实战开发h5旅游网(复习)_第10张图片

class Ticket(CommonModel):
    """ 门票 """
    sight = models.ForeignKey(Sight, related_name='tickets', verbose_name='景点门票',
                              on_delete=models.PROTECT)
    name = models.CharField('名称', max_length=128)
    desc = models.CharField('描述', max_length=64, null=True, blank=True)
    types = models.SmallIntegerField('类型',
                                     choices=TicketTypes.choices,
                                     default=TicketTypes.ADULT,
                                     help_text='默认为成人票')
    price = models.FloatField('价格(原价)')
    discount = models.FloatField('折扣', default=10)
    total_stock = models.PositiveIntegerField('总库存', default=0)
    remain_stock = models.PositiveIntegerField('剩余库存', default=0)
    expire_date = models.IntegerField('有效期', default=1)
    return_policy = models.CharField('退改政策', max_length=64, default='条件退')
    has_invoice = models.BooleanField('是否提供发票', default=True)
    entry_way = models.SmallIntegerField('入园方式',
                                         choices=EntryWay.choices,
                                         default=EntryWay.BY_TICKET)
    tips = models.TextField('预定须知',null=True, blank=True)
    remark = models.TextField('其他说明', null=True, blank=True)
    status = models.SmallIntegerField('状态',
                                      choices=TicketStatus.choices,
                                      default=TicketStatus.OPEN)
    
    class Meta:
        db_table = 'sight_ticket'

景点图片

vue+django实战开发h5旅游网(复习)_第11张图片

class ImageRelated(models.Model):
    """图片关联"""
    img = models.ImageField('图片',upload_to='%Y%m/file/',max_length=155)
    summary = models.CharField('图片说明',max_length=32,null=True,blank=True)
    user = models.ForeignKey(User,related_name='upload_images',verbose_name='上传的用户',on_delete=models.SET(None))
    content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE)
    object_id = models.IntegerField('关联的模型')
    content_object = GenericForeignKey('content_type','object_id')

    class Meta:
        db_table = 'system_image_related'

 用户账户

vue+django实战开发h5旅游网(复习)_第12张图片

class User(CommonModel):
    """用户模型"""
    username = models.CharField('用户名',max_length=32,unique=True)
    password = models.CharField('密码',max_length=256)
    nickname = models.CharField('昵称',max_length=32,unique=True)
    avatar = models.ImageField('用户头像',upload_to='avatar/%Y%m',null=True,blank=True)




    class Meta:
        db_table = 'account_user'

5.1 重构响应对象

开发步骤:

  1. 设计响应单个对象的基类
  2. 设计响应列表的基类
  3. 设计错误基础类

接口处 有很多重复的代码,接口结构都是{meta:{},objects:[]},重构后便于维护和修改

首先在utils下新建serializers.py

class BaseSerializer(object):

    def __init__(self, obj):
        self.obj = obj

    def to_dict(self):
        return {}

class MetaSerializer(object):
    """分页元数据"""

    def __init__(self, page, page_count, total_count):
        """

        :param page: 当前页
        :param page_count:总页数
        :param total_count: 总记录数
        """
        self.page = page
        self.page_count = page_count
        self.total_count = total_count

    def to_dict(self):
        return {
            'total_count': self.total_count,
            'page_count': self.page_count,
            'current_page': self.page
        }


class BaseListPageSerializer(object):
    """分页类的封装"""

    def __init__(self, page_obj, paginator=[], object_list=[]):
        """

        :param page_obj: 当前页的对象
        :param paginator: 分页器的对象
        :param object_list: 当前页的数据列表
        """
        self.page_obj = page_obj
        self.paginator = paginator if paginator else page_obj.paginator
        self.object_list = object_list if object_list else page_obj.object_list

    def get_object(self, obj):
        """对象内容,子类进行重写"""
        return {}

    def to_dict(self):
        page = self.page_obj.number
        page_count = self.paginator.num_pages
        total_count = self.paginator.count
        meta = MetaSerializer(page=page, page_count=page_count, total_count=total_count).to_dict()
        objects = []

        for obj in self.object_list:
            objects.append(self.get_object(obj))

        return {
            'meta': meta,
            'objects': objects
        }

sight下新建serializers.py文件类继承着BaseListPageSerializer,对get_object方法进行重写

from utils.serializers import BaseListPageSerializer

class SightListSerializer(BaseListPageSerializer):
    """景点列表"""

    def get_object(self, obj):
        return {
            'id': obj.id,
            'name': obj.name,
            'main_img': obj.main_img.url,
            'score': obj.score,
            'province': obj.province,
            'city': obj.city,
            'price': obj.min_price,
            'comment_count': 0
        }

设计错误类,比如400错误响应字段

from django.http import JsonResponse


class NotFoundJsonResponse(JsonResponse):

    """400对应JSON响应"""
    def __init__(self,*args,**kwargs):

        data = {
            'error_code':'404000',
            'error_msg':'您访问的内容不存在或已被删除'
        }
        super().__init__(data,*args,**kwargs)

5.2 景点详情接口

开发步骤:

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口

视图继承DetailView,get_query(写出从哪个模型获取数据),重写render_to_response(),返回对象

sight/serializers继承BaseSerializer重写to_dict函数,写出接口返回对象

class SightDetailSerializer(BaseSerializer):

    def to_dict(self):
        obj = self.obj
        return {
            'id' : obj.id,
            'name':obj.name,
            'img':obj.banner_img.url,
            'content':obj.content,
            'score':obj.score,
            'province':obj.province,
            'min_price':obj.min_price,
            'area':obj.area,
            'city':obj.city,
            'town':obj.town,
            'comment_count':0

        }
class SightDetailView(DetailView):

    """景点详细接口"""
    def get_queryset(self):
        return Sight.objects.all()

    def render_to_response(self, context, **response_kwargs):
        page_obj = context['object']
        if page_obj:
            if page_obj.is_valid == True:
                data = serializers.SightDetailSerializer(page_obj).to_dict()
                return http.JsonResponse(data)
        return NotFoundJsonResponse()

5.3 景点评论列表开发

开发步骤:

  • 设计接口及返回字段
  • 编写接口代码
  • 模拟HTTP请求,测试验证接口

视图中使用继承ListView的方法来写接口,注意设置paginate_by大小

class SightCommentView(ListView):
    """景点评论接口"""
    paginate_by = 5
    def get_queryset(self):
        sight_id = self.kwargs.get('pk',None)
        sight = Sight.objects.filter(id=sight_id,is_valid=True).first()

        if sight:
            return Comment.objects.filter(sight=sight,is_valid=True)
        return Comment.objects.none()

    def render_to_response(self, context, **response_kwargs):
        page_obj = context['page_obj']
        print(page_obj)
        if page_obj:

            data = serializers.CommentListSerializer(page_obj).to_dict()
            return http.JsonResponse(data)
        return NotFoundJsonResponse()
class CommentListSerializer(BaseListPageSerializer):
    """评论列表"""

    def get_object(self, obj):
        user = obj.user
        images = []
        for image in obj.images.filter(is_valid=True):
            images.append({
                'img': image.img.url,
                'summary': image.summary,
            })

        return {
            'user': {
                'pk': user.pk,
                'nickname': user.nickname},
            'pk': obj.pk,
            'content': obj.content,
            'is_top': obj.is_top,
            'love_count': obj.love_count,
            'score': obj.score,
            'images': images,
            //时间转换
            'create_at': obj.created_at.strftime('%Y-%m-%d')
        }

5.4 门票列表接口开发

开发步骤:

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口

视图使用面向对象的方法继承ListView,

class SightTicketView(ListView):
    """景点下的接口列表"""

    paginate_by = 10

    def get_queryset(self):
        sight_id = self.kwargs.get('pk', None)
        return Ticket.objects.filter(is_valid=True, sight_id=sight_id)

    def render_to_response(self, context, **response_kwargs):
        page_obj = context['page_obj']
        print(page_obj)
        if page_obj:
            data = serializers.TicketListSerializer(page_obj).to_dict()
            return http.JsonResponse(data)
        return NotFoundJsonResponse()
class TicketListSerializer(BaseListPageSerializer):
    """门票列表"""

    def get_object(self, obj):
        return {
            'pk': obj.pk,
            'name': obj.name,
            'desc':obj.desc,
            'types': obj.types,
            'price': obj.price,
            'discount': obj.discount,
            'total_stock': obj.total_stock,
            'remain_stock': obj.remain_stock
        }

5.5 景点详细信息接口

开发步骤:

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口

视图使用面向对象的方法,继承DetailView,

class SightInfoDetailView(DetailView):
    """景点介绍"""
    slug_field = 'sight_pk'

    def get_queryset(self):
        return Info.objects.all()

    def render_to_response(self, context, **response_kwargs):
        page_obj = context['object']
        if page_obj:
            data = serializers.SightInfoSerializer(page_obj).to_dict()
            return http.JsonResponse(data)
        return NotFoundJsonResponse()
class SightInfoSerializer(BaseSerializer):
    """景点介绍"""

    def to_dict(self):
        obj = self.obj
        return {
            'pk': obj.sight.pk,
            'entry_explain': obj.entry_explain,
            'play_way': obj.play_way,
            'tips': obj.tips,
            'traffic': obj.traffic
        }

六、景点搜索接口联调

实现步骤:

  1. 修改景点列表接口,实现搜索
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

由前端指定每页数据大小

Django重写get_paginate_by方法

request.GET.get()从前端传过来的数据,返回

实现从首页热门推荐和精选景点点击全部跳转到搜索页,将热门推荐榜单和精选景点榜单全部展示出来

vue+django实战开发h5旅游网(复习)_第13张图片vue+django实战开发h5旅游网(复习)_第14张图片

七、景点详情接口联调 

实现步骤:

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

景点详情url是动态的id,用特殊字符替换id,页面传递时再用实际的id替换回来

SightDatailUrl: apiHost + '/sight/sight/detail/#{id}/',
 getDetailList() {

            const url = SightApis.SightDatailUrl.replace('#{id}', this.id);

            ajax.get(url).then(({ data }) => {
                this.detailList = data
            })

        },

7.1 景点门票接口联调

实现步骤:

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层
 getTicketList() {
            const url = SightApis.SightTicketUrl.replace('#{id}', this.id)
            console.log(url)
            ajax.get(url).then(({ data: { objects } }) => {

                this.ticketList = objects
            })
        },

7.2 景点评论接口联调

实现步骤:

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

7.3 前端代码优化

添加loading动画(使用Vant中的Toast 轻提示)

添加请求拦截 

添加滚动加载和下拉刷新 (使用vant中的List 列表)

import { ajax } from "@/utils/ajax";
import { SightApis } from "@/utils/apis";
import CommentItem from '@/components/sight/CommentItem'
export default {
    
    data() {
        return {
            // 评论列表
            commentList: [],
            // 当前的页码
            currentPage: 1,
            // 正在加载中
            loading: false,
            // 所有的内容加载完
            finished: false,
            // 请求失败
            error: false,
            // 是否正在下拉刷新中
            refreshing: false
        }
    },
    components: {
        CommentItem
    },
    methods: {
        goBack() {
            this.$router.go(-1)
        },

        
        onRefresh() {
            // 清空数据
            this.commentList = []
            this.currentPage = 1

            this.finished = false
            this.error = false

            // 重新加载数据
            this.getCommentList()
        },
       
        getCommentList() {
            const url = SightApis.SightCommenttUrl.replace('#{id}', this.id)
            ajax.get(url, {
                params: {
                    page: this.currentPage
                }
            }).then(({ data: { meta, objects } }) => {
                this.commentList = this.commentList.concat(objects)
                // 加载状态结束
                this.loading = false
                // 设置下一页的页码
                this.currentPage = meta.current_page + 1
                // 数据全部加载完成: 当前页面 == 总页数
                if (meta.current_page === meta.page_count) {
                    this.finished = true
                }
                this.refreshing = false
            }).catch(() => {
                this.loading = false
                this.error = true
                this.refreshing = false
            })
        }

    },
    mounted() {
        this.id = this.$route.params.id



    }

}

八、用户登录页面开发

vue+django实战开发h5旅游网(复习)_第15张图片

登录页面拆分

顶部导航条 (Vant NavBar 导航栏)

表单 (Vant Form表单)

登录按钮 (Vant Form表单)

登录注意信息

8.1 用户注册页面的开发

vue+django实战开发h5旅游网(复习)_第16张图片

 实现步骤:

  1. 查找Vant中可以使用的组件
  2. 实现组件模板部分
  3. 实现验证码组件(需要用到window中的setInterval和clearInterval)

 验证码点击后不可再点击可以使用 disabled参数

vue中ref相当于给Dom起了个名字,



8.2 个人中心页面开发

vue+django实战开发h5旅游网(复习)_第17张图片

拆分:

顶部标题(vant NavBar 导航栏)

头像 

欢迎你 

订单 (vant 上查找图标)

底部菜单  (引用之前写的组件)

九、用户登录,退出接口开发

开发步骤

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口(使用postman)

登录为post请求

9.1 用户详情接口开发

开发步骤

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口(使用postman)

用户登录了才能查询信息(可以使用is_authenticated判断是否登录)

9.2 短信验证码接口

开发步骤

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口(使用postman)

验证码使用redis缓存,需要在根settings中配置

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/2",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

在系统模块中新建表单完成验证码功能

import random
import re

from django import forms
from django.core.cache import cache


class SendSmsCodeForm(forms.Form):
    """ 发送验证码的表单 """
    phone_num = forms.CharField(label='手机号码', required=True, error_messages={
        'required': '请输入手机号码'
    })

    def clean_phone_num(self):
        """ 验证是否为手机号 """
        phone_num = self.cleaned_data['phone_num']
        pattern = r'^1[0-9]{10}$'
        if not re.search(pattern, phone_num):
            raise forms.ValidationError('手机号%s输入不正确',
                                        code='invalid_phone',
                                        params=(phone_num, ))
        return phone_num

    def send_sms_code(self):
        """ 生成验证码并发送短信 """
        sms_code = random.randint(100000, 999999)
        phone_num = self.cleaned_data.get('phone_num', None)
        try:
            # TODO 调用发送验证码的短信接口
            # redis中的key
            key = 'sms_code_{}'.format(phone_num)
            # 将验证码存入redis
            timeout = 5 * 60
            cache.set(key, sms_code, timeout=timeout)
            return {
                'phone_num': phone_num,
                'sms_code': sms_code,
                'timeout': timeout
            }
        except Exception as e:
            print(e)
            return None

视图中

class SmsCodeView(FormView):
    form_class = SendSmsCodeForm

    # 1. 拿到手机号,判断是否为真实的手机号码

    # 2. 生成验证码,并存储
    # TODO 3. 调用短信的发送接口
    # 4. 告诉用户验证码发送是否成功(会把验证码直接告诉用户)

    def form_valid(self, form):
        """ 表单已经通过验证 """
        data = form.send_sms_code()
        if data is not None:
            return http.JsonResponse(data, status=201)
        return ServerErrorJsonResponse()

    def form_invalid(self, form):
        """ 表单没有通过验证 """
        err_list = json.loads(form.errors.as_json())
        return BadrequestJsonResponse(err_list)

9.3 用户注册接口

开发步骤

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口(使用postman)

1.表单验证用户输入信息

2.创建用户基础信息表,用户详细信息表

3.执行登录

4.保存登录日志

注册的表单

class RegisterForm(forms.Form):
    username = forms.CharField(label='手机号码', max_length=16, required=True, error_messages={
        'required': '请输入手机号码'
    })

    password = forms.CharField(label='密码', max_length=266, required=True, error_messages={
        'required': '请输入密码'
    })

    nickname = forms.CharField(label='验证码', max_length=16, required=True, error_messages={
        'required': '请输入昵称'
    })

    sms_code = forms.CharField(label='验证码', max_length=6, required=True, error_messages={
        'required': '请输入验证码'
    })

    def clean_username(self):
        """验证用户名的钩子函数"""
        username = self.cleaned_data['username']
        pattern = r'^1[0-9]{10}$'
        if not re.search(pattern, username):
            raise forms.ValidationError('手机号%输入不正确', params=(username,), code='invalid_phone')
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError('用户名已被使用')
        return username

    def clean_nickname(self):
        """昵称验证"""
        nickname = self.cleaned_data['nickname']
        if User.objects.filter(nickname=nickname).exists():
            raise forms.ValidationError('昵称已被使用')
        return nickname

    def clean(self):
        data = super().clean()
        if self.errors:
            return
            # 获取手机号
        phone_num = self.cleaned_data.get('username', None)
        # 获取验证码
        sms_code = self.cleaned_data.get('sms_code', None)

        key = '{}{}'.format(REGISTER_MSM_CODE_KEY, phone_num)
        code = cache.get(key)
        if code is None:
            raise forms.ValidationError('验证码已经失效')
        if str(code) != sms_code:
            raise forms.ValidationError('验证码输入错误')
        return data

    @transaction.atomic
    def do_register(self, request):
        """执行注册"""
        # 创建基础信息表
        try:
            data = self.cleaned_data
            version = request.headers.get('version', ''),
            source = request.headers.get('source', '')
            user = User.objects.create_user(
                username=data.get('username', None),
                password=data.get('password', None),
                nickname=data.get('nickname', None)
            )
            # 创建详细信息表
            profile = Profile.objects.create(
                user=user,
                username=user.username,
                version=version,
                source=source

            )
            # 执行登录
            login(request, user)
            # 记录登录日志
            user.last_login = now()
            user.save()
            ip = request.META.get('REMOTE_ADDR', '')
            user.add_login_record(username=user, ip=ip, source=source, version=version)
            return user, profile
        except Exception as e:
            print(e)
            return None

注册的视图

class UserRegisterView(FormView):
    form_class = RegisterForm
    http_method_names = ['post']

    def form_valid(self, form):
        result = form.do_register(request=self.request)
        if result is not None:
            user, profile = result
            data = {
                'user': serializers.UserSerializer(user).to_dict(),
                'profile': serializers.UserProfileSerializer(profile).to_dict()
            }
            return http.JsonResponse(data, status=201)
        return ServerErrorJsonResponse()

    def form_invalid(self, form):
        """ 表单没有通过验证 """
        err_list = json.loads(form.errors.as_json())
        return BadrequestJsonResponse(err_list)

9.4 用户登录接口联调

实现步骤

第一步:在登录组件中调用登录接口

第二步:设置用户信息到Vuex

第三步:在个人中心显示用户信息

第四步:刷新时调用个人信息接口

9.5 验证码获取接口联调

实现步骤

第一步:阅读接口文档

第二步:配置接口地址

第三步:使用axios获取数据

第四步:将数据设置到模型层

9.6 用户注册接口联调

实现步骤

第一步:阅读接口文档

第二步:配置接口地址

第三步:使用axios获取数据

第四步:将数据设置到模型层

 十、订单页面开发

vue+django实战开发h5旅游网(复习)_第18张图片

页面拆分

页面标题

景点门票标题(预定须知)

出行日期(Vant中日历组件)

购买数量(Vant Stepper 步进器)

收件人,手机号码

订单合计  (Vant SubmitBar 提交订单栏)

10.1 提交订单页 

实现步骤:

  1. 配置路由规则,新建组件
  2. 查找Vant中可以使用的组件
  3. 实现组件模板部分
  4. 模拟数据,实现效果

10.2 订单支付页面

vue+django实战开发h5旅游网(复习)_第19张图片

 拆分 

顶部导航 (Vant navbar)

订单号 (Vant中cell表格)

门票 (使用flex布局)

确认按钮(Vant SubmitBar 提交订单栏)

弹出的确认栏 (Vant中的Dialog 弹出框)

实现步骤:

  1. 配置路由规则,新建组件
  2. 查找Vant中可以使用的组件
  3. 实现组件模板部分
  4. 模拟数据,实现效果

10.3 我的订单列表页面开发 

vue+django实战开发h5旅游网(复习)_第20张图片

拆分:

顶部导航

订单状态tab切换(Vant中可找到tab)

十一、订单模块ORM模型设计

订单表

 vue+django实战开发h5旅游网(复习)_第21张图片

class Order(CommonModel):
    """ 订单 """
    sn = models.CharField('订单编号', max_length=32)
    user = models.ForeignKey(User, related_name='orders', on_delete=models.PROTECT)
    buy_count = models.IntegerField('购买数量', default=1)
    buy_amount = models.FloatField('总价')

    to_user = models.CharField('收货人', max_length=32)
    to_area = models.CharField('省市区', max_length=32, default='')
    to_address = models.CharField('详细地址', max_length=256, default='')
    to_phone = models.CharField('手机号码', max_length=32)

    remark = models.CharField('备注', max_length=255, null=True, blank=True)

    # 快递信息
    express_type = models.CharField('快递', max_length=32, null=True, blank=True)
    express_no = models.CharField('单号', max_length=32, null=True, blank=True)

    status = models.SmallIntegerField('订单状态',
                                      choices=OrderStatus.choices,
                                      default=OrderStatus.SUBMIT)
    types = models.SmallIntegerField('订单类型',
                                     choices=OrderTypes.choices,
                                     default=OrderTypes.SIGHT_TICKET)

    class Meta:
        db_table = 'order'

 订单明细表

vue+django实战开发h5旅游网(复习)_第22张图片

class OrderItem(CommonModel):
    """ 订单明细 """
    user = models.ForeignKey(User, related_name='order_items',
                             on_delete=models.CASCADE)
    order = models.ForeignKey(Order, verbose_name='订单',
                              related_name='order_items',
                              null=True,
                              on_delete=models.CASCADE)
    # 商品快照
    flash_name = models.CharField('商品名称', max_length=128)
    flash_img = models.ImageField('商品的主图')
    flash_price = models.FloatField('兑换价格')
    flash_origin_price = models.FloatField('原价')
    flash_discount = models.FloatField('折扣')

    count = models.PositiveIntegerField('购买数量')
    amount = models.FloatField('总额')

    status = models.SmallIntegerField('状态',
                                      choices=OrderStatus.choices,
                                      default=OrderStatus.SUBMIT)
    remark = models.CharField('备注', max_length=255, null=True, blank=True)

    # 复合关联
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    class Meta:
        db_table = 'order_item'

 支付凭证

vue+django实战开发h5旅游网(复习)_第23张图片

class Payment(CommonModel):
    """ 支付凭证 """

    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='payments')
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='payments')
    amount = models.FloatField('金额', help_text='支付实际的金额')
    sn = models.CharField('流水号', max_length=32)
    third_sn = models.CharField('第三方订单号', max_length=128, null=True, blank=True)

    status = models.SmallIntegerField('支付状态', default=1)

    meta = models.CharField('其他数据', max_length=128, null=True, blank=True)
    remark = models.CharField('备注信息', max_length=128, null=True, blank=True)

    class Meta:
        db_table = 'order_payment'

 11.1 门票下单接口开发

开发步骤:

  1. 设计接口返回内容及字段
  2. 编写接口代码
  3. 模拟HTTP请求,测试验证接口

1.判断用户是否已经登录

2.获取post数据 

3.数据的验证(手机号,门票ID,库存)

4.关联用户,生成订单号,计算购买总价,生成订单

5.返回内容:订单ID

from django import forms
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import F

from order.models import Order, OrderItem
from sight.models import Ticket
from utils import tools


class SubmitTicketForm(forms.ModelForm):
    """门票订单提交表单"""

    ticket_id = forms.IntegerField(label='门票ID', required=True)
    play_date = forms.DateField(label='出行的时间', required=True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.ticket = None

    class Meta:
        model = Order
        fields = ('to_user', 'to_phone', 'buy_count',)

    def clean_ticket_id(self):
        ticket_id = self.cleaned_data['ticket_id']
        ticket = Ticket.objects.filter(is_valid=True, pk=ticket_id).first()



        if ticket is None:
            raise forms.ValidationError('门票信息不存在')

        else:
            if ticket.remain_stock <= 0:
                raise forms.ValidationError('当日门票已售完')
        self.ticket = ticket
        return ticket_id

    @transaction.atomic
    def save(self, user, commit=False):
        obj = super().save(commit=commit)
        obj.user = user
        obj.sn = tools.gen_trans_id()

        # 计算价格
        buy_count = self.cleaned_data['buy_count']
        buy_amount = self.ticket.sell_price * buy_count
        obj.buy_amount = buy_amount
        obj.save()

        # 扣减库存
        self.ticket.remain_stock = F('remain_stock') - buy_count
        self.ticket.save()

        # 关联订单明细,保存快照
        ctype = ContentType.objects.get_for_model(Ticket)

        OrderItem.objects.create(
            user=user,
            order=obj,
            flash_name=self.ticket.name,
            flash_img=self.ticket.sight.main_img,
            flash_price=self.ticket.sell_price,
            flash_origin_price=self.ticket.price,
            flash_discount=self.ticket.discount,
            count=buy_count,
            amount=buy_amount,
            content_type=ctype,
            object_id=self.ticket.id,
            remark='出行时间:{}'.format(self.cleaned_data['play_date'])
        )
        return obj
method_decorator(login_required,name='dispatch')
class TicketOrderSubmitView(FormView):
    """门票订单提交接口"""
    form_class = SubmitTicketForm

    def form_invalid(self, form):
        err = json.loads(form.errors.as_json())
        return BadrequestJsonResponse(err)

    def form_valid(self, form):
        obj = form.save(user=self.request.user)
        return http.JsonResponse({
            'sn':obj.sn
        },status=201)

11.2 订单详情接口开发

开发步骤:

设计接口返回内容及字段 

编写接口代码

模拟HTTP请求,测试验证接口

使用BaseDetailView来写视图函数,使用sn号来查询订单的详情

class OrderDetail(BaseDetailView):
    slug_field = 'sn'
    slug_url_kwarg = 'sn'

    def get_queryset(self):
        user = self.request.user
        obj = Order.objects.filter(user=user,is_valid=True)
        return obj

    def get(self,request,*args,**kwargs):
        order_obj = self.get_object()
        data = serializers.OrderDetailSerializer(order_obj).to_dict()
        return http.JsonResponse(data)
class OrderItemSerializer(BaseSerializer):
    """订单明细"""
    def to_dict(self):
        obj = self.obj
        return {
            'pk':obj.pk,
            'flash_img':obj.flash_name,
            'flash_name':obj.flash_img.url,
            'flash_price':obj.flash_price,
            'flash_origin_price':obj.flash_origin_price,
            'flash_discount':obj.flash_discount,
            'count':obj.count,
            'amount':obj.amount,
            'object_id':obj.object_id,
            'app_label':obj.content_type.app_label,
            'model':obj.content_type.model
        }

class OrderDetailSerializer(BaseSerializer):

    def to_dict(self):
        obj = self.obj
        items=[]
        for item in obj.order_items.all():
            items.append(OrderItemSerializer(item).to_dict())
        return {
            'sn':obj.sn,
            'buy_count':obj.buy_count,
            'buy_amount':obj.buy_count,
            'types':obj.types,
            'status':obj.status,
            'created_at':obj.created_at,
            'remark':obj.remark,
            'to_user':obj.to_user,
            'to_area':obj.to_area,
            'to_address':obj.to_address,
            'to_phone':obj.to_phone,
            'express_type':obj.express_type,
            'express_no':obj.express_no,
            'items':items
        }

11.3 门票支付接口开发

开发步骤:

设计接口返回内容及字段 

编写接口代码

模拟HTTP请求,测试验证接口

使用面向对象方式,dispatch进行分发,根据不同的请求方式,执行不同的逻辑

@method_decorator(login_required, name='dispatch')
class OrderDetail(BaseDetailView):
    slug_field = 'sn'
    slug_url_kwarg = 'sn'

    def get_queryset(self):
        user = self.request.user
        obj = Order.objects.filter(user=user, is_valid=True)
        return obj

    def get(self, request, *args, **kwargs):
        order_obj = self.get_object()
        data = serializers.OrderDetailSerializer(order_obj).to_dict()
        return http.JsonResponse(data)

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        """订单支付"""
        # 选择支付方式
        # 数据验证
        # 完成支付,改变订单状态
        order_obj = self.get_object()
        if order_obj.status == choices.OrderStatus.SUBMIT:
            order_obj.status = choices.OrderStatus.PAID
            order_obj.save()
            order_obj.order_items.update(status=choices.OrderStatus.PAID)
            return http.JsonResponse('', status=201, safe=False)
        return http.JsonResponse('', status=200, safe=False)

11.4 取消订单接口开发

开发步骤:

设计接口返回内容及字段 

编写接口代码

模拟HTTP请求,测试验证接口

@transaction.atomic
    def put(self, request, *args, **kwargs):
        """取消订单"""
        # 获取订单对象
        # 数据验证

        order_obj = self.get_object()
        if order_obj.status == choices.OrderStatus.SUBMIT:
            # 改变状态
            order_obj.status = choices.OrderStatus.CANCELED
            order_obj.save()
            # 改变状态
            items = order_obj.order_items.filter(status=choices.OrderStatus.SUBMIT)

            # 加回已经扣减的库存
            for item in items:
                obj = item.content_object
                obj.remain_stock = F('remain_stock') + item.count
                obj.save()
            items.update(status=choices.OrderStatus.CANCELED)
            return http.JsonResponse('', status=201, safe=False)
        return http.JsonResponse('', 200, safe=False)

11.5 删除订单接口开发

开发步骤:

设计接口返回内容及字段 

编写接口代码

模拟HTTP请求,测试验证接口

    def delete(self,request,*args,**kwargs):
        """删除订单"""
        # 获取订单对象
        # 验证数据(已支付,已取消)
        # 是否已经删除过了

        order_obj = self.get_object()
        if order_obj.status == choices.OrderStatus.PAID or order_obj.status == choices.OrderStatus.CANCELED:
            if order_obj.is_valid:
                order_obj.is_valid = False
                order_obj.save()
                return http.JsonResponse('',status=201,safe=False)
        return http.JsonResponse('',status=200,safe=False)

11.6 我的订单列表接口开发

开发步骤:

设计接口返回内容及字段 

编写接口代码

模拟HTTP请求,测试验证接口

视图中继承ListView,

1.获取当前用户

2.查询当前用户,is_valid=True和订单状态(前台穿过来)查询

class OrderListView(ListView):

    paginate_by = 5

    def get_queryset(self):
        # 获取用户
        user = self.request.user
        # 获取前台传过来的status
        status = self.request.GET.get('status',None)
        query = Q(user=user,is_valid=True)
        if status and status !='0':
            query = query & Q(status=status)
        # 在Order中查询
        return Order.objects.filter(query)
    def render_to_response(self, context, **response_kwargs):
        page_obj = context['page_obj']
        if page_obj:
            data = serializers.OrderListSerializer(page_obj).to_dict()
            return http.JsonResponse(data)
        return NotFoundJsonResponse()

    def get_paginate_by(self, queryset):
        """根据接口参数limit来控制分页大小"""
        page_size = self.request.GET.get('limit',None)
        return page_size or self.paginate_by

11.7 门票详细信息接口开发

开发步骤:

设计接口返回内容及字段 

编写接口代码

模拟HTTP请求,测试验证接口

class TicketDetailView(DetailView):
    """门票列表"""
    def get_queryset(self):
        return Ticket.objects.filter(is_valid=True)

    def render_to_response(self, context, **response_kwargs):
        page_obj = context['object']
        if page_obj:
            data = serializers.TicketDetailSerializer(page_obj).to_dict()
            return http.JsonResponse(data)
        return NotFoundJsonResponse()

十二、门票接口联调

12.1 门票下单接口联调

实现步骤

  1. 完成页面间的跳转
  2. 配置接口地址及请求参数
  3. 处理数据返回

12.2 门票详细信息接口联调

实现步骤:

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

12.3 门票支付接口联调

实现步骤:

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

用到两个接口,订单支付,订单信息接口

12.4 我的订单列表接口联调

  1. 完成页面间的跳转
  2. 配置接口地址及请求参数
  3. 处理数据返回
  4. 路由切换后的数据处理

12.4 取消订单接口联调

  1. 完成页面间的跳转
  2. 配置接口地址及请求参数
  3. 处理数据返回

12.5 删除订单接口联调

  1. 阅读接口文档
  2. 配置接口地址
  3. 使用axios获取数据
  4. 将数据设置到模型层

删除后的订单要在页面消失,在删除的ajax中将item.sn设为控制,再使用v-show将item.sn绑定,以为删除的该条目的sn为空值,所以为false,会在页面中隐藏

你可能感兴趣的:(前端,javascript,开发语言)