基于 Python/Vue/D2 实现的CRM管理系统(客户管理,产品管理,商机管理,合同管理,客户公海,权限管理等业务模块)

发布于:2022-11-08 ⋅ 阅读:(743) ⋅ 点赞:(0)

目录

一、系统概述

业务模块

部署方式

二、效果展示

登录页面

客户页面

联系人

产品类目

产品 

合同

消息中心

供应商

客户公海

三、关键代码 

后端 http 使用 python django 

后端数据库模型代码 - 客户管理

后端业务模块代码 - 客户管理

前端页面 vue 代码

前端业务逻辑处理


一、系统概述

客户关系管理系统(CRM)是利用信息科学技术,实现市场营销、销售、服务等活动自动化,是企业能更高效地为客户提供满意、周到的服务,以提高客户满意度、忠诚度为目的的一种管理经营方式。客户关系管理既是一种管理理念,又是一种软件技术。以客户为中心的管理理念是CRM实施的基础。

业务模块

  • 客户管理:增删改查,客户名称,公司地址,联系方式等信息
  • 产品管理:增删改查,树状结构,自定义多产品类别,多产品名称
  • 商机管理:增删改查,辅助业务员对客户持续跟进,挖掘商机,制定推进计划
  • 合同管理:增删改查,包含交货方式,付款方式,合同状态等信息
  • 客户公海:增删改查,管理员制定策略自动将久未更新的客户划入公海,或手动划入公海
  • 数据导入导出功能:业务数据模块均支持
  • 数据报表:按时间(日月年)进行数据统计分析
  • 企业驾驶舱:将销售关键指标提炼出来,以数据可视化方式展示给企业决策者,助力更有价值的决策经营

  • 部门管理:增删改查,树状结构,与企业组织架构一一对应
  • 用户管理:增删改查,与企业员工一一对应
  • 角色管理:管理员,部分负责人,业务员等权限
  • 消息中心:群发通知,可以指定部门,指定用户
  • 日志管理:所有模块的增删改查数据,便于问题追踪回溯

部署方式

支持Windows、Linux、Mac等各种操作系统;

观看方式:既可在服务器上直接观看程序界面,也可在远程用浏览器打开播放,例如Chrome浏览器、360浏览器等。

二、效果展示

登录页面

客户页面

联系人

产品类目

产品 

 商机

合同

消息中心

供应商

客户公海

三、关键代码 

后端 http 使用 python django 

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

后端数据库模型代码 - 客户管理


class Customer(CoreModel):
    name = models.CharField(
        max_length=150, unique=True, db_index=True, verbose_name="客户名称", help_text="客户名称")

    mobile = models.CharField(
        max_length=255, verbose_name="手机", null=True, blank=True, help_text="手机")
    telephone = models.CharField(
        max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话")
    email = models.EmailField(
        max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱")
    address = models.CharField(
        max_length=255, verbose_name="客户公司地址", null=True, blank=True, help_text="客户公司地址")
    
    task_status = models.IntegerField(
        default=0, verbose_name="跟进状态", help_text="跟进状态")
    task_record = models.CharField(
        max_length=255, verbose_name="最新跟进记录", null=True, blank=True, help_text="最新跟进记录")

    
    is_active = models.BooleanField(
        default=0, verbose_name="启用 or 停用", help_text="启用 or 停用")

    owner = models.ForeignKey(
        to="Users",
        related_name='customer_owner',
        verbose_name="负责人",
        on_delete=models.PROTECT,
        db_constraint=False,
        null=True,
        blank=True,
        help_text="负责人",
    )
    dept = models.ForeignKey(
        to="Dept",
        verbose_name="所属部门",
        on_delete=models.PROTECT,
        db_constraint=False,
        null=True,
        blank=True,
        help_text="关联部门",
    )
    # contacter = models.ForeignKey(
    #     to="Contacter",
    #     related_name='contacter_name',
    #     verbose_name="联系人",
    #     on_delete=models.PROTECT,
    #     db_constraint=False,
    #     null=True,
    #     blank=True,
    #     help_text="联系人",
    # )
    class Meta:
        db_table = table_prefix + "customer"
        verbose_name = "客户表"
        verbose_name_plural = verbose_name
        ordering = ("-create_datetime",)

后端业务模块代码 - 客户管理

from django_restql.fields import DynamicSerializerMethodField
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated

from application import dispatch
from dvadmin.system.models import Customer, Dept, Contacter
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.system.views.contacter import ContacterSerializer


class CustomerSerializer(CustomModelSerializer):
    """
    客户管理-序列化器
    """

    dept_name = serializers.CharField(source="dept.name", default="")
    owner_name = serializers.CharField(source="users.name", default="")
    contacter_name = serializers.CharField(source="contacter.name", default="")

    class Meta:
        model = Customer
        fields = "__all__"
        read_only_fields = ["id"]
        extra_kwargs = {

        }


class CustomerInitSerializer(CustomModelSerializer):
    """
    初始化获取数信息(用于生成初始化json文件)
    """

    class Meta:
        model = Customer
        fields = "__all__"
        read_only_fields = ['id']
        extra_kwargs = {
            'creator': {'write_only': True},
            'dept_belong_id': {'write_only': True}
        }


class CustomerCreateSerializer(CustomModelSerializer):
    """
    客户新增-序列化器
    """

    name = serializers.CharField(
        max_length=50,
        validators=[
            CustomUniqueValidator(
                queryset=Customer.objects.all(), message="客户名称必须唯一")
        ],
    )

    def save(self, **kwargs):
        data = super().save(**kwargs)
        data.save()
        return data

    class Meta:
        model = Customer
        fields = "__all__"
        read_only_fields = ["id"]
        extra_kwargs = {
            "post": {"required": False},
        }


class CustomerUpdateSerializer(CustomModelSerializer):
    """
    客户修改-序列化器
    """

    name = serializers.CharField(
        max_length=50,
        validators=[
            CustomUniqueValidator(
                queryset=Customer.objects.all(), message="账号必须唯一")
        ],
    )
    # password = serializers.CharField(required=False, allow_blank=True)
    mobile = serializers.CharField(
        max_length=50,
        validators=[
            CustomUniqueValidator(
                queryset=Customer.objects.all(), message="手机号必须唯一")
        ],
        allow_blank=True
    )

    def save(self, **kwargs):
        data = super().save(**kwargs)
        data.save()
        return data

    class Meta:
        model = Customer
        read_only_fields = ["id"]
        fields = "__all__"
        extra_kwargs = {
            "post": {"required": False, "read_only": True},
        }


class ExportCustomerSerializer(CustomModelSerializer):
    """
    客户导出 序列化器
    """

    is_active = serializers.SerializerMethodField(read_only=True)
    dept_name = serializers.CharField(source="dept.name", default="")
    # dept_owner = serializers.CharField(source="dept.owner", default="")

    def get_is_active(self, instance):
        return "启用" if instance.is_active else "停用"

    class Meta:
        model = Customer
        fields = (
            "name",
            "email",
            "mobile",
            "is_active",
        )


class CustomerImportSerializer(CustomModelSerializer):
    def save(self, **kwargs):
        data = super().save(**kwargs)
        data.save()
        return data

    class Meta:
        model = Customer
        exclude = (
            "date_joined",
        )


class CustomerViewSet(CustomModelViewSet):
    """
    客户接口
    list:查询
    create:新增
    update:修改
    retrieve:单例
    destroy:删除
    """

    queryset = Customer.objects.exclude().all()
    serializer_class = CustomerSerializer
    create_serializer_class = CustomerCreateSerializer
    update_serializer_class = CustomerUpdateSerializer
    filter_fields = {
        "name": ["icontains"],
        "mobile": ["icontains"],
        "telephone": ["icontains"],
        "owner": ["exact"],
        # "contacter": ["exact"],
        "task_status": ["exact"],
        "is_active": ["exact"],
    }
    # search_fields = ["name", "contacter",
    #                  "status", "mobile", "owner"]
    # 导出
    export_field_label = [
        "客户名称",
        "客户邮箱",
        "手机号码",
        "帐号状态",
    ]
    export_serializer_class = ExportCustomerSerializer
    # 导入
    import_serializer_class = CustomerImportSerializer
    import_field_dict = {
        "name": "客户名称",
        "email": "客户邮箱",
        "mobile": "手机号码",

        "is_active": {
            "title": "帐号状态",
            "choices": {
                "data": {"启用": True, "禁用": False},
            }
        },
    }

    @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
    def customer_info(self, request):
        """获取当前客户信息"""
        customer = request.customer
        result = {
            "name": customer.name,
            "mobile": customer.mobile,
            "email": customer.email,

        }
        return DetailResponse(data=result, msg="获取成功")

    @action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated])
    def update_customer_info(self, request):
        """修改当前客户信息"""
        customer = request.customer
        Customer.objects.filter(id=customer.id).update(**request.data)
        return DetailResponse(data=None, msg="修改成功")

    def customer_lazy_tree(self, request, *args, **kwargs):
        parent = self.request.query_params.get('parent')
        queryset = self.filter_queryset(self.get_queryset())
        if not parent:
            if self.request.user.is_superuser:
                queryset = queryset.filter(parent__isnull=True)
            else:
                queryset = queryset.filter(id=self.request.user.dept_id)
        data = queryset.filter(status=True).order_by(
            'sort').values('name', 'id', 'parent')
        return DetailResponse(data=data, msg="获取成功")

前端页面 vue 代码

<template>
  <d2-container :class="{ 'page-compact': crud.pageOptions.compact }">
    <d2-crud-x ref="d2Crud" v-bind="_crudProps" v-on="_crudListeners">
      <div slot="header">
        <crud-search
          ref="search"
          :options="crud.searchOptions"
          @submit="handleSearch"
        />
        <el-button-group>
          <el-button
            size="small"
            type="primary"
            v-permission="'Create'"
            @click="addRow"
          >
            <i class="el-icon-plus" /> 新增
          </el-button>
          <el-button size="small" type="danger" @click="batchDelete">
            <i class="el-icon-delete"></i> 批量删除
          </el-button>
          <el-button size="small" type="warning" @click="onExport" v-permission="'Export'"><i class="el-icon-download" /> 导出
          </el-button>
          <importExcel importApi="api/system/customer/import/" v-permission="'Import'"
            >导入
          </importExcel>
        </el-button-group>
        <crud-toolbar
          :search.sync="crud.searchOptions.show"
          :compact.sync="crud.pageOptions.compact"
          :columns="crud.columns"
          @refresh="doRefresh()"
          @columns-filter-changed="handleColumnsFilterChanged"
        />
      </div>
    </d2-crud-x>
  </d2-container>
</template>

<script>
import * as api from './api'
import { crudOptions } from './crud'
import { d2CrudPlus } from 'd2-crud-plus'
export default {
  name: 'customer',
  mixins: [d2CrudPlus.crud],
  data() {
    return {}
  },
  methods: {
    getCrudOptions() {
      this.crud.searchOptions.form.user_type = 0
      return crudOptions(this)
    },
    pageRequest(query) {
      return api.GetList(query)
    },
    addRequest(row) {
      return api.AddObj(row)
    },
    updateRequest(row) {
      return api.UpdateObj(row)
    },
    delRequest(row) {
      return api.DelObj(row.id)
    },
    batchDelRequest(ids) {
      return api.BatchDel(ids)
    },
    onExport() {
      const that = this
      this.$confirm('是否确认导出所有数据项?', '警告', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(function () {
        const query = that.getSearch().getForm()
        return api.exportData({ ...query })
      })
    }
  }
}
</script>

<style lang="scss">
.yxtInput {
  .el-form-item__label {
    color: #49a1ff;
  }
}
</style>

前端业务逻辑处理

import { urlPrefix as userPrefix } from '../system/user/api'
export const crudOptions = (vm) => {
  return {
    pageOptions: {
      compact: true
    },
    options: {
      height: '100%',
      tableType: 'vxe-table',
      rowKey: true,
      rowId: 'id'
    },
    selectionRow: {
      align: 'center',
      width: 46
    },
    rowHandle: {
      width: 240,
      fixed: 'right',
      view: {
        thin: true,
        text: '',
        disabled() {
          return !vm.hasPermissions('Retrieve')
        }
      },
      edit: {
        thin: true,
        text: '',
        disabled() {
          return !vm.hasPermissions('Update')
        }
      },
      remove: {
        thin: true,
        text: '',
        disabled() {
          return !vm.hasPermissions('Delete')
        }
      }
    },
    viewOptions: {
      componentType: 'form'
    },
    formOptions: {
      defaultSpan: 12 // 默认的表单 span
    },
    indexRow: {
      // 或者直接传true,不显示title,不居中
      title: '序号',
      align: 'center',
      width: 60
    },
    columns: [
      {
        title: 'ID',
        key: 'id',
        disabled: true,
        form: {
          disabled: true
        }
      },
      {
        title: '客户名称',
        key: 'name',
        search: {
          disabled: false
        },
        minWidth: 100,
        type: 'input',
        form: {
          rules: [
            // 表单校验规则
            {
              required: true,
              message: '客户名称必填项'
            }
          ],
          component: {
            placeholder: '请输入客户名称'
          },
          itemProps: {
            class: { yxtInput: true }
          }
        }
      },
      {
        title: '电话',
        key: 'telephone',
        search: {
          disabled: true
        },
        minWidth: 110,
        type: 'input',
        form: {
          rules: [
            {
              max: 20,
              message: '请输入正确的电话号码',
              trigger: 'blur'
            },
            {
              // pattern: /^0\d{11-15}$/,
              message: '请输入正确的手机号码'
            }
          ],

          component: {
            placeholder: '示例 0755-34513212'
          }
        }
      },
      {
        title: '手机号码',
        key: 'mobile',
        search: {
          disabled: true
        },
        minWidth: 140,
        type: 'input',
        form: {
          rules: [
            {
              max: 20,
              message: '请输入正确的手机号码',
              trigger: 'blur'
            },
            {
              pattern: /^1[3-9]\d{9}$/,
              message: '请输入正确的手机号码'
            }
          ],

          component: {
            placeholder: '请输入手机号码'
          }
        }
      },
      {
        title: '邮箱',
        key: 'email',
        minWidth: 180,
        form: {
          rules: [
            {
              type: 'email',
              message: '请输入正确的邮箱地址',
              trigger: ['blur', 'change']
            }
          ],
          component: {
            placeholder: '请输入邮箱'
          }
        }
      },
      {
        title: '公司地址',
        key: 'address',
        type: 'input',
        minWidth: 180,
        form: {
          rules: [
            {
              message: '请输入正确的公司地址',
              trigger: ['blur', 'change']
            }
          ],
          component: {
            placeholder: '请输入公司地址'
          }
        }
      },
      {
        title: '跟进状态',
        key: 'task_status',
        search: {
          disabled: false
        },
        type: 'select',
        width: 145,
        dict: {
          data: vm.dictionary('customer_task_status')
        },
        form: {
          value: 1,
          component: {
            span: 12
          }
        },
        component: { props: { color: 'auto' } } // 自动染色
      },
      {
        title: '负责人',
        key: 'owner',
        minWidth: 90,
        search: {
          disabled: false
        },
        type: 'tree-selector',
        dict: {
          cache: false,
          isTree: true,
          url: userPrefix,
          value: 'id', // 数据字典中value字段的属性名
          label: 'username' // 数据字典中label字段的属性名
        },
        form: {
          rules: [
            // 表单校验规则
            {
              required: true,
              message: '负责人必填项'
            }
          ],
          itemProps: {
            class: { yxtInput: true }
          },
          component: {
            span: 12,
            pagination: true,
            props: { multiple: false }
          }
        }
      },
      // {
      //   title: '部门',
      //   key: 'dept',
      //   search: {
      //     disabled: true
      //   },
      //   minWidth: 140,
      //   type: 'tree-selector',
      //   dict: {
      //     cache: false,
      //     isTree: true,
      //     url: deptPrefix,
      //     value: 'id', // 数据字典中value字段的属性名
      //     label: 'name' // 数据字典中label字段的属性名
      //   },
      //   form: {
      //     rules: [
      //       // 表单校验规则
      //       {
      //         required: true,
      //         message: '必填项'
      //       }
      //     ],
      //     itemProps: {
      //       class: { yxtInput: true }
      //     },
      //     component: {
      //       span: 12,
      //       pagination: true,
      //       props: { multiple: false }
      //     }
      //   },
      //   component: {
      //     name: 'foreignKey',
      //     valueBinding: 'dept_name'
      //   }
      // },
      {
        title: '是否启用',
        key: 'is_active',
        search: {
          disabled: false
        },
        width: 70,
        type: 'radio',
        dict: {
          data: vm.dictionary('button_status_bool')
        },
        form: {
          value: true,
          component: {
            span: 12
          }
        }
      }
    ].concat(
      vm.commonEndColumns({
        create_datetime: { showTable: false },
        update_datetime: { showTable: false }
      })
    )
  }
}

参考资料 

Django-Vue-Admin

本次分享结束,欢迎交流定制 ~

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到