吴树波 1 день назад
Родитель
Сommit
780e6bb3b9

+ 9 - 0
src/api/company/companyVoiceRobotic.js

@@ -140,3 +140,12 @@ export function getCIDGroupList(params) {
     params
   })
 }
+
+// 查询任务执行记录
+export function getExecRecords(roboticId) {
+  return request({
+    url: '/company/companyVoiceRobotic/execRecords',
+    method: 'get',
+    params: { roboticId }
+  })
+}

+ 508 - 2
src/views/company/companyVoiceRobotic/index.vue

@@ -120,6 +120,13 @@
             @click="wxOpen(scope.row.id)"
             v-hasPermi="['system:companyVoiceRobotic:list']"
           >加微管理</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-document"
+            @click="showExecLogs(scope.row)"
+            v-hasPermi="['system:companyVoiceRobotic:list']"
+          >执行日志</el-button>
           <el-button
             size="mini"
             type="text"
@@ -317,6 +324,147 @@
         @pagination="getWxList"
       />
     </el-drawer>
+
+    <!-- 执行日志对话框 -->
+    <el-drawer
+      title="任务执行日志"
+      size="75%"
+      :visible.sync="execLogs.show"
+      append-to-body
+      class="exec-logs-drawer">
+      <div class="exec-logs-container" v-loading="execLogs.loading">
+        <div v-if="execLogs.list.length === 0" class="empty-logs">
+          <i class="el-icon-document"></i>
+          <p>暂无执行记录</p>
+        </div>
+
+        <div v-else>
+          <!-- 统计信息卡片 -->
+          <el-card class="stats-card" shadow="never">
+            <div class="stats-grid">
+              <div class="stat-item">
+                <i class="el-icon-user" style="color: #1890ff;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">客户总数</div>
+                  <div class="stat-value">{{ execLogs.list.length }}</div>
+                </div>
+              </div>
+              <div class="stat-item">
+                <i class="el-icon-phone" style="color: #52c41a;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">已打电话</div>
+                  <div class="stat-value">{{ execLogs.stats.callDone }}</div>
+                </div>
+              </div>
+              <div class="stat-item">
+                <i class="el-icon-chat-line-square" style="color: #faad14;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">已加微信</div>
+                  <div class="stat-value">{{ execLogs.stats.addWxDone }}</div>
+                </div>
+              </div>
+              <div class="stat-item">
+                <i class="el-icon-message" style="color: #722ed1;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">已发短信</div>
+                  <div class="stat-value">{{ execLogs.stats.sendMsgDone }}</div>
+                </div>
+              </div>
+            </div>
+          </el-card>
+
+          <!-- 执行记录列表 -->
+          <div class="logs-list">
+            <el-collapse v-model="execLogs.activeNames" accordion>
+              <el-collapse-item
+                v-for="(record, index) in execLogs.list"
+                :key="index"
+                :name="index">
+                <template slot="title">
+                  <div class="record-header">
+                    <div class="record-left">
+                      <el-avatar :size="48" icon="el-icon-user-solid" :style="{background: getAvatarColor(index)}"></el-avatar>
+                      <div class="customer-detail">
+                        <div class="customer-name-row">
+                          <span class="customer-name">{{ record.customerName }}</span>
+                          <el-tag
+                            :type="getWorkflowStatusType(record.workflowStatus)"
+                            size="small"
+                            class="status-tag">
+                            {{ record.workflowStatusName }}
+                          </el-tag>
+                        </div>
+                        <div class="customer-meta">
+                          <span class="meta-item phone">
+                            <i class="el-icon-phone-outline"></i>
+                            {{ record.customerPhone }}
+                          </span>
+                          <span class="meta-divider">|</span>
+                          <span class="meta-item node">
+                            <i class="el-icon-s-operation"></i>
+                            当前节点:<strong>{{ record.currentNodeTypeName }}</strong>
+                          </span>
+                        </div>
+                      </div>
+                    </div>
+                    <div class="record-right">
+                      <div class="progress-info">
+                        <span class="progress-label">执行进度</span>
+                        <span class="progress-value">{{ getProgress(record) }}%</span>
+                      </div>
+                      <el-progress
+                        :percentage="getProgress(record)"
+                        :color="getProgressColor(record)"
+                        :stroke-width="10"
+                        :show-text="false"></el-progress>
+                    </div>
+                  </div>
+                </template>
+
+                <!-- 节点执行日志 -->
+                <div class="node-logs">
+                  <el-timeline>
+                    <el-timeline-item
+                      v-for="(log, logIndex) in record.nodeLogs"
+                      :key="logIndex"
+                      :timestamp="log.startTime"
+                      :color="getNodeStatusColor(log.statusName)"
+                      placement="top">
+                      <el-card class="node-log-card">
+                        <div class="node-log-header">
+                          <div class="node-info">
+                            <i :class="getNodeIcon(log.nodeKey)" class="node-icon"></i>
+                            <span class="node-name">{{ log.nodeName }}</span>
+                          </div>
+                          <el-tag
+                            :type="getStatusTagType(log.statusName)"
+                            size="mini">
+                            {{ log.statusName }}
+                          </el-tag>
+                        </div>
+                        <div class="node-log-body" v-if="log.errorMsg">
+                          <div class="error-msg">
+                            <i class="el-icon-warning"></i>
+                            {{ log.errorMsg }}
+                          </div>
+                        </div>
+                        <div class="node-log-footer">
+                          <span class="duration" v-if="log.duration">
+                            <i class="el-icon-time"></i>
+                            耗时: {{ formatDuration(log.duration) }}
+                          </span>
+                        </div>
+                      </el-card>
+                    </el-timeline-item>
+                  </el-timeline>
+                </div>
+              </el-collapse-item>
+            </el-collapse>
+          </div>
+        </div>
+      </div>
+    </el-drawer>
+
     <el-drawer size="75%" title="客户详情" :visible.sync="customerDetailShow" append-to-body>
       <customer-details ref="customerDetails" />
     </el-drawer>
@@ -340,7 +488,8 @@ import {
   taskRun,
   getTypes,
   getSmsTempList,
-  getCIDGroupList
+  getCIDGroupList,
+  getExecRecords
 } from "@/api/company/companyVoiceRobotic";
 import draggable from 'vuedraggable'
 import { listAll } from '@/api/company/wxDialog';
@@ -447,6 +596,17 @@ export default {
           pageSize: 10,
         },
       },
+      execLogs: {
+        show: false,
+        loading: false,
+        list: [],
+        activeNames: [],
+        stats: {
+          callDone: 0,
+          addWxDone: 0,
+          sendMsgDone: 0
+        }
+      },
       // 表单校验
       rules: {
         name: [
@@ -732,6 +892,99 @@ export default {
     },
     removeQwUser(index){
       this.form.qwUser.splice(index, 1)
+    },
+    // 打开执行日志
+    async showExecLogs(row) {
+      this.execLogs.show = true;
+      this.execLogs.loading = true;
+      this.execLogs.list = [];
+
+      try {
+        const res = await getExecRecords(row.id);
+        this.execLogs.list = res.data || [];
+
+        // 计算统计数据
+        this.execLogs.stats = {
+          callDone: this.execLogs.list.reduce((sum, r) => sum + (r.callPhoneDone || 0), 0),
+          addWxDone: this.execLogs.list.reduce((sum, r) => sum + (r.addWxDone || 0), 0),
+          sendMsgDone: this.execLogs.list.reduce((sum, r) => sum + (r.sendMsgDone || 0), 0)
+        };
+      } catch (error) {
+        console.error('获取执行日志失败:', error);
+        this.$message.error('获取执行日志失败');
+      } finally {
+        this.execLogs.loading = false;
+      }
+    },
+    // 获取工作流状态类型
+    getWorkflowStatusType(status) {
+      const typeMap = {
+        1: '',
+        2: 'success',
+        3: 'warning',
+        4: 'danger'
+      };
+      return typeMap[status] || 'info';
+    },
+    // 获取进度
+    getProgress(record) {
+      if (!record.nodeLogs || record.nodeLogs.length === 0) return 0;
+      const total = record.nodeLogs.length;
+      const completed = record.nodeLogs.filter(log => log.statusName === '执行成功').length;
+      return Math.round((completed / total) * 100);
+    },
+    // 获取进度条颜色
+    getProgressColor(record) {
+      const progress = this.getProgress(record);
+      if (progress === 100) return '#52c41a';
+      if (progress >= 50) return '#1890ff';
+      return '#faad14';
+    },
+    // 获取节点状态颜色
+    getNodeStatusColor(statusName) {
+      const colorMap = {
+        '执行成功': '#52c41a',
+        '执行中': '#1890ff',
+        '执行失败': '#f5222d',
+        '等待执行': '#d9d9d9'
+      };
+      return colorMap[statusName] || '#d9d9d9';
+    },
+    // 获取状态标签类型
+    getStatusTagType(statusName) {
+      const typeMap = {
+        '执行成功': 'success',
+        '执行中': 'warning',
+        '执行失败': 'danger',
+        '等待执行': 'info'
+      };
+      return typeMap[statusName] || 'info';
+    },
+    // 获取节点图标
+    getNodeIcon(nodeKey) {
+      const iconMap = {
+        'node_start': 'el-icon-video-play',
+        'node_addwx': 'el-icon-chat-line-square',
+        'node_call': 'el-icon-phone',
+        'node_sms': 'el-icon-message',
+        'node_end': 'el-icon-circle-check'
+      };
+      return iconMap[nodeKey] || 'el-icon-document';
+    },
+    // 格式化时长
+    formatDuration(ms) {
+      if (!ms) return '-';
+      if (ms < 1000) return ms + 'ms';
+      const seconds = Math.floor(ms / 1000);
+      if (seconds < 60) return seconds + 's';
+      const minutes = Math.floor(seconds / 60);
+      const remainSeconds = seconds % 60;
+      return minutes + 'm' + remainSeconds + 's';
+    },
+    // 获取头像颜色
+    getAvatarColor(index) {
+      const colors = ['#1890ff', '#52c41a', '#faad14', '#722ed1', '#eb2f96'];
+      return colors[index % colors.length];
     }
   }
 };
@@ -770,7 +1023,260 @@ export default {
   position: absolute;
 }
 .sortable-ghost{
-  //background: #FFF !important;
+  /* background: #FFF !important; */
   background: rgb(217, 236, 255) !important;
 }
+
+/* 执行日志样式 */
+.exec-logs-container {
+  padding: 20px;
+  background: #f5f7fa;
+  min-height: calc(100vh - 60px);
+}
+
+.exec-logs-container ::v-deep .el-card {
+  border: none;
+  box-shadow: none;
+}
+
+.empty-logs {
+  text-align: center;
+  padding: 80px 20px;
+  color: #909399;
+}
+
+.empty-logs i {
+  font-size: 64px;
+  margin-bottom: 16px;
+  color: #dcdfe6;
+}
+
+.empty-logs p {
+  font-size: 16px;
+}
+
+/* 统计卡片 */
+.stats-card {
+  margin-bottom: 20px;
+  border-radius: 8px;
+  border: none;
+}
+
+.stats-card ::v-deep .el-card__body {
+  padding: 20px;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 20px;
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px;
+  background: #f9fafb;
+  border-radius: 6px;
+}
+
+.stat-item i {
+  font-size: 32px;
+}
+
+.stat-content {
+  flex: 1;
+}
+
+.stat-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  font-size: 24px;
+  font-weight: 600;
+  color: #303133;
+}
+
+/* 日志列表 */
+.logs-list {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+  border: none;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+.el-collapse-item__header{
+  height: auto !important;
+}
+/* 记录头部 */
+.record-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  padding: 20px 16px;
+  gap: 32px;
+}
+
+.record-left {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  flex: 1;
+  min-width: 0;
+}
+
+.customer-detail {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.customer-name-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.customer-name {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.4;
+}
+
+.status-tag {
+  flex-shrink: 0;
+}
+
+.customer-meta {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.4;
+}
+
+.meta-item {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.meta-item i {
+  font-size: 14px;
+  color: #909399;
+}
+
+.meta-item.node strong {
+  color: #1890ff;
+  font-weight: 600;
+}
+
+.meta-divider {
+  color: #dcdfe6;
+}
+
+.record-right {
+  width: 200px;
+  flex-shrink: 0;
+}
+
+.progress-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.progress-label {
+  font-size: 13px;
+  color: #909399;
+}
+
+.progress-value {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+/* 节点日志 */
+.node-logs {
+  padding: 20px;
+  background: #fafafa;
+  border-radius: 6px;
+}
+
+.node-log-card {
+  margin-bottom: 0;
+}
+
+.node-log-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.node-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.node-icon {
+  font-size: 18px;
+  color: #1890ff;
+}
+
+.node-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.node-log-body {
+  margin: 12px 0;
+}
+
+.error-msg {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  background: #fff2e8;
+  border-left: 3px solid #faad14;
+  border-radius: 4px;
+  font-size: 13px;
+  color: #d46b08;
+}
+
+.error-msg i {
+  font-size: 16px;
+}
+
+.node-log-footer {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  font-size: 13px;
+  color: #909399;
+}
+
+.duration {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.duration i {
+  font-size: 14px;
+}
 </style>