feat(mobile): 新增消息系统

This commit is contained in:
xiongxiaoyang committed 2026-10-20 08:20:39 +08:00
1 parent 916f5d17a5
commit 2c92b10684
17 files changed
+917 -6

No files matched your search

+17
View File
@@ -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 ='用户消息表';
+18
View File
@@ -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);
}
@@ -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">&#xe667;</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">&#xe667;</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">&#xe611;</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);