前端使用ant design vue,后端使用DRF结合django_filters,完成分页,过滤搜索功能

这是一个比较大的过程,我尽量把相关的重要知识点连贯起来。

一,先上效果图

截屏2020-12-11下午4.07.28.png

二,后端Model定义

import uuid
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()


# 指标集
class ViewSet(models.Model):
    view_id = models.CharField(max_length=64,
                               verbose_name='指标集id')
    view_name = models.CharField(max_length=64,
                                 verbose_name='指标集名称')
    description = models.CharField(max_length=1024,
                                   null=True,
                                   blank=True,
                                   verbose_name='指标集描述')
    create_user = models.ForeignKey(User,
                                    null=True,
                                    blank=True,
                                    related_name='ra_view_set',
                                    on_delete=models.CASCADE,
                                    verbose_name='创建者')
    create_date = models.DateTimeField(auto_now_add=True, verbose_name='新建时间')
    update_date = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    status = models.BooleanField(default=True, verbose_name='状态')

    @property
    def username(self):
        return self.create_user.username

    def __str__(self):
        return self.view_name

    class Meta:
        db_table = 'ViewSet'
        ordering = ('-update_date', )


# 指标
class Attr(models.Model):
    attr_id = models.CharField(max_length=64,
                               verbose_name='指标id')
    attr_name = models.CharField(max_length=64,
                                 verbose_name='指标名称')
    view_set = models.ForeignKey(ViewSet,
                                 related_name='ra_attr',
                                 on_delete=models.CASCADE,
                                 verbose_name='指标集')
    description = models.CharField(max_length=1024,
                                   null=True,
                                   blank=True,
                                   verbose_name='指标集描述')
    security_token = models.CharField(max_length=64,
                                      null=True,
                                      blank=True,
                                      verbose_name='连接token')
    url = models.CharField(max_length=1024,
                           null=True,
                           blank=True,
                           verbose_name='监控url')
    create_user = models.ForeignKey(User,
                                    null=True,
                                    blank=True,
                                    related_name='ra_attr',
                                    on_delete=models.CASCADE,
                                    verbose_name='创建者')
    create_date = models.DateTimeField(auto_now_add=True, verbose_name='新建时间')
    update_date = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    status = models.BooleanField(default=True, verbose_name='状态')

    @property
    def username(self):
        return self.create_user.username

    def __str__(self):
        return self.attr_name

    class Meta:
        db_table = 'Attr'
        ordering = ('-update_date',)


# 异常库
class Anomaly(models.Model):
    attr = models.ForeignKey(Attr,
                             related_name='ra_anomaly',
                             on_delete=models.CASCADE,
                             verbose_name='指标')
    anomaly_time = models.DateTimeField(verbose_name='异常检测时间')
    data_a = models.TextField(verbose_name='当天180分钟数据')
    data_b = models.TextField(verbose_name='一天前180分钟数据')
    data_c = models.TextField(verbose_name='一周前180分钟数据')
    mark_flag = models.CharField(max_length=16,
                                 null=True,
                                 blank=True,
                                 verbose_name='标注正负样本')
    create_user = models.ForeignKey(User,
                                    related_name='ra_anomaly',
                                    on_delete=models.CASCADE,
                                    verbose_name='创建者')
    create_date = models.DateTimeField(auto_now_add=True, verbose_name='新建时间')
    update_date = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    status = models.BooleanField(default=True, verbose_name='状态')

    @property
    def username(self):
        return self.create_user.username

    @property
    def attr_name(self):
        return self.attr.attr_name

    @property
    def view_set_name(self):
        return self.attr.view_set.view_name

    def __str__(self):
        return self.attr.attr_name

    class Meta:
        db_table = 'Anomaly'
        ordering = ('-update_date', )

这里主要关注的是异常库Anomaly有外键关联Attr指标库,而Attr库有外键关联指标集库ViewSet。

三,urls.py里定义路由

path('list/', api_views.AnomalyListView.as_view(), name='list'),
as_view()用于将类方法化。

四,DRF中的类视图定义

class AnomalyListView(ListAPIView):
    queryset = Anomaly.objects.all()
    serializer_class = AnomalySerializer
    pagination_class = PNPagination
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filter_class = AnomalyFilter
    ordering_fields = ['id']

    def get(self, request, *args, **kwargs):
        res = super().get(self, request, *args, **kwargs)
        return_dict = build_ret_data(OP_SUCCESS, res.data)
        return render_json(return_dict)

DRF的ListAPIView及其它几个generics View,是封装度合适的。相比于API VIEW,很多方法和变量都已有内置实现。相比于VIEWSET,又保留了很多清晰明白的自定义实现功能。
其中build_ret_data函数,用于统一封装返回数据,定义如下:

def build_ret_data(ret_code, data=''):
    return {'code': ret_code, 'message': ERR_CODE[ret_code], 'data': data}

ERROR_CODE字典定义如下:

# coding:utf-8

OP_SUCCESS = 0
THROW_EXP = 1000
OP_DB_FAILED = 1001
CHECK_PARAM_FAILED = 1002
FILE_FORMAT_ERR = 1003
NOT_POST = 1004
NOT_GET = 1005
CAL_FEATURE_ERR = 2001
READ_FEATURE_FAILED = 2002
TRAIN_ERR = 2003
LACK_SAMPLE = 2004

ERR_CODE = {
    0: '操作成功',
    1000: "抛出异常",
    1001: "数据库操作失败",
    1002: "参数检查失败",
    1003: "文件格式有误",
    1004: "非post请求",
    1005: "非get请求",
    2001: "特征计算出错",
    2002: "读取特征数据失败",
    2003: "训练出错",
    2004: "缺少正样本或负样本"
}

其中render_json,用于将返回数据json化,函数定义如下:

def render_json(dictionary={}):
    response = HttpResponse(json.dumps(dictionary), content_type="application/json")
    response['Access-Control-Allow-Origin'] = '*'
    response["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type"
    response["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS"
    return response

由于使用了自定义的json返回,所以DRF的返回,都要作自定义操作。
这样或许增加了一些代码量,但可以在整个应用内实现返回码,消息体的统一。

五,序列化文件

from rest_framework import serializers
from MetisModels.models import Anomaly


class AnomalySerializer(serializers.ModelSerializer):
    class Meta:
        model = Anomaly
        # fields = '__all__'
        fields = ['id', 'attr', 'mark_flag', 'anomaly_time',
                  'data_a', 'data_b', 'data_c',
                  'attr_name', 'view_set_name',
                  'create_user', 'username']
        extra_kwargs = {
            'username': {
                'read_only': True,
            },
            'attr_name': {
                'read_only': True,
            },
            'view_set_name': {
                'read_only': True,
            },
            'create_user': {
                'write_only': True,
            },
        }

在序列化文件,read_only用于提示此字段仅作序列化显示,write_only仅作反序列化写入。

六,分页文件

from rest_framework.pagination import LimitOffsetPagination
from rest_framework.pagination import PageNumberPagination


class PNPagination(PageNumberPagination):
    page_size = 10
    max_page_size = 100
    page_size_query_param = 'pageSize'
    page_query_param = 'currentPage'

    '''
    age_query_param:表示url中的页码参数
    page_size_query_param:表示url中每页数量参数
    page_size:表示每页的默认显示数量
    max_page_size:表示每页最大显示数量,做限制使用,避免突然大量的查询数据,数据库崩溃
    '''


class LOPagination(LimitOffsetPagination):
    default_limit = 10
    max_limit = 100
    limit_query_param = 'pageSize'
    offset_query_param = 'currentPage'

    '''
    default_limit:表示默认每页显示几条数据
    limit_query_param:表示url中本页需要显示数量参数
    offset_query_param:表示从数据库中的第几条数据开始显示参数
    max_limit:表示每页最大显示数量,做限制使用,避免突然大量的查询数据,数据库崩溃
    '''

这里,使用了PageNumber的分页,我们可以传入类似下面的页面参数
/anomaly/list/?pageSize=6¤tPage=1&ordering=-id&mark_flag=all&begin_time=&end_time=&attr=Kafka&view_set=%E4%B8%AD%E9%97%B4
其中的pageSize和currentPage,要和定义的参数对应。

七,filters定义文件


class AnomalyFilter(FilterSet):
    attr = filters.CharFilter(field_name='attr__attr_name', lookup_expr='icontains',)
    view_set = filters.CharFilter(field_name='attr__view_set__view_name', lookup_expr='icontains',)
    begin_time = filters.CharFilter(field_name='anomaly_time', lookup_expr='gte',)
    end_time = filters.DateTimeFilter(field_name='anomaly_time', lookup_expr='lte',)
    mark_flag = filters.CharFilter(field_name='mark_flag', method='mark_flag_filter',)

    def mark_flag_filter(self, queryset, field_name, value):
        print(value)
        if value == 'no':
            return queryset.filter(~Q(mark_flag='negative') & ~Q(mark_flag='positive'))
        elif value == 'yes':
            return queryset.filter(Q(mark_flag='negative') | Q(mark_flag='positive'))
        elif value == 'all':
            return queryset
        else:
            return queryset


    class Meta:
        model = Anomaly
        fields = ['attr', 'view_set', 'begin_time', 'end_time',  'mark_flag']

算是django_filters这个库比较在代表性的实现了。
attr字符串,使用了外键的关联,模糊搜索。
view_set字符串,使用了外键的外键关联,模糊搜索。
begin_time和end_time日期时间,范围的选择。
mark_flag字符串,使用了自定义过滤。其中queryset为已过滤集合,value为取到的field字段的值。Q是django orm的魔法功能,此处用于包含或不包含。

八,前端搜索框定义

查询 重置

v-model作双向绑定

九,data中的pagination定义

pagination: {
                'total': 0,
                'pageSize': 6,
                'currentPage': 1,
                'ordering': '-id',
                'searchKey': {
                    'markFlag': this.$route.meta.label,
                    'viewSet': '',
                    'attr': '',
                    'beginTime': '',
                    'endTime': ''
                },
                onChange: page => {
                    const pager = { ...this.pagination };
                    pager.currentPage = page;
                    this.pagination = pager;
                    this.fetch(this.pagination);
                },
            },

这个定义中,不但包含了分页参数,还包含了搜索过滤的参数。
onChange用于分页技巧。

十,methods中时间选择时触发的变量更新

moment,
        createChange(dates, dateStrings) {
          this.pagination.searchKey.beginTime = dateStrings[0]
            this.pagination.searchKey.endTime = dateStrings[1]
        },

十一,前端向后端请求数据时,参数作一次理顺。

import {ANOMALY_LIST, ANOMALY_UPDATE} from '@/services/api'
import {request, METHOD} from '@/utils/request'

/**
 * 获取所有异常时序
 */
export async function getAnomalyList(data) {
    const pageSize = data['pageSize']
    const currentPage = data['currentPage']
    const ordering = data['ordering']
    const markFlag = data['searchKey']['markFlag']
    const beginTime = data['searchKey']['beginTime']
    const endTime = data['searchKey']['endTime']
    const viewSet = data['searchKey']['viewSet']
    const attr = data['searchKey']['attr']
  return request(ANOMALY_LIST, METHOD.GET, {
        pageSize,
        currentPage,
        ordering, 
        'mark_flag': markFlag,
        'begin_time': beginTime,
        'end_time': endTime,
        'attr': attr,
        'view_set': viewSet,
        
    })
}

这里面,作了axios的封装,token,守卫路由这些都有。

十二,一个请求ajax的demo。

fetch(params={}) {
            this.loading = true;
            getAnomalyList(params).then(resp => {
                let retData = resp.data
                if (retData.code == 0) {
                    this.dataSource = []
                    this.dataAbc = []
                    this.dataGraphAbc = {}
                    const results = retData.data.results
                    for (let i = 0; i < results.length; i++) {
                        const anomalyTime = `${this.$options.filters.secFormat(results[i].anomaly_time)}`
                        const attrName = results[i].attr_name
                        const view_set_name = results[i].view_set_name
                        this.dataSource.push({
                            key: i,
                            id: results[i].id,
                            attrName: attrName,
                            viewSetName: view_set_name,
                            anomalyTime: anomalyTime,
                            markFlag: results[i].mark_flag,
                            titleSample: `[${view_set_name}-${attrName}]:${anomalyTime}`,
                            dataGraphAbc: dataSeries(anomalyTime, 
                                                                results[i].data_a, 
                                                                results[i].data_b, 
                                                                results[i].data_c),
                            createDate: results[i].create_date,
                            createUser: results[i].username
                        })
                        this.dataAbc.push(dataSeries(anomalyTime, 
                                                                results[i].data_a, 
                                                                results[i].data_b, 
                                                                results[i].data_c))
                        this.titleSamples.push(`[${view_set_name}-${attrName}]:${anomalyTime}`)
                    }
                    const pager = { ...this.pagination };
                    // Read total count from server
                    pager.total = retData.data.count;
                    this.pagination = pager;
                    this.loading = false;
                } else {
                    this.loading = false;
                    this.$message.error(createRes.message, 3)
                }
            })
        },

这里的params={}参数,就是传的pagination变量。

十三,完工。

你可能感兴趣的:(前端使用ant design vue,后端使用DRF结合django_filters,完成分页,过滤搜索功能)