Jelajahi Sumber

修改流程图页面

吴树波 2 hari lalu
induk
melakukan
46682a26b4

+ 471 - 24
src/views/company/companyWorkflow/design.scss

@@ -1,8 +1,9 @@
 .workflow-designer {
-  height: 100vh;
+  height: calc(100vh - 84px);
   display: flex;
   flex-direction: column;
   background: #f5f5f5;
+  overflow: hidden;
 
   .toolbar {
     height: 50px;
@@ -26,51 +27,125 @@
   }
 
   .node-panel {
-    width: 220px;
+    width: 240px;
     background: #fff;
     border-right: 1px solid #e8e8e8;
     overflow-y: auto;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background: #f8f9fa;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #d0d0d0;
+      border-radius: 3px;
+      transition: background 0.2s;
+
+      &:hover {
+        background: #b0b0b0;
+      }
+    }
 
     .panel-title {
-      padding: 12px 16px;
+      padding: 16px 20px;
       font-weight: 600;
-      border-bottom: 1px solid #e8e8e8;
+      font-size: 15px;
+      background: #1890ff;
+      color: #fff;
+      border-bottom: none;
+      box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
     }
 
     .node-category {
+      margin-bottom: 4px;
+
       .category-title {
-        padding: 8px 16px;
+        padding: 12px 20px 8px;
         font-size: 12px;
-        color: #999;
+        font-weight: 600;
+        color: #666;
         background: #fafafa;
+        display: flex;
+        align-items: center;
+        position: relative;
+
+        &::before {
+          content: '';
+          display: inline-block;
+          width: 3px;
+          height: 12px;
+          background: #1890ff;
+          border-radius: 2px;
+          margin-right: 8px;
+        }
       }
 
       .node-list {
-        padding: 8px;
+        padding: 12px 16px;
+        background: #fff;
       }
 
       .node-item {
         display: flex;
         align-items: center;
-        padding: 8px 12px;
-        margin-bottom: 8px;
+        padding: 10px 14px;
+        margin-bottom: 10px;
         background: #fff;
-        border: 1px solid #e8e8e8;
-        border-radius: 4px;
+        border: 2px solid;
+        border-radius: 8px;
         cursor: grab;
-        transition: all 0.2s;
+        transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+        position: relative;
+        overflow: hidden;
+
+        &::before {
+          content: '';
+          position: absolute;
+          left: 0;
+          top: 0;
+          bottom: 0;
+          width: 4px;
+          background: var(--node-color, currentColor);
+          opacity: 0;
+          transition: opacity 0.25s;
+        }
 
         &:hover {
-          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+          transform: translateX(4px);
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+          
+          &::before {
+            opacity: 1;
+          }
+        }
+
+        &:active {
+          cursor: grabbing;
+          transform: scale(0.98) translateX(4px);
+        }
+
+        &:last-child {
+          margin-bottom: 0;
         }
 
         i {
           font-size: 18px;
-          margin-right: 8px;
+          margin-right: 10px;
+          flex-shrink: 0;
         }
 
         span {
           font-size: 13px;
+          font-weight: 500;
+          color: #333;
+          flex: 1;
         }
       }
     }
@@ -80,6 +155,99 @@
     flex: 1;
     position: relative;
     overflow: auto;
+    background: #f8f9fa;
+
+    &::-webkit-scrollbar {
+      width: 8px;
+      height: 8px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background: #f1f1f1;
+      border-radius: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #c1c1c1;
+      border-radius: 4px;
+      transition: background 0.2s;
+
+      &:hover {
+        background: #a8a8a8;
+      }
+    }
+
+    // 右键菜单样式
+    .context-menu {
+      position: absolute;
+      background: #fff;
+      border-radius: 8px;
+      box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+      padding: 6px 0;
+      min-width: 160px;
+      z-index: 1000;
+      user-select: none;
+
+      .menu-item {
+        padding: 8px 12px;
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        cursor: pointer;
+        transition: background 0.2s;
+        font-size: 13px;
+        color: #333;
+
+        &:hover {
+          background: #f5f7fa;
+        }
+
+        &.danger {
+          color: #f56c6c;
+
+          &:hover {
+            background: #fef0f0;
+          }
+        }
+
+        &.disabled {
+          color: #999;
+          cursor: not-allowed;
+
+          &:hover {
+            background: transparent;
+          }
+        }
+
+        i {
+          font-size: 14px;
+          width: 14px;
+          text-align: center;
+          flex-shrink: 0;
+        }
+
+        > span:first-of-type {
+          flex: 1;
+          white-space: nowrap;
+        }
+
+        .shortcut {
+          font-size: 11px;
+          color: #999;
+          font-family: 'Courier New', monospace;
+          background: #f5f5f5;
+          padding: 2px 6px;
+          border-radius: 3px;
+          flex-shrink: 0;
+        }
+      }
+
+      .menu-divider {
+        height: 1px;
+        background: #e8e8e8;
+        margin: 6px 0;
+      }
+    }
 
     .canvas-svg {
       min-width: 100%;
@@ -94,10 +262,17 @@
         cursor: move;
 
         rect {
-          transition: stroke-width 0.2s;
+          transition: stroke-width 0.2s, filter 0.2s;
 
           &.selected {
-            stroke-width: 3;
+            stroke-width: 4;
+            filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.25));
+          }
+        }
+
+        &.selected {
+          .node-name {
+            font-weight: 600;
           }
         }
 
@@ -140,10 +315,11 @@
         cursor: pointer;
 
         path {
-          transition: stroke-width 0.2s;
+          transition: all 0.2s;
 
           &.selected {
-            stroke-width: 3;
+            stroke-width: 4;
+            filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
           }
         }
       }
@@ -151,31 +327,302 @@
   }
 
   .property-panel {
-    width: 500px;
+    width: 380px;
     background: #fff;
     border-left: 1px solid #e8e8e8;
     overflow-y: auto;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background: #f8f9fa;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #d0d0d0;
+      border-radius: 3px;
+      transition: background 0.2s;
+
+      &:hover {
+        background: #b0b0b0;
+      }
+    }
 
     .panel-title {
-      padding: 12px 16px;
+      padding: 16px 20px;
       font-weight: 600;
-      border-bottom: 1px solid #e8e8e8;
+      font-size: 15px;
+      background: #1890ff;
+      color: #fff;
       display: flex;
       justify-content: space-between;
       align-items: center;
+      box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
+
+      span {
+        display: flex;
+        align-items: center;
+        
+        i {
+          margin-right: 8px;
+          font-size: 16px;
+        }
+      }
 
       .el-icon-close {
         cursor: pointer;
-        color: #999;
+        color: rgba(255, 255, 255, 0.8);
+        font-size: 18px;
+        transition: all 0.2s;
 
         &:hover {
-          color: #333;
+          color: #fff;
+          transform: rotate(90deg);
         }
       }
     }
 
+    .property-content {
+      flex: 1;
+      overflow-y: auto;
+    }
+
     .el-form {
-      padding: 16px;
+      padding: 0;
+    }
+
+    // 节点类型徽章样式
+    .node-type-badge {
+      display: inline-flex;
+      align-items: center;
+      padding: 8px 16px;
+      background: rgba(24, 144, 255, 0.08);
+      border: 1px solid rgba(24, 144, 255, 0.2);
+      border-radius: 20px;
+      font-size: 13px;
+      color: #333;
+      font-weight: 500;
+      transition: all 0.2s;
+
+      &:hover {
+        background: rgba(24, 144, 255, 0.15);
+        border-color: rgba(24, 144, 255, 0.3);
+        transform: translateY(-1px);
+        box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
+      }
+
+      i {
+        font-size: 16px;
+        margin-right: 8px;
+      }
+
+      span {
+        line-height: 1;
+      }
+    }
+
+    .property-section {
+      margin-bottom: 8px;
+      background: #fff;
+      border-radius: 8px;
+      overflow: hidden;
+
+      .section-title {
+        padding: 12px 20px;
+        margin-bottom: 16px;
+        font-size: 13px;
+        font-weight: 600;
+        color: #333;
+        background: #f7f8fa;
+        border-bottom: 1px solid #e8e8e8;
+        display: flex;
+        align-items: center;
+
+        i {
+          margin-right: 8px;
+          color: #1890ff;
+          font-size: 14px;
+        }
+      }
+
+      .el-form-item {
+        margin-bottom: 18px;
+        padding: 0 20px;
+
+        &:first-of-type {
+          margin-top: 4px;
+        }
+
+        &:last-child {
+          margin-bottom: 12px;
+        }
+      }
+    }
+
+    .form-row {
+      display: flex;
+      gap: 12px;
+      padding: 0 20px;
+
+      .el-form-item {
+        padding: 0;
+        
+        &.half-width {
+          flex: 1;
+        }
+      }
+    }
+
+    .property-actions {
+      padding: 16px 20px;
+      border-top: 1px solid #e8e8e8;
+      background: #fafafa;
+      display: flex;
+      gap: 10px;
+
+      .el-button {
+        flex: 1;
+        border-radius: 6px;
+        font-weight: 500;
+      }
+    }
+
+    // 表单样式优化
+    .el-input,
+    .el-select,
+    .el-textarea {
+      .el-input__inner,
+      .el-textarea__inner {
+        border-radius: 6px;
+        border-color: #e0e0e0;
+        transition: all 0.2s;
+
+        &:focus {
+          border-color: #1890ff;
+          box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
+        }
+      }
+    }
+
+    .el-input-number {
+      width: 100%;
+
+      .el-input__inner {
+        border-radius: 6px;
+        text-align: left;
+      }
+    }
+
+    .el-radio-group {
+      .el-radio {
+        margin-right: 20px;
+
+        &:last-child {
+          margin-right: 0;
+        }
+      }
+    }
+
+    .el-color-picker {
+      .el-color-picker__trigger {
+        border-radius: 6px;
+        border: 1px solid #e0e0e0;
+        transition: all 0.2s;
+
+        &:hover {
+          border-color: #667eea;
+        }
+      }
+    }
+
+    // 条件判断样式
+    .add-condition-btn {
+      margin-left: auto;
+      border-radius: 4px;
+      font-size: 12px;
+    }
+
+    .conditions-container {
+      padding: 12px 20px;
+    }
+
+    .condition-item {
+      background: #f9fafb;
+      border: 1px solid #e5e7eb;
+      border-radius: 8px;
+      padding: 12px;
+      margin-bottom: 12px;
+      transition: all 0.2s;
+
+      &:hover {
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+        border-color: #667eea;
+      }
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .condition-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 12px;
+        padding-bottom: 8px;
+        border-bottom: 1px dashed #e5e7eb;
+
+        .condition-number {
+          font-size: 13px;
+          font-weight: 600;
+          color: #667eea;
+          display: flex;
+          align-items: center;
+
+          &::before {
+            content: '';
+            display: inline-block;
+            width: 4px;
+            height: 4px;
+            background: #667eea;
+            border-radius: 50%;
+            margin-right: 6px;
+          }
+        }
+
+        .el-button {
+          padding: 6px;
+        }
+      }
+
+      .condition-content {
+        .el-form-item {
+          margin-bottom: 12px;
+          padding: 0;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
+        }
+      }
+    }
+
+    .empty-condition {
+      text-align: center;
+      padding: 32px 20px;
+      color: #999;
+      font-size: 13px;
+
+      i {
+        font-size: 32px;
+        color: #d0d0d0;
+        display: block;
+        margin-bottom: 8px;
+      }
     }
   }
 }

+ 479 - 97
src/views/company/companyWorkflow/design.vue

@@ -44,7 +44,7 @@
               :key="nodeType.typeCode"
               draggable="true"
               @dragstart="onDragStart($event, nodeType)"
-              :style="{ borderColor: nodeType.typeColor }"
+              :style="{ borderColor: nodeType.typeColor, '--node-color': nodeType.typeColor }"
             >
               <i :class="nodeType.typeIcon" :style="{ color: nodeType.typeColor }"></i>
               <span>{{ nodeType.typeName }}</span>
@@ -64,6 +64,11 @@
         @mousedown="onCanvasMouseDown"
         @keydown.delete="handleDelete"
         @keydown.backspace="handleDelete"
+        @keydown.ctrl.c="copyNode"
+        @keydown.meta.c="copyNode"
+        @keydown.ctrl.v="pasteNode"
+        @keydown.meta.v="pasteNode"
+        @contextmenu.prevent="showContextMenu"
       >
         <svg class="canvas-svg" ref="canvasSvg" :width="canvasSize.width" :height="canvasSize.height"
           :class="{ 'dragging-canvas': isDraggingCanvas }">
@@ -78,11 +83,11 @@
               markerWidth="10"
               markerHeight="10"
               refX="9"
-              refY="3"
+              refY="5"
               orient="auto"
-              markerUnits="strokeWidth"
+              markerUnits="userSpaceOnUse"
             >
-              <path d="M0,0 L0,6 L9,3 z" fill="#999" />
+              <path d="M0,1 L0,9 L9,5 z" fill="#999" />
             </marker>
             <!-- 选中状态的箭头 -->
             <marker
@@ -90,11 +95,11 @@
               markerWidth="10"
               markerHeight="10"
               refX="9"
-              refY="3"
+              refY="5"
               orient="auto"
-              markerUnits="strokeWidth"
+              markerUnits="userSpaceOnUse"
             >
-              <path d="M0,0 L0,6 L9,3 z" fill="#1890ff" />
+              <path d="M0,1 L0,9 L9,5 z" fill="#666" />
             </marker>
           </defs>
           <rect :width="canvasSize.width" :height="canvasSize.height" fill="url(#grid)" />
@@ -145,6 +150,7 @@
               v-for="node in nodes"
               :key="node.nodeKey"
               class="node-group"
+              :class="{ selected: selectedNode === node }"
               :transform="`translate(${node.posX}, ${node.posY})`"
               @mousedown="onNodeMouseDown($event, node)"
               @click.stop="selectNode(node)"
@@ -177,111 +183,290 @@
             </g>
           </g>
         </svg>
+
+        <!-- 右键菜单 -->
+        <div
+          v-show="contextMenu.visible"
+          class="context-menu"
+          :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
+          @click.stop
+        >
+          <div
+            v-if="contextMenu.node"
+            class="menu-item"
+            @click="copyNodeFromMenu"
+          >
+            <i class="el-icon-document-copy"></i>
+            <span>复制节点</span>
+            <span class="shortcut">Ctrl+C</span>
+          </div>
+          <div
+            v-if="copiedNode"
+            class="menu-item"
+            @click="pasteNodeFromMenu"
+          >
+            <i class="el-icon-document-add"></i>
+            <span>粘贴节点</span>
+            <span class="shortcut">Ctrl+V</span>
+          </div>
+          <div class="menu-divider" v-if="contextMenu.node && copiedNode"></div>
+          <div
+            v-if="contextMenu.node"
+            class="menu-item danger"
+            @click="deleteNodeFromMenu"
+          >
+            <i class="el-icon-delete"></i>
+            <span>删除节点</span>
+            <span class="shortcut">Delete</span>
+          </div>
+          <div
+            v-if="!contextMenu.node"
+            class="menu-item disabled"
+          >
+            <i class="el-icon-info"></i>
+            <span>请选中节点</span>
+          </div>
+        </div>
       </div>
 
       <!-- 右侧属性面板 -->
       <div class="property-panel" v-if="selectedNode || selectedEdge">
         <div class="panel-title">
-          {{ selectedNode ? '节点属性' : '连线属性' }}
+          <span>
+            <i :class="selectedNode ? 'el-icon-setting' : 'el-icon-share'"></i>
+            {{ selectedNode ? '节点属性' : '连线属性' }}
+          </span>
           <i class="el-icon-close" @click="clearSelection"></i>
         </div>
 
         <!-- 节点属性 -->
-        <el-form v-if="selectedNode" label-width="80px" size="small">
-          <el-form-item label="节点内容">
-            <el-input
-              v-model="selectedNode.nodeName"
-              type="textarea"
-              :autosize="{ minRows: 1, maxRows: 6 }"
-            />
-          </el-form-item>
-          <div v-if="selectedNode.nodeType == 'http'">
-            <el-form-item label="URL地址">
-              <el-input v-model="selectedNode.nodeConfig.url" />
-            </el-form-item>
-          </div>
-          <el-form-item label="节点类型">
-            <el-input :value="getNodeTypeName(selectedNode.nodeType)" disabled />
-          </el-form-item>
-          <el-form-item label="节点颜色">
-            <el-color-picker v-model="selectedNode.nodeColor" />
-          </el-form-item>
-          <el-form-item label="X坐标">
-            <el-input-number v-model="selectedNode.posX" :min="0" />
-          </el-form-item>
-          <el-form-item label="Y坐标">
-            <el-input-number v-model="selectedNode.posY" :min="0" />
-          </el-form-item>
-          <el-form-item>
-            <el-button type="danger" size="mini" @click="deleteNode">删除节点</el-button>
-          </el-form-item>
-        </el-form>
-
-        <!-- 连线属性 -->
-        <el-form v-if="selectedEdge" label-width="80px" size="small">
-          <el-form-item label="备注">
-            <el-input v-model="selectedEdge.edgeLabel" />
-          </el-form-item>
-          <el-form-item label="连线颜色">
-            <el-color-picker v-model="selectedEdge.edgeColor" />
-          </el-form-item>
-          <el-form-item v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK' || edgeSourceNode.nodeType == 'AI_SEND_MSG_TASK' || edgeSourceNode.nodeType == 'AI_ADD_WX_TASK'">
-            <el-button type="success" @click="addCondition">新增条件判断</el-button>
-          </el-form-item>
-          <div v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK'">
-            <div v-for="(item, index) in selectedEdge.conditionExprObj">
-              <el-form-item label="是否拨通">
-                <div style="display: flex;justify-content: space-between;">
-                  <el-select v-model="item.callConnected">
-                    <el-option :value="false" label="否" />
-                    <el-option :value="true" label="是" />
-                  </el-select>
-                  <el-button type="danger" size="mini" round @click="removeCondition(index)">删除</el-button>
+        <div v-if="selectedNode" class="property-content">
+          <el-form label-width="90px" size="small" label-position="left">
+            <!-- 基本信息 -->
+            <div class="property-section">
+              <div class="section-title">
+                <i class="el-icon-document"></i>基本信息
+              </div>
+              <el-form-item label="节点内容">
+                <el-input
+                  v-model="selectedNode.nodeName"
+                  type="textarea"
+                  :autosize="{ minRows: 2, maxRows: 4 }"
+                  placeholder="请输入节点内容"
+                />
+              </el-form-item>
+              <el-form-item label="节点类型">
+                <div class="node-type-badge">
+                  <i :class="selectedNode.nodeIcon" :style="{ color: selectedNode.nodeColor }"></i>
+                  <span>{{ getNodeTypeName(selectedNode.nodeType) }}</span>
                 </div>
               </el-form-item>
-              <el-form-item label="延迟时间" v-if="item.callConnected == false">
-                <el-input v-model="item.callTime" style="width: 180px">
-                  <template slot="append">分钟</template>
-                </el-input>
+            </div>
+
+            <!-- AI外呼配置 -->
+            <div v-if="selectedNode.nodeType == 'AI_CALL_TASK'" class="property-section">
+              <div class="section-title">
+                <i class="el-icon-phone"></i>外呼配置
+              </div>
+              <el-form-item label="机器人">
+                <el-select v-model="selectedNode.nodeConfig.robot" filterable placeholder="请选择机器人">
+                  <el-option v-for="item in robotList" :key="item.id" :label="item.name + '('+item.num+')'" :value="item.id"/>
+                </el-select>
+              </el-form-item>
+              <el-form-item label="话术">
+                <el-select v-model="selectedNode.nodeConfig.dialogId" filterable placeholder="请选择话术">
+                  <el-option v-for="item in dialogList" :key="item.id" :label="item.name" :value="item.id"/>
+                </el-select>
               </el-form-item>
-              <el-form-item label="意向度">
-                <el-select v-model="item.intention" placeholder="意向等级" filterable clearable>
-                  <el-option v-for="item in levelList" :label="item.dictLabel" :value="item.dictValue"/>
+              <el-form-item label="主叫分组">
+                <el-select v-model="selectedNode.nodeConfig.cidGroupId" filterable placeholder="请选择主叫分组">
+                  <el-option v-for="item in CIDGroupList" :key="item.id" :label="item.name" :value="item.id"/>
                 </el-select>
               </el-form-item>
-            </div>
-          </div>
-          <div v-if="edgeSourceNode.nodeType == 'AI_SEND_MSG_TASK'">
-            <div v-for="(item, index) in selectedEdge.conditionExprObj">
-              <el-form-item label="添加状态">
-                <div style="display: flex;justify-content: space-between;">
-                  <el-select v-model="item.isAdd">
-                    <el-option :value="false" label="未同意" />
-                    <el-option :value="true" label="已同意" />
+              <el-form-item label="模式">
+                <el-select v-model="selectedNode.nodeConfig.mode" placeholder="请选择模式">
+                  <el-option label="呼叫机器人后挂断" :value="7"/>
+                </el-select>
+              </el-form-item>
+              
+              <div class="form-row">
+                <el-form-item label="呼叫倍率" class="half-width">
+                  <el-select v-model="selectedNode.nodeConfig.multiplier" @change="handleConfigChange" placeholder="选择倍率">
+                    <el-option label="1" :value="1"/>
+                    <el-option label="2" :value="2"/>
+                    <el-option label="3" :value="3"/>
+                    <el-option label="4" :value="4"/>
+                    <el-option label="5" :value="5"/>
                   </el-select>
-                  <el-button type="danger" size="mini" round @click="removeCondition(index)">删除</el-button>
-                </div>
+                </el-form-item>
+              </div>
+
+              <el-form-item label="自动重呼">
+                <el-radio-group v-model="selectedNode.nodeConfig.autoRecall" @change="handleConfigChange">
+                  <el-radio :label="0">否</el-radio>
+                  <el-radio :label="1">是</el-radio>
+                </el-radio-group>
               </el-form-item>
-              <el-form-item label="超时时间" v-if="item.isAdd == false">
-                <el-input v-model="item.addTime" style="width: 180px">
-                  <template slot="append">分钟</template>
-                </el-input>
+              
+              <el-form-item label="重呼次数" v-if="selectedNode.nodeConfig.autoRecall == 1">
+                <el-select v-model="selectedNode.nodeConfig.recallTimes" @change="handleConfigChange" placeholder="选择重呼次数">
+                  <el-option label="不自动重呼" :value="0"/>
+                  <el-option label="1次" :value="1"/>
+                  <el-option label="2次" :value="2"/>
+                  <el-option label="3次" :value="3"/>
+                  <el-option label="4次" :value="4"/>
+                  <el-option label="5次" :value="5"/>
+                </el-select>
               </el-form-item>
             </div>
-          </div>
-          <div v-if="edgeSourceNode.nodeType == 'AI_ADD_WX_TASK'">
 
-          </div>
-          <el-form-item>
-            <el-button type="danger" size="mini" @click="deleteEdge">删除连线</el-button>
-          </el-form-item>
-        </el-form>
+            <!-- 样式设置 -->
+            <div class="property-section">
+              <div class="section-title">
+                <i class="el-icon-brush"></i>样式设置
+              </div>
+              <el-form-item label="节点颜色">
+                <el-color-picker v-model="selectedNode.nodeColor" show-alpha />
+              </el-form-item>
+              <!-- <div class="form-row">
+                <el-form-item label="X坐标" class="half-width">
+                  <el-input-number v-model="selectedNode.posX" :min="0" controls-position="right" />
+                </el-form-item>
+                <el-form-item label="Y坐标" class="half-width">
+                  <el-input-number v-model="selectedNode.posY" :min="0" controls-position="right" />
+                </el-form-item>
+              </div> -->
+            </div>
+
+            <!-- 操作按钮 -->
+            <div class="property-actions">
+              <el-button type="primary" size="small" icon="el-icon-document-copy" @click="copyNode">复制节点</el-button>
+              <el-button type="danger" size="small" icon="el-icon-delete" @click="deleteNode">删除节点</el-button>
+            </div>
+          </el-form>
+        </div>
+
+        <!-- 连线属性 -->
+        <div v-if="selectedEdge" class="property-content">
+          <el-form label-width="90px" size="small" label-position="left">
+            <!-- 基本信息 -->
+            <div class="property-section">
+              <div class="section-title">
+                <i class="el-icon-share"></i>连线信息
+              </div>
+              <el-form-item label="备注">
+                <el-input v-model="selectedEdge.edgeLabel" placeholder="请输入备注" />
+              </el-form-item>
+              <el-form-item label="连线颜色">
+                <el-color-picker v-model="selectedEdge.edgeColor" show-alpha />
+              </el-form-item>
+            </div>
+
+            <!-- 条件判断 -->
+            <div v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK' || edgeSourceNode.nodeType == 'AI_SEND_MSG_TASK' || edgeSourceNode.nodeType == 'AI_ADD_WX_TASK'" class="property-section">
+              <div class="section-title">
+                <i class="el-icon-set-up"></i>条件判断
+                <el-button type="success" size="mini" icon="el-icon-plus" @click="addCondition" class="add-condition-btn">新增条件</el-button>
+              </div>
+
+              <!-- AI外呼任务条件 -->
+              <div v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK'" class="conditions-container">
+                <div v-for="(item, index) in selectedEdge.conditionExprObj" :key="index" class="condition-item">
+                  <div class="condition-header">
+                    <span class="condition-number">条件 {{ index + 1 }}</span>
+                    <el-button type="danger" size="mini" icon="el-icon-delete" circle @click="removeCondition(index)"></el-button>
+                  </div>
+                  <div class="condition-content">
+                    <el-form-item label="是否拨通">
+                      <el-select v-model="item.callConnected" placeholder="请选择">
+                        <el-option :value="false" label="否" />
+                        <el-option :value="true" label="是" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="延迟时间" v-if="item.callConnected == false">
+                      <el-input v-model="item.callTime" placeholder="请输入时间">
+                        <template slot="append">分钟</template>
+                      </el-input>
+                    </el-form-item>
+                    <el-form-item label="意向度">
+                      <el-select v-model="item.intention" placeholder="请选择意向等级" filterable clearable>
+                        <el-option v-for="level in levelList" :key="level.dictValue" :label="level.dictLabel" :value="level.dictValue"/>
+                      </el-select>
+                    </el-form-item>
+                  </div>
+                </div>
+                <div v-if="selectedEdge.conditionExprObj.length === 0" class="empty-condition">
+                  <i class="el-icon-info"></i>
+                  <span>暂无条件,请点击上方按钮添加</span>
+                </div>
+              </div>
+
+              <!-- 加微信任务条件 -->
+              <div v-if="edgeSourceNode.nodeType == 'AI_SEND_MSG_TASK'" class="conditions-container">
+                <div v-for="(item, index) in selectedEdge.conditionExprObj" :key="index" class="condition-item">
+                  <div class="condition-header">
+                    <span class="condition-number">条件 {{ index + 1 }}</span>
+                    <el-button type="danger" size="mini" icon="el-icon-delete" circle @click="removeCondition(index)"></el-button>
+                  </div>
+                  <div class="condition-content">
+                    <el-form-item label="添加状态">
+                      <el-select v-model="item.isAdd" placeholder="请选择">
+                        <el-option :value="false" label="未同意" />
+                        <el-option :value="true" label="已同意" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="超时时间" v-if="item.isAdd == false">
+                      <el-input v-model="item.addTime" placeholder="请输入时间">
+                        <template slot="append">分钟</template>
+                      </el-input>
+                    </el-form-item>
+                  </div>
+                </div>
+                <div v-if="selectedEdge.conditionExprObj.length === 0" class="empty-condition">
+                  <i class="el-icon-info"></i>
+                  <span>暂无条件,请点击上方按钮添加</span>
+                </div>
+              </div>
+
+              <!-- 其他任务类型条件 -->
+              <div v-if="edgeSourceNode.nodeType == 'AI_ADD_WX_TASK'" class="conditions-container">
+                <div class="empty-condition">
+                  <i class="el-icon-info"></i>
+                  <span>此类型暂不支持条件设置</span>
+                </div>
+              </div>
+            </div>
+
+            <!-- 操作按钮 -->
+            <div class="property-actions">
+              <el-button type="danger" size="small" icon="el-icon-delete" @click="deleteEdge">删除连线</el-button>
+            </div>
+          </el-form>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script>
+import {
+  listRobotic,
+  getRobotic,
+  delRobotic,
+  addRobotic,
+  updateRobotic,
+  exportRobotic,
+  calleesList,
+  statusList,
+  startRobotic,
+  stopRobotic,
+  companyUserList,
+  wxList,
+  taskRun,
+  getTypes,
+  getSmsTempList,
+  getCIDGroupList
+} from "@/api/company/companyVoiceRobotic";
 import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes } from '@/api/company/companyWorkflow'
 import {getDicts} from "@/api/system/dict/data";
 
@@ -334,6 +519,15 @@ export default {
       isDraggingCanvas: false,
       // 画布拖动起始点
       canvasDragStart: { x: 0, y: 0 },
+      // 复制的节点
+      copiedNode: null,
+      // 右键菜单
+      contextMenu: {
+        visible: false,
+        x: 0,
+        y: 0,
+        node: null
+      },
       // 工作流类型字典
       workflowTypeOptions: [
         { dictValue: '1', dictLabel: '对话流程' },
@@ -342,6 +536,9 @@ export default {
       ],
       conditionExprTemp:{
       },
+      CIDGroupList:[],
+      robotList: [],
+      dialogList: [],
     }
   },
   created() {
@@ -361,11 +558,14 @@ export default {
     // window.addEventListener('keydown', this.handleGlobalKeydown)
     document.addEventListener('mousemove', this.onMouseMove)
     document.addEventListener('mouseup', this.onMouseUp)
+    // 点击任意地方隐藏右键菜单
+    document.addEventListener('click', this.hideContextMenu)
   },
   beforeDestroy() {
     // window.removeEventListener('keydown', this.handleGlobalKeydown)
     document.removeEventListener('mousemove', this.onMouseMove)
     document.removeEventListener('mouseup', this.onMouseUp)
+    document.removeEventListener('click', this.hideContextMenu)
   },
   methods: {
     // 聚焦画布容器
@@ -373,6 +573,12 @@ export default {
       this.$refs.canvasContainer.focus()
     },
 
+    // 处理配置变化,强制更新视图
+    handleConfigChange() {
+      // 强制触发Vue响应式更新
+      this.$forceUpdate()
+    },
+
     // 点击画布时聚焦
     onCanvasClick() {
       this.clearSelection()
@@ -382,9 +588,46 @@ export default {
     // 点击节点时聚焦
     selectNode(node) {
       this.selectedNode = node
-      // this.selectedNode.nodeConfig = JSON.parse(node.nodeConfig)
+      // 将nodeConfig从字符串解析为对象
+      if (typeof this.selectedNode.nodeConfig === 'string') {
+        try {
+          this.selectedNode.nodeConfig = JSON.parse(this.selectedNode.nodeConfig)
+        } catch (e) {
+          this.selectedNode.nodeConfig = {}
+        }
+      }
+      // 确保nodeConfig是对象
+      if (!this.selectedNode.nodeConfig || typeof this.selectedNode.nodeConfig !== 'object') {
+        this.selectedNode.nodeConfig = {}
+      }
+      
       this.selectedEdge = null
       this.edgeSourceNode = null
+      if(this.selectedNode.nodeType == "AI_CALL_TASK"){
+        // 使用$set初始化默认值,确保响应式
+        if (!this.selectedNode.nodeConfig.mode) {
+          this.$set(this.selectedNode.nodeConfig, 'mode', 7)
+        }
+        if (!this.selectedNode.nodeConfig.multiplier) {
+          this.$set(this.selectedNode.nodeConfig, 'multiplier', 1)
+        }
+        if (this.selectedNode.nodeConfig.autoRecall === undefined) {
+          this.$set(this.selectedNode.nodeConfig, 'autoRecall', 0)
+        }
+        if (!this.selectedNode.nodeConfig.recallTimes && this.selectedNode.nodeConfig.recallTimes !== 0) {
+          this.$set(this.selectedNode.nodeConfig, 'recallTimes', 0)
+        }
+        
+        getTypes().then(e => {
+          this.robotList = e.robot;
+          this.dialogList = e.dialog;
+        })
+        getCIDGroupList().then(res=>{
+          this.CIDGroupList = res.data;
+        }).catch(res=>{
+          console.log(res);
+        });
+      }
       this.focusCanvasContainer()
     },
 
@@ -518,6 +761,23 @@ export default {
             console.info(edge)
           })
         }
+        // 处理nodes,将nodeConfig从字符串转为对象
+        if(data.nodes){
+          data.nodes.forEach(node => {
+            if (typeof node.nodeConfig === 'string') {
+              try {
+                // 使用Object.assign创建新对象,确保响应式
+                const parsedConfig = JSON.parse(node.nodeConfig)
+                node.nodeConfig = Object.assign({}, parsedConfig)
+              } catch (e) {
+                node.nodeConfig = {}
+              }
+            }
+            if (!node.nodeConfig || typeof node.nodeConfig !== 'object') {
+              node.nodeConfig = {}
+            }
+          })
+        }
         this.nodes = data.nodes || []
         this.edges = data.edges || []
       })
@@ -544,6 +804,96 @@ export default {
     generateKey() {
       return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
     },
+    /** 复制节点 */
+    copyNode(e) {
+      if (!this.selectedNode) {
+        return
+      }
+      e.preventDefault()
+      // 深拷贝选中的节点,包括所有属性
+      this.copiedNode = JSON.parse(JSON.stringify(this.selectedNode))
+      this.$message.success('节点已复制')
+    },
+    /** 粘贴节点 */
+    pasteNode(e) {
+      if (!this.copiedNode) {
+        this.$message.warning('没有可粘贴的节点')
+        return
+      }
+      e.preventDefault()
+      
+      // 创建新节点,复制所有属性
+      const newNode = {
+        ...this.copiedNode,
+        nodeKey: this.generateKey(),
+        // 在原位置偏移一定距离
+        posX: this.copiedNode.posX + 30,
+        posY: this.copiedNode.posY + 30,
+        // 深拷贝 nodeConfig
+        nodeConfig: this.copiedNode.nodeConfig ? JSON.parse(JSON.stringify(this.copiedNode.nodeConfig)) : {}
+      }
+      
+      this.nodes.push(newNode)
+      // 选中新节点
+      this.selectedNode = newNode
+      this.selectedEdge = null
+      
+      // 检查是否需要扩展画布
+      this.checkAndExpandCanvas(newNode)
+      
+      this.$message.success('节点已粘贴')
+    },
+    /** 显示右键菜单 */
+    showContextMenu(e) {
+      // 获取鼠标相对于画布容器的位置
+      const rect = this.$refs.canvasContainer.getBoundingClientRect()
+      this.contextMenu.x = e.clientX - rect.left
+      this.contextMenu.y = e.clientY - rect.top
+      this.contextMenu.node = this.selectedNode
+      this.contextMenu.visible = true
+    },
+    /** 隐藏右键菜单 */
+    hideContextMenu() {
+      this.contextMenu.visible = false
+    },
+    /** 从菜单复制节点 */
+    copyNodeFromMenu() {
+      if (this.contextMenu.node) {
+        this.copiedNode = JSON.parse(JSON.stringify(this.contextMenu.node))
+        this.$message.success('节点已复制')
+      }
+      this.hideContextMenu()
+    },
+    /** 从菜单粘贴节点 */
+    pasteNodeFromMenu() {
+      if (!this.copiedNode) {
+        this.$message.warning('没有可粘贴的节点')
+        return
+      }
+      
+      const newNode = {
+        ...this.copiedNode,
+        nodeKey: this.generateKey(),
+        posX: this.copiedNode.posX + 30,
+        posY: this.copiedNode.posY + 30,
+        nodeConfig: this.copiedNode.nodeConfig ? JSON.parse(JSON.stringify(this.copiedNode.nodeConfig)) : {}
+      }
+      
+      this.nodes.push(newNode)
+      this.selectedNode = newNode
+      this.selectedEdge = null
+      this.checkAndExpandCanvas(newNode)
+      
+      this.$message.success('节点已粘贴')
+      this.hideContextMenu()
+    },
+    /** 从菜单删除节点 */
+    deleteNodeFromMenu() {
+      if (this.contextMenu.node) {
+        this.deleteNode()
+      }
+      this.hideContextMenu()
+    },
     /** 拖拽开始 */
     onDragStart(e, nodeType) {
       e.dataTransfer.setData('nodeType', JSON.stringify(nodeType))
@@ -566,7 +916,7 @@ export default {
         posX: Math.round(x - 50),
         posY: Math.round(y - 20),
         height: 40,
-        nodeConfig: nodeType.defaultConfig || '{}'
+        nodeConfig: {} // 初始化为空对象
       }
       this.nodes.push(newNode)
       this.selectNode(newNode)
@@ -626,8 +976,8 @@ export default {
         const scrollTop = this.$refs.canvasContainer.scrollTop
         const x = (e.clientX - rect.left + scrollLeft - this.canvasOffset.x) / this.scale - this.dragOffset.x
         const y = (e.clientY - rect.top + scrollTop - this.canvasOffset.y) / this.scale - this.dragOffset.y
-        this.draggingNode.posX = Math.max(0, Math.round(x))
-        this.draggingNode.posY = Math.max(0, Math.round(y))
+        this.draggingNode.posX = Math.max(0, x)
+        this.draggingNode.posY = Math.max(0, y)
         // 检测并扩展画布
         this.checkAndExpandCanvas(this.draggingNode)
       }
@@ -751,49 +1101,59 @@ export default {
       const dx = x2 - x1
       const dy = y2 - y1
       const distance = Math.sqrt(dx * dx + dy * dy)
-      
-      // 根据锚点位置计算控制点偏移量
-      let offset = Math.min(distance * 0.5, 100)
-      
+
+      // 根据锚点位置计算控制点偏移量,动态调整
+      let offset = Math.min(distance * 0.4, 80)
+
       let cp1x = x1, cp1y = y1
       let cp2x = x2, cp2y = y2
-      
+
       // 根据源节点锚点方向设置第一个控制点
       switch(sourceAnchor) {
         case 'top':
           cp1y = y1 - offset
+          cp1x = x1
           break
         case 'bottom':
           cp1y = y1 + offset
+          cp1x = x1
           break
         case 'left':
           cp1x = x1 - offset
+          cp1y = y1
           break
         case 'right':
           cp1x = x1 + offset
+          cp1y = y1
           break
         default:
           cp1y = y1 + offset
+          cp1x = x1
       }
       
       // 根据目标节点锚点方向设置第二个控制点
       switch(targetAnchor) {
         case 'top':
           cp2y = y2 - offset
+          cp2x = x2
           break
         case 'bottom':
           cp2y = y2 + offset
+          cp2x = x2
           break
         case 'left':
           cp2x = x2 - offset
+          cp2y = y2
           break
         case 'right':
           cp2x = x2 + offset
+          cp2y = y2
           break
         default:
           cp2y = y2 - offset
+          cp2x = x2
       }
-      
+
       return `M ${x1} ${y1} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`
     },
     /** 获取连线标签位置 */
@@ -808,6 +1168,28 @@ export default {
     /** 获取节点背景色 */
     getNodeBgColor(node) {
       const color = node.nodeColor || '#1890ff'
+      // 处理各种颜色格式
+      if (color.startsWith('rgba')) {
+        // rgba 格式,替换透明度
+        return color.replace(/[\d.]+\)$/g, '0.08)')
+      } else if (color.startsWith('rgb')) {
+        // rgb 格式,转为 rgba
+        return color.replace('rgb', 'rgba').replace(')', ', 0.08)')
+      } else if (color.startsWith('#')) {
+        // 十六进制格式
+        // 处理 3 位和 6 位十六进制
+        let hex = color.slice(1)
+        if (hex.length === 3) {
+          hex = hex.split('').map(c => c + c).join('')
+        }
+        if (hex.length === 6) {
+          return `#${hex}15`
+        } else if (hex.length === 8) {
+          // 已经带透明度,替换为 15
+          return `#${hex.slice(0, 6)}15`
+        }
+      }
+      // 默认返回原颜色 + 15
       return color + '15'
     },