帶你進(jìn)入異步Django+Vue的世界 - Didi打車實(shí)戰(zhàn)(3)

帶你進(jìn)入異步Django+Vue的世界 - Didi打車實(shí)戰(zhàn)(2) http://www.reibang.com/p/f6a83315e055
Vue + Vuetify 前端鑒權(quán)實(shí)現(xiàn)
Demo: https://didi-taxi.herokuapp.com/

后臺(tái)數(shù)據(jù)模型設(shè)計(jì)

數(shù)據(jù)模型是后臺(tái)的靈魂萝挤,需要考慮周全。
數(shù)據(jù)模型的更新,使用python manage.py makemigrations可以很方便地遷移

  1. User,繼承AbstractUser
    group指明用戶是乘客還是司機(jī)
    photo用來(lái)上存儲(chǔ)用戶的頭像
# /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來(lái)指明一下唯一的訂單編號(hào)
    pick_up_address/drop_off_address指明上車地點(diǎn)和目的地
    status用來(lái)存儲(chǔ)訂單的狀態(tài):
  • 下單REQUESTED
  • 已接單STARTED
  • 行程中IN_PROGRESS
  • 行程結(jié)束COMPLETED
    driver/rider是外鍵,關(guān)聯(lián)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只處理鑒權(quán)和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里測(cè)試下Trip

創(chuàng)建幾個(gè)測(cè)試用戶汽烦,然后創(chuàng)建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',)

其中三個(gè)字段礁叔,是只讀的,不需要Serializer創(chuàng)建: 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.
這個(gè)路由是需要鑒權(quán)的画机。

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
]

創(chuàng)建子路由文件:

# 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">當(dāng)前訂單</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>

分成已完成訂單和正在進(jìn)行中的訂單,用trip的status區(qū)別:

  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')
    }
  },

裝載此頁(yè)面時(shí)榛瓮,讀取后臺(tái)的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服務(wù):

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

以上是讀取所有Trips的列表禀晓,對(duì)于單條trip記錄的讀取坝锰,需要后臺(tái)添加view:

更新views.py

  • lookup_field 告訴后臺(tái)通過id來(lái)查找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
]

測(cè)試一下:

瀏覽器輸入:http://localhost:8080/api/trip/6e446f7f-606d-488c-9274-f786b9f06800/,應(yīng)該就可以查到詳情了凫乖。

用戶退出時(shí),清除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 })
      })
  },

總結(jié)

這篇主要是數(shù)據(jù)庫(kù)設(shè)計(jì)和前帽芽、后臺(tái)的綜合運(yùn)用,加深印象导街。
下一篇,會(huì)進(jìn)入到Django Channels + Websockets的使用搬瑰。

帶你進(jìn)入異步Django+Vue的世界 - Didi打車實(shí)戰(zhàn)(4)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市徽职,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌姆钉,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件潮瓶,死亡現(xiàn)場(chǎng)離奇詭異钙姊,居然都是意外死亡毯辅,警方通過查閱死者的電腦和手機(jī)煞额,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)膊毁,“玉大人,你說我怎么就攤上這事婚温。” “怎么了荆秦?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵力图,是天一觀的道長(zhǎng)步绸。 經(jīng)常有香客問我搪哪,道長(zhǎng)坪圾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任兽泄,我火速辦了婚禮漾月,結(jié)果婚禮上病梢,老公的妹妹穿的比我還像新娘梁肿。我一直安慰自己,他們只是感情好吩蔑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著隧期,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仆潮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天性置,我揣著相機(jī)與錄音揍堰,去河邊找鬼鹏浅。 笑死屏歹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的西采。 我是一名探鬼主播继控,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼械馆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了霹崎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤尾菇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后派诬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡默赂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了曲掰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡栏妖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吊趾,到底是詐尸還是另有隱情,我是刑警寧澤趾徽,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布翰守,位于F島的核電站孵奶,受9級(jí)特大地震影響蜡峰,放射性物質(zhì)發(fā)生泄漏了袁。R本人自食惡果不足惜湿颅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望油航。 院中可真熱鬧,春花似錦谊囚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)奠伪。三九已至首懈,卻和暖如春谨敛,著一層夾襖步出監(jiān)牢的瞬間究履,已是汗流浹背佣盒。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肥惭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓蜜葱,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親牵囤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子爸黄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容