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

上一篇: 带你进入异步Django+Vue的世界 - Didi打车实战(5)
后台创建订单、Channels群发群收
Demo: https://didi-taxi.herokuapp.com/

前端处理订单更新

用户登录后,针对乘客还是司机,显示不同的界面。

  • 司机:不需要下单按钮,替换为抢单按钮
  • 抢单页面,显示所有当前处于REQUESTED状态的单子
    image.png

更新一下导航栏

# /src/App.vue
<template>
  <v-app>
    <v-toolbar app :dark="user && user.group !== null && user.group === 'driver'">

...
computed: {
    ...mapState(['alert', 'user']),
    menuItems () {
      let items = [
        { icon: 'face', title: 'Register', route: '/sign_up' },
        { icon: 'lock_open', title: 'Login', route: '/log_in' }
      ]
      if (this.userIsAuthenticated) {
        items = this.user.group === 'driver' ? [
          { icon: 'notifications', title: 'Pending', route: '/all_requests' },
          { icon: 'exit_to_app', title: 'Exit', route: '' }
        ] : [
          { icon: 'local_taxi', title: 'Call', route: '' },
          { icon: 'exit_to_app', title: 'Exit', route: '' }
        ]
      }
      return items
    },

新增路由:

# /src/router.js
    {
      path: '/all_requests',
      name: 'all_requests',
      component: Requests
    },

Vue抢单页面:

# /views/Requests.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/cards/plane.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="subheading mb-3">
                  {{ item.pick_up_address }} to {{ item.drop_off_address }}
                </v-list-tile-title>
                <div class="caption">{{ item.created | datefilter(true) }}</div>
              </v-list-tile-content>
              <div class="subheading" v-html="item.rider.username" />
              <v-list-tile-avatar>
                <img :src="`https://randomuser.me/api/portraits/thumb/${item.rider.id%2 === 0 ? 'women' : 'men'}/${item.rider.id}.jpg`" :title="item.rider.username" />
              </v-list-tile-avatar>
              <v-btn round color="pink white--text" @click.prevent="startTrip(item.id)">Start</v-btn>
            </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 | datefilter }}</v-card-text>
                  <v-card-text class="grey lighten-3">updated: {{ item.updated | datefilter }}</v-card-text>
                </v-card>
              </v-expansion-panel-content>
            </v-expansion-panel>
      </div>
        </v-list>
      </v-card>
    </v-flex>

  </v-layout>
</template>

接单流程

  1. 打开浏览器窗口,以乘客身份下单:
    显示REQUESTED状态,并且司机头像为灰色,说明暂时无人接单

    image.png

  2. 打开浏览器隐身窗口,以司机角色登录
    会显示提示:“有新的订单”

image.png
  1. 司机点击“START”,乘客端会显示“已接单”,并且显示司机的信息


    image.png
  2. 司机端则提示下一步操作为“Pick-Up”


    image.png
  3. 司机接到乘客后,点“Pick-Up”,提示下一步操作为“Drop-Off”


    image.png
  4. 司机到达目的地后,点“Drop-off”,订单结束!


    image.png

以上步骤看起来很复杂,其实,我们之前已经搭建好框架,通过添加Vuex store的actionsmutations就可以了。
点击“Pick-up”之后,同一group里的成员,都会收到echo.message:

WS create.trip

在前一篇已经阐述了。
这里要添加的是:新订单创建后,司机群里的成员都能收到。

WS received: 
  {"type":"echo.message",
  "data":{"...
","status":"REQUESTED"}}

我们在store里添加OnMessage,如果收到echo.message的Trip.status==REQUESTED,则通知司机们:

# /src/store/modules/ws.js
  // handle msg from server
  wsOnMessage ({ dispatch, commit }, e) {
    const rdata = JSON.parse(e.data)
    console.log('WS received: ' + JSON.stringify(rdata))
    switch (rdata.type) {
      case 'create.trip':
        commit('setAlert', { type: 'info', msg: '成功添加订单' }, { root: true })
        commit('messages/addTrip', rdata.data, { root: true })
        break
      case 'update.trip':
        commit('setAlert', { type: 'info', msg: '成功更新订单' }, { root: true })
        commit('messages/updateTrip', rdata.data, { root: true })
        // driver redirect to Home.vue
        router.push('/')
        break
      case 'echo.message':
        switch (rdata.data.status) {
          case 'REQUESTED':
            commit('setAlert', { type: 'info', msg: '有新的订单!' }, { root: true })
            commit('messages/updateTrip', rdata.data, { root: true })
            // driver redirect to Requests.vue
            router.push('/all_requests')
            break
          case 'STARTED':
            ...
        }
        break
    }

WS update.trip

乘客和司机会同时收到两条:

WS received: 
  {"type":"update.trip",
  "data":{"id":"6e446f7f-606d-488c-9274-f786b9f06800","driver":{"id":6,"username":"driver3","first_name":"","last_name":"","group":"driver"},"rider":{"id":2,"username":"rider1","first_name":"","last_name":"","group":null},"created":"2019-05-18T07:18:31.096533Z","updated":"2019-05-20T05:06:47.219332Z","pick_up_address":"南京","drop_off_address":"镇江","status":"STARTED"}}

WS received: 
  {"type":"echo.message",
  "data":{"id":"6e446f7f-606d-488c-9274-f786b9f06800","driver":{"id":6,"username":"driver3","first_name":"","last_name":"","group":"driver"},"rider":{"id":2,"username":"rider1","first_name":"","last_name":"","group":null},"created":"2019-05-18T07:18:31.096533Z","updated":"2019-05-20T05:06:47.219332Z","pick_up_address":"南京","drop_off_address":"镇江","status":"STARTED"}}

同理,在/src/store/modules/ws.js里,针对Trip.status==XXX进行操作即可。

Vue页面处理

Requests.vue <template>,处理“Start”按钮:

# /src/views/Requests.vue
<script>
import { mapState, mapActions } from 'vuex'

export default {
  data () {
    return {
      card_text: 'No data'
    }
  },
  filters: {
    datefilter (para, part) {
      let time = new Date(para)
      if (part) return `${time.toLocaleTimeString()}`
      return `${time.toLocaleDateString()} ${time.toLocaleTimeString()}`
    }
  },
  computed: {
    ...mapState(['alert', 'user']),
    ...mapState('messages', ['trips', 'websocket']),
    userIsAuthenticated () {
      return this.user !== null && this.$store.getters.user !== undefined
    },
    trips_ongoing () {
      return this.trips.filter(obj => obj.status === 'REQUESTED')
    }
  },
  mounted () {
  },
  methods: {
    ...mapActions(['clearAlert']),
    ...mapActions('ws', [
      'wsOnOpen',
      'wsOnError',
      'wsOnMessage'
    ]),
    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')
      }
    },
    startTrip (id) {
      this.$store.dispatch('ws/updateTrip', { id: id, status: 'STARTED', driver: this.user.id })
    }
  }
}
</script>

store里添加actions:

# /src/store/modules/ws.js
  async createTrip ({ commit }, message) {
    await wsService.createTrip(state.websocket.ws, message)
  },
  async updateTrip ({ commit }, message) {
    await wsService.updateTrip(state.websocket.ws, message)
  }

Home.vue <template>,不同状态,显示不同按钮

# /src/views/Home.vue
<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="subheading mb-3">
                  {{ item.pick_up_address }} to {{ item.drop_off_address }}
                </v-list-tile-title>
                <div class="caption">{{ item.created | datefilter(true) }}</div>
              </v-list-tile-content>

              <div class="subheading" v-html="user.group ==='driver' ? item.rider.username : item.driver ? item.driver.username : ''" />
              <v-list-tile-avatar v-if="user.group ==='driver'">
                <img :src="`https://randomuser.me/api/portraits/thumb/${item.rider.id%2 === 0 ? 'women' : 'men'}/${item.rider.id}.jpg`" :title="item.rider.username" />
              </v-list-tile-avatar>
              <v-list-tile-avatar v-if="!item.driver && user.group ==='rider'">
                  <v-icon x-large color="grey lighten-3" title="no driver">account_circle</v-icon>
                </v-list-tile-avatar>
              <v-list-tile-avatar v-if="item.driver && user.group ==='rider'">
                    <img :src="`https://randomuser.me/api/portraits/thumb/${item.driver.id%2 === 0 ? 'women' : 'men'}/${item.driver.id}.jpg`" :title="item.driver.username" />
                  </v-list-tile-avatar>
              <v-btn round color="pink white--text" @click.prevent="pickupTrip(item.id)" v-if="user.group ==='driver' && item.status === 'STARTED'">Pick-Up</v-btn>
              <v-btn round color="pink white--text" @click.prevent="dropoffTrip(item.id)" v-if="user.group ==='driver' && item.status === 'IN_PROGRESS'">Drop-off</v-btn>
            </v-list-tile>

            <v-timeline align-top dense v-if="item.status !== 'REQUESTED'">
              <v-timeline-item color="green" small>
                <v-layout pt-3>
                  <v-flex xs3>
                    <div class="caption" v-if="item.status === 'STARTED'">{{ item.updated | datefilter(true)}}</div>
                  </v-flex>
                  <v-flex>
                    <strong :class="item.status === 'IN_PROGRESS' ? 'green--text' : 'pink--text'">
                  司机已接单,飞奔而来</strong>
                  </v-flex>
                </v-layout>
              </v-timeline-item>
              <v-timeline-item :color="item.status === 'IN_PROGRESS' ? 'green' : 'grey'" small >
                <v-layout pt-3>
                  <v-flex xs3>
                    <div class="caption" v-if="item.status === 'IN_PROGRESS'">{{ item.updated | datefilter(true)}}</div>
                  </v-flex>
                  <v-flex>
                    <strong :class="item.status === 'IN_PROGRESS' ? 'pink--text' : 'grey--text'">
                    正驶往目的地</strong>
                  </v-flex>
                </v-layout>
              </v-timeline-item>
            </v-timeline>

            <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 | datefilter }}</v-card-text>
                  <v-card-text class="grey lighten-3">updated: {{ item.updated | datefilter }}</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>

<script>里,发送到store actions即可:

    pickupTrip (id) {
      this.$store.dispatch('ws/updateTrip', { id: id, status: 'IN_PROGRESS', driver: this.user.id })
    },
    dropoffTrip (id) {
      this.$store.dispatch('ws/updateTrip', { id: id, status: 'COMPLETED', driver: this.user.id })
    }

Start, Pick-Up, Drop-Off

司机的这些操作,其实都是WebSockets发送update.trip

# /src/store/modules/ws.js
          case 'STARTED':
            commit('setAlert', { type: 'info', msg: '司机已接单!' }, { root: true })
            commit('messages/updateTrip', rdata.data, { root: true })
            break
          case 'IN_PROGRESS':
            commit('setAlert', { type: 'info', msg: '正驶往目的地!' }, { root: true })
            commit('messages/updateTrip', rdata.data, { root: true })
            break
          case 'COMLETED':
            commit('setAlert', { type: 'info', msg: '订单完成!' }, { root: true })
            commit('messages/updateTrip', rdata.data, { root: true })
            break

统一更新trips:

# /src/store/modules/messages.js
const mutations = {
  addTrip (state, message) {
    // state.trips.push(message)
    state.trips.splice(0, 0, message)
  },
  updateTrip (state, message) {
    // how to trigger Vuex update: https://blog.csdn.net/qq_34935885/article/details/75734365
    let index = state.trips.findIndex((e) => e.id === message.id)
    state.trips.splice(index, 1, message)
  },

总结

前后端的一个Didi打车系统,已经大功告成了!!
后续大家可以进一步优化:

  • 每个用户只能看到自己订单
  • 用户可以修改资料,上传头像
  • 邮件确认、重置密码
  • WebSockets中断后,自动重连
  • 地图选点
  • 取消订单
  • 删除订单
  • cache
  • 排行榜(Redis)
  • static文件单独serve
  • 。。。

下一步:部署到远程服务器
需要源码的请留言哦~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269