帶你進(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
可以很方便地遷移
- 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
- Trip,繼承通用模型
Model
id
用uuid4
來(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})
- 把模型登記到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',
)
- 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訂單:
用戶查看Trip功能
- 后端需要提供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里账劲,顯示所有的訂單信息
# /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的使用搬瑰。