[Easy Mini Program Project] Product details display + comments, comment display, comment likes + product collection [the backend is developed based on Ruoyi management system]

Interface effect

【illustrate】

  • The pictures of the products in the interface are from Xianyu. If there is any infringement, please contact us to delete them.

【product details】
Insert image description here

【Comment】
Insert image description here

Interface implementation

Tool js

The function of this tool class is to calculate the aspect ratio of the image given the url address of an image. The purpose of calculating the aspect ratio is to allow the image to be displayed in normal proportions.

/**
 * 获取uuid
 */
export default {
    
    
	/**
	 * 获取高宽比 乘以 100%
	 */
	getAspectRatio(url) {
    
    
		uni.getImageInfo({
    
    
			src: url,
			success: function(res) {
    
    
				let aspectRatio = res.height * 100.0 / res.width;
				// console.log("aspectRatio:" + aspectRatio);
				return aspectRatio + "%";
			}
		});
	},
}
export default {
    
    
	/**
	 * 日期格式化
	 */
	formatDateToString(date) {
    
    
	 return new Date(date).toLocaleString();
	},
}

page

<template>
	<view class="container">
		<u-toast ref="uToast"></u-toast>
		<view class="userItem">
			<view class="userProfile">
				<u--image :src="productVo.avatar" width="35" height="35" shape="circle"></u--image>
				<view style="width: 10px;"></view>
				<view>
					<view class="nickname">{
   
   {productVo.nickname}}</view>
					<view class="other">10分钟前来过 广东工业大学大学城校区</view>
				</view>
			</view>
			<view class="follow" @click="follow" v-if="hadFollow==false">
				<view>
					<u-icon name="plus" color="#ffffff" style="font-weight: bold;" size="15"></u-icon>
				</view>
				<view style="margin-left: 10rpx;font-size: 15px;">
					关 注
				</view>
			</view>
			<view class="followed" @click="cancelFollow" v-else>
				<view style="font-size: 15px;color: #C2C2C2;">
					已 关 注
				</view>
			</view>
		</view>
		<view class="productItem">
			<view class="top">
				<view class="price">¥<text class="number">{
   
   {productVo.price}}</text>/{
   
   {productVo.unit}}</view>
				<view class="browseInformation">
					{
   
   {product.starNum}}人想要 | {
   
   {product.readNum}}个浏览
				</view>
			</view>
			<view class="productDetail">
				{
   
   {productVo.description}}
			</view>
			<u--image :showLoading="true" v-for="(pic,index) in productVo.picList" :src="pic" width="100%"
				:height="getAspectRatio(pic)" radius="10" mode="widthFix"></u--image>
		</view>

		<view class="commentView">
			<view style="color: #3D3D3D;">
				{
   
   {commentNum}}条评论
			</view>
			<view v-for="(commentItem,index) in commentVoList">
				<view class="commentItem">
					<view style="display: flex;">
						<u--image :src="commentItem.userAvatar" width="30" height="30" shape="circle"></u--image>
						<view style="width: 10px;"></view>
						<view @click="clickShowBottomPopup(1, commentItem.id,commentItem.userNickName)">
							<view class="nickname">{
   
   {commentItem.userNickName}}</view>
							<view class="content">
								{
   
   {commentItem.content}}
							</view>
							<view class="dateAndPosition">{
   
   {formatDateToString(commentItem.createTime)}}</view>
						</view>
					</view>
					<view style="display: inline-block;text-align: center;">
						<u-icon name="thumb-up" size="28" @click="likeComment(commentItem.id,commentItem)"
							v-if="commentItem.isLike==0"></u-icon>
						<u-icon name="thumb-up-fill" color="#2B92FF" size="28"
							@click="cancelLikeComment(commentItem.id,commentItem)" v-else></u-icon>
						<view style="font-size: 12px;color: #B9B9B9;">
							{
   
   {commentItem.likeNum}}
						</view>
					</view>

				</view>
				<view class="sonCommentItem" v-for="(commentItem1,index1) in commentItem.children">
					<view style="display: flex;">
						<u--image :src="commentItem1.userAvatar" width="30" height="30" shape="circle"></u--image>
						<view style="width: 10px;"></view>
						<view @click="clickShowBottomPopup(1, commentItem1.id,commentItem1.userNickName)">
							<view class="nickname">{
   
   {commentItem1.userNickName}}</view>
							<view class="content">
								<text style="font-size: 14px;">
									回复了<text style="color:#B9B9B9 ;">{
   
   {commentItem1.toUserNickName}}</text></text>
								<text>
									{
   
   { commentItem1.content }}
								</text>
							</view>
							<view class="dateAndPosition">{
   
   {formatDateToString(commentItem1.createTime)}}</view>
						</view>
					</view>
					<view style="display: inline-block;text-align: center;">
						<u-icon name="thumb-up" size="28" @click="likeComment(commentItem1.id,commentItem1)"
							v-if="commentItem1.isLike==0"></u-icon>
						<u-icon name="thumb-up-fill" color="#2B92FF" size="28"
							@click="cancelLikeComment(commentItem1.id, commentItem1)" v-else></u-icon>
						<view style="font-size: 12px;color: #B9B9B9;">
							{
   
   {commentItem1.likeNum}}
						</view>
					</view>
				</view>

			</view>

		</view>

		<view class="footer">
			<view>
				<view class="item" @click="clickShowBottomPopup(0, productVo.id,)">
					<u-icon name="chat" size="28"></u-icon>
					<view class="comment">评论</view>
				</view>
				<view class="item" @click="starProduct()" v-if="hadStar==false">
					<u-icon name="star" size="28"></u-icon>
					<view class="comment">我想要</view>
				</view>
				<view class="item" @click="cancelStar()" v-if="hadStar==true">
					<u-icon name="star-fill" color="#2B92FF" size="28"></u-icon>
					<view class="comment" style="color: #2B92FF">已收藏</view>
				</view>
			</view>

			<view class="chat">
				<u-icon name="chat" color="#ffffff" size="18"></u-icon>
				<view style="width: 5px;"></view>
				私 聊
			</view>
		</view>

		<!-- 底部弹出框:用于输入评论 -->
		<!-- @close="this.showBottomPopup=false" 点击遮罩层关闭弹框  -->
		<u-popup :show="showBottomPopup" mode="bottom" :round="10" @close="this.showBottomPopup=false">
			<view class="commentPopup">
				<u--textarea v-model="comment.content" :placeholder="commentPlaceHolder" autoHeight height="200"
					border="surround"></u--textarea>
				<view class="commentButton" @click="commitComment()">
					<u-icon name="chat" color="#ffffff" size="18"></u-icon>
					<view style="width: 5px;"></view>
					评 论
				</view>
			</view>
		</u-popup>

	</view>
</template>

<script>
	import pictureApi from "@/utils/picture.js";
	import {
      
      
		addFollow,
		hadFollowSomeone,
		cancelFollowSomeone
	} from "@/api/market/follow.js";
	import {
      
      
		starProduct,
		cancelStar,
		hadStar
	} from "@/api/market/star.js";
	import {
      
      
		addComment,
		listCommentVoOfProduct
	} from "@/api/market/comment.js";
	import dateUtil from "@/utils/date.js";
	import {
      
      
		likeComment,
		cancelLikeComment
	} from "@/api/market/commentLike.js"
	import {
      
      
		getProduct
	} from "@/api/market/prodct.js"

	export default {
      
      
		data() {
      
      
			return {
      
      
				productVo: {
      
      },
				product: {
      
      },
				// 是否已经关注商品主人
				hadFollow: false,
				// 是否已经收藏商品
				hadStar: false,
				// 是否显示底部弹出框
				showBottomPopup: false,
				// 评论
				comment: {
      
      
					itemId: undefined,
					type: undefined,
					content: '',
					isTop: 0
				},
				// 存储商品对应的评论集合
				commentVoList: [],
				// 评论数量
				commentNum: undefined,
				commentPlaceHolder: "",
			}
		},
		methods: {
      
      
			/**
			 * 获取高宽比 乘以 100%
			 */
			getAspectRatio(url) {
      
      
				// uni.getImageInfo({
      
      
				// 	src: url,
				// 	success: function(res) {
      
      
				// 		let aspectRatio = res.height * 100.0 / res.width;
				// 		// console.log("aspectRatio:" + aspectRatio);
				// 		return aspectRatio + "%";
				// 	}
				// });
				return pictureApi.getAspectRatio(url);
			},
			/**
			 * 关注用户
			 */
			follow() {
      
      
				let data = {
      
      
					followedId: this.productVo.userId
				}
				addFollow(data).then(res => {
      
      
					this.hadFollow = true;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "关注成功",
						duration: 300
					})
				}).catch(err => {
      
      
					this.$refs.uToast.show({
      
      
						type: 'error',
						message: err.msg,
						duration: 300
					})
				})
			},
			/**
			 * 取消关注
			 */
			cancelFollow() {
      
      
				cancelFollowSomeone(this.productVo.userId).then(res => {
      
      
					this.hadFollow = false;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "取消关注成功",
						duration: 300
					})
				})
			},
			/**
			 * 查询是否已经关注了用户
			 */
			searchWhetherFollow() {
      
      
				hadFollowSomeone(this.productVo.userId).then(res => {
      
      
					// console.log("res:" + JSON.stringify(res));
					this.hadFollow = res.hadFollow;
					// console.log("this.hadFollow :" + this.hadFollow);
				})
			},
			/**
			 * 收藏商品
			 */
			starProduct() {
      
      
				starProduct(this.productVo.id).then(res => {
      
      
					this.hadStar = true;
					this.getProduct();
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "收藏成功",
						duration: 300
					})
				})
			},
			/**
			 * 取消收藏
			 */
			cancelStar() {
      
      
				cancelStar(this.productVo.id).then(res => {
      
      
					this.hadStar = false;
					this.getProduct();
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "取消收藏成功",
						duration: 300
					})
				})
			},
			/**
			 * 点赞评论
			 */
			likeComment(commentId, comment) {
      
      
				// console.log("comment:" + JSON.stringify(comment))
				likeComment(commentId).then(res => {
      
      
					comment.isLike = 1;
					comment.likeNum += 1;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "点赞成功",
						duration: 300
					})
				})
			},
			/**
			 * 取消点赞评论
			 */
			cancelLikeComment(commentId, comment) {
      
      
				cancelLikeComment(commentId).then(res => {
      
      
					comment.isLike = 0;
					comment.likeNum -= 1;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "取消点赞成功",
						duration: 300
					})
				})
			},
			/**
			 * 查询是否已经关注了用户
			 */
			searchWhetherStar() {
      
      
				hadStar(this.productVo.id).then(res => {
      
      
					// console.log("res:" + JSON.stringify(res));
					this.hadStar = res.hadStar;
					// console.log("this.hadFollow :" + this.hadFollow);
				})
			},
			/**
			 * 显示底部弹出框
			 */
			clickShowBottomPopup(type, itemId, username = undefined) {
      
      
				this.showBottomPopup = true;
				this.comment.type = type;
				this.comment.itemId = itemId;
				if (type == 0) {
      
      
					this.commentPlaceHolder = "想要了解更多信息,可以评论让商品主人看见哟";
				} else {
      
      
					this.commentPlaceHolder = "正在回复" + username + "";
				}
			},
			/**
			 * 发表评论
			 */
			commitComment() {
      
      
				// console.log("发送评论,comment:" + JSON.stringify(this.comment))
				addComment(this.comment).then(res => {
      
      
					this.showBottomPopup = false;
					this.comment.content = '';
					this.listCommentVoOfProduct();
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "评论发送成功",
						duration: 300
					})
				})
			},
			/**
			 * 获取商品对应的所有评论
			 */
			listCommentVoOfProduct() {
      
      
				listCommentVoOfProduct(this.productVo.id).then(res => {
      
      
					// console.log("listCommentVoOfProduct:" + JSON.stringify(res));
					this.commentVoList = res.tree;
					this.commentNum = res.commentNum;
				})
			},
			/**
			 * 格式化日期
			 * @param {Object} date
			 */
			formatDateToString(dateStr) {
      
      
				let date = new Date(dateStr);
				// 月份需要加一
				return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
			},
			/**
			 * 获取商品详细信息,同时增加阅读量
			 */
			getProduct() {
      
      
				getProduct(this.productVo.id).then(res => {
      
      
					console.log("product:" + JSON.stringify(res.data));
					this.product = res.data;
				})
			}
		},
		onLoad(e) {
      
      
			this.productVo = JSON.parse(decodeURIComponent(e.productVo));
			this.searchWhetherFollow();
			this.searchWhetherStar();
			this.listCommentVoOfProduct();
			this.getProduct();
			// console.log("productVo:" + JSON.stringify(productVo));
		}
	}
</script>

<style lang="scss">
	.container {
      
      
		// padding: 20rpx;
		background: #F7F7F7;

		.userItem {
      
      
			display: flex;
			align-items: center;
			justify-content: space-between;
			background: #ffffff;
			padding: 20rpx;

			.userProfile {
      
      
				display: flex;

				.nickname {
      
      
					color: #202020;
					font-weight: bold;
					font-size: 14px;
				}

				.other {
      
      
					color: #A6A4A5;
					font-size: 11px;
				}
			}

			.follow {
      
      
				display: flex;
				align-items: center;
				font-weight: bold;
				color: #ffffff;
				background: #2B92FF;
				border-radius: 20px;
				padding: 4px 8px;
			}

			.followed {
      
      
				background: #F6F6F6;
				border-radius: 20px;
				padding: 4px 8px;
			}
		}

		.productItem {
      
      
			background: #ffffff;
			padding: 20rpx;

			.top {
      
      
				display: flex;
				align-items: center;
				justify-content: space-between;

				.price {
      
      
					color: #F84442;
					font-weight: bold;

					.number {
      
      
						font-size: 30px;
					}
				}

				.browseInformation {
      
      
					color: #A6A4A5;
					font-size: 14px;
				}
			}

			.productDetail {
      
      
				margin-top: 20rpx;
				margin-bottom: 10rpx;
				color: #4C4C4C;
				font-size: 15px;
				line-height: 30px;
				font-weight: bold;
			}
		}

		.commentView {
      
      
			margin-top: 10px;
			// 用来预留展示 footer 的高度,不然footer会挡住评论
			margin-bottom: calc(60px + 10rpx);
			background: #ffffff;
			padding: 30rpx 30rpx;

			.nickname {
      
      
				font-size: 14px;
				color: #B9B9B9;
			}

			.content {
      
      
				margin: 5px;
				// 解决英文字符串、数字不换行的问题
				word-break: break-all;
				word-wrap: break-word;
			}

			.dateAndPosition {
      
      
				font-size: 11px;
				color: #B9B9B9;
			}

			.commentItem {
      
      
				display: flex;
				margin: 10px;
				justify-content: space-between;
			}

			.sonCommentItem {
      
      
				display: flex;
				margin: 10px 10px 10px 50px;
				justify-content: space-between;
			}

		}

		.footer {
      
      
			padding: 20rpx;
			position: fixed;
			// right: 20rpx;
			bottom: 0rpx;
			background: #ffffff;
			height: 60px;
			width: 710rpx;
			padding-top: 2px;

			display: flex;
			align-items: center;
			justify-content: space-between;

			.item {
      
      
				display: inline-block;
				text-align: center;
				margin-right: 10px;

				.comment {
      
      
					font-size: 10px;
				}
			}

			.chat {
      
      
				display: flex;
				align-items: center;
				background-color: #2B92FF;
				border-radius: 20px;
				padding: 7px;
				color: #ffffff;
				// margin-right: 20px;
				font-size: 12px;
			}

		}

		.commentPopup {
      
      
			display: flex;
			padding: 10px;
			min-height: 200rpx;

			.commentButton {
      
      
				background-color: #2B92FF;
				border-radius: 5px;
				padding: 7px;
				color: #ffffff;
				font-size: 12px;
				height: 20px;
				display: flex;
				align-items: center;
			}
		}


	}
</style>

date formatting

Sometimes the date format passed from the backend is not very beautiful or concise to display directly on the front-end page. Then you can write a date formatting method yourself to convert the date into the format we need for display.

/**
 * 格式化日期
 * @param {Object} date
 */
formatDateToString(dateStr) {
    
    
	let date = new Date(dateStr);
	// 月份需要加一
	return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
},

English automatic line wrapping display

.content {
    
    
	margin: 5px;
	// 解决英文字符串、数字不换行的问题
	word-break: break-all;
	word-wrap: break-word;
}

rear end

collect

Controller

In order to facilitate the query of product data, I added a redundant field of collection number to the product table when designing the database. Therefore, every time a product is collected or canceled, the collection number of the product table needs to be updated.

Insert image description here

/**
 * 收藏商品
 */
@PreAuthorize("@ss.hasPermi('market:star:star')")
@GetMapping("/starProduct/{productId}")
public AjaxResult starProduct(@PathVariable("productId") Long productId) {
    
    
    Star star = new Star();
    star.setUserId(getLoginUser().getUserId());
    star.setProductId(productId);
    boolean isStar = starService.addStar(star);
    if (isStar){
    
    
        // 需要将商品的收藏量+1
        productService.starNumPlusOne(productId);
    }
    return AjaxResult.success();
}

Service

package com.shm.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.domain.entity.Star;
import com.shm.mapper.StarMapper;
import com.shm.service.IStarService;
import org.springframework.stereotype.Service;

/**
* @author dam
* @description 针对表【collection(收藏表)】的数据库操作Service实现
* @createDate 2023-08-09 19:41:23
*/
@Service
public class IStarServiceImpl extends ServiceImpl<StarMapper, Star>
    implements IStarService {
    
    

    @Override
    public boolean addStar(Star star) {
    
    
        return baseMapper.addStar(star);
    }
}

mapper

public interface StarMapper extends BaseMapper<Star> {
    
    
    boolean addStar(@Param("star") Star star);
}

When adding items to collections, you need to first determine that the same collection data does not exist in the database before performing the insertion operation. Otherwise, if the user's network is stuck and the user sends collection requests multiple times, redundant dirty data will appear in the database.

<insert id="addStar">
    INSERT INTO `star` (`user_id`, `product_id`)
    SELECT #{star.userId},#{star.productId} FROM DUAL
    WHERE NOT EXISTS (
            SELECT 1 FROM `star`
            WHERE `user_id` = #{star.productId} AND `product_id` = #{star.productId} limit 1
        );
</insert>

Comment

Controller

/**
 * 获取商品对应的所有评论
 *
 * @param productId
 * @return
 */
@PreAuthorize("@ss.hasPermi('market:comment:list')")
@GetMapping("/listCommentVoOfProduct/{productId}")
public AjaxResult listCommentVoOfProduct(@PathVariable("productId") Long productId) {
    
    
    // 查询出商品对应的所有评论数据
    List<CommentVo> commentVoList = commentService.listCommentVoOfProduct(productId, getLoginUser().getUserId());
    int commentNum = commentVoList.size();
    // 将评论数据封装成树形结构
    List<CommentVo> tree = commentService.buildTree(commentVoList);
    return AjaxResult.success().put("tree", tree).put("commentNum", commentNum);
}

Service

It should be noted that the tree structure here only has two layers of data (one layer for product reviews and one layer for all comments), because it is inconvenient for the mini program to display too many layers of data, otherwise the width will be very large. Users need to swipe repeatedly to view the full review
Insert image description here

Insert image description here

@Override
public List<CommentVo> listCommentVoOfProduct(Long productId, Long userId) {
    
    
    return commentMapper.listCommentVoOfProduct(productId, userId);
}

/**
 * 将评论数据封装成树形结构
 *
 * @param commentVoList
 * @return
 */
@Override
public List<CommentVo> buildTree(List<CommentVo> commentVoList) {
    
    
    // 将所有父级评论过滤出来
    List<CommentVo> fatherList = commentVoList.stream().filter((item) -> {
    
    
        return item.getType() == 0;
    }).collect(Collectors.toList());
    commentVoList.removeAll(fatherList);
    // 为所有父级评论寻找孩子
    for (CommentVo father : fatherList) {
    
    
        father.setChildren(new ArrayList<>());
        this.searchSon(father.getId(), father.getUserNickName(), father.getChildren(), commentVoList);
    }
    return fatherList;
}

/**
 * 寻找孩子
 *
 * @param fatherId
 * @param children
 * @param commentVoList
 */
private void searchSon(Long fatherId, String fatherNickName, List<CommentVo> children, List<CommentVo> commentVoList) {
    
    
    for (CommentVo commentVo : commentVoList) {
    
    
        if (commentVo.getItemId().equals(fatherId)) {
    
    
            commentVo.setToUserNickName(fatherNickName);
            children.add(commentVo);
            this.searchSon(commentVo.getId(), commentVo.getUserNickName(), children, commentVoList);
        }
    }
}

Mapper

This sql is very complex. It can find out the nickname of the comment owner, avatar, and the number of likes of the comment at once. It also uses recursive query to continuously query the sub-comments of the comment. I can't guarantee the efficiency of this SQL at present. I just implemented the function. If the performance is insufficient later, I will find a way to optimize it.

<select id="listCommentVoOfProduct" resultType="com.ruoyi.common.core.domain.vo.CommentVo">
      SELECT
          ct.id,
          ct.user_id,
          ct.item_id,
          ct.type,
          ct.content,
          ct.create_time,
          u.nick_name AS userNickName,
          u.avatar AS userAvatar,
          CASE
              WHEN cl.user_id IS NULL THEN
                  0 ELSE 1
              END AS isLike,
          ct.LEVEL,
          COALESCE ( likeNum, 0 ) AS likeNum
      FROM
          (
              WITH RECURSIVE comment_tree AS (
                  SELECT
                      id,
                      user_id,
                      item_id,
                      type,
                      content,
                      create_time,
                      0 AS LEVEL
                  FROM
                      COMMENT
                  WHERE
                      item_id = #{productId} and type=0
                  UNION ALL
                  SELECT
                      c.id,
                      c.user_id,
                      c.item_id,
                      c.type,
                      c.content,
                      c.create_time,
                      ct.LEVEL + 1 AS LEVEL
                  FROM
                      COMMENT c
                          INNER JOIN comment_tree ct ON c.item_id = ct.id
                  WHERE
                      c.type = 1
              ) SELECT
                  *
              FROM
                  comment_tree
          ) ct
              LEFT JOIN ( SELECT comment_id, COUNT(*) AS likeNum FROM comment_like WHERE is_deleted = 0 GROUP BY comment_id ) pc ON ct.id = pc.comment_id
              LEFT JOIN sys_user AS u ON ct.user_id = u.user_id
              LEFT JOIN comment_like cl ON ct.id = cl.comment_id
              AND cl.user_id = #{userId} and cl.is_deleted =0
  </select>

merchandise

Controller

/**
 * 获取商品详细信息
 */
@PreAuthorize("@ss.hasPermi('market:product:query')")
@GetMapping(value = "/{id}")
@Transactional // 同时处理多个表,添加事务
public AjaxResult getInfo(@PathVariable("id") Long id) {
    
    
    // 首先判断用户有没有阅读该商品
    boolean isAdd = productReadService.addRead(new ProductRead(getLoginUser().getUserId(), id));
    if (isAdd) {
    
    
        // 需要将商品的阅读量+1
        productService.readNumPlusOne(id);
    }
    return success(productService.getById(id));
}

read

Service

<insert id="addRead">
    INSERT INTO `product_read` (`user_id`, `product_id`)
    SELECT #{productRead.userId},#{productRead.productId} FROM DUAL
    WHERE NOT EXISTS (
            SELECT 1 FROM `product_read`
            WHERE `user_id` = #{productRead.userId} AND `product_id` = #{productRead.productId} limit 1
        );

</insert>

Other articles on the same project

For other articles on this project, please check [Easy Mini Program Project] project introduction, mini program page display and series of articles collection

Guess you like

Origin blog.csdn.net/laodanqiu/article/details/132343876