Ver Fonte

feat: 优化顶部标题栏和重构节点面板 UI

顶部标题栏优化:
- 添加渐变背景和阴影效果
- 优化项目名称和状态标签样式
- 优化保存状态指示器(圆点+背景)
- 按钮添加圆角、渐变背景、hover 动画
- Active 状态按钮使用蓝色渐变高亮

节点面板重构:
- 添加 Tab 分类展示(来源、动作、要素)
- Tab 切换使用渐变背景和图标
- 移除折叠组,改为扁平化展示
- 优化 section 标题样式
- 统一节点卡片样式
何文松 há 22 horas atrás
pai
commit
7e07250477

+ 136 - 81
frontend/vue-demo/src/components/workflow/panels/NodePanel.vue

@@ -6,6 +6,7 @@ const props = defineProps({
   elements: { type: Array, default: () => [] }
 })
 
+const activeTab = ref('source')
 const searchQuery = ref('')
 const expandedGroups = ref(['source', 'action', 'element'])
 
@@ -88,6 +89,34 @@ function onDragStartElement(event, element) {
       <span class="panel-title">节点面板</span>
     </div>
     
+    <!-- Tab 切换 -->
+    <div class="panel-tabs">
+      <div 
+        class="tab-item" 
+        :class="{ active: activeTab === 'source' }"
+        @click="activeTab = 'source'"
+      >
+        <span class="tab-icon">📦</span>
+        <span class="tab-label">来源</span>
+      </div>
+      <div 
+        class="tab-item" 
+        :class="{ active: activeTab === 'action' }"
+        @click="activeTab = 'action'"
+      >
+        <span class="tab-icon">⚡</span>
+        <span class="tab-label">动作</span>
+      </div>
+      <div 
+        class="tab-item" 
+        :class="{ active: activeTab === 'element' }"
+        @click="activeTab = 'element'"
+      >
+        <span class="tab-icon">🏷️</span>
+        <span class="tab-label">要素</span>
+      </div>
+    </div>
+    
     <div class="panel-search">
       <el-input
         v-model="searchQuery"
@@ -99,14 +128,10 @@ function onDragStartElement(event, element) {
     </div>
     
     <div class="panel-content">
-      <!-- 来源节点组 -->
-      <div class="node-group">
-        <div class="group-header" @click="toggleGroup('source')">
-          <span class="group-icon">{{ expandedGroups.includes('source') ? '▼' : '▶' }}</span>
-          <span class="group-title">来源节点</span>
-          <span class="group-count">{{ sourceNodes.length }}</span>
-        </div>
-        <div class="group-content" v-show="expandedGroups.includes('source')">
+      <!-- 来源节点 Tab -->
+      <div v-show="activeTab === 'source'" class="tab-content">
+        <div class="node-section">
+          <div class="section-title">基础来源</div>
           <div
             v-for="node in sourceNodes"
             :key="node.subType"
@@ -122,16 +147,28 @@ function onDragStartElement(event, element) {
             </div>
           </div>
         </div>
+        
+        <div class="node-section" v-if="attachments.length > 0">
+          <div class="section-title">项目附件 <span class="section-count">{{ filteredAttachments.length }}</span></div>
+          <div
+            v-for="att in filteredAttachments"
+            :key="att.id"
+            class="node-item attachment-item"
+            draggable="true"
+            @dragstart="onDragStartAttachment($event, att)"
+          >
+            <span class="node-icon">📎</span>
+            <div class="node-info">
+              <span class="node-label">{{ att.displayName || att.fileName }}</span>
+            </div>
+          </div>
+        </div>
       </div>
       
-      <!-- 动作节点组 -->
-      <div class="node-group">
-        <div class="group-header" @click="toggleGroup('action')">
-          <span class="group-icon">{{ expandedGroups.includes('action') ? '▼' : '▶' }}</span>
-          <span class="group-title">动作节点</span>
-          <span class="group-count">{{ actionNodes.length }}</span>
-        </div>
-        <div class="group-content" v-show="expandedGroups.includes('action')">
+      <!-- 动作节点 Tab -->
+      <div v-show="activeTab === 'action'" class="tab-content">
+        <div class="node-section">
+          <div class="section-title">可用动作</div>
           <div
             v-for="node in actionNodes"
             :key="node.subType"
@@ -149,37 +186,10 @@ function onDragStartElement(event, element) {
         </div>
       </div>
       
-      <!-- 附件列表 -->
-      <div class="node-group" v-if="attachments.length > 0">
-        <div class="group-header" @click="toggleGroup('attachments')">
-          <span class="group-icon">{{ expandedGroups.includes('attachments') ? '▼' : '▶' }}</span>
-          <span class="group-title">项目附件</span>
-          <span class="group-count">{{ filteredAttachments.length }}</span>
-        </div>
-        <div class="group-content" v-show="expandedGroups.includes('attachments')">
-          <div
-            v-for="att in filteredAttachments"
-            :key="att.id"
-            class="node-item attachment-item"
-            draggable="true"
-            @dragstart="onDragStartAttachment($event, att)"
-          >
-            <span class="node-icon">📎</span>
-            <div class="node-info">
-              <span class="node-label">{{ att.displayName || att.fileName }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-      
-      <!-- 要素列表 -->
-      <div class="node-group">
-        <div class="group-header" @click="toggleGroup('element')">
-          <span class="group-icon">{{ expandedGroups.includes('element') ? '▼' : '▶' }}</span>
-          <span class="group-title">输出要素</span>
-          <span class="group-count">{{ filteredElements.length }}</span>
-        </div>
-        <div class="group-content" v-show="expandedGroups.includes('element')">
+      <!-- 要素节点 Tab -->
+      <div v-show="activeTab === 'element'" class="tab-content">
+        <div class="node-section">
+          <div class="section-title">输出要素 <span class="section-count">{{ filteredElements.length }}</span></div>
           <div
             v-for="elem in filteredElements"
             :key="elem.elementKey"
@@ -207,68 +217,113 @@ function onDragStartElement(event, element) {
 }
 
 .panel-header {
-  padding: 14px 16px;
-  border-bottom: 1px solid #e4e7ed;
+  padding: 16px;
+  border-bottom: 1.5px solid #e8ecf0;
+  background: linear-gradient(135deg, #fafbfc 0%, #f8fafc 100%);
 }
 
 .panel-title {
-  font-size: 14px;
+  font-size: 15px;
+  font-weight: 700;
+  color: #1f2937;
+  letter-spacing: 0.3px;
+}
+
+.panel-tabs {
+  display: flex;
+  padding: 8px;
+  gap: 4px;
+  border-bottom: 1.5px solid #e8ecf0;
+  background: #fafbfc;
+}
+
+.tab-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+  padding: 10px 8px;
+  border-radius: 10px;
+  cursor: pointer;
+  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+  border: 1.5px solid transparent;
+}
+
+.tab-item:hover {
+  background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
+  border-color: #d9e9ff;
+}
+
+.tab-item.active {
+  background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+  border-color: #409eff;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
+}
+
+.tab-item.active .tab-icon {
+  filter: brightness(0) invert(1);
+}
+
+.tab-item.active .tab-label {
+  color: white;
+  font-weight: 700;
+}
+
+.tab-icon {
+  font-size: 20px;
+  transition: filter 0.2s;
+}
+
+.tab-label {
+  font-size: 12px;
   font-weight: 600;
-  color: #303133;
+  color: #606266;
+  transition: color 0.2s;
 }
 
 .panel-search {
   padding: 12px;
-  border-bottom: 1px solid #e4e7ed;
+  border-bottom: 1.5px solid #e8ecf0;
 }
 
 .panel-content {
   flex: 1;
   overflow-y: auto;
-  padding: 8px 0;
+  padding: 12px;
 }
 
-.node-group {
-  margin-bottom: 4px;
+.tab-content {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
 }
 
-.group-header {
+.node-section {
   display: flex;
-  align-items: center;
+  flex-direction: column;
   gap: 8px;
-  padding: 10px 16px;
-  cursor: pointer;
-  user-select: none;
-  transition: background 0.2s;
 }
 
-.group-header:hover {
-  background: #f5f7fa;
-}
-
-.group-icon {
-  font-size: 10px;
-  color: #909399;
-  width: 12px;
-}
-
-.group-title {
-  flex: 1;
-  font-size: 13px;
-  font-weight: 500;
+.section-title {
+  font-size: 12px;
+  font-weight: 700;
   color: #606266;
+  padding: 0 4px;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
 }
 
-.group-count {
-  font-size: 12px;
+.section-count {
+  font-size: 11px;
   color: #909399;
   background: #f0f2f5;
   padding: 2px 8px;
   border-radius: 10px;
-}
-
-.group-content {
-  padding: 4px 8px;
+  font-weight: 600;
 }
 
 .node-item {

+ 61 - 28
frontend/vue-demo/src/views/Editor.vue

@@ -4760,96 +4760,129 @@ onMounted(async () => {
   // 编辑器标题栏 - 参考设计风格
   // ==========================================
   .editor-title-bar {
-    padding: 0 16px;
-    height: 48px;
-    border-bottom: 1px solid var(--border);
+    padding: 0 20px;
+    height: 54px;
+    border-bottom: 1.5px solid #e8ecf0;
     display: flex;
     align-items: center;
     justify-content: space-between;
-    background: var(--white);
+    background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
     flex-shrink: 0;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
 
     .titlebar-left {
       display: flex;
       align-items: center;
-      gap: 6px;
+      gap: 8px;
       min-width: 0;
       flex: 1;
 
       .titlebar-folder-icon {
-        font-size: 18px;
-        color: var(--text-3);
+        font-size: 20px;
+        color: #909399;
         flex-shrink: 0;
       }
 
       .titlebar-sep {
-        color: var(--text-3);
-        font-size: 14px;
+        color: #d0d7de;
+        font-size: 16px;
         flex-shrink: 0;
+        font-weight: 300;
       }
 
       .titlebar-project-name {
-        font-size: 14px;
-        font-weight: 600;
-        color: var(--text-1);
+        font-size: 15px;
+        font-weight: 700;
+        color: #1f2937;
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
         max-width: 360px;
+        letter-spacing: 0.2px;
       }
 
       .titlebar-status-tag {
         flex-shrink: 0;
-        margin-left: 4px;
-        border-radius: 4px;
+        margin-left: 6px;
+        border-radius: 6px;
         font-size: 11px;
+        font-weight: 600;
+        padding: 3px 10px;
+        background: linear-gradient(135deg, #e6f0ff 0%, #d9e9ff 100%);
+        color: #409eff;
+        border: 1px solid #d9e9ff;
       }
     }
 
     .titlebar-right {
       display: flex;
       align-items: center;
-      gap: 4px;
+      gap: 6px;
       flex-shrink: 0;
 
       .titlebar-save-status {
         display: flex;
         align-items: center;
-        gap: 5px;
+        gap: 6px;
         font-size: 12px;
-        color: var(--text-3);
+        color: #606266;
         white-space: nowrap;
-        margin-right: 4px;
+        margin-right: 6px;
+        padding: 6px 12px;
+        border-radius: 8px;
+        background: #f5f7fa;
+        font-weight: 500;
 
         .save-dot {
-          width: 6px;
-          height: 6px;
+          width: 7px;
+          height: 7px;
           border-radius: 50%;
-          background: #bbb;
+          background: #d0d7de;
+          box-shadow: 0 0 0 2px rgba(208, 215, 222, 0.2);
 
           &.saved {
             background: #52c41a;
+            box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
           }
         }
       }
 
       :deep(.el-button.is-circle) {
-        width: 32px;
-        height: 32px;
+        width: 36px;
+        height: 36px;
+        border-radius: 10px;
+        transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+        border: 1.5px solid transparent;
+        
+        &:hover {
+          background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
+          border-color: #d9e9ff;
+          transform: translateY(-1px);
+          box-shadow: 0 2px 8px rgba(64, 158, 255, 0.08);
+        }
       }
 
       :deep(.el-icon) {
-        font-size: 16px;
+        font-size: 18px;
       }
 
       :deep(.el-button.is-active-view) {
-        color: var(--primary);
-        background: var(--primary-light);
+        color: white;
+        background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+        border-color: #409eff;
+        box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
+        
+        &:hover {
+          background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+          transform: translateY(-1px);
+          box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+        }
       }
 
       :deep(.el-divider--vertical) {
-        height: 20px;
-        margin: 0 4px;
+        height: 24px;
+        margin: 0 6px;
+        background: #e8ecf0;
       }
     }
   }