美文网首页python自学Vue.js
带你进入异步Django+Vue的世界 - Didi打车实战(3

带你进入异步Django+Vue的世界 - Didi打车实战(3

作者: 非梦nj | 来源:发表于2019-05-18 20:21 被阅读13次

    带你进入异步Django+Vue的世界 - Didi打车实战(2) https://www.jianshu.com/p/f6a83315e055
    Vue + Vuetify 前端鉴权实现

    后台数据模型设计

    数据模型是后台的灵魂,需要考虑周全。
    数据模型的更新,使用python manage.py makemigrations可以很方便地迁移

    1. User,继承AbstractUser
      group指明用户是乘客还是司机
      photo用来上存储用户的头像
    # /backend/api/models.py
    from django.db import models
    from django.conf import settings
    from django.shortcuts import reverse
    from django.contrib.auth.models import AbstractUser
    
    import uuid
    
    
    class User(AbstractUser):
        photo = models.ImageField(upload_to='photos', null=True, blank=True)
    
        @property
        def group(self):
            groups = self.groups.all()
            return groups[0].name if groups else None
    
    1. Trip,继承通用模型Model
      iduuid4来指明一下唯一的订单编号
      pick_up_address/drop_off_address指明上车地点和目的地
      status用来存储订单的状态:
    • 下单REQUESTED
    • 已接单STARTED
    • 行程中IN_PROGRESS
    • 行程结束COMPLETED
      driver/rider是外键,关联User模型
    # /backend/api/models.py
    class Trip(models.Model):
        REQUESTED = 'REQUESTED'
        STARTED = 'STARTED'
        IN_PROGRESS = 'IN_PROGRESS'
        COMPLETED = 'COMPLETED'
        STATUSES = (
            (REQUESTED, REQUESTED),
            (STARTED, STARTED),
            (IN_PROGRESS, IN_PROGRESS),
            (COMPLETED, COMPLETED),
        )
    
        id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
        created = models.DateTimeField(auto_now_add=True)
        updated = models.DateTimeField(auto_now=True)
        pick_up_address = models.CharField(max_length=255)
        drop_off_address = models.CharField(max_length=255)
        status = models.CharField(max_length=20, choices=STATUSES, default=REQUESTED)
        driver = models.ForeignKey(
            settings.AUTH_USER_MODEL,
            null=True,
            blank=True,
            on_delete=models.DO_NOTHING,
            related_name='trip_as_driver'
        )
        rider = models.ForeignKey(
            settings.AUTH_USER_MODEL,
            null=True,
            blank=True,
            on_delete=models.DO_NOTHING,
            related_name='trip_as_rider'
        )
    
        def __str__(self):
            return f'{self.id}'
    
        def get_absolute_url(self):
            return reverse('trip:trip_detail', kwargs={'trip_id': self.id})
    
    1. 把模型登记到django admin里:
    # /backend/api/admin.py
    from django.contrib import admin
    from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
    
    from .models import User, Trip
    
    
    @admin.register(User)
    class UserAdmin(DefaultUserAdmin):
        ...
    
    
    @admin.register(Trip)
    class TripAdmin(admin.ModelAdmin):
        fields = (
            'id', 'pick_up_address', 'drop_off_address', 'status',
            'driver', 'rider', 'created', 'updated',
        )
        list_display = (
            'id', 'pick_up_address', 'drop_off_address', 'status',
            'driver', 'rider', 'created', 'updated',
        )
        list_filter = ('status',)
        readonly_fields = (
            'id', 'created', 'updated',
        ) 
    
    1. DRF只处理鉴权和trips view,所以先删除不需要的URL:
    # /backend/urls.py 删除后如下所示:
    from django.contrib import admin
    from django.urls import path, re_path, include
    
    from .api.views import index_view, serve_worker_view
    
    
    urlpatterns = [
        # http://localhost:8000/
        path('', index_view, name='index'),
    
        # serve static files for PWA
        path('index.html', index_view, name='index'),
        re_path(r'^(?P<worker_name>manifest).json$', serve_worker_view, name='manifest'),
        re_path(r'^(?P<worker_name>[-\w\d.]+).js$', serve_worker_view, name='serve_worker'),
        re_path(r'^(?P<worker_name>robots).txt$', serve_worker_view, name='robots'),
    
        # http://localhost:8000/admin/
        path('admin/', admin.site.urls),
    
        # support vue-router history mode
        re_path(r'^\S+$', index_view, name='SPA_reload'),
    ]
    

    删除不需要的view:

    # /backend/api/views.py
    删除 from .models import Message, MessageSerializer
    删除 class MessageViewSet
    

    模型更新:

    (didi-project) git/didi-project$ python manage.py makemigrations
    Migrations for 'api':
      backend/api/migrations/0002_auto_20190518_0708.py
        - Create model Trip
        - Delete model Message
        - Add field photo to user
        - Add field driver to trip
        - Add field rider to trip
    (didi-project) git/didi-project$ python manage.py migrate
    Operations to perform:
      Apply all migrations: admin, api, auth, contenttypes, sessions
    Running migrations:
      Applying api.0002_auto_20190518_0708... OK
    

    在Admin里测试下Trip

    创建几个测试用户,然后创建Trip订单:


    image.png image.png

    用户查看Trip功能

    1. 后端需要提供Serializer、View、Url

    Serializer

    # trips/serializers.py
    from .models import Trip
    
    class TripSerializer(serializers.ModelSerializer):
        class Meta:
            model = Trip
            fields = '__all__'
            read_only_fields = ('id', 'created', 'updated',)
    

    其中三个字段,是只读的,不需要Serializer创建: id, created, updated .

    View

    Add the TripView to api/views.py:

    # trips/views.py
    from django.contrib.auth import get_user_model, login, logout
    from django.contrib.auth.forms import AuthenticationForm
    from rest_framework import generics, permissions, status, views, viewsets # new
    from rest_framework.response import Response
    
    from .models import Trip # new
    from .serializers import TripSerializer, UserSerializer # new
    
    class TripView(viewsets.ReadOnlyModelViewSet):
        permission_classes = (permissions.IsAuthenticated,)
        queryset = Trip.objects.all()
        serializer_class = TripSerializer
    

    TripView非常基本,使用DRF ReadOnlyModelViewSet:返回trip列表和 trip详情 views.
    这个路由是需要鉴权的。

    URLs

    在总路由里,添加trips.urls子路由:

    # taxi/urls.py
    
    from django.contrib import admin
    from django.urls import include, path # new
    
    from .api.views import index_view, serve_worker_view, SignUpView, LogInView, LogOutView
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('api/sign_up/', SignUpView.as_view(), name='sign_up'),
        path('api/log_in/', LogInView.as_view(), name='log_in'),
        path('api/log_out/', LogOutView.as_view(), name='log_out'),
        path('api/trip/', include('api.urls', 'trip',)), # new
    ]
    

    创建子路由文件:

    # trips/urls.py
    
    from django.urls import path
    
    from .views import TripView
    
    app_name = 'api'
    
    urlpatterns = [
        path('', TripView.as_view({'get': 'list'}), name='trip_list'),
    ]
    

    更新前端,显示Trips

    Home.vue里,显示所有的订单信息


    image.png
    # /src/views/Home.vue
    <template>
      <v-layout row wrap>
        <v-flex xs12 sm6 offset-sm3>
          <v-card class="mb-4">
            <v-img
              src="https://cdn.vuetifyjs.com/images/parallax/material2.jpg"
              aspect-ratio="5" class="white--text">
              <v-container fill-height fluid>
                    <span class="display-2">当前订单</span>
              </v-container>
            </v-img>
            <v-list v-if="!userIsAuthenticated || !trips_ongoing">
              <div class="grey--text ml-5"> {{ card_text }} </div>
            </v-list>
    
            <v-list v-if="trips_ongoing">
              <div v-for="(item, index) in trips_ongoing" :key="index">
                <v-list-tile avatar class="my-2">
                  <v-list-tile-content>
                    <v-list-tile-title class="title mb-3">
                      {{ item.pick_up_address }} to {{ item.drop_off_address }}
                    </v-list-tile-title>
                  </v-list-tile-content>
    
                  <v-list-tile-avatar v-if="!item.driver">
                    <v-icon x-large>account_circle</v-icon>
                </v-list-tile-avatar>
                  <v-list-tile-avatar v-else>
                  <img :src="`https://randomuser.me/api/portraits/thumb/women/2${item.driver}.jpg`">
                </v-list-tile-avatar>
                </v-list-tile>
                <v-expansion-panel>
                  <v-expansion-panel-content>
                    <template v-slot:header>
                      <v-chip class="yellow " small>{{ item.status }}</v-chip>
                      <v-spacer></v-spacer>
                    </template>
                    <v-card>
                      <v-card-text class="grey lighten-3">created: {{ item.created }}</v-card-text>
                      <v-card-text class="grey lighten-3">updated: {{ item.updated }}</v-card-text>
                      <v-card-actions>
                    <v-spacer></v-spacer>
                    <v-btn flat color="red" @click.prevent="cancelTrip(item.id)">Cancel</v-btn>
                  </v-card-actions>
                    </v-card>
                  </v-expansion-panel-content>
                </v-expansion-panel>
          </div>
            </v-list>
          </v-card>
        </v-flex>
    
        <v-flex xs12 sm6 offset-sm3>
          <v-card class="mb-4">
            <v-img
              src="https://cdn.vuetifyjs.com/images/cards/docks.jpg"
              aspect-ratio="5" class="white--text">
              <v-container fill-height fluid>
                    <span class="display-2">历史订单</span>
              </v-container>
            </v-img>
            <v-list v-if="!userIsAuthenticated || !trips_done">
              <div class="grey--text ml-5"> {{ card_text }} </div>
            </v-list>
    
            <v-list v-if="trips_done">
              <div v-for="(item, index) in trips_done" :key="index">
                <v-list-tile avatar class="my-2">
                  <v-list-tile-content>
                    <v-list-tile-title class="title mb-3">
                      {{ item.pick_up_address }} to {{ item.drop_off_address }}
                    </v-list-tile-title>
                  </v-list-tile-content>
    
                  <v-list-tile-avatar v-if="!item.driver">
                    <v-icon x-large>account_circle</v-icon>
                </v-list-tile-avatar>
                  <v-list-tile-avatar v-else>
                  <img :src="`https://randomuser.me/api/portraits/thumb/women/2${item.driver}.jpg`">
                </v-list-tile-avatar>
                </v-list-tile>
                <v-expansion-panel>
                  <v-expansion-panel-content>
                    <template v-slot:header>
                      <v-chip small>{{ item.status }}</v-chip>
                      <v-spacer></v-spacer>
                    </template>
                    <v-card>
                      <v-card-text class="grey lighten-3">created: {{ item.created }}</v-card-text>
                      <v-card-text class="grey lighten-3">updated: {{ item.updated }}</v-card-text>
                    </v-card>
                  </v-expansion-panel-content>
                </v-expansion-panel>
          </div>
            </v-list>
          </v-card>
        </v-flex>
    
      </v-layout>
    </template>
    
    <script>
    import { mapState, mapActions } from 'vuex'
    
    export default {
      data () {
        return {
          card_text: 'No data'
        }
      },
      computed: {
        ...mapState(['alert', 'user']),
        ...mapState('messages', ['trips']),
        userIsAuthenticated () {
          return this.user !== null && this.$store.getters.user !== undefined
        },
        trips_ongoing () {
          return this.trips.filter(obj => obj.status !== 'COMPLETED')
        },
        trips_done () {
          return this.trips.filter(obj => obj.status === 'COMPLETED')
        }
      },
      mounted () {
        if (this.userIsAuthenticated) {
          this.$store.dispatch('messages/getTrips')
        }
      },
      methods: {
        ...mapActions(['clearAlert']),
        cancelTrip (id) {
          console.log(id)
        },
        menu_click (title) {
          if (title === 'Exit') {
            this.$store.dispatch('messages/signUserOut')
          } else if (title === 'Call') {
            this.$store.dispatch('messages/callTaxi')
          }
        }
      }
    }
    </script>
    

    分成已完成订单和正在进行中的订单,用trip的status区别:

      computed: {
        ...mapState(['alert', 'user']),
        ...mapState('messages', ['trips']),
        userIsAuthenticated () {
          return this.user !== null && this.$store.getters.user !== undefined
        },
        trips_ongoing () {
          return this.trips.filter(obj => obj.status !== 'COMPLETED')
        },
        trips_done () {
          return this.trips.filter(obj => obj.status === 'COMPLETED')
        }
      },
    

    装载此页面时,读取后台的trip信息:

      mounted () {
        if (this.userIsAuthenticated) {
          this.$store.dispatch('messages/getTrips')
        }
      },
    

    Vuex store里,添加trips的操作:

    # /src/store/modules/message.js
    const state = {
      messages: [],
      trips: []
    }
    
    const mutations = {
      setTrips (state, messages) {
        state.trips = messages
      },
    
    const actions = {
      getTrips ({ commit }) {
        messageService.fetchTrips()
          .then(messages => {
            commit('setTrips', messages)
          })
      },
    

    ajax服务:

    # /src/services/messageService.js
      fetchTrips () {
        return api.get(`trip/`)
          .then(response => response.data)
      },
    

    以上是读取所有Trips的列表,对于单条trip记录的读取,需要后台添加view:

    更新views.py

    • lookup_field 告诉后台通过id来查找trip记录
    • lookup_url_kwarg 是url的是kwarg名字
    # api/views.py
    
    class TripView(viewsets.ReadOnlyModelViewSet):
        lookup_field = 'id' # new
        lookup_url_kwarg = 'trip_id' # new
        permission_classes = (permissions.IsAuthenticated,)
        queryset = Trip.objects.all()
        serializer_class = TripSerializer
    

    更新URL记录:

    # api/urls.py
    
    from django.urls import path, re_path  # changed
    
    from .views import TripView
    
    
    app_name = 'api'
    
    urlpatterns = [
        path('', TripView.as_view({'get': 'list'}), name='trip_list'),
        path('<uuid:trip_id>/', TripView.as_view({'get': 'retrieve'}), name='trip_detail'),  # new
    ]
    

    测试一下:

    浏览器输入:http://localhost:8080/api/trip/6e446f7f-606d-488c-9274-f786b9f06800/,应该就可以查到详情了。

    用户退出时,清除Trip记录

    # /src/store/modules/messages.js
      signUserOut ({ commit }) {
        commit('setLoading', true, { root: true })
        messageService.signUserOut()
          .then(messages => {
            commit('setAlert', { type: 'info', msg: 'Log-out success!' }, { root: true })
            commit('setUser', null, { root: true })
            commit('setTrips', [])
            localStorage.removeItem('user')
            commit('setLoading', false, { root: true })
          })
      },
    

    总结

    这篇主要是数据库设计和前、后台的综合运用,加深印象。
    下一篇,会进入到Django Channels + Websockets的使用。

    带你进入异步Django+Vue的世界 - Didi打车实战(4)

    相关文章

      网友评论

        本文标题:带你进入异步Django+Vue的世界 - Didi打车实战(3

        本文链接:https://www.haomeiwen.com/subject/uaskoqtx.html