最近沉迷学习无法自拔,太久没有码字,码一个小程序留言功能实现。先上一波最后效果图:
(删除按钮,是用户自己的留言时才会显示该按钮)
实现技术
后台:SSM框架
数据库:MySQL数据库
数据库设计
评论功能的实现主要涉及三个表
comment:存储留言评论信息,表结构如下:
表中,必须的字段:id,user_id,reply_comment_id,comment,insert_time,source_id
添加了冗余字段username,reply_user_name,userphoto
主要用于存储微信名、回复的微信名、微信头像(这三个字段完全不应该冗余,当小程序用户更换用户名时,该表要跟着更新,可维护性差,不建议存储这些冗余信息,我就是懒得写SQL了)
source:存储你在小程序需要回复的内容。
user:存储小程序使用的用户信息,主要包括用户名、用户头像等微信用户信息。
小程序端
wxml
<scroll-view scroll-top="{{scrollTop}}" scroll-y="true" style="height:{{scrollHeight}}px;" class="list" bindscrolltolower="bindDownLoad" bindscrolltoupper="refresh">
<view class="pro-con">
<block wx:for="{{list}}" wx:key="{{index}}">
<view class="pro-box">
<view class="head">
<image class="img" src="{{item.userPhoto}}" mode="aspectFit"></image>
<view class="box">
<view class="shead clear">
<view class="names fl">{{item.userName}}
<view wx:if="{{!item.replyUserName == \" \"}}">
-> {{item.replyUserName}}
</view>
</view>
</view>
</view>
</view>
<view class="addr-info">
<view class="addr-text">
{{item.comment}}
</view>
</view>
<view class="info">
<view class="text">
<text decode="true">{{item.insertTime}}</text>
</view>
<view class="text">
<button class="sharebtn" data-commentId="{{item.id}}" data-commentUserName="{{item.userName}}" bindtap="bindReply">回复</button>
</view>
<view wx:if="{{item.userId == userId}}" class="status text fr">
<text class="delete" decode="true" bindtap='deleteComment' data-CommentId="{{item.id}}">删除</text>
</view>
</view>
</view>
</block>
</view>
</scroll-view>
<form bindsubmit="submitForm" report-submit="true">
<view class="release">
<view wx:if="{{reply}}" class="replyinfo1">
回复<text class="text">{{replyUserName}}</text>
<button class="cancel" bindtap="cancleReply">取消回复</button>
</view>
<view class="replyinfo2">
<textarea placeholder-class="input_null" fixed="true" maxlength="-1" show-confirm-bar="false" cursor-spacing="15" auto-height="true" placeholder="请输入回复" name="comment"></textarea>
<button form-type="submit" class="submit">发送</button>
</view>
</view>
</form>
css
.names {
display: flex;
font-size: 30rpx;
line-height: 40rpx;
}
.input_null {
color: #c9c9c9;
}
.replyAll {
position:absolute;
}
.release {
align-items: flex-end; /*底部对齐*/
box-sizing: border-box;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
padding: 18rpx 0 18rpx 30rpx;
background-color: #f7f8f7;
font-size: 28rpx;
z-index: 999;
}
.replyinfo1{
display: flex;
justify-content: space-between; /*两端对齐*/
font-size: 35rpx;
}
.replyinfo2{
display: flex;
justify-content: space-between; /*两端对齐*/
}
.release textarea {
width: 550rpx;
min-height: 34rpx;
max-height: 102rpx; /*最多显示三行*/
border-width: 15rpx 20rpx; /*使用padding与预期留白不一致,故使用border*/
border-style: solid;
border-color: #fff;
line-height: 34rpx;
font-size: 28rpx;
background-color: #fff;
border-radius: 4rpx;
}
.release .text {
font-size: 40rpx;
color: #c9c9c9;
}
.cancel {
width: 240rpx;
height: 64rpx;
line-height: 64rpx;
text-align: center;
color: #6c0;
margin: 0 3px;
padding: 0;
}
.release .submit {
width: 120rpx;
height: 64rpx;
line-height: 64rpx;
text-align: center;
color: #6c0;
margin: 0 3px;
padding: 0;
}
.pro-box .info .text .delete {
color: #f68135;
border-radius: 50rpx;
border: 1px solid #f68135;
font-size: 28 rpx;
width: 150rpx;
height: 48rpx;
text-align: center;
}
js
// pages/comment/comment.js
const model = require('../cityChoose/cityChoose.js')
const config = require('../../utils/config.js')
const util = require('../../utils/util.js')
const app = getApp()
var mydata = {
end: 0,
replyUserName: ""
}
Page({
/**
* 页面的初始数据
*/
data: {
list: [],
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function(options) {
var that = this;
mydata.sourceId = options.sourceId
mydata.commentId = "";
mydata.replyUserName = "";
//设置scroll的高度
wx.getSystemInfo({
success: function(res) {
that.setData({
scrollHeight: res.windowHeight,
userId:app.globalData.haulUserInfo.id
});
}
});
mydata.page = 1;
that.getPageInfo(mydata.page);
},
/**
* 页面下拉刷新事件的处理函数
*/
refresh: function() {
console.log('refresh');
mydata.page = 1
this.getPageInfo(mydata.page, function() {
this.setData({
list: []
})
});
mydata.end = 0;
},
/**
* 页面上拉触底事件的处理函数
*/
bindDownLoad: function() {
console.log("onReachBottom");
var that = this;
if (mydata.end == 0) {
mydata.page++;
that.getPageInfo(mydata.page);
}
},
bindReply: function(e) {
console.log(e);
mydata.commentId = e.target.dataset.commentid;
mydata.replyUserName = e.target.dataset.commentusername;
this.setData({
replyUserName: mydata.replyUserName,
reply: true
})
},
// 合并数组
addArr(arr1, arr2) {
for (var i = 0; i < arr2.length; i++) {
arr1.push(arr2[i]);
}
return arr1;
},
deleteComment:function(e){
console.log(e);
var that = this;
var commentId = e.target.dataset.commentid;
wx.showModal({
title: '删除评论',
content: '请确认是否删除该评论?',
success: function (res) {
if (res.confirm) {
wx.request({
url: config.deleteComment,
method: "POST",
data: {
commentId: commentId
},
header: {
"content-type": "application/x-www-form-urlencoded;charset=utf-8",
},
success: res => {
that.refresh();
wx.showToast({
title: "删除成功"
})
}
})
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
},
cancleReply: function(e) {
mydata.commentId = "";
mydata.replyUserName = "";
this.setData({
replyUserName: mydata.replyUserName,
reply: false
})
},
// 更新页面信息
// 此处的回调函数在 传入新值之前执行 主要用来清除页面信息
getPageInfo(page, callback) {
var that = this;
util.showLoading();
console.log("getPageInfo");
console.log("page" + page);
var limited = 6;
var offset = (page - 1) * 6;
wx.request({
url: config.getComments,
method: "POST",
data: {
sourceId: mydata.sourceId,
limited: limited,
offset: offset
},
header: {
"content-type": "application/x-www-form-urlencoded;charset=utf-8",
},
success: res => {
console.log(res);
if (page == 1) {
that.data.list = res.data;
that.setData({
list: that.data.list
})
mydata.end = 0;
} else {
// 当前页为其他页
var list = that.data.list;
if (res.data.length != 0) {
list = that.addArr(list, res.data);
that.setData({
list: list
})
mydata.end = 0;
} else {
mydata.end = 1;
}
}
wx.hideLoading();
}
})
},
submitForm(e) {
var form = e.detail.value;
var that = this;
console.log(app.globalData.haulUserInfo);
if(form.comment == ""){
util.showLog('请输入评论');
return;
}
// 提交评论
wx.request({
url: config.insertComment,
method: "POST",
data: {
sourceId: mydata.sourceId,
comment: form.comment,
userId: app.globalData.haulUserInfo.id,
userName: app.globalData.haulUserInfo.userName,
replyCommentId: mydata.commentId,
replyUserName: mydata.replyUserName,
userPhoto: app.globalData.haulUserInfo.userPhoto
},
header: {
"content-type": "application/x-www-form-urlencoded;charset=utf-8",
//token: app.globalData.token
},
success: res => {
console.log(res)
if (res.data.success) {
wx.showToast({
title: "回复成功"
})
that.refresh();
mydata.commentId = "";
mydata.replyUserName = "";
this.setData({
replyUserName: mydata.replyUserName,
reply: false
})
} else {
wx.showToast({
title: '回复失败,请检查您的网络',
})
}
}
})
}
})
后台
后台功能:获取评论、删除评论、插入评论,都是简单的数据库操作,放在一个controller类中实现即可
package com.melon.haul.web;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.melon.haul.dto.DataUtil;
import com.melon.haul.dto.GetLocation;
import com.melon.haul.dto.Result;
import com.melon.haul.entity.Comment;
import com.melon.haul.entity.District;
import com.melon.haul.entity.Source;
import com.melon.haul.service.CommentService;
import com.melon.haul.service.DistrictService;
import com.melon.haul.service.SourceService;
@Controller
@WebAppConfiguration
@RequestMapping("/Comment")
public class CommentController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private CommentService commentService;
@RequestMapping(value = "/getComments", method = RequestMethod.POST)
private @ResponseBody List<Comment> getComments(@RequestParam("sourceId") int sourceId,
@RequestParam("limited") int limited,@RequestParam("offset") int offset) {
logger.info("getComments");
List<Comment> list = new ArrayList<Comment>();
try{
list = commentService.getComment(sourceId, limited, offset);
}catch(Exception e){
}
return list;
}
@RequestMapping(value = "/insertComment", method = RequestMethod.POST)
private @ResponseBody
Result<Map<String,String>>insertComment(@RequestParam("sourceId") String sourceId,
@RequestParam("comment") String comment,@RequestParam("userId") int userId,
@RequestParam("userName") String userName,@RequestParam("replyCommentId") String replyCommentId,
@RequestParam("replyUserName") String replyUserName,@RequestParam("userPhoto")String userPhoto) {
logger.info("insertComment");
Map<String, String> resultMap = new HashMap<String, String>();
try{
Integer rCId = -1;
if(!replyCommentId.equals(""))
rCId = Integer.parseInt(replyCommentId);
commentService.insertComment(Integer.parseInt(sourceId), comment, userId,userName,rCId,replyUserName,userPhoto);
resultMap.put("msg", "insertComment success");
}catch(Exception e){
System.out.print(e);
resultMap.put("msg", "insertComment error");
}
return new Result<Map<String, String>>(true, resultMap);
}
@RequestMapping(value = "/deleteComment", method = RequestMethod.POST)
private @ResponseBody
Result<Map<String,String>>deleteComment(@RequestParam("commentId") String commentId) {
logger.info("deleteComment");
Map<String, String> resultMap = new HashMap<String, String>();
try{
commentService.deleteComment(commentId);
resultMap.put("msg", "deleteComment success");
}catch(Exception e){
System.out.print(e);
resultMap.put("msg", "deleteComment error");
}
return new Result<Map<String, String>>(true, resultMap);
}
}
公共CSS(app.wxss)
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}
/* large button style */
.large-btn{
background: #f68135;
border-radius: 50rpx;
border: 1px solid #f68135;
color: #fff;
height: 100rpx;
line-height: 100rpx;
margin: 0 auto;
width: 96%;
text-align: center;
}
.large-btn.empty{
background: transparent;
color: #f68135;
margin-top: 50rpx;
}
.large-btn.disabled{
border-color: #ccc;
background: #ccc;
color: #fff;
}
/* public style to clear default styles */
.fl{
float: left;
}
.fr{
float: right;
}
.fc{
float:none;
}
.col-gray{
color: #999!important;
}
/* the message of auction about goods & cars */
.pro-con{
padding: 20rpx;
background: #f1f1f1;
}
.pro-box{
background: #fff;
padding: 20rpx;
box-sizing: border-box;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.pro-box .img{
display: inline-block;
vertical-align: top;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 10rpx;
}
.pro-box .box{
display: inline-block;
vertical-align: top;
width: calc(98% - 80rpx);
}
.pro-box .shead{
padding-bottom: 20rpx;
}
.pro-box .shead .name{
font-size: 30rpx;
line-height: 40rpx;
}
.pro-box .shead .stxt{
font-size: 26rpx;
color: #999;
}
.pro-box .shead .fr{
padding-top: 10rpx;
}
.pro-box .shead .fr navigator{
font-size: 0;
}
.pro-box .shead .fr image{
width: 48rpx;
height: 48rpx;
}
.pro-box .sharebtn{
height:48rpx;
background: #f68135;
border-radius: 50rpx;
border: 1px solid #f68135;
color: #fff;
text-align: center;
line-height: 50rpx;
font-size:30rpx;
}
.pro-box .addr-info{
align-items: center;
justify-content: space-between;
border-bottom: 1px dashed #ccc;
margin: 0 -20rpx;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
padding-left: 20rpx;
padding-right: 20rpx;
display: inline-block;
}
.pro-box .addr-info .addr-text{
font-size: 35rpx;
line-height: 40rpx;
width:100%;
}
.pro-box .addr-info .addr-text .color1{
color:lightskyblue;
border-color: #ccc;
border: 1px solid lightskyblue;
border-radius:15px;
margin-right: 5px;
padding: 0rpx,2rpx,0rpx,2rpx;
}
.pro-box .addr-info .addr-text .color2{
color: #f68135;
border-color: #ccc;
border: 1px solid #f68135;
border-radius:10px;
margin-right: 5px;
margin-left: 5px;
padding: 0rpx,2rpx,0rpx,2rpx;
}
.pro-box .position{
width: 48rpx;
height: 48rpx;
}
.pro-box .comment{
width: 55rpx;
height: 48rpx;
}
.pro-box .addr{
align-items: center;
justify-content: space-between;
border-bottom: 1px dashed #ccc;
margin: 0 -20rpx;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
padding-left: 20rpx;
padding-right: 20rpx;
display: flex;
}
.pro-box .addr .addr-text{
font-size: 34rpx;
line-height: 40rpx;
max-width: 240rpx;
min-width:200rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pro-box .addr .addr-text .color-text{
color: #f68135;
}
.pro-box .addr .time{
font-size: 26rpx;
line-height: 36rpx;
text-align: center;
}
.pro-box .addr .line{
background: #ccc;
height: 1px;
margin: 6rpx -20rpx;
position: relative;
}
.pro-box .info{
display: flex;
align-items: center;
justify-content: space-between;
}
.pro-box .info .text{
vertical-align:text-top;
font-size: 26rpx;
}
.pro-box .info .text .delete{
color: #f68135;
border-radius: 50rpx;
border: 1px solid #f68135;
width: 100rpx;
height: 48rpx;
text-align: center;
}