美食杰-----发布菜谱(完结)

本文详细介绍了美食杰应用中发布菜谱页面的实现过程,包括页面布局、数据请求与渲染、双向绑定、事件处理及提交功能。通过ElementUi组件库进行界面构建,使用Vue实现数据绑定和交互,涉及下拉选项框、输入框、上传图片、删除增加功能及提交菜谱至后台的完整流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇写的是美食杰---发布菜谱页面,这是美食杰最后一篇文章了。

给大家整理一下其余几篇美食杰项目的页面:

美食杰---首页:

美食杰项目---首页_m0_62201566的博客-优快云博客

美食杰---登录页:美食杰项目----登录页面,看了不会来找我!!!_m0_62201566的博客-优快云博客

 美食杰---编辑个人资料:

用vue实现美食杰项目---编辑个人资料,傻瓜式教程,看了就会!!!_m0_62201566的博客-优快云博客

 美食杰---个人空间:

美食杰项目---个人空间_m0_62201566的博客-优快云博客

 美食杰---菜谱大全:

美食杰----菜谱大全_m0_62201566的博客-优快云博客

美食杰----菜谱大全(二)_m0_62201566的博客-优快云博客

美食杰---详情页:

美食杰----详情页(续写)_m0_62201566的博客-优快云博客

美食杰---发布菜谱:

也就是本篇文章了!

下面来介绍一下本篇文章:

这个页面分为三个部分:

介绍作品,记录原材料,写步骤,最后提交菜谱。

思路:

1.先用Element Ui 来进行页面的布局。

2.向后端请求数据,再把数据渲染到页面上。

3.下拉选项框和输入框里绑定v-model事件,实现数据双向绑定事件。

4.在删除和增加按钮上绑定点击事件。

5.最后点击提交时向后端请求数据。

效果展示:

 

 

 代码展示:

create.vue:

<template>
  <div class="create">
    <h2>欢迎发布新菜谱,先介绍一下你的大作!</h2>
    <section class="create-introduce">
      <h5>标题</h5>
      {{ backData.title }}
      <el-input
        class="create-input"
        placeholder="请输入内容"
        v-model="backData.title"
      ></el-input>
      <h5>属性</h5>
      <div>
        <el-select
          v-for="item in properties"
          :key="item.parent_type"
          :placeholder="item.parent_name"
          v-model="backData.property[item.title]"
        >
          <el-option
            v-for="option in item.list"
            :key="option.type"
            :label="option.name"
            :value="option.type"
          >
          </el-option>
        </el-select>
      </div>
      <h5>菜谱分类</h5>
      <div>
        <el-select placeholder="请选择菜谱分类" v-model="backData.classify">
          <el-option-group
            v-for="group in classifies"
            :key="group.parent_type"
            :label="group.parent_name"
          >
            <el-option
              v-for="item in group.list"
              :key="item.type"
              :label="item.name"
              :value="item.type"
            >
            </el-option>
          </el-option-group>
        </el-select>
      </div>
      <h5>成品图 (328*440)</h5>
      <div class="upload-img-box clearfix">
        <div class="upload-img">
          <upload-img
            action="/api/upload?type=product"
            :image-url="backData.product_pic_url"
            @res-url="
              (data) => {
                backData.product_pic_url = data.resImgUrl;
              }
            "
          ></upload-img>
        </div>
        <el-input
          class="introduce-text"
          type="textarea"
          :rows="10"
          placeholder="请输入内容"
          v-model="backData.product_story"
        >
        </el-input>
      </div>
    </section>

    <h2>记录所有原材料</h2>
    <section class="create-introduce">
      <h5>主料</h5>
      <!--[ { "name": "", "specs": "" }, { "name": "", "specs": "" }, { "name": "", "specs": "" } ]-->
      <Stuff v-model="backData.raw_material.main_material"></Stuff>
      <h5>辅料</h5>
      <Stuff v-model="backData.raw_material.accessories_material"></Stuff>
    </section>

    <h2>开始写步骤了!能否简单易学就看你怎么写了,加油!</h2>
    <section class="create-introduce">
      <Upload
        v-for="(item, index) in backData.steps"
        :key="item.customeId"
        :n="index + 1"
        v-model="backData.steps[index]"
        @remove="removeStep"
      ></Upload>
      <el-button
        class="eaeaea add-step-button"
        type="primary"
        size="medium"
        icon="el-icon-plus"
        @click="addStep"
        >增加一步</el-button
      >
      <h5>烹饪小技巧</h5>
      <el-input
        class="introduce-text"
        type="textarea"
        :rows="8"
        placeholder="分享下你做这道菜的过程中的心得和小技巧吧!"
        v-model="backData.skill"
      >
      </el-input>
    </section>

    <el-button
      class="send"
      type="primary"
      size="medium"
      :icon="icon"
      @click="send"
      >搞定,提交审核</el-button
    >
  </div>
</template>
<script>
import Stuff from "./stuff";
import Upload from "./step-upload";
import UploadImg from "@/components/upload-img";
import { getProperty, getClassify, publish } from "@/service/api";
// 向后端发送的数据结构
const backData = {
  title: "", // 标题
  product_pic_url: "", // 成品图URL
  product_story: "", // 成品图故事
  property: {
    craft: 0, // 工艺 enum: [1,2,3,4],
    flavor: 0, // 口味  enum: [1,2,3,4],
    hard: 0, // 难度 enum: [1,2,3,4],
    pepole: 0, // pepole 人数: [1,2,3,4],
  }, // 属性
  raw_material: {
    // 料
    main_material: [{ name: "", specs: "" }], // 主料
    accessories_material: [{ name: "", specs: "" }], // 辅料
  },
  steps: [{ img_url: "", describe: "" }], // 步骤
  classify: "", // 菜谱分类
  skill: "",
};
// 用料的数据结构
const raw_material_struct = {
  name: "",
  specs: "",
};

// 步骤中, 每一步的数据结构
const steps_struct = { img_url: "", describe: "" };

// 步骤中,存储序号
let n = 1;

//用于提交的测试数据, 避免每次测试时在页面逐个选中
const mockData = {
  title: "测试数据123",
  property: {
    craft: "1-2",
    flavor: "2-1",
    hard: "3-1",
    people: "4-1",
  },
  classify: "1-1",
  product_pic_url:
    "http://127.0.0.1:7001/static/upload/product/328X4401565628820747.jpg",
  product_story: "1",
  raw_material: {
    main_material: [
      {
        name: "1",
        specs: "1",
      },
      {
        name: "2",
        specs: "2",
      },
      {
        name: "3",
        specs: "3",
      },
    ],
    accessories_material: [
      {
        name: "1",
        specs: "3",
      },
      {
        name: "2",
        specs: "4",
      },
      {
        name: "4",
        specs: "4",
      },
    ],
  },
  steps: [
    {
      img_url:
        "http://127.0.0.1:7001/static/upload/step/210X210X21565628835530.jpg",
      describe: "1",
    },
    {
      img_url:
        "http://127.0.0.1:7001/static/upload/step/210X210X21565628839458.jpg",
      describe: "",
    },
    {
      img_url:
        "http://127.0.0.1:7001/static/upload/step/210X2101565628842198.jpg",
      describe: "3",
    },
  ],
  skill: "心得",
};

export default {
  name: "create",
  components: { Stuff, Upload, UploadImg },
  data() {
    return {
      properties: [], // 页面展示的数据
      classifies: [],
      icon: "",
      backData: {
        title: "",
        property: {
          // craft: '',
          // flavor: ''
        },
        classify: "",
        product_pic_url:
          "https://s1.c.meishij.net/n/images/upload_big_img.png?_=1561906961",
        product_story: "",
        raw_material: {
          // 料
          main_material: Array(3)
            .fill(1)
            .map(() => ({ ...raw_material_struct })), // 主料  //
          accessories_material: Array(3)
            .fill(1)
            .map(() => ({ ...raw_material_struct })), // 辅料
        },
        steps: Array(3)
          .fill(1)
          .map(() => ({ ...steps_struct, customeId: this.uuid() })),
        skill: "",
      },
    };
  },
  mounted() {
    getProperty().then(({ data }) => {
      this.properties = data;
      this.backData.property = data.reduce((o, item) => {
        o[item.title] = "";
        return o;
      }, {});
    });
    getClassify().then(({ data }) => {
      console.log(data);
      this.classifies = data;
    });
  },
  methods: {
    uuid() {
      n++;
      return Date.now() + n;
    },
    addStep() {
      this.backData.steps.push({
        ...steps_struct,
        customeId: this.uuid(),
      });
    },
    removeStep(index) {
      this.backData.steps.splice(index - 1, 1);
    },
    send() {
      this.icon = "el-icon-loading";
      let param = this.backData;
      // 验证
      // 删除字段 删除字段后,当前页面需要用到这个字段的地方可能会有问题
      // 提取出需要的字段
      param.steps = param.steps.map((item) => {
        return {
          img_url: item.img_url,
          describe: item.describe,
        };
      });
      // 1. 测试过程中不跳转,手动去打开指定的跳转的页面去看数据对不对
      // 2. mock数据,模拟一套数据,预先准备一套

      //console.log(JSON.stringify(param, null, 2)); 这个2 看懵了吧? 这是转字符串输出, 2个间距,有空多看看基础吧
      param = mockData;

      publish(param).then((data) => {
        console.log(data);
        this.$router.push({
          name: "space",
        });
      });
    },
  },
};
</script>
<style lang="stylus">

.create-introduce
  background-color #fff
  padding 20px

  .add-step-button
    margin-left 100px

.create
  width 100%
  h2
    text-align center
    margin 20px 0
  .send
    // ff3232()
    height: 70px;
    width: 220px;
    background #ff3232
    color #fff
    border none
    margin 20px auto
    display block


  h5
    margin 20px 0

.create-input input
  width 446px
  line-height 22px
.upload-img-box
  .upload-img
    float left
  .introduce-text
    float left
  .el-textarea
    width 60%
    margin-left 10px
</style>

stuff.vue:

<template>
  <div class="stuff">
    <div class="clearfix">
      <div class="raw-item" v-for="(item, index) in value" :key="index">
        <el-input
          v-model="item.name"
          placeholder="请输入内容"
          style="width: 200px"
        ></el-input>
        <el-input
          v-model="item.specs"
          placeholder="请输入内容"
          style="width: 100px"
        ></el-input>
        <i
          class="delete-icon el-icon-close"
          v-show="value.length !== 1"
          @click="remove(index)"
        ></i>
      </div>
    </div>
    <el-button
      class="eaeaea"
      type="primary"
      size="medium"
      icon="el-icon-plus"
      @click="add"
      >增加一项</el-button
    >
  </div>
</template>
<script>
// 组件通信使用v-model会监听 在组件上面双向绑定 value 发布事件input
export default {
  props: {
    value: {
      type: Array,
      default: () => [],
    },
  },
  methods: {
    add() {
      this.$emit("input", [...this.value, { name: "", specs: "" }]);
    },
    remove(index) {
      // this.value.splice(index,1);
      const newValue = this.value.filter((item, i) => {
        return i !== index;
      });
      this.$emit("input", newValue);
    },
  },
};
</script>

<style lang="stylus">
.delete-icon
  background-color #ccc
  border-radius 50%
  color #fff
  :hover
    background: #ff3232;
    color: #fff;
.raw-item
  float left
  margin-right 65px
  margin-bottom 20px
  .el-input
    margin-right 5px
</style>

 step-upload.vue:

<template>
  <div class="step clearfix">
    <div class="step-number">{{ n }}.</div>
    <div class="upload-box">
      <upload-img
        action="/api/upload?type=step"
        :image-url="$options.imageUrl"
        :img-max-width="184"
        @res-url="changeUrl"
      ></upload-img>
    </div>
    <el-input
      class="introduce-text"
      type="textarea"
      :rows="8"
      placeholder="请输入内容"
      v-model="value.describe"
    >
    </el-input>
    <i class="delete-icon el-icon-close"></i>
  </div> 
</template>
<script>
import UploadImg from "@/components/upload-img";
export default {
  components: { UploadImg },
  imageUrl: "https://s1.c.meishij.net/n/images/upload_step_img.png",
  props: {
    n: {
      type: Number,
      default: 1,
    },
    length: {
      type: Number,
      default: 1,
    },
    value: {
      type: Object,
      default: () => ({}),
    },
  },
  methods: {
    changeUrl(data) {
      this.$emit("input", {
        ...this.value,
        img_url: data.resImgUrl,
      });
    },
    remove() {
      this.$emit("remove", this.n);
    },
  },
};
</script>

<style lang="stylus">
.step
  margin-bottom 20px
  > div
    float left
  .step-number
    height 180px
    width 100px
    font-size 60px
    color #aaa
    font-family Arial, Helvetica, sans-serif
    font-weight bold
    vertical-align top
    line-height 180px
  .introduce-text
    width 60%
    margin-left 40px
  .upload-box
    img
      vertical-align top
</style>

 到这里,美食杰---发布菜谱页面就完成了,欢迎大家观看!!!

美食杰整个项目到这里也就完结了,有需要的快冲!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值