美食杰----详情页(续写)

本文介绍了如何构建美食杰详情页面,重点讲述了页面的三个部分:头部、内容和评价。在头部,根据用户ID渲染信息并实现收藏功能;在内容部分,详细展示了菜品步骤;在评论区,使用了v-model实现双向数据绑定,并通过点击事件提交评论。

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

今天要讲的是美食杰详情页面,这个页面主要讲述的是菜品制作的详细步骤,其中分为三部分:

头部,内容和评价。

思路:

      根据menuId 请求数据(查看数据结构),渲染页面(如果没有menuld,提示框请登录)

      注意:数据结构过多,在保存到组件中时,提前写好默认值。避免在异步请求返回数据之前找不到属性。

      头部:

      1.根据数据,逐个渲染头部页面。用户信息中,query传入用户id,跳转个人空间。

      2收藏按钮:判断是否本人登录再显示。

      3.收藏功能:判断是否登录,再请求,否则提示框,请先登录。

      内容:

      1.逐个渲染数据中,菜品的步骤:编号使用数组下标+1即可(index) 。

      评论:

       1.在输入框里绑定v-model,实现双向数据绑定。

        2.在提交框里写一个点击事件,在评论完之后提交数据。

下面展示效果图:

头部:

 内容:

 评论:

 展示代码:

detail.vue:

<template>
  <div class="menu-detail">
    <detail-header :info="menuInfo"></detail-header>
    <detail-content :info="menuInfo"></detail-content>
    <Comment :info="menuInfo"></Comment>
  </div>
</template>
<script>
import DetailHeader from "./detail-header";
import DetailContent from "./detail-content";
import Comment from "./comment";
import { menuInfo } from "@/service/api";
export default {
  components: { DetailHeader, DetailContent, Comment },
  data() {
    return {
      menuInfo: {
        userInfo: {},
        raw_material: {
          main_material: [],
          accessories_material: [],
        },
        steps: [],
      },
    };
  },
  watch: {
    $route: {
      async handler() {
        //来判断路由是否有信息
        let { menuId } = this.$route.query;
        if (menuId) {
          menuInfo({ menuId }).then(({ data }) => {
            this.menuInfo = data.info;
          });
        } else {
          this.$message({
            showClose: true,
            message: "请重新进入",
            type: "warning",
          });
        }
      },
      immediate: true,
    },
  },
};
</script>

detail-header.vue:

<template>
  <section class="detail-header">
    <img class="detail-img" :src="info.product_pic_url" />
    <div class="detail-header-right">
      <div class="detail-title clearfix">
        <h1 class="title">{{ info.title }}</h1>
        <!--
            1. 不显示,这个菜谱是当前用户发布的
            2. 显示,后端返回一个是否收藏的字段
          -->
        <div class="detail-collection" v-if="!isOnwer">
          <!-- collection-at  no-collection-at-->
          <a
            href="javascript:;"
            class="collection-at"
            :class="{ 'no-collection-at': info.isCollection }"
            @click="toggleCollection"
          >
            {{ info.isCollection ? "已收藏" : "收藏" }}
          </a>
        </div>
      </div>

      <ul
        class="detail-property clearfix"
        v-for="item in info.properties_show"
        :key="item.parent_type"
      >
        <li>
          <strong>{{ item.parent_name }}</strong>
          <span>{{ item.name }}</span>
        </li>
      </ul>

      <div class="user">
        <router-link
          id="tongji_author_img"
          class="img"
          :to="{ name: 'space', query: { userId: info.userInfo.userId } }"
        >
          <img :src="info.userInfo.avatar" />
        </router-link>
        <div class="info">
          <h4>
            <router-link
              id="tongji_author"
              :to="{ name: 'space', query: { userId: info.userInfo.userId } }"
              >{{ info.userInfo.name }}
            </router-link>
          </h4>
          <span
            >菜谱:{{ info.userInfo.work_menus_len }}/ 关注:{{
              info.userInfo.following_len
            }}
            / 粉丝:{{ info.userInfo.follows_len }}</span
          >
          <strong>{{ info.userInfo.createdAt }}</strong>
        </div>
      </div>
    </div>
  </section>
</template>
<script>
import { toggleCollection } from "@/service/api";

export default {
  props: {
    info: {
      type: Object,
      default: () => ({}),
    },
  },
  computed: {
    isOnwer() {
      return this.info.userInfo.userId === this.$store.state.userInfo.userId;//判断这个作品是否是本人发布的
    },
  },
  data() {
    return {};
  },
  methods: {
    async toggleCollection() {
      //先判断一下是否登录
      if (!this.$store.getters.isLogin) {
        this.$message({
          showClose: true,
          message: "请先登录,再收藏",
          type: "warning",
        });
        return;
      }
      const data = await toggleCollection({ menuId: this.info.menuId });

      this.info.isCollection = data.data.isCollection;
    },
  },
};
</script>

<style lang="stylus">
.detail-header
  margin-top 40px
  display flex
  background-color #fff
  .detail-img
    width 328px
  .detail-header-right
    width 662px

    .detail-title
      box-sizing border-box
      width 100%
      padding-left: 25px;
      border-bottom: 1px solid #eee;
      .title
        max-width: 288px;
        height: 44px;
        padding: 28px 0px;
        line-height: 44px;
        font-size: 36px;
        color: #333;
        float left
      .collected
        background: #999;
      .collecte
        background: #ff3232;
      .detail-collection
        float right
        display block
        height: 50px;
        line-height: 44px;
        color #fff
        padding: 0px 14px;
        text-align center

        margin-top 25px
        cursor pointer
        a
          display: inline-block;
          padding: 3px 0;
          width: 50px;
          color: #fff;
          text-align: center;
          line-height 20px
        .collection-at
          background-color: #ff3232;
        .no-collection-at
          background-color: #999;

    .detail-property
      margin-left 2px
      overflow hidden
      display inline-block
      li
        width 199px
        float left
        height 48px
        border-right 1px solid #eee
        padding 18px 0px 18px 20px
        border-bottom 1px solid #eee
        strong
          color #999
          line-height 18px
          display block
          height 18px
        span
          display block
          font-size 24px
          color #ff3232
          line-height 30px
          width 100px

    .user
      height 70px
      padding 20px 0px 20px 20px
      overflow hidden
      font-size 12px
      a.img
        display block
        height 70px
        width 70px
        float left
        position relative
        border-radius 35px
        overflow hidden
        img
          display block
          height 70px
          width 70px
      .info
        float left
        padding-left 10px
        height 70px
      h4
        height 22px
        line-height 22px
        font-size 14px
        color #ff3232
        position relative
        a
          color #ff3232
          display inline-block
          vertical-align top
          padding-right 0px
          height 22px
      span
        line-height 24px
        display block
        color #666
        padding-top 4px
      strong
        line-height 22px
        color #999
</style>

 detail-content.vue:

<template>
  <section class="detail-content">
    <div class="detail-materials">
      <p class="">
        <strong>“</strong>{{ info.product_story }}<strong>”</strong>
      </p>
      <h2>用料</h2>
      <div
        class="detail-materials-box clearfix"
        v-if="info.raw_material.main_material.length"
      >
        <h3>主料</h3>
        <ul>
          <li
            class=""
            v-for="item in info.raw_material.main_material"
            :key="item._id"
          >
            {{ item.name }}
            <span>{{ item.specs }}</span>
          </li>
        </ul>
      </div>
      <div
        class="detail-materials-box clearfix"
        v-if="info.raw_material.accessories_material.length"
      >
        <h3>辅料</h3>
        <ul>
          <li
            class=""
            v-for="item in info.raw_material.accessories_material"
            :key="item._id"
          >
            {{ item.name }}
            <span>{{ item.specs }}</span>
          </li>
        </ul>
      </div>
    </div>
    <div class="detail-explain">
      <h2>{{ info.title }}的做法</h2>
      <section
        class="detail-section clearfix"
        v-for="(item, index) in info.steps"
        :key="item._id"
      >
        <em class="detail-number">{{ index + 1 }}</em>
        <div class="detail-explain-desc">
          <p>{{ item.describe }}</p>
          <img class="conimg" :src="item.img_url" v-if="item.img_url" alt="" />
        </div>
      </section>
      <div class="skill">
        <h2>烹饪技巧</h2>
        <p>{{ info.skill }}</p>
      </div>
    </div>
  </section>
</template>
<script>
export default {
  name: "DetailContent",
  props: {
    info: {
      type: Object,
      default: () => ({}),
    },
  },
};
</script>
<style lang="stylus">
.detail-content
  margin-top 20px
  h2
    font-size 24px
    color #333
    height 66px
    line-height 66px
    border-bottom 1px solid #eee
    text-indent 24px
    font-family Microsoft Yahei
    display block
  .detail-materials
    background-color #fff
    > p
      line-height 24px
      font-size 14px
      padding 20px 24px 10px
      color #666
    .detail-materials-box
      margin-bottom 20px
      h3
        width 48px
        height 22px
        color #999
        background #f5f5f5
        border 1px solid #ddd
        text-align center
        line-height 24px
        margin 14px 5px 14px 25px
        float left
      ul
        float left
        width 910px
        li
          float left
          box-sizing border-box
          height 54px
          line-height 54px
          margin 0 5px 5px 5px
          padding 0 10px
          border 1px solid #eee
  .detail-explain
    background-color #fff
    padding-bottom 20px
    .detail-section
      .detail-number
        font-size 50px
        color #ff3232
        font-style italic
        text-align center
        font-family arial
        height 50px
        width 100px
        display block
        float left
        line-height 50px
      .detail-explain-desc
        float left
        width 650px
        overflow hidden
        p
          line-height 24px
          color #666
          padding 10px 20px 10px 0px
          position relative
          font-size 14px
        img
          max-width 550px
    .skill
      p
        font-size 12px
        padding-left 100px
        padding-top 10px
</style>

 comment.vue:

<template>
  <div class="comment-box">
    <h2>{{ info.title }}的讨论</h2>
    <div class="comment-text">
      <a href="javascript:;" class="useravatar">
        <img :src="userInfo.avatar" />
      </a>
      <!-- <div v-if="!isLogin">
        请先登录后,再评论<router-link to="">登录</router-link>
      </div> -->

      <div class="comment-right">
        <el-input
          type="textarea"
          :rows="5"
          :cols="50"
          placeholder="请输入内容"
          v-model="commentText"
        >
        </el-input>
        <div class="comment-button">
          <el-button
            class="send-comment"
            type="primary"
            size="medium"
            @click="send"
            >提交</el-button
          >
        </div>
      </div>
    </div>
    <div class="comment-list-box">
      <ul class="comment-list">
        <li v-for="item in comments" :key="item.commentId">
          <router-link
            :to="{ name: 'space', query: { userId: item.userId } }"
            class="avatar"
          >
            <img :src="item.userInfo.avatar" />
            <h5>{{ item.userInfo.name }}</h5>
          </router-link>
          <div class="comment-detail">
            <p class="p1">{{ item.commentText }}</p>
            <div class="info clearfix">
              <span style="float: left;">{{ item.createdAt }}</span>
            </div>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>
<script>
import { getComments, postComment } from "@/service/api";
export default {
  name: "Comment",
  props: {
    info: {
      type: Object,
      default: () => ({}),//获取数据
    },
  },
  data() {
    return {
      comments: [],//创建一个空数组,保存数据
      commentText: "",//初始值为空
    };
  },
  computed: {
    userInfo() {
      return this.$store.state.userInfo;
    },
  },
  async mounted() {
    let { menuId } = this.$route.query;
    if (menuId) {
      let data = await getComments({ menuId: menuId });
      console.log(data);
      this.comments = data.data.comments;
    }
  },
  methods: {
    async send() {
      //console.log('发送');
      let data = postComment({//发送数据到后端
        menuId: this.info.menuId,
        commentText: this.commentText,
      });
      //console.log(data)
      this.comments.unshift(data.data.comments);//在数组开头添加元素
      this.commentText = "";//让评论的内容为空
    },
  },
};
</script>
<style lang="stylus">
.comment-box
  background-color #ffffff
  margin-top 20px
  padding 0 20px
  h2
    font-size 24px
    color #333
    height 66px
    line-height 66px
    border-bottom 1px solid #eee
  .comment-text
    margin-top 20px
    .useravatar
      margin-right 20px
      img
        vertical-align top
        width 36px
        height 36px
    .comment-right
      display inline-block
      width 80%
      .comment-button
        text-align right
        margin-top 10px
        .send-comment
          background #ff3232
          border none
  .comment-list-box
    border-top 1px solid #eee
    margin-top 20px
    padding-top 30px

    ul li
      border-bottom 1px solid #eee
      margin-bottom 20px
      .avatar
        height 82px
        width 50px
        overflow hidden
        display inline-block
        h5
          white-space nowrap
        img
          height 50px
          width 50px
      .comment-detail
        display inline-block
        vertical-align top
        margin-left 20px
        width 80%
        p
          margin 0
          font-size 14px
        .info
          margin-top 10px
          color #999
          .thumbs
            cursor pointer

            font-size 18px
          .thumbs-actve
            color red
</style>

看到这里就结束了,本篇主要讲述的是美食杰---详情页,这个效果大多以渲染数据为主,谢谢大家观看!!!  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值