豆宝社区项目实战教程简介
本项目实战教程配有免费视频教程,配套代码完全开源。手把手从零开始搭建一个目前应用最广泛的Springboot+Vue前后端分离多用户社区项目。本项目难度适中,为便于大家学习,每一集视频教程对应在Github上的每一次提交。
项目首页截图
代码开源地址
视频教程地址
前端技术栈
Vue Vuex Vue Router Axios Bulma Buefy Element Vditor DarkReader
后端技术栈
Spring Boot Mysql Mybatis MyBatis-Plus Spring Security JWT Lombok
##前端
1.src\api\修改post.js
// 获取文章详情
export function getTopic(id) {
return request({
url: `/post`,
method: 'get',
params: {
id: id
}
})
}
2.路由
// 文章详情
{
name: "post-detail",
path: "/post/:id",
component: () => import("@/views/post/Detail"),
meta: { title: "详情" },
}
3.src\views\post\创建Detail.vue
<template>
<div class="columns">
<!--文章详情-->
<div class="column is-three-quarters">
<!--主题-->
<el-card
class="box-card"
shadow="never"
>
<div
slot="header"
class="has-text-centered"
>
<p class="is-size-5 has-text-weight-bold">{{ topic.title }}</p>
<div class="has-text-grey is-size-7 mt-3">
<span>{{ dayjs(topic.createTime).format('YYYY/MM/DD HH:mm:ss') }}</span>
<el-divider direction="vertical" />
<span>发布者:{{ topicUser.alias }}</span>
<el-divider direction="vertical" />
<span>查看:{{ topic.view }}</span>
</div>
</div>
<!--Markdown-->
<div id="preview" />
<!--标签-->
<nav class="level has-text-grey is-size-7 mt-6">
<div class="level-left">
<p class="level-item">
<b-taglist>
<router-link
v-for="(tag, index) in tags"
:key="index"
:to="{ name: 'tag', params: { name: tag.name } }"
>
<b-tag type="is-info is-light mr-1">
{{ "#" + tag.name }}
</b-tag>
</router-link>
</b-taglist>
</p>
</div>
<div
v-if="token && user.id === topicUser.id"
class="level-right"
>
<router-link
class="level-item"
:to="{name:'topic-edit',params: {id:topic.id}}"
>
<span class="tag">编辑</span>
</router-link>
<a class="level-item">
<span
class="tag"
@click="handleDelete(topic.id)"
>删除</span>
</a>
</div>
</nav>
</el-card>
</div>
<div class="column">
作者信息
</div>
</div>
</template>
<script>
import { deleteTopic, getTopic } from '@/api/post'
import { mapGetters } from 'vuex'
import Vditor from 'vditor'
import 'vditor/dist/index.css'
export default {
name: 'TopicDetail',
computed: {
...mapGetters([
'token','user'
])
},
data() {
return {
flag: false,
topic: {
content: '',
id: this.$route.params.id
},
tags: [],
topicUser: {}
}
},
mounted() {
this.fetchTopic()
},
methods: {
renderMarkdown(md) {
Vditor.preview(document.getElementById('preview'), md, {
hljs: { style: 'github' }
})
},
// 初始化
async fetchTopic() {
getTopic(this.$route.params.id).then(response => {
const { data } = response
document.title = data.topic.title
this.topic = data.topic
this.tags = data.tags
this.topicUser = data.user
// this.comments = data.comments
this.renderMarkdown(this.topic.content)
this.flag = true
})
},
handleDelete(id) {
deleteTopic(id).then(value => {
const { code, message } = value
alert(message)
if (code === 200) {
setTimeout(() => {
this.$router.push({ path: '/' })
}, 500)
}
})
}
}
}
</script>
<style>
#preview {
min-height: 300px;
}
</style>
文章详情后端
vo
import lombok.Data;
@Data
public class ProfileVO {
/**
* 用户ID
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 别称
*/
private String alias;
/**
* 头像
*/
private String avatar;
/**
* 关注数
*/
private Integer followCount;
/**
* 关注者数
*/
private Integer followerCount;
/**
* 文章数
*/
private Integer topicCount;
/**
* 专栏数
*/
private Integer columns;
/**
* 评论数
*/
private Integer commentCount;
}
BmsFollow
@Data
@TableName("bms_follow")
public class BmsFollow implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 被关注人id
*/
@TableField("parent_id")
private String parentId;
/**
* 关注人id
*/
@TableField("follower_id")
private String followerId;
public BmsFollow() {
}
}
BmsFollowMapper
public interface BmsFollowMapper extends BaseMapper<BmsFollow> {
}
BmsPostController
/**
* 根据id获取文章详情
*
* @return
*/
@GetMapping
public ApiResult getPostById(@RequestParam("id") String id) {
Map<String, Object> map = postService.getPostById(id);
return ApiResult.success(map);
}
BmsPostservice
public Map<String, Object> getPostById(String id) {
HashMap<String, Object> map = new HashMap<>(16);
// 查询文章
BmsPost post = this.getById(id);
Assert.notNull(post,"当前文章不存在,或被删除");
// 查看 +1
post.setView(post.getView()+1);
this.updateById(post);
// 表情转码
post.setContent(EmojiParser.parseToUnicode(post.getContent()));
map.put("topic",post);
// 文章的标签
// 标签
QueryWrapper<BmsPostTag> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(BmsPostTag::getPostId, post.getId());
Set<String> set = new HashSet<>();
for (BmsPostTag articleTag : postTagService.list(wrapper)) {
set.add(articleTag.getTagId());
}
List<BmsTag> tags = tagService.listByIds(set);
map.put("tags", tags);
// 作者
ProfileVO user = userService.getUserProfile(post.getUserId());
map.put("user", user);
return map;
}
UmsUserService
@Autowired
private BmsPostMapper postMapper;
@Autowired
private BmsFollowMapper followMapper;
public ProfileVO getUserProfile(String userId) {
ProfileVO profile = new ProfileVO();
UmsUser user = baseMapper.selectById(userId);
BeanUtils.copyProperties(user, profile);
// 用户文章数
int count = postMapper.selectCount(new LambdaQueryWrapper<BmsPost>().eq(BmsPost::getUserId, userId));
profile.setTopicCount(count);
// 粉丝数
int followers = followMapper.selectCount((new LambdaQueryWrapper<BmsFollow>().eq(BmsFollow::getParentId, userId)));
profile.setFollowerCount(followers);
return profile;
}