Django:CRM - 相关功能分解(1)
客户列表的展示(公和私)
本项目中的后台页面是基于后台模板的,因此,需要就是 母版
上面和侧边栏基于母版样式,其中内容区域 和 js 部分添加了 block盒子
<!DOCTYPE html> <!-- This is a starter template page. Use this page to start your new project from scratch. This page gets rid of all links and provides the needed markup only. --> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>CRM</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> <!-- Bootstrap 3.3.6 --> <link rel="stylesheet" href="/static/AdminLTE-2.3.3/bootstrap/css/bootstrap.min.css"> <!-- Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css"> <!-- Ionicons --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.min.css"> <!-- Theme style --> <link rel="stylesheet" href="/static/AdminLTE-2.3.3/dist/css/AdminLTE.min.css"> <!-- AdminLTE Skins. We have chosen the skin-blue for this starter page. However, you can choose any other skin. Make sure you apply the skin class to the body tag so the changes take effect. --> <link rel="stylesheet" href="/static/AdminLTE-2.3.3/dist/css/skins/skin-blue.min.css"> <link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.css"> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> </head> <!-- BODY TAG OPTIONS: ================= Apply one or more of the following classes to get the desired effect |---------------------------------------------------------| | SKINS | skin-blue | | | skin-black | | | skin-purple | | | skin-yellow | | | skin-red | | | skin-green | |---------------------------------------------------------| |LAYOUT OPTIONS | fixed | | | layout-boxed | | | layout-top-nav | | | sidebar-collapse | | | sidebar-mini | |---------------------------------------------------------| --> <body class="hold-transition skin-blue sidebar-mini"> <div class="wrapper"> <!-- Main Header --> <header class="main-header"> <!-- Logo --> <a href="/static/AdminLTE-2.3.3/index2.html" class="logo"> <!-- logo for regular state and mobile devices --> <span class="logo-lg"><b>CRM</b></span> </a> <!-- Header Navbar --> <nav class="navbar navbar-static-top" role="navigation"> <!-- Sidebar toggle button--> <a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button"> <span class="sr-only">Toggle navigation</span> </a> <!-- Navbar Right Menu --> <div class="navbar-custom-menu"> <ul class="nav navbar-nav"> <!-- Messages: style can be found in dropdown.less--> <li class="dropdown messages-menu"> <!-- Menu toggle button --> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> <i class="fa fa-envelope-o"></i> <span class="label label-success">4</span> </a> <ul class="dropdown-menu"> <li class="header">You have 4 messages</li> <li> <!-- inner menu: contains the messages --> <ul class="menu"> <li><!-- start message --> <a href="#"> <div class="pull-left"> <!-- User Image --> <img src="/static/AdminLTE-2.3.3/dist/img/user2-160x160.jpg" class="img-circle" alt="User Image"> </div> <!-- Message title and timestamp --> <h4> Support Team <small><i class="fa fa-clock-o"></i> 5 mins</small> </h4> <!-- The message --> <p>Why not buy a new awesome theme?</p> </a> </li> <!-- end message --> </ul> <!-- /.menu --> </li> <li class="footer"><a href="#">See All Messages</a></li> </ul> </li> <!-- /.messages-menu --> <!-- Notifications Menu --> <li class="dropdown notifications-menu"> <!-- Menu toggle button --> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> <i class="fa fa-bell-o"></i> <span class="label label-warning">10</span> </a> <ul class="dropdown-menu"> <li class="header">You have 10 notifications</li> <li> <!-- Inner Menu: contains the notifications --> <ul class="menu"> <li><!-- start notification --> <a href="#"> <i class="fa fa-users text-aqua"></i> 5 new members joined today </a> </li> <!-- end notification --> </ul> </li> <li class="footer"><a href="#">View all</a></li> </ul> </li> <!-- Tasks Menu --> <li class="dropdown tasks-menu"> <!-- Menu Toggle Button --> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> <i class="fa fa-flag-o"></i> <span class="label label-danger">9</span> </a> <ul class="dropdown-menu"> <li class="header">You have 9 tasks</li> <li> <!-- Inner menu: contains the tasks --> <ul class="menu"> <li><!-- Task item --> <a href="#"> <!-- Task title and progress text --> <h3> Design some buttons <small class="pull-right">20%</small> </h3> <!-- The progress bar --> <div class="progress xs"> <!-- Change the css width attribute to simulate progress --> <div class="progress-bar progress-bar-aqua" style="width: 20%" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"> <span class="sr-only">20% Complete</span> </div> </div> </a> </li> <!-- end task item --> </ul> </li> <li class="footer"> <a href="#">View all tasks</a> </li> </ul> </li> <!-- User Account Menu --> <li class="dropdown user user-menu"> <!-- Menu Toggle Button --> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> <!-- The user image in the navbar--> <img src="/static/AdminLTE-2.3.3/dist/img/user2-160x160.jpg" class="user-image" alt="User Image"> <!-- hidden-xs hides the username on small devices so only the image appears. --> <span class="hidden-xs">Alexander Pierce</span> </a> <ul class="dropdown-menu"> <!-- The user image in the menu --> <li class="user-header"> <img src="/static/AdminLTE-2.3.3/dist/img/user2-160x160.jpg" class="img-circle" alt="User Image"> <p> Alexander Pierce - Web Developer <small>Member since Nov. 2012</small> </p> </li> <!-- Menu Body --> <li class="user-body"> <div class="row"> <div class="col-xs-4 text-center"> <a href="#">Followers</a> </div> <div class="col-xs-4 text-center"> <a href="#">Sales</a> </div> <div class="col-xs-4 text-center"> <a href="#">Friends</a> </div> </div> <!-- /.row --> </li> <!-- Menu Footer--> <li class="user-footer"> <div class="pull-left"> <a href="#" class="btn btn-default btn-flat">个人中心</a> </div> <div class="pull-right"> <a href="/logout/" class="btn btn-default btn-flat">注销</a> </div> </li> </ul> </li> <!-- Control Sidebar Toggle Button --> <li> <a href="#" data-toggle="control-sidebar"><i class="fa fa-gears"></i></a> </li> </ul> </div> </nav> </header> <!-- Left side column. contains the logo and sidebar --> <aside class="main-sidebar"> <!-- sidebar: style can be found in sidebar.less --> <section class="sidebar"> <!-- Sidebar user panel (optional) --> <div class="user-panel"> <div class="pull-left image"> <img src="/static/AdminLTE-2.3.3/dist/img/user2-160x160.jpg" class="img-circle" alt="User Image"> </div> <div class="pull-left info"> <p>Alexander Pierce</p> <!-- Status --> <a href="#"><i class="fa fa-circle text-success"></i> Online</a> </div> </div> <!-- search form (Optional) --> <form action="#" method="get" class="sidebar-form"> <div class="input-group"> <input type="text" name="q" class="form-control" placeholder="Search..."> <span class="input-group-btn"> <button type="submit" name="search" id="search-btn" class="btn btn-flat"><i class="fa fa-search"></i> </button> </span> </div> </form> <!-- /.search form --> <!-- Sidebar Menu --> <ul class="sidebar-menu"> <li class="header">HEADER</li> <!-- Optionally, you can add icons to the links --> <li class="active"><a href="{% url "customers_list" %}"><i class="fa fa-link"></i> <span>客户信息</span></a></li> <li class="active"><a href="{% url "mycustomer" %}"><i class="fa fa-link"></i> <span>我的客户</span></a></li> <li class="active"><a href="{% url "consultrecord_list" %}"><i class="fa fa-link"></i> <span>客户跟进情况</span></a></li> <li class="treeview"> <a href="#"><i class="fa fa-link"></i> <span>Multilevel</span> <i class="fa fa-angle-left pull-right"></i></a> <ul class="treeview-menu"> <li><a href="#">Link in level 2</a></li> <li><a href="#">Link in level 2</a></li> </ul> </li> </ul> <!-- /.sidebar-menu --> </section> <!-- /.sidebar --> </aside> <!-- Content Wrapper. Contains page content --> {% block content %} <div class="content-wrapper"> <!-- Content Header (Page header) --> <section class="content-header"> <h1> INDEX <small>Optional description</small> </h1> <ol class="breadcrumb"> <li><a href="#"><i class="fa fa-dashboard"></i> Level</a></li> <li class="active">Here</li> </ol> </section> <!-- Main content --> <section class="content"> <!-- Your Page Content Here --> </section> <!-- /.content --> </div> {% endblock %} <!-- /.content-wrapper --> <!-- Main Footer --> <footer class="main-footer"> <!-- To the right --> <div class="pull-right hidden-xs"> Anything you want </div> <!-- Default to the left --> <strong>Copyright © 2015 <a href="#">Company</a>.</strong> All rights reserved. </footer> <!-- Control Sidebar --> <aside class="control-sidebar control-sidebar-dark"> <!-- Create the tabs --> <ul class="nav nav-tabs nav-justified control-sidebar-tabs"> <li class="active"><a href="#control-sidebar-home-tab" data-toggle="tab"><i class="fa fa-home"></i></a></li> <li><a href="#control-sidebar-settings-tab" data-toggle="tab"><i class="fa fa-gears"></i></a></li> </ul> <!-- Tab panes --> <div class="tab-content"> <!-- Home tab content --> <div class="tab-pane active" id="control-sidebar-home-tab"> <h3 class="control-sidebar-heading">Recent Activity</h3> <ul class="control-sidebar-menu"> <li> <a href=""> <i class="menu-icon fa fa-birthday-cake bg-red"></i> <div class="menu-info"> <h4 class="control-sidebar-subheading">Langdon's Birthday</h4> <p>Will be 23 on April 24th</p> </div> </a> </li> </ul> <!-- /.control-sidebar-menu --> <h3 class="control-sidebar-heading">Tasks Progress</h3> <ul class="control-sidebar-menu"> <li> <a href=""> <h4 class="control-sidebar-subheading"> Custom Template Design <span class="label label-danger pull-right">70%</span> </h4> <div class="progress progress-xxs"> <div class="progress-bar progress-bar-danger" style="width: 70%"></div> </div> </a> </li> </ul> <!-- /.control-sidebar-menu --> </div> <!-- /.tab-pane --> <!-- Stats tab content --> <div class="tab-pane" id="control-sidebar-stats-tab">Stats Tab Content</div> <!-- /.tab-pane --> <!-- Settings tab content --> <div class="tab-pane" id="control-sidebar-settings-tab"> <form method="post"> <h3 class="control-sidebar-heading">General Settings</h3> <div class="form-group"> <label class="control-sidebar-subheading"> Report panel usage <input type="checkbox" class="pull-right" checked> </label> <p> Some information about this general settings option </p> </div> <!-- /.form-group --> </form> </div> <!-- /.tab-pane --> </div> </aside> <!-- /.control-sidebar --> <!-- Add the sidebar's background. This div must be placed immediately after the control sidebar --> <div class="control-sidebar-bg"></div> </div> <!-- ./wrapper --> <!-- REQUIRED JS SCRIPTS --> <!-- jQuery 2.2.0 --> <script src="/static/AdminLTE-2.3.3/plugins/jQuery/jQuery-2.2.0.min.js"></script> <!-- Bootstrap 3.3.6 --> <script src="/static/AdminLTE-2.3.3/bootstrap/js/bootstrap.min.js"></script> <!-- AdminLTE App --> <script src="/static/AdminLTE-2.3.3/dist/js/app.min.js"></script> <!-- Optionally, you can add Slimscroll and FastClick plugins. Both of these plugins are recommended to enhance the user experience. Slimscroll is required when using the fixed layout. --> {% block js %} {% endblock %} </body> </html>
公私客户列表展示具体代码
公户是没有销售人员
私户是有自己的销售人员
包含了模糊查询和批量处理等小功能
# urls.py
path("customers/list/", views.CustomerView.as_view(),name="customers_list"), path("mycustomer/",views.CustomerView.as_view(),name="mycustomer"), path("customer/add/",views.AddEditCustomerView.as_view(),name="customer_add"), re_path("customer/edit/(\d+)/",views.AddEditCustomerView.as_view(),name="customer_edit"), re_path("customer/delete/(\d+)/",views.CustomerDeleteView.as_view(),name="customer_delete"),
# views.py
class CustomerView(View): @method_decorator(login_require) def get(self,request): if reverse("customers_list") == request.path: # 根据url做分支,求不同的customer_list customer_list = Customer.objects.filter(consultant__isnull=True).order_by("-pk") # 判断字段(销售)是否为空 else: customer_list = Customer.objects.filter(consultant=request.user).order_by("-pk") # 过滤私户 # 模糊查询 field = request.GET.get("field") val = request.GET.get('val') if val: # 必须判断 q = Q() q.children.append((field + "__contains", val)) customer_list = customer_list.filter(q).order_by("-pk") # 分页展示(带有保存搜索路径) current_page_num = request.GET.get("page") count = customer_list.count() pagination = Pagination(current_page_num, count, request) customer_list = customer_list[pagination.start:pagination.end] path = request.path next = "?next=%s"%(path) # 拼路径,到后面添加和编辑成功之后,可以回到上次请求的页面,也就是从哪里来回哪里去 return render(request, "customer_list.html", {"customer_list": customer_list, "pagination": pagination,"next":next})
# post请求是批量处理 def post(self,request): print(request.POST) # action批量处理,有操作因此发送的是post请求 func_action = request.POST.get("action") # 获取的是处理的方法 data = request.POST.getlist("select_pk_list") #传值是数组,一定要用getlist来取值,否则只能取一个 if hasattr(self,func_action): func = getattr(self,func_action) queryset = Customer.objects.filter(pk__in=data) print(queryset) ret = func(request,queryset) if ret: return ret return redirect((request.path)) # 当前路径 else: return HttpResponse("非法输入!") def patch_delete(self,request,queryset): # 批量删除 queryset.delete() def patch_change_gs(self,request,queryset): # 批量公户转私户 # 公转私更新之前需要判断一下销售是由为空,空的时候才更新,不为空就不要更新 ret = queryset.filter(consultant__isnull=True) if ret: queryset.update(consultant=request.user) else: return HttpResponse("该客户已被他人转为私户,下次请快点") def patch_change_sg(self,request,queryset): # 私户转公户 queryset.update(consultant=None) # 也可以是空字符串 ""
# views.py CustomerView
from django.contrib.auth.models import AbstractUser from django.db import models from django.contrib import auth from django.core.exceptions import PermissionDenied from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager, User from django.utils.translation import ugettext_lazy as _ from multiselectfield import MultiSelectField from django.utils.safestring import mark_safe course_choices = (('LinuxL', 'Linux中高级'), ('PythonFullStack', 'Python高级全栈开发'),) class_type_choices = (('fulltime', '脱产班',), ('online', '网络班'), ('weekend', '周末班',),) source_type = (('qq', "qq群"), ('referral', "内部转介绍"), ('website', "官方网站"), ('baidu_ads', "百度推广"), ('office_direct', "直接上门"), ('WoM', "口碑"), ('public_class', "公开课"), ('website_luffy', "路飞官网"), ('others', "其它"),) enroll_status_choices = (('signed', "已报名"), ('unregistered', "未报名"), ('studying', '学习中'), ('paid_in_full', "学费已交齐")) seek_status_choices = (('A', '近期无报名计划'), ('B', '1个月内报名'), ('C', '2周内报名'), ('D', '1周内报名'), ('E', '定金'), ('F', '到班'), ('G', '全款'), ('H', '无效'),) pay_type_choices = (('deposit', "订金/报名费"), ('tuition', "学费"), ('transfer', "转班"), ('dropout', "退学"), ('refund', "退款"),) attendance_choices = (('checked', "已签到"), ('vacate', "请假"), ('late', "迟到"), ('absence', "缺勤"), ('leave_early', "早退"),) score_choices = ((100, 'A+'), (90, 'A'), (85, 'B+'), (80, 'B'), (70, 'B-'), (60, 'C+'), (50, 'C'), (40, 'C-'), (0, ' D'), (-1, 'N/A'), (-100, 'COPY'), (-1000, 'FAIL'),) class UserInfo(AbstractUser): tel=models.CharField(max_length=32,null=True,blank=True) gender=models.IntegerField(choices=((1,"男"),(2,"女")),default=1) class Customer(models.Model): """ 客户表 """ qq = models.CharField('QQ', max_length=64, unique=True, help_text='QQ号必须唯一') qq_name = models.CharField('QQ昵称', max_length=64, blank=True, null=True) name = models.CharField('姓名', max_length=32, blank=True, null=True, help_text='学员报名后,请改为真实姓名',default="") sex_type = (('male', '男'), ('female', '女')) sex = models.CharField("性别", choices=sex_type, max_length=16, default='male', blank=True, null=True) birthday = models.DateField('出生日期', default=None, help_text="格式yyyy-mm-dd", blank=True, null=True) phone = models.BigIntegerField('手机号', blank=True, null=True) source = models.CharField('客户来源', max_length=64, choices=source_type, default='qq') introduce_from = models.ForeignKey('Customer', verbose_name="转介绍自学员", blank=True, null=True,on_delete=models.CASCADE) course = MultiSelectField("咨询课程", choices=course_choices) class_type = models.CharField("班级类型", max_length=64, choices=class_type_choices, default='fulltime') customer_note = models.TextField("客户备注", blank=True, null=True, ) status = models.CharField("状态", choices=enroll_status_choices, max_length=64, default="unregistered", help_text="选择客户此时的状态") date = models.DateTimeField("咨询日期", auto_now_add=True) last_consult_date = models.DateField("最后跟进日期", auto_now_add=True) next_date = models.DateField("预计再次跟进时间", blank=True, null=True) consultant = models.ForeignKey('UserInfo', verbose_name="销售", related_name='customers', blank=True, null=True,on_delete=models.CASCADE ) class_list = models.ManyToManyField('ClassList', verbose_name="已报班级", ) def __str__(self): return self.name+":"+self.qq def get_classlist(self): ls=[] for cls in self.class_list.all(): ls.append(str(cls)) return mark_safe("<br>".join(ls)) def get_static_color(self): static_color = { "signed":"orange", "unregistered":"red", "studying":"blue", "paid_in_full":"green" } return mark_safe("<span style='background-color:%s;color:white'>%s</span>"%(static_color[self.status],self.get_status_display())) class Campuses(models.Model): """ 校区表 """ name = models.CharField(verbose_name='校区', max_length=64) address = models.CharField(verbose_name='详细地址', max_length=512, blank=True, null=True) def __str__(self): return self.name class ClassList(models.Model): """ 班级表 """ course = models.CharField("课程名称", max_length=64, choices=course_choices) semester = models.IntegerField("学期") campuses = models.ForeignKey('Campuses', verbose_name="校区",on_delete=models.CASCADE) price = models.IntegerField("学费", default=10000) memo = models.CharField('说明', blank=True, null=True, max_length=100) start_date = models.DateField("开班日期") graduate_date = models.DateField("结业日期", blank=True, null=True) #contract = models.ForeignKey('ContractTemplate', verbose_name="选择合同模版", blank=True, null=True,on_delete=models.CASCADE) teachers = models.ManyToManyField('UserInfo', verbose_name="老师") class_type = models.CharField(choices=class_type_choices, max_length=64, verbose_name='班额及类型', blank=True, null=True) class Meta: unique_together = ("course", "semester", 'campuses') def __str__(self): return "{}{}({})".format(self.get_course_display(), self.semester, self.campuses)
{% extends "base.html" %}
{% block content %}
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
全部客户
<small>所有客户信息</small>
</h1>
<form action="" method="get" class="pull-right">
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> Level</a></li>
<li class="active">Here</li>
<select name="field" id="s1" class="form-control" style="display: inline-block;width: 150px">
<!-- 这里最好是与数据库库中的字段相对应,方便查询-->
<option value="name">姓名</option>
<option value="qq">qq</option>
<option value="phone">手机</option>
<option value="status">状态</option>
</select>
<input type="text" name="val" class="form-control" style="display: inline-block;width: 200px">
<button class="btn btn-warning" style="vertical-align: 0px">search</button>
</ol>
</form>
</section>
<form action="" method="post">
{% csrf_token %}
<ol class="breadcrumb">
<select name="action" id="" class="form-control" style="display: inline-block;width: 200px;">
<option value="">-----------------------</option>
<option value="patch_delete">批量删除</option>
<option value="patch_change_gs">公户转私户</option>
<option value="patch_change_sg">私转公户</option>
</select>
<button class="btn btn-warning" style="vertical-align: 0px;">go</button>
</ol>
{#列表展示#}
<section class="content">
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Hover Data Table</h3>
</div>
<!-- /.box-header -->
<div class="box-body">
<a href="{% url "customer_add" %}{{ next }}" class="btn btn-success btn-sm">添加客户</a>
<table id="example2" class="text-center table table-bordered table-hover">
<thead>
<tr>
<th><input type="checkbox"></th>
<th>编号</th>
<th>客户姓名</th>
<th>QQ</th>
<th>手机号</th>
<th>性别</th>
<th>客户来源</th>
<th>咨询日期</th>
<th>当前状态</th>
<th>销售</th>
<th>所报班级</th>
<th>跟进记录</th>
<th>编辑</th>
<th>删除</th>
</tr>
</thead>
<tbody>
{% for customer in customer_list %}
<tr>
{# name一样 value不一样 #}
<td><input type="checkbox" name="select_pk_list" value="{{ customer.pk }}">
</td>
{# <td><input type="checkbox" name="select_pk_list" value="{{ customer.pk }}">#}
{# </td>#}
<td>{{ forloop.counter }}</td>
<td>{{ customer.name }}</td>
<td>{{ customer.qq }}</td>
<td>{{ customer.phone }}</td>
<td>{{ customer.get_sex_display }}</td>
<td>{{ customer.get_source_display }}</td>
<td>{{ customer.date|date:'Y-m-d' }}</td>
<!-- 状态带有颜色、销售默认暂无、班级列表多对多 两行显示,在models中写入函数即可-->
<td>{{ customer.get_static_color }}</td>
<td>{{ customer.consultant|default:"暂无" }}</td>
<td>{{ customer.get_classlist|default:"暂无" }}</td>
<td><a href="{% url "consultrecord_list" %}?customer_id={{ customer.pk }}">跟进详情</a>
</td>
<td>
<!-- next,携带上次的路径作为参数,以便编辑之后仍然回到上次页面 编辑、删除按钮-->
<a href="{% url "customer_edit" customer.pk %}{{ next }}"><i
class="fa fa-edit" aria-hidden="true"></i></a>
</td>
<td><a href="{% url "customer_delete" customer.pk %}{{ next }}"><i
class="fa fa-trash-o fa-lg"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
<nav aria-label="Page navigation" class="pull-right">
<ul class="pagination">
<!-- 渲染分页标签 -->
{{ pagination.page_html|safe }}
</ul>
</nav>
</div>
<!-- /.box-body -->
</div>
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
</form>
<!-- Main content -->
<!-- /.content -->
</div>
{% endblock %}
{% block js %}
<script>
{# 选择状态,出现下拉菜单 #}
$("#s1").change(function () {
if ($(this).val() === "status") {
let s2 = `
<select name="q" id="s2" class="form-control" style="display: inline-block;width: 200px">
<option value="signed">------------------------</option>
<option value="signed">已报名</option>
<option value="unregistered">未报名</option>
<option value="studying">学习中</option>
<option value="paid_in_full">学费已交齐</option>
</select>
`;
$(this).next().replaceWith(s2)
} else {
$(this).next().replaceWith("<input type=\"text\" name=\"val\" class=\"form-control\" style=\"display: inline-block;width: 200px\">")
}
})
</script>
{% endblock %}
from django import forms class CustomerModelForm(forms.ModelForm): class Meta: model=Customer fields="__all__" def __init__(self,*args,**kwargs): super().__init__(*args,**kwargs) for field in self.fields.values(): from multiselectfield.forms.fields import MultiSelectFormField # 判断,排除multiselectformfield字段就不加 form-control if not isinstance(field,MultiSelectFormField): field.widget.attrs.update({'class': 'form-control'})
功能1:分页保存搜索条件
需求背景:当我们条件搜索后,我们查询下一页的数据,我们希望能够保存之前的条件。利用自定义分页。
class Pagination(object): def __init__(self ,current_page_num ,all_count ,request ,per_page_num=2 ,pager_count=11 ,): # 默认参数可以改变 """ 封装分页相关数据 :param current_page_num: 当前访问页的数字 :param all_count: 分页数据中的数据总条数 :request:里面request.GET中含有get请求发送的键值对 :param per_page_num: 每页显示的数据条数 :param pager_count: 最多显示的页码个数 """ try: current_page_num = int(current_page_num) except Exception as e: current_page_num = 1 if current_page_num <1: current_page_num = 1 self.current_page_num = current_page_num self.all_count = all_count self.per_page_num = per_page_num # 实际总页码 all_pager, tmp = divmod(all_count, per_page_num) if tmp: all_pager += 1 self.all_pager = all_pager self.pager_count = pager_count self.pager_count_half = int((pager_count - 1) / 2) # 5 # 保存搜索条件 import copy self.params = copy.deepcopy(request.GET) # {"a":"1","b":"2"} @property def start(self): return (self.current_page_num - 1) * self.per_page_num @property def end(self): return self.current_page_num * self.per_page_num # 该方法用于在页面渲染分页的标签按钮 def page_html(self): # 如果总页码 < 11个: if self.all_pager <= self.pager_count: pager_start = 1 pager_end = self.all_pager + 1 # 总页码 > 11 else: # 当前页如果<=页面上最多显示11/2个页码 if self.current_page_num <= self.pager_count_half: pager_start = 1 pager_end = self.pager_count + 1 # 当前页大于5 else: # 页码翻到最后 if (self.current_page_num + self.pager_count_half) > self.all_pager: pager_start = self.all_pager - self.pager_count + 1 pager_end = self.all_pager + 1 else: pager_start = self.current_page_num - self.pager_count_half pager_end = self.current_page_num + self.pager_count_half + 1 page_html_list = [] # 存放分页标签,用于整体渲染 # 首页 self.params["page"] = 1 # 添加键值对,page=1 first_page = '<li><a href="?%s">首页</a></li>' % (self.params.urlencode(),) page_html_list.append(first_page) # 上一页 self.params["page"] = self.current_page_num - 1 if self.current_page_num <= 1: prev_page = '<li class="disabled"><a href="#">上一页</a></li>' else: prev_page = '<li><a href="?%s">上一页</a></li>' % (self.params.urlencode(),) page_html_list.append(prev_page) # 中间页 for i in range(pager_start, pager_end): self.params["page"] = i if i == self.current_page_num: temp = '<li class="active"><a href="?%s">%s</a></li>' % (self.params.urlencode(), i) else: temp = '<li><a href="?%s">%s</a></li>' % (self.params.urlencode(), i,) # 保存搜索条件 page_html_list.append(temp) # 下一页 self.params["page"] = self.current_page_num + 1 if self.current_page_num >= self.all_pager: next_page = '<li class="disabled"><a href="#">下一页</a></li>' else: next_page = '<li><a href="?%s">下一页</a></li>' % (self.params.urlencode(),) page_html_list.append(next_page) # 尾页 self.params["page"] = self.all_pager last_page = '<li><a href="?%s">尾页</a></li>' % (self.params.urlencode(),) page_html_list.append(last_page) return ''.join(page_html_list)
功能2:模糊查询
模糊查询实现了,输入关键字也能够查出来
# views.py
# 模糊查询 field = request.GET.get("field") # name 获取到的 name是个字符串 val = request.GET.get('val') # 输入的值 if val: # 必须判断 q = Q() q.children.append((field + "__contains", val)) # contains 包含,append里面是元组, 等价于 Q(field__contains=val)其中field为字段名,不是字符串 customer_list = customer_list.filter(q)
效果:

1、Q()
Q 查询的时候,里面含有的是 字段名=... ,现在从前端查询处获得是的字段名为字符串,直接用大Q不行,因此提供了实例化 Q()
q = Q()
q.children.append((字段名字符串,val值))
queryset对象 = queryset对象.filter(q)
2、前端中,块标签垂直移动,可以使用vertical-align
style="vertical-align: 3px"
功能3:批量处理
批量处理之后也是展示客户列表,因此也要写在 class CustomerView(View): 视图类中,
通过post请求可以接受 方法名 和 处理的数据记录的 id,然后根据反射 在该类中查找对应的方法进行操作。
其中包含 批量删除、私户转公户、公户转私户
需要注意的是,在公户转私户的过程中,如果过滤出要转换的记录之后,直接更新的话,会有bug,就是当两个用户同时公户转私户操作同一条记录 a , 如果前者将 a 转成自己的私户,后者操作的可以将 a 再次转成自己的,因此后者提交的时候同样获取了 a 的内容。因此在公户转私户之前,要对销售人员是否存在进行判断。
def patch_change_gs(self,request,queryset): # 批量公户转私户 # 公转私更新之前需要判断一下销售是由为空,空的时候才更新,不为空就不要更新 ret = queryset.filter(consultant__isnull=True) # 判断是否为空 if ret: queryset.update(consultant=request.user) else: return HttpResponse("该客户已被他人转为私户,下次请快点")
效果:

功能4:不同的状态显示不同的颜色
客户表中的当前状态是用 choice来写的,也就是获取文本值时候需要用到 .get_status_display
<td>{{ customer.get_status_display }}</td>
但是这样只是单纯的显示文本
需求:希望获取到的文本能够添加不同背景色,然后可以一目了然的观察每个人的状态,
<td>{{ customer.get_static_color }}</td> # get_static_color 是自己在 模型类中写的方法
# 写在 Customer(models.Model) 中
def get_static_color(self): static_color = { "signed":"orange", "unregistered":"red", "studying":"blue", "paid_in_full":"green" } return mark_safe("<span style='background-color:%s;color:white'>%s</span>"%(static_color[self.status],self.get_status_display()))
其中:mark_safe(...) 中的 html 代码,是告诉浏览器代码是安全的,可以渲染,如果不加的话,会当做字符串显示在页面上。
效果:

功能5:列表中文本两行显示(一对多关系时)
需求:在页面展示的时候,有些字段是一对多的关系,添加的时候,有两个内容,希望能够分行展示,使得页面更加美观,这种情况类似上面的改变背景色。
<td>{{ customer.get_classlist|default:"暂无" }}</td> # 班级列表是一对多,可以有多个
在 Customer(models.Model) 模型类中添加函数:
def get_classlist(self): ls=[] for cls in self.class_list.all(): ls.append(str(cls)) return mark_safe("<br>".join(ls))
客户的增删改查
具体的实现代码:
# urls.py
path("customer/add/",views.AddEditCustomerView.as_view(),name="customer_add"), re_path("customer/edit/(\d+)/",views.AddEditCustomerView.as_view(),name="customer_edit"), re_path("customer/delete/(\d+)/",views.CustomerDeleteView.as_view(),name="customer_delete"),
# views.py
class AddCustomerView(View): @method_decorator(login_require) def get(self,request): form = CustomerModelForm() return render(request,"customer_add.html",{"form":form}) def post(self,request): form = CustomerModelForm(request.POST) if form.is_valid(): form.save() return redirect(reverse("customers_list")) else: return render(request,"customer_add.html",{"form":form}) class CustomerEditView(View): @method_decorator(login_require2) def get(self,request,edit_id): # customer_obj = Customer.objects.filter(pk=edit_id) #用filter是错误的, customer_obj = Customer.objects.get(pk=edit_id) # get 只能取到一个对象,非queryset对象 form = CustomerModelForm(instance=customer_obj) # path用于取消的时候走的路径 path= request.GET.get("next") return render(request,"customer_edit.html",{"path":path,"form":form,"request":request}) def post(self,request,edit_id): customer_obj = Customer.objects.get(pk=edit_id) form = CustomerModelForm(request.POST,instance=customer_obj) if form.is_valid(): form.save() return redirect(request.GET.get("next")) else: return render(request,"customer_edit.html",{"form":form}) class CustomerDeleteView(View): @method_decorator(login_require2) def get(self,request,del_id): customer_obj = Customer.objects.filter(pk=del_id).delete() print(request.GET.get("next")) # 回到上次访问的url 展示(全部或我的) return redirect(request.GET.get("next"))
# html
{% extends "base.html" %}
{% block content %}
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header" style="text-align: center">
<h2>添加客户
<small>填写客户详细信息</small>
</h2>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> Level</a></li>
<li class="active">Here</li>
</ol>
</section>
<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="">{{ field.label }}</label>
{{ field }}
<span class="error">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<a href="" class="btn btn-default">取消</a>
<button class="btn btn-default">确认</button>
</form>
</div>
</div>
</section>
<!-- /.content -->
</div>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header" style="text-align: center">
<h2>修改客户
<small>填写客户详细信息</small>
</h2>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> Level</a></li>
<li class="active">Here</li>
</ol>
</section>
<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="">{{ field.label }}</label>
{{ field }}
<span class="error">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<a href="{{ path }}" class="btn btn-default">取消</a>
<button class="btn btn-default">确认</button>
</form>
</div>
</div>
</section>
<!-- /.content -->
</div>
{% endblock %}
优化后的代码:
由于添加和编辑无论就后台还是前台代码都有很多的重复,因此需要进行优化,将添加和编辑合二为一,由于添加的时候参数只有request, 编辑的时候还多了 edit_id 的参数,因此,需要给的参数是默认值,
# 优化后,添加的时候edit_id为空,编辑的时候有值
class AddEditCustomerView(View): def get(self,request,edit_id=None): # customer_obj = Customer.objects.get(pk=edit_id) # 错误的,用get(没有值会报错) customer_obj = Customer.objects.filter(pk=edit_id).first() # modelform 和 form 校验的时候里面的参数传的是对象,并非queryset类型 form = CustomerModelForm(instance=customer_obj) # 取消的时候传的路径 path = request.GET.get("next") return render(request,"customer_add_edit.html",{"form":form,"path":path,"customer_obj":customer_obj}) def post(self,request,edit_id=None): customer_obj = Customer.objects.filter(pk=edit_id).first() form = CustomerModelForm(request.POST,instance=customer_obj) if form.is_valid(): form.save() return redirect(request.GET.get("next")) else: return render(request,"customer_add_edit.html",{"form":form,"customer_obj":customer_obj})
{% extends "base.html" %}
{% block content %}
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header" style="text-align: center">
<h2>
{% if not customer_obj %}
添加客户信息
{% else %}
修改客户信息
{% endif %}
</h2>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> Level</a></li>
<li class="active">Here</li>
</ol>
</section>
<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="">{{ field.label }}</label>
{{ field }}
<span class="error">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<a href="{{ path }}" class="btn btn-default">取消</a>
<button class="btn btn-default">确认</button>
</form>
</div>
</div>
</section>
<!-- /.content -->
</div>
{% endblock %}
总结1:携带上次请求路径,?next=/.../
?next=/customer/list/ 、 ?next=/mycustomer/
由于客户的列表展示中分为公户和私户,在不同的界面打开添加和编辑成功后,应该回到原来展示的界面,这里由于是公户和私户两条线都有编辑和添加功能,因此重定向的时候是不能写死的,所以要在发送添加和编辑请求的时候,携带上次请求的路径,作为参数,也就是 ?next=/.../
从哪里来回哪里去
思路:在展示的视图函数(get请求)中,要获取到路径进行拼接,在添加和编辑按钮的 请求路径中添加参数
获取next路径
path = request.path next = "?next=%s"%(path) # 拼路径,到后面添加和编辑成功之后,可以回到上次请求的页面,也就是从哪里来回哪里去 render给页面
编辑和删除按钮的请求路径
<td> <!-- next,携带上次的路径作为参数,以便编辑之后仍然回到上次页面 编辑、删除按钮--> <a href="{% url "customer_edit" customer.pk %}{{ next }}"><i class="fa fa-edit" aria-hidden="true"></i></a> # 其中i是图标,customer.pk是反向解析的动态传参 </td> <td> <a href="{% url "customer_delete" customer.pk %}{{ next }}"><i class="fa fa-trash-o fa-lg"></i></a> </td>
跟进记录情况
跟进记录的展示、添加、编辑和客户列表是类似的,只是这里不再分公户和私户两条线,这里应该都是私户的,显示的是当前登录用户的客户的跟进情况
展示、添加、编辑 具体实现代码:
# urls.py
path("consultrecord/list/",views.ConsultRecordView.as_view(),name="consultrecord_list"), re_path("consultrecord/add/",views.AddEditConsultRecordView.as_view(),name="consultrecord_add"), re_path("consultrecord/edit/(\d+)/",views.AddEditConsultRecordView.as_view(),name="consultrecord_edit"), re_path("consultrecord/delete/(\d+)/",views.ConsultRecordDeleteView.as_view(),name="consultrecord_delete")
# views.py 添加和编辑是优化的代码
class ConsultRecordView(View): @method_decorator(login_require) def get(self,request): consultrecord_list = ConsultRecord.objects.filter(consultant=request.user) customer_id = request.GET.get("customer_id") if customer_id: consultrecord_list = ConsultRecord.objects.filter(customer_id=customer_id) # 分页 current_page_num = request.GET.get("page",1) pagination = Pagination(current_page_num,consultrecord_list.count(),request) consultrecord_list = consultrecord_list[pagination.start:pagination.end] path = request.path next = "?next=%s"%(path) return render(request,"consultrecord_list.html",{"next":next,"consultrecord_list":consultrecord_list,"pagination":pagination}) class AddEditConsultRecordView(View): def get(self,request,edit_id=None): consultrecord_obj = ConsultRecord.objects.filter(pk=edit_id).first() form = ConsultrecordModelForm(instance=consultrecord_obj) path = request.GET.get("next") return render(request,"consultrecord_add_edit.html",{"form":form,"path":path,"consultrecord_obj":consultrecord_obj}) def post(self,request,edit_id=None): consultrecord_obj = ConsultRecord.objects.filter(pk=edit_id).first() form = ConsultrecordModelForm(request.POST,instance=consultrecord_obj) if form.is_valid(): form.save() return redirect(reverse("consultrecord_list")) # 跟进记录 else: return render(request,"consultrecord_add_edit.html",{"form":form,"consultrecord_obj":consultrecord_obj}) class ConsultRecordDeleteView(View): def get(self,request,del_id): consultrecord_obj = ConsultRecord.objects.filter(pk=del_id).delete() print(request.GET.get("next")) return redirect(request.GET.get("next"))
# html
{% extends "base.html" %} {% block content %} <div class="content-wrapper"> <!-- Content Header (Page header) --> <section class="content-header"> <h1> 客户跟进记录 <small>跟进记录信息</small> </h1> </section> {#列表展示#} <section class="content"> <div class="row"> <div class="col-xs-12"> <div class="box"> <div class="box-header"> <h3 class="box-title">Hover Data Table</h3> </div> <!-- /.box-header --> <div class="box-body"> <a href="{% url "consultrecord_add" %}{{ next }}" class="btn btn-success btn-sm">添加客户跟进信息</a> <table id="example2" class="text-center table table-bordered table-hover"> <thead> <tr> <th>编号</th> <th>客户姓名</th> <th>当前状态</th> <th>跟进内容</th> <th>跟进人</th> <th>编辑</th> <th>删除</th> </tr> </thead> <tbody> {% for consultrecord in consultrecord_list %} <tr> <td>{{ forloop.counter }}</td> <td>{{ consultrecord.customer.name }}</td> <td>{{ consultrecord.get_status_display }}</td> <td>{{ consultrecord.note }}</td> <td>{{ consultrecord.consultant }}</td> <td> <!-- next,携带上次的路径作为参数,以便编辑之后仍然回到上次页面 编辑、删除按钮--> <a href="{% url "consultrecord_edit" consultrecord.pk %}{{ next }}"><i class="fa fa-edit" aria-hidden="true"></i></a> </td> <td><a href="{% url "consultrecord_delete" consultrecord.pk %}{{ next }}"><i class="fa fa-trash-o fa-lg"></i></a></td> </tr> {% endfor %} </tbody> </table> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {# 渲染分页标签 #} {{ pagination.page_html|safe }} </ul> </nav> </div> <!-- /.box-body --> </div> </div> <!-- /.col --> </div> <!-- /.row --> </section> </div> {% endblock %} {% block js %} {% endblock %}
{% extends "base.html" %} {% block content %} <div class="content-wrapper"> <!-- Content Header (Page header) --> <section class="content-header" style="text-align: center"> <h2> {% if not consultrecord_obj %} 添加跟进记录信息 {% else %} 修改跟进记录信息 {% endif %} </h2> <ol class="breadcrumb"> <li><a href="#"><i class="fa fa-dashboard"></i> Level</a></li> <li class="active">Here</li> </ol> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <form action="" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> <label for="">{{ field.label }}</label> {{ field }} <span class="error">{{ field.errors.0 }}</span> </div> {% endfor %} <a href="{{ path }}" class="btn btn-default">取消</a> <button class="btn btn-default">确认</button> </form> </div> </div> </section> <!-- /.content --> </div> {% endblock %}
效果:
显示的跟进情况都是当前登录用户 chen 的私户,并且一个私户可以有多条跟进记录

功能1:客户列表展示中添加跟进详情
需求:希望能够在客户展示界面有一个 跟进详情 的操作,点开之后,只显示,该私户的跟进详情。
customer_list.html 在客户列表展示中添加 跟进详情的标签,点击添加跟进详情的,将该记录的pk值作为参数传给后台。
<td><a href="{% url "consultrecord_list" %}?customer_id={{ customer.pk }}">跟进详情</a></td>
class ConsultRecordView(View): @method_decorator(login_require) def get(self,request): consultrecord_list = ConsultRecord.objects.filter(consultant=request.user) # 左侧栏点击的时候不存在参数 # 判断是否存在参数id customer_id = request.GET.get("customer_id") # a标签点击的时候存在参数 if customer_id: consultrecord_list = ConsultRecord.objects.filter(customer_id=customer_id) # 分页 current_page_num = request.GET.get("page",1) pagination = Pagination(current_page_num,consultrecord_list.count(),request) consultrecord_list = consultrecord_list[pagination.start:pagination.end] path = request.path next = "?next=%s"%(path) return render(request,"consultrecord_list.html",{"next":next,"consultrecord_list":consultrecord_list,"pagination":pagination})
效果:
在客户界面点击跟进详情会只出现,该客户的跟进详情,若是在左侧栏点击客户跟进详情,那么会显示当前用户所有客户的跟进详情。


浙公网安备 33010602011771号