Просмотр исходного кода

Merge branch 'dev-wks' into dev

wks 1 год назад
Родитель
Сommit
83319082f8

BIN
src/assets/customerAvatar.png


+ 2 - 0
src/http/api.js

@@ -52,6 +52,7 @@ import * as one from './moudules/service/one'
 import * as sys from './moudules/sys.js'
 //客服
 import * as aiSupport from './moudules/service/aiSupport'
+import * as customerManage from './moudules/customer/customerManage'
 
 import * as bindUser from './moudules/bindUser.js'
 //读取pdf设置
@@ -79,6 +80,7 @@ export default {
     dict,
     log,
     aiSupport,
+    customerManage,
     settlement,
     /////////////////////////////
     esmTeam,

+ 97 - 0
src/http/moudules/customer/customerManage.js

@@ -0,0 +1,97 @@
+import axios from '../../axios'
+
+
+//获取企业微信通讯录部门树
+export const getDeptTree = () => {
+  return axios({
+    url: '/wechat/customer/manage/getDeptTree',
+    method: 'get',
+  })
+}
+
+//通过部门id获取用户列表
+export const getDeptUser = (deptId) => {
+  return axios({
+    url: '/wechat/customer/manage/getDeptUser?deptId=' + deptId,
+    method: 'get',
+  })
+}
+
+// 修改和添加值班配置
+export const dutyConfig = (data) => {
+  return axios({
+    url: '/wechat/customer/manage/dutyConfig',
+    method: 'put',
+    data
+  })
+}
+
+//获取值班配置
+export const getDutyConfig = (customerId) => {
+  return axios({
+    url: '/wechat/customer/manage/getDutyConfig/' + customerId,
+    method: 'get',
+  })
+}
+
+// 清除值班配置
+export const clearDutyConfig = (customerId) => {
+  return axios({
+    url: '/wechat/customer/manage/clearDutyConfig/' + customerId,
+    method: 'delete',
+  })
+}
+
+// 分页查询客服数据
+export const pageQuery = (data) => {
+  return axios({
+    url: '/wechat/customer/manage/pageQuery',
+    method: 'post',
+    data
+  })
+}
+
+//获取所有客服
+export const getAll = () => {
+  return axios({
+    url: '/wechat/customer/manage/getAll',
+    method: 'get',
+  })
+}
+
+// 编辑客服
+export const getCustomer = (customerId) => {
+  return axios({
+    url: '/wechat/customer/manage/' + customerId,
+    method: 'get',
+  })
+}
+
+// 添加客服
+export const addCustomer = (data) => {
+  return axios({
+    url: '/wechat/customer/manage',
+    method: 'post',
+    data
+  })
+}
+
+// 删除客服
+export const deleteCustomer = (customerId) => {
+  return axios({
+    url: '/wechat/customer/manage/' + customerId,
+    method: 'delete',
+  })
+}
+
+// 修改客服
+export const reviseCustomer = (customerId, data) => {
+  return axios({
+    url: '/wechat/customer/manage/' + customerId,
+    method: 'put',
+    data
+  })
+}
+
+
+

+ 657 - 0
src/views/PeopleManage/Customer/CustomerServiceAccount.vue

@@ -0,0 +1,657 @@
+<script setup>
+import KtCommonTable from "@/views/Core/KtCommonTable.vue";
+import dayjs from "dayjs";
+
+export default {
+  components: {
+    KtCommonTable,
+  },
+  data() {
+    return {
+      pageResult: [],
+      queryCondition: {
+        nickname: "",
+        pageNum: 1,
+        pageSize: 10,
+      },
+      imageUrl: "",
+      dialogVisible: false,
+      dialogType: 'add',
+      uploadProgress: 0,
+      userLoading: false,
+      defaultId: "",
+      //提交
+      submitLoading: false,
+      // 表单数据
+      form: {
+        id: null,
+        nickname: '',
+        avatar: '',
+        userId: null,
+        departmentId: null,
+        receptionist: '',
+        receptionistId: null
+      },
+
+      dialogVisibleDuty: false,
+
+      allServiceList: [], // 从接口获取的客服列表
+      formData: {
+        nickname: '',
+        customerId: '',
+        dutyCustomerId: null,
+        dutyDateTimeList: [{
+          startTime: new Date().setHours(8, 30, 0),
+          endTime: new Date().setHours(18, 30, 0)
+        }]
+      },
+
+      // 选项数据
+      backendOptions: [],
+      departmentTree: [],
+      wecomUsers: [],
+
+      // 树配置
+      treeProps: {
+        label: 'deptName',
+        children: 'childDept',
+      },
+
+      // 验证规则
+      columns: [ //数据列
+        {prop: "avatarUrl", label: "客服头像", formatter: this.imgFormatter},
+        {prop: "nickname", label: "客服昵称"},
+        {prop: "username", label: "姓名"},
+        {prop: "userId", label: "工号"},
+        {prop: "receptionist", label: "企微用户"},
+        {prop: "dutyCustomer", label: "值班客服"},
+        {prop: "dutyDatetime", label: "值班时间", minWidth: 200, formatter: this.dutyDatetimeFormatter},
+      ],
+
+      rules: {
+        nickname: [
+          {required: true, message: '请输入客服昵称', trigger: 'blur'},
+          {min: 2, max: 16, message: '长度在2到16个字符', trigger: 'blur'}
+        ],
+        userId: [
+          {required: true, message: '请选择后线人员', trigger: 'change'},
+        ],
+        departmentId: [
+          {required: true, message: '请选择部门', trigger: 'change'}
+        ],
+        receptionistId: [
+          {required: true, message: '请选择企微用户', trigger: 'change'}
+        ]
+      },
+    }
+  },
+  created() {
+    this.uploadDefaultImage()
+    this.queryPage();
+  },
+  methods: {
+    changeDateTime(value) {
+      return dayjs(value).format("yyyy-MM-dd HH:mm:ss");
+    },
+    queryPage() {
+      this.$api.customerManage.pageQuery(this.queryCondition).then((res) => {
+        if (res.code === 200) {
+          this.pageResult = res.data
+          this.queryCondition.pageNum = res.data.pageNum
+          this.queryCondition.pageSize = res.data.pageSize
+        } else {
+          this.$message.error(res.msg)
+        }
+      })
+    },
+    findPage(data) {
+      if (data.pageRequest.pageNum !== this.queryCondition.pageNum) {
+        this.queryCondition.pageNum = data.pageRequest.pageNum;
+      }
+      if (data.pageRequest.pageSize !== this.queryCondition.pageSize) {
+        this.queryCondition.pageSize = data.pageRequest.pageSize;
+      }
+      this.queryPage();
+    },
+    // 删除客服
+    deleteCustomer({row}) {
+      this.$confirm('此操作将永久删除客服, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.$api.customerManage.deleteCustomer(row.id).then((res) => {
+          if (res.code === 200) {
+            this.$message({
+              type: 'success',
+              message: '删除成功!'
+            });
+            this.queryPage();
+          } else {
+            this.$message.error(res.msg);
+          }
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        });
+      });
+    },
+    // 清除值班配置
+    clearDutyConfig({row}) {
+      this.$confirm('此操作将永久清除值班配置, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.$api.customerManage.clearDutyConfig(row.id).then((res) => {
+          if (res.code === 200) {
+            this.$message({
+              type: 'success',
+              message: '清除成功!'
+            });
+            this.queryPage();
+          } else {
+            this.$message.error(res.msg);
+          }
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消清除'
+        });
+      });
+    },
+    handleClick() {
+      if (this.submitLoading) return;
+      this.submitLoading = true;
+      setTimeout(() => {
+        this.handleSubmit();
+        this.submitLoading = false;
+      }, 3000);
+    },
+    // 打开弹窗(新增)
+    handleAdd() {
+      this.dialogType = 'add'
+      this.dialogVisible = true
+      this.resetForm()
+      this.loadDepartment();
+      this.loadBackendOptions();
+    },
+    // 打开弹窗(编辑)
+    handleEdit({row}) {
+      this.dialogType = 'edit'
+      this.dialogVisible = true
+      this.$nextTick(() => {
+        this.loadEditData(row)
+        this.loadBackendOptions();
+      })
+    },
+    imgFormatter(row) {
+      return <img src={"" + process.env.BASE_URL + row.avatarUrl + ""} style="width: 74px; height: 74px"></img>;
+    },
+    dutyDatetimeFormatter(row) {
+      if (row.dutyDatetime != null && row.dutyDatetime.length > 0) {
+        return row.dutyDatetime.map(x => {
+          return <el-tag>{x} </el-tag>
+        })
+      }
+    },
+    // 加载编辑数据
+    loadEditData(data) {
+      this.loadDepartment()
+      this.$refs.deptTree.setCurrentKey(data.departmentId)
+      this.loadDepartmentUsers(data.departmentId)
+      // 加载部门路径
+      if (data.id) {
+        this.form = {
+          id: data.id,
+          nickname: data.nickname,
+          avatar: data.avatar,
+          userId: data.userId,
+          departmentId: data.departmentId,
+          receptionist: data.receptionist,
+          receptionistId: data.receptionistId
+        }
+        this.imageUrl = process.env.BASE_URL + data.avatarUrl
+      }
+    },
+    // 加载后线人员选项
+    loadBackendOptions() {
+      this.$api.user.findHxUserList().then((res) => {
+        this.backendOptions = res.data
+      })
+    },
+    // 部门加载
+    loadDepartment() {
+      this.$api.customerManage.getDeptTree().then((rep) => {
+        this.departmentTree = rep.data
+      })
+    },
+    // 部门点击事件
+    async handleDeptClick(data) {
+      this.form.departmentId = data.deptId
+      await this.loadDepartmentUsers(data.deptId)
+    },
+    // 加载部门用户
+    async loadDepartmentUsers(departmentId) {
+      this.userLoading = true
+      try {
+        const {data} = await this.$api.customerManage.getDeptUser(departmentId)
+        this.wecomUsers = data
+        const isReceptionistId = this.wecomUsers.some(user => user.userId === this.form.receptionistId);
+        if (!isReceptionistId) {
+          this.form.receptionistId = null;
+          this.form.receptionist = null;
+        }
+      } finally {
+        this.userLoading = false
+      }
+    },
+    // 上传图片
+    imageUpload(file) {
+      this.$refs.upload1.clearFiles();
+      const isImage = /\.(jpg|png)$/i.test(file.name)
+      const isLt2M = file.size / 1024 / 1024 < 2
+      if (!isImage) {
+        this.$message.error('只能上传png,jpg格式图片!')
+        return false
+      }
+      if (!isLt2M) {
+        this.$message.error('图片大小不能超过2MB!')
+        return false
+      }
+      let file1 = file.raw;
+      //转临时路径
+      this.imageUrl = window.URL.createObjectURL(file1);
+      const params = new FormData();
+      params.append("multipartFile", file1);
+      params.append("type", "avatar");
+      this.$api.letter.uploadFile(params).then((res) => {
+        if (res.code === 200) {
+          this.form.avatar = res.data.id;
+        } else {
+          this.$message.error(res.msg);
+        }
+      });
+      return true
+    },
+    async getLocalImageAsFile(imagePath) {
+      const response = await fetch(imagePath);
+      const blob = await response.blob();
+      return new File([blob], 'default-image.png', {type: blob.type});
+    },
+    //上传默认图片
+    async uploadDefaultImage() {
+      const imagePath = require('@/assets/customerAvatar.png'); // 本地图片路径
+      const file = await this.getLocalImageAsFile(imagePath);
+      const params = new FormData();
+      params.append("multipartFile", file);
+      params.append("type", "avatar");
+      this.$api.letter.uploadFile(params).then((res) => {
+        if (res.code === 200) {
+          this.defaultId = res.data.id;
+        }
+      });
+    },
+    // 表单提交
+    async handleSubmit() {
+      await this.$refs.formRef.validate()
+      if (this.form.avatar == null || this.form.avatar.length === 0) {
+        this.form.avatar = this.defaultId
+      }
+      this.form.receptionist = this.wecomUsers.filter((item) => item.userId == this.form.receptionistId)[0].name
+      if (this.dialogType === 'add') {
+        this.$api.customerManage.addCustomer(this.form).then(res => {
+          if (res.code === 200) {
+            this.$message.success('操作成功')
+            this.$emit('success')
+            this.dialogVisible = false
+            this.queryPage()
+          } else {
+            this.$message.error(res.msg);
+          }
+        })
+      } else if (this.dialogType === 'edit') {
+        this.$api.customerManage.reviseCustomer(this.form.id, this.form).then(res => {
+          if (res.code === 200) {
+            this.$message.success('操作成功')
+            this.$emit('success')
+            this.dialogVisible = false
+            this.queryPage()
+          } else {
+            this.$message.error(res.msg);
+          }
+        })
+      }
+    },
+    // 重置表单
+    resetForm() {
+      this.form = {
+        id: null,
+        nickname: '',
+        avatar: '',
+        avatarUrl: '',
+        userId: null,
+        departmentId: null,
+        receptionist: '',
+        receptionistId: null
+      }
+      this.imageUrl = ''
+    },
+    // 重置值班规则表单
+    resetFormData() {
+      this.formData = {
+        nickname: '',
+        customerId: '',
+        dutyCustomerId: null,
+        dutyDateTimeList: [{
+          startTime: new Date().setHours(8, 30, 0),
+          endTime: new Date().setHours(18, 30, 0)
+        }]
+      }
+    },
+    // 弹窗关闭处理
+    handleClose() {
+      this.resetForm()
+      this.uploadProgress = 0
+    },
+    // 获取客服列表
+    getCustomerServiceList() {
+      this.$api.customerManage.getAll().then(res => {
+        this.allServiceList = res.data
+      })
+    },
+
+    // 获取详情数据
+    getDutyConfig(customerId) {
+      this.$api.customerManage.getDutyConfig(customerId).then(res => {
+        if (res.code === 200) {
+          const isReceptionistId = this.allServiceList.some(service => service.id === Number(res.data.dutyCustomerId));
+          if (isReceptionistId) {
+            this.formData.dutyCustomerId = Number(res.data.dutyCustomerId)
+          }
+
+          res.data.dutyDateTimeList.map(item => {
+            this.startTime = dayjs(item.startTime).valueOf()
+            this.endTime = dayjs(item.endTime).valueOf()
+          })
+        }
+        this.formData.dutyDateTimeList = res.data.dutyDateTimeList
+      })
+    },
+    openDialog({row}) {
+      this.getCustomerServiceList()
+      this.getDutyConfig(row.id)
+      this.formData.customerId = row.id
+      this.formData.nickname = row.nickname
+      this.dialogVisibleDuty = true;
+    },
+    addTime() {
+      this.formData.dutyDateTimeList.push({
+        startTime: new Date().setHours(8, 30, 0),
+        endTime: new Date().setHours(18, 30, 0)
+      });
+    },
+    removeTime(index) {
+      if (this.formData.dutyDateTimeList.length === 1) {
+        this.$message.warning('请至少保留一条数据');
+        return;
+      }
+      this.formData.dutyDateTimeList.splice(index, 1);
+    },
+    handleCancel() {
+      this.resetFormData();
+      this.dialogVisibleDuty = false;
+      this.$message({type: 'info', message: '已取消操作'});
+    },
+    dutyConfigData() {
+      return {
+        customerId: this.formData.customerId,
+        dutyCustomerId: this.formData.dutyCustomerId,
+        dutyDateTimeList: this.formData.dutyDateTimeList.map(t => ({
+          startTime: dayjs(t.startTime).format('YYYY-MM-DD HH:mm:ss'),
+          endTime: dayjs(t.endTime).format('YYYY-MM-DD HH:mm:ss')
+        }))
+      }
+    },
+    // 确认操作
+    handleConfirm() {
+      // 数据校验
+      if (!this.validateTime()) return;
+      // 提交逻辑(示例)
+      this.$api.customerManage.dutyConfig(this.dutyConfigData())
+        .then((res) => {
+          if (res.code === 200) {
+            this.$message.success('保存成功');
+            this.dialogVisibleDuty = false;
+            this.queryPage()
+          } else {
+            this.$message.error(res.msg);
+          }
+        })
+        .catch(error => {
+          this.$message.error(`保存失败: ${error.message}`);
+        });
+    },
+
+    // 时间有效性校验
+    validateTime() {
+      for (const time of this.formData.dutyDateTimeList) {
+        if(time.startTime === null){
+          this.$message.error('开始时间必填');
+          return false;
+        }
+        if(time.endTime === null){
+          this.$message.error('结束时间必填');
+          return false;
+        }
+        if (new Date(time.startTime) >= new Date(time.endTime)) {
+          this.$message.error('结束时间必须晚于开始时间');
+          return false;
+        }
+      }
+      return true;
+    }
+  },
+  computed: {
+    showAddButton() {
+      return this.formData.dutyDateTimeList.length < 10;
+    }
+  },
+}
+</script>
+
+<template>
+
+  <div>
+    <div class="container">
+      <el-form size="small" label-width="130px">
+        <el-row>
+          <el-col :span="6">
+            <el-form-item label="昵称">
+              <el-input v-model="queryCondition.nickname" placeholder="请输入昵称" class="widths" clearable></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-button type="primary" size="small" @click="queryPage">查询</el-button>
+            <el-button type="primary" size="small" @click="handleAdd">添加客服</el-button>
+          </el-col>
+        </el-row>
+      </el-form>
+      <kt-common-table :showBatchDelete="false" :data="pageResult" :operationWidth="250" :columns="columns"
+                       @findPage="findPage"
+                       button2Name="编辑" @handleButton2="handleEdit" button2Type="text"
+                       button3Name="删除" @handleButton3="deleteCustomer" button3Type="text"
+                       button4Name="值班配置" @handleButton4="openDialog" button4Type="text"
+                       button5Name="清除值班配置" @handleButton5="clearDutyConfig" button5Type="text"
+      >
+      </kt-common-table>
+
+    </div>
+
+    <!-- 弹窗主体 -->
+    <el-dialog
+      :title="dialogType === 'add' ? '新增客服' : '编辑客服'"
+      :visible.sync="dialogVisible"
+      width="700px"
+      :close-on-click-modal="false"
+      @closed="handleClose">
+      <el-form
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="100px"
+        label-position="right">
+
+        <!-- 客服昵称 -->
+        <el-form-item label="客服昵称" prop="nickname">
+          <el-input
+            v-model.trim="form.nickname"
+            placeholder="请输入2-16个字符"
+            clearable
+            maxlength="16"/>
+        </el-form-item>
+
+        <!-- 客服头像 -->
+        <el-form-item label="客服头像" prop="avatar">
+          <el-upload
+            style="margin-bottom: 10px"
+            class="upload-demo"
+            action="#"
+            :limit="1"
+            accept=".png, .jpg, .jpeg"
+            :auto-upload="false"
+            :on-change="imageUpload"
+            :show-file-list="false"
+            list-type="picture-card"
+            ref="upload1"
+          >
+            <img v-if="imageUrl" :src="imageUrl" class="avatar" style="width: 148px; height: 148px" alt=""/>
+            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+          <span>支持png,jpg格式图片,大小不超过2MB</span>
+        </el-form-item>
+
+        <!-- 后线人员 -->
+        <el-form-item label="后线人员" prop="userId">
+          <el-select
+            v-model="form.userId"
+            filterable
+            clearable
+            placeholder="请选择后线人员">
+            <el-option
+              v-for="item in backendOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"/>
+          </el-select>
+        </el-form-item>
+
+        <!-- 企微部门树 -->
+        <el-form-item label="企微部门" prop="departmentId">
+          <el-tree
+            ref="deptTree"
+            :data="departmentTree"
+            :props="treeProps"
+            node-key="deptId"
+            highlight-current
+            @node-click="handleDeptClick"/>
+        </el-form-item>
+
+        <!-- 企微用户 -->
+        <el-form-item label="企微用户" prop="receptionistId">
+          <el-select
+            v-model="form.receptionistId"
+            :loading="userLoading"
+            filterable
+            clearable
+            placeholder="请先选择企微用户">
+            <el-option
+              v-for="user in wecomUsers"
+              :key="user.userId"
+              :label="user.name"
+              :value="user.userId"/>
+          </el-select>
+        </el-form-item>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button
+          type="primary"
+          :disabled="submitLoading"
+          @click="handleClick">
+          确 定
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 值班客服配置 -->
+    <el-dialog :visible.sync="dialogVisibleDuty" title="值班配置"
+               :close-on-click-modal="false">
+      <el-form :model="formData" label-width="100px" label-position="right">
+        <!-- 当前客服(只读) -->
+        <el-form-item label="当前客服">
+          <el-input v-model="formData.nickname" width="700px" clearable readonly></el-input>
+        </el-form-item>
+
+        <!-- 值班客服(可编辑) -->
+        <el-form-item label="值班客服">
+          <el-select v-model="formData.dutyCustomerId" placeholder="请选择">
+            <el-option
+              v-for="item in allServiceList"
+              :key="item.id"
+              :label="item.nickname"
+              :value="item.id">
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <!-- 值班时间列表 -->
+        <el-form-item label="值班时间" v-for="(time, index) in formData.dutyDateTimeList" :key="index">
+          <el-date-picker
+            v-model="time.startTime"
+            type="datetime"
+            format="yyyy-MM-dd HH:mm:ss"
+            placeholder="开始时间">
+          </el-date-picker>
+          <span>至</span>
+          <el-date-picker
+            v-model="time.endTime"
+            type="datetime"
+            format="yyyy-MM-dd HH:mm:ss"
+            placeholder="结束时间">
+          </el-date-picker>
+          <!-- 删除按钮(首行隐藏) -->
+          <el-button
+            v-if="index !== 0"
+            @click="removeTime(index)"
+            icon="el-icon-close"
+            circle>
+          </el-button>
+        </el-form-item>
+
+        <!-- 添加按钮(动态隐藏) -->
+        <el-button
+          v-if="showAddButton"
+          @click="addTime"
+          type="text"
+          icon="el-icon-plus">
+          添加值班时间
+        </el-button>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="handleCancel">取 消</el-button>
+        <el-button type="primary" @click="handleConfirm">确 认</el-button>
+      </div>
+    </el-dialog>
+  </div>
+
+</template>
+
+<style scoped>
+
+</style>