Article directory
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】
【Comment】
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.
/**
* 收藏商品
*/
@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
@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