CMDB-表格组件

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6     <link rel="stylesheet" href="/static/plugins/bootstrap/css/bootstrap.min.css">
 7 </head>
 8 <body>
 9     <div style="margin: 0 auto;width: 900px;position:relative;">
10         <h1>资产列表</h1>
11         <div class="row" style="position:absolute;left: 0;">
12             <form class="navbar-form navbar-left" role="search" style="padding: 0;">
13                 <div class="bs-example " style="float: left;" >
14                     <button id="idCheckAll" type="button"  class="btn btn-default">全选</button>
15                     <button id="idReversAll" type="button" class="btn btn-default">反选</button>
16                     <button id="idCancelAll" type="button" class="btn btn-default">取消</button>
17                     <button id="idEdit" type="button" class="btn btn-default">批量编辑</button>
18                     <button id="idDelete" type="button" class="btn btn-default">批量删除</button>
19                     <button id="idSave" type="button" class="btn btn-default">保存</button>
20                 </div>
21 
22                 <div style="float: right;position:absolute;right: 0">
23                     <input type="text" class="form-control" placeholder="Search">
24                     <button type="submit" class="btn btn-default">搜索</button>
25                 </div>
26             </form>
27             <table class="table table-bordered">
28                 <thead id="table_th"></thead>
29                 <tbody id="table_td"></tbody>
30             </table>
31             <nav aria-label="Page navigation">
32                 <ul id="idPagination" class="pagination">
33 
34                 </ul>
35             </nav>
36         </div>
37     </div>
38     <script src="/static/js/jquery-3.1.1.js"></script>
39     <script src="/static/js/NBlist.js"></script>
40     <script>
41         $(function () {
42             $.NB("{% url 'datajson' %}");
43             $.ChangePager();
44         })
45     </script>
46 </body>
47 
48 </html>
html代码
  1 /**
  2  * Created by Administrator on 2018-12-02.
  3  */
  4 (function () {
  5     /*进入编辑模式*/
  6     function IntoEdit($currentr) {
  7         $currentr.addClass('success');
  8         $currentr.attr('isedit','true');
  9         //循环tr得每个td属性
 10         $currentr.children().each(function () {
 11             var EditEnable = $(this).attr('edit-enable');
 12             var EditType = $(this).attr('edit-type');
 13             //可以编辑
 14             if(EditEnable == 'true'){
 15                 if(EditType == 'select'){
 16                     //下拉框
 17                     var globalName = $(this).attr('global-name');
 18                     var old_val = $(this).attr('old_val'); //数据库id
 19                     //生成select标签
 20                     var sel = document.createElement('select');
 21                     $.each(window[globalName],function (k1,v1) {
 22                         var op = document.createElement('option');
 23                         op.setAttribute('value',v1[0]);
 24                         op.innerHTML = v1[1];
 25                         $(sel).append(op);
 26                     });
 27                     sel.className = "form-control";
 28                     //select 默认值
 29                     $(sel).val(old_val);
 30                     $(this).html(sel);
 31 
 32                     //文本框
 33                 }else if(EditType == 'input'){
 34                     // 获取文本值
 35                     var innerText = $(this).text();
 36                     //创建input
 37                     var tag = document.createElement('input');
 38                     //为input赋值
 39                     tag.className = "form-control";
 40                     tag.value = innerText;
 41                     $(this).html(tag);
 42                 }
 43             }
 44         })
 45     }
 46     /*退出编辑模式*/
 47     function OutEdit($currentr) {
 48         $currentr.removeClass('success');
 49         $currentr.children().each(function () {
 50             var EditEnable = $(this).attr('edit-enable');
 51             var editType = $(this).attr('edit-type');
 52             if(EditEnable == 'true'){
 53                 if (editType == 'select'){
 54                     //获取正在编辑的selected对象
 55                     var $select = $(this).children().first();
 56                     //获取选中的option的value
 57                     var newId = $select.val();
 58                     //获取选中的option文本内容
 59                     var newText = $select[0].selectedOptions[0].innerHTML;
 60                     //在td中设置文本内容
 61                     $(this).html(newText);
 62                     //将新值添加到属性中
 63                     $(this).attr('new-val',newId);
 64 
 65                 }else if(editType == 'input'){
 66                     //退出编辑模式
 67                     var inputvalue = $(this).children().first().val();
 68                     //在td中设置文本内容
 69                     $(this).html(inputvalue);
 70                     //将新值添加到属性中
 71                     $(this).attr('new-val',inputvalue);
 72                 }
 73             }
 74         })
 75     }
 76     /*编辑模式*/
 77     function bindEdit() {
 78         $('#idEdit').click(function () {
 79             var editing =$('#idEdit').hasClass("btn-warning");
 80             if (editing){
 81                 //退出编辑
 82                 $('#idEdit').removeClass("btn-warning");
 83                 $('#idEdit').text('批量编辑');
 84                 $('#table_td').find(':checked').each(function () {
 85                     var $currentr = $(this).parent().parent();
 86                     OutEdit($currentr);
 87                 })
 88             }else {
 89                 //进入编辑
 90                 $('#idEdit').addClass("btn-warning");
 91                 $('#idEdit').text('退出编辑');
 92                 $('#table_td').find(':checked').each(function () {
 93                     var $currentr = $(this).parent().parent();
 94                     IntoEdit($currentr);
 95                 })
 96             }
 97         })
 98     }
 99     /*绑定复选框*/
100     function bindCheckBox() {
101         $('#table_td').on('click',':checkbox',function () {
102             var isEdit = $('#idEdit').hasClass('btn-warning');
103             if (isEdit){
104                 var ck = $(this).prop('checked');
105                 var $currentr = $(this).parent().parent();
106                 if(ck){
107                     //  进入编辑
108                     IntoEdit($currentr);
109                 }else {
110                     //退出编辑
111                     OutEdit($currentr);
112                 }
113             }else {
114 
115             }
116         })
117     }
118     /*全选*/
119     function IsCheckAll() {
120         $('#idCheckAll').click(function () {
121             $('#table_td').find(':checkbox').each(function () {
122                 if($('#idEdit').hasClass('btn-warning')){
123                     if($(this).prop('checked')){
124                         //已经进入编辑模式了
125                     }else {
126                         //进入编辑模式
127                         var $currentr = $(this).parent().parent();
128                         IntoEdit($currentr);
129                         $(this).prop('checked',true);
130                     }
131                 }else {
132                     $(this).prop('checked',true);
133                 }
134             })
135         })
136     }
137     /*取消*/
138     function IsCancelAll() {
139         $('#idCancelAll').click(function () {
140             $('#table_td').find(':checked').each(function () {
141                 if($('#idEdit').hasClass('btn-warning')){
142                     $(this).prop('checked',false);
143                     //退出编辑模式
144                     var $currentr = $(this).parent().parent();
145                     OutEdit($currentr);
146                 }else {
147                     $(this).prop('checked',false);
148                 }
149             })
150         })
151     }
152     /*反选*/
153     function IsReversAll() {
154         $('#idReversAll').click(function () {
155             $('#table_td').find(':checkbox').each(function () {
156                 if($('#idEdit').hasClass('btn-warning')){
157                     if($(this).prop('checked')){
158                         $(this).prop('checked',false);
159                         var $currentr = $(this).parent().parent();
160                         OutEdit($currentr);
161                     }else {
162                         $(this).prop('checked',true);
163                         var $currentr = $(this).parent().parent();
164                         IntoEdit($currentr);
165                     }
166                 }else {
167                     if($(this).prop('checked')){
168                         $(this).prop('checked',false);
169                     }else {
170                         $(this).prop('checked',true);
171                     }
172                 }
173             })
174         })
175     }
176     /*保存*/
177     function bindSave() {
178         $('#idSave').click(function () {
179             var postList = [];
180             //找到编辑过得tr 属性isedit="true"
181             $('#table_td').find('tr[isedit="true"]').each(function () {
182                 var temp = {};
183                 var row_id = $(this).attr('row-id'); //数据库对应id
184                 temp['id'] = row_id;
185                 $(this).children('[edit-enable="true"]').each(function () {
186                     var name = $(this).attr('name');
187                     var old_val = $(this).attr('old_val');
188                     var new_val = $(this).attr('new-val');
189                     if(old_val != new_val) {
190                         temp[name] = new_val;
191                     }
192                 });
193                 postList.push(temp);
194             });
195 
196             $.ajax({
197                 url:DataJsonUrl,
198                 type:'POST',
199                 data:{'type':'update','post_list':JSON.stringify(postList),csrfmiddlewaretoken: '{{ csrf_token }}'},
200                 dataType:'JSON',
201                 success:function (arg) {
202                     if(arg.status){
203                         init(1);
204                         console.log(arg.message);
205                     }else {
206                         console.log(arg.message);
207                     }
208                 }
209             })
210         })
211     }
212     /*删除*/
213     function bindDel() {
214         // $('#idDelete').click(function () {
215         //     $('#table_td').find(':checked').each(function () {
216         //         //var row_id = $(this).attr('row-id'); //数据库对应id
217         //         var row_id = $(this).parent().parent().attr("row-id");
218         //
219         //         $.ajax({
220         //             url:DataJsonUrl,
221         //             type:'POST',
222         //             data:{'type':'delete','post_list':row_id,csrfmiddlewaretoken: '{{ csrf_token }}'},
223         //             dataType:'JSON',
224         //             success:function (arg) {
225         //                 if(arg.status){
226         //                     init();
227         //                     console.log(arg.message);
228         //                 }else {
229         //                     console.log(arg.message);
230         //                 }
231         //             }
232         //         })
233         //     })
234         // })
235 
236     }
237 
238 
239     /*自定义格式化方法*/
240     String.prototype.format = function (kwargs) {
241         //this "ceshiceshi:{age}-{gender}"
242         //kwargs {'age':18,'gender':'女'}
243 
244         var ret = this.replace(/\{(\w+)\}/g,function (km,m) {
245             return kwargs[m]
246         });
247         return ret
248     };
249     /*初始化*/
250     function init(pager) {
251         $.ajax({
252             url:DataJsonUrl,
253             type:'GET',
254             data:{'pager':pager},
255             dataType:'JSON',
256             success:function (result) {
257                 initGlobalData(result.global_dict);
258                 initHeader(result.table_config);
259                 initBody(result.table_config,result.data_list);
260                 initPager(result.pager);
261             }
262         })
263     }
264     /*生成表头*/
265     function initHeader(table_config) {
266         /*创建tr标签*/
267        var tr = document.createElement('tr');
268         /*创建tr下th标签并取值*/
269        $.each(table_config,function (k,item) {
270            if (item.display){
271                var th = document.createElement('th');
272                th.innerHTML = item.title;
273                $(tr).append(th);
274            }
275        });
276        $('#table_th').empty();
277        $('#table_th').append(tr);
278     }
279     /*生成数据*/
280     function initBody(table_config,data_list) {
281        $('#table_td').empty();
282        $.each(data_list,function (k,row) {
283            /*创建tr标签*/
284            var tr = document.createElement('tr');
285            tr.setAttribute('row-id',row['id']);
286                 /*根据配置文件显示字段*/
287                 $.each(table_config,function (i,colConfig) {
288                     if(colConfig.display){
289                         var td = document.createElement('td');
290                         //对colConfig.text.kwargs去@重写
291                         //生成文本信息
292                         var kwargs = {};
293                         $.each(colConfig.text.kwargs,function (key,value) {
294                             if(value.substring(0,2) == '@@'){
295                                 var global_name = value.substring(2,value.length); //全局变量名称
296                                 var global_id = row[colConfig.q]; //获取数据库数字类型的值
297                                 var temp_id = GetTextFromGlobalById(global_name,global_id);
298                                 kwargs[key] = temp_id
299                             }
300                             else if(value[0]=='@'){
301                                 kwargs[key] = row[value.substring(1,value.length)]; //数据库里的字段.使用row[]
302                             }else {
303                                 kwargs[key] = value; //配置文件默认值
304                             }
305                         });
306                         var temp = colConfig.text.content.format(kwargs);
307                         td.innerHTML = temp;
308                         //设置属性colConfig.attr
309                         $.each(colConfig.attr,function (kk,vv) {
310                             //处理attr里得origin带@得数值
311                             if(vv[0]=='@'){
312                                 td.setAttribute(kk,row[vv.substring(1,vv.length)]);
313                             }else {
314                                 td.setAttribute(kk,vv);
315                             }
316                         });
317 
318                         $(tr).append(td)
319                     }
320                 });
321            $('#table_td').append(tr);
322        });
323     }
324     /*设置全局变量*/
325     function initGlobalData(global_dict) {
326         $.each(global_dict,function (k,v) {
327             window[k] = v;  //设置为全局变量
328         })
329     }
330     /*设置数据库内存数据*/
331     function GetTextFromGlobalById(global_name,global_id) {
332         var ret =null;
333         $.each(window[global_name],function (k,item) {
334             if(item[0] == global_id){
335                 ret = item[1];
336                 return
337             }
338         });
339         return ret;
340     }
341     /*页码改变*/
342     function bindChangePager() {
343         $('#idPagination').on('click','a',function () {
344             var num = $(this).attr('pnum');
345             init(num);
346         })
347     }
348     /*分页*/
349     function initPager(pager) {
350         $('#idPagination').html(pager);
351     }
352 
353     /*初始加载数据*/
354         jQuery.extend({
355             'NB': function (url) {
356                 DataJsonUrl = url;
357                 init();
358                 bindEdit();
359                 bindCheckBox();
360                 IsCheckAll();
361                 IsCancelAll();
362                 IsReversAll();
363                 bindSave();
364                 bindDel();
365                 bindChangePager();
366             },
367             'ChangePager':function (num) {
368                 init(num);
369             }
370         })
371 })();
前端js

后端代码:

1 from django.urls import path
2 from webdemo import views
3 urlpatterns = [
4     path('', views.index),
5     path('datajson/', views.datajson,name='datajson'),
6 
7 ]
urls
  1 from django.shortcuts import render,HttpResponse
  2 from DB import models
  3 import json
  4 from unitpub.pager_Class import Pagination
  5 # Create your views here.
  6 
  7 
  8 def index(req):
  9     """
 10     数据库获取数据
 11     :param req:
 12     :return:
 13     """
 14     return render(req,'tabledemo.html')
 15 
 16 
 17 def datajson(req):
 18     """
 19     从数据库获取数据
 20     :param req:
 21     :return:
 22     """
 23     if req.method == 'POST':
 24         ret = {'status': False, 'message': '数据没有任何变化'}
 25         data_list = req.POST.get('post_list')
 26         data_type = req.POST.get('type')
 27         if data_type == 'update':
 28             for data in json.loads(data_list):
 29                 if len(data) > 1:
 30                     # print(data)
 31                     isupdate = models.Asset.objects.filter(id=data['id']).update(**data)
 32                     if isupdate:
 33                         ret['status'] = True
 34                         ret['message'] = '更新成功'
 35             return HttpResponse(json.dumps(ret))
 36         elif data_type == 'delete':
 37             isdelete = models.Asset.objects.filter(id=data_list).delete()
 38             if isdelete:
 39                 ret['status'] = True
 40                 ret['message'] = '删除数据'
 41             return HttpResponse(json.dumps(ret))
 42         else:
 43             return HttpResponse(json.dumps(ret))
 44     else:
 45         # q:数据库字段
 46         # title:显示表头
 47         # display:是否显示
 48         # text:{'content':"{m}-{n}",'kwargs':{'m':'某机房','n':'@cabinet_order'}} 字符串格式化带@符号取数据库否则固定值
 49         # @@代表取数据库内存值
 50         """
 51         'attr':{
 52                     'name': 'idc_id',   更新时更新数据库什么字段
 53                     'old_val': '@idc__id',  为了让编辑时select得value值与数据库id一致
 54                     'edit-enable': 'true', 是否编辑
 55                     'edit-type': 'select', 前端类型
 56                     'global-name':'idc_choices'  全局变量名字
 57                 }
 58         """
 59         table_config=[
 60             # 配置选项
 61             {
 62                 'q': None,
 63                 'title': "选项",
 64                 'display': True,
 65                 'text': {'content': "<input type='checkbox'/>", 'kwargs': {}},
 66                 'attrs': {},
 67             },
 68             # 配置id
 69             {
 70                 'q': 'id',
 71                 'title':'ID',
 72                 'display':False,
 73                 'text':{},
 74                 'attr':{}
 75             },
 76             # 配置类型
 77             {
 78                 'q': 'device_type_id',
 79                 'title': '资产类型',
 80                 'display': True,
 81                 'text': {'content':"{n}",'kwargs':{'n':'@@device_type_choices'}},
 82                 'attr':{
 83                     'name':'device_type_id',
 84                     'old_val': '@device_type_id',
 85                     'edit-enable': 'true',
 86                     'edit-type': 'select',
 87                     'global-name': 'device_type_choices'
 88                 }
 89             },
 90             # 配置类型
 91             {
 92                 'q': 'device_status_id',
 93                 'title': '状态',
 94                 'display': True,
 95                 'text': {'content': "{n}", 'kwargs': {'n': '@@device_status_choices'}},
 96                 'attr':{
 97                     'name':'device_status_id',
 98                     'old_val': '@device_status_id',
 99                     'edit-enable': 'true',
100                     'edit-type': 'select',
101                     'global-name': 'device_status_choices'
102                 }
103             },
104             # 配置数据:ForeignKey 为了select框单独取id才能有默认值
105             {
106                 'q': 'idc__id',
107                 'title': 'IDC',
108                 'display': False,
109                 'text': {},
110                 'attr': {}
111             },
112             # 配置数据:ForeignKey
113             {
114                 'q': 'idc__name',
115                 'title': 'IDC',
116                 'display': True,
117                 'text': {'content': "{n}", 'kwargs': {'n': '@idc__name'}},
118                 'attr':{
119                     'name': 'idc_id', # 注意是单下划线
120                     'old_val': '@idc__id',
121                     'edit-enable': 'true',
122                     'edit-type': 'select',
123                     'global-name':'idc_choices'
124                 }
125             },
126             # 配置数据
127             {
128                 'q': 'cabinet_num',
129                 'title': '机柜号',
130                 'display': True,
131                 'text': {'content':"{n}",'kwargs':{'n':'@cabinet_num'}},
132                 'attr':{
133                     'name': 'cabinet_num',
134                     'old_val': '@cabinet_num',
135                     'edit-enable': 'true',
136                     'edit-type': 'input'
137                 }
138             },
139             # 配置数据
140             {
141                 'q': 'cabinet_order',
142                 'title': '机柜位置',
143                 'display': True,
144                 'text': {'content':"{m}-{n}",'kwargs':{'m':'某机房','n':'@cabinet_order'}},
145                 'attr':{
146                     'name': 'cabinet_order',
147                     'old_val': '@cabinet_order',
148                     'edit_enable':'true',
149                     'edit_type':'input',
150                 }
151             },
152             # 配置操作
153             {
154                 'q': None,
155                 'title': '操作',
156                 'display': True,
157                 'text': {'content': "<a href='/data_show-{x}/'>{n}</a> <a href='/data_show-{x}/'>{m}</a>", 'kwargs': {'n': '详情','m': '编辑', 'x':'@id'}},
158                 'attr': {}
159             }
160         ]
161 
162         q_list = []
163         for i in table_config:
164             if not i['q']:
165                 continue
166             q_list.append(i['q'])
167 
168         # 数据库取哪几列数据
169         data_list = models.Asset.objects.all().values(*q_list)
170         data_list = list(data_list)
171 
172         # 分页
173         data_count = models.Asset.objects.count()
174         current_page = req.GET.get('pager')
175         page_obj = Pagination(data_count, current_page)
176         data_list = data_list[page_obj.start():page_obj.end()]
177 
178         result = {
179             'table_config':table_config,  # 配置文件
180             'data_list':data_list, # 构造数据
181             'global_dict':{
182                 'device_type_choices': models.Asset.device_type_choices,  # 内存中的类型
183                 'device_status_choices': models.Asset.device_status_choices,  # 内存中的类型
184                 'idc_choices': list(models.IDC.objects.values_list('id', 'name')),
185             },
186             'pager': page_obj.page_str(),
187         }
188 
189         return HttpResponse(json.dumps(result))
Views
 1 '''
 2 TotalNumber:总个数
 3 CurrentPage:当前页【1/2/3/4/5/6/7】
 4 barsNumber:每页显示多少条【10条数据】
 5 MaxPageNumber:最大显示多少个【1,2,3,4,5,6,7】
 6 '''
 7 
 8 class Pagination(object):
 9     def __init__(self,TotalNumber,CurrentPage,barsNumber=5,MaxPageNumber=7):
10         self.Total_number=TotalNumber
11         try:
12             v = int(CurrentPage)
13             if v<=0:
14                 v=1
15             self.Current_page=v
16         except Exception as e:
17             self.Current_page = 1
18         self.bars_number=barsNumber
19         self.Max_pageNumber=MaxPageNumber
20     # 起始数据
21     def start(self):
22         return (self.Current_page - 1) * self.bars_number
23     # 结束数据
24     def end(self):
25         return self.Current_page * self.bars_number
26 
27     # 总页数
28     @property
29     def num_pages(self):
30         '''
31         总页数
32         :return:
33         '''
34         a,b = divmod(self.Total_number,self.bars_number)
35         if b==0:
36             return a
37         else:
38             return a+1
39 
40     # 显示页码数
41     def page_num_range(self):
42         #总页数小于显示页码
43         if self.num_pages < self.Max_pageNumber:
44             return range(1,self.num_pages+1)
45         #总页数很多
46         part = int(self.Max_pageNumber/2)
47         if self.Current_page <= part:
48             return range(1,self.Max_pageNumber+1)
49         if (self.Current_page+part) > self.num_pages:
50             return range(self.num_pages-self.Max_pageNumber,self.num_pages+1)
51         return range(self.Current_page-part,self.Current_page+part+1)
52 
53     # 返回页码给前端
54     def page_str(self):
55         page_list=[]
56         # 首页
57         first="<li><a pnum='1'>首页</a></li>"
58         page_list.append(first)
59         # 上一页
60         if self.Current_page ==1 :
61             prev="<li><a href='javascript:void(0)'>上一页</a></li>"
62         else:
63             prev="<li><a pnum='%d'>上一页</a></li>" % (self.Current_page-1)
64         page_list.append(prev)
65         # 页码
66         for i in self.page_num_range():
67             if i == self.Current_page:
68                 temp = "<li pnum='%d' class='active'><a>%s</a></li>" % (i,i)
69             else:
70                 temp="<li><a pnum='%d'>%s</a></li>" % (i,i)
71             page_list.append(temp)
72         # 下一页
73         if self.Current_page == self.num_pages :
74             next_page="<li><a href='javascript:void(0)'>下一页</a></li>"
75         else:
76             next_page="<li><a pnum='%d'>下一页</a></li>" % (self.Current_page+1)
77         page_list.append(next_page)
78         # # 尾页
79         end_page = "<li><a pnum='%d'>尾页</a></li>" % self.num_pages
80         page_list.append(end_page)
81         # 页码概要
82         end_html = "<li><a href='javascript:void(0)' >共 %d页 / %d 条数据</a></li>" % (self.num_pages, self.Total_number,)
83         page_list.append(end_html)
84         return ''.join(page_list)
Pagination分页组件

数据库:

 1 class Asset(models.Model):
 2     """
 3     资产信息表,所有资产公共信息(交换机,服务器,防火墙等)
 4     """
 5     device_type_choices = (
 6         (1, '服务器'),
 7         (2, '交换机'),
 8         (3, '防火墙'),
 9     )
10     device_status_choices = (
11         (1, '上架'),
12         (2, '在线'),
13         (3, '离线'),
14         (4, '下架'),
15     )
16 
17     device_type_id = models.IntegerField(choices=device_type_choices, default=1)
18     device_status_id = models.IntegerField(choices=device_status_choices, default=1)
19 
20     cabinet_num = models.CharField('机柜号', max_length=30, null=True, blank=True)
21     cabinet_order = models.CharField('机柜中序号', max_length=30, null=True, blank=True)
22 
23     idc = models.ForeignKey('IDC', verbose_name='IDC机房', null=True, blank=True,on_delete=models.CASCADE)
24     business_unit = models.ForeignKey('BusinessUnit', verbose_name='属于的业务线', null=True, blank=True,on_delete=models.CASCADE)
25 
26     tag = models.ManyToManyField('Tag')
27 
28     latest_date = models.DateField(null=True)
29     create_at = models.DateTimeField(auto_now_add=True)
30 
31     class Meta:
32         verbose_name_plural = "资产表"
33 
34     def __str__(self):
35         return "%s-%s-%s" % (self.idc.name, self.cabinet_num, self.cabinet_order)
models

 

posted @ 2018-12-02 18:32  我在地球凑人数的日子  阅读(168)  评论(0)    收藏  举报