feat(mobile): 新增消息系统
This commit is contained in:
1 parent
916f5d17a5
commit
2c92b10684
17 files changed
+917
-6
No files matched your search
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE `user_message`
|
||||
(
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) NOT NULL COMMENT '接收用户ID',
|
||||
`send_user_id` bigint(20) NOT NULL COMMENT '发送用户ID',
|
||||
`msg_type` tinyint(1) NOT NULL COMMENT '消息类型:1书评被回复 2段评被回复',
|
||||
`book_id` bigint(20) DEFAULT NULL COMMENT '书籍ID',
|
||||
`book_index_id` bigint(20) DEFAULT NULL COMMENT '章节ID(段评消息跳转阅读页定位用)',
|
||||
`target_id` bigint(20) NOT NULL COMMENT '被回复的主评论ID(书评ID或顶层段评ID)',
|
||||
`content` varchar(200) DEFAULT NULL COMMENT '回复内容摘要',
|
||||
`read_flag` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已读:0未读 1已读',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_read` (`user_id`, `read_flag`),
|
||||
KEY `idx_user_time` (`user_id`, `create_time`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='用户消息表';
|
||||
@@ -3333,3 +3333,21 @@ CREATE TABLE `book_paragraph_comment`
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='书籍段落评论表(含回复)';
|
||||
|
||||
CREATE TABLE `user_message`
|
||||
(
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) NOT NULL COMMENT '接收用户ID',
|
||||
`send_user_id` bigint(20) NOT NULL COMMENT '发送用户ID',
|
||||
`msg_type` tinyint(1) NOT NULL COMMENT '消息类型:1书评被回复 2段评被回复',
|
||||
`book_id` bigint(20) DEFAULT NULL COMMENT '书籍ID',
|
||||
`book_index_id` bigint(20) DEFAULT NULL COMMENT '章节ID(段评消息跳转阅读页定位用)',
|
||||
`target_id` bigint(20) NOT NULL COMMENT '被回复的主评论ID(书评ID或顶层段评ID)',
|
||||
`content` varchar(200) DEFAULT NULL COMMENT '回复内容摘要',
|
||||
`read_flag` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已读:0未读 1已读',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_read` (`user_id`, `read_flag`),
|
||||
KEY `idx_user_time` (`user_id`, `create_time`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='用户消息表';
|
||||
|
||||
@@ -59,5 +59,10 @@ public class BookParagraphComment {
|
||||
*/
|
||||
private Long createUserId;
|
||||
|
||||
/**
|
||||
* 被回复人用户ID(仅回复时前端传入,非表字段,用于消息通知)
|
||||
*/
|
||||
private Long replyUserId;
|
||||
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.java2nb.novel.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用户消息
|
||||
*
|
||||
* @author 11797
|
||||
*/
|
||||
@Data
|
||||
public class UserMessage {
|
||||
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 接收用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 发送用户ID
|
||||
*/
|
||||
private Long sendUserId;
|
||||
|
||||
/**
|
||||
* 消息类型:1书评被回复 2段评被回复
|
||||
*/
|
||||
private Byte msgType;
|
||||
|
||||
/**
|
||||
* 书籍ID
|
||||
*/
|
||||
private Long bookId;
|
||||
|
||||
/**
|
||||
* 章节ID(段评消息跳转阅读页定位用)
|
||||
*/
|
||||
private Long bookIndexId;
|
||||
|
||||
/**
|
||||
* 被回复的主评论ID(书评ID或顶层段评ID)
|
||||
*/
|
||||
private Long targetId;
|
||||
|
||||
/**
|
||||
* 回复内容摘要
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 是否已读:0未读 1已读
|
||||
*/
|
||||
private Byte readFlag;
|
||||
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 书评被回复
|
||||
*/
|
||||
public static final byte TYPE_BOOK_COMMENT_REPLY = 1;
|
||||
|
||||
/**
|
||||
* 段评被回复
|
||||
*/
|
||||
public static final byte TYPE_PARAGRAPH_REPLY = 2;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.java2nb.novel.controller;
|
||||
|
||||
import com.java2nb.novel.core.bean.UserDetails;
|
||||
import com.java2nb.novel.core.enums.ResponseStatus;
|
||||
import com.java2nb.novel.service.UserMessageService;
|
||||
import io.github.xxyopen.model.resp.RestResult;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 用户消息(评论/段评被回复通知)
|
||||
*
|
||||
* @author 11797
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("user/message")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MessageController extends BaseController {
|
||||
|
||||
private final UserMessageService userMessageService;
|
||||
|
||||
/**
|
||||
* 分页查询我的消息,msgType 可选过滤:1书评回复 2段评回复
|
||||
*/
|
||||
@GetMapping("listByPage")
|
||||
public RestResult<?> listByPage(@RequestParam(value = "curr", defaultValue = "1") int page,
|
||||
@RequestParam(value = "limit", defaultValue = "10") int pageSize,
|
||||
@RequestParam(value = "msgType", required = false) Byte msgType, HttpServletRequest request) {
|
||||
UserDetails userDetails = getUserDetails(request);
|
||||
if (userDetails == null) {
|
||||
return RestResult.fail(ResponseStatus.NO_LOGIN);
|
||||
}
|
||||
return RestResult.ok(userMessageService.listMyMessageByPage(userDetails.getId(), msgType, page, pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 未读消息数(个人中心角标用)
|
||||
*/
|
||||
@GetMapping("unreadCount")
|
||||
public RestResult<?> unreadCount(HttpServletRequest request) {
|
||||
UserDetails userDetails = getUserDetails(request);
|
||||
if (userDetails == null) {
|
||||
return RestResult.fail(ResponseStatus.NO_LOGIN);
|
||||
}
|
||||
return RestResult.ok(userMessageService.countUnread(userDetails.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条标记已读
|
||||
*/
|
||||
@PostMapping("read")
|
||||
public RestResult<Void> read(@RequestParam("messageId") Long messageId, HttpServletRequest request) {
|
||||
UserDetails userDetails = getUserDetails(request);
|
||||
if (userDetails == null) {
|
||||
return RestResult.fail(ResponseStatus.NO_LOGIN);
|
||||
}
|
||||
userMessageService.markRead(userDetails.getId(), messageId);
|
||||
return RestResult.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部标记已读
|
||||
*/
|
||||
@PostMapping("readAll")
|
||||
public RestResult<Void> readAll(HttpServletRequest request) {
|
||||
UserDetails userDetails = getUserDetails(request);
|
||||
if (userDetails == null) {
|
||||
return RestResult.fail(ResponseStatus.NO_LOGIN);
|
||||
}
|
||||
userMessageService.markAllRead(userDetails.getId());
|
||||
return RestResult.ok();
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,14 @@ public class PageController extends BaseController {
|
||||
return ThreadLocalUtil.getTemplateDir() + "user/paragraph_comment";
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的消息页
|
||||
*/
|
||||
@RequestMapping("user/message.html")
|
||||
public String userMessage() {
|
||||
return ThreadLocalUtil.getTemplateDir() + "user/message";
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号设置页
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.java2nb.novel.mapper;
|
||||
|
||||
import com.java2nb.novel.entity.UserMessage;
|
||||
import com.java2nb.novel.vo.UserMessageVO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户消息 Mapper
|
||||
*
|
||||
* @author 11797
|
||||
*/
|
||||
public interface FrontUserMessageMapper {
|
||||
|
||||
/**
|
||||
* 插入一条消息
|
||||
*/
|
||||
int insert(UserMessage userMessage);
|
||||
|
||||
/**
|
||||
* 分页查询用户消息(按接收人),附带发送人昵称/头像与书名/章节名
|
||||
*/
|
||||
List<UserMessageVO> listByPage(@Param("userId") Long userId, @Param("msgType") Byte msgType);
|
||||
|
||||
/**
|
||||
* 未读消息数
|
||||
*/
|
||||
long countUnread(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 单条标记已读(限本人消息)
|
||||
*/
|
||||
int markRead(@Param("userId") Long userId, @Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 全部标记已读
|
||||
*/
|
||||
int markAllRead(@Param("userId") Long userId);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.java2nb.novel.service;
|
||||
|
||||
import com.java2nb.novel.vo.UserMessageVO;
|
||||
import io.github.xxyopen.model.page.PageBean;
|
||||
|
||||
/**
|
||||
* 用户消息 Service
|
||||
*
|
||||
* @author 11797
|
||||
*/
|
||||
public interface UserMessageService {
|
||||
|
||||
/**
|
||||
* 投递一条被回复消息(自己回复自己不通知;接收人为空时跳过)
|
||||
*
|
||||
* @param sendUserId 发送人(回复者)
|
||||
* @param toUserId 接收人(被回复者)
|
||||
* @param msgType 消息类型 {@link com.java2nb.novel.entity.UserMessage#TYPE_BOOK_COMMENT_REPLY} 等
|
||||
* @param bookId 书籍ID
|
||||
* @param bookIndexId 章节ID(段评消息传入,书评消息可为空)
|
||||
* @param targetId 被回复的主评论ID
|
||||
* @param replyContent 回复内容(截取摘要)
|
||||
*/
|
||||
void sendReplyMessage(Long sendUserId, Long toUserId, byte msgType, Long bookId, Long bookIndexId, Long targetId, String replyContent);
|
||||
|
||||
/**
|
||||
* 分页查询我的消息
|
||||
*/
|
||||
PageBean<UserMessageVO> listMyMessageByPage(Long userId, Byte msgType, int page, int pageSize);
|
||||
|
||||
/**
|
||||
* 我的未读消息数
|
||||
*/
|
||||
long countUnread(Long userId);
|
||||
|
||||
/**
|
||||
* 单条标记已读
|
||||
*/
|
||||
void markRead(Long userId, Long id);
|
||||
|
||||
/**
|
||||
* 全部标记已读
|
||||
*/
|
||||
void markAllRead(Long userId);
|
||||
}
|
||||
+8
@@ -4,8 +4,10 @@ import com.github.houbb.sensitive.word.core.SensitiveWordHelper;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.java2nb.novel.core.enums.ResponseStatus;
|
||||
import com.java2nb.novel.entity.BookParagraphComment;
|
||||
import com.java2nb.novel.entity.UserMessage;
|
||||
import com.java2nb.novel.mapper.FrontBookParagraphCommentMapper;
|
||||
import com.java2nb.novel.service.BookParagraphCommentService;
|
||||
import com.java2nb.novel.service.UserMessageService;
|
||||
import com.java2nb.novel.vo.BookParagraphCommentVO;
|
||||
import io.github.xxyopen.model.page.PageBean;
|
||||
import io.github.xxyopen.model.page.builder.pagehelper.PageBuilder;
|
||||
@@ -43,6 +45,8 @@ public class BookParagraphCommentServiceImpl implements BookParagraphCommentServ
|
||||
|
||||
private final FrontBookParagraphCommentMapper bookParagraphCommentMapper;
|
||||
|
||||
private final UserMessageService userMessageService;
|
||||
|
||||
@Override
|
||||
public Map<Integer, Integer> listParagraphCommentCounts(Long bookIndexId) {
|
||||
Map<Integer, Integer> counts = new LinkedHashMap<>();
|
||||
@@ -130,6 +134,10 @@ public class BookParagraphCommentServiceImpl implements BookParagraphCommentServ
|
||||
comment.setParagraphIdx(root.getParagraphIdx());
|
||||
bookParagraphCommentMapper.insert(comment);
|
||||
bookParagraphCommentMapper.addReplyCount(rootId);
|
||||
//段评被回复消息通知:优先通知被回复人(前端传 replyUserId),缺省回退顶层评论作者,自己回复自己除外
|
||||
Long toUserId = comment.getReplyUserId() != null ? comment.getReplyUserId() : root.getCreateUserId();
|
||||
userMessageService.sendReplyMessage(userId, toUserId, UserMessage.TYPE_PARAGRAPH_REPLY,
|
||||
root.getBookId(), root.getBookIndexId(), rootId, comment.getCommentContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.java2nb.novel.service.AuthorService;
|
||||
import com.java2nb.novel.service.BookService;
|
||||
import com.java2nb.novel.service.FileService;
|
||||
import com.java2nb.novel.service.LikeService;
|
||||
import com.java2nb.novel.service.UserMessageService;
|
||||
import com.java2nb.novel.vo.*;
|
||||
import io.github.xxyopen.model.page.PageBean;
|
||||
import io.github.xxyopen.model.page.builder.pagehelper.PageBuilder;
|
||||
@@ -95,6 +96,8 @@ public class BookServiceImpl implements BookService {
|
||||
|
||||
private final LikeService likeService;
|
||||
|
||||
private final UserMessageService userMessageService;
|
||||
|
||||
private final BookPriceProperties bookPriceConfig;
|
||||
|
||||
private final OpenAiImageModel openAiImageModel;
|
||||
@@ -907,6 +910,13 @@ public class BookServiceImpl implements BookService {
|
||||
bookCommentReplyMapper.insertSelective(commentReply);
|
||||
//增加评论回复数
|
||||
bookCommentMapper.addReplyCount(commentReply.getCommentId());
|
||||
//书评被回复消息通知(回复者自己除外)
|
||||
BookComment bookComment = getBookComment(commentReply.getCommentId());
|
||||
if (bookComment != null) {
|
||||
userMessageService.sendReplyMessage(userId, bookComment.getCreateUserId(),
|
||||
UserMessage.TYPE_BOOK_COMMENT_REPLY, bookComment.getBookId(), null,
|
||||
bookComment.getId(), commentReply.getReplyContent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.java2nb.novel.service.impl;
|
||||
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.java2nb.novel.entity.UserMessage;
|
||||
import com.java2nb.novel.mapper.FrontUserMessageMapper;
|
||||
import com.java2nb.novel.service.UserMessageService;
|
||||
import com.java2nb.novel.vo.UserMessageVO;
|
||||
import io.github.xxyopen.model.page.PageBean;
|
||||
import io.github.xxyopen.model.page.builder.pagehelper.PageBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 用户消息服务实现
|
||||
*
|
||||
* @author 11797
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserMessageServiceImpl implements UserMessageService {
|
||||
|
||||
/**
|
||||
* 消息内容摘要最大长度,与表字段 content 保持一致
|
||||
*/
|
||||
private static final int CONTENT_MAX_LENGTH = 200;
|
||||
|
||||
private final FrontUserMessageMapper userMessageMapper;
|
||||
|
||||
@Override
|
||||
public void sendReplyMessage(Long sendUserId, Long toUserId, byte msgType, Long bookId,
|
||||
Long bookIndexId, Long targetId, String replyContent) {
|
||||
// 无接收人、自己回复自己、无被回复主评论,均不通知
|
||||
if (toUserId == null || targetId == null || toUserId.equals(sendUserId)) {
|
||||
return;
|
||||
}
|
||||
UserMessage message = new UserMessage();
|
||||
message.setUserId(toUserId);
|
||||
message.setSendUserId(sendUserId);
|
||||
message.setMsgType(msgType);
|
||||
message.setBookId(bookId);
|
||||
message.setBookIndexId(bookIndexId);
|
||||
message.setTargetId(targetId);
|
||||
message.setContent(abbreviateContent(replyContent));
|
||||
message.setReadFlag((byte) 0);
|
||||
userMessageMapper.insert(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取回复内容摘要,去除换行避免破坏展示
|
||||
*/
|
||||
private String abbreviateContent(String content) {
|
||||
if (StringUtils.isBlank(content)) {
|
||||
return null;
|
||||
}
|
||||
String clean = content.replaceAll("\\s+", " ").trim();
|
||||
return clean.length() > CONTENT_MAX_LENGTH ? clean.substring(0, CONTENT_MAX_LENGTH) : clean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageBean<UserMessageVO> listMyMessageByPage(Long userId, Byte msgType, int page, int pageSize) {
|
||||
PageHelper.startPage(page, pageSize);
|
||||
return PageBuilder.build(userMessageMapper.listByPage(userId, msgType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUnread(Long userId) {
|
||||
return userMessageMapper.countUnread(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markRead(Long userId, Long id) {
|
||||
userMessageMapper.markRead(userId, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAllRead(Long userId) {
|
||||
userMessageMapper.markAllRead(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.java2nb.novel.vo;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.java2nb.novel.core.serialize.CommentUserNameSerialize;
|
||||
import com.java2nb.novel.core.serialize.TimeAgoFormatSerialize;
|
||||
import com.java2nb.novel.entity.UserMessage;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用户消息展示VO
|
||||
*
|
||||
* @author 11797
|
||||
*/
|
||||
@Data
|
||||
public class UserMessageVO extends UserMessage {
|
||||
|
||||
/**
|
||||
* 发送人昵称(脱敏展示)
|
||||
*/
|
||||
@JsonSerialize(using = CommentUserNameSerialize.class)
|
||||
private String sendUserName;
|
||||
|
||||
/**
|
||||
* 发送人头像
|
||||
*/
|
||||
private String sendUserPhoto;
|
||||
|
||||
/**
|
||||
* 关联书名
|
||||
*/
|
||||
private String bookName;
|
||||
|
||||
/**
|
||||
* 关联章节名(段评消息)
|
||||
*/
|
||||
private String indexName;
|
||||
|
||||
@JsonSerialize(using = TimeAgoFormatSerialize.class)
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.java2nb.novel.mapper.FrontUserMessageMapper">
|
||||
|
||||
<insert id="insert" parameterType="com.java2nb.novel.entity.UserMessage">
|
||||
insert into user_message (user_id, send_user_id, msg_type, book_id, book_index_id, target_id, content, read_flag, create_time)
|
||||
values (#{userId}, #{sendUserId}, #{msgType}, #{bookId}, #{bookIndexId}, #{targetId}, #{content}, 0, now())
|
||||
</insert>
|
||||
|
||||
<select id="listByPage" resultType="com.java2nb.novel.vo.UserMessageVO">
|
||||
select t1.id, t1.user_id, t1.send_user_id, t1.msg_type, t1.book_id, t1.book_index_id, t1.target_id,
|
||||
t1.content, t1.read_flag, t1.create_time,
|
||||
t2.username send_user_name, t2.user_photo send_user_photo,
|
||||
t3.book_name, t4.index_name
|
||||
from user_message t1
|
||||
inner join user t2 on t1.send_user_id = t2.id
|
||||
left join book t3 on t1.book_id = t3.id
|
||||
left join book_index t4 on t1.book_index_id = t4.id
|
||||
where t1.user_id = #{userId}
|
||||
<if test="msgType != null">and t1.msg_type = #{msgType}</if>
|
||||
order by t1.read_flag, t1.create_time desc
|
||||
</select>
|
||||
|
||||
<select id="countUnread" resultType="java.lang.Long">
|
||||
select count(*) from user_message where user_id = #{userId} and read_flag = 0
|
||||
</select>
|
||||
|
||||
<update id="markRead">
|
||||
update user_message set read_flag = 1 where user_id = #{userId} and id = #{id} and read_flag = 0
|
||||
</update>
|
||||
|
||||
<update id="markAllRead">
|
||||
update user_message set read_flag = 1 where user_id = #{userId} and read_flag = 0
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -714,7 +714,7 @@
|
||||
return dfd.promise();
|
||||
},
|
||||
// 发表段落评论/回复(rootId>0表示回复),resolve 完整 RestResult
|
||||
add: function (idx, content, rootId, replyUserName) {
|
||||
add: function (idx, content, rootId, replyUserName, replyUserId) {
|
||||
var dfd = $.Deferred();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
@@ -725,6 +725,7 @@
|
||||
'paragraphIdx': idx,
|
||||
'rootId': rootId || 0,
|
||||
'replyUserName': replyUserName || '',
|
||||
'replyUserId': replyUserId || '',
|
||||
'commentContent': content
|
||||
},
|
||||
dataType: "json",
|
||||
@@ -826,7 +827,8 @@
|
||||
$('#pcList').on('click', '.pc-reply-btn', function () {
|
||||
pcReplyTarget = {
|
||||
rootId: $(this).data('root'),
|
||||
name: String($(this).data('name') || '')
|
||||
name: String($(this).data('name') || ''),
|
||||
uid: $(this).data('uid')
|
||||
};
|
||||
$('#pcReplyHintText').text('回复 @' + pcReplyTarget.name);
|
||||
$('#pcReplyHint').show();
|
||||
@@ -948,7 +950,7 @@
|
||||
if (replies.length > 0) {
|
||||
$op.append($('<span class="pc-reply-num"></span>').text(replies.length + '条回复'));
|
||||
}
|
||||
$op.append($('<span class="pc-reply-btn">回复</span>').data('root', item.id).data('name', item.createUserName));
|
||||
$op.append($('<span class="pc-reply-btn">回复</span>').data('root', item.id).data('name', item.createUserName).data('uid', item.createUserId));
|
||||
$item.append($op);
|
||||
return $item;
|
||||
}
|
||||
@@ -965,7 +967,7 @@
|
||||
$ri.append($ru);
|
||||
$ri.append($('<div class="pc-r-content"></div>').text(r.commentContent));
|
||||
$ri.append($('<div class="pc-op-bar"></div>')
|
||||
.append($('<span class="pc-reply-btn">回复</span>').data('root', rootId).data('name', r.createUserName)));
|
||||
.append($('<span class="pc-reply-btn">回复</span>').data('root', rootId).data('name', r.createUserName).data('uid', r.createUserId)));
|
||||
return $ri;
|
||||
}
|
||||
|
||||
@@ -995,7 +997,8 @@
|
||||
$btn.addClass('layui-btn-disabled').attr('disabled', 'disabled').text('提交中...');
|
||||
var rootId = pcReplyTarget ? pcReplyTarget.rootId : 0;
|
||||
var replyUserName = pcReplyTarget ? pcReplyTarget.name : '';
|
||||
ParagraphApi.add(idx, content, rootId, replyUserName).then(function (res) {
|
||||
var replyUserId = pcReplyTarget ? pcReplyTarget.uid : '';
|
||||
ParagraphApi.add(idx, content, rootId, replyUserName, replyUserId).then(function (res) {
|
||||
pcSubmitting = false;
|
||||
$btn.removeClass('layui-btn-disabled').removeAttr('disabled').text('发表');
|
||||
if (res.code == 200) {
|
||||
|
||||
@@ -40,8 +40,13 @@
|
||||
-webkit-transition: all .3s;
|
||||
}
|
||||
|
||||
/* 顶部 Logo:等比缩放不溢出 */
|
||||
/* 顶部 Logo:等比缩放不溢出;右侧消息入口 */
|
||||
.site-header {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px 0;
|
||||
}
|
||||
|
||||
@@ -50,6 +55,43 @@
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
/* 右上角未读消息:铃铛圆形入口 + 主题绿渐变角标,白描边与图标区分层 */
|
||||
.header-msg {
|
||||
position: relative;
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(62, 175, 124, .10);
|
||||
color: #3eaf7c;
|
||||
}
|
||||
|
||||
.header-msg .layui-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-msg .hm-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
display: none;
|
||||
min-width: 16px;
|
||||
padding: 0 4px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3eaf7c, #6cc798);
|
||||
border: 1.5px solid #fff;
|
||||
border-radius: 9px;
|
||||
box-shadow: 0 2px 4px rgba(62, 175, 124, .30);
|
||||
}
|
||||
|
||||
/* 搜索条:胶囊输入框 + 主题色等高按钮 */
|
||||
.search-bar {
|
||||
display: -webkit-flex;
|
||||
@@ -401,6 +443,10 @@
|
||||
<a name="top"></a>
|
||||
<div class="site-header">
|
||||
<img class="site-logo" th:src="${application.website.logo}" alt="logo">
|
||||
<a class="header-msg" href="/user/message.html">
|
||||
<i class="layui-icon"></i>
|
||||
<span class="hm-badge" id="headerMsgBadge"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
@@ -500,6 +546,19 @@
|
||||
<script>
|
||||
|
||||
|
||||
// 右上角未读消息角标(未登录或无未读时不显示,点击进消息页会自动引导登录)
|
||||
$.ajax({
|
||||
type: "get",
|
||||
url: "/user/message/unreadCount",
|
||||
data: {},
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (data.code == 200 && data.data > 0) {
|
||||
$("#headerMsgBadge").text(data.data > 99 ? '99+' : data.data).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//更新榜单查询
|
||||
$.ajax({
|
||||
type: "get",
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
|
||||
<title th:text="'我的消息_' + ${application.website.name}"></title>
|
||||
|
||||
<div th:include="mobile/common/css :: css"></div>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #f5f7f6;
|
||||
}
|
||||
|
||||
/* 顶部筛选标签 */
|
||||
.msg-tabs {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
padding: 10px 12px 4px;
|
||||
}
|
||||
|
||||
.msg-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
font-size: 13px;
|
||||
color: #5c6b64;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.msg-tab.active {
|
||||
color: #2e8b62;
|
||||
font-weight: 600;
|
||||
border-bottom-color: #3eaf7c;
|
||||
}
|
||||
|
||||
#msgList {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* 消息卡片 */
|
||||
.msg-item {
|
||||
position: relative;
|
||||
margin: 0 12px 10px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(62, 175, 124, .08);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.msg-item .mi-top {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.msg-item .mi-avatar {
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #f0f2f1;
|
||||
}
|
||||
|
||||
.msg-item .mi-head {
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.msg-item .mi-title {
|
||||
font-size: 13px;
|
||||
color: #1f2d26;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.msg-item .mi-title b {
|
||||
color: #2e8b62;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.msg-item .mi-time {
|
||||
font-size: 11px;
|
||||
color: #98a8a1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.msg-item .mi-content {
|
||||
margin-top: 10px;
|
||||
padding: 8px 10px;
|
||||
background: #f5f7f6;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #5c6b64;
|
||||
line-height: 19px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.msg-item .mi-from {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #98a8a1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 未读小圆点:主题绿主色,与消息角标同体系(圆点过小不施渐变) */
|
||||
.msg-item .mi-dot {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #3eaf7c;
|
||||
box-shadow: 0 1px 3px rgba(62, 175, 124, .35);
|
||||
}
|
||||
|
||||
.msg-item.readed .mi-dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 全部已读 */
|
||||
.msg-bar {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 6px 14px 0;
|
||||
}
|
||||
|
||||
.msg-bar .read-all {
|
||||
font-size: 12px;
|
||||
color: #2e8b62;
|
||||
padding: 4px 12px;
|
||||
background: rgba(62, 175, 124, .1);
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 空态 */
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #98a8a1;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.empty-tip .empty-link {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
padding: 6px 20px;
|
||||
background: rgba(62, 175, 124, .1);
|
||||
color: #2e8b62;
|
||||
font-size: 13px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.page-box {
|
||||
text-align: center;
|
||||
padding: 6px 12px 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="body">
|
||||
|
||||
<div th:replace="mobile/common/header :: pageHeader('我的消息', null)"></div>
|
||||
|
||||
<div class="msg-tabs">
|
||||
<div class="msg-tab active" data-type="">全部</div>
|
||||
<div class="msg-tab" data-type="1">书评回复</div>
|
||||
<div class="msg-tab" data-type="2">段评回复</div>
|
||||
</div>
|
||||
<div class="msg-bar"><span class="read-all" onclick="readAll()">全部标为已读</span></div>
|
||||
|
||||
<div id="msgList"></div>
|
||||
<div id="msgPage" class="page-box"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div th:replace="mobile/common/footer :: footer"></div>
|
||||
|
||||
<div th:replace="mobile/common/js :: js"></div>
|
||||
|
||||
<script>
|
||||
var curType = '';
|
||||
|
||||
search(1, 10);
|
||||
|
||||
// 顶部标签切换
|
||||
$('.msg-tab').on('click', function () {
|
||||
var $tab = $(this);
|
||||
if ($tab.hasClass('active')) {
|
||||
return;
|
||||
}
|
||||
$tab.addClass('active').siblings('.active').removeClass('active');
|
||||
curType = $tab.attr('data-type');
|
||||
search(1, 10);
|
||||
});
|
||||
|
||||
function search(curr, limit) {
|
||||
var params = {'curr': curr, 'limit': limit};
|
||||
if (curType !== '') {
|
||||
params.msgType = curType;
|
||||
}
|
||||
$.ajax({
|
||||
type: "get",
|
||||
url: "/user/message/listByPage",
|
||||
data: params,
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (data.code == 200) {
|
||||
renderMsgList(data.data);
|
||||
} else if (data.code == 1001) {
|
||||
location.href = '/user/login.html?originUrl=' + encodeURIComponent(location.href);
|
||||
} else {
|
||||
layer.alert(data.msg);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
layer.alert('网络异常');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderMsgList(pageBean) {
|
||||
var list = pageBean.list;
|
||||
if (!list || list.length == 0) {
|
||||
$("#msgList").html("<div class=\"empty-tip\">暂时没有新消息~<br><a href=\"/\" class=\"empty-link\">去书城逛逛</a></div>");
|
||||
$("#msgPage").html("");
|
||||
return;
|
||||
}
|
||||
var html = "";
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
html += buildMsgItem(list[i]);
|
||||
}
|
||||
$("#msgList").html(html);
|
||||
|
||||
layui.use('laypage', function () {
|
||||
layui.laypage.render({
|
||||
elem: 'msgPage'
|
||||
, count: pageBean.total
|
||||
, curr: pageBean.pageNum
|
||||
, limit: pageBean.pageSize
|
||||
, layout: ['prev', 'page', 'next']
|
||||
, jump: function (obj, first) {
|
||||
if (!first) {
|
||||
search(obj.curr, obj.limit);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildMsgItem(msg) {
|
||||
var photo = msg.sendUserPhoto ? msg.sendUserPhoto : '/images/default.gif';
|
||||
var sender = msg.sendUserName ? msg.sendUserName : '某用户';
|
||||
// msgType:1书评被回复 2段评被回复
|
||||
var action = msg.msgType == 2 ? '回复了你的段评' : '回复了你的书评';
|
||||
var from = '';
|
||||
if (msg.bookName) {
|
||||
from += msg.bookName;
|
||||
if (msg.indexName) {
|
||||
from += ' · ' + msg.indexName;
|
||||
}
|
||||
}
|
||||
var readed = (msg.readFlag == 1) ? ' readed' : '';
|
||||
return ('<div class="msg-item' + readed + '" data-id="' + msg.id + '" data-type="' + msg.msgType + '"' +
|
||||
' data-book="' + (msg.bookId || '') + '" data-index="' + (msg.bookIndexId || '') + '"' +
|
||||
' data-target="' + (msg.targetId || '') + '" onclick="openMsg(this)">' +
|
||||
'<span class="mi-dot"></span>' +
|
||||
'<div class="mi-top">' +
|
||||
'<img class="mi-avatar" src="' + photo + '" onerror="this.src=\'/images/default.gif\'" alt="">' +
|
||||
'<div class="mi-head">' +
|
||||
'<div class="mi-title"><b>' + sender + '</b> ' + action + '</div>' +
|
||||
'<div class="mi-time">' + msg.createTime + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mi-content">' + (msg.content || '') + '</div>' +
|
||||
(from ? '<div class="mi-from">来自:' + from + '</div>' : '') +
|
||||
'</div>');
|
||||
}
|
||||
|
||||
// 点击消息:标记已读并跳转到被回复内容
|
||||
function openMsg(el) {
|
||||
var $el = $(el);
|
||||
var id = $el.attr('data-id');
|
||||
var type = $el.attr('data-type');
|
||||
var bookId = $el.attr('data-book');
|
||||
var indexId = $el.attr('data-index');
|
||||
var targetId = $el.attr('data-target');
|
||||
// 先本地置已读,跳转返回后视觉即时反馈
|
||||
$el.addClass('readed');
|
||||
markRead(id);
|
||||
var url;
|
||||
if (type == 2) {
|
||||
// 段评:跳章节阅读页
|
||||
url = '/book/' + bookId + '/' + indexId + '.html';
|
||||
} else {
|
||||
// 书评:跳该书评的回复列表页
|
||||
url = '/book/reply-' + targetId + '.html';
|
||||
}
|
||||
setTimeout(function () {
|
||||
location.href = url;
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function markRead(id) {
|
||||
$.ajax({
|
||||
type: "post",
|
||||
url: "/user/message/read",
|
||||
data: {'messageId': id},
|
||||
dataType: "json"
|
||||
});
|
||||
}
|
||||
|
||||
function readAll() {
|
||||
$.ajax({
|
||||
type: "post",
|
||||
url: "/user/message/readAll",
|
||||
data: {},
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (data.code == 200) {
|
||||
$('.msg-item').addClass('readed');
|
||||
} else if (data.code == 1001) {
|
||||
location.href = '/user/login.html?originUrl=' + encodeURIComponent(location.href);
|
||||
} else {
|
||||
layer.alert(data.msg);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
layer.alert('网络异常');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -139,6 +139,23 @@
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* 菜单未读消息角标:主题绿渐变,与首页消息角标同体系 */
|
||||
.menu-item .mi-badge {
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
display: none;
|
||||
min-width: 18px;
|
||||
padding: 0 5px;
|
||||
margin-right: 8px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3eaf7c, #6cc798);
|
||||
border-radius: 9px;
|
||||
box-shadow: 0 2px 4px rgba(62, 175, 124, .30);
|
||||
}
|
||||
|
||||
/* 退出登录 */
|
||||
.logout-card {
|
||||
display: block;
|
||||
@@ -209,6 +226,13 @@
|
||||
<span class="mi-arrow"></span>
|
||||
</a>
|
||||
|
||||
<a class="menu-item" href="/user/message.html">
|
||||
<span class="mi-icon"><i class="layui-icon"></i></span>
|
||||
<span class="mi-text">我的消息</span>
|
||||
<span class="mi-badge" id="msgUnreadBadge"></span>
|
||||
<span class="mi-arrow"></span>
|
||||
</a>
|
||||
|
||||
<a class="menu-item" href="/user/comment.html">
|
||||
<span class="mi-icon"><i class="layui-icon"></i></span>
|
||||
<span class="mi-text">我的书评</span>
|
||||
@@ -276,6 +300,8 @@
|
||||
|
||||
$("#accountBalance").html(data.data.accountBalance);
|
||||
|
||||
loadUnreadMsgCount();
|
||||
|
||||
} else if (data.code == 1001) {
|
||||
//未登录
|
||||
location.href = '/user/login.html?originUrl=' + encodeURIComponent(location.href);
|
||||
@@ -295,6 +321,21 @@
|
||||
window.location.href = "/book/bookclass.html?sortBy=last_index_update_time";
|
||||
}
|
||||
|
||||
// 拉取未读消息数,在“我的消息”菜单展示角标(超过99显示 99+)
|
||||
function loadUnreadMsgCount() {
|
||||
$.ajax({
|
||||
type: "get",
|
||||
url: "/user/message/unreadCount",
|
||||
data: {},
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (data.code == 200 && data.data > 0) {
|
||||
$("#msgUnreadBadge").text(data.data > 99 ? '99+' : data.data).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function searchBooks() {
|
||||
var keywords = $("#title").val();
|
||||
window.location.href = "/book/bookclass.html?keyword=" + encodeURI(keywords);
|
||||
|
||||
Reference in new issue
Block a user