专栏首页图雀社区全栈“食”代:Django + Nuxt 实现美食分享网站(下)

全栈“食”代:Django + Nuxt 实现美食分享网站(下)

上篇[1]中,我们分别用 Django 和 Nuxt 实现了后端和前端的雏形。在这一部分,我们将实现前后端之间的通信,使得前端可以从后端获取数据,并且将进一步丰富网站的功能。

本文所涉及的源代码都放在了 Github[2] 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点个在看+Github仓库加星❤️哦~ 本文代码改编自 Scotch[3]

从服务器获取数据

在这一部分,我们将真正实现一个全栈应用——让前端能够向后端发起请求,从而获取想要的数据。

配置 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 之类的服务器,在后续教程中我们将更深入地讨论)。

实现前端的数据请求功能

在客户端,我们先要对 Nuxt 进行全局配置。Nuxt 包括 axios[4] 包,这是一个非常出色的基于 Promise 的 HTTP 请求库。在 nuxt.config.js 中的 axios 一项中添加 Django 服务器的 URL:

export default {
  // ...

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

  // ...
}

将食谱列表页面中暂时填充的假数据删去,通过 asyncData 方法获取数据。由于我们之前配置好了 axios,所以 asyncData 函数可以获取到 $axios 对象用于发起 HTTP 请求。我们实现页面加载的数据获取以及 deleteRecipe 事件,代码如下:

<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 文件,代码如下:

<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[5]),添加一些数据:

再运行前端页面,可以看到我们刚刚在 Django 后台管理中添加的项目:

实现食谱的编辑和创建页面

有了前面的铺垫,实现食谱的添加和删除也基本上是按部就班了。我们在 pages/recipes/_id 中实现 edit.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="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',
  ],
  
  // ...
}

欧耶,一个具有完整增删改查功能、实现了前后端分离的美食分享网站就完成了!

想要学习更多精彩的实战技术教程?来图雀社区[6]逛逛吧。

本文所涉及的源代码都放在了 Github[7] 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点个在看+Github仓库加星❤️哦~ 本文代码改编自 Scotch[8]

参考资料

[1]

上篇: https://juejin.im/post/5e435dfc6fb9a07cc3213686

[2]

Github: https://github.com/tuture-dev/recipes_app

[3]

Scotch: https://scotch.io/tutorials/building-a-universal-application-with-nuxtjs-and-django

[4]

axios: https://github.com/axios/axios

[5]

http://localhost:8000/admin: http://localhost:8000/admin

[6]

图雀社区: https://tuture.co/

[7]

Github: https://github.com/tuture-dev/recipes_app

[8]

Scotch: https://scotch.io/tutorials/building-a-universal-application-with-nuxtjs-and-django

● 一杯茶的时间,上手Django框架开发
● 全栈“食”代:用Django+Nuxt实现美食分享网站(一)
● 用Vue+ElementUI搭建后台管理极简模板

·END·

本文分享自微信公众号 - 图雀社区(tuture-dev),作者:一只图雀

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 从一道面试题引发的原理性探究

    key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速。对于简单列表页渲染来说 diff 节点也更快,但会产生一些...

    一只图雀
  • 从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(五)

    欢迎阅读《从零到部署:用 Vue 和 Express 实现迷你全栈电商应用》系列:

    一只图雀
  • 简单代码的秘诀

    成为10倍开发人员有捷径可走吗? 是否有这样一个神奇的秘密,可以帮助我们打开一个全新的软件开发精通和生产力世界?怀疑者们通常会说:“当然没有捷径可走! 每个人都...

    一只图雀
  • 文件上传

    首先创建一个servlet用来获取从前端(form表单或者其它方法)传过来的数据,我这里用到人员信息的提交,使用的是form表单。 前端代码form

    微醺
  • ABP入门系列(5)——展现层实现增删改查

    这一章节将通过完善Controller、View、ViewModel,来实现展现层的增删改查。最终实现效果如下图: ? 一、定义Controller ABP对A...

    圣杰
  • Django之模板层

      在一个项目里面有一个专门放模板的文件夹Templates,有一个专门放视图的文件views,而且我们大多给浏览器响应的都应该是一个完整的页面,也就是读取的是...

    py3study
  • 入门指南:Node/JavaScript中的模板引擎

    在本文中,我们将介绍如何用Node.js和Express来使用 Handlebars 模板引擎。还会介绍什么是模板引擎,以及如何使用把 Handlebars 建...

    前端小智@大迁世界
  • Java单体应用 - 项目实战(后台) - 02.首页布局

    原文地址:http://www.work100.net/training/monolithic-project-iot-cloud-admin-index.ht...

    光束云
  • 九宫格抽奖转盘

    3.将此抽奖网址集成在APP上 因为我们的需求是转盘抽奖用h5写,然后将此网址用APP加载,APP在加载时传入Token,后台在抽奖接口中判断此人积分是否可用...

    honey缘木鱼
  • bootstrap 网页实例 常用样式

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Bootstrap 实例 - 一...

    用户5760343

扫码关注云+社区

领取腾讯云代金券