开发个人博客的那些事 下
-
编辑博客内容使用Markdown编辑风格
-
编辑时使用Markdown编辑风格的方法
-
首先需要引入使用Markdown编辑风格所需要的资源。
<!--css--> <link rel="stylesheet" href="../../static/editormd/css/editormd.min.css" th:href="@{/editormd/css/editormd.min.css}"> <!--js--> <script src="../../static/editormd/editormd.min.js" th:src="@{/editormd/editormd.min.js}"></script>
-
在script标签域初始化Markdown编辑器
<script> //初始化Markdown编辑器 var contentEditor; $(function() { contentEditor = editormd("md-content", { width: "100%", height: 640, syncScrolling: "single", //path: "../../static/editormd/lib/", //注意文件夹要放到resources资源包下的static包下 path: "/editormd/lib/" }); }); </script>
-
在需要的使用的地方加入下面代码即可
<body> <div class="required field"> <div id="md-content" style="z-index: 1!important;"> <textarea placeholder="博客内容" name="content" th:text="*{content}"> </textarea> </div> </div> </body>
-
-
详情页显示时使用Markdown编辑风格的方法
-
需要使用到Markdown工具类
package com.fmxu.util; import org.commonmark.Extension; import org.commonmark.ext.gfm.tables.TableBlock; import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.ext.heading.anchor.HeadingAnchorExtension; import org.commonmark.node.Link; import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.AttributeProvider; import org.commonmark.renderer.html.AttributeProviderContext; import org.commonmark.renderer.html.AttributeProviderFactory; import org.commonmark.renderer.html.HtmlRenderer; import java.util.*; /** * @Auther: Phoenix * @Date: 2021/03/17 20:07 * @Description: markdown编辑器工具类 */ public class MarkdownUtils { /** * markdown格式转换成HTML格式 * @param markdown * @return */ public static String markdownToHtml(String markdown) { Parser parser = Parser.builder().build(); Node document = parser.parse(markdown); HtmlRenderer renderer = HtmlRenderer.builder().build(); return renderer.render(document); } /** * 增加扩展[标题锚点,表格生成] * Markdown转换成HTML * @param markdown * @return */ public static String markdownToHtmlExtensions(String markdown) { //h标题生成id Set<Extension> headingAnchorExtensions = Collections.singleton(HeadingAnchorExtension.create()); //转换table的HTML List<Extension> tableExtension = Arrays.asList(TablesExtension.create()); Parser parser = Parser.builder() .extensions(tableExtension) .build(); Node document = parser.parse(markdown); HtmlRenderer renderer = HtmlRenderer.builder() .extensions(headingAnchorExtensions) .extensions(tableExtension) .attributeProviderFactory(new AttributeProviderFactory() { public AttributeProvider create(AttributeProviderContext context) { return new CustomAttributeProvider(); } }) .build(); return renderer.render(document); } /** * 处理标签的属性 */ static class CustomAttributeProvider implements AttributeProvider { @Override public void setAttributes(Node node, String tagName, Map<String, String> attributes) { //改变a标签的target属性为_blank if (node instanceof Link) { attributes.put("target", "_blank"); } if (node instanceof TableBlock) { attributes.put("class", "ui celled table"); } } } }
-
在获取到博客信息时,调用工具类对于博客正文内容渲染
//定义的sevice层实现方法 @Override public Blog getAndConvert(Long id) { Blog blog=blogRepository.getOne(id); blog.setViews(blog.getViews()+1); blogRepository.save(blog); if(blog==null){ throw new NotFoundException("该博客不存在"); } Blog blog1=new Blog(); BeanUtils.copyProperties(blog,blog1); String content=blog.getContent(); //此处调用Markdown工具类方法渲染博客正文 blog1.setContent(MarkdownUtils.markdownToHtmlExtensions(content)); return blog1; }
-
-
-
评论功能
-
评论实体类的设计
@Entity @Table(name = "t_comment") public class Comment { @Id @GeneratedValue private Long id; //昵称 private String nickname; //邮箱 private String email; //内容 private String content; //头像 private String avatar; //创立时间 @Temporal(TemporalType.TIMESTAMP) private Date createTime; @ManyToOne private Blog blog; //需要自关联 //一个父节点评论可能有多个子节点评论 @OneToMany(mappedBy = "parentComment") private List<Comment> replyComments=new ArrayList<>(); //多个子节点评论对应于一个父节点评论 @ManyToOne private Comment parentComment; //判断是否为博主的评论 private Boolean adminComment; ... }
-
service接口
package com.fmxu.service; import com.fmxu.po.Comment; import java.util.List; /** * @author Dell * @projectName blog * @description: 评论接口 * @date 2021/3/19/10:02 */ public interface CommentService { //根据博客id查询博客评论信息 List<Comment> listCommentByBlogId(Long blogId); //保存评论信息 Comment saveComment(Comment comment); }
-
service实现类中方法的实现
package com.fmxu.service; import com.fmxu.dao.CommentRepository; import com.fmxu.po.Comment; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * @author Dell * @projectName blog * @description: 评论接口实现类 * @date 2021/3/19/10:02 */ @Service public class CommentServiceImpl implements CommentService { @Autowired private CommentRepository commentRepository; //根据 @Override public List<Comment> listCommentByBlogId(Long blogId) { //根据评论的创立时间逆序排序 Sort sort=Sort.by(Sort.Direction.DESC,"createTime"); //找到评论信息中parentComment属性值为null的,说明它是顶级评论,即是评论的根节点 List<Comment> comments=commentRepository.findByBlogIdAndParentCommentNull(blogId,sort); //调用eachComment方法去查询父评论的所有子评论,并按序排列放入列表 return eachComment(comments); } @Override public Comment saveComment(Comment comment) { Long parentCommentId=comment.getParentComment().getId(); //判断是否有父评论,即判断是新添加评论还是回复评论 if(parentCommentId!=-1){ comment.setParentComment(commentRepository.getOne(parentCommentId)); }else { comment.setParentComment(null); } comment.setCreateTime(new Date()); return commentRepository.save(comment); } /** * 循环每个顶级的评论节点 * @param comments * @return */ private List<Comment> eachComment(List<Comment> comments) { List<Comment> commentsView = new ArrayList<>(); for (Comment comment : comments) { Comment c = new Comment(); BeanUtils.copyProperties(comment,c); commentsView.add(c); } //合并评论的各层子代到第一级子代集合中 combineChildren(commentsView); return commentsView; } /** * * @param comments root根节点,blog不为空的对象集合 * @return */ private void combineChildren(List<Comment> comments) { for (Comment comment : comments) { List<Comment> replys1 = comment.getReplyComments(); for(Comment reply1 : replys1) { //循环迭代,找出子代,存放在tempReplys中 recursively(reply1); } //修改顶级节点的reply集合为迭代处理后的集合 comment.setReplyComments(tempReplys); //清除临时存放区 tempReplys = new ArrayList<>(); } } //存放迭代找出的所有子代的集合 private List<Comment> tempReplys = new ArrayList<>(); /** * 递归迭代,剥洋葱 * @param comment 被迭代的对象 * @return */ private void recursively(Comment comment) { tempReplys.add(comment);//顶节点添加到临时存放集合 if (comment.getReplyComments().size()>0) { List<Comment> replys = comment.getReplyComments(); for (Comment reply : replys) { if (reply.getReplyComments().size()>0) { recursively(reply); }else { tempReplys.add(reply); } } } } }
-
Controller类的设计
package com.fmxu.web; import com.fmxu.po.Blog; import com.fmxu.po.Comment; import com.fmxu.po.User; import com.fmxu.service.BlogService; import com.fmxu.service.CommentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import sun.dc.pr.PRError; import javax.servlet.http.HttpSession; /** * @author Dell * @projectName blog * @description: * @date 2021/3/19/10:39 */ @Controller public class CommentController { @Autowired private CommentService commentService; @Autowired private BlogService blogService; @Value("${comment.avatar}") private String avatar; //根据博客id查询对应的评论信息 @GetMapping("/comments/{blogId}") public String comments(@PathVariable Long blogId, Model model){ model.addAttribute("comments",commentService.listCommentByBlogId(blogId)); //返回局部区域,只刷新当前区域 return "blog :: commentList"; } //保存评论信息 @PostMapping("/comments") public String post(Comment comment, HttpSession session){ Long blogId=comment.getBlog().getId(); comment.setBlog(blogService.getBlog(blogId)); //判断session域中是否有user,即判断是否为博主 User user= (User) session.getAttribute("user"); if(user!=null){ comment.setAvatar(user.getAvatar()); comment.setEmail(user.getEmail()); comment.setNickname(user.getNickname()); comment.setAdminComment(true); }else { comment.setAvatar(avatar); } commentService.saveComment(comment); return "redirect:/comments/"+blogId; } }
-
前端页面
<!--评论区域--> <div class="ui bottom attached segment" th:if="${blog.commentabled}"> <div id="comment-container" class="ui teal segment"> <div th:fragment="commentList"> <div class="ui threaded comments" style="max-width: 100%"> <h3 class="ui dividing header">评论</h3> <div class="comment" th:each="comment :${comments}"> <a class="avatar"> <img src="https://unsplash.it/800/450?image=1005" th:src="@{${comment.avatar}}"> </a> <div class="content"> <a class="author" > <span th:text="${comment.nickname}">Matt</span> <div class="ui mini basic teal left pointing label m-padded-mini" th:if="${comment.adminComment}">博主</div> </a> <div class="metadata"> <span class="date" th:text="${#dates.format(comment.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span> </div> <div class="text" th:text="${comment.content}"> How artistic! </div> <div class="actions"> <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${comment.id},data-commentnickname=${comment.nickname}" onclick="reply(this)">回复</a> </div> </div> <div class="comments" th:if="${#arrays.length(comment.replyComments)}>0"> <div class="comment" th:each="reply : ${comment.replyComments}"> <a class="avatar"> <img src="https://unsplash.it/100/100?image=1005" th:src="@{${reply.avatar}}"> </a> <div class="content"> <a class="author" > <span th:text="${reply.nickname}">小红</span> <div class="ui mini basic teal left pointing label m-padded-mini" th:if="${reply.adminComment}">博主</div> <span th:text="|@ ${reply.parentComment.nickname}|" style="color: #1abc9c">@ 小白</span> </a> <div class="metadata"> <span class="date" th:text="${#dates.format(reply.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span> </div> <div class="text" th:text="${reply.content}"> How artistic! </div> <div class="actions"> <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${reply.id},data-commentnickname=${reply.nickname}" onclick="reply(this)">回复</a> </div> </div> </div> </div> </div> </div> </div> </div> <div id="comment-form" class="ui form"> <input type="hidden" name="blog.id" th:value="${blog.id}"> <input type="hidden" name="parentComment.id" value="-1"> <div class="field"> <textarea name="content" placeholder="请输入评论信息..."></textarea> </div> <div class="fields"> <div class="field m-mobile-wide m-margin-bottom-small"> <div class="ui left icon input"> <i class="user icon"></i> <input type="text" name="nickname" placeholder="姓名" th:value="${session.user}!=null ? ${session.user.nickname}"> </div> </div> <div class="field m-mobile-wide m-margin-bottom-small"> <div class="ui left icon input"> <i class="mail icon"></i> <input type="text" name="email" placeholder="邮箱" th:value="${session.user}!=null ? ${session.user.email}"> </div> </div> <div class="field m-mobile-wide m-margin-bottom-small"> <button id="commentpost-btn" type="button" class="ui teal button"><i class="ui edit icon"></i>发布</button> </div> </div> </div> </div>
-
JS代码
<script th:inline="javascript"> //表单校验 $('.ui.form').form({ fields:{ content:{ identifier:'content', rules:[{ type:'empty', prompt:'内容:请输入您的评论内容', }] }, nickname:{ identifier:'nickname', rules:[{ type:'empty', prompt:'内容:请输入您的昵称', }] }, email:{ identifier:'email', rules:[{ type: 'email', prompt:'内容:请输入您的邮箱', }] }, } }); $('#commentpost-btn').click(function () { var boo= $('.ui.form').form('validate form'); if(boo){ console.log("校验成功") postData(); }else { console.log("校验失败") } }); //向后端发送请求,传递数据 function postData() { $("#comment-container").load(/*[[@{/comments}]]*/"",{ "parentComment.id" :$("[name='parentComment.id']").val(), "blog.id" :$("[name='blog.id']").val(), "nickname" :$("[name='nickname']").val(), "email" :$("[name='email']").val(), "content" :$("[name='content']").val(), },function (responseTxt,statusTxt,xhr) { $(window).scrollTo($('#comment-container'),500); clearContent(); }); } //清空表单 function clearContent() { $("[name='content']").val(''); $("[name='parentComment.id']").val(-1); $("[name='content']").attr("placeholder", "请输入评论信息..."); }; //评论回复 function reply(obj) { //获取到回复按钮传递过来的值,即被回复者的id和昵称 var commentId = $(obj).data('commentid'); var commentNickname = $(obj).data('commentnickname'); $("[name='content']").attr("placeholder", "@"+commentNickname).focus(); $("[name='parentComment.id']").val(commentId); $(window).scrollTo($('#comment-form'),500); } </script>
-