重构工作流绘制

This commit is contained in:
2026-05-28 17:55:49 +08:00
parent 9bd4a44ab6
commit 032f258912
9 changed files with 2202 additions and 25 deletions

View File

@@ -3856,15 +3856,11 @@ const deleteSelectedElement = async () => {
if (affectedNodeNames.length > 0) {
const previewNames = affectedNodeNames.slice(0, 8);
const overflowText = affectedNodeNames.length > 8 ? `\n...等 ${affectedNodeNames.length} 个节点` : '';
await ElMessageBox.confirm(
`删除该节点将清理以下下级节点中的引用:\n${previewNames.join('、')}${overflowText}`,
'删除确认',
{
confirmButtonText: '继续删除',
cancelButtonText: '取消',
type: 'warning',
}
);
await ElMessageBox.confirm(`删除该节点将清理以下下级节点中的引用:\n${previewNames.join('、')}${overflowText}`, '删除确认', {
confirmButtonText: '继续删除',
cancelButtonText: '取消',
type: 'warning',
});
}
affectedCount = cleanupReferencesToNode(cur.id);
@@ -3878,7 +3874,7 @@ const deleteSelectedElement = async () => {
if (error === 'cancel') return;
ElMessage.error('删除失败');
}
};// 从后端 DSL 恢复工作流
}; // 从后端 DSL 恢复工作流
const loadWorkflowFromDsl = (dsl: any) => {
const lf = logicFlowInstance.value;
if (!lf || !dsl) return;
@@ -5626,7 +5622,3 @@ onBeforeUnmount(() => {
justify-content: center;
}
</style>

View File

@@ -0,0 +1,279 @@
<template>
<div class="input-source-manager">
<el-divider content-position="left">上级参数引用</el-divider>
<!-- 已引用的参数列表 -->
<div v-if="currentInputSource && currentInputSource.length > 0" class="input-source-list">
<div v-for="(sourceNode, index) in currentInputSource" :key="index" class="input-source-item">
<div class="input-source-header">
<span class="input-source-node-name">{{ getNodeName(sourceNode.nodeId) }}</span>
</div>
<div v-if="sourceNode.field && sourceNode.field.length > 0" class="input-source-fields">
<div v-for="fieldName in sourceNode.field" :key="fieldName" class="field-tag">
<el-tag size="small">{{ fieldName }}</el-tag>
<el-button type="danger" link size="small" @click="emit('removeField', sourceNode.nodeId, fieldName)">删除</el-button>
</div>
</div>
<div class="input-source-output">
<el-switch
:model-value="sourceNode.quoteOutput === true"
@change="(val: boolean) => emit('toggleOutput', sourceNode.nodeId, val)"
size="small"
active-text="引入输出"
inactive-text=""
/>
</div>
</div>
</div>
<!-- 显示所有上级节点的输出引用选项 -->
<div v-if="availableParentNodes.length > 0" class="parent-nodes-output">
<div class="parent-nodes-title">上级节点输出</div>
<div v-for="parentNode in availableParentNodes" :key="parentNode.id" class="parent-node-output-item">
<span class="parent-node-name">{{ parentNode.name }}</span>
<el-switch
:model-value="isNodeOutputQuoted(parentNode.id)"
@change="(val: boolean) => emit('toggleOutput', parentNode.id, val)"
size="small"
active-text="引入输出"
inactive-text=""
/>
</div>
</div>
<!-- 选择参数下拉框 -->
<el-form-item label="选择参数">
<el-select :model-value="selectedParam" @update:model-value="handleParamSelect" placeholder="选择上级节点的参数" class="w100">
<el-option v-for="param in availableParams" :key="param.value" :label="param.label" :value="param.value" />
</el-select>
</el-form-item>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { Node } from '@vue-flow/core';
interface NodeData {
label: string;
nodeCode: string;
inputSource: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
formConfig?: any[];
modelConfig?: any;
skillName?: string;
}
interface ParentNode {
id: string;
name: string;
}
interface ParamOption {
label: string;
value: string;
}
const props = defineProps<{
selectedNode: Node<NodeData> | null;
nodes: Node<NodeData>[];
edges: any[];
}>();
const emit = defineEmits<{
(e: 'removeField', nodeId: string, fieldName: string): void;
(e: 'toggleOutput', nodeId: string, enabled: boolean): void;
(e: 'addParam', paramValue: string): void;
}>();
const selectedParam = ref('');
// 当前节点的 inputSource
const currentInputSource = computed(() => {
if (!props.selectedNode?.data.inputSource) return [];
return props.selectedNode.data.inputSource.filter((item) => item.field && item.field.length > 0);
});
// 获取节点名称
const getNodeName = (nodeId: string) => {
const node = props.nodes.find((n) => n.id === nodeId);
return node?.data.label || nodeId;
};
// 获取所有上级节点(用于显示输出引用选项)
const availableParentNodes = computed(() => {
if (!props.selectedNode) return [];
// 获取已经引用了字段的节点ID列表
const inputSource = props.selectedNode.data.inputSource;
const nodesWithFields = new Set<string>();
if (Array.isArray(inputSource)) {
inputSource.forEach((item) => {
if (item.field && item.field.length > 0) {
nodesWithFields.add(item.nodeId);
}
});
}
// 递归查找所有上级节点
const findAllParentNodes = (nodeId: string, visited = new Set<string>()): string[] => {
if (visited.has(nodeId)) return [];
visited.add(nodeId);
const incomingEdges = props.edges.filter((e) => e.target === nodeId);
const parentIds: string[] = [];
incomingEdges.forEach((edge) => {
parentIds.push(edge.source);
parentIds.push(...findAllParentNodes(edge.source, visited));
});
return parentIds;
};
const allParentIds = findAllParentNodes(props.selectedNode.id);
const parentNodes = allParentIds
.map((parentId) => {
const parentNode = props.nodes.find((n) => n.id === parentId);
if (!parentNode) return null;
const nodeCode = String(parentNode.data.nodeCode || '').toLowerCase();
const isJudge = ['判断', 'judge', 'condition', 'if', 'branch', 'gateway'].some((k) => nodeCode.includes(k));
const isStart = nodeCode === '__start__';
if (isJudge || isStart || nodesWithFields.has(parentId)) return null;
return {
id: parentId,
name: parentNode.data.label,
};
})
.filter(Boolean);
return parentNodes as ParentNode[];
});
// 检查节点输出是否被引用
const isNodeOutputQuoted = (nodeId: string): boolean => {
if (!props.selectedNode) return false;
const inputSource = props.selectedNode.data.inputSource;
if (!Array.isArray(inputSource)) return false;
const node = inputSource.find((item) => item.nodeId === nodeId);
return node?.quoteOutput === true;
};
// 获取可用的参数选项
const availableParams = computed(() => {
if (!props.selectedNode) return [];
const params: ParamOption[] = [];
const visited = new Set<string>();
const findParents = (nodeId: string) => {
if (visited.has(nodeId)) return;
visited.add(nodeId);
props.edges
.filter((e) => e.target === nodeId)
.forEach((edge) => {
const parent = props.nodes.find((n) => n.id === edge.source);
if (parent && parent.data.nodeCode !== '__start__' && parent.data.nodeCode !== 'judge') {
params.push({
label: `${parent.data.label}.output`,
value: `\${${parent.id}.output}`,
});
}
findParents(edge.source);
});
};
findParents(props.selectedNode.id);
return params;
});
const handleParamSelect = (value: string) => {
if (!value) return;
emit('addParam', value);
selectedParam.value = '';
};
</script>
<style scoped lang="scss">
.input-source-manager {
margin-top: 16px;
}
.input-source-list {
margin-bottom: 16px;
}
.input-source-item {
padding: 12px;
margin-bottom: 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.input-source-header {
margin-bottom: 8px;
}
.input-source-node-name {
font-size: 14px;
font-weight: 600;
color: #334155;
}
.input-source-fields {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 8px;
}
.field-tag {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
background: #fff;
border-radius: 4px;
}
.input-source-output {
padding-top: 8px;
border-top: 1px solid #e2e8f0;
}
.parent-nodes-output {
margin-bottom: 16px;
}
.parent-nodes-title {
font-size: 13px;
font-weight: 600;
color: #64748b;
margin-bottom: 8px;
}
.parent-node-output-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin-bottom: 6px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
}
.parent-node-name {
font-size: 13px;
color: #475569;
}
.w100 {
width: 100%;
}
</style>

View File

@@ -0,0 +1,263 @@
<template>
<div class="config-panel">
<h3>节点配置</h3>
<div v-if="selectedNode">
<el-form label-position="top">
<el-form-item label="节点名称">
<el-input :model-value="selectedNode.data.label" @update:model-value="updateNodeLabel" />
</el-form-item>
<el-form-item label="节点类型">
<el-tag>{{ selectedNode.data.nodeCode }}</el-tag>
</el-form-item>
<!-- 模型选择 -->
<el-form-item v-if="nodeConfig?.modelConfig && nodeConfig.modelConfig.length > 0" label="选择模型">
<el-button type="primary" @click="emit('openModelSelector')" style="width: 100%">选择模型</el-button>
<div v-if="selectedNode.data.modelConfig?.modelName" class="selected-tag">
<el-tag type="success" size="large" closable @close="emit('removeModel')">
{{ selectedNode.data.modelConfig.modelName }}
</el-tag>
</div>
</el-form-item>
<!-- 技能选择 -->
<el-form-item v-if="nodeConfig?.skillOption" label="选择技能">
<el-button type="primary" @click="emit('openSkillSelector')" style="width: 100%">选择技能</el-button>
<div v-if="selectedNode.data.skillName" class="selected-tag">
<el-tag type="success" size="large" closable @close="emit('removeSkill')">
{{ selectedNode.data.skillName }}
</el-tag>
</div>
</el-form-item>
<!-- 动态表单字段 -->
<template v-if="nodeConfig?.formConfig && nodeConfig.formConfig.length > 0">
<el-divider content-position="left">节点参数</el-divider>
<el-form-item v-for="field in nodeConfig.formConfig" :key="field.field" :label="field.label" :required="field.required">
<el-input
v-if="field.type === 'input' || field.type === 'string'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
:placeholder="field.required ? '必填' : '选填'"
/>
<el-input-number
v-else-if="field.type === 'number' || field.type === 'inputNumber'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
class="w100"
/>
<el-input
v-else-if="field.type === 'textarea'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
type="textarea"
:rows="3"
:placeholder="field.required ? '必填' : '选填'"
/>
<el-switch
v-else-if="field.type === 'switch'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
/>
<el-select
v-else-if="field.type === 'select' && field.options"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
:placeholder="field.required ? '必填' : '选填'"
class="w100"
>
<el-option v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-input
v-else
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
:placeholder="field.required ? '必填' : '选填'"
/>
</el-form-item>
</template>
</el-form>
<!-- 上级参数管理 -->
<InputSourceManager
v-if="selectedNode.data.nodeCode !== '__start__'"
:selected-node="selectedNode"
:nodes="allNodes"
:edges="allEdges"
@remove-field="(nodeId: string, fieldName: string) => emit('removeField', nodeId, fieldName)"
@toggle-output="(nodeId: string, enabled: boolean) => emit('toggleOutput', nodeId, enabled)"
@add-param="(paramValue: string) => emit('addParamByValue', paramValue)"
/>
</div>
<el-empty v-else description="请选择一个节点" :image-size="100" />
</div>
</template>
<script setup lang="ts">
import type { Node } from '@vue-flow/core';
import InputSourceManager from './InputSourceManager.vue';
interface NodeData {
label: string;
nodeCode: string;
inputSource: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
formConfig?: any[];
modelConfig?: any;
skillName?: string;
}
interface ParamRef {
id: string;
label: string;
}
interface NodeConfig {
formConfig: any[];
modelConfig: any[];
skillOption: boolean;
}
const props = defineProps<{
selectedNode: Node<NodeData> | null;
availableParams: ParamRef[];
nodeConfig: NodeConfig | null;
allNodes: Node<NodeData>[];
allEdges: any[];
}>();
const emit = defineEmits<{
(e: 'update:selectedNode', node: Node<NodeData>): void;
(e: 'addParam', param: ParamRef): void;
(e: 'openModelSelector'): void;
(e: 'removeModel'): void;
(e: 'openSkillSelector'): void;
(e: 'removeSkill'): void;
(e: 'removeField', nodeId: string, fieldName: string): void;
(e: 'toggleOutput', nodeId: string, enabled: boolean): void;
(e: 'addParamByValue', paramValue: string): void;
}>();
const updateNodeLabel = (newLabel: string) => {
if (!props.selectedNode) return;
const updatedNode = {
...props.selectedNode,
data: {
...props.selectedNode.data,
label: newLabel,
},
};
emit('update:selectedNode', updatedNode);
};
const getFieldValue = (fieldName: string) => {
if (!props.selectedNode?.data.formConfig) return '';
const field = props.selectedNode.data.formConfig.find((f: any) => f.field === fieldName);
return field?.value ?? '';
};
const updateFieldValue = (fieldName: string, value: any) => {
if (!props.selectedNode) return;
const formConfig = props.selectedNode.data.formConfig || [];
const existingIndex = formConfig.findIndex((f: any) => f.field === fieldName);
let updatedFormConfig;
if (existingIndex >= 0) {
updatedFormConfig = [...formConfig];
updatedFormConfig[existingIndex] = { ...updatedFormConfig[existingIndex], value };
} else {
const fieldDef = props.nodeConfig?.formConfig.find((f) => f.field === fieldName);
if (fieldDef) {
updatedFormConfig = [...formConfig, { ...fieldDef, value }];
} else {
updatedFormConfig = formConfig;
}
}
const updatedNode = {
...props.selectedNode,
data: {
...props.selectedNode.data,
formConfig: updatedFormConfig,
},
};
emit('update:selectedNode', updatedNode);
};
</script>
<style scoped lang="scss">
.config-panel {
background: #ffffff;
border-radius: 10px;
padding: 16px;
overflow-y: auto;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
border: 1px solid #e6eaf0;
h3 {
margin: 0 0 16px 0;
font-size: 15px;
font-weight: 700;
color: #334155;
letter-spacing: 0;
}
:deep(.el-form) {
.el-form-item {
margin-bottom: 14px;
.el-form-item__label {
font-weight: 600;
color: #475569;
margin-bottom: 6px;
font-size: 13px;
}
.el-input__wrapper {
border-radius: 6px;
box-shadow: none;
border: 1px solid #dbe3ee;
transition: all 0.2s ease;
&:hover {
border-color: #b8c4d6;
}
&.is-focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12);
}
}
}
}
.param-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 8px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition: all 0.2s ease;
&:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
}
.selected-tag {
margin-top: 10px;
}
.w100 {
width: 100%;
}
:deep(.el-empty) {
padding: 32px 12px;
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div class="node-library-panel" :class="{ collapsed: collapsed }">
<div class="node-library-header">
<span>节点库</span>
<el-button class="collapse-btn" text circle @click="emit('update:collapsed', !collapsed)">
<el-icon>
<ArrowRightBold v-if="collapsed" />
<ArrowLeftBold v-else />
</el-icon>
</el-button>
</div>
<div v-if="!collapsed" class="node-library-content">
<el-empty v-if="nodeLibraryGroups.length === 0" description="暂无节点" :image-size="40" />
<div v-else class="node-library-groups">
<div v-for="group in nodeLibraryGroups" :key="group.group" class="node-group">
<div class="node-group-title">{{ group.label }}</div>
<div class="node-group-items">
<el-button
v-for="item in group.items"
:key="item.nodeCode"
text
class="node-item"
@click="emit('addNode', item.nodeCode, item.nodeName)"
>
{{ item.nodeName }}
</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue';
import type { NodeLibraryGroup } from '/@/api/settings/creation';
defineProps<{
nodeLibraryGroups: NodeLibraryGroup[];
collapsed: boolean;
}>();
const emit = defineEmits<{
(e: 'update:collapsed', value: boolean): void;
(e: 'addNode', nodeCode: string, nodeName: string): void;
}>();
</script>
<style scoped lang="scss">
.node-library-panel {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
background: #fff;
border: 1px solid #d8e0eb;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
width: 132px;
max-height: calc(100% - 20px);
display: flex;
flex-direction: column;
overflow: hidden;
transition: width 0.18s ease;
&.collapsed {
width: 40px;
.node-library-header {
padding: 8px;
border-bottom: none;
> span {
display: none;
}
}
.node-library-content {
display: none;
}
}
}
.node-library-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #e7ecf3;
font-size: 12px;
font-weight: 700;
color: #1f2937;
background: #f8fafc;
.collapse-btn {
width: 20px;
height: 20px;
border: 1px solid #cfd8e6;
color: #64748b;
}
}
.node-library-content {
padding: 12px;
overflow-y: auto;
max-height: 480px;
}
.node-library-groups {
display: flex;
flex-direction: column;
gap: 16px;
}
.node-group {
.node-group-title {
font-size: 12px;
font-weight: 700;
color: #64748b;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #e7ecf3;
}
.node-group-items {
display: flex;
flex-direction: column;
gap: 2px;
}
.node-item {
justify-content: flex-start;
width: 100%;
padding: 8px 10px;
color: #475569;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
background: transparent;
border: none;
transition: all 0.15s ease;
&:hover {
background: #f1f5f9;
color: #1e293b;
}
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" :close-on-click-modal="false">
<el-form :model="saveForm" label-position="top">
<el-form-item label="工作流名称" required>
<el-input v-model="saveForm.flowName" placeholder="请输入工作流名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="工作流描述">
<el-input v-model="saveForm.description" type="textarea" :rows="4" placeholder="请输入工作流描述(选填)" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="emit('confirm')">{{ confirmText }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
modelValue: boolean;
saveForm: { flowName: string; description: string };
currentEditingWorkflowId: string | null;
saving: boolean;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'confirm'): void;
}>();
const dialogVisible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value),
});
const dialogTitle = computed(() => (props.currentEditingWorkflowId ? '编辑工作流' : '保存工作流'));
const confirmText = computed(() => (props.currentEditingWorkflowId ? '确定更新' : '确定保存'));
</script>

View File

@@ -0,0 +1,191 @@
<template>
<div class="workflow-list-panel">
<div class="panel-header">
<el-tabs v-model="activeTab" class="workflow-tabs">
<el-tab-pane label="我的工作流" name="user"></el-tab-pane>
<el-tab-pane label="模板工作流" name="template"></el-tab-pane>
</el-tabs>
<el-button type="success" size="small" @click="emit('create')">新建</el-button>
</div>
<div class="workflow-list-content" v-loading="loading">
<el-empty v-if="currentList.length === 0" description="暂无工作流" :image-size="60" />
<div v-else class="workflow-items">
<div
v-for="workflow in currentList"
:key="workflow.id"
class="workflow-item"
:class="{ active: currentEditingId === workflow.id }"
@click="emit('edit', workflow)"
>
<div class="workflow-item-content">
<div class="workflow-item-name">{{ workflow.flowName || workflow.flowTemplateName || '未命名工作流' }}</div>
<div class="workflow-item-desc">{{ workflow.description || '暂无描述' }}</div>
</div>
<div class="workflow-item-actions">
<el-button type="primary" link size="small" @click.stop="emit('edit', workflow)">编辑</el-button>
<el-button type="danger" link size="small" @click.stop="emit('delete', workflow)">删除</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { WorkflowItem } from '/@/api/settings/creation';
const props = defineProps<{
userWorkflowList: WorkflowItem[];
templateWorkflowList: WorkflowItem[];
currentEditingId: string | null;
loading: boolean;
}>();
const emit = defineEmits<{
(e: 'edit', workflow: WorkflowItem): void;
(e: 'delete', workflow: WorkflowItem): void;
(e: 'create'): void;
}>();
const activeTab = ref<'user' | 'template'>('user');
const currentList = computed(() => {
return activeTab.value === 'user' ? props.userWorkflowList : props.templateWorkflowList;
});
</script>
<style scoped lang="scss">
.workflow-list-panel {
background: #ffffff;
border-radius: 10px;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
border: 1px solid #e6eaf0;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
:deep(.el-button) {
border-radius: 6px;
font-weight: 500;
font-size: 12px;
padding: 6px 12px;
}
}
.workflow-tabs {
flex: 1;
:deep(.el-tabs__header) {
margin: 0;
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
:deep(.el-tabs__item) {
font-size: 14px;
font-weight: 600;
color: #64748b;
padding: 0 16px;
height: 36px;
line-height: 36px;
&.is-active {
color: #3b82f6;
}
}
:deep(.el-tabs__active-bar) {
background-color: #3b82f6;
}
}
.workflow-list-content {
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(102, 126, 234, 0.05);
border-radius: 10px;
}
&::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
}
}
.workflow-items {
display: flex;
flex-direction: column;
gap: 8px;
}
.workflow-item {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: #fff;
&:hover {
border-color: #c5d2e6;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
}
&.active {
border-color: #3b82f6;
background: #eff6ff;
}
}
.workflow-item-content {
margin-bottom: 8px;
}
.workflow-item-name {
font-size: 14px;
font-weight: 600;
color: #1e293b;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workflow-item-desc {
font-size: 12px;
color: #64748b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workflow-item-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
:deep(.el-button) {
border-radius: 6px;
font-weight: 500;
font-size: 12px;
padding: 4px 8px;
}
}
</style>

File diff suppressed because it is too large Load Diff