• (Vue+SpringBoot+elementUi+WangEditer)仿论坛项目


    项目使用到的技术与库

    1.前端 Vue2 elementUi Cookie WangEditer

    2.后端 SpringBoot Mybatis-Plus

    3.数据库 MySql

    一、效果展示

    1.1主页效果:

    1.2 文章编辑页面:

    1.3 成功发布文章

    1.4 文章关键字搜索提示

     1.5 文章查询结果展示

    1.6 文章内容及交互展示


    二、表单设计的sql

    用户:

    1. create table paitool.user
    2. (
    3. id int auto_increment
    4. primary key,
    5. account varchar(255) not null,
    6. password varchar(255) not null,
    7. phone varchar(20) null,
    8. address varchar(255) null,
    9. isVip tinyint(1) default 0 null,
    10. email varchar(255) null,
    11. registration_date datetime default CURRENT_TIMESTAMP null,
    12. last_login datetime null,
    13. status enum ('active', 'inactive') default 'active' null,
    14. constraint account_UNIQUE
    15. unique (account),
    16. constraint email_UNIQUE
    17. unique (email),
    18. constraint phone_UNIQUE
    19. unique (phone)
    20. );

    文章:

    1. create table paitool.forum_posts
    2. (
    3. id int auto_increment
    4. primary key,
    5. title varchar(255) not null,
    6. content text not null,
    7. author_id int not null,
    8. created_at timestamp default CURRENT_TIMESTAMP null,
    9. updated_at timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
    10. heat_value int default 0 null,
    11. rating decimal(3, 2) default 0.00 null,
    12. tag varchar(10) default '其它' null,
    13. constraint forum_posts_ibfk_1
    14. foreign key (author_id) references paitool.user (id)
    15. );

    文章交互表-点赞:

    1. create table paitool.forum_post_likes
    2. (
    3. user_id int not null,
    4. post_id int not null,
    5. primary key (user_id, post_id),
    6. constraint forum_post_likes_ibfk_1
    7. foreign key (user_id) references paitool.user (id),
    8. constraint forum_post_likes_ibfk_2
    9. foreign key (post_id) references paitool.forum_posts (id)
    10. );

    文章交互表-收藏:

    1. create table paitool.forum_post_favorites
    2. (
    3. user_id int not null,
    4. post_id int not null,
    5. primary key (user_id, post_id),
    6. constraint forum_post_favorites_ibfk_1
    7. foreign key (user_id) references paitool.user (id),
    8. constraint forum_post_favorites_ibfk_2
    9. foreign key (post_id) references paitool.forum_posts (id)
    10. );

    文章交互表-评论

    1. create table paitool.forum_comments
    2. (
    3. id int auto_increment
    4. primary key,
    5. post_id int not null,
    6. user_id int not null,
    7. comment_text text not null,
    8. created_at timestamp default CURRENT_TIMESTAMP null,
    9. constraint forum_comments_ibfk_1
    10. foreign key (user_id) references paitool.user (id),
    11. constraint forum_comments_ibfk_2
    12. foreign key (post_id) references paitool.forum_posts (id)
    13. );

    三、前端代码

    3.1 论坛主页

    Html:

    1. <template>
    2. <div id="forumLayOut">
    3. <div id="Top" style="background-color: rgb(250, 250, 250); padding-top: 20px">
    4. <div id="serchBorder" style="padding-bottom: 13px;">
    5. <el-autocomplete v-model="searchKeyWord" :fetch-suggestions="querySearchAsync" :trigger-on-focus="false"
    6. placeholder="请输入关键字" style="width: 300px;" @select="handleSelect">
    7. el-autocomplete>
    8. <el-button type="primary" @click="onSubmit">查询el-button>
    9. div>
    10. <div>
    11. <div style="margin-bottom: 15px;">
    12. <el-checkbox-group v-model="checkboxGroup1" :max="1">
    13. <el-checkbox-button v-for="city in cities" :label="city" :key="city">{{ city
    14. }}el-checkbox-button>
    15. el-checkbox-group>
    16. div>
    17. div>
    18. <div style="height: 380px; width: 100%;">
    19. <div class="block" style="width: 30%; float: left; margin-left: 5%; height: 400px;">
    20. <el-carousel height="350px" style="width: 100%; ">
    21. <el-carousel-item v-for="item in 4" :key="item">
    22. <img src="https://img95.699pic.com/photo/50035/3211.jpg_wh860.jpg" alt="风景测试">
    23. <h3 class="small">{{ item }}h3>
    24. el-carousel-item>
    25. el-carousel>
    26. div>
    27. <div style=" height: 350px; background-color: rgb(250, 250, 250); width: 25%; float: left;
    28. border: 1px solid rgb(240, 240, 242); margin-left: 3%;">
    29. <div style="height: 50px; width: 100%; background-color: rgb(245, 245, 245); ">
    30. <i class="el-icon-share">i>
    31. <div><b>热门b>div>
    32. <hr>
    33. div>
    34. <div class="link-container">
    35. <a href="#" class="link" id="TurnLink">杀死谷歌,成为AI时代的搜索皇帝!a>
    36. <p style="color: gray;">Perplexity CEO 最新四万字访谈p>
    37. div>
    38. <div class="link-container">
    39. <a href="#" class="link" id="TurnLink">重写系统后痛批:这门语言烂透了!a>
    40. <p style="color: gray;">耗时18个月,开发者弃TypeScript投Rustp>
    41. div>
    42. <div class="link-container">
    43. <a href="#" class="link" id="TurnLink">Shire 编码智能体语言a>
    44. <p style="color: gray;">打造你的专属 AI 编程助手p>
    45. div>
    46. div>
    47. <div style="float: left; margin-left: 3%; height: 350px; background-color: rgb(250, 250, 250); width: 25%; float: left;
    48. border: 1px solid rgb(240, 240, 242); ">
    49. <div style="height: 50px; width: 100%; background-color: rgb(245, 245, 245); ">
    50. <i class="el-icon-message-solid">i>
    51. <div><b>头条b>div>
    52. <hr>
    53. div>
    54. <div class="link-container">
    55. <a href="#" class="link" id="TurnLink">史上开发最久的游戏!a>
    56. <p style="color: gray;">耗时 22 年,5 名打工人凑了几百欧就开工,只剩 1 人坚守到发布...p>
    57. div>
    58. <div class="link-container">
    59. <a href="#" class="link" id="TurnLink">实习期间创下 Transformera>
    60. <p style="color: gray;">他说:当年整个 AI 圈都无法预见我们今天的高度p>
    61. div>
    62. <div class="link-container">
    63. <a href="#" class="link" id="TurnLink">杀死谷歌,成为AI时代的搜索皇帝!a>
    64. <p style="color: gray;">Perplexity CEO 最新四万字访谈p>
    65. div>
    66. div>
    67. div>
    68. div>
    69. <el-divider>el-divider>
    70. <div id="bottom">
    71. <el-tabs v-model="activeName" @tab-click="handleClick" style="padding-left: 2em; ">
    72. <el-tab-pane label="我的文章" name="first">
    73. <div class="parent-div" style="min-height: 500px">
    74. <div v-if="posts.length === 0">
    75. <el-empty :image-size="200">el-empty>
    76. div>
    77. <div class="custom-card" v-for="(post, index) in posts" :key="index"
    78. @click="getForumPostDetail(post.id)">
    79. <div class="card-content">
    80. <h1 class="card-title">标题: {{ post.title }}h1>
    81. <div class="card-meta">
    82. <span>作者: {{ post.account }}span>
    83. <span>标签: {{ post.tag }}span>
    84. <span><i class="el-icon-view">{{ post.heatValue }}i>span>
    85. div>
    86. <div class="card-rating">
    87. <span>文章评分:span>
    88. <el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
    89. score-template="{value}" style="display: inline-block;">el-rate>
    90. div>
    91. div>
    92. div>
    93. div>
    94. el-tab-pane>
    95. <el-tab-pane label="推荐文章" name="second">
    96. <div v-if="posts.length === 0">
    97. <el-empty :image-size="200">el-empty>
    98. div>
    99. <div class="custom-card" v-for="(post, index) in posts" :key="index"
    100. @click="getForumPostDetail(post.postId)">
    101. <div class="card-content">
    102. <h1 class="card-title">标题: {{ post.title }}h1>
    103. <div class="card-meta">
    104. <span>作者: {{ post.account }}span>
    105. <span>标签: {{ post.tag }}span>
    106. <span><i class="el-icon-view">{{ post.heat_value }}i>span>
    107. div>
    108. <div class="card-rating">
    109. <span>文章评分:span>
    110. <el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
    111. score-template="{value}" style="display: inline-block;">el-rate>
    112. div>
    113. div>
    114. div>
    115. el-tab-pane>
    116. <el-tab-pane label="热门文章" name="third">
    117. <div class="parent-div" style="min-height: 500px">
    118. <div v-if="posts.length === 0">
    119. <el-empty :image-size="200">el-empty>
    120. div>
    121. <div class="custom-card" v-for="(post, index) in posts" :key="index"
    122. @click="getForumPostDetail(post.id)">
    123. <div class="card-content">
    124. <h1 class="card-title">标题: {{ post.title }}h1>
    125. <div class="card-meta">
    126. <span>作者: {{ post.account }}span>
    127. <span>标签: {{ post.tag }}span>
    128. <span><i class="el-icon-view">{{ post.heatValue }}i>span>
    129. div>
    130. <div class="card-rating">
    131. <span>文章评分:span>
    132. <el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
    133. score-template="{value}" style="display: inline-block;">el-rate>
    134. div>
    135. div>
    136. div>
    137. div>
    138. el-tab-pane>
    139. <el-tab-pane label="优质文章" name="fourth">
    140. <div class="parent-div" style="min-height: 500px">
    141. <div v-if="posts.length === 0">
    142. <el-empty :image-size="200">el-empty>
    143. div>
    144. <div class="custom-card" v-for="(post, index) in posts" :key="index"
    145. @click="getForumPostDetail(post.id)">
    146. <div class="card-content">
    147. <h1 class="card-title">标题: {{ post.title }}h1>
    148. <div class="card-meta">
    149. <span>作者: {{ post.account }}span>
    150. <span>标签: {{ post.tag }}span>
    151. <span><i class="el-icon-view">{{ post.heatValue }}i>span>
    152. div>
    153. <div class="card-rating">
    154. <span>文章评分:span>
    155. <el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
    156. score-template="{value}" style="display: inline-block;">el-rate>
    157. div>
    158. div>
    159. div>
    160. div>
    161. el-tab-pane>
    162. <el-tab-pane label="我的收藏" name="fifth">
    163. <div v-if="posts.length === 0">
    164. <el-empty :image-size="200">el-empty>
    165. div>
    166. <div class="parent-div" style="min-height: 500px">
    167. <div v-if="posts.length === 0">
    168. <el-empty :image-size="200">el-empty>
    169. div>
    170. <div class="custom-card" v-for="(post, index) in posts" :key="index"
    171. @click="getForumPostDetail(post.id)">
    172. <div class="card-content">
    173. <h1 class="card-title">标题: {{ post.title }}h1>
    174. <div class="card-meta">
    175. <span>作者: {{ post.account }}span>
    176. <span>标签: {{ post.tag }}span>
    177. <span><i class="el-icon-view">{{ post.heatValue }}i>span>
    178. div>
    179. <div class="card-rating">
    180. <span>文章评分:span>
    181. <el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
    182. score-template="{value}" style="display: inline-block;">el-rate>
    183. div>
    184. div>
    185. div>
    186. div>
    187. el-tab-pane>
    188. el-tabs>
    189. <el-button type="warning" round id="iWantPost" @click="navigateToPostEdit">我要发布文章el-button>
    190. div>
    191. div>
    192. template>

    js:

    css:


     

    3.2 发布文章页面

    1. <template>
    2. <div style="border: 1px solid #ccc; line-height: normal; height: 100%;">
    3. <div>
    4. <el-form :inline="true" :model="formInline" class="demo-form-inline">
    5. <el-form-item label="文章标题">
    6. <el-input v-model="formInline.title" placeholder="请输入文章标题" maxlength="20">el-input>
    7. el-form-item>
    8. <el-form-item label="类别">
    9. <el-select v-model="formInline.category" placeholder="请选择文章类别">
    10. <el-option label="新闻报道" value="news">el-option>
    11. <el-option label="科技动态" value="technology">el-option>
    12. <el-option label="生活时尚" value="lifestyle">el-option>
    13. <el-option label="教育学习" value="education">el-option>
    14. <el-option label="健康养生" value="health">el-option>
    15. el-select>
    16. el-form-item>
    17. <el-form-item>
    18. <el-button type="primary" @click="onSubmit"
    19. v-loading.fullscreen.lock="fullscreenLoading">提交el-button>
    20. el-form-item>
    21. el-form>
    22. div>
    23. <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" />
    24. <Editor style="height: 500px; overflow-y: hidden; height: 100%;" v-model="html" :defaultConfig="editorConfig"
    25. :mode="mode" @onCreated="onCreated" />
    26. div>
    27. template>
    28. <script>
    29. import Vue from 'vue'
    30. import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
    31. import axios from 'axios'
    32. import Cookies from 'js-cookie'
    33. export default Vue.extend({
    34. components: { Editor, Toolbar },
    35. data() {
    36. return {
    37. editor: null,
    38. html: ' ',
    39. toolbarConfig: {},
    40. editorConfig: { placeholder: '请输入内容...' },
    41. mode: 'default', // or 'simple'
    42. formInline: {
    43. title: '',
    44. category: ''
    45. },
    46. fullscreenLoading: false
    47. }
    48. },
    49. methods: {
    50. onCreated(editor) {
    51. this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
    52. },
    53. onSubmit() {
    54. this.fullscreenLoading = true;
    55. const userId = Cookies.get('userId'); // 获取并转换userId
    56. axios.post('/api/forum/add', {
    57. "title": this.formInline.title,
    58. "content": this.editor.getHtml(),
    59. "authorId": userId,
    60. "tag": this.formInline.category,
    61. }).then((response) => {
    62. console.log(response.data);
    63. this.fullscreenLoading = false;
    64. this.$router.push({ name: 'ForumSucessPostView' });
    65. }).catch(error => {
    66. console.error(error);
    67. this.fullscreenLoading = false;
    68. });
    69. },
    70. },
    71. mounted() {
    72. },
    73. beforeDestroy() {
    74. const editor = this.editor
    75. if (editor == null) return
    76. editor.destroy() // 组件销毁时,及时销毁编辑器
    77. },
    78. })
    79. script>
    80. <style src="@wangeditor/editor/dist/css/style.css">style>

    3.3 文章发布成功页面


    3.4 查看文章页面

    html:

    1. <template>
    2. <div style="line-height: normal; background-color: rgb(246, 247, 249); height: auto; min-height: 80%;">
    3. <div style="padding-top: 10px; width: auto; min-width: 40%;">
    4. <div id="Infor" style="background-color: white;">
    5. <h1 style="font-size: 28px; text-align: center">文章标题:{{title}}h1>
    6. <span>创作者:{{author}}span>
    7. <span style="margin-left: 20px;">创作日期:{{createAt}}span>
    8. <span style="margin-left: 20px;"><i class="el-icon-view">{{heatValue}}i>span>
    9. div>
    10. <el-divider><i class="el-icon-mobile-phone">i>el-divider>
    11. <div id="contentDisplay">
    12. <div v-html="content"
    13. style="padding-left: 2em; padding-top: 15px; padding-right: 2em; padding-bottom: 30px;">div>
    14. div>
    15. div>
    16. <el-divider><i class="el-icon-edit">i>el-divider>
    17. <div id="buttom">
    18. <el-button type="warning" round @click="getBackToForum">返回到论坛el-button>
    19. <el-button type="warning" icon="el-icon-star-off" circle @click="PostFavorite">el-button>
    20. <el-button type="danger" icon="el-icon-thumb" circle @click="PostLike">el-button>
    21. div>
    22. <div id="commentListShow">
    23. <el-card class="box-card">
    24. <div slot="header" class="clearfix">
    25. <span>评论详情span>
    26. div>
    27. <div id="commentInputArea">
    28. <el-input type="textarea" placeholder="请您输入友善的评论吧" v-model="textarea" maxlength="300"
    29. show-word-limit id="inputFrame" :clearable="clearAble" resize="none">
    30. el-input>
    31. <div style="margin-top: 10px; padding-bottom: 50px;">
    32. <el-button type="primary" @click="SubmitComment">发表评论el-button>
    33. <el-button type="primary" @click="CancelComment">取消评论el-button>
    34. div>
    35. div>
    36. <div id="commentList">
    37. <div class="comment-card" v-for="comment in comments" :key="comment.id">
    38. <div class="comment-head">
    39. <h1 class="username">{{ comment.account }}h1>
    40. <p class="created-at">发表于:{{ comment.createdAt }}p>
    41. div>
    42. <el-divider>el-divider>
    43. <p class="comment-text">{{ comment.commentText }}p>
    44. div>
    45. div>
    46. el-card>
    47. div>
    48. div>
    49. template>

    script:

    1. <script>
    2. import axios from 'axios';
    3. import Cookies from 'js-cookie';
    4. export default{
    5. data() {
    6. return {
    7. postId: '',
    8. title: '',
    9. content: '',
    10. value1: null,
    11. textarea: '',
    12. userId:'',
    13. clearAble: true,
    14. comments:{},
    15. author:'',
    16. createAt:'',
    17. heatValue:'',
    18. }
    19. },
    20. created() {
    21. this.postId = this.$route.params.postId;
    22. this.fetchPostDetail(this.$route.params.postId);
    23. this.userId = Cookies.get('userId');
    24. },
    25. mounted() {
    26. this.readComment();
    27. },
    28. methods: {
    29. // 前端实现路径传参
    30. async fetchPostDetail(postId) {
    31. try {
    32. this.fullscreenLoading = true;
    33. const url = `/api/forum/post/${postId}`;
    34. // 发起GET请求
    35. const response = await axios.get(url);
    36. if (response.status === 200) {
    37. // 请求成功,处理响应数据
    38. const postData = response.data;
    39. console.log('文章详情:', postData);
    40. // 更新组件状态或执行其他操作
    41. this.title = response.data.data.title;
    42. this.content = response.data.data.content;
    43. this.author = response.data.data.account;
    44. this.createAt = response.data.data.createdAt;
    45. this.heatValue = response.data.data.heatValue;
    46. this.fullscreenLoading = false;
    47. } else {
    48. console.error('请求失败,状态码:', response.status);
    49. }
    50. } catch (error) {
    51. console.error('请求错误:', error);
    52. }
    53. },
    54. getBackToForum() {
    55. this.$router.push({ name: 'forum' });
    56. },
    57. // 取消评论
    58. CancelComment(){
    59. this.textarea = '';
    60. },
    61. // 执行点赞按钮
    62. PostLike(){
    63. this.isLogin();
    64. axios.get('/api/forum/like',{
    65. params:{
    66. postId : this.postId,
    67. userId : this.userId
    68. }
    69. }).then((response)=>{
    70. this.MessageNotify(response);
    71. })
    72. },
    73. // 执行收藏按钮
    74. PostFavorite(){
    75. this.isLogin();
    76. axios.get('/api/forum/favorite',{
    77. params:{
    78. postId : this.postId,
    79. userId : this.userId
    80. }
    81. }).then((response)=>{
    82. this.MessageNotify(response);
    83. })
    84. },
    85. SubmitComment(){
    86. this.isLogin();
    87. axios.post('/api/forum/writeComment',{
    88. postId : this.postId,
    89. userId : this.userId,
    90. commentText : this.textarea
    91. }).then((response)=>{
    92. this.MessageNotify(response);
    93. this.textarea = '';
    94. this.readComment();
    95. })
    96. console.log("submit");
    97. },
    98. isLogin(){
    99. if(Cookies.get('userId') == null){
    100. this.$message.error('请先登录');
    101. return;
    102. }
    103. },
    104. // 消息提醒
    105. MessageNotify(response){
    106. if(response.data.code == 200){
    107. this.$message.success(response.data.data);
    108. }else{
    109. console.log(response.data);
    110. this.$message.error(response.data.message);
    111. }
    112. },
    113. readComment(){
    114. axios.get('/api/forum/getComment',{
    115. params:{
    116. postId : this.postId
    117. }
    118. }).then((response)=>{
    119. this.comments = response.data.data;
    120. })
    121. },
    122. }
    123. }
    124. script>

    css:


    3.5 文章搜索页面

    html:

    1. <template>
    2. <div id="layout">
    3. <div id="searchFrame">
    4. <div id="InputFrame">
    5. <el-input type="textarea" placeholder="请输入内容" v-model="textarea" rows="1" resize="none"
    6. style="font-size: larger; width: 80%;">
    7. el-input>
    8. <el-button type="warning" @click="SearchSubmit" icon="el-icon-search">查询el-button>
    9. div>
    10. div>
    11. <div id="excess">
    12. <div id="Interate">
    13. <i class="el-icon-search"> 搜索结果i>
    14. div>
    15. div>
    16. <div id="SearchContent">
    17. <div v-if="posts.length === 0">
    18. <el-empty :image-size="200">el-empty>
    19. div>
    20. <div class="custom-card" v-for="(post, index) in posts" :key="index" @click="getForumPostDetail(post.id)">
    21. <div class="card-content">
    22. <h1 class="card-title">标题: {{ post.title }}h1>
    23. <div class="card-meta">
    24. <span>作者: {{ post.account }}span>
    25. <span>标签: {{ post.tag }}span>
    26. <span><i class="el-icon-view">{{ post.heatValue }}i>span>
    27. div>
    28. <div class="card-rating">
    29. <span>文章评分:span>
    30. <el-rate v-model="post.rating" disabled show-score text-color="#ff9900" score-template="{value}"
    31. style="display: inline-block;">el-rate>
    32. div>
    33. div>
    34. div>
    35. div>
    36. div>
    37. template>

    script:

    1. <script>
    2. import axios from 'axios';
    3. export default {
    4. data() {
    5. return {
    6. textarea: '',
    7. searchKeyWord: '',
    8. posts: [],
    9. }
    10. },
    11. methods: {
    12. SearchSubmit() {
    13. console.log(this.textarea);
    14. axios.get('/api/forum/search', {
    15. params: {
    16. searchKeyWord: this.textarea
    17. }
    18. }).then((response) => {
    19. if (response.data.code !== 200) {
    20. this.$notify({
    21. title: '警告',
    22. message: '搜索失败',
    23. type: 'warning'
    24. });
    25. }
    26. console.log(response.data.data);
    27. this.posts = response.data.data;
    28. });
    29. },
    30. Search() {
    31. axios.get('/api/forum/search', {
    32. params: {
    33. searchKeyWord: this.searchKeyWord
    34. }
    35. }).then((response) => {
    36. if (response.data.code !== 200) {
    37. this.$notify({
    38. title: '警告',
    39. message: '搜索失败',
    40. type: 'warning'
    41. });
    42. }
    43. console.log(response.data.data);
    44. this.posts = response.data.data;
    45. });
    46. },
    47. // 跳转到文章详情
    48. getForumPostDetail(postId) {
    49. console.log("getForumPostDetail");
    50. console.log(postId);
    51. this.$router.push(`/post/${postId}`);
    52. },
    53. },
    54. mounted() {
    55. this.searchKeyWord = this.$route.params.searchKeyWord;
    56. this.textarea = this.searchKeyWord;
    57. this.Search();
    58. }
    59. }
    60. script>

    css:

    1. <style scoped>
    2. #layout {
    3. width: 100%;
    4. min-height: 90%;
    5. background-color: rgb(245, 246, 247);
    6. line-height: normal;
    7. }
    8. #SearchContent{
    9. min-height: 800px;
    10. }
    11. #searchFrame {
    12. height: 70px;
    13. width: 100%;
    14. background-color: white;
    15. box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
    16. position: -webkit-sticky;
    17. /* Safari */
    18. position: sticky;
    19. top: 0;
    20. z-index: 1000;
    21. line-height: normal;
    22. }
    23. #InputFrame {
    24. width: 40%;
    25. margin: 0 auto;
    26. height: 60%;
    27. padding-top: 15px;
    28. }
    29. #Interate {
    30. float: left;
    31. margin-top: 20px;
    32. margin-left: 20px;
    33. }
    34. #excess {
    35. height: 61px;
    36. width: 80%;
    37. background-color: white;
    38. margin: 0 auto;
    39. margin-top: 25px;
    40. border: 1px solid rgb(245, 245, 245);
    41. border-radius: 4px;
    42. }
    43. .custom-card {
    44. background-color: #ffffff;
    45. border-radius: 4px;
    46. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    47. margin-bottom: 16px;
    48. transition: box-shadow 0.3s ease-in-out;
    49. width: 80%;
    50. margin: 0 auto;
    51. border: 1px solid rgb(245, 245, 245);
    52. }
    53. .custom-card:hover {
    54. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
    55. cursor: pointer;
    56. background-color: rgb(245, 245, 245);
    57. }
    58. .card-content {
    59. padding: 16px;
    60. }
    61. .card-title {
    62. font-size: 1.2em;
    63. margin-bottom: 8px;
    64. color: #333;
    65. }
    66. .card-meta {
    67. display: flex;
    68. align-items: center;
    69. justify-content: space-between;
    70. margin-bottom: 12px;
    71. color: #666;
    72. }
    73. .card-rating {
    74. color: #666;
    75. }
    76. .card-rating {
    77. color: #666;
    78. }
    79. style>

     


     

    四、后端代码

    4.1项目后端依赖库

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starterartifactId>
    4. dependency>
    5. <dependency>
    6. <groupId>org.springframework.bootgroupId>
    7. <artifactId>spring-boot-starter-webartifactId>
    8. <scope>compilescope>
    9. dependency>
    10. <dependency>
    11. <groupId>com.mysqlgroupId>
    12. <artifactId>mysql-connector-jartifactId>
    13. <scope>runtimescope>
    14. dependency>
    15. <dependency>
    16. <groupId>org.projectlombokgroupId>
    17. <artifactId>lombokartifactId>
    18. <optional>trueoptional>
    19. dependency>
    20. <dependency>
    21. <groupId>org.springframework.bootgroupId>
    22. <artifactId>spring-boot-starter-testartifactId>
    23. <scope>testscope>
    24. dependency>
    25. <dependency>
    26. <groupId>org.mybatis.spring.bootgroupId>
    27. <artifactId>mybatis-spring-boot-starterartifactId>
    28. <version>2.3.1version>
    29. dependency>
    30. <dependency>
    31. <groupId>cn.hutoolgroupId>
    32. <artifactId>hutool-allartifactId>
    33. <version>5.8.11version>
    34. dependency>
    35. <dependency>
    36. <groupId>com.baomidougroupId>
    37. <artifactId>mybatis-plus-boot-starterartifactId>
    38. <version>3.5.3.1version>
    39. dependency>
    40. <dependency>
    41. <groupId>com.github.xiaoymingroupId>
    42. <artifactId>knife4j-spring-boot-starterartifactId>
    43. <version>3.0.2version>
    44. dependency>
    45. <dependency>
    46. <groupId>org.springframework.bootgroupId>
    47. <artifactId>spring-boot-starter-webartifactId>
    48. dependency>
    49. <dependency>
    50. <groupId>org.springframeworkgroupId>
    51. <artifactId>spring-webmvcartifactId>
    52. <version>5.3.15version>
    53. dependency>
    54. <dependency>
    55. <groupId>io.springfoxgroupId>
    56. <artifactId>springfox-spring-webartifactId>
    57. <version>3.0.0version>
    58. dependency>
    59. <dependency>
    60. <groupId>com.alibabagroupId>
    61. <artifactId>fastjsonartifactId>
    62. <version>1.2.76version>
    63. dependency>
    64. <dependency>
    65. <groupId>com.alibabagroupId>
    66. <artifactId>dashscope-sdk-javaartifactId>
    67. <version>2.8.2version>
    68. dependency>
    69. <dependency>
    70. <groupId>com.squareup.okhttp3groupId>
    71. <artifactId>okhttpartifactId>
    72. <version>4.9.3version>
    73. dependency>
    74. <dependency>
    75. <groupId>org.projectlombokgroupId>
    76. <artifactId>lombokartifactId>
    77. <optional>trueoptional>
    78. dependency>
    79. <dependency>
    80. <groupId>org.apache.commonsgroupId>
    81. <artifactId>commons-lang3artifactId>
    82. <version>3.12.0version>
    83. dependency>

    4.2工具类Result类与实体类

    1. public class Result {
    2. // 状态码常量
    3. public static final int SUCCESS = 200;
    4. public static final int ERROR = 500;
    5. private int code; // 状态码
    6. private String message; // 消息
    7. private T data; // 数据
    8. // 构造函数,用于创建成功的结果对象
    9. private Result(int code, String message, T data) {
    10. this.code = code;
    11. this.message = message;
    12. this.data = data;
    13. }
    14. // 成功结果的静态方法
    15. public static Result success(T data) {
    16. return new Result<>(SUCCESS, "Success", data);
    17. }
    18. // 错误结果的静态方法
    19. public static Result error(String message) {
    20. return new Result<>(ERROR, message, null);
    21. }
    22. // 错误结果的静态方法,可以传入自定义的状态码
    23. public static Result error(int code, String message) {
    24. return new Result<>(code, message, null);
    25. }
    26. // 获取状态码
    27. public int getCode() {
    28. return code;
    29. }
    30. // 设置状态码
    31. public void setCode(int code) {
    32. this.code = code;
    33. }
    34. // 获取消息
    35. public String getMessage() {
    36. return message;
    37. }
    38. // 设置消息
    39. public void setMessage(String message) {
    40. this.message = message;
    41. }
    42. // 获取数据
    43. public T getData() {
    44. return data;
    45. }
    46. // 设置数据
    47. public void setData(T data) {
    48. this.data = data;
    49. }
    50. // 用于转换为Map类型的方法,方便序列化为JSON
    51. public Map toMap() {
    52. Map map = new HashMap<>();
    53. map.put("code", code);
    54. map.put("message", message);
    55. map.put("data", data);
    56. return map;
    57. }
    58. }

    Entity:

    Forumpost:

    1. @TableName(value ="forum_posts")
    2. @Data
    3. public class ForumPosts implements Serializable {
    4. /**
    5. *
    6. */
    7. @TableId(value = "id", type = IdType.AUTO)
    8. private Integer id;
    9. /**
    10. *
    11. */
    12. @TableField(value = "title")
    13. private String title;
    14. /**
    15. *
    16. */
    17. @TableField(value = "content")
    18. private String content;
    19. /**
    20. *
    21. */
    22. @TableField(value = "author_id")
    23. private Integer authorId;
    24. /**
    25. *
    26. */
    27. @TableField(value = "created_at")
    28. private LocalDateTime createdAt;
    29. /**
    30. *
    31. */
    32. @TableField(value = "updated_at")
    33. private LocalDateTime updatedAt;
    34. /**
    35. *
    36. */
    37. @TableField(value = "heat_value")
    38. private Integer heatValue;
    39. /**
    40. *
    41. */
    42. @TableField(value = "rating")
    43. private BigDecimal rating;
    44. /**
    45. *
    46. */
    47. @TableField(value = "tag")
    48. private String tag;
    49. @TableField(exist = false)
    50. private static final long serialVersionUID = 1L;
    51. @Override
    52. public boolean equals(Object that) {
    53. if (this == that) {
    54. return true;
    55. }
    56. if (that == null) {
    57. return false;
    58. }
    59. if (getClass() != that.getClass()) {
    60. return false;
    61. }
    62. ForumPosts other = (ForumPosts) that;
    63. return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
    64. && (this.getTitle() == null ? other.getTitle() == null : this.getTitle().equals(other.getTitle()))
    65. && (this.getContent() == null ? other.getContent() == null : this.getContent().equals(other.getContent()))
    66. && (this.getAuthorId() == null ? other.getAuthorId() == null : this.getAuthorId().equals(other.getAuthorId()))
    67. && (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()))
    68. && (this.getUpdatedAt() == null ? other.getUpdatedAt() == null : this.getUpdatedAt().equals(other.getUpdatedAt()))
    69. && (this.getHeatValue() == null ? other.getHeatValue() == null : this.getHeatValue().equals(other.getHeatValue()))
    70. && (this.getRating() == null ? other.getRating() == null : this.getRating().equals(other.getRating()))
    71. && (this.getTag() == null ? other.getTag() == null : this.getTag().equals(other.getTag()));
    72. }
    73. @Override
    74. public int hashCode() {
    75. final int prime = 31;
    76. int result = 1;
    77. result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
    78. result = prime * result + ((getTitle() == null) ? 0 : getTitle().hashCode());
    79. result = prime * result + ((getContent() == null) ? 0 : getContent().hashCode());
    80. result = prime * result + ((getAuthorId() == null) ? 0 : getAuthorId().hashCode());
    81. result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());
    82. result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode());
    83. result = prime * result + ((getHeatValue() == null) ? 0 : getHeatValue().hashCode());
    84. result = prime * result + ((getRating() == null) ? 0 : getRating().hashCode());
    85. result = prime * result + ((getTag() == null) ? 0 : getTag().hashCode());
    86. return result;
    87. }
    88. @Override
    89. public String toString() {
    90. StringBuilder sb = new StringBuilder();
    91. sb.append(getClass().getSimpleName());
    92. sb.append(" [");
    93. sb.append("Hash = ").append(hashCode());
    94. sb.append(", id=").append(id);
    95. sb.append(", title=").append(title);
    96. sb.append(", content=").append(content);
    97. sb.append(", authorId=").append(authorId);
    98. sb.append(", createdAt=").append(createdAt);
    99. sb.append(", updatedAt=").append(updatedAt);
    100. sb.append(", heatValue=").append(heatValue);
    101. sb.append(", rating=").append(rating);
    102. sb.append(", tag=").append(tag);
    103. sb.append(", serialVersionUID=").append(serialVersionUID);
    104. sb.append("]");
    105. return sb.toString();
    106. }
    107. }
    ForumPostLike:
    1. @Data
    2. @TableName(value ="forum_post_likes")
    3. public class ForumPostLike {
    4. private int userId;
    5. private int postId;
    6. }
    ForumPostFavorites:
    1. @Data
    2. @TableName(value ="forum_post_favorites")
    3. public class ForumPostFavorites {
    4. private int userId;
    5. private int postId;
    6. }
    ForumComments
    1. @TableName(value ="forum_comments")
    2. @Data
    3. public class ForumComments implements Serializable {
    4. /**
    5. *
    6. */
    7. @TableId(value = "id", type = IdType.AUTO)
    8. private Integer id;
    9. /**
    10. *
    11. */
    12. @TableField(value = "post_id")
    13. private Integer postId;
    14. /**
    15. *
    16. */
    17. @TableField(value = "user_id")
    18. private Integer userId;
    19. /**
    20. *
    21. */
    22. @TableField(value = "comment_text")
    23. private String commentText;
    24. /**
    25. *
    26. */
    27. @TableField(value = "created_at")
    28. private LocalDateTime createdAt;
    29. @TableField(exist = false)
    30. private static final long serialVersionUID = 1L;
    31. @Override
    32. public boolean equals(Object that) {
    33. if (this == that) {
    34. return true;
    35. }
    36. if (that == null) {
    37. return false;
    38. }
    39. if (getClass() != that.getClass()) {
    40. return false;
    41. }
    42. ForumComments other = (ForumComments) that;
    43. return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
    44. && (this.getPostId() == null ? other.getPostId() == null : this.getPostId().equals(other.getPostId()))
    45. && (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
    46. && (this.getCommentText() == null ? other.getCommentText() == null : this.getCommentText().equals(other.getCommentText()))
    47. && (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()));
    48. }
    49. @Override
    50. public int hashCode() {
    51. final int prime = 31;
    52. int result = 1;
    53. result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
    54. result = prime * result + ((getPostId() == null) ? 0 : getPostId().hashCode());
    55. result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
    56. result = prime * result + ((getCommentText() == null) ? 0 : getCommentText().hashCode());
    57. result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());
    58. return result;
    59. }
    60. @Override
    61. public String toString() {
    62. StringBuilder sb = new StringBuilder();
    63. sb.append(getClass().getSimpleName());
    64. sb.append(" [");
    65. sb.append("Hash = ").append(hashCode());
    66. sb.append(", id=").append(id);
    67. sb.append(", postId=").append(postId);
    68. sb.append(", userId=").append(userId);
    69. sb.append(", commentText=").append(commentText);
    70. sb.append(", createdAt=").append(createdAt);
    71. sb.append(", serialVersionUID=").append(serialVersionUID);
    72. sb.append("]");
    73. return sb.toString();
    74. }
    75. }

     DTO:

    1. @Data
    2. public class CommentDTO {
    3. private int userId;
    4. private int postId;
    5. private String commentText;
    6. }
    1. @Data
    2. public class ForumAddPostDTO {
    3. @JsonProperty("title")
    4. private String title;
    5. @JsonProperty("content")
    6. private String content;
    7. @JsonProperty("authorId")
    8. private Integer authorId;
    9. @JsonProperty("tag")
    10. private String tag;
    11. }

     VO:

    1. @Data
    2. public class ArticleVO {
    3. private String title;
    4. private String content;
    5. private String account;
    6. private Integer heatValue;
    7. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    8. private LocalDateTime createdAt;
    9. }
    1. @Data
    2. public class CommentVo {
    3. private int id;
    4. private String commentText;
    5. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    6. private LocalDateTime createdAt;
    7. private String account;
    8. }
    1. @Data
    2. public class LikeSearchVo {
    3. private String value;
    4. }

     


    4.3 自定义异常与全局异常

    1. public class BaseException extends RuntimeException{
    2. public BaseException(){
    3. }
    4. public BaseException(String msg){
    5. super(msg);
    6. }
    7. }
    1. public class NotFoundArticleException extends BaseException{
    2. public NotFoundArticleException(String msg){
    3. super(msg);
    4. }
    5. }
    1. public class AlreadyLikeException extends BaseException{
    2. public AlreadyLikeException(String msg){
    3. super(msg);
    4. }
    5. }

    全局异常处理类:

    1. @RestControllerAdvice
    2. @Slf4j
    3. public class GlobalExceptionHandler {
    4. @ExceptionHandler
    5. public Result exceptionHandler(BaseException ex){
    6. log.error("异常信息:{}", ex.getMessage());
    7. return Result.error(ex.getMessage());
    8. }
    9. }

    4.4Controller层

    1.ForumPostController

    1. @RequestMapping("/forum")
    2. @RestController
    3. @Api(tags = "文章管理")
    4. @Slf4j
    5. public class ForumPostController {
    6. @Autowired
    7. ForumPostsService forumPostsService;
    8. @Autowired
    9. UserService userService;
    10. @Autowired
    11. ForumCommentsService forumCommentsService;
    12. @ApiOperation("新增文章")
    13. @PostMapping("/add")
    14. public Result addForumPost(@RequestBody ForumAddPostDTO forumAddPostDTO) throws ParseException {
    15. ForumPosts forumPosts = new ForumPosts();
    16. BeanUtils.copyProperties(forumAddPostDTO, forumPosts);
    17. forumPostsService.save(forumPosts);
    18. return Result.success("新增成功");
    19. }
    20. @GetMapping("/getAllForumPost")
    21. @ApiOperation("推荐文章查询")
    22. public Result getAllForumPost(@RequestParam(value="pageSize", defaultValue = "10") int pageSize,
    23. @RequestParam(value="pageNumber", defaultValue = "1") int pageNumber){
    24. Page page = new Page<>(pageNumber, pageSize);
    25. // 创建查询包装器并指定排序规则
    26. QueryWrapper queryWrapper = new QueryWrapper<>();
    27. // 假设你想按照创建时间降序排序
    28. queryWrapper.orderByDesc("created_at");
    29. Page paged = forumPostsService.page(page,queryWrapper);
    30. List postsList = paged.getRecords();
    31. List posts = new ArrayList<>();
    32. for (ForumPosts post : postsList) {
    33. ForumPageVO vo = new ForumPageVO();
    34. // 获取账户信息
    35. User user = userService.getById(post.getAuthorId());
    36. vo.setAccount(user.getAccount());
    37. // 直接从ForumPosts对象复制其他字段
    38. vo.setTag(post.getTag());
    39. vo.setRating(post.getRating()); // 如果需要字符串形式
    40. vo.setTitle(post.getTitle());
    41. vo.setPostId(post.getId());
    42. vo.setHeat_value(post.getHeatValue());
    43. // 添加到列表
    44. posts.add(vo);
    45. }
    46. return Result.success(posts);
    47. }
    48. // 文章阅读
    49. @ApiOperation("读取文章")
    50. @GetMapping("post/{id}")
    51. public Result readArtical(@PathVariable int id){
    52. ArticleVO articleVO = forumPostsService.readArticle(id);
    53. return Result.success(articleVO);
    54. }
    55. // 我的文章功能
    56. @ApiOperation("我的文章")
    57. @GetMapping("/MyArticle")
    58. public Result getMyArticle(@Param("id") int id){
    59. List forumPostsList = forumPostsService.getByAuthorId(id);
    60. return Result.success(forumPostsList);
    61. }
    62. // 热门文章功能
    63. @ApiOperation("热门文章")
    64. @GetMapping("/getHotPosts")
    65. public Result getHotPosts(){
    66. QueryWrapper queryWrapper = new QueryWrapper();
    67. queryWrapper.orderByDesc("heat_value");
    68. List forumPosts = forumPostsService.list(queryWrapper);
    69. return Result.success(forumPosts);
    70. }
    71. // 热门文章功能
    72. @ApiOperation("优质文章")
    73. @GetMapping("/getOutStandPosts")
    74. public Result getOutStandPosts(){
    75. QueryWrapper queryWrapper = new QueryWrapper();
    76. queryWrapper.orderByDesc("rating");
    77. List forumPosts = forumPostsService.list(queryWrapper);
    78. return Result.success(forumPosts);
    79. }
    80. // 我的收藏
    81. @ApiOperation("我的收藏")
    82. @GetMapping("/getMyFavorite")
    83. public Result getMyFavorite(@Param("id") int id){
    84. List forumPostsList = forumPostsService.getMyFavorite(id);
    85. return Result.success(forumPostsList);
    86. }
    87. // 文章查询
    88. @ApiOperation("文章查询")
    89. @GetMapping("/search")
    90. public Result postSearch(@RequestParam("searchKeyWord") String searchText){
    91. if (searchText == null){
    92. return Result.error("不能输入为空噢");
    93. }
    94. LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
    95. // TODO:通过LamdaQueryWrapper 模糊查询
    96. // 模糊查询title字段,%searchText%会被自动添加
    97. lambdaQueryWrapper.like(ForumPosts::getTitle, searchText);
    98. // 假设这里有一个service接口用于操作ForumPosts表
    99. List postsList = forumPostsService.list(lambdaQueryWrapper);
    100. // 根据你的Result类的具体实现,返回查询结果
    101. return Result.success(postsList);
    102. }
    103. /**
    104. * 标题模糊查询
    105. * @param keyword 关键词
    106. * @return 匹配的标题列表
    107. */
    108. @GetMapping("/getLikeSearch")
    109. @ApiOperation("标题模糊查询")
    110. public Result getLikeSearch(@RequestParam String keyword){
    111. List titles = forumPostsService.findTitlesByKeyword(keyword);
    112. return Result.success(titles);
    113. }
    114. // 用户交互
    115. // 点赞
    116. @ApiOperation("点赞")
    117. @GetMapping("/like")
    118. public Result PostLike(@RequestParam("postId") int postId,@RequestParam("userId") int userId){
    119. String result = forumPostsService.PostLike(postId,userId);
    120. return Result.success(result);
    121. }
    122. // 收藏
    123. @ApiOperation("收藏")
    124. @GetMapping("/favorite")
    125. public Result PostFavorite(@RequestParam("postId") int postId,@RequestParam("userId") int userId){
    126. String result = forumPostsService.PostFavorite(postId,userId);
    127. return Result.success(result);
    128. }
    129. // 写评论
    130. @ApiOperation("写评论")
    131. @PostMapping("/writeComment")
    132. public Result WriteComment(@RequestBody CommentDTO commentdto){
    133. ForumComments forumComments = new ForumComments();
    134. BeanUtils.copyProperties(commentdto,forumComments);
    135. forumCommentsService.save(forumComments);
    136. return Result.success("评论成功");
    137. }
    138. // 读评论
    139. @ApiOperation("读取评论")
    140. @GetMapping("/getComment")
    141. public Result> GetComment(@RequestParam("postId") int postId){
    142. List comment = forumCommentsService.getComment(postId);
    143. return Result.success(comment);
    144. }
    145. }


    4.5 Service层
     

    1. public interface ForumPostsService extends IService {
    2. List getByAuthorId(Integer id);
    3. String PostLike(int postId, int userId);
    4. String PostFavorite(int postId, int userId);
    5. ArticleVO readArticle(int id);
    6. List getMyFavorite(int id);
    7. List findTitlesByKeyword(String keyword);
    8. }
    1. public interface ForumCommentsService extends IService {
    2. List getComment(int postId);
    3. }

    4.6 Mapper

    1. public interface ForumCommentsMapper extends BaseMapper {
    2. }
    1. @Mapper
    2. public interface ForumPostFavoritesMapper extends BaseMapper {
    3. }
    1. @Mapper
    2. public interface ForumPostLikeMapper extends BaseMapper {
    3. }
    1. @Mapper
    2. public interface ForumPostsMapper extends BaseMapper {
    3. @Select("select * from paitool.user as a,paitool.forum_posts as b where a.id=b.author_id and a.id = #{id}")
    4. List getByAuthorId(Integer id);
    5. }

     


    五、进阶思路

    1.通过ElasticSearch优化搜索引擎

    2.使用Redis存储热门文章,以减少数据库压力

    3.通过若依框架+AI 完善管理系统


  • 相关阅读:
    [翻译] NVIDIA HugeCTR,GPU 版本参数服务器 --(10)--- 推理架构
    CVBS、VGA、HDMI、MIPI等8种视频接口详解
    MySQL查询成本
    公司新来了个00后卷王,一副毛头小子的样儿,哪想到...
    Gbase8s数据库ALTER INDEX 语句
    消息推送平台的实时数仓?!flink消费kafka消息入到hive
    工程项目管理系统的Java实现:高效协同与信息共享
    前端修罗场,祝您中秋快乐
    2023-11-17 服务器开发-性能分析-intel-vtune-安装
    518抽奖软件,支持中途临时追加名单
  • 原文地址:https://blog.csdn.net/dogxixi/article/details/140369263