0%

Django + Nuxt 实现美食分享网站(二)

从服务器获取数据

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

配置 Django 的静态文件服务

首先我们要配置一下 Django 服务器,使前端能够访问其静态文件。调整 api/api/urls.py 文件如下:

api/api/urls.py
# ...
"""
from django.contrib import admin
from django.urls import path, include
[tuture-add]from django.conf import settings
[tuture-add]from django.conf.urls.static import static

urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('core.urls')),
[tuture-del]]
[tuture-add]] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

注意

这样配置静态文件路由的方式仅应当在开发环境下使用。在生产环境下(settings.py 中的 DEBUG 设为 False 时),静态文件路由将自动失效(因为 Django 并不适合作为静态文件服务器,应该选用类似 Nginx 之类的服务器,在后续教程中我们将更深入地讨论)。

实现前端的数据请求功能

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

client/nuxt.config.js
 // ...
** See https://axios.nuxtjs.org/options
*/
axios: {
[tuture-add] baseURL: 'http://localhost:8000/api',
},
/*
** Build configuration
// ...

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

client/pages/recipes/index.vue
<!-- ... -->
<script>
import RecipeCard from "~/components/RecipeCard.vue";

[tuture-del]const sampleData = [
[tuture-del] {
[tuture-del] id: 1,
[tuture-del] name: "通心粉",
[tuture-del] picture: "/images/food-1.jpeg",
[tuture-del] ingredients: "牛肉, 猪肉, 羊肉",
[tuture-del] difficulty: "easy",
[tuture-del] prep_time: 15,
[tuture-del] prep_guide:
[tuture-del] "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum "
[tuture-del] },
[tuture-del] {
[tuture-del] id: 2,
[tuture-del] name: "羊肉串",
[tuture-del] picture: "/images/food-2.jpeg",
[tuture-del] ingredients: "牛肉, 猪肉, 羊肉",
[tuture-del] difficulty: "easy",
[tuture-del] prep_time: 15,
[tuture-del] prep_guide:
[tuture-del] "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum "
[tuture-del] },
[tuture-del] {
[tuture-del] id: 3,
[tuture-del] name: "炒饭",
[tuture-del] picture: "/images/banner.jpg",
[tuture-del] ingredients: "牛肉, 猪肉, 羊肉",
[tuture-del] difficulty: "easy",
[tuture-del] prep_time: 15,
[tuture-del] prep_guide:
[tuture-del] "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum "
[tuture-del] }
[tuture-del]];
[tuture-del]
export default {
head() {
return {
<!-- ... -->
components: {
RecipeCard
},
[tuture-del] asyncData(context) {
[tuture-del] let data = sampleData;
[tuture-del] return {
[tuture-del] recipes: data
[tuture-del] };
[tuture-add] async asyncData({ $axios, params }) {
[tuture-add] try {
[tuture-add] let recipes = await $axios.$get(`/recipes/`);
[tuture-add] return { recipes };
[tuture-add] } catch (e) {
[tuture-add] return { recipes: [] };
[tuture-add] }
},
data() {
return {
<!-- ... -->
};
},
methods: {
[tuture-del] deleteRecipe(recipe_id) {
[tuture-del] console.log(deleted`${recipe.id}`);
[tuture-add] async deleteRecipe(recipe_id) {
[tuture-add] try {
[tuture-add] if (confirm('确认要删除吗?')) {
[tuture-add] await this.$axios.$delete(`/recipes/${recipe_id}/`);
[tuture-add] let newRecipes = await this.$axios.$get("/recipes/");
[tuture-add] this.recipes = newRecipes;
[tuture-add] }
[tuture-add] } catch (e) {
[tuture-add] console.log(e);
[tuture-add] }
}
}
};
<!-- ... -->

实现食谱详情页面

我们进一步实现食谱详情页面。在 pages/recipes 目录中创建 _id 目录,在其中添加 index.vue 文件,代码如下:

client/pages/recipes/_id/index.vue
[tuture-add]<template>
[tuture-add] <main class="container my-5">
[tuture-add] <div class="row">
[tuture-add] <div class="col-12 text-center my-3">
[tuture-add] <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
[tuture-add] </div>
[tuture-add] <div class="col-md-6 mb-4">
[tuture-add] <img
[tuture-add] class="img-fluid"
[tuture-add] style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
[tuture-add] :src="recipe.picture"
[tuture-add] alt
[tuture-add] >
[tuture-add] </div>
[tuture-add] <div class="col-md-6">
[tuture-add] <div class="recipe-details">
[tuture-add] <h4>食材</h4>
[tuture-add] <p>{{ recipe.ingredients }}</p>
[tuture-add] <h4>准备时间 ⏱</h4>
[tuture-add] <p>{{ recipe.prep_time }} mins</p>
[tuture-add] <h4>制作难度</h4>
[tuture-add] <p>{{ recipe.difficulty }}</p>
[tuture-add] <h4>制作指南</h4>
[tuture-add] <textarea class="form-control" rows="10" v-html="recipe.prep_guide" disabled/>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </main>
[tuture-add]</template>
[tuture-add]
[tuture-add]<script>
[tuture-add]export default {
[tuture-add] head() {
[tuture-add] return {
[tuture-add] title: "食谱详情"
[tuture-add] };
[tuture-add] },
[tuture-add] async asyncData({ $axios, params }) {
[tuture-add] try {
[tuture-add] let recipe = await $axios.$get(`/recipes/${params.id}`);
[tuture-add] return { recipe };
[tuture-add] } catch (e) {
[tuture-add] return { recipe: [] };
[tuture-add] }
[tuture-add] },
[tuture-add] data() {
[tuture-add] return {
[tuture-add] recipe: {
[tuture-add] name: "",
[tuture-add] picture: "",
[tuture-add] ingredients: "",
[tuture-add] difficulty: "",
[tuture-add] prep_time: null,
[tuture-add] prep_guide: ""
[tuture-add] }
[tuture-add] };
[tuture-add] }
[tuture-add]};
[tuture-add]</script>
[tuture-add]
[tuture-add]<style scoped>
[tuture-add]</style>

为了测试前端页面能否真正从后端获取数据,我们先要在后端数据库中添加一些数据,而这对 Django 来说就非常方便了。进入 api 目录,运行 python manage.py runserver 打开服务器,然后进入后台管理页面(http://localhost:8000/admin),添加一些数据:

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

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

有了前面的铺垫,实现食谱的添加和删除也基本上是按部就班了。我们在 pages/recipes/_id 中分别实现 edit.vue(食谱编辑页面) 和 add.vue (创建食谱页面)如下。

client/pages/recipes/_id/edit.vue
[tuture-add]<template>
[tuture-add] <main class="container my-5">
[tuture-add] <div class="row">
[tuture-add] <div class="col-12 text-center my-3">
[tuture-add] <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
[tuture-add] </div>
[tuture-add] <div class="col-md-6 mb-4">
[tuture-add] <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">
[tuture-add] <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview">
[tuture-add] </div>
[tuture-add] <div class="col-md-4">
[tuture-add] <form @submit.prevent="submitRecipe">
[tuture-add] <div class="form-group">
[tuture-add] <label for>Recipe Name</label>
[tuture-add] <input type="text" class="form-control" v-model="recipe.name" >
[tuture-add] </div>
[tuture-add] <div class="form-group">
[tuture-add] <label for>Ingredients</label>
[tuture-add] <input type="text" v-model="recipe.ingredients" class="form-control" name="Ingredients" >
[tuture-add] </div>
[tuture-add] <div class="form-group">
[tuture-add] <label for>Food picture</label>
[tuture-add] <input type="file" @change="onFileChange">
[tuture-add] </div>
[tuture-add] <div class="row">
[tuture-add] <div class="col-md-6">
[tuture-add] <div class="form-group">
[tuture-add] <label for>Difficulty</label>
[tuture-add] <select v-model="recipe.difficulty" class="form-control" >
[tuture-add] <option value="Easy">Easy</option>
[tuture-add] <option value="Medium">Medium</option>
[tuture-add] <option value="Hard">Hard</option>
[tuture-add] </select>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="col-md-6">
[tuture-add] <div class="form-group">
[tuture-add] <label for>
[tuture-add] Prep time
[tuture-add] <small>(minutes)</small>
[tuture-add] </label>
[tuture-add] <input type="text" v-model="recipe.prep_time" class="form-control" name="Ingredients" >
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="form-group mb-3">
[tuture-add] <label for>Preparation guide</label>
[tuture-add] <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea>
[tuture-add] </div>
[tuture-add] <button type="submit" class="btn btn-success">Save</button>
[tuture-add] </form>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </main>
[tuture-add]</template>
[tuture-add]
[tuture-add]<script>
[tuture-add]export default {
[tuture-add] head(){
[tuture-add] return {
[tuture-add] title: "编辑食谱"
[tuture-add] }
[tuture-add] },
[tuture-add] async asyncData({ $axios, params }) {
[tuture-add] try {
[tuture-add] let recipe = await $axios.$get(`/recipes/${params.id}`);
[tuture-add] return { recipe };
[tuture-add] } catch (e) {
[tuture-add] return { recipe: [] };
[tuture-add] }
[tuture-add] },
[tuture-add] data() {
[tuture-add] return {
[tuture-add] recipe: {
[tuture-add] name: "",
[tuture-add] picture: "",
[tuture-add] ingredients: "",
[tuture-add] difficulty: "",
[tuture-add] prep_time: null,
[tuture-add] prep_guide: ""
[tuture-add] },
[tuture-add] preview: ""
[tuture-add] };
[tuture-add] },
[tuture-add] methods: {
[tuture-add] onFileChange(e) {
[tuture-add] let files = e.target.files || e.dataTransfer.files;
[tuture-add] if (!files.length) {
[tuture-add] return;
[tuture-add] }
[tuture-add] this.recipe.picture = files[0]
[tuture-add] this.createImage(files[0]);
[tuture-add] },
[tuture-add] createImage(file) {
[tuture-add] let reader = new FileReader();
[tuture-add] let vm = this;
[tuture-add] reader.onload = e => {
[tuture-add] vm.preview = e.target.result;
[tuture-add] };
[tuture-add] reader.readAsDataURL(file);
[tuture-add] },
[tuture-add] async submitRecipe() {
[tuture-add] let editedRecipe = this.recipe
[tuture-add] if (editedRecipe.picture.indexOf("http://") != -1){
[tuture-add] delete editedRecipe["picture"]
[tuture-add] }
[tuture-add] const config = {
[tuture-add] headers: { "content-type": "multipart/form-data" }
[tuture-add] };
[tuture-add] let formData = new FormData();
[tuture-add] for (let data in editedRecipe) {
[tuture-add] formData.append(data, editedRecipe[data]);
[tuture-add] }
[tuture-add] try {
[tuture-add] let response = await this.$axios.$patch(`/recipes/${editedRecipe.id}/`, formData, config);
[tuture-add] this.$router.push("/recipes/");
[tuture-add] } catch (e) {
[tuture-add] console.log(e);
[tuture-add] }
[tuture-add] }
[tuture-add] }
[tuture-add]};
[tuture-add]</script>
[tuture-add]
[tuture-add]<style>
[tuture-add]</style>

实现之后的页面如下:

client/pages/recipes/add.vue
[tuture-add]<template>
[tuture-add] <main class="container my-5">
[tuture-add] <div class="row">
[tuture-add] <div class="col-12 text-center my-3">
[tuture-add] <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
[tuture-add] </div>
[tuture-add] <div class="col-md-6 mb-4">
[tuture-add] <img
[tuture-add] v-if="preview"
[tuture-add] class="img-fluid"
[tuture-add] style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
[tuture-add] :src="preview"
[tuture-add] alt
[tuture-add] >
[tuture-add] <img
[tuture-add] v-else
[tuture-add] class="img-fluid"
[tuture-add] style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
[tuture-add] src="@/static/images/placeholder.png"
[tuture-add] >
[tuture-add] </div>
[tuture-add] <div class="col-md-4">
[tuture-add] <form @submit.prevent="submitRecipe">
[tuture-add] <div class="form-group">
[tuture-add] <label for>食谱名称</label>
[tuture-add] <input type="text" class="form-control" v-model="recipe.name">
[tuture-add] </div>
[tuture-add] <div class="form-group">
[tuture-add] <label for>食材</label>
[tuture-add] <input v-model="recipe.ingredients" type="text" class="form-control">
[tuture-add] </div>
[tuture-add] <div class="form-group">
[tuture-add] <label for>图片</label>
[tuture-add] <input type="file" name="file" @change="onFileChange">
[tuture-add] </div>
[tuture-add] <div class="row">
[tuture-add] <div class="col-md-6">
[tuture-add] <div class="form-group">
[tuture-add] <label for>难度</label>
[tuture-add] <select v-model="recipe.difficulty" class="form-control">
[tuture-add] <option value="Easy">容易</option>
[tuture-add] <option value="Medium">中等</option>
[tuture-add] <option value="Hard">困难</option>
[tuture-add] </select>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="col-md-6">
[tuture-add] <div class="form-group">
[tuture-add] <label for>
[tuture-add] 制作时间
[tuture-add] <small>(分钟)</small>
[tuture-add] </label>
[tuture-add] <input v-model="recipe.prep_time" type="number" class="form-control">
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="form-group mb-3">
[tuture-add] <label for>制作指南</label>
[tuture-add] <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea>
[tuture-add] </div>
[tuture-add] <button type="submit" class="btn btn-primary">提交</button>
[tuture-add] </form>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </main>
[tuture-add]</template>
[tuture-add]
[tuture-add]<script>
[tuture-add]export default {
[tuture-add] head() {
[tuture-add] return {
[tuture-add] title: "Add Recipe"
[tuture-add] };
[tuture-add] },
[tuture-add] data() {
[tuture-add] return {
[tuture-add] recipe: {
[tuture-add] name: "",
[tuture-add] picture: "",
[tuture-add] ingredients: "",
[tuture-add] difficulty: "",
[tuture-add] prep_time: null,
[tuture-add] prep_guide: ""
[tuture-add] },
[tuture-add] preview: ""
[tuture-add] };
[tuture-add] },
[tuture-add] methods: {
[tuture-add] onFileChange(e) {
[tuture-add] let files = e.target.files || e.dataTransfer.files;
[tuture-add] if (!files.length) {
[tuture-add] return;
[tuture-add] }
[tuture-add] this.recipe.picture = files[0];
[tuture-add] this.createImage(files[0]);
[tuture-add] },
[tuture-add] createImage(file) {
[tuture-add] let reader = new FileReader();
[tuture-add] let vm = this;
[tuture-add] reader.onload = e => {
[tuture-add] vm.preview = e.target.result;
[tuture-add] };
[tuture-add] reader.readAsDataURL(file);
[tuture-add] },
[tuture-add] async submitRecipe() {
[tuture-add] const config = {
[tuture-add] headers: { "content-type": "multipart/form-data" }
[tuture-add] };
[tuture-add] let formData = new FormData();
[tuture-add] for (let data in this.recipe) {
[tuture-add] formData.append(data, this.recipe[data]);
[tuture-add] }
[tuture-add] try {
[tuture-add] let response = await this.$axios.$post("/recipes/", formData, config);
[tuture-add] this.$router.push("/recipes/");
[tuture-add] } catch (e) {
[tuture-add] console.log(e);
[tuture-add] }
[tuture-add] }
[tuture-add] }
[tuture-add]};
[tuture-add]</script>
[tuture-add]
[tuture-add]<style scoped>
[tuture-add]</style>

实现的页面如下:

一点强迫症:全局页面跳转效果

在这一节中,我们将演示如何在 Nuxt 中添加全局样式文件,来实现前端页面之间的跳转效果。

首先在 assets 目录中创建 css 目录,并在其中添加 transition.css 文件,代码如下:

client/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 中:

client/nuxt.config.js
 // ...
** Global CSS
*/
css: [
[tuture-add] '~/assets/css/transition.css',
],
/*
** Plugins to load before mounting the App
// ...

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

图雀社区 微信公众号

扫一扫关注上方公众号,拉学习群和答疑解惑

  • 本文作者: 图雀社区
  • 本文链接: https://tuture.co/2019/10/18/425a3a9/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!