5.1 django项目-新闻博客系统之新闻主页

05 新闻主页

一、功能需求分析

  • 轮播图
  • 热门文章
  • 文章标签导航
  • 文章列表
  • 瀑布流分页

二、模型设计

1、表

  • 文章标签(分类)表
  • 文章表
  • 热门文章表
  • 轮播图

2、模型设计

1、基类

抽取公共字段,用于继承

from django.db import models


class BaseModel(models.Model):
    """
    基类,公共字段
    """
    # django会自动创建自增长的id字段
    create_time = models.DateTimeField('创建时间', auto_now_add=True)
    update_time = models.DateTimeField('更新时间', auto_now=True)
    is_delete = models.BooleanField('逻辑删除', default=False)

    class Meta:
        # 抽象类,用于继承,迁移时不会创建
        abstract = True

2、模型设计

from django.db import models
from utils.models.models import BaseModel
# Create your models here.


class Tag(BaseModel):
    """
    文章分类标签模型
    """
    name = models.CharField('标签名', max_length=64, help_text='标签名')

    class Meta:
        ordering = ['-update_time', '-id']      # 排序
        db_table = "tb_tag"                     # 指明数据库表名
        verbose_name = "文章标签"                # 在admin站点中显示的名称
        verbose_name_plural = verbose_name      # 显示的复数名称

    def __str__(self):
        return 'Tag <name={}>'.format(self.name)


class News(BaseModel):
    """
    文章模型
    """
    title = models.CharField('标题', max_length=150, help_text='标题')
    digest = models.CharField('摘要', max_length=200, help_text='摘要')
    content = models.TextField('内容', help_text='内容')
    clicks = models.IntegerField('点击量', default=0, help_text='点击量')
    image_url = models.URLField('图片url', default='', help_text='图片url')
    tag = models.ForeignKey('Tag', on_delete=models.SET_NULL, null=True)

    author = models.ForeignKey('user.User', on_delete=models.SET_NULL, null=True)

    class Meta:
        ordering = ['-update_time', '-id']  # 排序
        db_table = "tb_news"  # 指明数据库表名
        verbose_name = "新闻"  # 在admin站点中显示的名称
        verbose_name_plural = verbose_name  # 显示的复数名称

    def __str__(self):
        return 'News <title={}>'.format(self.title)

class HotNews(BaseModel):
    """
    推荐文章表
    """
    news = models.OneToOneField('News', on_delete=models.CASCADE)
    priority = models.IntegerField('优先级', help_text='优先级')

    class Meta:
        ordering = ['-update_time', '-id']  # 排序
        db_table = "tb_hotnews"  # 指明数据库表名
        verbose_name = "热门新闻"  # 在admin站点中显示的名称
        verbose_name_plural = verbose_name  # 显示的复数名称

    def __str__(self):
        return 'HotNews <id={}>'.format(self.id)


class Banner(BaseModel):
    """
    轮播图
    """
    image_url = models.URLField('轮播图url', help_text='轮播图url')
    priority = models.IntegerField('优先级', help_text='优先级')

    news = models.OneToOneField('News', on_delete=models.CASCADE)

    class Meta:
        ordering = ['priority', '-update_time', '-id']  # 排序
        db_table = "tb_banner"  # 指明数据库表名
        verbose_name = "轮播图"  # 在admin站点中显示的名称
        verbose_name_plural = verbose_name  # 显示的复数名称

    def __str__(self):
        return 'Banner <id={}>'.format(self.id)

三、文章标签列表

1、接口设计

  1. 接口说明

    项目项目 说明说明
    请求方法 GET
    url /news/tags/
    参数类型 查询参数
  2. 参数说明

    参数名 类型 是否必须 描述
    size 整型 每页的标签数
  3. 返回结果

    {
        "error": "0",
        "errmsg": "",
        "data": {
            "size": 6,
            "tags":{
                "id": id,
                "name": name,
            }
        }
    }
    

2、后端代码

def tag_list(request):
    '''
    新闻标签列表接口函数
    :url: /news/tags/?size=10
    :return: tags_list(json)
    '''
    # 获取tag_id
    try:
        size = int(request.GET.get('size', constants.SHOW_TAG_COUNT))
    except Exception as e:
        logger.error('size错误:\n{}'.format(e))
        size = constants.SHOW_TAG_COUNT

    tags = Tag.objects.values('id', 'name').filter(is_delete=False)[:size]

    data = {
        'size': size,
        'tags': list(tags)
    }

    return json_response(data=data)

路由

path('news/tags/', views.tag_list, name='tag_list'),

导入数据

mysql -u 用户名 -p -D 数据库名 < tb_news_20181217.sql

mysql -uroot -pqwe123 -D djproject28 < tb_tags_20181217.sql

四、新闻列表

1、接口设计

  1. 接口说明

    项目项目 说明说明
    请求方法 GET
    url /news/
    参数类型 查询参数
  2. 参数说明

    参数名 类型 是否必须 描述
    tag 整型 标签id
    page 整型 页码
    pagesize 整型 每页的新闻数
  3. 返回结果

    {
        "error": "0",
        "errmsg": "",
        "data": {
            'total_pages': paginator.num_pages,
            'page': page,
            'pagesize': pagesize,
            'news': [
                {
                    "id": id,
                    "title": title,
                    "digest": digest,
                    "image_url": image_url,
                    "update_time": update_time,
                    "tag_name": tag_name,
                    "author": author
                },
                            {
                    "id": id,
                    "title": title,
                    "digest": digest,
                    "image_url": image_url,
                    "update_time": update_time,
                    "tag_name": tag_name,
                    "author": author
                }
            ]
        }
    }
    

2、后端代码

def news_list(request):
    '''
    新闻列表接口函数
    :url: /news/?tag=*&page=*&pagesize=*
    :return: news_list(json)
    '''
    # 获取tag_id
    try:
        tag_id = int(request.GET.get('tag', 0))
    except Exception as e:
        logger.error('标签错误:\n{}'.format(e))
        tag_id = 0
    # 获取页码page
    try:
        page = int(request.GET.get('page', 1))
    except Exception as e:
        logger.error('页码错误:\n{}'.format(e))
        page = 1
    # 获取pagesize
    try:
        pagesize = int(request.GET.get('pagesize', constants.NEWS_LIST_PAGESIZE))
    except Exception as e:
        logger.error('pagesize错误:\n{}'.format(e))
        pagesize = constants.NEWS_LIST_PAGESIZE
    # 使用only返回的是对象,所以传递到前端时需要迭代处理
    # news_queryset = News.objects.select_related('tag', 'author').only(
    #     'title', 'digest', 'image_url', 'update_time', 'tag__name', 'author__username')
    # values 返回字典
    news_queryset = News.objects.values('id', 'title', 'digest', 'image_url', 'update_time').annotate(tag_name=F('tag__name'), author=F('author__username'))
    news = news_queryset.filter(is_delete=False, tag_id=tag_id) or news_queryset.filter(is_delete=False)

    paginator = Paginator(news, pagesize)
    # 获取页面数据 get_page可以容错
    news_info = paginator.get_page(page)

    data = {
        'total_pages': paginator.num_pages,
        'page': page,
        'pagesize': pagesize,
        'news': list(news_info)
    }

    return json_response(data=data)

路由

path('news/', views.news_list, name='news_list'),

导入数据

mysql -u 用户名 -p -D 数据库名 < tb_news_20181217.sql

mysql -uroot -pqwe123 -D djproject28 < tb_news_20181217.sql

3、media文件访问

在项目根目录下创建一个media文件夹,用于存放新闻图片以及用户上传文件。

# 在settings.py文件中添加
# 媒体文件配置
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

django在调试模式下提供静态文件服务,为了能够返回media中的媒体文件还需在根urls.py中做如下配置

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # path('admin/', admin.site.urls),
    path('course/', include('course.urls')),
    path('doc', include('doc.urls')),
    path('', include('news.urls')),
    path('', include('verification.urls')),
    path('', include('user.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

五、轮播图列表

1、接口设计

  1. 接口说明

    项目项目 说明说明
    请求方法 GET
    url /news/banners/
    参数类型 查询参数
  2. 参数说明

    参数名 类型 是否必须 描述
    size 整型 每页的新闻数
  3. 返回结果

    {
        "error": "0",
        "errmsg": "",
        "data": {
            'size': size,
            'banners':{
                    "news_id": news_id,
                    "image_url": image_url,
                    "news_title": news_title,
                }
        }
    }
    

2、后端代码

def banner_list(request):
    '''
    轮播图列表接口函数
    :url: /news/banners/?size=6
    :return:banner_list(json)
    '''
    # 获取tag_id
    try:
        banner_size = int(request.GET.get('size', constants.SHOW_BANNER_COUNT))
    except Exception as e:
        logger.error('size错误:\n{}'.format(e))
        banner_size = constants.SHOW_BANNER_COUNT

    banners = Banner.objects.values('image_url', 'news_id').annotate(news_title=F('news__title')).filter(is_delete=False)[:banner_size]
    data = {
        'size': banner_size,
        'banners': list(banners)
    }

    return json_response(data=data)

路由

path('news/banners/', views.banner_list, name='banner_list'),

导入数据

mysql -u 用户名 -p -D 数据库名 < tb_news_20181217.sql

mysql -uroot -pqwe123 -D djproject28 < tb_banners_20181217.sql

六、热门新闻列表

1、接口设计

  1. 接口说明

    项目项目 说明说明
    请求方法 GET
    url /news/hotnews/
    参数类型 查询参数
  2. 参数说明

    参数名 类型 是否必须 描述
    size 整型 每页的新闻数
  3. 返回结果

    {
        "error": "0",
        "errmsg": "",
        "data": {
            'size': size,
            'banners':{
                    "news_id": news_id,
                    "image_url": image_url,
                    "news_title": news_title,
                }
        }
    }
    

2、后端代码

def hotnews_list(request):
    '''
    热门新闻列表接口函数
    :url:  /news/hotnews/?size=10
    :return:
    '''
    try:
        page_size = int(request.GET.get('size', constants.SHOW_HOTNEWS_COUNT))
    except Exception as e:
        logger.error('size错误:\n{}'.format(e))
        page_size = constants.SHOW_HOTNEWS_COUNT

    hot_news = HotNews.objects.values('news_id').\
        annotate(news_title=F('news__title'), news_imageurl=F('news__image_url')).\
        filter(is_delete=False).\
        order_by('priority', '-news__clicks')[:page_size]

    data = {
        'size': page_size,
        'hot_news': list(hot_news)
    }
    return json_response(data=data)

路由

path('news/hotnews/', views.hotnews_list, name='hotnews_list'),

导入数据

mysql -u 用户名 -p -D 数据库名 < tb_news_20181217.sql

mysql -uroot -pqwe123 -D djproject28 < tb_hotnews_20181217.sql

七、前端设计

1、前端html

{% extends 'base/base.html' %}
{% load static %}

{% block title %}首页{% endblock title %}

{% block link %}
    <link rel="stylesheet" href="{% static 'css/news/index.css' %}">
{% endblock link %}

{% block main_contain %}
    <div class="main-contain">
        <!-- banner start -->
        <div class="banner">
            <ul class="pic">
                <!--淡入淡出banner-->

            </ul>
            <a href="javascript:void(0);" class="btn prev">
                <i class="PyWhich py-arrow-left"></i></a>
            <a href="javascript:void(0);" class="btn next">
                <i class="PyWhich py-arrow-right"></i></a>
            <ul class="tab">
                <!-- 按钮数量必须和图片一致 -->

            </ul>
        </div>
        <!-- banner end -->

        <!-- content start -->
        <div class="content">
            <!-- recommend-news start -->
            <ul class="recommend-news">

            </ul>
            <!-- recommend-news end -->

            <!--  news-nav start-->
            <nav class="news-nav">
                <ul class="clearfix">
                    <li class="active"><a href="javascript:void(0)">最新资讯</a></li>
{#                    {% for tag in tags %}#}
{#                        <li><a href="javascript:void(0)" data-id="{{ tag.id }}">{{ tag.name }}</a></li>#}
{#                    {% endfor %}#}
                </ul>
            </nav>
            <!--  news-nav end -->

            <!-- news-contain start -->
            <div class="news-contain">
                <ul class="news-list">

                </ul>
            </div>
            <!-- news-contain end -->

            <!-- btn-more start -->
            <a href="javascript:void(0);" class="btn-more">加载更多</a>
            <!-- btn-more end -->
        </div>
        <!-- content end -->
    </div>
{% endblock main_contain %}

{% block otherjs %}
    <script src="{% static 'js/base/message.js' %}"></script>
    <script src="{% static 'js/news/index.js' %}"></script>
{% endblock otherjs %}

2、前端js

/*=== tagsStart ===*/
$(function () {
    fn_load_tags();
    function fn_load_tags() {
        $
            .ajax({
                url:'/news/tags/',
                type:'GET',
                dataType:'json'
            })
            .done((resp)=>{
                if(resp.errno === '0')
                {
                    resp.data.tags.forEach(function (tag) {
                        let content = `<li><a href="javascript:void(0)" data-id="${tag.id}">${tag.name}</a></li>`;
                         $('.news-nav .clearfix').append(content);
                    })
                }
            })
            .fail(()=>{
                message.showError('服务器超时,请重试!')
            })
    }
});
/*=== tagsEnd ===*/


/*=== news_listStart ===*/
$(()=> {
    // 新闻列表
    // let $newNavLi = $('.news-nav ul li');   // 标签li
    let iPage = 1;                          // 默认第一页
    let iTotalPage = 1;                     // 默认总页数为1
    let iCurrentTagId = 0;                  // 默认分类标签为0
    let bIsLoadData = true;                 // 是否正在向后台加载数据

    fn_load_content();

    // js加载的标签需要动态获取
    // 动态添加的标签要事件委托才能获取到节点
    $('.news-nav ul').on('mouseenter', function (){
         // 点击分类标签
    $('.news-nav ul li').click(function () {
        // 点击分类标签,则为点击的标签加上一个active的class属性
        // 并移除其他兄弟元素上的active的class属性
        $(this).addClass('active').siblings('li').removeClass('active');
        // 获取绑定在data-id属性上的tag_id
        let iClickTagId = $(this).children('a').attr('data-id');
        if (iClickTagId !== iCurrentTagId){
            iCurrentTagId = iClickTagId;  // 记录当前分类id
            // 重置分页参数
            iPage = 1;
            iTotalPage = 1;
            fn_load_content()
        }

    });
    });

    // 页面滚动加载
    $(window).scroll(function () {
       // 浏览器窗口高度
        let showHeigtht = $(window).height();
       // 整个网页高度
        let pageHeight = $(document).height();
        //页面可以滚动的距离
        let canScrollHeight = pageHeight - showHeigtht;
        // 页面滚动了多少, 整个是随着页面滚动实时变化的
        let nowScroll = $(document).scrollTop();
        if ((canScrollHeight - nowScroll) < 100){
            if(!bIsLoadData){
                bIsLoadData = true;
                //判断页数,去更新新闻,小于总数才加载
                if(iPage < iTotalPage){
                    iPage += 1;
                    fn_load_content();

                }else {
                    message.showInfo('已全部加载,没有更多内容!');
                    $('a.btn-more').html('已全部加载,没有更多内容!')
                }

            }
        }
    });

    // 向后端获取新闻列表数据
    function fn_load_content() {
        $.ajax({
            url: '/news/',
            type: 'GET',
            data:{
                tag: iCurrentTagId,
                page: iPage
            },
            dataType: 'json',
            success: function (res) {
                if(res.errno === '0'){
                    iTotalPage = res.data.total_pages;
                    if(iPage === 1){
                        // 第一页清空内容
                        $('.news-list').html('')
                    }
                    res.data.news.forEach(function (one_news) {
                        let content = `<li class="news-item">
                      <a href="/news/${one_news.id}" class="news-thumbnail"
                         target="_blank">
                          <img src="${one_news.image_url}" alt="${one_news.title}"
                               title="${one_news.title}">
                      </a>
                      <div class="news-content">
                          <h4 class="news-title"><a
                                  href="/news/${one_news.id}"">${one_news.title}</a>
                          </h4>
                          <p class="news-details">${one_news.digest}</p>
                          <div class="news-other">
                              <span class="news-type">${one_news.tag_name}</span>
                              <span class="news-time">${one_news.update_time}</span>
                              <span class="news-author">${one_news.author}</span>
                          </div>
                      </div>
                  </li>`;
                        $('.news-list').append(content);
                    });
                  // $('.news-list').append($('<a href="javascript:void(0);" class="btn-more">滚动加载更多</a>'));
                  //数据加载完毕,设置正在加载数据变量为false,表示当前没有加载数据
                  bIsLoadData = false;
                  $('a.btn-more').html('滚动加载更多')
                }else {
                    // 加载失败,打印错误信息
                    message.showError(res.errmsg)
                }
            },
            error: function () {
                message.showError('服务器超时,请重试!')
            }
        });
    }
});
/*=== newslistEnd ===*/

// /*=== bannerStart ===*/
$(function () {
    // 新闻轮播图功能
    fn_load_banner();                   // 先加载banner

    let $banner = $('.banner');         // banner容器div
    let $picLi = $('.banner .pic li');  // 图片li标签
    let $pre = $('.banner .prev');      // 上一张
    let $next = $('.banner .next');     // 下一张
    let $tabLi = $('.banner .tab li');  // 按钮
    let index = 0;                      // 当前索引

    // 导航小圆点
    $tabLi.click(function () {
        index = $(this).index();
        $(this).addClass('active').siblings('li').removeClass('active');
        $picLi.eq(index).fadeIn(1500).siblings('li').fadeOut(1500);
    });

    // 点击切换上一张
    $pre.click(()=> {
        index --;
        if(index<0){
            index = $tabLi.length - 1       // 最后一张
        }
        $tabLi.eq(index).addClass('active').siblings('li').removeClass('active');
        $picLi.eq(index).fadeIn(1500).siblings('li').fadeOut(1500);
    });

    // 点击切换下一张
    $next.click(()=>{
        auto();
    });

    // 图片向前滑动
    function auto() {
        index ++;
        index %= $tabLi.length;
        $tabLi.eq(index).addClass('active').siblings('li').removeClass('active');
        $picLi.eq(index).fadeIn(1500).siblings('li').fadeOut(1500)

    }
    // 定时器
    let timer = setInterval(auto, 2500);
    $banner.hover(
        ()=>{
            clearInterval(timer)
        },
        ()=>{
            timer = setInterval(auto, 2500);
        }
    );
    // 定义向后端获取banner
    function fn_load_banner() {
        $
            .ajax({
                url: '/news/banners/',
                type: 'GET',
                async: false,               // 同步执行,下面的代码依赖banner的加载
                dataType: "json",

            })
            .done( (res)=> {
                if(res.errno === '0'){
                    let content = '';
                    let tab_content = '';
                    res.data.banners.forEach( (one_banner, index) =>{
                        if(index === 0){        // 第一页 加active属性
                            content = `<li style="display:block;"><a href="/news/${one_banner.news_id}/">
                 <img src="${one_banner.image_url}" alt="${one_banner.news_title}"></a></li>`;
                            tab_content = '<li class="active"></li>';
                        }else {
                            content = `<li><a href="/news/${one_banner.news_id}/"><img src="${one_banner.image_url}" alt="${one_banner.news_title}"></a></li>`;
                            tab_content = '<li></li>';
                        }
                        $('.pic').append(content);
                        $('.tab').append(tab_content)
                    })

                }else {
                    message.showError(res.errmsg)
                }
            })
            .fail(()=>{
                message.showError('服务器超时,请重试!')
            })
    }
});
// /*=== bannerEnd ===*/

// /*=== hotnewsStart ===*/
$(function () {
    fn_load_hotnews();
    function fn_load_hotnews() {
        $
            .ajax({
                url:'/news/hotnews/',
                type:'GET',
                dataType:'json'
            })
            .done((resp)=>{
                if(resp.errno === '0')
                {
                    resp.data.hot_news.forEach(function (one_hot_news) {
                        let content = `<li>
                                            <a href="/news/${one_hot_news.news_id}" target="_blank">
                                                <div class="recommend-thumbnail">
                                                    <img src="${one_hot_news.news_imageurl}" alt="title">
                                                </div>
                                                <p class="info">${one_hot_news.news_title}</p>
                                            </a>
                                        </li>`;
                         $('.content .recommend-news').append(content);
                    })
                }
            })
            .fail(()=>{
                message.showError('服务器超时,请重试!')
            })
    }
});
// /*=== hotnewsrEnd ===*/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335