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


    article.png
  • t_comment


    comment.png
  • t_img


    img.png
  • 自行填充一些数据
    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
entity.png

其中,Article、Img、Comment类参照以前写法,自行完成

其余vo包中的两个视图对象类如下:


articleVo.png
commentVo.png
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>

本站以现代、古代情诗为主,情诗网创办于2013年,以原创爱情诗歌、经典情诗、现代情诗、古代情诗、英文情诗、情诗绝句为主并收集古诗、古诗词、诗歌大全、诗词名句的文学门户。方便您下次继续阅读;可以放在浏览器的收藏夹中(快捷键Ctrl+D);或者看到喜欢或者有趣的诗词可以通过分享按钮给你的好友分享;情诗网是目前最全情诗大全网站之一。并欢迎广大诗歌爱好者阅览投稿!喜欢本站的话请大家把本站告诉给你朋友哦!地址是 www.qingshiwang.com !