Explorar el Código

feat(complaint): 新增投诉回复功能及相关配置

yz hace 3 semanas
padre
commit
ec7ab62c90

+ 133 - 0
src/api/complaint/reply.js

@@ -0,0 +1,133 @@
+import request from '@/router/axios';
+
+/**
+ * 投诉回复查询参数类型定义
+ * @typedef {Object} ComplaintReplyQueryParams
+ * @property {string} complaintId - 投诉ID
+ * @property {number} [current] - 当前页码
+ * @property {number} [size] - 每页大小
+ */
+
+/**
+ * 投诉回复表单数据类型定义
+ * @typedef {Object} ComplaintReplyForm
+ * @property {string} [id] - 回复ID(修改时必填)
+ * @property {string} complaintId - 投诉ID
+ * @property {string} complaintNo - 投诉单号
+ * @property {number} replyType - 回复类型 1-系统回复 2-客户反馈 3-申诉
+ * @property {string} replyContent - 回复内容
+ * @property {string} [replyAttachUrl] - 回复附件URL
+ * @property {string} [replierId] - 回复人ID
+ * @property {string} [replierName] - 回复人姓名
+ * @property {string} [replyTime] - 回复时间
+ * @property {string} [createUser] - 创建用户ID
+ * @property {string} [createDept] - 创建部门ID
+ * @property {string} [createTime] - 创建时间
+ * @property {string} [updateUser] - 更新用户ID
+ * @property {string} [updateTime] - 更新时间
+ * @property {number} [status] - 状态
+ * @property {number} [isDeleted] - 是否删除
+ */
+
+/**
+ * 投诉回复列表项类型定义
+ * @typedef {Object} ComplaintReplyRecord
+ * @property {string} id - 回复ID
+ * @property {string} createUser - 创建用户ID
+ * @property {string} createDept - 创建部门ID
+ * @property {string} createTime - 创建时间
+ * @property {string} updateUser - 更新用户ID
+ * @property {string} updateTime - 更新时间
+ * @property {number} status - 状态
+ * @property {number} isDeleted - 是否删除
+ * @property {string} complaintId - 投诉ID
+ * @property {string} complaintNo - 投诉单号
+ * @property {number} replyType - 回复类型 1-系统回复 2-客户反馈 3-申诉
+ * @property {string} replyContent - 回复内容
+ * @property {string|null} replyAttachUrl - 回复附件URL
+ * @property {string} replierId - 回复人ID
+ * @property {string} replierName - 回复人姓名
+ * @property {string} replyTime - 回复时间
+ */
+
+/**
+ * 分页结果类型定义
+ * @typedef {Object} PageResult
+ * @property {ComplaintReplyRecord[]} records - 数据列表
+ * @property {number} total - 总记录数
+ * @property {number} size - 每页大小
+ * @property {number} current - 当前页码
+ * @property {Array} orders - 排序信息
+ * @property {boolean} optimizeCountSql - 是否优化count查询
+ * @property {boolean} hitCount - 是否命中count缓存
+ * @property {string|null} countId - count查询ID
+ * @property {number|null} maxLimit - 最大限制
+ * @property {boolean} searchCount - 是否查询count
+ * @property {number} pages - 总页数
+ */
+
+/**
+ * 获取投诉回复列表
+ * @param {ComplaintReplyQueryParams} params - 查询参数
+ * @returns {Promise<AxiosResponse<PageResult>>} 分页结果
+ */
+export const getReplyList = (params) => {
+  return request({
+    url: '/api/blade-factory/api/factory/complaint-reply',
+    method: 'get',
+    params
+  });
+};
+
+/**
+ * 获取投诉回复详情
+ * @param {string} replyId - 回复ID
+ * @returns {Promise<AxiosResponse<ComplaintReplyRecord>>} 回复详情
+ */
+export const getReplyDetail = (replyId) => {
+  return request({
+    url: `/api/blade-factory/api/factory/complaint-reply/${replyId}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增投诉回复
+ * @param {ComplaintReplyForm} row - 回复表单数据
+ * @returns {Promise<AxiosResponse<boolean>>} 操作结果
+ */
+export const addReply = (row) => {
+  return request({
+    url: '/api/blade-factory/api/factory/complaint-reply',
+    method: 'post',
+    data: row
+  });
+};
+
+/**
+ * 修改投诉回复
+ * @param {ComplaintReplyForm} row - 回复表单数据
+ * @returns {Promise<AxiosResponse<boolean>>} 操作结果
+ */
+export const updateReply = (row) => {
+  return request({
+    url: '/api/blade-factory/api/factory/complaint-reply',
+    method: 'put',
+    data: row
+  });
+};
+
+/**
+ * 删除投诉回复
+ * @param {string} ids - 回复ID列表,多个用逗号分隔
+ * @returns {Promise<AxiosResponse<boolean>>} 操作结果
+ */
+export const removeReply = (ids) => {
+  return request({
+    url: '/api/blade-factory/api/factory/complaint-reply',
+    method: 'delete',
+    params: {
+      ids
+    }
+  });
+};

+ 86 - 0
src/constants/complaint.js

@@ -66,6 +66,20 @@ export const REPLY_STATUS = {
 }
 
 /**
+ * 回复类型枚举
+ * @readonly
+ * @enum {number}
+ */
+export const REPLY_TYPE = {
+  /** 系统回复 */
+  SYSTEM: 1,
+  /** 客户反馈 */
+  CUSTOMER: 2,
+  /** 申诉 */
+  APPEAL: 3
+}
+
+/**
  * 投诉人类型配置映射
  * @readonly
  * @type {Record<number, {label: string, type: string, color: string}>}
@@ -173,6 +187,29 @@ export const REPLY_STATUS_CONFIG = {
 }
 
 /**
+ * 回复类型配置映射
+ * @readonly
+ * @type {Record<number, {label: string, type: string, color: string}>}
+ */
+export const REPLY_TYPE_CONFIG = {
+  [REPLY_TYPE.SYSTEM]: {
+    label: '系统回复',
+    type: 'primary',
+    color: '#409EFF'
+  },
+  [REPLY_TYPE.CUSTOMER]: {
+    label: '客户反馈',
+    type: 'success',
+    color: '#67C23A'
+  },
+  [REPLY_TYPE.APPEAL]: {
+    label: '申诉',
+    type: 'warning',
+    color: '#E6A23C'
+  }
+}
+
+/**
  * 投诉人类型选项数据
  * @readonly
  * @type {Array<{label: string, value: number}>}
@@ -220,6 +257,17 @@ export const REPLY_STATUS_OPTIONS = [
 ]
 
 /**
+ * 回复类型选项数据
+ * @readonly
+ * @type {Array<{label: string, value: number}>}
+ */
+export const REPLY_TYPE_OPTIONS = [
+  { label: '系统回复', value: REPLY_TYPE.SYSTEM },
+  { label: '客户反馈', value: REPLY_TYPE.CUSTOMER },
+  { label: '申诉', value: REPLY_TYPE.APPEAL }
+]
+
+/**
  * 获取投诉人类型标签
  * @param {number} complainantType - 投诉人类型值
  * @returns {string} 投诉人类型标签
@@ -400,6 +448,44 @@ export function getAllComplaintStatusValues() {
 }
 
 /**
+ * 获取回复类型标签
+ * @param {number} replyType - 回复类型值
+ * @returns {string} 回复类型标签
+ */
+export function getReplyTypeLabel(replyType) {
+  const config = REPLY_TYPE_CONFIG[replyType]
+  return config ? config.label : '未知类型'
+}
+
+/**
+ * 获取回复类型Element UI标签类型
+ * @param {number} replyType - 回复类型值
+ * @returns {string} Element UI标签类型
+ */
+export function getReplyTypeType(replyType) {
+  const config = REPLY_TYPE_CONFIG[replyType]
+  return config ? config.type : 'info'
+}
+
+/**
+ * 获取回复类型颜色
+ * @param {number} replyType - 回复类型值
+ * @returns {string} 十六进制颜色值
+ */
+export function getReplyTypeColor(replyType) {
+  const config = REPLY_TYPE_CONFIG[replyType]
+  return config ? config.color : '#909399'
+}
+
+/**
+ * 获取所有回复类型值
+ * @returns {Array<number>} 回复类型值数组
+ */
+export function getAllReplyTypeValues() {
+  return Object.values(REPLY_TYPE)
+}
+
+/**
  * 获取所有回复状态值
  * @returns {Array<number>} 回复状态值数组
  */

+ 192 - 10
src/views/complaint/complaintMixin.js

@@ -1,10 +1,23 @@
-import { getList, add, update, remove, getDetail, updateStatus, batchUpdateStatus } from '@/api/complaint'
+import {
+  getList,
+  add,
+  update,
+  remove,
+  getDetail,
+  updateStatus,
+  batchUpdateStatus
+} from '@/api/complaint'
+import {
+  getReplyList,
+  addReply
+} from '@/api/complaint/reply'
 import { mapGetters } from 'vuex'
 import {
   COMPLAINANT_TYPE_OPTIONS,
   COMPLAINT_TYPE_OPTIONS,
   COMPLAINT_STATUS_OPTIONS,
   REPLY_STATUS_OPTIONS,
+  REPLY_TYPE_OPTIONS,
   getComplainantTypeLabel,
   getComplaintTypeLabel,
   getComplainantTypeType,
@@ -12,8 +25,10 @@ import {
   getComplaintStatusType,
   getReplyStatusLabel,
   getReplyStatusType,
+  getReplyTypeLabel,
+  getReplyTypeType,
+  getReplyTypeColor,
   isComplaintEditable,
-  // getComplainantTypeText,
   isComplaintProcessable,
   isComplaintClosable,
   isValidComplaintStatus,
@@ -83,6 +98,30 @@ export default {
           { required: true, message: '请输入关闭原因', trigger: 'blur' }
         ]
       },
+      // 回复列表相关数据
+      replyListVisible: false,
+      replyList: [],
+      replyListLoading: false,
+      replyPage: {
+        current: 1,
+        size: 10
+      },
+      replyTotal: 0,
+      currentComplaintId: '',
+      currentComplaintNo: '',
+      // 新增回复相关数据
+      addReplyVisible: false,
+      replyForm: {
+        complaintId: '',
+        replyType: 1,
+        replyContent: '',
+        replyAttachUrl: ''
+      },
+      replyRules: {
+        replyType: [{ required: true, message: '请选择回复类型', trigger: 'change' }],
+        replyContent: [{ required: true, message: '请输入回复内容', trigger: 'blur' }]
+      },
+      addReplyLoading: false,
       option: {
         height: 'auto',
         calcHeight: 30,
@@ -203,6 +242,13 @@ export default {
         ids.push(ele.id)
       })
       return ids.join(',')
+    },
+    /**
+     * 回复类型选项
+     * @returns {Array<{label: string, value: number}>} 回复类型选项数组
+     */
+    replyTypeOptions() {
+      return REPLY_TYPE_OPTIONS
     }
   },
   methods: {
@@ -406,10 +452,11 @@ export default {
     handleProcess(row) {
       this.statusDialogTitle = '处理投诉'
       this.statusForm = {
-        status: 2,
+        id: row.id,
+        status: 1,
         closeReason: ''
       }
-      this.currentIds = [row.id]
+      this.currentIds = null
       this.statusVisible = true
     },
 
@@ -420,14 +467,147 @@ export default {
     handleClose(row) {
       this.statusDialogTitle = '关闭投诉'
       this.statusForm = {
-        status: 4,
+        id: row.id,
+        status: 3,
         closeReason: ''
       }
-      this.currentIds = [row.id]
+      this.currentIds = null
       this.statusVisible = true
     },
 
     /**
+     * 打开回复列表
+     * @param {Object} row - 投诉数据行
+     */
+    async handleReplyList(row) {
+      this.currentComplaintId = row.id
+      this.currentComplaintNo = row.complaintNo
+      this.replyListVisible = true
+      this.replyPage.current = 1
+      await this.loadReplyList()
+    },
+
+    /**
+     * 加载回复列表
+     */
+    async loadReplyList() {
+      if (!this.currentComplaintId) return
+      
+      this.replyListLoading = true
+      try {
+        const params = {
+          current: this.replyPage.current,
+          size: this.replyPage.size,
+          complaintId: this.currentComplaintId
+        }
+        
+        const response = await getReplyList(params)
+        const { records, total } = response.data.data
+        
+        this.replyList = records || []
+        this.replyTotal = total || 0
+      } catch (error) {
+        this.$message.error('获取回复列表失败')
+        console.error('获取回复列表失败:', error)
+      } finally {
+        this.replyListLoading = false
+      }
+    },
+
+    /**
+     * 回复列表分页大小变化
+     * @param {number} size - 新的分页大小
+     */
+    async handleReplyPageSizeChange(size) {
+      this.replyPage.size = size
+      this.replyPage.current = 1
+      await this.loadReplyList()
+    },
+
+    /**
+     * 回复列表页码变化
+     * @param {number} current - 新的页码
+     */
+    async handleReplyPageChange(current) {
+      this.replyPage.current = current
+      await this.loadReplyList()
+    },
+
+    /**
+     * 打开新增回复对话框
+     */
+    handleAddReply() {
+      this.replyForm = {
+        complaintId: this.currentComplaintId,
+        complaintNo: this.currentComplaintNo,
+        replyType: 1,
+        replyContent: '',
+        replyAttachUrl: ''
+      }
+      this.addReplyVisible = true
+      this.$nextTick(() => {
+        this.$refs.replyForm && this.$refs.replyForm.clearValidate()
+      })
+    },
+
+    /**
+     * 确认新增回复
+     */
+    async confirmAddReply() {
+      try {
+        const valid = await this.$refs.replyForm.validate()
+        if (!valid) return
+        
+        this.addReplyLoading = true
+        
+        const formData = {
+          ...this.replyForm
+        }
+        
+        const response = await addReply(formData)
+        
+        if (response.data.success) {
+          this.$message.success('新增回复成功')
+          this.addReplyVisible = false
+          
+          // 重新加载回复列表
+          await this.loadReplyList()
+          
+          // 重新加载投诉列表以更新回复状态
+          this.onLoad(this.page)
+        } else {
+          this.$message.error(response.data.msg || '新增回复失败')
+        }
+      } catch (error) {
+        this.$message.error('新增回复失败')
+        console.error('新增回复失败:', error)
+      } finally {
+        this.addReplyLoading = false
+      }
+    },
+
+    /**
+     * 获取回复类型标签
+     * @param {number} replyType - 回复类型
+     * @returns {string} 回复类型标签
+     */
+    getReplyTypeLabel,
+
+    /**
+     * 获取回复类型Element UI标签类型
+     * @param {number} replyType - 回复类型
+     * @returns {string} Element UI标签类型
+     */
+    getReplyTypeType,
+
+    /**
+     * 获取回复类型颜色
+     * @param {number} replyType - 回复类型
+     * @returns {string} 回复类型颜色
+     */
+    getReplyTypeColor,
+
+    /**
      * 批量状态处理
      */
     handleBatchStatus() {
@@ -437,7 +617,7 @@ export default {
       }
       this.statusDialogTitle = '批量处理投诉'
       this.statusForm = {
-        status: 2,
+        status: 1,
         closeReason: ''
       }
       this.currentIds = this.selectionList.map(item => item.id)
@@ -453,10 +633,12 @@ export default {
         this.statusLoading = true
 
         let res
-        if (this.currentIds.length === 1) {
-          res = await updateStatus(this.currentIds[0], this.statusForm.status, this.statusForm.closeReason)
-        } else {
+        if (this.currentIds && this.currentIds.length > 0) {
+          // 批量处理
           res = await batchUpdateStatus(this.currentIds, this.statusForm.status, this.statusForm.closeReason)
+        } else {
+          // 单个处理
+          res = await updateStatus(this.statusForm.id, this.statusForm.status, this.statusForm.closeReason)
         }
 
         if (res.data.success) {

+ 153 - 5
src/views/complaint/index.vue

@@ -80,6 +80,14 @@
         <el-button
           type="text"
           size="small"
+          icon="el-icon-chat-line-round"
+          @click="handleReplyList(row)"
+        >
+          回复列表
+        </el-button>
+        <el-button
+          type="text"
+          size="small"
           icon="el-icon-edit"
           v-if="permission.complaint_edit && isComplaintEditable(row.status)"
           @click="handleEdit(row)"
@@ -236,6 +244,123 @@
       </div>
     </el-dialog>
 
+    <!-- 回复列表对话框 -->
+    <el-dialog
+      title="回复列表"
+      :visible.sync="replyListVisible"
+      width="1000px"
+      append-to-body
+      :close-on-click-modal="false"
+      :destroy-on-close="true"
+      class="reply-list-dialog"
+    >
+      <div class="reply-list-header">
+        <el-button
+          type="primary"
+          size="small"
+          icon="el-icon-plus"
+          @click="handleAddReply"
+        >
+          新增回复
+        </el-button>
+      </div>
+      
+      <el-table
+        :data="replyList"
+        v-loading="replyListLoading"
+        border
+        stripe
+        style="margin-top: 15px;"
+      >
+        <el-table-column label="回复类型" width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag
+              :type="getReplyTypeType(scope.row.replyType)"
+              size="small"
+            >
+              {{ getReplyTypeLabel(scope.row.replyType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="回复内容" prop="replyContent" min-width="300">
+          <template slot-scope="scope">
+            <div class="reply-content">
+              {{ scope.row.replyContent }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="回复人" prop="replierName" width="120" align="center"></el-table-column>
+        <el-table-column label="回复时间" prop="replyTime" width="160" align="center"></el-table-column>
+        <el-table-column label="创建时间" prop="createTime" width="160" align="center"></el-table-column>
+      </el-table>
+      
+      <el-pagination
+        v-if="replyTotal > 0"
+        @size-change="handleReplyPageSizeChange"
+        @current-change="handleReplyPageChange"
+        :current-page="replyPage.current"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="replyPage.size"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="replyTotal"
+        style="margin-top: 15px; text-align: right;"
+      >
+      </el-pagination>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="replyListVisible = false">关闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新增回复对话框 -->
+    <el-dialog
+      title="新增回复"
+      :visible.sync="addReplyVisible"
+      width="600px"
+      append-to-body
+      :close-on-click-modal="false"
+      :destroy-on-close="true"
+    >
+      <el-form
+        :model="replyForm"
+        :rules="replyRules"
+        ref="replyForm"
+        label-width="100px"
+      >
+        <el-form-item label="回复类型" prop="replyType">
+          <el-select v-model="replyForm.replyType" placeholder="请选择回复类型" style="width: 100%">
+            <el-option
+              v-for="item in replyTypeOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="回复内容" prop="replyContent">
+          <el-input
+            type="textarea"
+            :rows="5"
+            v-model="replyForm.replyContent"
+            placeholder="请输入回复内容"
+            maxlength="1000"
+            show-word-limit
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="附件URL" prop="replyAttachUrl">
+          <el-input
+            v-model="replyForm.replyAttachUrl"
+            placeholder="请输入附件URL(可选)"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="addReplyVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmAddReply" :loading="addReplyLoading">确定</el-button>
+      </div>
+    </el-dialog>
+
     <!-- 状态处理对话框 -->
     <el-dialog
       :title="statusDialogTitle"
@@ -247,13 +372,13 @@
       <el-form :model="statusForm" :rules="statusRules" ref="statusForm" label-width="100px">
         <el-form-item label="新状态" prop="status">
           <el-select v-model="statusForm.status" placeholder="请选择状态" style="width: 100%">
-            <el-option label="待处理" :value="1"></el-option>
-            <el-option label="处理中" :value="2"></el-option>
-            <el-option label="已完成" :value="3"></el-option>
-            <el-option label="已关闭" :value="4"></el-option>
+            <el-option label="待处理" :value="0"></el-option>
+            <el-option label="处理中" :value="1"></el-option>
+            <el-option label="已回复" :value="2"></el-option>
+            <el-option label="已关闭" :value="3"></el-option>
           </el-select>
         </el-form-item>
-        <el-form-item label="关闭原因" prop="closeReason" v-if="statusForm.status !== 4">
+        <el-form-item label="关闭原因" prop="closeReason" v-if="statusForm.status === 3">
           <el-input
             type="textarea"
             :rows="3"
@@ -301,4 +426,27 @@ export default {
     }
   }
 }
+
+.reply-list-dialog {
+  .reply-list-header {
+    display: flex;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+
+  .reply-content {
+    word-break: break-word;
+    line-height: 1.5;
+    max-height: 100px;
+    overflow-y: auto;
+  }
+
+  .el-table {
+    font-size: 14px;
+
+    .el-table__cell {
+      padding: 12px 0;
+    }
+  }
+}
 </style>