带你进入异步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
可以很方便地迁移
- 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
- Trip,继承通用模型
Model
id
用uuid4
来指明一下唯一的订单编号
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})
- 把模型登记到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只处理鉴权和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功能
- 后端需要提供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的使用。