A complete website is composed of the front desk and the management background, the front desk is used for real users to browse and use, the background is used for administrators to manage the site content, configure various functions and data. The management background of blog is used to carry the subsystem of creating blog, publishing blog, viewing messages and managing blog users.

Hello everyone, I am Luo Xia Gu Fan. In the last article, we have realized the functions of user registration, login and logout. In this chapter, we start to build the management background of blog to achieve the management function of blog website. I will also explain how to implement it in terms of a complete feature, from requirements analysis to code writing.

I. Demand analysis

As a complete blogging system, the admin background is a core part of content management. In the Python and PHP world, there are many libraries and open source projects that do content management. Based on actual needs, we sorted out the following requirements:

  1. Dashboard: mainly displays the visit status of the whole blog website, including the number of page views, likes, comments, messages and so on.
  2. Classification management: mainly used to organize the classification of articles, through classification to help users better browse the whole blog site.
  3. Label management: it is mainly used to manage the labels of articles and mark the types of articles to help users better identify the types of documents.
  4. The article management: mainly used to complete the article to add, modify, release, delete, etc., considering the convenience of the article release, need supportMarkdownSyntax.
  5. Comment management: it is mainly used to view the comment information of the article. If there is sensitive content, it can be deleted through the background.
  6. User management: mainly used to manage the blog website registered user information, can be disabled users.

The above functions are also the core functional framework of a 2B end product.

Second, back-end interface development

The back-end is responsible for business logic processing and data persistence. Based on the business objects involved in requirements analysis, we need to design the Model first, and map it to Django, that is, build the Model first.

2.1 ModelLayer code implementation

2.1.1 Physical Model Description

Based on demand analysis, through the transformation of business model to physical model, there are mainly the following physical model:

  1. Category table: stores article categories. The relationship with the article table is one-to-many, that is, a category can be associated with multiple articles, and an article can only belong to one category
  2. Label table: Labels that store articles. The relationship with the article table is many-to-many, that is, a label can belong to multiple articles, and an article can manage multiple tags
  3. Article table: store article information, need to record the title of the article, abstract, body, cover, page views, comments, likes and so on
  4. Comment table: Stores the comment information of articles. It has a many-to-one relationship with the article table, that is, a comment can only be associated with one article, but one article can be associated with multiple comments
  5. “Like table” : records the user’s “like” information, and the article table is many-to-one relationship, that is, one “like” corresponds to an article, and one article corresponds to more “like”.
  6. User table: Records user information, including blog viewers and site administrators

2.1.2 Code implementation

2.1.2.1 Installation Dependencies

In the design of classification list, we often adopt the way of adjacency list, through a parent_id self-association, realize the association of parent and child, forming a tree structure. This design is very convenient to add and modify, only need a query can be completed, but in the parent check child, child check parent, delete operation but need more IO loss.

In practice, there are more queries than modifications, so here we use a new data structure MPTT, a pre-sorted traversal tree, a more efficient data structure for querying and managing tree data. Therefore, dependencies need to be installed

PIP install django - MPTT = = 0.12.0Copy the code

Then add the dependency information to requirements.txt

django-mptt= =0.12.0
Copy the code
2.1.2.2 Managing Constants

When the back-end processes various types of business, it will encounter various enumeration types, such as user identity, gender, article status, etc. In the code world, try not to use Magic Number, but through constant management.

Add file constants.py under common and write the following code:

class Constant(object) :
    ARTICLE_STATUS = (
        ('Draft'.'draft'),
        ('Published'.'Published'),
        ('Deleted'.'Deleted')
    )
    ARTICLE_STATUS_DELETED = 'Deleted'
    ARTICLE_STATUS_PUBLISHED = 'Published'
    ARTICLE_STATUS_DRAFT = 'Draft'

    GENDERS = (
        ('Male'.'male'),
        ('Female'.'woman'),
        ('Unknown'.'unknown'),
    )
    GENDERS_UNKNOWN = 'Unknown'
Copy the code
2.1.2.3 Model section

A few points need to be made here:

  1. In the definition of individual tables, through inner classesMetaYou can define meta information about model classes, such as table namesdb_table, sorting modeordering.-Represents the reverse order.
  2. The MPTT model has a separate inner classMPTTMeta, can be definedparentAnd sort fieldsorder_insertion_by.
  3. For many-to-many relationships,DjangoprovidesManyToManyThis automatically generates an intermediate table to record many-to-many data for both tables.
  4. When defining a foreign key association, you need to specify a uniquerelated_nameTo facilitate the joint retrieval of tables.
  5. Foreign key managementon_delete, you need to determine whether to perform cascading deletion or not based on the actual situation.

Write the following code under blog/models.py:

import mptt.models
from django.db import models

from common.constants import Constant
from common.models import AbstractBaseModel, User

class Tag(AbstractBaseModel) :
    name = models.CharField('Label name', max_length=50, unique=True, null=False, blank=False)

    class Meta:
        db_table = 'blog_tag'

    def __str__(self) :
        return self.name


class Catalog(mptt.models.MPTTModel, AbstractBaseModel) :
    name = models.CharField('Category name', max_length=50, unique=True, null=False, blank=False)
    parent = mptt.models.TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True,
                                        related_name='children')

    class Meta:
        db_table = 'blog_catalog'

    class MPTTMeta:
        order_insertion_by = ['name']

    def __str__(self) :
        return self.name


class Article(AbstractBaseModel) :
    title = models.CharField('Article Title', max_length=100, unique=True, null=False, blank=False)
    cover = models.TextField('cover', max_length=1000, null=False, blank=False)
    excerpt = models.CharField('the', max_length=200, blank=True)
    keyword = models.CharField('Key words', max_length=200, blank=True)
    markdown = models.TextField(The 'body', max_length=100000, null=False, blank=False)
    status = models.CharField('Article Status', max_length=30, choices=Constant.ARTICLE_STATUS,
                              default=Constant.ARTICLE_STATUS_DRAFT)
    catalog = models.ForeignKey(Catalog, verbose_name='Category of ownership', null=False, blank=False,
                                on_delete=models.DO_NOTHING, related_name='cls_articles')
    tags = models.ManyToManyField(Tag, verbose_name='Article tag', blank=True, related_name='tag_articles')

    author = models.ForeignKey(User, verbose_name='the writer', on_delete=models.DO_NOTHING, null=False, blank=False)
    views = models.PositiveIntegerField('Views', default=0, editable=False)
    comments = models.PositiveIntegerField('Number of comments', default=0, editable=False)
    likes = models.PositiveIntegerField('Likes', default=0, editable=False)
    words = models.PositiveIntegerField(The '-', default=0, editable=False)

    class Meta:
        db_table = 'blog_article'
        ordering = ["-created_at"]

    def __str__(self) :
        return self.title


class Like(AbstractBaseModel) :
    article = models.ForeignKey(Article, on_delete=models.DO_NOTHING, related_name='article_likes')
    user = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name='like_users')

    class Meta:
        db_table = 'blog_like'


class Comment(AbstractBaseModel) :
    article = models.ForeignKey(Article, verbose_name='Review article', on_delete=models.DO_NOTHING,
                                related_name='article_comments')
    user = models.ForeignKey(User, verbose_name='Commenter', on_delete=models.DO_NOTHING, related_name='comment_users')
    reply = models.ForeignKey('self', verbose_name='Comment reply', on_delete=models.CASCADE, related_name='comment_reply',
                              null=True, blank=True)
    content = models.TextField('comments', max_length=10000, null=False, blank=False)

    class Meta:
        db_table = 'blog_comment'


class Message(AbstractBaseModel) :
    email = models.EmailField('email', max_length=100, null=False, blank=False)
    content = models.TextField('content', max_length=10000, null=False, blank=False)
    phone = models.CharField('mobile phone', max_length=20, null=True, blank=True)
    name = models.CharField('name', max_length=30, null=True, blank=True)
    
    class Meta:
        db_table = 'blog_message'

Copy the code

2.2 Code implementation of Serializer layer

2.2.1 Sorting instructions

Defining Serializer is one of the core aspects of using the Rest Framework Framework. There are several points that need to be addressed:

  1. For a model, which fields are required as input parameters and which fields as output parameters in the API (passfieldsDefinition)
  2. For an interface, which fields are read-only and which fields are writable
  3. How do you serialize and deserialize foreign key fields, specifying how each field should be serialized
  4. How do I add fields that do not appear in the model

Rest Framework support includes automatic serialization of base models and class-based serialization. See Serializers – Django Rest Framework for more details

2.2.2 Code implementation

For part of the definition of the article, considering the article is the core of the blog, so its serialization scheme, here has realized three versions ArticleListSerializer, ArticleSerializer, ArticleChangeStatusSerializer:

  • ArticleListSerializer: corresponds to the list query, completes the display on the interface, and better isolates read and write permissions
  • ArticleSerializer: Used to add, modify, delete, view details through integrationArticleListSerializerimplementation
  • ArticleChangeStatusSerializer: used to perform online and offline operations. These two interfaces only require input and output parameters of a limited number of fields

The code for blog App Serializer is in blog/serializers. Py:

from rest_framework import serializers

from blog.models import Catalog, Tag, Article, Like, Message, Comment


class CatalogSerializer(serializers.ModelSerializer) :
    class Meta:
        model = Catalog
        fields = ['id'.'name'.'parent']


class TagSerializer(serializers.ModelSerializer) :
    class Meta:
        model = Tag
        fields = ['id'.'name'.'created_at'.'modified_at']
        extra_kwargs = {
            'created_at': {'read_only': True},
            'modified_at': {'read_only': True}},class ArticleListSerializer(serializers.ModelSerializer) :
    tags_info = serializers.SerializerMethodField(read_only=True)
    catalog_info = serializers.SerializerMethodField(read_only=True)
    status = serializers.SerializerMethodField(read_only=True)

    class Meta:
        model = Article
        fields = ['id'.'title'.'excerpt'.'cover'.'created_at'.'modified_at'.'tags'.'tags_info'.'catalog'.'catalog_info'.'views'.'comments'.'words'.'likes'.'status', ]

        extra_kwargs = {
            'tags': {'write_only': True},
            'catalog': {'write_only': True},
            'views': {'read_only': True},
            'comments': {'read_only': True},
            'words': {'read_only': True},
            'likes': {'read_only': True},
            'created_at': {'read_only': True},
            'modified_at': {'read_only': True}},    @staticmethod
    def get_tags_info(obj: Article) - >list:
        if not obj.title:
            article = Article.objects.get(id=obj.id)
            tags = article.tags.all(a)else:
            tags = obj.tags.all(a)return [{'id': tag.id.'name': tag.name} for tag in tags]

    @staticmethod
    def get_catalog_info(obj: Article) - >dict:
        if not obj.catalog:
            book = Article.objects.get(id=obj.id)
            catalog = book.catalog
        else:
            catalog = obj.catalog
        return {
            'id': catalog.id.'name': catalog.name,
            'parents': [c.id for c in catalog.get_ancestors(include_self=True)]}    @staticmethod
    def get_status(obj: Article) - >list:
        return obj.get_status_display()


class ArticleSerializer(ArticleListSerializer) :
    tags_info = serializers.SerializerMethodField(read_only=True)
    catalog_info = serializers.SerializerMethodField(read_only=True)

    class Meta(ArticleListSerializer.Meta) :
        fields = ['markdown'.'keyword']
        fields.extend(ArticleListSerializer.Meta.fields)


class ArticleChangeStatusSerializer(serializers.ModelSerializer) :
    class Meta:
        model = Article
        fields = ['id'.'status', ]
        extra_kwargs = {
            'status': {'read_only': True}},class LikeSerializer(serializers.ModelSerializer) :
    user_info = serializers.SerializerMethodField(read_only=True)
    article_info = serializers.SerializerMethodField(read_only=True)

    class Meta:
        model = Like
        fields = ['user'.'user_info'.'article'.'article_info'.'created_at']
        extra_kwargs = {
            'created_at': {'read_only': True}},    @staticmethod
    def get_user_info(obj: Like) - >dict:
        if not obj.user:
            return {}
        else:
            user = obj.user
        return {'id': user.id.'name': user.nickname or user.username, 'avatar': user.avatar}

    @staticmethod
    def get_article_info(obj: Like) - >dict:
        if not obj.article:
            return {}
        else:
            article = obj.article
        return {'id': article.id.'title': article.title}


class CommentSerializer(serializers.ModelSerializer) :
    user_info = serializers.SerializerMethodField(read_only=True)
    article_info = serializers.SerializerMethodField(read_only=True)
    comment_replies = serializers.SerializerMethodField(read_only=True)

    class Meta:
        model = Comment
        fields = ['id'.'user'.'user_info'.'article'.'article_info'.'created_at'.'reply'.'content'.'comment_replies']
        extra_kwargs = {
            'created_at': {'read_only': True}},    @staticmethod
    def get_user_info(obj: Comment) - >dict:
        if not obj.user:
            return {}
        else:
            user = obj.user
        return {'id': user.id.'name': user.nickname or user.username, 'avatar': user.avatar}

    @staticmethod
    def get_article_info(obj: Comment) - >dict:
        if not obj.article:
            return {}
        else:
            article = obj.article
        return {'id': article.id.'title': article.title}

    @staticmethod
    def get_comment_replies(obj: Comment) :
        if not obj.comment_reply:
            return []
        else:
            replies = obj.comment_reply.all(a)return [{
            'id': reply.id.'content': reply.content,
            'user_info': {
                'id': reply.user.id.'name': reply.user.nickname or reply.user.username,
                'avatar': reply.user.avatar,
                'role': reply.user.role,
            },
            'created_at': reply.created_at
        } for reply in replies]


class MessageSerializer(serializers.ModelSerializer) :
    class Meta:
        model = Message
        fields = ['email'.'phone'.'name'.'content'.'created_at']
        extra_kwargs = {
            'created_at': {'read_only': True}},Copy the code

2.3 Tools and Methods

In order to reuse code logic, we usually abstract some utility methods, mainly the time handling method and upload related path handling, and write the following code in common/utils.py:

import os
import random
import string
import time
from datetime import datetime

from django.conf import settings
from django.template.defaultfilters import slugify


def get_upload_file_path(upload_name) :
    # Generate date based path to put uploaded file.
    date_path = datetime.now().strftime('%Y/%m/%d')

    # Complete upload path (upload_path + date_path).
    upload_path = os.path.join(settings.UPLOAD_URL, date_path)
    full_path = os.path.join(settings.BASE_DIR, upload_path)
    make_sure_path_exist(full_path)
    file_name = slugify_filename(upload_name)
    return os.path.join(full_path, file_name).replace('\ \'.'/'), os.path.join('/', upload_path, file_name).replace('\ \'.'/')


def slugify_filename(filename) :
    """ Slugify filename """
    name, ext = os.path.splitext(filename)
    slugified = get_slugified_name(name)
    return slugified + ext


def get_slugified_name(filename) :
    slugified = slugify(filename)
    return slugified or get_random_string()


def get_random_string() :
    return ' '.join(random.sample(string.ascii_lowercase * 6.6))


def make_sure_path_exist(path) :
    if os.path.exists(path):
        return
    os.makedirs(path, exist_ok=True)


def format_time(dt: datetime, fmt: str = ' ') :
    fmt_str = fmt or '%Y-%m-%d %H:%M:%S'
    return dt.strftime(fmt_str)


def get_year(dt: datetime) - >int:
    return dt.year


def get_now() - >str:
    return format_time(datetime.now())


def format_time_from_str(date_time_str: str, fmt: str = ' ') :
    fmt_str = fmt or '%Y-%m-%d %H:%M:%S'
    return datetime.strptime(date_time_str, fmt_str)


def transform_time_to_str(t: int) :
    return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))

Copy the code

2.4 ViewSetLayer code implementation

2.4.1 Installing Dependencies

To better implement search criteria identification and validation in list queries, we have installed a new library: Django-filter

PIP install django - filter = = 2.4.0Copy the code

Add dependency information to requirements.txt

django-filter= =2.4.0
Copy the code

General 2.4.2ViewSetdefine

When dealing with the definition of the interface layer, we need to consider the access permission of the interface, paging, query filtering conditions, operators when adding and modifying, user role judgment, etc. These processes need to be dealt with in each interface, so we will unify these logic into a basic class to complete. We then extend the capabilities of subclasses through Python’s multiple inheritance.

  1. Defines theBaseErrorIs used to throw an exception when service verification fails.
  2. Defines theBasePaginationFor pagination of the list query interface.
  3. Defines theBaseViewSetMixinClass, the base class of a regular ViewSet, handles paging, filtering criteria, permissions, operator population, user identification, and so on.
  4. Defines theConstantViewSetClass to provide constants used by the back end to the front end for determining enumeration values.
  5. Defines theImageUploadViewSetClass for uploading the cover when a new article is added.

Add the following code to common/views.py:

import logging

import django.conf
from django.contrib.auth import authenticate, login, logout as auth_logout
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AnonymousUser
from django.core.mail import send_mail
from django.db.models import QuerySet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets, permissions, status
from rest_framework.exceptions import ValidationError
from rest_framework.generics import GenericAPIView
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from rest_framework.views import APIView

from common.constants import Constant
from common.models import User
from common.serializers import UserSerializer, UserLoginSerializer, UserPasswordSerializer
from common.utils import get_upload_file_path


def get_random_password() :
    import random
    import string
    return ' '.join(random.sample(string.ascii_letters + string.digits + string.punctuation, 8))


class BaseError(ValidationError) :
    def __init__(self, detail=None, code=None) :
        super(BaseError, self).__init__(detail={'detail': detail})


class BasePagination(PageNumberPagination) :
    """ customer pagination """
    # default page size
    page_size = 10
    # page size param in page size
    page_size_query_param = 'page_size'
    # page param in api
    page_query_param = 'page'
    # max page size
    max_page_size = 100


class BaseViewSetMixin(object) :
    pagination_class = BasePagination
    filter_backends = [DjangoFilterBackend]
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def __init__(self, **kwargs) :
        super(BaseViewSetMixin, self).__init__(**kwargs)
        self.filterset_fields = []
        self.init_filter_field()

    def init_filter_field(self) :
        """ Init filter field by the fields' intersection in model and serializer e.g. `book/? id=1&authors=2` :return: None """
        serializer = self.get_serializer_class()
        if not hasattr(serializer, 'Meta') :return
        meta = serializer.Meta

        if not hasattr(meta, 'model') :return
        model = meta.model

        if not hasattr(meta, 'fields'):
            ser_fields = []
        else:
            ser_fields = meta.fields

        for field in ser_fields:
            if not hasattr(model, field):
                continue
            self.filterset_fields.append(field)

    def perform_update(self, serializer) :
        user = self.fill_user(serializer, 'update')
        return serializer.save(**user)

    def perform_create(self, serializer) :
        user = self.fill_user(serializer, 'create')
        return serializer.save(**user)

    @staticmethod
    def fill_user(serializer, mode) :
        """ before save, fill user info into para from session :param serializer: Model's serializer :param mode: create or update :return: None """
        request = serializer.context['request']

        user_id = request.user.id
        ret = {'modifier': user_id}

        if mode == 'create':
            ret['creator'] = user_id
        return ret

    def get_pk(self) :
        if hasattr(self, 'kwargs') :return self.kwargs.get('pk')

    def is_reader(self) :
        return isinstance(self.request.user, AnonymousUser) or not self.request.user.is_superuser


class BaseModelViewSet(BaseViewSetMixin, viewsets.ModelViewSet) :
    pass


class UserViewSet(viewsets.ModelViewSet) :
    queryset = User.objects.all().order_by('username')
    serializer_class = UserSerializer
    permission_classes = [permissions.AllowAny]


class UserLoginViewSet(GenericAPIView) :
    permission_classes = [permissions.AllowAny]
    serializer_class = UserLoginSerializer
    queryset = User.objects.all(a)def post(self, request, *args, **kwargs) :
        username = request.data.get('username'.' ')
        password = request.data.get('password'.' ')

        user = authenticate(username=username, password=password)
        if user is not None and user.is_active:
            login(request, user)
            serializer = UserSerializer(user)
            return Response(serializer.data, status=200)
        else:
            ret = {'detail': 'Username or password is wrong'}
            return Response(ret, status=403)


class UserLogoutViewSet(GenericAPIView) :
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = UserLoginSerializer

    def get(self, request, *args, **kwargs) :
        auth_logout(request)
        return Response({'detail': 'logout successful ! '})


class PasswordUpdateViewSet(GenericAPIView) :
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = UserPasswordSerializer
    queryset = User.objects.all(a)def post(self, request, *args, **kwargs) :
        user_id = request.user.id
        password = request.data.get('password'.' ')
        new_password = request.data.get('new_password'.' ')
        user = User.objects.get(id=user_id)
        if not user.check_password(password):
            ret = {'detail': 'old password is wrong ! '}
            return Response(ret, status=403)

        user.set_password(new_password)
        user.save()
        return Response({
            'detail': 'password changed successful ! '
        })

    def put(self, request, *args, **kwargs) :
        """ Parameter: username->user's username who forget old password """
        username = request.data.get('username'.' ')
        users = User.objects.filter(username=username)
        user: User = users[0] if users else None

        if user is not None and user.is_active:
            password = get_random_password()

            try:
                send_mail(subject="New password for Blog site",
                          message="Hi: Your new password is: \n{}".format(password),
                          from_email=django.conf.settings.EMAIL_HOST_USER,
                          recipient_list=[user.email],
                          fail_silently=False)
                user.password = make_password(password)
                user.save()
                return Response({
                    'detail': 'New password will send to your email! '
                })
            except Exception as e:
                print(e)
                return Response({
                    'detail': 'Send New email failed, Please check your email address! '
                })
        else:
            ret = {'detail': 'User does not exist(Account is incorrect ! '}
            return Response(ret, status=403)


class ConstantViewSet(GenericAPIView) :
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = UserPasswordSerializer
    queryset = QuerySet()

    def get(self, request, *args, **kwargs) :
        ret = {}
        for key in dir(Constant):
            if not key.startswith("_"):
                ret[key] = getattr(Constant, key)
        return Response(ret)


class ImageUploadViewSet(APIView) :
    permission_classes = [permissions.AllowAny]

    def post(self, request, *args, **kwargs) :

        try:
            if request.method == 'POST' and request.FILES:
                uploaded_file = request.FILES['file']

                full_file_path, file_path = get_upload_file_path(uploaded_file.name)
                self.handle_uploaded_file(uploaded_file, full_file_path)

                response = {
                    'url': file_path
                }
                return Response(response)

        except Exception as e:
            logging.getLogger('default').error(e, exc_info=True)
            raise BaseError(detail='Upload failed', code=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @staticmethod
    def handle_uploaded_file(f, file_path) :
        destination = open(file_path, 'wb+')
        for chunk in f.chunks():
            destination.write(chunk)
        destination.close()

Copy the code

2.4.3 Blog relatedViewSetdefine

The ViewSet class here inherits the base class provided by the framework or the BaseViewSet class encapsulated by ourselves to achieve the corresponding business interface. If it is a very traditional CURD interface, the ViewSet may only need to define querySet attributes to complete the addition, modification, deletion. Detailed query, list query interface.

It can be seen that there are 7 viewsets related to the Article object, which mainly take into account the query and management requirements of the Article in various dimensions. For example, we need to query the Article according to the time, display the list of articles in reverse order by the number of page views, go online and take down the Article, and browse the Article without logging in. You need a login administrator to manage articles and other requirements.

The specific code is as follows:

import datetime

from common.utils import get_year
from django.db.models import QuerySet, Sum, Count
from rest_framework import mixins
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

from blog.models import Article, Comment, Message, Tag, Catalog, Like
from blog.serializers import ArticleSerializer, CommentSerializer, MessageSerializer, TagSerializer, \
    ArticleListSerializer, CatalogSerializer, ArticleChangeStatusSerializer, LikeSerializer
from common.constants import Constant
from common.views import BaseModelViewSet, BaseViewSetMixin


class ArticleArchiveListViewSet(BaseViewSetMixin, mixins.ListModelMixin, GenericViewSet) :
    queryset = Article.objects.all()
    serializer_class = ArticleListSerializer

    def filter_queryset(self, queryset) -> QuerySet:
        queryset = super(ArticleArchiveListViewSet, self).filter_queryset(queryset)
        if self.is_reader():
            queryset = queryset.exclude(status=Constant.ARTICLE_STATUS_DRAFT)
        return queryset.exclude(status=Constant.ARTICLE_STATUS_DELETED)

    def list(self, request, *args, **kwargs) :
        queryset = self.filter_queryset(self.get_queryset())
        total = len(queryset)
        page_size, page_number = self.get_page_info()
        start_year, end_year = self.get_datetime_range(page_size, page_number)
        queryset = queryset.filter(created_at__gte=start_year).filter(created_at__lt=end_year)
        ret = {
            "count": total,
            "next": None."previous": None.'results': []
        }
        years = {}
        for article in queryset.all():
            year = article.created_at.year
            articles = years.get(year)
            if not articles:
                articles = []
                years[year] = articles
            serializer = self.get_serializer(article)
            articles.append(serializer.data)
        for key, value in years.items():
            ret['results'].append({
                'year': key,
                'list': value
            })
        ret['results'].sort(key=lambda i: i['year'], reverse=True)
        return Response(ret)

    def get_page_info(self) :
        page_size = self.paginator.get_page_size(self.request)
        page_number = self.request.query_params.get(self.paginator.page_query_param, 1)
        return page_size, int(page_number)

    @staticmethod
    def get_datetime_range(page_size, page_number) :
        current_year = get_year(datetime.datetime.now())
        start_year = current_year - page_size * page_number + 1
        start_datetime = '{:d}-01-01 00:00:00'.format(start_year)
        end_datetime = '{:d}-01-01 00:00:00'.format(start_year + page_size)
        return start_datetime, end_datetime


class ArticleListViewSet(BaseViewSetMixin, mixins.ListModelMixin, GenericViewSet) :
    queryset = Article.objects.all().select_related('catalog'.'author')
    serializer_class = ArticleListSerializer

    def filter_queryset(self, queryset) :
        self.filterset_fields.remove('catalog')
        queryset = super(ArticleListViewSet, self).filter_queryset(queryset)
        if self.is_reader():
            queryset = queryset.exclude(status=Constant.ARTICLE_STATUS_DRAFT)
        params = self.request.query_params
        if 'catalog' in params:
            catalog_id = params.get('catalog'.1)
            catalog = Catalog.objects.get(id=catalog_id)
            catalogs = catalog.get_descendants(include_self=True)
            queryset = queryset.filter(catalog__in=[c.id for c in catalogs])
        return queryset.exclude(status=Constant.ARTICLE_STATUS_DELETED)


class ArticleViewSet(BaseViewSetMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, GenericViewSet) :
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    def perform_create(self, serializer) :
        extra_infos = self.fill_user(serializer, 'create')
        extra_infos['author'] = self.request.user
        serializer.save(**extra_infos)

    def filter_queryset(self, queryset) :
        queryset = super(ArticleViewSet, self).filter_queryset(queryset)
        if self.is_reader():
            queryset = queryset.exclude(status=Constant.ARTICLE_STATUS_DRAFT).exclude(
                status=Constant.ARTICLE_STATUS_DELETED)
        return queryset

    def perform_destroy(self, instance: Article) :
        instance.status = Constant.ARTICLE_STATUS_DELETED
        instance.save()

    def retrieve(self, request, *args, **kwargs) :
        instance: Article = self.get_object()
        serializer = self.get_serializer(instance)
        if self.is_reader():
            instance.views += 1
            instance.save()
        return Response(serializer.data)


class ArticlePublishViewSet(BaseViewSetMixin, mixins.UpdateModelMixin, GenericViewSet) :
    queryset = Article.objects.all()
    serializer_class = ArticleChangeStatusSerializer

    def filter_queryset(self, queryset) :
        queryset = super(ArticlePublishViewSet, self).filter_queryset(queryset)
        return queryset.exclude(status=Constant.ARTICLE_STATUS_DELETED)

    def perform_update(self, serializer) :
        extra_infos = self.fill_user(serializer, 'update')
        extra_infos['status'] = Constant.ARTICLE_STATUS_PUBLISHED
        serializer.save(**extra_infos)


class ArticleOfflineViewSet(ArticlePublishViewSet) :
    def perform_update(self, serializer) :
        extra_infos = self.fill_user(serializer, 'update')
        extra_infos['status'] = Constant.ARTICLE_STATUS_DRAFT
        serializer.save(**extra_infos)


class CommentViewSet(BaseModelViewSet) :
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer

    def filter_queryset(self, queryset) :
        queryset = super(CommentViewSet, self).filter_queryset(queryset)
        return queryset.filter(reply__isnull=True)

    def perform_create(self, serializer) :
        super(CommentViewSet, self).perform_create(serializer)
        article: Article = serializer.validated_data['article']
        article.comments += 1
        article.save()


class LikeViewSet(BaseModelViewSet) :
    queryset = Like.objects.all()
    serializer_class = LikeSerializer

    def perform_create(self, serializer) :
        super(LikeViewSet, self).perform_create(serializer)
        article: Article = serializer.validated_data['article']
        article.likes += 1
        article.save()


class MessageViewSet(BaseModelViewSet) :
    queryset = Message.objects.all()
    serializer_class = MessageSerializer


class TagViewSet(BaseModelViewSet) :
    queryset = Tag.objects.all()
    serializer_class = TagSerializer


class CatalogViewSet(BaseModelViewSet) :
    queryset = Catalog.objects.all()
    serializer_class = CatalogSerializer

    def list(self, request, *args, **kwargs) :
        ret = []
        roots = Catalog.objects.filter(id=1).filter(parent__isnull=True)
        if not roots:
            return Response(ret)
        root: Catalog = roots[0]
        root_dict = CatalogSerializer(root).data
        root_dict['children'] = []
        ret.append(root_dict)
        parent_dict = {root.id: root_dict}
        for cls in root.get_descendants():
            data = CatalogSerializer(cls).data

            parent_id = data.get('parent')
            parent = parent_dict.get(parent_id)
            parent['children'].append(data)

            if not cls.is_leaf_node() and cls.id not in parent_dict:
                data['children'] = []
                parent_dict[cls.id] = data
        return Response(ret)


class NumberViewSet(BaseViewSetMixin, mixins.ListModelMixin, GenericViewSet) :
    queryset = Article.objects.all()
    serializer_class = ArticleListSerializer

    def list(self, request, *args, **kwargs) :
        queryset = self.get_queryset().aggregate(Sum('views'), Sum('likes'), Sum('comments'))
        messages = Message.objects.aggregate(Count('id'))

        return Response({
            'views': queryset['views__sum'].'likes': queryset['likes__sum'].'comments': queryset['comments__sum'].'messages': messages['id__count']})class TopArticleViewSet(NumberViewSet) :
    def list(self, request, *args, **kwargs) :
        queryset = self.filter_queryset(self.get_queryset()).order_by('-views')[:10]

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

Copy the code

2.5 define the URL

2.5.1 Installation API documents automatically generate dependencies

You can use the tool to automatically generate Restful interface descriptions.

PIP install DRF - yasg = = 1.20.0Copy the code

Modify requirements.txt to add the following:

drf-yasg= =1.20.0
Copy the code

2.5.2 commonUnder theurls.py

Modify common/urls.py with the following code:

from django.conf.urls import url
from django.urls import include, path
from rest_framework import routers

from common import views
from common.views import ImageUploadViewSet

router = routers.DefaultRouter()
router.register('user', views.UserViewSet)

app_name = 'common'

urlpatterns = [
    path(' ', include(router.urls)),
    url(r'^user/login', views.UserLoginViewSet.as_view()),
    url(r'^user/logout', views.UserLogoutViewSet.as_view()),
    url(r'^user/pwd', views.PasswordUpdateViewSet.as_view()),
    url(r'^dict', views.ConstantViewSet.as_view()),
    url(r'upload/$', ImageUploadViewSet.as_view()),
]
Copy the code

2.5.3 blogUnder theurls.py

Write the following code in your blog/urls.py:

from django.urls import include, path
from rest_framework import routers

from blog import views

router = routers.DefaultRouter()
router.register('article', views.ArticleViewSet)
router.register('list', views.ArticleListViewSet)
router.register('publish', views.ArticlePublishViewSet)
router.register('offline', views.ArticleOfflineViewSet)
router.register('archive', views.ArticleArchiveListViewSet)
router.register('tag', views.TagViewSet)
router.register('catalog', views.CatalogViewSet)
router.register('comment', views.CommentViewSet)
router.register('like', views.LikeViewSet)
router.register('message', views.MessageViewSet)
router.register('number', views.NumberViewSet)
router.register('top', views.TopArticleViewSet)

app_name = 'blog'

urlpatterns = [
    path(' ', include(router.urls)),
]
Copy the code

2.5.4 projectUnder theurls.py

Here we use the method provided by DRF_YASG to automatically generate interface description documents, which is a very useful tool in the actual project of front and back end classification. It allows the front end and back end to develop in parallel based on interface conventions, ensuring the development efficiency of front end and back end.

Modify project/urls.py to look like this:

from django.conf import settings
from django.conf.urls import url
from django.urls import path, re_path, include
from django.views.generic import RedirectView
from django.views.static import serve
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions

schema_view = get_schema_view(
    openapi.Info(
        title="Blog System API",
        description="Blog site ",
        default_version='v1',
        terms_of_service="",
        contact=openapi.Contact(email="[email protected]"),
        license=openapi.License(name="GPLv3 License"),
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
)

urlpatterns = [
    path(' ', include('blog.urls', namespace='blog')),
    path(' ', include('common.urls', namespace='common')),
    url(r'^favicon.ico$', RedirectView.as_view(url=r'static/img/favicon.ico')),
    url(r'upload/(? P
      
       .*)'
      , serve, {'document_root': settings.MEDIA_ROOT}),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    re_path(
        r"api/swagger(? P
      
       \.json|\.yaml)"
      ,
        schema_view.without_ui(cache_timeout=0),
        name="schema-json",
    ),
    path(
        "swagger/",
        schema_view.with_ui("swagger", cache_timeout=0),
        name="schema-swagger-ui",
    ),
    path("docs/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),]Copy the code

2.6 Configuration Adjustment

2.6.1 adjustmentproject/setting.py

Add blog, drF_yASG, and django_filters to INSTALLED_APPS in project/setting.py. If you do not add blog, drf_yASG, and django_filters, the template path will not be found

INSTALLED_APPS = [
    'django.contrib.admin'.'django.contrib.auth'.'django.contrib.contenttypes'.'django.contrib.sessions'.'django.contrib.messages'.'django.contrib.staticfiles'.'rest_framework'.'drf_yasg'.'django_filters'.'common'.'blog'
]
Copy the code

TEMPLATES in project/setting.py is set to:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates'.'DIRS': [os.path.join(BASE_DIR, 'templates')].'APP_DIRS': True.'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug'.'django.template.context_processors.request'.'django.contrib.auth.context_processors.auth'.'django.contrib.messages.context_processors.messages',],},},]Copy the code

Add configuration for media files and upload paths in project/setting.py

MEDIA_ROOT = os.path.join(BASE_DIR, 'upload')
MEDIA_URL = "/upload/"
UPLOAD_URL = 'upload'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'upload'),Copy the code

2.6.2 Performing Model Migration

python manage.py makemigrations
python manage.py migrate
Copy the code

So far personal blog backend part of the development is complete. This actually includes the interface that visitors need to access the site on a blog site.

Third, front-end interface development

A function of management background generally needs to start from the management of the most basic business objects. On our blog website, the dependence between business objects is in turn user, tag, classification, article, comment, like, message, home page statistics.

Based on this dependency, our back-end management functions are built in this logical order. Then, when building the management page for each business object, organize and code it in the order of Type, API, Component, View, Route.

Create two folders admin and client under SRC/Views, and move the login. vue created in the previous section to admin and the home.vue file to client.

3.1 Menu Management

3.1.1 Admin.vue

The admin background requires an independent menu navigation function, so add admin. vue file under SRC /views/admin to complete the left menu navigation. The code is as follows:

<template> <div class="body"> <div class="menu"> <el-menu :default-active="state.activePath" :router="true"> <el-menu-item index="AdminDashboard" route="/admin/dashboard"><i class="el-icon-s-home"></i> Dashboard </el-menu-item> <el-menu-item index="ArticleManagement" route="/admin/article">< I class="el-icon-s-order"></ I ></ el-menu-item> <el-menu-item index="TagManagement" route="/admin/tag">< I class="el-icon-collection-tag"></ I > tag </el-menu-item> <el-menu-item index="CommentManagement" route="/admin/comment">< I class="el-icon-chat-line-round"></ I > comments </el-menu-item> <el-menu-item index="UserManagement" route="/admin/user">< I class="el-icon-user"></ I > user </el-menu-item> </el-menu> </div> <div class="view"> <router-view/> </div> </div> </template> <script> import {defineComponent, reactive} from "vue"; import {useRoute} from "vue-router"; export default defineComponent({ name: "Admin", setup() { const state = reactive({ activePath: '', }); const route = useRoute() if (route.name === 'Dashboard') { state.activePath = 'AdminDashboard' } else { state.activePath  = route.name; } return { state, } }, }); </script> <style lang="less" scoped> .body { width: 100%; height: 100%; box-sizing: border-box; display: flex; } .user { font-size: 20px; } .menu { width: 200px; } .view { width: calc(100% - 200px); padding: 24px; } .el-menu { height: 100%; } </style>Copy the code

3.1.2 Dashboard.vue

In order to carry out the following development well, we first deal with the default page Dashboard of the management background, create the file dashboard. vue under SRC /views/admin, and write the code:

<template>
    <h3>Dashboard</h3>
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";
export default defineComponent({
   name: 'Dashboard', 
})
</script>
Copy the code

3.1.3 Adding a Route

SRC /router/index.ts

import {createRouter, createWebHistory, RouteRecordRaw} from "vue-router";
import Home from ".. /views/client/Home.vue";

const routes: Array<RouteRecordRaw> = [
    {
        path: "/".name: "Home".component: Home,
        meta: {}}, {path: "/login/".name: "Login".component: () = >
            import(".. /views/admin/Login.vue")}, {path: '/admin'.name: 'Admin'.component: () = > import(".. /views/admin/Admin.vue"),
        children: [{path: '/admin/'.name: 'Dashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/dashboard'.name: 'AdminDashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),},]},]const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
});


export default router;
Copy the code

3.2 User Management

3.2.1 Type

While we are dealing with login and registration, we have completed the type definition of the User, namely the interface definition of the User. Here we add the definition of all returned results to manage the data structure returned by the interface. In the SRC /types/index.ts file the code looks like this:

export interface User {
    id: number.username: string.email: string.avatar: string | any.nickname: string | any, is_active? :any, is_superuser? :boolean, created_at? :string,}export interface ResponseData {
    count: number; results? :any; detail? :string;
}
Copy the code

3.2.2 APIlayer

Here to prepare user management related interface, list query, enable, disable, view details. Write the following code in SRC/API /service.ts:

import { User, ResponseData } from ".. /types"


export function getUserDetail(userId: number) {
    return request({
        url: '/user/' + userId + '/'.method: 'get',})as unknown as User
}

export function saveUser(method: string, data: User) {
    // @ts-ignore
    return request({
        url: '/user/' + data.id + '/',
        method,
        data,
    }) as unknown as ResponseData
}
Copy the code

3.2.3 Component

To view user details, we need a drawer that displays user details, so create a file named userdetail. vue under SRC /components and write the following code:

<template> <el-drawer V-model ="state.visible" :before-close="handleClose" direction=" RTL "size="500px" title=" user details" @opened="handleSearch" > < el-Descriptions: Column ="1" border class="detail" > < el-Descriptions -item label=" user name ">{{ State.user. username}}</el-descriptions-item> <el-descriptions-item label=" role ">{{state.user.role </el-descriptions-item> <el-descriptions-item label=" status ">{{state. User. Is_active}}</el-descriptions-item <el-descriptions-item label=" mailbox ">{{state.user.email}}</el-descriptions-item> <el-descriptions-item label=" created time ">{{ State.user. created_at}}</el-descriptions-item> <el-descriptions-item label=" last login time ">{{state.user. Last_login }}</el-descriptions-item> </el-descriptions> </el-drawer> </template> <script lang="ts"> import {defineComponent, reactive} from "vue"; import {User} from ".. /types"; import {getUserDetail} from ".. /api/service"; export default defineComponent({ name: "UserDetail", props: { visible: { type: Boolean, require: true, }, userId: { type: Number, require: true, }, loading: { type: Boolean, require: true, } }, emits: ["close",], watch: { '$props.visible': { async handler(val: Boolean, oldVal: Boolean) { if (val ! == oldVal) { this.state.visible = val } } } }, setup(props) { const state = reactive({ visible: props.visible as Boolean, user: {} as User, }); return { state, } }, methods: { handleClose(isOk: Boolean) { this.$emit("close", { user: this.state.user, isOk, }) }, async handleSearch() { this.state.user = await getUserDetail(this.$props.userId) } } }) </script> <style scoped> .detail  { padding: 24px; margin-top: -12px; border-top: #eeeeee 1px solid; } </style>Copy the code

3.2.4 Viewlayer

In user management, we display all user information in one table, and provide details view, enable and disable functions in the operation column of the table.

Add method timestampToTime under SRC /utils/index.ts

export function timestampToTime(timestamp: Date | any, dayMinSecFlag: boolean) {
    const date = new Date(timestamp);
    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();
    if(! dayMinSecFlag) {return Y + M + D;
    }
    return Y + M + D + h + m + s;
}
Copy the code

Add the file user. vue under SRC /views/admin to reference the UserDetail component. The specific code is as follows:

<template> <div> <div> < EL-form :inline="true" :model="state.params" class="demo-form-inline"> <el-form-item label=" name "> <el-input V-model ="state.params.name" placeholder=" account "/> </el-form-item> <el-form-item label=" state "> <el-select V-model ="state.params.is_active" placeholder=" select "> <el-option :value="1" label=" placeholder "/> <el-option :value="0" /> </el-select> </el-form-item> <el-form-item> <el-button :loading="state.isLoading" type="primary" @click="handleSearch"> query </el-form> </el-form> </div> <div> <el-table ref="userTable" :data="state.userList" :header-cell-style="{background:'#eef1f6',color:'#606266'}" stripe> <el-table-column Type ="selection" width="55"/> <el-table-column label="ID" prop=" ID" width="80"/> <el-table-column label=" account" Width ="200"/> <el-table-column label=" nickname" width="200"/> <el-table-column label=" status" Prop ="is_active"/> <el-table-column :formatter="datetimeFormatter" label=" registration time "prop="created_at"/> <el-table-column <template #default="scope"> <el-popconfirm V-if ="scope.row.is_active" cancelButtonText=' Cancel ' ConfirmButtonText =' disable 'icon="el-icon-info" iconColor="red" title=" Are you sure to disable this user?" @confirm="disableUser(scope.$index,scope.row)"> <template #reference> <el-button size="small" type="text" </el-button> </template> </el-popconfirm> <el-button v-if="! scope.row.is_active" size="small" type="text" @click.native.prevent="enableUser(scope.$index, Scope.row)"> enable </el-button> <el-button size="small" type="text" @click.native. Prevent ="showUserDetail(scope.row)" </el-button> </template> </el-table-column> </el-table> </div> <div class="pagination"> <el-pagination :page-size="10" :total="state.total" background layout="prev, pager, next"></el-pagination> </div> </div> <UserDetail :user-id="state.userId" :visible="state.showDialog" @close="state.showDialog = false" /> </template> <script lang="ts"> import {defineComponent, reactive} from "vue"; import {ResponseData, User} from ".. /.. /types"; import {ElMessage} from "element-plus"; import {timestampToTime} from ".. /.. /utils"; import {getUserList, saveUser} from ".. /.. /api/service"; import UserDetail from ".. /.. /components/UserDetail.vue"; export default defineComponent({ name: "User", components: {UserDetail}, setup: function () { const state = reactive({ userList: [] as Array<User>, params: { name: '', role: 'Reader', is_active: undefined, page: 1, page_size: 10, }, isLoading: false, total: 0, showDialog: false, userId: 0, saveLoading: false, }); const handleSearch = async (): Promise<void> => { state.isLoading = true; try { const data: ResponseData = await getUserList(state.params); state.isLoading = false; state.userList = data.results; state.total = data.count } catch (e) { console.error(e) state.isLoading = false; }}; const disableUser = async (index: number, row: User) => { await saveUser('patch', {id: row.id, is_active: false} as User); ElMessage({message: "Disabled successfully!" , type: "success", }); await handleSearch() } const enableUser = async (index: number, row: User) => { await saveUser('patch', {id: row.id, is_active: true} as User); ElMessage({message: "Enabled!" , type: "success", }); await handleSearch() } const datetimeFormatter = (row: User, column: number, cellValue: string, index: number) => { return timestampToTime(cellValue, true); } handleSearch() return { state, handleSearch, datetimeFormatter, disableUser, enableUser, } }, methods: { showUserDetail(row: User) { this.state.userId = row.id this.state.showDialog = true; }, } }) </script> <style scoped> .pagination { text-align: right; margin-top: 12px; } </style>Copy the code

3.2.5 Routerlayer

With a new page, we need to define a route to complete the route jump. Write the following code in the SRC /route/index.ts file:

import {createRouter, createWebHistory, RouteRecordRaw} from "vue-router";
import Home from ".. /views/client/Home.vue";

const routes: Array<RouteRecordRaw> = [
    {
        path: "/".name: "Home".component: Home,
        meta: {}}, {path: "/login/".name: "Login".component: () = >
            import(".. /views/admin/Login.vue")}, {path: '/admin'.name: 'Admin'.component: () = > import(".. /views/admin/Admin.vue"),
        children: [{path: '/admin/'.name: 'Dashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/dashboard'.name: 'AdminDashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/user'.name: 'UserManagement'.component: () = > import(".. /views/admin/User.vue"),},]},]const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
});


export default router;
Copy the code

3.3 Label Management

Mainly in order to facilitate flexible to the article marking type, so there is a label management, the attribute of the label is very simple, is a name.

3.3.1 Typelayer

Add the following code to the SRC /types/index.ts file:

export interface Tag {
    id: number.name: string.created_at: string.modified_at: string,}export interface TagList {
    count: number.results: Array<Tag> | any
}
Copy the code

3.3.2 rainfall distribution on 10-12APIlayer

Here to prepare label management related interface, list query, add, modify, delete. Write the following code in SRC/API /service.ts:

export function getTagList(params: any) {
    return request({
        url: '/tag/'.method: 'get',
        params,
    }) as unknown as TagList
}

export function saveTag(method: string, data: Tag) {
    let url = '/tag/'
    if (['put'.'patch'].includes(method)) {
        url += data.id + '/'
    }
    // @ts-ignore
    return request({
        url,
        method,
        data,
    }) as unknown as ResponseData
}

export function addTag(data: Tag) {
    return request({
        url: '/tag/'.method: 'post',
        data,
    }) as unknown as ResponseData
}

export function deleteTag(id: number) {
    return request({
        url: '/tag/' + id + '/'.method: 'delete',})as unknown as ResponseData
}
Copy the code

3.3.3 Component

Provide a popbox component for adding and modifying labels, so create a file tagEditdialog. vue under SRC/Components and write the following code:

<template> <el-dialog v-model="state.visible" :title="state.title" @close="handleClose(false)" width="440px" > <el-form Size = "medium" label - suffix = ": "Class ="form"> <el-form-item label=" name" label-width="80px"> <el-input V-model ="state.name" autocomplete="off" size=""></el-input> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button </el-button> <el-button :loading="loading" type="primary" @click="handleClose(true)" </el-button> </span> </template> </el-dialog> </template> <script lang="ts"> import {defineComponent, PropType, reactive} from "vue"; import {Tag} from ".. /types"; export default defineComponent({ name: "TagEditDialog", props: { visible: { type: Boolean, require: true, }, tag: { type: Object as PropType<Tag>, require: true, }, loading: { type: Boolean, require: true, } }, emits: ["close",], watch: { '$props.visible': { handler(val: Boolean, oldVal: Boolean) { if (val ! == oldVal) { this.state.visible = val } if (val) { this.state.name = this.$props.tag.name this.state.title = }}}}, setup(props) {const state = reactive({visible: props.visible as Boolean, //@ts-ignore name: '', //@ts-ignore title: '' }); return { state, } }, methods: { handleClose(isOk: Boolean) { this.$emit("close", { obj: { //@ts-ignore id: this.$props.tag.id, name: this.state.name }, isOk, }) } } }) </script> <style scoped> .form{ padding-right: 24px; } </style>Copy the code

We doViewlayer

Add, modify, delete and view the list of labels by managing labels in tables. Add the tag. vue file under SRC /views/admin, and write the following code:

<template> <div> <div> < EL-form :inline="true" :model="state.params" class="demo-form-inline"> <el-form-item label=" name "> <el-input V-model ="state.params.name" placeholder=" placeholder "/> </el-form-item> <el-form-item> <el-button IsLoading "type="primary" @click="handleSearch" > query </el-button > </el-form> </el-form> </div> <div class="button-container"> <el-button :loading="state.isLoading" type="primary" @click="showAddDialog" ><i Class ="el-icon-plus" /> 新 增 </el-button> </div> <el-table ref="tagTable" :data=" state.taglist" :header-cell-style="{ background: '#eef1f6', color: '#606266' }" stripe > <el-table-column type="selection" width="55" /> <el-table-column label="ID" prop="id" width="80" /> <el-table-column label=" name" prop="name" width="200" /> <el-table-column :formatter="datetimeFormatter" label=" change time" Prop ="modified_at" /> <el-table-column fixed="right" label=" Operation "width="120"> <template #default="scope"> <el-popconfirm CancelButtonText =" cancelButtonText "confirmButtonText=" delete" icon="el-icon-info" iconColor="red" title=" Confirm to delete series?" @confirm="deleteObject(scope.$index, <el-button size="small" type="text"> </el-button> </template> </el-popconfirm> <el-button size="small" type="text" @click.prevent="showEditDialog(scope.$index, Scope. row)" > edit </el-button> </template> </el-table-column> </el-table> </div> <div class="pagination"> <el-pagination :page-size="10" :total="state.total" background layout="prev, pager, next" ></el-pagination> </div> </div> <TagEditDialog :loading="state.saveLoading" :tag="state.tag" :visible="state.showDialog" @close="handleCloseDialog" /> </template> <script lang="ts"> import { defineComponent, reactive } from "vue"; import { ResponseData, Tag } from ".. /.. /types"; import { addTag, deleteTag, getTagList, saveTag } from ".. /.. /api/service"; import { timestampToTime } from ".. /.. /utils"; import { ElMessage } from "element-plus"; import TagEditDialog from ".. /.. /components/TagEditDialog.vue"; import { useRoute } from "vue-router"; export default defineComponent({ name: "Tag", components: { TagEditDialog }, watch: { "$route.path": { handler(val, oldVal) { if (val ! == oldVal && ["/admin/tag"].includes(val)) this.handleSearch(); }, deep: true, }, }, setup: function () { const route = useRoute(); const state = reactive({ tagList: [] as Array<Tag>, params: { name: undefined, page: 1, page_size: 10, }, isLoading: false, total: 0, showDialog: false, tag: { id: 0, name: "", } as Tag, saveLoading: false, }); const handleSearch = async (): Promise<void> => { state.isLoading = true; try { const data: ResponseData = await getTagList(state.params); state.isLoading = false; state.tagList = data.results; state.total = data.count; } catch (e) { console.error(e); state.isLoading = false; }}; const deleteObject = async (index: number, row: Tag) => { await deleteTag(row.id); ElMessage({message: "Delete successful!" , type: "success", }); await handleSearch(); }; const datetimeFormatter = ( row: Tag, column: number, cellValue: string, index: number ) => { return timestampToTime(cellValue, true); }; handleSearch(); return { state, handleSearch, datetimeFormatter, deleteObject, }; }, methods: { showEditDialog(index: number, row: Tag) { this.state.tag = row; this.state.showDialog = true; }, showAddDialog() { this.state.tag = {} as Tag; this.state.showDialog = true; }, async handleCloseDialog(params: any) { if (! params.isOk) { this.state.showDialog = false; return; } this.state.saveLoading = true; const method = this.state.tag.id ? "put" : "post"; try { await saveTag(method, params.obj); this.state.showDialog = false; this.state.saveLoading = false; await this.handleSearch(); } catch (e) { console.error(e); this.state.saveLoading = false; ,}}}}); </script> <style scoped> .pagination { text-align: right; margin-top: 12px; } </style>Copy the code

3.3.5Routerlayer

Define a route to complete route hops. Add code to SRC /route/index.ts file:

import {createRouter, createWebHistory, RouteRecordRaw} from "vue-router";
import Home from ".. /views/client/Home.vue";

const routes: Array<RouteRecordRaw> = [
    {
        path: "/".name: "Home".component: Home,
        meta: {}}, {path: "/login/".name: "Login".component: () = >
            import".. /views/admin/Login.vue")}, {path: '/admin'.name: 'Admin'.component: () = > import(".. /views/admin/Admin.vue"),
        children: [{path: '/admin/'.name: 'Dashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/dashboard'.name: 'AdminDashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/user'.name: 'UserManagement'.component: () = > import(".. /views/admin/User.vue"),}, {path: '/admin/tag'.name: 'Tag'.component: () = > import(".. /views/admin/Tag.vue"),},]},]const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
});


export default router;
Copy the code

3.4 Classification and article management

Article and classification are two business objects closely related, so the functions of classification management and article management are put on the same page.

3.4.1 trackTypelayer

Add the following code to the SRC /types/index.ts file:

export interface Catalog {
    id: number.name: string.parent: number.parents: Array<number>,
    children: Array<Catalog>

}

export interface Article {
    id: number.title: string.cover: string.toc: string.excerpt: string.markdown: string.html: string.create_at: string.views: number.likes: number.comments: number.words: number.tags: Array<number> | any.tags_info: Array<Tag> | any
    catalog: number.catalog_info: Catalog,
    created_at: string.modified_at: string.author: string, status? :string,}export interface ArticleArray {
    count: number.results: Array<Article> | any
}

export interface ArticleParams {
    title: string | any.status: string | any.tags: Array<number> | any.catalog: number | any.page: number.page_size: number,}Copy the code

3.4.2 APIlayer

Here to prepare label management related interface, list query, add, modify, delete. Write the following code in SRC/API /service.ts:

export function getCatalogTree() {
    return request({
        url: '/catalog/'.method: 'get',})as unknown as Array<Catalog>
}

export function saveCatalog(method: string, data: Catalog) {
    let url = '/catalog/'
    if (['put'.'patch'].includes(method)) {
        url += data.id + '/'
    }
    // @ts-ignore
    return request({
        url,
        method,
        data,
    }) as unknown as ResponseData

}

export function deleteCatalog(catalogId: number) {

    return request({
        url: '/catalog/' + catalogId + '/'.method: 'delete',})as unknown as ResponseData

}

export function getArticleList(params: ArticleParams) {
    return request({
        url: '/list/'.method: 'get',
        params
    }) as unknown as ArticleArray
}

export function remoteDeleteArticle(articleId: number) {
    return request({
        url: '/article/' + articleId + '/'.method: 'delete',})as unknown as ResponseData
}

export function getArticleDetail(articleId: number) {
    return request({
        url: '/article/' + articleId + '/'.method: 'get',})as unknown as Article
}

export function remoteSaveArticle(method: string, data: Article) {
    let url = '/article/'
    if (['put'.'patch'].includes(method)) {
        url += data.id + '/'
    }
    // @ts-ignore
    return request({
        url,
        method,
        data,
    }) as unknown as Article
}

export function remotePublishArticle(articleId: number) {

    // @ts-ignore
    return request({
        url: '/publish/' + articleId + '/'.method: 'patch',})as unknown as Article
}

export function remoteOfflineArticle(articleId: number) {
    return request({
        url: '/offline/' + articleId + '/'.method: 'patch',})as unknown as Article
}
Copy the code

Rule 3.4.3Component

Create a file catalogtree. vue under SRC/Components to manage catalogtree. vue

<template> <el-drawer V-model ="state.visible" :before-close="handleClose" direction=" RTL "size="500px" title=" directory management" @opened="handleSearch" > <div class="drawer-content"> <el-tree :data="state.catalogs" :expand-on-click-node="false" :props="defaultProps" default-expand-all node-key="id"> <template #default="{ node, data }"> <span class="custom-tree-node"> <span>{{ node.label }}</span> <span> <el-dropdown trigger="click"> <span class="el-dropdown-link"> <i class="el-icon-more"/> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item <a class="more button" @click.prevent="showEditDialog(data)"> </a> </el-dropdown-item> <a class="more button" @click.prevent="showAddDialog(data)"> New </a> </el-dropdown-item> <el-dropdown-item icon="el-icon-delete-solid"> <el-popconfirm :title=" confirm to delete ['+data.name+']? '" cancelButtonText=' cancel 'confirmButtonText=' delete' icon="el-icon-info" iconColor="red" @confirm="remove(data)"> <template #reference> <a class="more-button"> Delete </a> </template> </el-popconfirm> </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </span> </span> </template> </el-tree> </div> </el-drawer> <el-dialog V-model ="state.showDialog" :title="state.dialogTitle"> <el-form class="form" label-suffix=" : "Label-width ="120px" size="medium"> <el-form-item label=" catalog name" > <el-input V-model ="state.catalog. Name" autocomplete="off"></el-input> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="state. ShowDialog =false"> </el-button> <el-button :loading="state. @click="saveCatalog"> save </el-button> </span> </template> </el-dialog> </template> <script lang="ts"> import {defineComponent, reactive} from "vue"; import {Catalog} from ".. /types"; import {deleteCatalog, getCatalogTree, saveCatalog} from ".. /api/service"; import {ElMessage} from "element-plus"; export default defineComponent({ name: "CatalogTree", props: { visible: { type: Boolean, require: true, } }, watch: { '$props.visible': { handler(val, oldVal) { if (val ! = oldVal) { this.state.visible = val } } } }, emits: ['close',], setup(props) { const state = reactive({ catalogs: [] as Array<Catalog>, visible: props.visible, showDialog: false, catalog: {} as Catalog, dialogTitle: '', loading: false, }) const handleSearch = async () => { state.catalogs = await getCatalogTree(); } const defaultProps = { children: 'children', label: 'name', } return { state, handleSearch, defaultProps } }, methods: { handleClose() { this.$emit('close') }, showAddDialog(data: Catalog) { this.state.showDialog = true //@ts-ignore this.state.catalog.id = undefined //@ts-ignore This. The state. The catalog. Name = undefined this. State. The catalog. The parent = data. The id this. State. DialogTitle = 'new directory'}, showEditDialog(data: Catalog) {this.state.showDialog = true this.state. Catalog = data this.state.dialogTitle = 'modify directory'}, async saveCatalog() { try { this.state.loading = true const method = this.state.catalog.id ? 'patch' : 'post' await saveCatalog(method, This.state. Catalog) this.state. Loading = false this.state. ShowDialog = false ElMessage({message: 'saved successfully ', type: 'success'}) await this.handlesearch ()} catch (e) {console.error(e) ElMessage({message: 'save failed ', type: 'error' }) this.state.loading = false } }, async remove(data: Catalog) {await deleteCatalog(data.id) ElMessage({message: 'delete succeeded ', type: 'success' }) await this.handleSearch() } } }) </script> <style lang="less" scoped> .drawer-content { padding: 12px 0 0 24px; border-top: #eeeeee 1px solid; overflow: auto; } .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 32px; } .add-button { margin-bottom: 12px; } </style>Copy the code

Since the article management interface requires a Markdown editor, install a dependency on the Markdown editor

Yarn add @kangc/[email protected] yarn add [email protected]Copy the code

Add editor JS, CSS, and plug-ins to main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from "./router";
import { StateKey, store } from "./store";
import 'element-plus/lib/theme-chalk/index.css';
import 'element-plus/lib/theme-chalk/base.css';

// @ts-ignore
import VMdEditor from '@kangc/v-md-editor';
import '@kangc/v-md-editor/lib/style/base-editor.css';
// @ts-ignore
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
import '@kangc/v-md-editor/lib/theme/style/github.css';

// highlightjs
import hljs from 'highlight.js';

VMdEditor.use(githubTheme, {
    Hljs: hljs,
});
import {
    ElAffix,
    ElButton,
    ElCard,
    ElCascader,
    ElCol,
    ElDescriptions,
    ElDescriptionsItem,
    ElDialog,
    ElDrawer,
    ElDropdown,
    ElDropdownItem,
    ElDropdownMenu,
    ElForm,
    ElFormItem,
    ElIcon,
    ElInput,
    ElLoading,
    ElMenu,
    ElMenuItem,
    ElMessage,
    ElMessageBox,
    ElOption,
    ElPagination,
    ElPopconfirm,
    ElProgress,
    ElRow,
    ElSelect,
    ElTable,
    ElTableColumn,
    ElTag,
    ElTimeline,
    ElTimelineItem,
    ElTooltip,
    ElTree,
    ElUpload,
} from 'element-plus';

const app = createApp(App)


const components = [
    ElAffix,
    ElButton,
    ElCard,
    ElCascader,
    ElCol,
    ElDescriptions,
    ElDescriptionsItem,
    ElDialog,
    ElDrawer,
    ElDropdown,
    ElDropdownItem,
    ElDropdownMenu,
    ElForm,
    ElFormItem,
    ElIcon,
    ElInput,
    ElLoading,
    ElMenu,
    ElMenuItem,
    ElMessage,
    ElMessageBox,
    ElOption,
    ElPagination,
    ElPopconfirm,
    ElProgress,
    ElRow,
    ElSelect,
    ElTable,
    ElTableColumn,
    ElTag,
    ElTimeline,
    ElTimelineItem,
    ElTooltip,
    ElTree,
    ElUpload,
]

const plugins = [
    ElLoading,
    ElMessage,
    ElMessageBox,
]

components.forEach(component= > {
    app.component(component.name, component)
})

plugins.forEach(plugin= > {
    app.use(plugin)
})

app.use(router).use(store, StateKey).use(VMdEditor).mount('#app')
Copy the code

Provide a drawer component for editing articles, so create the file editarticle. vue under SRC/Components and write the following code:

<template> <el-drawer v-model="state.visible" :before-close="handleClose" :title="articleId? @opened="handleSearch" > <div class="article-form" style="overflow-y: Auto "> < el - form the label - suffix =" : "Label-width ="120px"> <el-form-item label=" title">< el-input ref="articleTitle" v-model="state.article.title"></el-input> </el-form-item> <el-form-item label=" category "> < el-Cascader V-model ="state.catalogs" :options="state.catalogTree" :props="{  checkStrictly: true, value:'id',label:'name',expandTrigger: 'hover'}" clearable size="medium" style="width: 100%"/> </el-form-item> <el-form-item label=" label "> <el-select V-model ="state.article. Tags "clearable multiple Placeholder =" size="medium" style=" max-width: 100%; 100%"> <el-option v-for="s in state.tags" :label="s.name" :value="s.id" :key="s.id"/> </el-select> </el-form-item> <el-form-item label=" abstract ">< el-input V-model ="state.article. File ":rows="5" type="textarea"></el-input> </el-form-item> <el-form-item label=" text ">< V-md-editor v-model="state.article.markdown" height="600px"></ V-md-Editor > </el-form-item> <el-form-item label=" cover "> <el-upload :before-upload="beforeAvatarUpload" :headers="csrfToken" :on-success="handleAvatarSuccess" :show-file-list="false" action="/api/upload/" class="avatar-uploader" > <img v-if="state.article.cover" :src="state.article.cover" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> </el-form-item> </el-form> </div> <div class="demo-drawer__footer"> <el-button </el-button> <el-button :loading="state.loading" type="primary" @click="saveArticle"> save </el-button> </div> </el-drawer> </template> <script lang="ts"> import {defineComponent, reactive} from "vue"; import {getArticleDetail, getCatalogTree, getTagList, remoteSaveArticle} from ".. /api/service"; import {Article, Catalog, Tag, TagList} from ".. /types"; import {getCookie} from ".. /utils"; export default defineComponent({ name: "EditArticle", props: { articleId: { type: Number, require: true, default: undefined, }, visible: { type: Boolean, require: true, } }, watch: { '$props.visible': { handler(val: Boolean, oldVal: Boolean) { if (val ! == oldVal) { this.state.visible = val } } } }, emits: ["close",], setup(props, context) { const state = reactive({ article: {} as Article, loading: false, visible: false as Boolean, catalogTree: [] as Array<Catalog>, tags: [] as Array<Tag>, catalogs: [] as Array<number> }) const saveArticle = async () => { try { state.loading = true if (state.catalogs.length) { state.article.catalog = state.catalogs[state.catalogs.length - 1] } if (props.articleId) { await remoteSaveArticle('put', state.article) } else { await remoteSaveArticle('post', state.article) } state.loading = false context.emit('close', true) } catch (e) { state.loading = false } } const csrfToken = {'X-CSRFToken': getCookie('csrftoken')} return { state, saveArticle, csrfToken } }, methods: { async handleSearch() { this.$refs.articleTitle.focus() if (this.$props.articleId) { this.state.article = await getArticleDetail(this.$props.articleId) this.state.article.tags = this.state.article.tags_info.map((tag: Tag) => tag.id) this.state.catalogs = this.state.article.catalog_info.parents } else { this.state.article = {} as Article } this.state.catalogTree = await getCatalogTree() if (! this.state.tags.length) { const tags: TagList = await getTagList({}) this.state.tags = tags. Results}}, handleClose(done: any) {this.$confirm(' Confirm to close drawer? ', 'prompt ', {confirmButtonText:' close ', cancelButtonText: 'cancel ', type: 'warning'}). Then ((_: any): void => { this.$emit("close", false) this.state.article = {} as Article done(); }) .catch((_: any): void => { console.error(_) }); }, handleAvatarSuccess(res: any, file: File) { this.state.article.cover = res.url }, beforeAvatarUpload(file: File) { const isImage = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'].includes(file.type); const isLt2M = file.size / 1024 / 1024 < 2; if (! IsImage) {this.$message.error(' Upload image can only be JPG! '); } if (! IsLt2M) {this.$message.error(' Upload image size cannot exceed 2MB! '); } return isImage && isLt2M; } } }) </script> <style lang="less"> .article-form { padding: 24px; overflow-y: auto; border-top: 1px solid #e8e8e8; height: calc(100% - 100px); } // drawer //element- UI fixed bottom button of drawer. El-drawer__body {margin-bottom: 50px; height: 100% ! important; } .el-drawer__header{ margin-bottom: 16px; } .demo-drawer__footer { width: 100%; position: absolute; bottom: 0; left: 0; border-top: 1px solid #e8e8e8; padding: 10px 16px; text-align: right; background-color: white; } // remove element- UI in the header of the drawer :deep(:focus){outline: 0; } .avatar-uploader { background-color: #fbfdff; border: 1px dashed #c0ccda; border-radius: 6px; box-sizing: border-box; width: 125px; height: 100px; cursor: pointer; line-height: 100px; text-align: center; font-size: 20px; } </style>Copy the code

3.4.4 Viewlayer

Add Article. Vue file under SRC /views/admin and write the following code:

<template> <div> <div> < EL-form :inline="true" class="demo-form-inline"> <el-form-item label=" title" > <el-input ref="title" V-model ="state.params.title" placeholder=" title" /> </el-form-item> <el-form-item label=" status "> <el-select V-model =" state.ams. Status "placeholder=" placeholder "> <el-option label=" Published" value="Published"/> <el-option label=" draft" value="Draft"/> </el-select> </el-form-item> <el-form-item> <el-button :loading="state.isLoading" type="primary" @click="handleSearch"> </el-form> </el-form> </div> <div class="button-container"> <el-button :loading="state. IsLoading "type="primary" @click="showAddDrawer">< I class="el-icon-plus"/> new </el-button> <el-button circle icon="el-icon-s-unfold" @click="state.showCatalogTree=true"/> </div> <div> <el-table ref="articleTable" :data="state.articleList" :header-cell-style="{background:'#eef1f6',color:'#606266'}" stripe style="width: 100%"> <el-table-column type="selection" width="55"/> <el-table-column label="ID" prop="id" width="80"/> <el-table-column label=" title" prop="title" width="200"/> <el-table-column label=" status" prop="status" width="100"/> <el-table-column label=" catalog "prop="catalog_info.name"/> <el-table-column :formatter="datetimeFormatter" label=" Modified time" Prop ="modified_at"/> <el-table-column fixed="right" label=" Operation "width="120"> <template #default="scope"> <el-popconfirm CancelButtonText =' cancelButtonText 'confirmButtonText=' delete' icon="el-icon-info" iconColor="red" title=" Are you sure to delete this article?" $index,scope.row)"> <template #reference> <el-button size="small" type="text" </el-button> </template> </el-popconfirm> <el-button size="small" type="text" @click.prevent="showEditDrawer(scope.$index, Scope. Row)"> </el-button> <el-button v-if="scope.row. Status ===' draft '" size="small" type="text" @click.prevent="publishArticle(scope.$index, $scope. Row)"> </el-button> <el-button V-else size="small" type="text" @click.prevent="offlineArticle(scope. Scope. row)"> </el-button> </template> </el-table-column> </el-table> </div> <div class="pagination"> <el-pagination :page-size="10" :total="state.total" background layout="prev, pager, next"></el-pagination> </div> </div> <EditArticle :article-id="state.articleId" :visible="state.showDrawer" @close="handleCloseDrawer" /> <CatalogTree :visible="state.showCatalogTree" @close="state.showCatalogTree=false" /> </template> <script lang="ts"> import {defineComponent, reactive} from "vue"; import {Article, ArticleArray, ArticleParams} from ".. /.. /types"; import {getArticleList, remoteDeleteArticle, remoteOfflineArticle, remotePublishArticle} from ".. /.. /api/service"; import {timestampToTime} from ".. /.. /utils"; import {ElMessage} from "element-plus"; import EditArticle from ".. /.. /components/EditArticle.vue"; import CatalogTree from ".. /.. /components/CatalogTree.vue"; export default defineComponent({ name: "Article", components: {CatalogTree, EditArticle}, setup: function () { const state = reactive({ articleList: [] as Array<Article>, params: { title: undefined, status: undefined, tags: undefined, catalog: undefined, page: 1, page_size: 10, } as ArticleParams, isLoading: false, total: 0, showDrawer: false, articleId: 0, showCatalogTree: false, }); const handleSearch = async (): Promise<void> => { state.isLoading = true; try { const data: ArticleArray = await getArticleList(state.params); state.isLoading = false; state.articleList = data.results; state.total = data.count } catch (e) { console.error(e) state.isLoading = false; }}; const publishArticle = async (index: number, row: Article) => {try {await remotePublishArticle(row.id) ElMessage({message: "publish success!" , type: "success", }); await handleSearch() } catch (e) { console.error(e) } } const offlineArticle = async (index: number, row: Article) => {try {await remoteOfflineArticle(row.id) ElMessage({message: "offline successfully!" , type: "success", }); await handleSearch() } catch (e) { console.error(e) } } const deleteArticle = async (index: number, row: Article) => { await remoteDeleteArticle(row.id); ElMessage({message: "Delete successful!" , type: "success", }); await handleSearch() } const datetimeFormatter = (row: Article, column: number, cellValue: string, index: number) => { return timestampToTime(cellValue, true); } handleSearch() const handleCloseDrawer = (isOk: boolean) => { state.showDrawer = false if (isOk) { handleSearch() } } return { state, handleSearch, datetimeFormatter, deleteArticle, handleCloseDrawer, publishArticle, offlineArticle } }, mounted() { this.$refs.title.focus() }, methods: { showEditDrawer(index: number, row: Article) { this.$refs.articleTable.setCurrentRow(row) this.state.showDrawer = true; this.state.articleId = row.id }, showAddDrawer() { this.state.showDrawer = true; this.state.articleId = 0; } } }) </script> <style scoped> .pagination { text-align: right; margin-top: 12px; } </style>Copy the code

3.4.5 Routerlayer

Define a route to complete route hops. Add code to SRC /route/index.ts file:

import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import Home from ".. /views/client/Home.vue";

const routes: Array<RouteRecordRaw> = [
    {
        path: "/".name: "Home".component: Home,
        meta: {}}, {path: "/login/".name: "Login".component: () = >
            import(".. /views/admin/Login.vue")}, {path: '/admin'.name: 'Admin'.component: () = > import(".. /views/admin/Admin.vue"),
        children: [{path: '/admin/'.name: 'Dashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/dashboard'.name: 'AdminDashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/user'.name: 'UserManagement'.component: () = > import(".. /views/admin/User.vue"),}, {path: '/admin/tag'.name: 'Tag'.component: () = > import(".. /views/admin/Tag.vue"),}, {path: '/admin/article'.name: 'ArticleManagement'.component: () = > import(".. /views/admin/Article.vue"),},]},]const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
});


export default router;
Copy the code

3.4.6 vite. Config. Ts

Since we need to display the uploaded image, we need to proxy the uploaded image. Add the following proxy in the viet.config. ts file:

'/upload': {
     target: 'http://localhost:8000/'.changeOrigin: true.ws: false.rewrite: (pathStr) = > pathStr.replace('/api'.' '),
     timeout: 5000,},Copy the code

3.5 Comment Management

3.5.1 track ofTypelayer

Add the following code to the SRC /types/index.ts file:

export interface CommentInfo {
    id: number.user: number.user_info: User | any.article: number.article_info: Article | any.created_at: string.reply: number | any.content: string.comment_replies: CommentInfo | any,}export interface CommentPara {
    user: number.article: number.reply: number | any.content: string.page: number.page_size: number
}
Copy the code

3.5.2 APIlayer

This is where the list query is handled. Write the following code in SRC/API /service.ts:

export function getCommentList(params: CommentPara) {
    return request({
        url: '/comment/'.method: 'get',
        params,
    }) as unknown as ResponseData
}
Copy the code

3.5.3 Component

Because the comments do not need to modify delete operations, only view the details of the comments, so reuse the article details page.

3.5.4 Viewlayer

Add comment. vue file under SRC /views/admin to view comments in the table, and write the following code:

<template> <div> <div> < EL-form :inline="true" :model="state.params" class="demo-form-inline"> <el-form-item label=" account "> <el-select V-model ="state.params.user" filterable placeholder=" please select "> <el-select v-for="item in state.userList" :key="item.id" :label="item.nickname || item.username" :value="item.id"> </el-option> </el-select> </el-form-item> <el-form-item label=" content" > < EL-input V-model =" state.ams. Content "placeholder=" comment content" /> </el-form-item> <el-form-item> <el-button :loading="state. Loading "type="primary" @click="handleSearch" </el-button> </el-form-item> </el-form> </div> <div> <el-table ref="articleTable" :data="state.commentList" :header-cell-style="{background:'#eef1f6',color:'#606266'}" stripe> <el-table-column type="selection" width="55"/> <el-table-column label="ID" prop=" ID" width="80"/> <el-table-column label=" commenter "prop="user_info.name" width="200"/> <el-table-column label=" article "prop="content" width="200"/> <el-table-column label=" article" prop="article_info.title"/> <el-table-column label=" prop="reply. Id "width="200"/> <el-table-column :formatter="datetimeFormatter" label=" Comment time" Prop ="created_at"/> <el-table-column label=" operation "> <template #default="scope"> <el-button size="small" type="text" @click.prevent="showDetail(scope.row)" </el-button> </template> </el-table-column> </el-table> </div> <div class="pagination"> <el-pagination :page-size="10" :total="state.total" background layout="prev, pager, next"></el-pagination> </div> </div> </template> <script lang="ts"> import {defineComponent, reactive} from "vue"; import {Article, CommentInfo, CommentPara, ResponseData, User} from ".. /.. /types"; import {ElMessage} from "element-plus"; import {timestampToTime} from ".. /.. /utils"; import {getCommentList, getUserList, saveUser} from ".. /.. /api/service"; import UserDetail from ".. /.. /components/UserDetail.vue"; export default defineComponent({ name: "Comment", components: {UserDetail}, setup: function () { const state = reactive({ commentList: [] as Array<CommentInfo>, params: { user: undefined, article: undefined, reply: undefined, content: '', page: 1, page_size: 10, } as unknown as CommentPara, total: 0, userList: [] as Array<User>, loading: false, }); const handleSearch = async (): Promise<void> => { state.loading = true; try { const data: ResponseData = await getCommentList(state.params); state.loading = false; state.commentList = data.results; state.total = data.count } catch (e) { console.error(e) state.loading = false; }}; const getUsers = async (): Promise<void> => { try { const data: ResponseData = await getUserList({}); state.userList = data.results; } catch (e) { console.error(e) } }; const datetimeFormatter = (row: Article, column: number, cellValue: string, index: number) => { return timestampToTime(cellValue, true); } handleSearch() getUsers() return { state, handleSearch, datetimeFormatter, } }, methods: { showDetail(row: CommentInfo) { const {href} = this.$router.resolve({ path: '/article/', query: { id: row.article_info.id } }); window.open(href, "_blank"); }, } }) </script> <style scoped> .pagination { text-align: right; margin-top: 12px; } </style>Copy the code

2.6.2Routerlayer

Define a route to complete route hops. Add code to SRC /route/index.ts file:

import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import Home from ".. /views/client/Home.vue";

const routes: Array<RouteRecordRaw> = [
    {
        path: "/".name: "Home".component: Home,
        meta: {}}, {path: "/login/".name: "Login".component: () = >
            import(/* webpackChunkName: "login" */ ".. /views/admin/Login.vue")}, {path: '/admin'.name: 'Admin'.component: () = > import(/* webpackChunkName: "admin" */ ".. /views/admin/Admin.vue"),
        children: [{path: '/admin/'.name: 'Dashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/dashboard'.name: 'AdminDashboard'.component: () = > import(".. /views/admin/Dashboard.vue"),}, {path: '/admin/user'.name: 'UserManagement'.component: () = > import(".. /views/admin/User.vue"),}, {path: '/admin/tag'.name: 'Tag'.component: () = > import(".. /views/admin/Tag.vue"),}, {path: '/admin/article'.name: 'ArticleManagement'.component: () = > import(".. /views/admin/Article.vue"),}, {path: '/admin/comment'.name: 'CommentManagement'.component: () = > import(".. /views/admin/Comment.vue"),},]},]const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
});


export default router;
Copy the code

3.6 Managing the Background Home Page

3.6.1 trackTypelayer

Add the following code to the SRC /types/index.ts file:

export interface NumberInfo {
    views: number.likes: number.comments: number.messages: number
}
Copy the code

3.6.2 APIlayer

Here to prepare label management related interface, list query, add, modify, delete. Write the following code in SRC/API /service.ts:

export function getTopArticleList() {
    return request({
        url: '/top/'.method: 'get',})as unknown as ResponseData
}

export function getNumbers() {
    return request({
        url: '/number/'.method: 'get',})as unknown as NumberInfo
}
Copy the code

3.6.3 Component

No additional components need to be provided.

3.6.4 radar echoes capturedViewlayer

Through ICONS and index CARDS show site, in the form of the overall situation, modify SRC/views/admin/Dashboard vue, write the following code:

<template> <div> <div class="title"> <el-row :gutter="24" class="numbers"> <el-col :span="6" class="el-col-6"> <el-card> <div class="number-card"> <div> <i class="el-icon-user number-icon"></i> </div> <div Class ="number-right"> <div class="number-num">{{state.numbers. Views}}</div> <div> </div> </div> </div> </el-card> </el-col> <el-col :span="6" class="el-col-6"> <el-card> <div class="number-card"> <div> <i class="el-icon-thumb number-icon" style="background: #64d572;" > < / I > < / div > < div class = "number - right" > < div class = "number - num" > {{state. Numbers. Likes}} < / div > < div > thumb up amount < / div > < / div > </div> </el-card> </el-col> <el-col :span="6" class="el-col-6"> <el-card> <div class="number-card"> <div> <i class="el-icon-chat-line-square number-icon" style="background: #f25e43;" > < / I > < / div > < div class = "number - right" > < div class = "number - num" > {{state.numbers.com ments}} < / div > < div > comment quantity < / div > < / div >  </div> </el-card> </el-col> <el-col :span="6" class="el-col-6"> <el-card> <div class="number-card"> <div> <i class="el-icon-message number-icon" style="background-color: #42B983"></i> </div> <div class="number-right"> <div class="number-num">{{ state.numbers.messages }}</div> < div > message quantity < / div > < / div > < / div > < / el - card > < / el - col > < / el - row > < div class = "top - articles" > < el - card > < # template header > </template> <div class="article-list"> <div v-for="(article,index) in state.articlelist "class="article" @click="viewArticle(article.id)"> <span style="font-size: 14px">{{ index + 1 + '. ' + article.title }}</span> <span style="color: #999999; font-size: 14px">{{ article.views }} / {{ article.likes }}</span> </div> </div> </el-card> </div> </div> </template> <script lang="ts"> import {defineComponent, reactive} from "vue"; import {Article} from ".. /.. /types"; import {getNumbers, getTopArticleList} from ".. /.. /api/service"; export default defineComponent({ name: "Dashboard", setup() { const state = reactive({ numbers: { views: 0, likes: 0, comments: 0, messages: 0 }, articleList: [{title: 'a', views: 1, likes: 1}] as Array<Article>, }) return { state, } }, async mounted() { this.state.articleList = (await getTopArticleList()).results this.state.numbers = await getNumbers() }, methods: { viewArticle(id: number) { const {href} = this.$router.resolve({ path: '/article/', query: { id } }); window.open(href, "_blank"); } } }) </script> <style lang="less" scoped> .numbers { width: 100%; } .title { color: #999; margin: 12px 0; padding-left: 8px; font-size: 14px; } :deep(.el-card__body){ margin: 0; padding: 0; } .number-card { margin: 0; padding: 0; display: -webkit-box; display: -ms-flexbox; display: flex; flex: 1; -webkit-box-align: center; -ms-flex-align: center; align-items: center; height: 80px; border: 1px solid #ebeef5; background-color: #fff; border-radius: 4px; overflow: hidden; } .number-right { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; text-align: center; font-size: 14px; color: #999; } .number-num { font-size: 30px; font-weight: 700; color: #2d8cf0; text-align: center; } .number-icon { font-size: 50px; width: 80px; height: 80px; text-align: center; line-height: 80px; color: #fff; background: #2d8cf0; } .top-articles { margin: 24px 24px 24px 0; } .article-list { padding: 20px; } .article { cursor: pointer; display: flex; flex: 1; justify-content: space-between; padding: 12px 24px 12px 12px; border-top: #eeeeee 1px solid; } .article:first-child { border-top: none; padding-top: 0; } .article:last-child { padding-bottom: 0; } .dashboard-list { display: flex; flex: 1; justify-content: space-evenly; padding: 24px; margin-right: 24px;; } .percentage-value { display: block; margin-top: 10px; font-size: 28px; } .percentage-label { display: block; margin-top: 10px; font-size: 12px; } </style>Copy the code

3.6.5Routerlayer

When accessing the admin path, you need to check whether the user is logged in and whether the user is an administrator. Therefore, add the following code to SRC /router/index.ts:

router.beforeEach((to, from, next) = > {
    if (/\/admin/i.test(to.path) && (! store.state.user.id || store.state.user.role ! = ='Admin')) {
        next('/login')
        return
    }
    next()
})
Copy the code

In the SRC/views/admin/Login. Line 143 in the vue after add one line of code:

is_superuser: data.is_superuser
Copy the code

At this point, the front-end development of the management background is completed

Four, to achieve the effect display

4.1 Back-end Effect

4.1.1 Accessing API Documentation

In the browser to http://127.0.0.1:8000/swagger/, the effect is as follows:

4.1.2 Back-end Folder Structure

4.2 Front-end Effect

4.2.1 Front-end Management Background Page Effect

4.2.2 Front-end code structure

In the next post, we wrote a page for our blog site to be used by users.