项目使用到的技术与库
1.前端 Vue2 elementUi Cookie WangEditer
2.后端 SpringBoot Mybatis-Plus
3.数据库 MySql
一、效果展示
1.1主页效果:
1.2 文章编辑页面:
1.3 成功发布文章
1.4 文章关键字搜索提示
1.5 文章查询结果展示
1.6 文章内容及交互展示
二、表单设计的sql
用户:
create table paitool.user
(
id int auto_increment
primary key,
account varchar(255) not null,
password varchar(255) not null,
phone varchar(20) null,
address varchar(255) null,
isVip tinyint(1) default 0 null,
email varchar(255) null,
registration_date datetime default CURRENT_TIMESTAMP null,
last_login datetime null,
status enum ('active', 'inactive') default 'active' null,
constraint account_UNIQUE
unique (account),
constraint email_UNIQUE
unique (email),
constraint phone_UNIQUE
unique (phone)
);
文章:
create table paitool.forum_posts
(
id int auto_increment
primary key,
title varchar(255) not null,
content text not null,
author_id int not null,
created_at timestamp default CURRENT_TIMESTAMP null,
updated_at timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
heat_value int default 0 null,
rating decimal(3, 2) default 0.00 null,
tag varchar(10) default '其它' null,
constraint forum_posts_ibfk_1
foreign key (author_id) references paitool.user (id)
);
文章交互表-点赞:
create table paitool.forum_post_likes
(
user_id int not null,
post_id int not null,
primary key (user_id, post_id),
constraint forum_post_likes_ibfk_1
foreign key (user_id) references paitool.user (id),
constraint forum_post_likes_ibfk_2
foreign key (post_id) references paitool.forum_posts (id)
);
文章交互表-收藏:
create table paitool.forum_post_favorites
(
user_id int not null,
post_id int not null,
primary key (user_id, post_id),
constraint forum_post_favorites_ibfk_1
foreign key (user_id) references paitool.user (id),
constraint forum_post_favorites_ibfk_2
foreign key (post_id) references paitool.forum_posts (id)
);
文章交互表-评论
create table paitool.forum_comments
(
id int auto_increment
primary key,
post_id int not null,
user_id int not null,
comment_text text not null,
created_at timestamp default CURRENT_TIMESTAMP null,
constraint forum_comments_ibfk_1
foreign key (user_id) references paitool.user (id),
constraint forum_comments_ibfk_2
foreign key (post_id) references paitool.forum_posts (id)
);
三、前端代码
3.1 论坛主页
Html:
<template>
<div id="forumLayOut">
<div id="Top" style="background-color: rgb(250, 250, 250); padding-top: 20px">
<div id="serchBorder" style="padding-bottom: 13px;">
<!-- 搜索框 -->
<el-autocomplete v-model="searchKeyWord" :fetch-suggestions="querySearchAsync" :trigger-on-focus="false"
placeholder="请输入关键字" style="width: 300px;" @select="handleSelect">
</el-autocomplete>
<el-button type="primary" @click="onSubmit">查询</el-button>
</div>
<!-- 分类查询 -->
<div>
<div style="margin-bottom: 15px;">
<el-checkbox-group v-model="checkboxGroup1" :max="1">
<el-checkbox-button v-for="city in cities" :label="city" :key="city">{{ city
}}</el-checkbox-button>
</el-checkbox-group>
</div>
</div>
<div style="height: 380px; width: 100%;">
<!-- 轮播图 -->
<div class="block" style="width: 30%; float: left; margin-left: 5%; height: 400px;">
<el-carousel height="350px" style="width: 100%; ">
<el-carousel-item v-for="item in 4" :key="item">
<img src="https://img95.699pic.com/photo/50035/3211.jpg_wh860.jpg" alt="风景测试">
<h3 class="small">{{ item }}</h3>
</el-carousel-item>
</el-carousel>
</div>
<div style=" height: 350px; background-color: rgb(250, 250, 250); width: 25%; float: left;
border: 1px solid rgb(240, 240, 242); margin-left: 3%;">
<div style="height: 50px; width: 100%; background-color: rgb(245, 245, 245); ">
<i class="el-icon-share"></i>
<div><b>热门</b></div>
<hr>
</div>
<div class="link-container">
<a href="#" class="link" id="TurnLink">杀死谷歌,成为AI时代的搜索皇帝!</a>
<p style="color: gray;">Perplexity CEO 最新四万字访谈</p>
</div>
<div class="link-container">
<a href="#" class="link" id="TurnLink">重写系统后痛批:这门语言烂透了!</a>
<p style="color: gray;">耗时18个月,开发者弃TypeScript投Rust</p>
</div>
<div class="link-container">
<a href="#" class="link" id="TurnLink">Shire 编码智能体语言</a>
<p style="color: gray;">打造你的专属 AI 编程助手</p>
</div>
</div>
<div style="float: left; margin-left: 3%; height: 350px; background-color: rgb(250, 250, 250); width: 25%; float: left;
border: 1px solid rgb(240, 240, 242); ">
<div style="height: 50px; width: 100%; background-color: rgb(245, 245, 245); ">
<i class="el-icon-message-solid"></i>
<div><b>头条</b></div>
<hr>
</div>
<div class="link-container">
<a href="#" class="link" id="TurnLink">史上开发最久的游戏!</a>
<p style="color: gray;">耗时 22 年,5 名打工人凑了几百欧就开工,只剩 1 人坚守到发布...</p>
</div>
<div class="link-container">
<a href="#" class="link" id="TurnLink">实习期间创下 Transformer</a>
<p style="color: gray;">他说:当年整个 AI 圈都无法预见我们今天的高度</p>
</div>
<div class="link-container">
<a href="#" class="link" id="TurnLink">杀死谷歌,成为AI时代的搜索皇帝!</a>
<p style="color: gray;">Perplexity CEO 最新四万字访谈</p>
</div>
</div>
</div>
</div>
<el-divider></el-divider>
<div id="bottom">
<!-- Tabs 标签页 -->
<el-tabs v-model="activeName" @tab-click="handleClick" style="padding-left: 2em; ">
<el-tab-pane label="我的文章" name="first">
<div class="parent-div" style="min-height: 500px">
<div v-if="posts.length === 0">
<el-empty :image-size="200"></el-empty>
</div>
<div class="custom-card" v-for="(post, index) in posts" :key="index"
@click="getForumPostDetail(post.id)">
<div class="card-content">
<h1 class="card-title">标题: {{ post.title }}</h1>
<div class="card-meta">
<span>作者: {{ post.account }}</span>
<span>标签: {{ post.tag }}</span>
<span><i class="el-icon-view">{{ post.heatValue }}</i></span>
</div>
<div class="card-rating">
<span>文章评分:</span>
<el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
score-template="{value}" style="display: inline-block;"></el-rate>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="推荐文章" name="second">
<div v-if="posts.length === 0">
<el-empty :image-size="200"></el-empty>
</div>
<div class="custom-card" v-for="(post, index) in posts" :key="index"
@click="getForumPostDetail(post.postId)">
<div class="card-content">
<h1 class="card-title">标题: {{ post.title }}</h1>
<div class="card-meta">
<span>作者: {{ post.account }}</span>
<span>标签: {{ post.tag }}</span>
<span><i class="el-icon-view">{{ post.heat_value }}</i></span>
</div>
<div class="card-rating">
<span>文章评分:</span>
<el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
score-template="{value}" style="display: inline-block;"></el-rate>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="热门文章" name="third">
<div class="parent-div" style="min-height: 500px">
<div v-if="posts.length === 0">
<el-empty :image-size="200"></el-empty>
</div>
<div class="custom-card" v-for="(post, index) in posts" :key="index"
@click="getForumPostDetail(post.id)">
<div class="card-content">
<h1 class="card-title">标题: {{ post.title }}</h1>
<div class="card-meta">
<span>作者: {{ post.account }}</span>
<span>标签: {{ post.tag }}</span>
<span><i class="el-icon-view">{{ post.heatValue }}</i></span>
</div>
<div class="card-rating">
<span>文章评分:</span>
<el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
score-template="{value}" style="display: inline-block;"></el-rate>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="优质文章" name="fourth">
<div class="parent-div" style="min-height: 500px">
<div v-if="posts.length === 0">
<el-empty :image-size="200"></el-empty>
</div>
<div class="custom-card" v-for="(post, index) in posts" :key="index"
@click="getForumPostDetail(post.id)">
<div class="card-content">
<h1 class="card-title">标题: {{ post.title }}</h1>
<div class="card-meta">
<span>作者: {{ post.account }}</span>
<span>标签: {{ post.tag }}</span>
<span><i class="el-icon-view">{{ post.heatValue }}</i></span>
</div>
<div class="card-rating">
<span>文章评分:</span>
<el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
score-template="{value}" style="display: inline-block;"></el-rate>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="我的收藏" name="fifth">
<div v-if="posts.length === 0">
<el-empty :image-size="200"></el-empty>
</div>
<div class="parent-div" style="min-height: 500px">
<div v-if="posts.length === 0">
<el-empty :image-size="200"></el-empty>
</div>
<div class="custom-card" v-for="(post, index) in posts" :key="index"
@click="getForumPostDetail(post.id)">
<div class="card-content">
<h1 class="card-title">标题: {{ post.title }}</h1>
<div class="card-meta">
<span>作者: {{ post.account }}</span>
<span>标签: {{ post.tag }}</span>
<span><i class="el-icon-view">{{ post.heatValue }}</i></span>
</div>
<div class="card-rating">
<span>文章评分:</span>
<el-rate v-model="post.rating" disabled show-score text-color="#ff9900"
score-template="{value}" style="display: inline-block;"></el-rate>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<el-button type="warning" round id="iWantPost" @click="navigateToPostEdit">我要发布文章</el-button>
</div>
</div>
</template>
js:
<script>
import axios from 'axios';
import Cookies from 'js-cookie';
const cityOptions = ['新闻报道', '科技动态', '生活时尚', '教育学习', '健康养生'];
export default {
components: {
},
data() {
return {
searchKeyWord: '',
suggestions: [], // 添加这个属性
checkboxGroup1: [],
cities: cityOptions,
activeName: 'first',
currentPage1: 5,
currentPage2: 5,
currentPage3: 5,
currentPage4: 4,
posts: [
],
}
},
methods: {
onSubmit() {
this.$router.push({
name: 'ArticalSearchView',
params: {
searchKeyWord: this.searchKeyWord
}
})
},
handleClick(tab) {
// 我的文章
if (tab.name === 'first') {
this.posts = []
this.getMyArticle();
}
// 推荐文章
if (tab.name === 'second') {
this.posts = [];
axios.get('/api/forum/getAllForumPost', {
params: {
pageSize: 1,
pageNumber: 10
}
}).then((response) => {
console.log(response.data.data);
this.posts = response.data.data;
});
}
// 热门文章
if (tab.name === 'third') {
this.posts = [];
axios.get('/api/forum/getHotPosts').then((response) => {
console.log(response.data.data);
this.posts = response.data.data;
});
}
// 优质文章
if (tab.name === 'fourth') {
this.posts = [];
axios.get('/api/forum/getOutStandPosts').then((response) => {
console.log(response.data.data);
this.posts = response.data.data;
});
}
// 我的收藏
if (tab.name === 'fifth') {
this.posts = [];
const id = Cookies.get("userId");
if (id === null) {
this.$message({
message: '请先登录',
type: 'warning'
});
return;
}
axios.get('/api/forum/getMyFavorite', {
params: {
id: id
}
}).then((response) => {
console.log(response.data.data);
this.posts = response.data.data;
});
}
},
// 处理分页功能
handleSizeChange(val) {
console.log(`每页 ${val} 条`);
},
handleCurrentChange(val) {
console.log(`当前页: ${val}`);
},
navigateToPostEdit() {
this.$router.push({ name: 'ForumPostEditView' });
},
// 跳转到文章详情
getForumPostDetail(postId) {
console.log("getForumPostDetail");
console.log(postId);
this.$router.push(`/post/${postId}`);
},
getMyArticle() {
this.posts = [];
const id = Cookies.get("userId");
if (id === null) {
this.$message({
message: '请先登录',
type: 'warning'
});
return;
} else {
axios.get('/api/forum/MyArticle', {
params: {
id: id
}
}).then((response) => {
console.log(response.data.data);
this.posts = response.data.data;
})
}
},
// 异步获取建议列表
querySearchAsync(queryString, cb) {
if (queryString.length === 0) {
return cb([]); // 当查询字符串为空时,直接返回空数组
}
axios.get('/api/forum/getLikeSearch', { params: { keyword: queryString } })
.then(response => {
// 确保从后端返回的数据中提取出正确的数组
const results = response.data.data || [];
// 调用callback函数,传入搜索结果
cb(results);
})
.catch(error => {
console.error('Error fetching search suggestions:', error);
cb([]);
});
},
// 处理选择事件
handleSelect(item) {
this.searchKeyWord = item.value;
this.onSubmit();
}
},
mounted() {
this.getMyArticle();
}
}
</script>
css:
<style scoped>
#forumLayOut {
background-color: white;
height: auto;
width: 100%;
line-height: normal;
}
#serchBorder {
line-height: normal;
}
.el-carousel__item h3 {
color: #475669;
font-size: 14px;
opacity: 0.75;
line-height: 150px;
margin: 0;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n+1) {
background-color: #d3dce6;
}
#Pagination {
align-self: center;
/* 居中对齐 */
margin-bottom: 1rem;
/* 可选,增加底部边距 */
margin-top: 10%;
}
#iWantPost {
position: fixed;
/* 设置为固定定位 */
bottom: 60px;
/* 距离底部的距离,可根据需要调整 */
right: 40px;
/* 距离右侧的距离,可根据需要调整 */
}
.el-tabs__content {
overflow: hidden;
position: relative;
height: auto;
}
.custom-card {
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
transition: box-shadow 0.3s ease-in-out;
}
.custom-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
cursor: pointer;
background-color: rgb(245, 245, 245);
}
.card-content {
padding: 16px;
}
.card-title {
font-size: 1.2em;
margin-bottom: 8px;
color: #333;
}
.card-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
color: #666;
}
.card-rating {
color: #666;
}
.link-container {
line-height: normal;
float: left;
width: 100%;
text-align: left;
padding-left: 40px;
padding-top: 10px;
}
.link {
text-decoration: none;
font-size: large;
color: black;
}
.link:hover {
text-decoration: underline;
}
</style>
3.2 发布文章页面
<template>
<div style="border: 1px solid #ccc; line-height: normal; height: 100%;">
<div>
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="文章标题">
<el-input v-model="formInline.title" placeholder="请输入文章标题" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="类别">
<el-select v-model="formInline.category" placeholder="请选择文章类别">
<el-option label="新闻报道" value="news"></el-option>
<el-option label="科技动态" value="technology"></el-option>
<el-option label="生活时尚" value="lifestyle"></el-option>
<el-option label="教育学习" value="education"></el-option>
<el-option label="健康养生" value="health"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit"
v-loading.fullscreen.lock="fullscreenLoading">提交</el-button>
</el-form-item>
</el-form>
</div>
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" />
<Editor style="height: 500px; overflow-y: hidden; height: 100%;" v-model="html" :defaultConfig="editorConfig"
:mode="mode" @onCreated="onCreated" />
</div>
</template>
<script>
import Vue from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import axios from 'axios'
import Cookies from 'js-cookie'
export default Vue.extend({
components: { Editor, Toolbar },
data() {
return {
editor: null,
html: ' ',
toolbarConfig: {},
editorConfig: { placeholder: '请输入内容...' },
mode: 'default', // or 'simple'
formInline: {
title: '',
category: ''
},
fullscreenLoading: false
}
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
},
onSubmit() {
this.fullscreenLoading = true;
const userId = Cookies.get('userId'); // 获取并转换userId
axios.post('/api/forum/add', {
"title": this.formInline.title,
"content": this.editor.getHtml(),
"authorId": userId,
"tag": this.formInline.category,
}).then((response) => {
console.log(response.data);
this.fullscreenLoading = false;
this.$router.push({ name: 'ForumSucessPostView' });
}).catch(error => {
console.error(error);
this.fullscreenLoading = false;
});
},
},
mounted() {
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时,及时销毁编辑器
},
})
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
3.3 文章发布成功页面
3.4 查看文章页面
html:
<template>
<div style="line-height: normal; background-color: rgb(246, 247, 249); height: auto; min-height: 80%;">
<!-- 文章信息 -->
<div style="padding-top: 10px; width: auto; min-width: 40%;">
<!-- 实现文字垂直居中 -->
<div id="Infor" style="background-color: white;">
<h1 style="font-size: 28px; text-align: center">文章标题:{{title}}</h1>
<span>创作者:{{author}}</span>
<span style="margin-left: 20px;">创作日期:{{createAt}}</span>
<span style="margin-left: 20px;"><i class="el-icon-view">{{heatValue}}</i></span>
</div>
<el-divider><i class="el-icon-mobile-phone"></i></el-divider>
<!-- 文章内容展示区 -->
<div id="contentDisplay">
<div v-html="content"
style="padding-left: 2em; padding-top: 15px; padding-right: 2em; padding-bottom: 30px;"></div>
</div>
</div>
<el-divider><i class="el-icon-edit"></i></el-divider>
<!-- 交互按键 -->
<div id="buttom">
<el-button type="warning" round @click="getBackToForum">返回到论坛</el-button>
<el-button type="warning" icon="el-icon-star-off" circle @click="PostFavorite"></el-button>
<el-button type="danger" icon="el-icon-thumb" circle @click="PostLike"></el-button>
</div>
<!-- 评论区 -->
<div id="commentListShow">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>评论详情</span>
</div>
<div id="commentInputArea">
<el-input type="textarea" placeholder="请您输入友善的评论吧" v-model="textarea" maxlength="300"
show-word-limit id="inputFrame" :clearable="clearAble" resize="none">
</el-input>
<div style="margin-top: 10px; padding-bottom: 50px;">
<el-button type="primary" @click="SubmitComment">发表评论</el-button>
<el-button type="primary" @click="CancelComment">取消评论</el-button>
</div>
</div>
<div id="commentList">
<div class="comment-card" v-for="comment in comments" :key="comment.id">
<div class="comment-head">
<h1 class="username">{{ comment.account }}</h1>
<p class="created-at">发表于:{{ comment.createdAt }}</p>
</div>
<el-divider></el-divider>
<p class="comment-text">{{ comment.commentText }}</p>
</div>
</div>
</el-card>
</div>
</div>
</template>
script:
<script>
import axios from 'axios';
import Cookies from 'js-cookie';
export default{
data() {
return {
postId: '',
title: '',
content: '',
value1: null,
textarea: '',
userId:'',
clearAble: true,
comments:{},
author:'',
createAt:'',
heatValue:'',
}
},
created() {
this.postId = this.$route.params.postId;
this.fetchPostDetail(this.$route.params.postId);
this.userId = Cookies.get('userId');
},
mounted() {
this.readComment();
},
methods: {
// 前端实现路径传参
async fetchPostDetail(postId) {
try {
this.fullscreenLoading = true;
const url = `/api/forum/post/${postId}`;
// 发起GET请求
const response = await axios.get(url);
if (response.status === 200) {
// 请求成功,处理响应数据
const postData = response.data;
console.log('文章详情:', postData);
// 更新组件状态或执行其他操作
this.title = response.data.data.title;
this.content = response.data.data.content;
this.author = response.data.data.account;
this.createAt = response.data.data.createdAt;
this.heatValue = response.data.data.heatValue;
this.fullscreenLoading = false;
} else {
console.error('请求失败,状态码:', response.status);
}
} catch (error) {
console.error('请求错误:', error);
}
},
getBackToForum() {
this.$router.push({ name: 'forum' });
},
// 取消评论
CancelComment(){
this.textarea = '';
},
// 执行点赞按钮
PostLike(){
this.isLogin();
axios.get('/api/forum/like',{
params:{
postId : this.postId,
userId : this.userId
}
}).then((response)=>{
this.MessageNotify(response);
})
},
// 执行收藏按钮
PostFavorite(){
this.isLogin();
axios.get('/api/forum/favorite',{
params:{
postId : this.postId,
userId : this.userId
}
}).then((response)=>{
this.MessageNotify(response);
})
},
SubmitComment(){
this.isLogin();
axios.post('/api/forum/writeComment',{
postId : this.postId,
userId : this.userId,
commentText : this.textarea
}).then((response)=>{
this.MessageNotify(response);
this.textarea = '';
this.readComment();
})
console.log("submit");
},
isLogin(){
if(Cookies.get('userId') == null){
this.$message.error('请先登录');
return;
}
},
// 消息提醒
MessageNotify(response){
if(response.data.code == 200){
this.$message.success(response.data.data);
}else{
console.log(response.data);
this.$message.error(response.data.message);
}
},
readComment(){
axios.get('/api/forum/getComment',{
params:{
postId : this.postId
}
}).then((response)=>{
this.comments = response.data.data;
})
},
}
}
</script>
css:
3.5 文章搜索页面
html:
<template>
<div id="layout">
<div id="searchFrame">
<div id="InputFrame">
<el-input type="textarea" placeholder="请输入内容" v-model="textarea" rows="1" resize="none"
style="font-size: larger; width: 80%;">
</el-input>
<el-button type="warning" @click="SearchSubmit" icon="el-icon-search">查询</el-button>
</div>
</div>
<div id="excess">
<div id="Interate">
<i class="el-icon-search"> 搜索结果</i>
</div>
</div>
<div id="SearchContent">
<div v-if="posts.length === 0">
<el-empty :image-size="200"></el-empty>
</div>
<div class="custom-card" v-for="(post, index) in posts" :key="index" @click="getForumPostDetail(post.id)">
<div class="card-content">
<h1 class="card-title">标题: {{ post.title }}</h1>
<div class="card-meta">
<span>作者: {{ post.account }}</span>
<span>标签: {{ post.tag }}</span>
<span><i class="el-icon-view">{{ post.heatValue }}</i></span>
</div>
<div class="card-rating">
<span>文章评分:</span>
<el-rate v-model="post.rating" disabled show-score text-color="#ff9900" score-template="{value}"
style="display: inline-block;"></el-rate>
</div>
</div>
</div>
</div>
</div>
</template>
script:
<script>
import axios from 'axios';
export default {
data() {
return {
textarea: '',
searchKeyWord: '',
posts: [],
}
},
methods: {
SearchSubmit() {
console.log(this.textarea);
axios.get('/api/forum/search', {
params: {
searchKeyWord: this.textarea
}
}).then((response) => {
if (response.data.code !== 200) {
this.$notify({
title: '警告',
message: '搜索失败',
type: 'warning'
});
}
console.log(response.data.data);
this.posts = response.data.data;
});
},
Search() {
axios.get('/api/forum/search', {
params: {
searchKeyWord: this.searchKeyWord
}
}).then((response) => {
if (response.data.code !== 200) {
this.$notify({
title: '警告',
message: '搜索失败',
type: 'warning'
});
}
console.log(response.data.data);
this.posts = response.data.data;
});
},
// 跳转到文章详情
getForumPostDetail(postId) {
console.log("getForumPostDetail");
console.log(postId);
this.$router.push(`/post/${postId}`);
},
},
mounted() {
this.searchKeyWord = this.$route.params.searchKeyWord;
this.textarea = this.searchKeyWord;
this.Search();
}
}
</script>
css:
<style scoped>
#layout {
width: 100%;
min-height: 90%;
background-color: rgb(245, 246, 247);
line-height: normal;
}
#SearchContent{
min-height: 800px;
}
#searchFrame {
height: 70px;
width: 100%;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
position: -webkit-sticky;
/* Safari */
position: sticky;
top: 0;
z-index: 1000;
line-height: normal;
}
#InputFrame {
width: 40%;
margin: 0 auto;
height: 60%;
padding-top: 15px;
}
#Interate {
float: left;
margin-top: 20px;
margin-left: 20px;
}
#excess {
height: 61px;
width: 80%;
background-color: white;
margin: 0 auto;
margin-top: 25px;
border: 1px solid rgb(245, 245, 245);
border-radius: 4px;
}
.custom-card {
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
transition: box-shadow 0.3s ease-in-out;
width: 80%;
margin: 0 auto;
border: 1px solid rgb(245, 245, 245);
}
.custom-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
cursor: pointer;
background-color: rgb(245, 245, 245);
}
.card-content {
padding: 16px;
}
.card-title {
font-size: 1.2em;
margin-bottom: 8px;
color: #333;
}
.card-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
color: #666;
}
.card-rating {
color: #666;
}
.card-rating {
color: #666;
}
</style>
四、后端代码
4.1项目后端依赖库
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-spring-web</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.8.2</version>
</dependency>
<!--okhttp3 依赖-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<!-- Lombok dependency -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 验证码模块-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
4.2工具类Result类与实体类
public class Result<T> {
// 状态码常量
public static final int SUCCESS = 200;
public static final int ERROR = 500;
private int code; // 状态码
private String message; // 消息
private T data; // 数据
// 构造函数,用于创建成功的结果对象
private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
// 成功结果的静态方法
public static <T> Result<T> success(T data) {
return new Result<>(SUCCESS, "Success", data);
}
// 错误结果的静态方法
public static <T> Result<T> error(String message) {
return new Result<>(ERROR, message, null);
}
// 错误结果的静态方法,可以传入自定义的状态码
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
// 获取状态码
public int getCode() {
return code;
}
// 设置状态码
public void setCode(int code) {
this.code = code;
}
// 获取消息
public String getMessage() {
return message;
}
// 设置消息
public void setMessage(String message) {
this.message = message;
}
// 获取数据
public T getData() {
return data;
}
// 设置数据
public void setData(T data) {
this.data = data;
}
// 用于转换为Map类型的方法,方便序列化为JSON
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("code", code);
map.put("message", message);
map.put("data", data);
return map;
}
}
Entity:
Forumpost:
@TableName(value ="forum_posts")
@Data
public class ForumPosts implements Serializable {
/**
*
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
*
*/
@TableField(value = "title")
private String title;
/**
*
*/
@TableField(value = "content")
private String content;
/**
*
*/
@TableField(value = "author_id")
private Integer authorId;
/**
*
*/
@TableField(value = "created_at")
private LocalDateTime createdAt;
/**
*
*/
@TableField(value = "updated_at")
private LocalDateTime updatedAt;
/**
*
*/
@TableField(value = "heat_value")
private Integer heatValue;
/**
*
*/
@TableField(value = "rating")
private BigDecimal rating;
/**
*
*/
@TableField(value = "tag")
private String tag;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
ForumPosts other = (ForumPosts) that;
return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
&& (this.getTitle() == null ? other.getTitle() == null : this.getTitle().equals(other.getTitle()))
&& (this.getContent() == null ? other.getContent() == null : this.getContent().equals(other.getContent()))
&& (this.getAuthorId() == null ? other.getAuthorId() == null : this.getAuthorId().equals(other.getAuthorId()))
&& (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()))
&& (this.getUpdatedAt() == null ? other.getUpdatedAt() == null : this.getUpdatedAt().equals(other.getUpdatedAt()))
&& (this.getHeatValue() == null ? other.getHeatValue() == null : this.getHeatValue().equals(other.getHeatValue()))
&& (this.getRating() == null ? other.getRating() == null : this.getRating().equals(other.getRating()))
&& (this.getTag() == null ? other.getTag() == null : this.getTag().equals(other.getTag()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
result = prime * result + ((getTitle() == null) ? 0 : getTitle().hashCode());
result = prime * result + ((getContent() == null) ? 0 : getContent().hashCode());
result = prime * result + ((getAuthorId() == null) ? 0 : getAuthorId().hashCode());
result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());
result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode());
result = prime * result + ((getHeatValue() == null) ? 0 : getHeatValue().hashCode());
result = prime * result + ((getRating() == null) ? 0 : getRating().hashCode());
result = prime * result + ((getTag() == null) ? 0 : getTag().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", title=").append(title);
sb.append(", content=").append(content);
sb.append(", authorId=").append(authorId);
sb.append(", createdAt=").append(createdAt);
sb.append(", updatedAt=").append(updatedAt);
sb.append(", heatValue=").append(heatValue);
sb.append(", rating=").append(rating);
sb.append(", tag=").append(tag);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
ForumPostLike:
@Data
@TableName(value ="forum_post_likes")
public class ForumPostLike {
private int userId;
private int postId;
}
ForumPostFavorites:
@Data
@TableName(value ="forum_post_favorites")
public class ForumPostFavorites {
private int userId;
private int postId;
}
ForumComments
@TableName(value ="forum_comments")
@Data
public class ForumComments implements Serializable {
/**
*
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
*
*/
@TableField(value = "post_id")
private Integer postId;
/**
*
*/
@TableField(value = "user_id")
private Integer userId;
/**
*
*/
@TableField(value = "comment_text")
private String commentText;
/**
*
*/
@TableField(value = "created_at")
private LocalDateTime createdAt;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
ForumComments other = (ForumComments) that;
return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
&& (this.getPostId() == null ? other.getPostId() == null : this.getPostId().equals(other.getPostId()))
&& (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
&& (this.getCommentText() == null ? other.getCommentText() == null : this.getCommentText().equals(other.getCommentText()))
&& (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
result = prime * result + ((getPostId() == null) ? 0 : getPostId().hashCode());
result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
result = prime * result + ((getCommentText() == null) ? 0 : getCommentText().hashCode());
result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", postId=").append(postId);
sb.append(", userId=").append(userId);
sb.append(", commentText=").append(commentText);
sb.append(", createdAt=").append(createdAt);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
DTO:
@Data
public class CommentDTO {
private int userId;
private int postId;
private String commentText;
}
@Data
public class ForumAddPostDTO {
@JsonProperty("title")
private String title;
@JsonProperty("content")
private String content;
@JsonProperty("authorId")
private Integer authorId;
@JsonProperty("tag")
private String tag;
}
VO:
@Data
public class ArticleVO {
private String title;
private String content;
private String account;
private Integer heatValue;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
}
@Data
public class CommentVo {
private int id;
private String commentText;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
private String account;
}
@Data
public class LikeSearchVo {
private String value;
}
4.3 自定义异常与全局异常
public class BaseException extends RuntimeException{
public BaseException(){
}
public BaseException(String msg){
super(msg);
}
}
public class NotFoundArticleException extends BaseException{
public NotFoundArticleException(String msg){
super(msg);
}
}
public class AlreadyLikeException extends BaseException{
public AlreadyLikeException(String msg){
super(msg);
}
}
全局异常处理类:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
}
4.4Controller层
1.ForumPostController
@RequestMapping("/forum")
@RestController
@Api(tags = "文章管理")
@Slf4j
public class ForumPostController {
@Autowired
ForumPostsService forumPostsService;
@Autowired
UserService userService;
@Autowired
ForumCommentsService forumCommentsService;
@ApiOperation("新增文章")
@PostMapping("/add")
public Result addForumPost(@RequestBody ForumAddPostDTO forumAddPostDTO) throws ParseException {
ForumPosts forumPosts = new ForumPosts();
BeanUtils.copyProperties(forumAddPostDTO, forumPosts);
forumPostsService.save(forumPosts);
return Result.success("新增成功");
}
@GetMapping("/getAllForumPost")
@ApiOperation("推荐文章查询")
public Result getAllForumPost(@RequestParam(value="pageSize", defaultValue = "10") int pageSize,
@RequestParam(value="pageNumber", defaultValue = "1") int pageNumber){
Page<ForumPosts> page = new Page<>(pageNumber, pageSize);
// 创建查询包装器并指定排序规则
QueryWrapper<ForumPosts> queryWrapper = new QueryWrapper<>();
// 假设你想按照创建时间降序排序
queryWrapper.orderByDesc("created_at");
Page<ForumPosts> paged = forumPostsService.page(page,queryWrapper);
List<ForumPosts> postsList = paged.getRecords();
List<ForumPageVO> posts = new ArrayList<>();
for (ForumPosts post : postsList) {
ForumPageVO vo = new ForumPageVO();
// 获取账户信息
User user = userService.getById(post.getAuthorId());
vo.setAccount(user.getAccount());
// 直接从ForumPosts对象复制其他字段
vo.setTag(post.getTag());
vo.setRating(post.getRating()); // 如果需要字符串形式
vo.setTitle(post.getTitle());
vo.setPostId(post.getId());
vo.setHeat_value(post.getHeatValue());
// 添加到列表
posts.add(vo);
}
return Result.success(posts);
}
// 文章阅读
@ApiOperation("读取文章")
@GetMapping("post/{id}")
public Result<ArticleVO> readArtical(@PathVariable int id){
ArticleVO articleVO = forumPostsService.readArticle(id);
return Result.success(articleVO);
}
// 我的文章功能
@ApiOperation("我的文章")
@GetMapping("/MyArticle")
public Result getMyArticle(@Param("id") int id){
List<ForumPosts> forumPostsList = forumPostsService.getByAuthorId(id);
return Result.success(forumPostsList);
}
// 热门文章功能
@ApiOperation("热门文章")
@GetMapping("/getHotPosts")
public Result getHotPosts(){
QueryWrapper<ForumPosts> queryWrapper = new QueryWrapper();
queryWrapper.orderByDesc("heat_value");
List<ForumPosts> forumPosts = forumPostsService.list(queryWrapper);
return Result.success(forumPosts);
}
// 热门文章功能
@ApiOperation("优质文章")
@GetMapping("/getOutStandPosts")
public Result getOutStandPosts(){
QueryWrapper<ForumPosts> queryWrapper = new QueryWrapper();
queryWrapper.orderByDesc("rating");
List<ForumPosts> forumPosts = forumPostsService.list(queryWrapper);
return Result.success(forumPosts);
}
// 我的收藏
@ApiOperation("我的收藏")
@GetMapping("/getMyFavorite")
public Result getMyFavorite(@Param("id") int id){
List<ForumPosts> forumPostsList = forumPostsService.getMyFavorite(id);
return Result.success(forumPostsList);
}
// 文章查询
@ApiOperation("文章查询")
@GetMapping("/search")
public Result postSearch(@RequestParam("searchKeyWord") String searchText){
if (searchText == null){
return Result.error("不能输入为空噢");
}
LambdaQueryWrapper<ForumPosts> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// TODO:通过LamdaQueryWrapper 模糊查询
// 模糊查询title字段,%searchText%会被自动添加
lambdaQueryWrapper.like(ForumPosts::getTitle, searchText);
// 假设这里有一个service接口用于操作ForumPosts表
List<ForumPosts> postsList = forumPostsService.list(lambdaQueryWrapper);
// 根据你的Result类的具体实现,返回查询结果
return Result.success(postsList);
}
/**
* 标题模糊查询
* @param keyword 关键词
* @return 匹配的标题列表
*/
@GetMapping("/getLikeSearch")
@ApiOperation("标题模糊查询")
public Result getLikeSearch(@RequestParam String keyword){
List<LikeSearchVo> titles = forumPostsService.findTitlesByKeyword(keyword);
return Result.success(titles);
}
// 用户交互
// 点赞
@ApiOperation("点赞")
@GetMapping("/like")
public Result PostLike(@RequestParam("postId") int postId,@RequestParam("userId") int userId){
String result = forumPostsService.PostLike(postId,userId);
return Result.success(result);
}
// 收藏
@ApiOperation("收藏")
@GetMapping("/favorite")
public Result PostFavorite(@RequestParam("postId") int postId,@RequestParam("userId") int userId){
String result = forumPostsService.PostFavorite(postId,userId);
return Result.success(result);
}
// 写评论
@ApiOperation("写评论")
@PostMapping("/writeComment")
public Result WriteComment(@RequestBody CommentDTO commentdto){
ForumComments forumComments = new ForumComments();
BeanUtils.copyProperties(commentdto,forumComments);
forumCommentsService.save(forumComments);
return Result.success("评论成功");
}
// 读评论
@ApiOperation("读取评论")
@GetMapping("/getComment")
public Result<List<CommentVo>> GetComment(@RequestParam("postId") int postId){
List<CommentVo> comment = forumCommentsService.getComment(postId);
return Result.success(comment);
}
}
4.5 Service层
public interface ForumPostsService extends IService<ForumPosts> {
List<ForumPosts> getByAuthorId(Integer id);
String PostLike(int postId, int userId);
String PostFavorite(int postId, int userId);
ArticleVO readArticle(int id);
List<ForumPosts> getMyFavorite(int id);
List<LikeSearchVo> findTitlesByKeyword(String keyword);
}
public interface ForumCommentsService extends IService<ForumComments> {
List<CommentVo> getComment(int postId);
}
4.6 Mapper层
public interface ForumCommentsMapper extends BaseMapper<ForumComments> {
}
@Mapper
public interface ForumPostFavoritesMapper extends BaseMapper<ForumPostFavorites> {
}
@Mapper
public interface ForumPostLikeMapper extends BaseMapper<ForumPostLike> {
}
@Mapper
public interface ForumPostsMapper extends BaseMapper<ForumPosts> {
@Select("select * from paitool.user as a,paitool.forum_posts as b where a.id=b.author_id and a.id = #{id}")
List<ForumPosts> getByAuthorId(Integer id);
}
五、进阶思路
1.通过ElasticSearch优化搜索引擎
2.使用Redis存储热门文章,以减少数据库压力
3.通过若依框架+AI 完善管理系统