1.思考
一、文章列表页的三种布局方式?
超过三张图片:横排三张图(选最后三张图)
图片少于3张:图文左右排列(选最后一张图)
无图片:只显示文章内容(字数控制)
二、文章详情页的布局?
图文混排
三、关于文章模块的数据库设计问题?
1.文章表:id、u_id、title、content、create_time
2.图片表:id、a_id、img_url
3.评论表:id、u_id、a_id、content、comment_time
四、各层接口的设计?
2.数据库设计
-
t_article
-
t_comment
-
t_img
- 自行填充一些数据
t_article的content字段样例
<div>
<img src="http://hcoder.oss-cn-beijing.aliyuncs.com/public/images/xcp2.png" width="100%" />grace.hcoder.net<br />富文本可以展示html标签 _
</div>
3.entity
其中,Article、Img、Comment类参照以前写法,自行完成
其余vo包中的两个视图对象类如下:
4.mapper
- ArticleMapper接口,需要声明如下方法,自行实现
List<ArticleVO> selectAll();
ArticleVO getArticleById(int aId);
- ImgMapper接口
List<Img> selectImgsByAId(int aId);
- CommentMapper接口
List<CommentVO> selectCommentsByAId(int aId);
5.Service接口及实现、单元测试省略
6.Controller
- ArticleController
package com.soft1721.jianyue.api.controller;
import com.aliyun.oss.OSSClient;
import com.soft1721.jianyue.api.entity.Article;
import com.soft1721.jianyue.api.entity.Img;
import com.soft1721.jianyue.api.entity.User;
import com.soft1721.jianyue.api.entity.vo.ArticleVO;
import com.soft1721.jianyue.api.entity.vo.CommentVO;
import com.soft1721.jianyue.api.service.ArticleService;
import com.soft1721.jianyue.api.service.CommentService;
import com.soft1721.jianyue.api.service.ImgService;
import com.soft1721.jianyue.api.util.ResponseResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
@RestController
@RequestMapping(value = "/api/article")
public class ArticleController {
@Resource
private ArticleService articleService;
@Resource
private CommentService commentService;
@Resource
private ImgService imgService;
@GetMapping(value = "/list")
public ResponseResult getAll() {
List<ArticleVO> articleList = articleService.selectAll();
return ResponseResult.success(articleList);
}
@GetMapping(value = "/{aId}")
public ResponseResult getArticleById(@PathVariable("aId") int aId) {
ArticleVO article = articleService.getArticleById(aId);
List<CommentVO> comments = commentService.selectCommentsByAId(aId);
Map<String,Object> map = new HashMap<>();
map.put("article",article);
map.put("comments",comments);
return ResponseResult.success(map);
}
7.swagger测试
8.前端
- index.vue
<template>
<view class="container">
<view class="article-box">
<view class="article" v-for="(article,index) in articles" :key="index">
<!-- 标题 -->
<text class="article-title" @tap="gotoDetail(article.id)">{{article.title}}</text>
<!-- 大于等于三张图片的显示方式 -->
<view class="" v-if="article.imgs.length >= 3">
<view class="thumbnail-box">
<view class="thumbnail-item" v-for="(item,index1) in article.imgs" :key="index1" v-if="index1<3">
<image :src="item.imgUrl"></image>
</view>
</view>
</view>
<!-- 小于三张图片的显示方式 -->
<view class="" v-else-if="article.imgs.length >= 1">
<view class="text-img-box">
<view class="left">
<text>{{<!-- handleContent -->article.title}}...</text>
</view>
<view class="right">
<image :src="article.imgs[article.imgs.length - 1].imgUrl"></image>
</view>
</view>
</view>
<!-- 没有图片的显示方式 -->
<view class="text-box" v-else>
<text>{{<!-- handleContent -->article.title}}...</text>
</view>
<!-- 文章作者等信息 -->
<view class="article-info">
<image :src="article.avatar" class="avatar small"></image>
<text class="info-text">{{article.nickname}}</text>
<text class="info-text1">{{article.createTime}}</text>
</view>
</view>
</view>
<view>
<navigator url="../write/write" hover-class="navigator-hover" v-if="login" @tap="islogin()">
<button class="btn">+</button>
</navigator>
<navigator url="../signin/signin" hover-class="navigator-hover" @tap="islogin()" v-else>
<button class="btn">+</button>
</navigator>
</view>
</view>
</template>
<script>
export default {
data() {
return {
articles: [],
login:false
};
},
onLoad: function() {
this.getArticles();
},
onShow: function() {
const loginKey = uni.getStorageSync('login_key');
console.log(loginKey);
if (loginKey) {
this.login = true;
} else {
this.login = false;
}
},
onPullDownRefresh: function() {
this.getArticles();
},
methods: {
changeTab: function() {
uni.navigateTo({
url: '../write/write'
})
},
getArticles: function() {
var _this = this;
uni.request({
url: this.apiServer + '/article/list',
method: 'GET',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
success: res => {
_this.articles = res.data.data;
},
complete: function() {
uni.stopPullDownRefresh();
}
});
},
gotoDetail: function(aId) {
uni.navigateTo({
url: '../article_detail/article_detail?aId=' + aId
});
},
handleTime: function(createTime) {
var date = new Date(createTime);
var year = date.getFullYear();
var month =
date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
// 拼接
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
},
handleContent: function(content) {
content = content.replace(/(\n)/g, '');
content = content.replace(/(\t)/g, '');
content = content.replace(/(\r)/g, '');
content = content.replace(/<\/?[^>]*>/g, '');
content = content.replace(/\s*/g, '');
return content.substring(0, 50);
},
islogin: function() {
if (this.login) {
console.log('已登录');
} else {
console.log('未登录');
}
}
}
};
</script>
<style scoped>
/* 圆形按钮 */
.btn {
/* 阴影效果,四个参数分别是:水平阴影位置、垂直阴影位置、阴影尺寸位置大小、阴影颜色 */
box-shadow: 2px 5px 10px #AAA;
width: 65px;
height: 65px;
margin: 10px;
border-radius: 50%;
padding: 0;
cursor: pointer;
border: none;
outline: none;
bottom: 35px;
right: 20px;
background: linear-gradient(40deg, #ffd86f, #fc6262);
color: #FFF;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
font-size: 30px;
}
.icon-text {
color: #FFFFFF;
font-size: 35px;
}
.article-box {
height: auto;
width: 100%;
}
.article-title {
font-weight: bold;
}
.article {
display: flex;
flex-direction: column;
margin-top: 10px;
border-bottom: 1px solid #EEEEEE;
height: 210px;
}
.thumbnail-box {
margin-top: 10px;
display: flex;
height: 120px;
width: 100%;
}
.thumbnail-item {
flex: 1 1 33%;
height: 110px;
margin-right: 5px;
margin-left: 5px;
}
.thumbnail-item image {
width: 100%;
height: 100%;
}
.avatar {
width: 50px;
height: 50px;
}
.info-text {
margin-left: 10px;
font-size: 18px;
}
.info-text1 {
margin-left: 5px;
color: #CCCCCC;
font-size: 16px;
}
.text-img-box {
display: flex;
margin-top: 15px;
}
.text-box {
margin-top: 15px;
}
.left {
flex: 1 1 70%;
}
.right {
flex: 1 1 40%;
height: 110px;
}
.right image {
width: 100%;
height: 100%;
}
.article-info {
display: flex;
align-items: center;
}
</style>
- article_detail.vue
<template>
<view class="container">
<text class="article-title">{{ article.title }}</text>
<view class="article-info">
<image :src="article.avatar" class="avatar small"></image>
<text style="margin-left: 10px;">{{ article.nickname }}</text>
<text class="info-text">{{ handleTime(article.createTime)}}</text>
<!-- 登录用户和文章作者不是同一个人,就显示关注或取消关注按钮 -->
<button v-if="userId != article.uId && !followed" class="btn follow-btn" @tap="follow">+ 关注</button>
<button v-if="userId != article.uId && followed" class="btn follow-btn cancel" @tap="cancelFollow">取消</button>
</view>
<view class="grace-text" style="margin-top: 10px;">
<rich-text :nodes="article.content" bindtap="tap"></rich-text>
</view>
<button v-if="!liked" class="like-btn" @tap="like">收藏 </button>
<button v-if="liked" class="cancel-like" @tap="cancelLike">取消</button>
<text class="info-text">评论 {{ comments.length }}</text>
<view class="comment-item" v-for="(comment, index) in comments" :key="index">
<view class="left">
<image :src="comment.avatar" class="avatar small"></image>
</view>
<view class="right">
<view class="right-content">
<text>{{ comment.nickname }}</text>
<text>{{ comment.content }}</text>
</view>
<view class="right-time">
<text style="margin-right: 10px;">{{ comments.length - index }}楼·{{comment.commentTime}}</text>
<!-- <text>{{ handleTime(comment.commentTime)}}</text> -->
</view>
</view>
</view>
<input class="uni-input comment-box" type="text" placeholder="写下你的评论" v-model="content" required="required" />
<button class="green-btn" @tap="send">提交</button>
</view>
</template>
<script>
export default {
data() {
return {
article: {
aId: 0,
uId: 0,
title: '',
content: '',
avatar: '',
nickname: '',
createTime: ''
},
comments: [],
content: '',
userId: uni.getStorageSync('login_key').userId,
followed: false,
liked:false
};
},
onLoad: function(option) {
//option为object类型,会序列化上个页面传递的参数
this.article.aId = option.aId;
},
onShow: function() {
this.getArticle();
},
onPullDownRefresh: function() {
this.getArticle();
},
methods: {
getArticle: function() {
var _this = this;
uni.request({
url: this.apiServer + '/article/' + this.article.aId,
method: 'GET',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: {
userId: this.userId
},
success: res => {
// console.log(res.data.data.article);
_this.article.aId = res.data.data.article.id;
_this.article.uId = res.data.data.article.uid;
_this.article.title = res.data.data.article.title;
_this.article.content = res.data.data.article.content;
_this.article.nickname = res.data.data.article.nickname;
_this.article.avatar = res.data.data.article.avatar;
_this.article.createTime = res.data.data.article.createTime;
_this.comments = res.data.data.comments;
if (res.data.data.followed === '已关注') {
_this.followed = true;
}
},
complete: function() {
uni.stopPullDownRefresh();
}
});
},
handleTime: function(date) {
var d = new Date(date);
var year = d.getFullYear();
var month = d.getMonth() + 1;
var day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate();
var hour = d.getHours() < 10 ? '0' + d.getHours() : '' + d.getHours();
var minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : '' + d.getMinutes();
var seconds = d.getSeconds() < 10 ? '0' + d.getSeconds() : '' + d.getSeconds();
return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds;
},
send: function() {
console.log('评论人编号:' + this.userId + ',文章编号:' + this.article.aId + ',评论内容:' + this.content);
uni.request({
url: this.apiServer + '/comment/add',
method: 'POST',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: {
aId: this.article.aId,
uId: this.userId,
content: this.content
},
success: res => {
if (res.data.code === 0) {
uni.showToast({
title: '评论成功'
});
this.getArticle();
this.content = '';
}
}
});
},
follow: function() {
uni.request({
url: this.apiServer + '/follow/add',
method: 'POST',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: {
fromUId: this.userId,
toUId: this.article.uId
},
success: res => {
if (res.data.code === 0) {
uni.showToast({
title: '关注成功'
});
this.followed = true;
}
}
});
},
like: function() {
uni.request({
url: this.apiServer + '/like/add',
method: 'POST',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: {
uId: this.userId,
aId: this.article.aId
},
success: res => {
if (res.data.code === 0) {
uni.showToast({
title: '收藏成功'
});
this.liked = true;
}
}
});
},
cancelFollow: function() {
uni.request({
url: this.apiServer + '/follow/cancel',
method: 'POST',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: {
fromUId: this.userId,
toUId: this.article.uId
},
success: res => {
if (res.data.code === 0) {
uni.showToast({
title: '已取消关注'
});
this.followed = false;
}
}
});
},
cancelLike: function() {
uni.request({
url: this.apiServer + '/like/cancel',
method: 'POST',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: {
uId: this.userId,
aId: this.article.aId
},
success: res => {
if (res.data.code === 0) {
uni.showToast({
title: '已取消收藏'
});
this.liked = false;
}
}
});
}
}
};
</script>
<style>
.link {
cursor: pointer;
}
.article-title {
font-weight: bold;
padding: 10px;
font-size: 22px;
}
.article-info {
display: flex;
margin-top: 20px;
align-items: center;
}
.grace-text {
margin-top: 10px;
}
.avatar {
width: 60px;
height: 60px;
margin-left: 5px;
}
.info-text {
margin-left: 10px;
font-size: 18px;
margin-top: 10px;
display: flex;
flex-direction: column;
}
.btn {
margin-right: 10px;
width: 90px;
height: 40px;
background: #00C777;
display: flex;
justify-content: center;
align-items: center;
color: #EEEEEE;
}
.content {
width: 90%;
margin: auto;
}
.comment-item {
display: flex;
margin-top: 5px;
}
.right {
display: flex;
flex-direction: column;
margin-left: 10px;
}
.right-content {
display: flex;
flex-direction: column;
}
.right-time {
margin-top: 5px;
color: #C1C1C1;
font-size: 15px;
}
.uni-input {
margin-top: 10px;
font-size: 18px;
}
.green-btn {
margin-top: 10px;
width: 60%;
cursor: pointer;
border-radius: 10px;
background: #00EE76;
color: white;
}
.cancel {
background-color:#AAAAAA;
}
.like-btn{
width: 90px;
height: 40px;
background: white;
display: flex;
justify-content: center;
align-items: center;
color:#FF7900;
border: 1px solid #FF7900;
border-radius: 10px;
margin-top: 10px;
}
.cancel-like{
width: 90px;
height: 40px;
background-color: #aaa;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
margin-top: 10px;
}
</style>