全棧「食」代:Django + Nuxt 實現美食分享網站(下)

全棧「食」代:Django + Nuxt 實現美食分享網站(下)

上篇中,咱們分別用 Django 和 Nuxt 實現了後端和前端的雛形。在這一部分,咱們將實現先後端之間的通訊,使得前端能夠從後端獲取數據,而且將進一步豐富網站的功能。css

本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️ 這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch

從服務器獲取數據

在這一部分,咱們將真正實現一個全棧應用——讓前端可以向後端發起請求,從而獲取想要的數據。html

配置 Django 的靜態文件服務

首先咱們要配置一下 Django 服務器,使前端可以訪問其靜態文件。調整 api/api/urls.py 文件以下:前端

# ...
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('core.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
注意

這樣配置靜態文件路由的方式僅應當在開發環境下使用。在生產環境下(settings.py 中的 DEBUG 設爲 False 時),靜態文件路由將自動失效(由於 Django 並不適合做爲靜態文件服務器,應該選用相似 Nginx 之類的服務器,在後續教程中咱們將更深刻地討論)。vue

實現前端的數據請求功能

在客戶端,咱們先要對 Nuxt 進行全局配置。Nuxt 包括 axios 包,這是一個很是出色的基於 Promise 的 HTTP 請求庫。在 nuxt.config.js 中的 axios 一項中添加 Django 服務器的 URL:python

export default {
  // ...

  /*
  ** Axios module configuration
  ** See https://axios.nuxtjs.org/options
  */
  axios: {
    baseURL: 'http://localhost:8000/api',
  },

  // ...
}

將食譜列表頁面中暫時填充的假數據刪去,經過 asyncData 方法獲取數據。因爲咱們以前配置好了 axios,因此 asyncData 函數能夠獲取到 $axios 對象用於發起 HTTP 請求。咱們實現頁面加載的數據獲取以及 deleteRecipe 事件,代碼以下:ios

<template>
  <main class="container mt-5">
    <div class="row">
      <div class="col-12 text-right mb-4">
        <div class="d-flex justify-content-between">
          <h3>吃貨天堂</h3>
          <nuxt-link to="/recipes/add" class="btn btn-info">添加食譜</nuxt-link>
        </div>
      </div>
      <template v-for="recipe in recipes">
        <div :key="recipe.id" class="col-lg-3 col-md-4 col-sm-6 mb-4">
          <recipe-card :onDelete="deleteRecipe" :recipe="recipe"></recipe-card>
        </div>
      </template>
    </div>
  </main>
</template>

<script>
import RecipeCard from "~/components/RecipeCard.vue";

export default {
  head() {
    return {
      title: "食譜列表"
    };
  },
  components: {
    RecipeCard
  },
  async asyncData({ $axios, params }) {
    try {
      let recipes = await $axios.$get(`/recipes/`);
      return { recipes };
    } catch (e) {
      return { recipes: [] };
    }
  },
  data() {
    return {
      recipes: []
    };
  },
  methods: {
    async deleteRecipe(recipe_id) {
      try {
        if (confirm('確認要刪除嗎?')) {
          await this.$axios.$delete(`/recipes/${recipe_id}/`);
          let newRecipes = await this.$axios.$get("/recipes/");
          this.recipes = newRecipes;
        }
      } catch (e) {
        console.log(e);
      }
    }
  }
};
</script>

<style scoped>
</style>

實現食譜詳情頁面

咱們進一步實現食譜詳情頁面。在 pages/recipes 目錄中建立 _id 目錄,在其中添加 index.vue 文件,代碼以下:git

<template>
  <main class="container my-5">
    <div class="row">
      <div class="col-12 text-center my-3">
        <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
      </div>
      <div class="col-md-6 mb-4">
        <img
          class="img-fluid"
          style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
          :src="recipe.picture"
          alt
        >
      </div>
      <div class="col-md-6">
        <div class="recipe-details">
          <h4>食材</h4>
          <p>{{ recipe.ingredients }}</p>
          <h4>準備時間 ⏱</h4>
          <p>{{ recipe.prep_time }} mins</p>
          <h4>製做難度</h4>
          <p>{{ recipe.difficulty }}</p>
          <h4>製做指南</h4>
          <textarea class="form-control" rows="10" v-html="recipe.prep_guide" disabled/>
        </div>
      </div>
    </div>
  </main>
</template>

<script>
export default {
  head() {
    return {
      title: "食譜詳情"
    };
  },
  async asyncData({ $axios, params }) {
    try {
      let recipe = await $axios.$get(`/recipes/${params.id}`);
      return { recipe };
    } catch (e) {
      return { recipe: [] };
    }
  },
  data() {
    return {
      recipe: {
        name: "",
        picture: "",
        ingredients: "",
        difficulty: "",
        prep_time: null,
        prep_guide: ""
      }
    };
  }
};
</script>

<style scoped>
</style>

爲了測試前端頁面可否真正從後端獲取數據,咱們先要在後端數據庫中添加一些數據,而這對 Django 來講就很是方便了。進入 api 目錄,運行 python manage.py runserver 打開服務器,而後進入後臺管理頁面(http://localhost:8000/admin),添加一些數據:github

再運行前端頁面,能夠看到咱們剛剛在 Django 後臺管理中添加的項目:數據庫

實現食譜的編輯和建立頁面

有了前面的鋪墊,實現食譜的添加和刪除也基本上是循序漸進了。咱們在 pages/recipes/_id 中實現 edit.vue(食譜編輯頁面),代碼以下:django

<template>
  <main class="container my-5">
    <div class="row">
      <div class="col-12 text-center my-3">
        <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
      </div>
      <div class="col-md-6 mb-4">
        <img v-if="!preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"  :src="recipe.picture">
        <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"  :src="preview">
      </div>
      <div class="col-md-4">
        <form @submit.prevent="submitRecipe">
          <div class="form-group">
            <label for>Recipe Name</label>
            <input type="text" class="form-control" v-model="recipe.name" >
          </div>
          <div class="form-group">
            <label for>Ingredients</label>
            <input type="text" v-model="recipe.ingredients" class="form-control" name="Ingredients" >
          </div>
          <div class="form-group">
            <label for>Food picture</label>
            <input type="file" @change="onFileChange">
          </div>
          <div class="row">
            <div class="col-md-6">
              <div class="form-group">
                <label for>Difficulty</label>
                <select v-model="recipe.difficulty" class="form-control" >
                  <option value="Easy">Easy</option>
                  <option value="Medium">Medium</option>
                  <option value="Hard">Hard</option>
                </select>
              </div>
            </div>
            <div class="col-md-6">
              <div class="form-group">
                <label for>
                  Prep time
                  <small>(minutes)</small>
                </label>
                <input type="text" v-model="recipe.prep_time" class="form-control" name="Ingredients" >
              </div>
            </div>
          </div>
          <div class="form-group mb-3">
            <label for>Preparation guide</label>
            <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea>
          </div>
          <button type="submit" class="btn btn-success">Save</button>
        </form>
      </div>
    </div>
  </main>
</template>

<script>
export default {
  head(){
      return {
        title: "編輯食譜"
      }
    },
  async asyncData({ $axios, params }) {
    try {
      let recipe = await $axios.$get(`/recipes/${params.id}`);
      return { recipe };
    } catch (e) {
      return { recipe: [] };
    }
  },
  data() {
    return {
      recipe: {
        name: "",
        picture: "",
        ingredients: "",
        difficulty: "",
        prep_time: null,
        prep_guide: ""
      },
      preview: ""
    };
  },
  methods: {
    onFileChange(e) {
      let files = e.target.files || e.dataTransfer.files;
      if (!files.length) {
        return;
      }
      this.recipe.picture = files[0]
      this.createImage(files[0]);
    },
    createImage(file) {
      let reader = new FileReader();
      let vm = this;
      reader.onload = e => {
        vm.preview = e.target.result;
      };
      reader.readAsDataURL(file);
    },
    async submitRecipe() {
      let editedRecipe = this.recipe
      if (editedRecipe.picture.indexOf("http://") != -1){
        delete editedRecipe["picture"]
      }
      const config = {
        headers: { "content-type": "multipart/form-data" }
      };
      let formData = new FormData();
      for (let data in editedRecipe) {
        formData.append(data, editedRecipe[data]);
      }
      try {
        let response = await this.$axios.$patch(`/recipes/${editedRecipe.id}/`, formData, config);
        this.$router.push("/recipes/");
      } catch (e) {
        console.log(e);
      }
    }
  }
};
</script>

<style>
</style>

實現以後的頁面以下:

繼續在 pages/recipes/_id 中實現 add.vue (建立食譜頁面)以下:

<template>
  <main class="container my-5">
    <div class="row">
      <div class="col-12 text-center my-3">
        <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
      </div>
      <div class="col-md-6 mb-4">
        <img
          v-if="preview"
          class="img-fluid"
          style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
          :src="preview"
          alt
        >
        <img
          v-else
          class="img-fluid"
          style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
          src="@/static/images/placeholder.png"
        >
      </div>
      <div class="col-md-4">
        <form @submit.prevent="submitRecipe">
          <div class="form-group">
            <label for>食譜名稱</label>
            <input type="text" class="form-control" v-model="recipe.name">
          </div>
          <div class="form-group">
            <label for>食材</label>
            <input v-model="recipe.ingredients" type="text" class="form-control">
          </div>
          <div class="form-group">
            <label for>圖片</label>
            <input type="file" name="file" @change="onFileChange">
          </div>
          <div class="row">
            <div class="col-md-6">
              <div class="form-group">
                <label for>難度</label>
                <select v-model="recipe.difficulty" class="form-control">
                  <option value="Easy">容易</option>
                  <option value="Medium">中等</option>
                  <option value="Hard">困難</option>
                </select>
              </div>
            </div>
            <div class="col-md-6">
              <div class="form-group">
                <label for>
                  製做時間
                  <small>(分鐘)</small>
                </label>
                <input v-model="recipe.prep_time" type="number" class="form-control">
              </div>
            </div>
          </div>
          <div class="form-group mb-3">
            <label for>製做指南</label>
            <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea>
          </div>
          <button type="submit" class="btn btn-primary">提交</button>
        </form>
      </div>
    </div>
  </main>
</template>

<script>
export default {
  head() {
    return {
      title: "Add Recipe"
    };
  },
  data() {
    return {
      recipe: {
        name: "",
        picture: "",
        ingredients: "",
        difficulty: "",
        prep_time: null,
        prep_guide: ""
      },
      preview: ""
    };
  },
  methods: {
    onFileChange(e) {
      let files = e.target.files || e.dataTransfer.files;
      if (!files.length) {
        return;
      }
      this.recipe.picture = files[0];
      this.createImage(files[0]);
    },
    createImage(file) {
      let reader = new FileReader();
      let vm = this;
      reader.onload = e => {
        vm.preview = e.target.result;
      };
      reader.readAsDataURL(file);
    },
    async submitRecipe() {
      const config = {
        headers: { "content-type": "multipart/form-data" }
      };
      let formData = new FormData();
      for (let data in this.recipe) {
        formData.append(data, this.recipe[data]);
      }
      try {
        let response = await this.$axios.$post("/recipes/", formData, config);
        this.$router.push("/recipes/");
      } catch (e) {
        console.log(e);
      }
    }
  }
};
</script>

<style scoped>
</style>

實現的頁面以下:

一點強迫症:全局頁面跳轉效果

在這一節中,咱們將演示如何在 Nuxt 中添加全局樣式文件,來實現前端頁面之間的跳轉效果。

首先在 assets 目錄中建立 css 目錄,並在其中添加 transition.css 文件,代碼以下:

.page-enter-active,
.page-leave-active {
  transition: opacity .3s ease;
}

.page-enter,
.page-leave-to {
  opacity: 0;
}

在 Nuxt 配置文件中將剛纔寫的 transition.css 中添加到全局 CSS 中:

export default {
  // ...

  /*
  ** Global CSS
  */
  css: [
    '~/assets/css/transition.css',
  ],
  
  // ...
}

歐耶,一個具備完整增刪改查功能、實現了先後端分離的美食分享網站就完成了!

想要學習更多精彩的實戰技術教程?來 圖雀社區逛逛吧。

本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch

相關文章
相關標籤/搜索