一.上传之前的配置
1.上传js和css文件
在minio中创建leadnews桶,
在leadnews下面创建/plugins目录,在该目录下面分别创建js和css目录,
也就是/plugins/css和/plugins/js,向css中上传以下index.css:
html {overflow-x: hidden;
}#app {position: relative;width: 750px;margin: 0 auto;color: #333;background-color: #f8f8f8;
}.article {padding: 0 40px 120px;
}.article-title {margin-top: 48px;font-size: 40px;font-weight: bold;color: #3A3A3A;line-height: 65px;
}.article-header {margin-top: 57px;
}.article-content {margin-top: 39px;
}.article-avatar {width: 70px;height: 70px;
}.article-author {font-size: 28px;font-weight: 400;color: #3A3A3A;
}.article-publish-time {font-size: 24px;font-weight: 400;color: #B4B4B4;
}.article-focus {width: 170px;height: 58px;font-size: 28px;font-weight: 400;color: #FFFFFF;
}.article-text {font-size: 32px;font-weight: 400;color: #3A3A3A;line-height: 56px;text-align: justify;
}.article-action {margin-top: 59px;
}.article-like {width: 156px;height: 58px;font-size: 25px;font-weight: 400;color: #777777;
}.article-unlike {width: 156px;height: 58px;margin-left: 42px;font-size: 25px;font-weight: 400;color: #E22829;
}.article-comment {margin-top: 69px;
}.comment-author {font-size: 24px;font-weight: 400;color: #777777;line-height: 49px;
}.comment-content {font-size: 32px;font-weight: 400;color: #3A3A3A;line-height: 49px;
}.comment-time {font-size: 24px;font-weight: 400;color: #B4B4B4;line-height: 49px;
}.article-comment-reply {padding: 40px;
}.article-bottom-bar, .comment-reply-bottom-bar {position: fixed;bottom: 0;width: 750px;height: 99px;background: #F4F5F6;
}.article-bottom-bar .van-field, .comment-reply-bottom-bar .van-field {width: 399px;height: 64px;background: #FFFFFF;border: 2px solid #EEEEEE;border-radius: 32px;font-size: 25px;font-weight: 400;color: #777777;
}.article-bottom-bar .van-button, .comment-reply-bottom-bar .van-button {background-color: transparent;border-color: transparent;font-size: 25px;font-weight: 400;color: #777777;
}
在/plugins/js目录下上传以下index.js文件
// 初始化 Vue 实例
new Vue({el: '#app',data() {return {// Minio模板应该写真实接口地址baseUrl: 'http://192.168.200.150:51601', //'http://172.16.17.191:5001',token: '',equipmentId: '',articleId: '',title: '',authorId: 0,authorName: '',publishTime: '',relation: {islike: false,isunlike: false,iscollection: false,isfollow: false,isforward: false},followLoading: false,likeLoading: false,unlikeLoading: false,collectionLoading: false,// 评论comments: [],commentsLoading: false,commentsFinished: false,commentValue: '',currentCommentId: '',// 评论回复commentReplies: [],commentRepliesLoading: false,commentRepliesFinished: false,commentReplyValue: '',showPopup: false}},filters: {// TODO: js计算时间差timestampToDateTime: function (value) {if (!value) return ''const date = new Date(value)const Y = date.getFullYear() + '-'const M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'const D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' 'const h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':'const m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':'const s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds())return Y + M + D + h + m + s}},created() {this.token = this.getQueryVariable('token')this.equipmentId = this.getQueryVariable('equipmentId')this.articleId = this.getQueryVariable('articleId')this.title = this.getQueryVariable('title')const authorId = this.getQueryVariable('authorId')if (authorId) {this.authorId = parseInt(authorId, 10)}this.authorName = this.getQueryVariable('authorName')const publishTime = this.getQueryVariable('publishTime')if (publishTime) {this.publishTime = parseInt(publishTime, 10)}this.loadArticleBehavior()this.readArticleBehavior()},methods: {// 加载文章评论async loadArticleComments(index = 1, minDate = 20000000000000) {const url = `${this.baseUrl}/comment/api/v1/comment/load`const data = { articleId: this.articleId, index: index, minDate: minDate }const config = { headers: { 'token': this.token } }try {const { status, data: { code, errorMessage, data: comments } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}if (comments.length) {this.comments = this.comments.concat(comments)}// 加载状态结束this.commentsLoading = false;// 数据全部加载完成if (!comments.length) {this.commentsFinished = true}} catch (err) {this.commentsLoading = falsethis.commentsFinished = trueconsole.log('err: ' + err)}},// 滚动加载文章评论onLoadArticleComments() {let index = undefinedlet minDate = undefinedif (this.comments.length) {index = 2minDate = this.comments[this.comments.length - 1].createdTime}this.loadArticleComments(index, minDate)},// 加载文章行为async loadArticleBehavior() {const url = `${this.baseUrl}/article/api/v1/article/load_article_behavior/`const data = { equipmentId: this.equipmentId, articleId: this.articleId, authorId: this.authorId }const config = { headers: { 'token': this.token } }try {const { status, data: { code, errorMessage, data: relation } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}this.relation = relation} catch (err) {console.log('err: ' + err)}},//阅读文章行为async readArticleBehavior(){const url = `${this.baseUrl}/behavior/api/v1/read_behavior`const data = {equipmentId:this.equipmentId,articleId:this.articleId,count:1,readDuration:0,percentage:0,loadDuration:0}const config = {headers:{'token':this.token}}try{const {status,data:{code,errorMessage}} = await axios.post(url,data,config)if(status !== 200){vant.Toast.fail("当前系统正在维护,请稍后重试")return}if(code !== 0){vant.Toast.fail(errorMessage)return}}catch (err){console.log('err: '+ err)}},// 关注/取消关注async handleClickArticleFollow() {const url = `${this.baseUrl}/user/api/v1/user/user_follow/`const data = { authorId: this.authorId, operation: this.relation.isfollow ? 1 : 0, articleId: this.articleId }const config = { headers: { 'token': this.token } }this.followLoading = truetry {const { status, data: { code, errorMessage } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}this.relation.isfollow = !this.relation.isfollowvant.Toast.success(this.relation.isfollow ? '成功关注' : '成功取消关注')} catch (err) {console.log('err: ' + err)}this.followLoading = false},// 点赞/取消赞async handleClickArticleLike() {const url = `${this.baseUrl}/behavior/api/v1/likes_behavior/`const data = { equipmentId: this.equipmentId, articleId: this.articleId, type: 0, operation: this.relation.islike ? 1 : 0 }const config = { headers: { 'token': this.token } }this.likeLoading = truetry {const { status, data: { code, errorMessage } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}this.relation.islike = !this.relation.islikevant.Toast.success(this.relation.islike ? '点赞操作成功' : '取消点赞操作成功')} catch (err) {console.log('err: ' + err)}this.likeLoading = false},// 不喜欢/取消不喜欢async handleClickArticleUnlike() {const url = `${this.baseUrl}/behavior/api/v1/un_likes_behavior/`const data = { equipmentId: this.equipmentId, articleId: this.articleId, type: this.relation.isunlike ? 1 : 0 }const config = { headers: { 'token': this.token } }this.unlikeLoading = truetry {const { status, data: { code, errorMessage } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}this.relation.isunlike = !this.relation.isunlikevant.Toast.success(this.relation.isunlike ? '不喜欢操作成功' : '取消不喜欢操作成功')} catch (err) {console.log('err: ' + err)}this.unlikeLoading = false},// 提交评论async handleSaveComment() {if (!this.commentValue) {vant.Toast.fail('评论内容不能为空')return}if (this.commentValue.length > 140) {vant.Toast.fail('评论字数不能超过140字')return}const url = `${this.baseUrl}/comment/api/v1/comment/save`const data = { articleId: this.articleId, content: this.commentValue }const config = { headers: { 'token': this.token } }try {const { status, data: { code, errorMessage } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}vant.Toast.success('评论成功')this.commentValue = ''this.comments = []this.loadArticleComments()this.commentsFinished = false;} catch (err) {console.log('err: ' + err)}},// 页面滚动到评论区handleScrollIntoCommentView() {document.getElementById('#comment-view').scrollIntoView({ behavior: 'smooth' })},// 收藏/取消收藏async handleClickArticleCollection() {const url = `${this.baseUrl}/article/api/v1/collection_behavior/`const data = { equipmentId: this.equipmentId, entryId: this.articleId, publishedTime: this.publishTime, type: 0, operation: this.relation.iscollection ? 1 :0 }const config = { headers: { 'token': this.token } }this.collectionLoading = truetry {const { status, data: { code, errorMessage } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}this.relation.iscollection = !this.relation.iscollectionvant.Toast.success(this.relation.iscollection ? '收藏操作成功' : '取消收藏操作成功')} catch (err) {console.log('err: ' + err)}this.collectionLoading = false},// 评论点赞async handleClickCommentLike(comment) {const commentId = comment.idconst operation = comment.operation === 0 ? 1 : 0const url = `${this.baseUrl}/comment/api/v1/comment/like`const data = { commentId: comment.id, operation: operation }const config = { headers: { 'token': this.token } }try {const { status, data: { code, errorMessage, data: { likes } } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}const item = this.comments.find((item) => {return item.id === commentId})item.operation = operationitem.likes = likesvant.Toast.success((operation === 0 ? '点赞' : '取消点赞') + '操作成功!')} catch (err) {console.log('err: ' + err)}},// 弹出评论回复PopupshowCommentRepliesPopup(commentId) {this.showPopup = true;this.currentCommentId = commentIdthis.commentReplies = []this.commentRepliesFinished = false},// 加载评论回复async loadCommentReplies(minDate = 20000000000000) {const url = `${this.baseUrl}/comment/api/v1/comment_repay/load`const data = { commentId: this.currentCommentId, 'minDate': minDate}const config = { headers: { 'token': this.token } }try {const { status, data: { code, errorMessage, data: commentReplies } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}if (commentReplies.length) {this.commentReplies = this.commentReplies.concat(commentReplies)}// 加载状态结束this.commentRepliesLoading = false;// 数据全部加载完成if (!commentReplies.length) {this.commentRepliesFinished = true}} catch (err) {this.commentRepliesLoading = falsethis.commentRepliesFinished = trueconsole.log('err: ' + err)}},// 滚动加载评论回复onLoadCommentReplies() {let minDate = undefinedif (this.commentReplies.length) {minDate = this.commentReplies[this.commentReplies.length - 1].createdTime}this.loadCommentReplies(minDate)},// 提交评论回复async handleSaveCommentReply() {if (!this.commentReplyValue) {vant.Toast.fail('评论内容不能为空')return}if (this.commentReplyValue.length > 140) {vant.Toast.fail('评论字数不能超过140字')return}const url = `${this.baseUrl}/comment/api/v1/comment_repay/save`const data = { commentId: this.currentCommentId, content: this.commentReplyValue }const config = { headers: { 'token': this.token } }try {const { status, data: { code, errorMessage } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}vant.Toast.success('评论成功')this.commentReplyValue = ''this.commentReplies = []this.comments = []// 刷新评论回复列表this.loadCommentReplies()// 刷新文章评论列表this.loadArticleComments()} catch (err) {console.log('err: ' + err)}},// 评论回复点赞async handleClickCommentReplyLike(commentReply) {const commentReplyId = commentReply.idconst operation = commentReply.operation === 0 ? 1 : 0const url = `${this.baseUrl}/comment/api/v1/comment_repay/like`const data = { commentRepayId: commentReplyId, 'operation': operation }const config = { headers: { 'token': this.token } }try {const { status, data: { code, errorMessage, data: { likes } } } = await axios.post(url, data, config)if (status !== 200) {vant.Toast.fail('当前系统正在维护,请稍后重试')return}if (code !== 200) {vant.Toast.fail(errorMessage)return}const item = this.commentReplies.find((item) => {return item.id === commentReplyId})item.operation = operationitem.likes = likesvant.Toast.success((operation === 0 ? '点赞' : '取消点赞') + '操作成功!')} catch (err) {console.log('err: ' + err)}},getQueryVariable(aVariable) {const query = decodeURI(window.location.search).substring(1)const array = query.split('&')for (let i = 0; i < array.length; i++) {const pair = array[i].split('=')if (pair[0] == aVariable) {return pair[1]}}return undefined},// onSelect(option) {// vant.Toast(option.name);// this.showShare = false;// }}
})
2.在article微服务模块的resources目录下面创建/templates目录,创建articel.ftl模版文件:
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport"content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"><title>黑马头条</title><!-- 引入样式文件 --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vant@2.12.20/lib/index.css"><!-- 页面样式 --><link rel="stylesheet" href="../../../plugins/css/index.css">
</head><body>
<div id="app"><div class="article"><van-row><van-col span="24" class="article-title" v-html="title"></van-col></van-row><van-row type="flex" align="center" class="article-header"><van-col span="3"><van-image round class="article-avatar" src="https://p3.pstatp.com/thumb/1480/7186611868"></van-image></van-col><van-col span="16"><div v-html="authorName"></div><div>{{ publishTime | timestampToDateTime }}</div></van-col><van-col span="5"><van-button round :icon="relation.isfollow ? '' : 'plus'" type="info" class="article-focus":text="relation.isfollow ? '取消关注' : '关注'" :loading="followLoading" @click="handleClickArticleFollow"></van-button></van-col></van-row><van-row class="article-content"><#if content??><#list content as item><#if item.type='text'><van-col span="24" class="article-text">${item.value}</van-col><#else><van-col span="24" class="article-image"><van-image width="100%" src="${item.value}"></van-image></van-col></#if></#list></#if></van-row><van-row type="flex" justify="center" class="article-action"><van-col><van-button round :icon="relation.islike ? 'good-job' : 'good-job-o'" class="article-like":loading="likeLoading" :text="relation.islike ? '取消赞' : '点赞'" @click="handleClickArticleLike"></van-button><van-button round :icon="relation.isunlike ? 'delete' : 'delete-o'" class="article-unlike":loading="unlikeLoading" @click="handleClickArticleUnlike">不喜欢</van-button></van-col></van-row><!-- 文章评论列表 --><van-list v-model="commentsLoading" :finished="commentsFinished" finished-text="没有更多了"@load="onLoadArticleComments"><van-row id="#comment-view" type="flex" class="article-comment" v-for="(item, index) in comments" :key="index"><van-col span="3"><van-image round src="https://p3.pstatp.com/thumb/1480/7186611868" class="article-avatar"></van-image></van-col><van-col span="21"><van-row type="flex" align="center" justify="space-between"><van-col class="comment-author" v-html="item.authorName"></van-col><van-col><van-button round :icon="item.operation === 0 ? 'good-job' : 'good-job-o'" size="normal"@click="handleClickCommentLike(item)">{{ item.likes || '' }}</van-button></van-col></van-row><van-row><van-col class="comment-content" v-html="item.content"></van-col></van-row><van-row type="flex" align="center"><van-col span="10" class="comment-time">{{ item.createdTime | timestampToDateTime }}</van-col><van-col span="3"><van-button round size="normal" v-html="item.reply" @click="showCommentRepliesPopup(item.id)">回复 {{item.reply || '' }}</van-button></van-col></van-row></van-col></van-row></van-list></div><!-- 文章底部栏 --><van-row type="flex" justify="space-around" align="center" class="article-bottom-bar"><van-col span="13"><van-field v-model="commentValue" placeholder="写评论"><template #button><van-button icon="back-top" @click="handleSaveComment"></van-button></template></van-field></van-col><van-col span="3"><van-button icon="comment-o" @click="handleScrollIntoCommentView"></van-button></van-col><van-col span="3"><van-button :icon="relation.iscollection ? 'star' : 'star-o'" :loading="collectionLoading"@click="handleClickArticleCollection"></van-button></van-col><van-col span="3"><van-button icon="share-o"></van-button></van-col></van-row><!-- 评论Popup 弹出层 --><van-popup v-model="showPopup" closeable position="bottom":style="{ width: '750px', height: '60%', left: '50%', 'margin-left': '-375px' }"><!-- 评论回复列表 --><van-list v-model="commentRepliesLoading" :finished="commentRepliesFinished" finished-text="没有更多了"@load="onLoadCommentReplies"><van-row id="#comment-reply-view" type="flex" class="article-comment-reply"v-for="(item, index) in commentReplies" :key="index"><van-col span="3"><van-image round src="https://p3.pstatp.com/thumb/1480/7186611868" class="article-avatar"></van-image></van-col><van-col span="21"><van-row type="flex" align="center" justify="space-between"><van-col class="comment-author" v-html="item.authorName"></van-col><van-col><van-button round :icon="item.operation === 0 ? 'good-job' : 'good-job-o'" size="normal"@click="handleClickCommentReplyLike(item)">{{ item.likes || '' }}</van-button></van-col></van-row><van-row><van-col class="comment-content" v-html="item.content"></van-col></van-row><van-row type="flex" align="center"><!-- TODO: js计算时间差 --><van-col span="10" class="comment-time">{{ item.createdTime | timestampToDateTime }}</van-col></van-row></van-col></van-row></van-list><!-- 评论回复底部栏 --><van-row type="flex" justify="space-around" align="center" class="comment-reply-bottom-bar"><van-col span="13"><van-field v-model="commentReplyValue" placeholder="写评论"><template #button><van-button icon="back-top" @click="handleSaveCommentReply"></van-button></template></van-field></van-col><van-col span="3"><van-button icon="comment-o"></van-button></van-col><van-col span="3"><van-button icon="star-o"></van-button></van-col><van-col span="3"><van-button icon="share-o"></van-button></van-col></van-row></van-popup>
</div><!-- 引入 Vue 和 Vant 的 JS 文件 -->
<script src=" https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js">
</script>
<script src="https://cdn.jsdelivr.net/npm/vant@2.12.20/lib/vant.min.js"></script>
<!-- 引入 Axios 的 JS 文件 -->
<#--<script src="https://unpkg.com/axios/dist/axios.min.js"></script>-->
<script src="../../../plugins/js/axios.min.js"></script>
<!-- 页面逻辑 -->
<script src="../../../plugins/js/index.js"></script>
</body></html>
3.测试文章内容生成html文件上传到minio
3.1.查看文章内容格式
我们把数据库中的文章内容格式化一下:
[{"type": "text","value": "杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸"},{"type": "image","value": "http://192.168.200.130/group1/M00/00/00/wKjIgl892wKAZLhtAASZUi49De0836.jpg"},{"type": "text","value": "杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸杨澜回应一秒变脸"},{"type": "text","value": "请在这里输入正文"}
]
我们发现当type为text时,是文本内容,当type是image时,是图片,所以可以通过判断来组建一个html文章网页
3.2测试文章生成上传Minio
@SpringBootTest(classes = ArticleApplication.class)
@RunWith(SpringRunner.class)
public class ArticleFreemarkerTest {@Autowiredprivate Configuration configuration;@Autowiredprivate MinIoTemplate minIoTemplate;@Autowiredprivate ApArticleMapper apArticleMapper;@Autowiredprivate ApArticleContentMapper apArticleContentMapper;@Testpublic void createStaticUrlTest() throws Exception {//1.获取文章内容ApArticleContent apArticleContent =apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, 1302977558807060482L));//TODOSystem.out.println(apArticleContent);if(apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())){//2.文章内容通过freemarker生成html文件StringWriter out = new StringWriter();Template template = configuration.getTemplate("article.ftl");Map<String, Object> params = new HashMap<>();params.put("content", JSONArray.parseArray(apArticleContent.getContent()));template.process(params, out);InputStream is = new ByteArrayInputStream(out.toString().getBytes());//3.把html文件上传到minio中String path = minIoTemplate.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", is);//TODOSystem.out.println("上传成功:" + path);//4.修改ap_article表,保存static_url字段ApArticle article = new ApArticle();article.setId(apArticleContent.getArticleId());article.setStaticUrl(path);apArticleMapper.updateById(article);System.out.println(path);}}
}
流程:
1.先从数据库中根据文章id从文章内容表中将文章内容查询出来返回封装为文章对象
2.通过freemarker将文章对象中的值和模版对应,生成一个html静态页面
3.然后将该页面上传到minio中,返回上传成功后访问的url路径
4.在把文章信息表中的url路径修改为这个路径
5.之后前端在访问文章详情的时候,就可以通过该路径url去minio中获取html页面了
因为有些文件它在线访问很慢可能被拒绝访问,所以我们把模版文件中需要引入的文件都上传到minio里面:
还有一个vant的css文件
在加上之前的css文件,一共是6个文件,后面我会把这六个文件上传到资源里面,按照目录结构排版好,以及article.ftl模版文件
等我们都配置好之后,我们再去访问文章详情:
可以看到已经成功访问到文章详情了,以及里面包含的图片,