1.帖子详情页面组成
帖子详情页主要由以下五部分组成:
- 导航栏:顶部链接区域
- 侧边栏:页面右侧的数据展示区域
- 功能按钮区域:包括编辑按钮和收藏按钮
- 内容展示区域:包括帖子的基本信息和发帖人的信息
- 评论区域:包括评论列表展示区域和评论输入区域
然后帖子主要包含四个主要部分:
- 帖子标题区域:放置帖子标题;
- 功能按钮区域:包括编辑按钮,需要实现对应的页面跳转和编辑与收藏的功能;
- 帖子的基础信息区域:放置评论数量、阅读量字段、帖子的时间信息和发帖人的信息;
- 帖子详情区域:帖子内容的展示区域,以上三个区域通常是固定的,而内容区域则会因为帖子内容的多少动态的变化,内容多则占用的版面就多,反之就会少一些。
2.详情页模块功能
详情页是通过点击帖子列表页中的单个卡片中的链接跳转而来的,详情页的路径可以定义为 /detail/{postId},用帖子id来确定具体的帖子。在首页帖子列表渲染时已经做了处理,跳转路径为:
<a th:href="@{'/detail/'+${bbsEntity.postId}}" th:text="${bbsEntity.postTitle}">
添加链接后,在点击后就会跳转到详情页面。
2.1 数据查询
首先需要实现数据查询的功能,详情页中不止会展示帖子详情数据,还会展示该发帖人信息和评论数据,发帖人信息和帖子详情内容可以根据 postId 和 publicUserId 通过一定的逻辑查询到,因此会涉及到帖子表、用户表两张表的查询操作。
这里先需要将数据获取并转发到对应的模板页面中,需要在帖子请求的 Controller 方法中将查询到的数据放入 request 域中,在 BBSPostController 类中新增 postDetail() 方法。
2.1.1 控制层跳转
/**
* 跳转帖子详情页
* @param request
* @param postId
* @param commentPage
* @return
*/
@GetMapping("detail/{postId}")
public String postDetail(HttpServletRequest request, @PathVariable(value = "postId") Long postId,
@RequestParam(value = "commentPage", required = false, defaultValue = "1") Integer commentPage) {
List<BBSPostCategory> bbsPostCategories = bbsPostCategoryService.getBBSPostCategories();
if (CollectionUtils.isEmpty(bbsPostCategories)) {
return "error/error_404";
}
//将分类数据封装到request域中
request.setAttribute("bbsPostCategories", bbsPostCategories);
// 帖子内容
BBSPost bbsPost = bbsPostService.getBBSPostForDetail(postId);
if (bbsPost == null) {
return "error/error_404";
}
request.setAttribute("bbsPost", bbsPost);
// 发帖用户信息
BBSUser bbsUser = bbsUserService.getUserById(bbsPost.getPublishUserId());
if (bbsUser == null) {
return "error/error_404";
}
request.setAttribute("bbsUser", bbsUser);
// todo 是否收藏了本贴
// 本周热议的帖子
request.setAttribute("hotTopicBBSPostList", bbsPostService.getHotTopicBBSPostList());
// todo 评论数据
return "jie/detail";
}
2.1.2 业务层
首先通过BBSPostService类查询出帖子的详细信息。
BBSPostService
/**
* 获取详情&浏览数加1
*
* @param bbsPostId
* @return
*/
BBSPost getBBSPostForDetail(Long bbsPostId);
@Override
public BBSPost getBBSPostForDetail(Long bbsPostId) {
BBSPost bbsPost = bbsPostMapper.selectByPrimaryKey(bbsPostId);
if (bbsPost != null) {
bbsPost.setPostViews(bbsPost.getPostViews() + 1);
bbsPostMapper.updateByPrimaryKeySelective(bbsPost);
}
return bbsPost;
}
BBSPostMapper
BBSPost selectByPrimaryKey(Long postId);
int updateByPrimaryKeySelective(BBSPost record);
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="ResultMapWithBLOBs">
select
<include refid="Base_Column_List"/>
,
<include refid="Blob_Column_List"/>
from tb_bbs_post
where post_id = #{postId,jdbcType=BIGINT} and post_status=1
</select>
<update id="updateByPrimaryKeySelective" parameterType="top.picacho.bbs.entity.BBSPost">
update tb_bbs_post
<set>
<if test="publishUserId != null">
publish_user_id = #{publishUserId,jdbcType=BIGINT},
</if>
<if test="postTitle != null">
post_title = #{postTitle,jdbcType=VARCHAR},
</if>
<if test="postCategoryId != null">
post_category_id = #{postCategoryId,jdbcType=INTEGER},
</if>
<if test="postCategoryName != null">
post_category_name = #{postCategoryName,jdbcType=VARCHAR},
</if>
<if test="postStatus != null">
post_status = #{postStatus,jdbcType=TINYINT},
</if>
<if test="postViews != null">
post_views = #{postViews,jdbcType=BIGINT},
</if>
<if test="postComments != null">
post_comments = #{postComments,jdbcType=BIGINT},
</if>
<if test="postCollects != null">
post_collects = #{postCollects,jdbcType=BIGINT},
</if>
<if test="lastUpdateTime != null">
last_update_time = #{lastUpdateTime,jdbcType=TIMESTAMP},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=TIMESTAMP},
</if>
<if test="postContent != null">
post_content = #{postContent,jdbcType=LONGVARCHAR},
</if>
</set>
where post_id = #{postId,jdbcType=BIGINT}
</update>
BBSPostService
/**
* 获取用户详情
*
* @param userId
* @return
*/
BBSUser getUserById(Long userId);
@Override
public BBSUser getUserById(Long userId) {
return bbsUserMapper.selectByPrimaryKey(userId);
}
BBSUserMapper数据持久层前面实现过,这里不再阐述了。
2.2 数据渲染
在 templates/jie/ 目录下新增详情页 detail.html。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="header::head-fragment('帖子详情')"> </head>
<body>
<div th:replace="header::header-fragment"></div>
<div class="fly-panel fly-column">
<div class="layui-container">
<ul class="layui-clear">
<li th:class="${null ==categoryId} ?'layui-hide-xs layui-this':''">
<a href="/">首页</a>
</li>
<th:block th:unless="${null == bbsPostCategories}">
<th:block th:each="c : ${bbsPostCategories}">
<li
th:class="${null !=categoryId and categoryId==c.categoryId} ?'layui-hide-xs layui-this':''"
>
<a
th:href="@{${'/index?categoryId='+c.categoryId}}"
th:text="${c.categoryName}"
>分享</a
>
</li>
</th:block>
</th:block>
<li class="layui-hide-xs layui-hide-sm layui-show-md-inline-block">
<span class="fly-mid"></span>
</li>
</ul>
<div class="fly-column-right layui-hide-xs">
<a th:href="@{/addPostPage}" class="layui-btn">发布新帖</a>
</div>
</div>
</div>
<div class="layui-container">
<div class="layui-row layui-col-space15">
<div class="layui-col-md8 content detail">
<div class="fly-panel detail-box">
<h1 th:text="${bbsPost.postTitle}">My-BBs</h1>
<div class="fly-detail-info">
<div>
<a
class="layui-btn layui-btn-xs jie-admin"
th:href="@{'/editPostPage/'+${bbsPost.postId}}"
>编辑</a
>
</div>
<span class="fly-list-nums">
<a href="#comments"
><i class="iconfont" title="评论"></i>
<th:block th:text="${bbsPost.postComments}"></th:block
></a>
<i class="iconfont" title="人气"></i>
<th:block th:text="${bbsPost.postViews}"></th:block>
</span>
</div>
<div class="detail-about">
<a
class="fly-avatar"
th:href="@{${'/userCenter/'+bbsUser.userId}}"
>
<img th:src="@{${bbsUser.headImgUrl}}" />
</a>
<div class="fly-detail-user">
<a
th:href="@{${'/userCenter/'+bbsUser.userId}}"
class="fly-link"
>
<cite th:text="${bbsUser.nickName}">picacho</cite>
<span>
<th:block th:if="${bbsUser.userStatus==0}"
>账号正常
</th:block>
<th:block th:if="${bbsUser.userStatus==1}"
>账号已被封
</th:block>
</span>
</a>
</div>
<div class="detail-hits" id="LAY_jieAdmin" data-id="123">
<span
th:text="${#dates.format(bbsPost.createTime, 'yyyy-MM-dd')}"
>2021-08-01</span
>
<span
style="margin-left: 6px; padding-right: 10px; color: #FF7200"
th:text="${'最新修改时间:'+#dates.format(bbsPost.lastUpdateTime, 'yyyy-MM-dd HH:mm:ss')}"
>2021-08-01</span
>
</div>
</div>
<div
class="detail-body photos"
th:utext="${bbsPost.postContent}"
></div>
</div>
<div class="fly-panel detail-box" id="comments">
<fieldset
class="layui-elem-field layui-field-title"
style="text-align: center;"
>
<legend>回帖</legend>
</fieldset>
<ul class="jieda" id="jieda">
<li class="fly-none">消灭零回复</li>
</ul>
</div>
</div>
<div class="layui-col-md4">
<dl class="fly-panel fly-list-one">
<dt class="fly-panel-title">本周热议</dt>
<th:block th:if="${#lists.isEmpty(hotTopicBBSPostList)}">
<!-- 无数据时 -->
<div class="fly-none">没有相关数据</div>
</th:block>
<th:block th:unless="${#lists.isEmpty(hotTopicBBSPostList)}">
<th:block th:each="bbsEntity : ${hotTopicBBSPostList}">
<dd>
<a
th:href="@{'/detail/'+${bbsEntity.postId}}"
th:text="${bbsEntity.postTitle}"
>My-BBS</a
>
<span
><i class="iconfont icon-pinglun1"></i>
<th:block th:text="${bbsEntity.postComments}"></th:block
></span>
</dd>
</th:block>
</th:block>
</dl>
</div>
</div>
</div>
<div class="fly-footer">
<p>
My-BBS社区 2021 ©
<a href="#" target="_blank">picacho</a>
</p>
</div>
<script th:src="@{/layui/layui.js}"></script>
</body>
</html>
根据返回的帖子信息、帖子作者信息依次将数据渲染到作者信息展示区域、帖子标题区域、帖子的基础信息区域和帖子详情区域。有一点需要注意,读取帖子详情时,使用的 th 标签为 th:utext,并不是 th:text。
3.测试效果
4.修改帖子
4.1 页面跳转
将请求转发到编辑页,因为要获取帖子详情所以需要根据一个字段来查询,这里就选择 id 作为传参了。在访问 /editPostPage/{postId} 时,会把帖子编辑页所需的帖子详情内容查询出来并转发到 edit 页面。
BBSPostController
/**
* 跳转至编辑页
* @param request
* @param postId
* @return
*/
@GetMapping("editPostPage/{postId}")
public String editPostPage(HttpServletRequest request, @PathVariable(value = "postId") Long postId) {
BBSUser bbsUser = (BBSUser) request.getSession().getAttribute(Constants.USER_SESSION_KEY);
List<BBSPostCategory> bbsPostCategories = bbsPostCategoryService.getBBSPostCategories();
if (CollectionUtils.isEmpty(bbsPostCategories)) {
return "error/error_404";
}
//将分类数据封装到request域中
request.setAttribute("bbsPostCategories", bbsPostCategories);
if (null == postId || postId < 0) {
return "error/error_404";
}
BBSPost bbsPost = bbsPostService.getBBSPostById(postId);
if (bbsPost == null) {
return "error/error_404";
}
if (!bbsUser.getUserId().equals(bbsPost.getPublishUserId())) {
request.setAttribute("message", "非本人发帖,无权限修改");
return "error/error";
}
request.setAttribute("bbsPost", bbsPost);
request.setAttribute("postId", postId);
return "jie/edit";
}
BBSPostService
/**
* 获取详情
*
* @param bbsPostId
* @return
*/
BBSPost getBBSPostById(Long bbsPostId);
@Override
public BBSPost getBBSPostById(Long bbsPostId) {
return bbsPostMapper.selectByPrimaryKey(bbsPostId);
}
BBSPostMapper层前面已经实现,这里就不再阐述了。
4.2 页面回显
在 templates/jie目录下新增 edit.html 的代码,该页面与新增帖子的页面 add.html 基本一致,唯一的区别就是会回显当前帖子的数据,将前一个请求携带的 bbsPost 对象进行读取并显示在编辑页面对应的 DOM 中即可。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="header::head-fragment('编辑帖子')"> </head>
<body>
<div th:replace="header::header-fragment"></div>
<div class="layui-container fly-marginTop">
<div class="fly-panel" pad20 style="padding-top: 5px;">
<!--<div class="fly-none">没有权限</div>-->
<div class="layui-form layui-form-pane">
<div class="layui-tab layui-tab-brief" lay-filter="user">
<ul class="layui-tab-title">
<li class="layui-this">编辑帖子</li>
</ul>
<div
class="layui-form layui-tab-content"
id="LAY_ucm"
style="padding: 20px 0;"
>
<div class="layui-tab-item layui-show">
<form
method="post"
id="postForm"
onsubmit="return false;"
action="##"
>
<div class="layui-row layui-col-space15 layui-form-item">
<input
type="hidden"
id="postId"
th:value="${bbsPost.postId}"
/>
<div class="layui-col-md6">
<label for="postTitle" class="layui-form-label"
>标题</label
>
<div class="layui-input-block">
<input
type="text"
id="postTitle"
name="postTitle"
required
autocomplete="off"
class="layui-input"
th:value="${bbsPost.postTitle}"
/>
</div>
</div>
<div class="layui-col-md6">
<label class="layui-form-label">所在专栏</label>
<div class="layui-input-block">
<select
name="class"
lay-filter="column"
id="postCategoryId"
>
<option value="0"></option>
<th:block th:unless="${null == bbsPostCategories}">
<th:block th:each="c : ${bbsPostCategories}">
<option
th:value="${c.categoryId}"
th:text="${c.categoryName}"
th:selected="${null !=c.categoryId and bbsPost.postCategoryId==c.categoryId} ?true:false"
>
提问
</option>
</th:block>
</th:block>
</select>
</div>
</div>
</div>
<div class="layui-form-item layui-form-text">
<div class="layui-input-block">
<div
id="wangEditor"
name="postContent"
required
placeholder="详细描述"
style="height: 260px;"
th:utext="${bbsPost.postContent}"
></div>
</div>
</div>
<div class="layui-form-item" style="margin-top: 56px;">
<label for="verifyCode" class="layui-form-label"
>验证码</label
>
<div class="layui-input-inline">
<input
type="text"
id="verifyCode"
name="verifyCode"
required
placeholder="验证码"
autocomplete="off"
class="layui-input"
/>
</div>
<div class="layui-form-mid">
<span style="color: #c00;"
><img
data-tooltip="看不清楚?换一张"
th:src="@{/common/captcha}"
onclick="this.src='/common/captcha?d='+new Date()*1"
alt="单击图片刷新!"
/></span>
</div>
</div>
<div class="layui-form-item">
<button
class="layui-btn"
lay-filter="*"
lay-submit
onclick="editBBSPost()"
>
立即发布
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="fly-footer">
<p>
My-BBS社区 2021 ©
<a href="#" target="_blank">picacho</a>
</p>
</div>
<script th:src="@{/js/public.js}"></script>
<script th:src="@{/layui/layui.js}"></script>
<!-- wangEditor -->
<script
type="text/javascript"
src="//unpkg.com/wangeditor/dist/wangEditor.min.js"
></script>
<script type="text/javascript">
layui.use(["layer", "element", "jquery", "form"], function () {
var layer = layui.layer,
$ = layui.$,
element = layui.element,
form = layui.form;
var editorD;
//富文本编辑器 用于商品详情编辑
const E = window.wangEditor;
editorD = new E("#wangEditor");
// 设置编辑区域高度为 260px
editorD.config.height = 260;
editorD.config.zIndex = 1;
//配置服务端图片上传地址
editorD.config.uploadImgServer = "/uploadFiles";
editorD.config.uploadFileName = "files";
//限制图片大小 2M
editorD.config.uploadImgMaxSize = 2 * 1024 * 1024;
//限制一次最多能传几张图片 一次最多上传 5 个图片
editorD.config.uploadImgMaxLength = 5;
//隐藏插入网络图片的功能
editorD.config.showLinkImg = false;
editorD.config.uploadImgHooks = {
// 图片上传并返回了结果,图片插入已成功
success: function (xhr) {
console.log("success", xhr);
},
// 图片上传并返回了结果,但图片插入时出错了
fail: function (xhr, editor, resData) {
console.log("fail", resData);
},
// 上传图片出错,一般为 http 请求的错误
error: function (xhr, editor, resData) {
console.log("error", xhr, resData);
},
// 上传图片超时
timeout: function (xhr) {
console.log("timeout");
},
customInsert: function (insertImgFn, result) {
if (result != null && result.resultCode == 200) {
// insertImgFn 可把图片插入到编辑器,传入图片 src ,执行函数即可
result.data.forEach((img) => {
insertImgFn(img);
});
} else {
alert("error");
}
},
};
editorD.create();
window.editBBSPost = function () {
var postId = $("#postId").val();
var postTitle = $("#postTitle").val();
if (isNull(postTitle)) {
layer.alert('请输入标题!', {title: '提醒', skin: 'layui-layer-molv', icon: 2});
return;
}
var verifyCode = $("#verifyCode").val();
if (!validLength(verifyCode, 5)) {
layer.alert('请输入正确的验证码!', {title: '提醒', skin: 'layui-layer-molv', icon: 2});
return;
}
var postCategoryId = $('#postCategoryId option:selected').val();
if (isNull(postCategoryId)) {
layer.alert('请选择分类!', {title: '提醒', skin: 'layui-layer-molv', icon: 2});
return;
}
var postContent = editorD.txt.html();
if (!validLength(postContent, 100000)) {
layer.alert('内容超出长度!', {title: '提醒', skin: 'layui-layer-molv', icon: 2});
return;
}
var url = '/editPost';
var data = {
"postId" : postId,
"postTitle": postTitle, "verifyCode": verifyCode,
"postCategoryId": postCategoryId, "postContent": postContent
};
$.ajax({
type: 'POST',//方法类型
url: url,
data: data,
success: function (result) {
if (result.resultCode == 200) {
window.location.href = '/detail/'+postId;
} else {
layer.msg(result.message);
}
;
},
error: function () {
layer.alert('操作失败!', {title: '提醒', skin: 'layui-layer-molv', icon: 2});
}
});
}
});
</script>
</body>
</html>
4.3 帖子修改接口实现
在 BBSPostController 中新增 editPost() 方法,接口的映射地址为 /editPost,请求方法为 POST。
首先会对所有的参数进行基本的校验,之后交给业务层代码进行操作,与添加接口不同的是传参,多了帖子的主键 id,我们需要知道要修改的哪一条数据。
/**
* 修改帖子
* @param postId
* @param postTitle
* @param postCategoryId
* @param postContent
* @param verifyCode
* @param httpSession
* @return
*/
@PostMapping("/editPost")
@ResponseBody
public Result editPost(@RequestParam("postId") Long postId,
@RequestParam("postTitle") String postTitle,
@RequestParam("postCategoryId") Integer postCategoryId,
@RequestParam("postContent") String postContent,
@RequestParam("verifyCode") String verifyCode,
HttpSession httpSession) {
BBSUser bbsUser = (BBSUser) httpSession.getAttribute(Constants.USER_SESSION_KEY);
if (null == postId || postId < 0) {
return ResultGenerator.genFailResult("postId参数错误");
}
BBSPost temp = bbsPostService.getBBSPostById(postId);
if (temp == null) {
return ResultGenerator.genFailResult("postId参数错误");
}
if (!bbsUser.getUserId().equals(temp.getPublishUserId())) {
return ResultGenerator.genFailResult("非本人发帖,无权限修改");
}
if (!StringUtils.hasLength(postTitle)) {
return ResultGenerator.genFailResult("postTitle参数错误");
}
if (null == postCategoryId || postCategoryId < 0) {
return ResultGenerator.genFailResult("postCategoryId参数错误");
}
BBSPostCategory bbsPostCategory = bbsPostCategoryService.selectById(postCategoryId);
if (null == bbsPostCategory) {
return ResultGenerator.genFailResult("postCategoryId参数错误");
}
if (!StringUtils.hasLength(postContent)) {
return ResultGenerator.genFailResult("postContent参数错误");
}
if (postTitle.trim().length() > 32) {
return ResultGenerator.genFailResult("标题过长");
}
if (postContent.trim().length() > 100000) {
return ResultGenerator.genFailResult("内容过长");
}
String kaptchaCode = httpSession.getAttribute(Constants.VERIFY_CODE_KEY) + "";
if (!StringUtils.hasLength(kaptchaCode) || !verifyCode.equals(kaptchaCode)) {
return ResultGenerator.genFailResult(ServiceResultEnum.LOGIN_VERIFY_CODE_ERROR.getResult());
}
temp.setPostTitle(postTitle);
temp.setPostContent(postContent);
temp.setPostCategoryId(postCategoryId);
temp.setPostCategoryName(bbsPostCategory.getCategoryName());
temp.setLastUpdateTime(new Date());
if (bbsPostService.updateBBSPost(temp) > 0) {
httpSession.removeAttribute(Constants.VERIFY_CODE_KEY);//清空session中的验证码信息
return ResultGenerator.genSuccessResult();
} else {
return ResultGenerator.genFailResult("请求失败,请检查参数及账号是否有操作权限");
}
}
BBSPostService
/**
* 修改帖子
*
* @param bbsPost
* @return
*/
int updateBBSPost(BBSPost bbsPost);
@Override
public int updateBBSPost(BBSPost bbsPost) {
BBSUser bbsUser = bbsUserMapper.selectByPrimaryKey(bbsPost.getPublishUserId());
if (bbsUser == null || bbsUser.getUserStatus().intValue() == 1) {
//账号已被封禁
return 0;
}
BBSPostCategory bbsPostCategory = bbsPostCategoryMapper.selectByPrimaryKey(bbsPost.getPostCategoryId());
if (bbsPostCategory == null) {
//分类数据错误
return 0;
}
return bbsPostMapper.updateByPrimaryKeySelective(bbsPost);
}
BBSPostMapper数据持久层这里就不再阐述了。
帖子修改接口的实现方法实现的步骤如下:
- 根据 postId 查询帖子信息
- 如果帖子信息不存在则返回错误信息
- 如果帖子存在则判断帖子实体中的 publicUserId 是否与当前登录用户的 publicUserId 相等
- 如果不相等则返回错误信息,不允许修改别人的帖子
- 基本的参数验证,不符合条件的参数不允许修改
- 验证当前登录用户的状态是否正常,如果已被封禁也不允许修改
- 之后封装帖子数据并进行入库操作,修改当前的帖子数据
- 最后将修改结果返回给上层调用方法
4.3 测试效果
项目源码下载地址:项目源码