帶你進入異步Django+Vue的世界 - Didi打車實戰(3)

帶你進入異步Django+Vue的世界 - Didi打車實戰(2) http://www.lxweimin.com/p/f6a83315e055
Vue + Vuetify 前端鑒權實現
Demo: https://didi-taxi.herokuapp.com/

后臺數據模型設計

數據模型是后臺的靈魂,需要考慮周全。
數據模型的更新,使用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)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容