python自学Vue.js

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

2019-05-18  本文已影响13人  非梦nj

带你进入异步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用来存储订单的状态:
# /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

# 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)

上一篇下一篇

猜你喜欢

热点阅读