feat:1.任务大纲编排窗口重构及样式调整2.删除按钮添加
This commit is contained in:
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772093306648" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10322" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64C264.576 64 64 264.576 64 512c0 247.422 200.576 448 448 448 247.422 0 448-200.578 448-448C960 264.576 759.422 64 512 64zM593.63 755.834l-107.18 0L486.45 384.388l-68.612 37.82-27.47-89.816 119.986-64.226 83.278 0L593.632 755.834z" fill="#ffffff" p-id="10323"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772093306648" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10322" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64C264.576 64 64 264.576 64 512c0 247.422 200.576 448 448 448 247.422 0 448-200.578 448-448C960 264.576 759.422 64 512 64zM593.63 755.834l-107.18 0L486.45 384.388l-68.612 37.82-27.47-89.816 119.986-64.226 83.278 0L593.632 755.834z" p-id="10323"></path></svg>
|
||||
|
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 596 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772093383090" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11068" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64C264.576 64 64 264.576 64 512c0 247.422 200.576 448 448 448 247.422 0 448-200.578 448-448C960 264.576 759.422 64 512 64zM620.256 717.658c-32.132 29.45-77.228 45.014-130.41 45.014-54.27 0-98.822-16.862-120.484-31.298l-12.756-8.504 31.068-92.17 21.712 14.476c13.388 8.926 45.072 22.208 77.488 22.208 33.73 0 69.77-16.142 69.77-61.45 0-31.142-21.452-64.422-81.66-64.422l-50.32 0 0-89.344 49.13 0c28.724 0 69.176-16.298 69.176-52.532 0-27.742-17.812-43.02-50.154-43.02-25.572 0-52.78 12.508-68.308 23.218l-21.586 14.886-30.758-88.038 11.884-8.758c22.908-16.884 69.19-36.6 124.226-36.6 100.06 0 144.848 64.092 144.848 127.61 0 41.306-19.084 77.362-52.648 102.036 37.34 21.14 66.918 60.562 66.918 117.34C667.394 650.968 650.652 689.802 620.256 717.658z" p-id="11069" fill="#ffffff"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772093383090" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11068" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64C264.576 64 64 264.576 64 512c0 247.422 200.576 448 448 448 247.422 0 448-200.578 448-448C960 264.576 759.422 64 512 64zM620.256 717.658c-32.132 29.45-77.228 45.014-130.41 45.014-54.27 0-98.822-16.862-120.484-31.298l-12.756-8.504 31.068-92.17 21.712 14.476c13.388 8.926 45.072 22.208 77.488 22.208 33.73 0 69.77-16.142 69.77-61.45 0-31.142-21.452-64.422-81.66-64.422l-50.32 0 0-89.344 49.13 0c28.724 0 69.176-16.298 69.176-52.532 0-27.742-17.812-43.02-50.154-43.02-25.572 0-52.78 12.508-68.308 23.218l-21.586 14.886-30.758-88.038 11.884-8.758c22.908-16.884 69.19-36.6 124.226-36.6 100.06 0 144.848 64.092 144.848 127.61 0 41.306-19.084 77.362-52.648 102.036 37.34 21.14 66.918 60.562 66.918 117.34C667.394 650.968 650.652 689.802 620.256 717.658z" p-id="11069"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772093350376" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10815" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64c-247.296 0-448 200.704-448 448s200.704 448 448 448 448-200.704 448-448-200.704-448-448-448zM355.84 759.296v-70.144l52.736-55.296c95.744-97.792 141.312-154.624 141.312-211.968 0-40.96-19.456-61.44-57.344-61.44-29.184 0-56.32 16.896-73.728 30.72l-20.48 16.384-36.864-87.552 11.264-9.728c35.328-29.184 83.968-46.08 133.632-46.08 48.64 0 88.576 15.872 115.712 46.08 23.552 26.624 36.864 63.488 36.864 103.936 0 94.208-69.12 172.032-139.776 243.2l-4.608 4.608h153.6v97.28h-312.32z" fill="#ffffff" p-id="10816"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772093350376" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10815" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64c-247.296 0-448 200.704-448 448s200.704 448 448 448 448-200.704 448-448-200.704-448-448-448zM355.84 759.296v-70.144l52.736-55.296c95.744-97.792 141.312-154.624 141.312-211.968 0-40.96-19.456-61.44-57.344-61.44-29.184 0-56.32 16.896-73.728 30.72l-20.48 16.384-36.864-87.552 11.264-9.728c35.328-29.184 83.968-46.08 133.632-46.08 48.64 0 88.576 15.872 115.712 46.08 23.552 26.624 36.864 63.488 36.864 103.936 0 94.208-69.12 172.032-139.776 243.2l-4.608 4.608h153.6v97.28h-312.32z"p-id="10816"></path></svg>
|
||||
|
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 842 B |
163
frontend/src/components/BranchInput/index.vue
Normal file
163
frontend/src/components/BranchInput/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="external-input-container">
|
||||
<div class="branch-input-container">
|
||||
<el-input
|
||||
v-model="inputValue"
|
||||
placeholder="输入分支需求..."
|
||||
size="small"
|
||||
class="branch-input"
|
||||
@keydown="handleKeydown"
|
||||
ref="inputRef"
|
||||
/>
|
||||
<span
|
||||
class="submit-icon"
|
||||
:class="{ 'is-disabled': !inputValue.trim() }"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<svg-icon icon-class="paper-plane" size="14px" color="#fff" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
placeholder: '输入分支需求...'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'submit', value: string): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const inputValue = ref(props.modelValue)
|
||||
const inputRef = ref<InstanceType<typeof HTMLInputElement>>()
|
||||
|
||||
// 聚焦输入框
|
||||
const focus = () => {
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
focus
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (inputValue.value.trim()) {
|
||||
emit('submit', inputValue.value.trim())
|
||||
inputValue.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
inputValue.value = ''
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
handleSubmit()
|
||||
} else if (event.key === 'Escape') {
|
||||
handleCancel()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.external-input-container {
|
||||
position: absolute;
|
||||
bottom: -80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 260px;
|
||||
background: linear-gradient(var(--color-card-bg-task), var(--color-card-bg-task)) padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 30px;
|
||||
padding: 10px 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
.branch-input-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.branch-input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-taskbar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(to right, var(--color-accent), var(--color-accent-secondary));
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active:not(.is-disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<VueFlow
|
||||
v-model:nodes="nodes"
|
||||
v-model:edges="edges"
|
||||
v-model:edges="styledEdges"
|
||||
:default-zoom="0.5"
|
||||
:min-zoom="0.5"
|
||||
:delete-key-code="null"
|
||||
@@ -15,9 +15,9 @@
|
||||
class="flow-container"
|
||||
@node-click="onNodeClick"
|
||||
>
|
||||
<!-- 根节点 -->
|
||||
<!-- 节点 -->
|
||||
<template #node-root="nodeProps">
|
||||
<RootNode
|
||||
<TaskNode
|
||||
v-bind="nodeProps"
|
||||
:is-adding-branch="currentAddingBranchNodeId === nodeProps.id"
|
||||
@add-branch="handleAddBranch"
|
||||
@@ -33,12 +33,12 @@
|
||||
v-bind="nodeProps"
|
||||
:is-adding-branch="currentAddingBranchNodeId === nodeProps.id"
|
||||
:is-branch-selected="selectedNodeIds.has(nodeProps.id)"
|
||||
:is-deletable="getNodeDeletable(nodeProps.id).isDeletable"
|
||||
:branch-id="getNodeDeletable(nodeProps.id).branchId"
|
||||
@add-branch="handleAddBranch"
|
||||
@start-add-branch="handleStartAddBranch"
|
||||
@cancel-add-branch="handleCancelAddBranch"
|
||||
@save-task="handleSaveTask"
|
||||
@edit-task="handleEditTask"
|
||||
@cancel-edit="handleCancelEdit"
|
||||
@delete-branch="handleDeleteBranch"
|
||||
/>
|
||||
</template>
|
||||
</VueFlow>
|
||||
@@ -58,7 +58,6 @@ import '@vue-flow/core/dist/style.css'
|
||||
import '@vue-flow/core/dist/theme-default.css'
|
||||
import '@vue-flow/minimap/dist/style.css'
|
||||
import '@vue-flow/controls/dist/style.css'
|
||||
import RootNode from './components/RootNode.vue'
|
||||
import TaskNode from './components/TaskNode.vue'
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
@@ -75,7 +74,7 @@ const nodes = ref<Node[]>([])
|
||||
const edges = ref<Edge[]>([])
|
||||
|
||||
// Vue Flow 实例方法
|
||||
const { fitView: fit, setTransform, updateNode, findNode } = useVueFlow()
|
||||
const { fitView: fit, setTransform, updateNode, findNode, getNodes, getEdges } = useVueFlow()
|
||||
|
||||
// 当前正在添加分支的节点 ID
|
||||
const currentAddingBranchNodeId = ref<string | null>(null)
|
||||
@@ -83,6 +82,28 @@ const currentAddingBranchNodeId = ref<string | null>(null)
|
||||
// 当前选中的节点ID集合
|
||||
const selectedNodeIds = ref<Set<string>>(new Set())
|
||||
|
||||
// 带样式的边数据
|
||||
const styledEdges = computed(() => {
|
||||
return edges.value.map(edge => {
|
||||
// 根节点永远参与高亮
|
||||
const sourceIsRoot = edge.source === 'root-goal'
|
||||
const targetIsRoot = edge.target === 'root-goal'
|
||||
const sourceSelected = selectedNodeIds.value.has(edge.source) || sourceIsRoot
|
||||
const targetSelected = selectedNodeIds.value.has(edge.target) || targetIsRoot
|
||||
const isHighlighted = sourceSelected && targetSelected
|
||||
|
||||
return {
|
||||
...edge,
|
||||
style: {
|
||||
stroke: isHighlighted ? 'var(--color-node-active)' : 'var(--color-node-border)',
|
||||
strokeWidth: 2
|
||||
},
|
||||
animated: isHighlighted,
|
||||
class: isHighlighted ? 'edge-highlighted' : 'edge-normal'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 回溯到根节点的路径
|
||||
const getPathToRoot = (targetNodeId: string): string[] => {
|
||||
const path: string[] = []
|
||||
@@ -672,6 +693,33 @@ const handleCancelAddBranch = () => {
|
||||
currentAddingBranchNodeId.value = null
|
||||
}
|
||||
|
||||
// 判断节点是否可删除(分支从底部出发连接的节点)
|
||||
const getNodeDeletable = (nodeId: string): { isDeletable: boolean; branchId: string | null } => {
|
||||
// 找到指向该节点的边
|
||||
const incomingEdges = edges.value.filter(edge => edge.target === nodeId)
|
||||
|
||||
// 检查是否有从底部(sourceHandle='bottom')连接过来的边
|
||||
const bottomEdge = incomingEdges.find(edge => edge.sourceHandle === 'bottom')
|
||||
|
||||
if (bottomEdge && bottomEdge.data?.branchId) {
|
||||
return {
|
||||
isDeletable: true,
|
||||
branchId: bottomEdge.data.branchId
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDeletable: false,
|
||||
branchId: null
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分支
|
||||
const handleDeleteBranch = (branchId: string) => {
|
||||
console.log('删除分支:', branchId)
|
||||
// 后续实现删除逻辑
|
||||
}
|
||||
|
||||
// 开始编辑任务
|
||||
const handleEditTask = (nodeId: string) => {
|
||||
// 更新节点的编辑状态
|
||||
@@ -862,6 +910,9 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
|
||||
width: 20,
|
||||
height: 20,
|
||||
strokeWidth: 2
|
||||
},
|
||||
data: {
|
||||
branchId: String(timestamp)
|
||||
}
|
||||
}
|
||||
edges.value.push(newEdge)
|
||||
@@ -967,7 +1018,7 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
|
||||
|
||||
// 串行填充所有任务的详情(避免并发问题导致字段混乱)
|
||||
for (let i = 0; i < newTasks.length; i++) {
|
||||
await fillStepWithRetry(newTasks[i], i)
|
||||
await fillStepWithRetry(newTasks[i]!, i)
|
||||
}
|
||||
|
||||
// 更新 store 中的任务数据
|
||||
@@ -1125,13 +1176,16 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
|
||||
width: 20,
|
||||
height: 20,
|
||||
strokeWidth: 2
|
||||
},
|
||||
data: {
|
||||
branchId: String(timestamp)
|
||||
}
|
||||
}
|
||||
edges.value.push(newEdge)
|
||||
newBranchEdges.push(newEdge)
|
||||
} else {
|
||||
// 后续任务连接到前一个任务(左右连接)
|
||||
const prevTaskNodeId = taskNodeIds[index - 1]
|
||||
const prevTaskNodeId = taskNodeIds[index - 1]!
|
||||
const newEdge: Edge = {
|
||||
id: `edge-${prevTaskNodeId}-${taskNodeId}`,
|
||||
source: prevTaskNodeId,
|
||||
@@ -1202,7 +1256,7 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
|
||||
newTasks[index]!.Collaboration_Brief_frontEnd = filledTask.Collaboration_Brief_frontEnd
|
||||
|
||||
// 更新节点数据
|
||||
const taskNodeId = taskNodeIds[index]
|
||||
const taskNodeId = taskNodeIds[index]!
|
||||
updateNode(taskNodeId, {
|
||||
data: {
|
||||
...findNode(taskNodeId)?.data,
|
||||
@@ -1231,7 +1285,7 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
|
||||
|
||||
// 串行填充所有任务的详情(避免并发问题导致字段混乱)
|
||||
for (let i = 0; i < newTasks.length; i++) {
|
||||
await fillStepWithRetry(newTasks[i], i)
|
||||
await fillStepWithRetry(newTasks[i]!, i)
|
||||
}
|
||||
|
||||
// 更新 store 中的任务数据
|
||||
@@ -1312,7 +1366,7 @@ defineExpose({
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-bg-detail);
|
||||
background: var(--color-settings-panel-content-bg);
|
||||
}
|
||||
|
||||
.plan-modification-header {
|
||||
@@ -1343,7 +1397,7 @@ defineExpose({
|
||||
.flow-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-bg-detail);
|
||||
background: var(--color-settings-panel-content-bg);
|
||||
}
|
||||
|
||||
// Vue Flow 节点样式
|
||||
@@ -1351,11 +1405,20 @@ defineExpose({
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__edge-path) {
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__edge.selected .vue-flow__edge-path) {
|
||||
stroke: #409eff;
|
||||
}
|
||||
|
||||
// 节点连线样式
|
||||
:deep(.vue-flow__edge-path) {
|
||||
stroke: var(--color-node-border);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5 5;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__edge.edge-highlighted .vue-flow__edge-path) {
|
||||
stroke: var(--color-node-active) !important;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
<template>
|
||||
<div class="root-node-wrapper">
|
||||
<!-- 左侧连接点(输入) -->
|
||||
<Handle type="target" :position="Position.Left" id="left" />
|
||||
<!-- 右侧连接点(输出) -->
|
||||
<Handle type="source" :position="Position.Right" id="right" />
|
||||
<!-- 底部连接点(用于分支) -->
|
||||
<Handle type="source" :position="Position.Bottom" id="bottom" />
|
||||
|
||||
<el-card class="root-node-card" :shadow="true">
|
||||
<!-- 目标内容 -->
|
||||
<div class="goal-content">
|
||||
<div class="goal-label">初始目标</div>
|
||||
<div class="goal-text">{{ goal }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部添加按钮 -->
|
||||
<div v-if="!isAddingBranch" class="external-add-btn" @click="startAddBranch">
|
||||
<el-icon :size="20" color="#409eff">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 取消按钮(输入框显示时) -->
|
||||
<div v-else class="external-add-btn cancel-btn" @click="cancelAddBranch">
|
||||
<el-icon :size="20" color="#f56c6c">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 外部输入框 -->
|
||||
<div v-if="isAddingBranch" class="external-input-container">
|
||||
<el-input
|
||||
v-model="branchInput"
|
||||
placeholder="输入分支需求..."
|
||||
size="small"
|
||||
@keydown="handleBranchKeydown"
|
||||
ref="branchInputRef"
|
||||
class="branch-input"
|
||||
>
|
||||
<template #suffix>
|
||||
<svg-icon
|
||||
icon-class="paper-plane"
|
||||
size="16px"
|
||||
color="#409eff"
|
||||
class="submit-icon"
|
||||
:class="{ 'is-disabled': !branchInput.trim() }"
|
||||
@click="submitBranch"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { CirclePlus, Remove } from '@element-plus/icons-vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
interface RootNodeData {
|
||||
goal: string
|
||||
initialInput?: string[] | string
|
||||
isRoot?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
data: RootNodeData
|
||||
isAddingBranch?: boolean
|
||||
[key: string]: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'add-branch', nodeId: string, branchContent: string): void
|
||||
(e: 'start-add-branch', nodeId: string): void
|
||||
(e: 'cancel-add-branch'): void
|
||||
}>()
|
||||
|
||||
const goal = computed(() => props.data.goal || '')
|
||||
|
||||
const initialInput = computed(() => props.data.initialInput)
|
||||
|
||||
const displayInput = computed(() => {
|
||||
if (!initialInput.value) return false
|
||||
if (Array.isArray(initialInput.value)) {
|
||||
return initialInput.value.length > 0
|
||||
}
|
||||
return initialInput.value.trim().length > 0
|
||||
})
|
||||
|
||||
// 分支添加相关状态(使用父组件传入的 prop)
|
||||
const branchInput = ref('')
|
||||
const branchInputRef = ref<InstanceType<typeof HTMLInputElement>>()
|
||||
|
||||
// 计算属性,使用父组件传入的状态
|
||||
const isAddingBranch = computed(() => props.isAddingBranch || false)
|
||||
|
||||
// 分支添加相关方法
|
||||
const startAddBranch = () => {
|
||||
emit('start-add-branch', props.id)
|
||||
branchInput.value = ''
|
||||
nextTick(() => {
|
||||
branchInputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const cancelAddBranch = () => {
|
||||
emit('cancel-add-branch')
|
||||
branchInput.value = ''
|
||||
}
|
||||
|
||||
const submitBranch = () => {
|
||||
if (branchInput.value.trim()) {
|
||||
emit('add-branch', props.id, branchInput.value.trim())
|
||||
branchInput.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleBranchKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
submitBranch()
|
||||
} else if (event.key === 'Escape') {
|
||||
cancelAddBranch()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.root-node-card {
|
||||
width: 200px;
|
||||
min-height: 100px;
|
||||
background: var(--color-bg-three);
|
||||
border: 2px solid #409eff;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.root-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.goal-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.goal-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.goal-label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.goal-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-title-header);
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.initial-input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.input-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #409eff, transparent);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.input-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.input-text {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// 节点包装器
|
||||
.root-node-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 外部添加按钮
|
||||
.external-add-btn {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 2px solid #409eff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
|
||||
.el-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
// 取消按钮样式
|
||||
&.cancel-btn {
|
||||
border-color: #f56c6c;
|
||||
box-shadow: 0 2px 8px rgba(245, 108, 108, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #f56c6c;
|
||||
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 外部输入容器
|
||||
.external-input-container {
|
||||
position: absolute;
|
||||
bottom: -80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 260px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #fff;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
border-top: 1px solid #dcdfe6;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.branch-input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: transparent;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c4cc;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交图标样式
|
||||
.submit-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active:not(.is-disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="task-node-wrapper">
|
||||
<div class="node-wrapper">
|
||||
<!-- 左侧连接点(输入) -->
|
||||
<Handle type="target" :position="Position.Left" id="left" />
|
||||
<!-- 右侧连接点(输出) -->
|
||||
@@ -7,19 +7,27 @@
|
||||
<!-- 底部连接点(用于分支) -->
|
||||
<Handle type="source" :position="Position.Bottom" id="bottom" />
|
||||
|
||||
<!-- 删除按钮 - 仅在可删除的分支节点上显示,在左侧 -->
|
||||
<div v-if="isDeletable" class="node-delete-btn left" @click.stop="handleDeleteBranch">
|
||||
<SvgIcon icon-class="close" size="14px" color="#d8d8d8" />
|
||||
</div>
|
||||
|
||||
<!-- 根节点内容 -->
|
||||
<el-card v-if="isRoot" class="node-card root-style" :shadow="true">
|
||||
<div class="goal-content">
|
||||
<div class="goal-label">初始目标</div>
|
||||
<div class="goal-text">{{ goal }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 任务节点内容 -->
|
||||
<el-card
|
||||
class="task-node-card"
|
||||
:class="{
|
||||
'is-editing': isEditing,
|
||||
'is-active': isActive,
|
||||
'is-branch-selected': props.isBranchSelected
|
||||
}"
|
||||
v-else
|
||||
class="node-card task-style"
|
||||
:class="{ 'is-branch-selected': isBranchSelected }"
|
||||
:shadow="true"
|
||||
>
|
||||
<!-- 任务名称 -->
|
||||
<div class="task-name">{{ task.StepName }}</div>
|
||||
|
||||
<!-- 智能体列表 -->
|
||||
<div class="agents-container">
|
||||
<el-tooltip
|
||||
v-for="agentSelection in task.AgentSelection"
|
||||
@@ -45,42 +53,27 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部添加按钮(卡片外部) -->
|
||||
<!-- 底部添加按钮 -->
|
||||
<div v-if="!isAddingBranch" class="external-add-btn" @click="startAddBranch">
|
||||
<el-icon :size="20" color="#409eff">
|
||||
<el-icon :size="40" color="#000000">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 取消按钮(输入框显示时) -->
|
||||
<div v-else class="external-add-btn cancel-btn" @click="cancelAddBranch">
|
||||
<el-icon :size="20" color="#f56c6c">
|
||||
<el-icon :size="40" color="#f56c6c">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 外部输入框 -->
|
||||
<div v-if="isAddingBranch" class="external-input-container">
|
||||
<el-input
|
||||
v-model="branchInput"
|
||||
placeholder="输入分支需求..."
|
||||
size="small"
|
||||
@keydown="handleBranchKeydown"
|
||||
<BranchInput
|
||||
v-if="isAddingBranch"
|
||||
ref="branchInputRef"
|
||||
class="branch-input"
|
||||
>
|
||||
<template #suffix>
|
||||
<svg-icon
|
||||
icon-class="paper-plane"
|
||||
size="16px"
|
||||
color="#409eff"
|
||||
class="submit-icon"
|
||||
:class="{ 'is-disabled': !branchInput.trim() }"
|
||||
@click="submitBranch"
|
||||
@submit="handleBranchSubmit"
|
||||
@cancel="handleBranchCancel"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,85 +83,83 @@ import { CirclePlus, Remove } from '@element-plus/icons-vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { type IRawStepTask } from '@/stores'
|
||||
import { getAgentMapIcon } from '@/layout/components/config'
|
||||
import BranchInput from '@/components/BranchInput/index.vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
interface RootNodeData {
|
||||
goal: string
|
||||
initialInput?: string[] | string
|
||||
isRoot?: boolean
|
||||
}
|
||||
|
||||
interface TaskNodeData {
|
||||
task: IRawStepTask
|
||||
isEditing: boolean
|
||||
editingContent: string
|
||||
isEditing?: boolean
|
||||
editingContent?: string
|
||||
updateKey?: number
|
||||
}
|
||||
|
||||
// 使用更宽松的类型定义来避免类型错误
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
data: TaskNodeData
|
||||
data: RootNodeData | TaskNodeData
|
||||
isAddingBranch?: boolean
|
||||
isBranchSelected?: boolean // 是否属于选中的分支路径
|
||||
isBranchSelected?: boolean
|
||||
isDeletable?: boolean
|
||||
branchId?: string
|
||||
[key: string]: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'edit-task', nodeId: string): void
|
||||
(e: 'save-task', nodeId: string, content: string): void
|
||||
(e: 'cancel-edit', nodeId: string): void
|
||||
(e: 'add-branch', nodeId: string, branchContent: string): void
|
||||
(e: 'start-add-branch', nodeId: string): void
|
||||
(e: 'cancel-add-branch'): void
|
||||
(e: 'delete-branch', branchId: string): void
|
||||
}>()
|
||||
|
||||
const editingContent = ref(props.data.task.TaskContent || '')
|
||||
|
||||
// 分支添加相关状态(使用父组件传入的 prop)
|
||||
const branchInput = ref('')
|
||||
const branchInputRef = ref<InstanceType<typeof HTMLInputElement>>()
|
||||
|
||||
// 计算属性,使用父组件传入的状态
|
||||
const isAddingBranch = computed(() => props.isAddingBranch || false)
|
||||
|
||||
const isEditing = computed({
|
||||
get: () => props.data.isEditing,
|
||||
set: value => {
|
||||
if (!value) {
|
||||
emit('cancel-edit', props.id)
|
||||
}
|
||||
}
|
||||
// 判断是否为根节点
|
||||
const isRoot = computed(() => {
|
||||
const data = props.data as RootNodeData
|
||||
return !!data.goal || data.isRoot
|
||||
})
|
||||
|
||||
const isActive = computed(() => {
|
||||
return props.data.task.StepName === props.data.task.StepName
|
||||
// 根节点数据
|
||||
const goal = computed(() => {
|
||||
const data = props.data as RootNodeData
|
||||
return data.goal || ''
|
||||
})
|
||||
|
||||
const task = computed(() => props.data.task)
|
||||
const initialInput = computed(() => {
|
||||
const data = props.data as RootNodeData
|
||||
return data.initialInput
|
||||
})
|
||||
|
||||
const displayInput = computed(() => {
|
||||
if (!initialInput.value) return false
|
||||
if (Array.isArray(initialInput.value)) {
|
||||
return initialInput.value.length > 0
|
||||
}
|
||||
return initialInput.value.trim().length > 0
|
||||
})
|
||||
|
||||
// 任务节点数据
|
||||
const task = computed(() => {
|
||||
const data = props.data as TaskNodeData
|
||||
return data.task
|
||||
})
|
||||
|
||||
const getAgentIcon = (agentName: string) => {
|
||||
return getAgentMapIcon(agentName)
|
||||
}
|
||||
|
||||
const startEdit = () => {
|
||||
emit('edit-task', props.id)
|
||||
}
|
||||
// 分支输入框组件引用
|
||||
const branchInputRef = ref<InstanceType<typeof BranchInput>>()
|
||||
|
||||
const saveEdit = () => {
|
||||
emit('save-task', props.id, editingContent.value)
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
emit('cancel-edit', props.id)
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveEdit()
|
||||
} else if (event.key === 'Escape') {
|
||||
cancelEdit()
|
||||
}
|
||||
}
|
||||
// 计算属性,使用父组件传入的状态
|
||||
const isAddingBranch = computed(() => props.isAddingBranch || false)
|
||||
|
||||
// 分支添加相关方法
|
||||
const startAddBranch = () => {
|
||||
emit('start-add-branch', props.id)
|
||||
branchInput.value = ''
|
||||
nextTick(() => {
|
||||
branchInputRef.value?.focus()
|
||||
})
|
||||
@@ -176,64 +167,92 @@ const startAddBranch = () => {
|
||||
|
||||
const cancelAddBranch = () => {
|
||||
emit('cancel-add-branch')
|
||||
branchInput.value = ''
|
||||
}
|
||||
|
||||
const submitBranch = () => {
|
||||
if (branchInput.value.trim()) {
|
||||
emit('add-branch', props.id, branchInput.value.trim())
|
||||
branchInput.value = ''
|
||||
const handleBranchSubmit = (value: string) => {
|
||||
emit('add-branch', props.id, value)
|
||||
}
|
||||
|
||||
const handleBranchCancel = () => {
|
||||
emit('cancel-add-branch')
|
||||
}
|
||||
|
||||
// 删除分支
|
||||
const handleDeleteBranch = () => {
|
||||
if (props.branchId) {
|
||||
emit('delete-branch', props.branchId)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBranchKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
submitBranch()
|
||||
} else if (event.key === 'Escape') {
|
||||
cancelAddBranch()
|
||||
}
|
||||
}
|
||||
// 判断是否显示删除按钮
|
||||
const isDeletable = computed(() => props.isDeletable || false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-node-card {
|
||||
.node-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.node-card {
|
||||
box-sizing: border-box;
|
||||
|
||||
&.root-style {
|
||||
width: 200px;
|
||||
min-height: 100px;
|
||||
background: linear-gradient(var(--color-card-bg-task), var(--color-card-bg-task)) padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(var(--color-card-bg-task), var(--color-card-bg-task)) padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
&.task-style {
|
||||
width: 150px;
|
||||
min-height: 100px;
|
||||
background-color: var(--color-card-bg-task);
|
||||
border: 1px solid var(--color-card-border-task);
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--color-node-border);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-card-bg-task-hover);
|
||||
border-color: var(--color-card-border-hover);
|
||||
border-color: var(--color-node-border);
|
||||
box-shadow: var(--color-card-shadow-hover);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
// 分支选中高亮样式(绿色)
|
||||
// 分支选中高亮样式(渐变边框)
|
||||
&.is-branch-selected {
|
||||
border-color: #67c23a;
|
||||
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.3);
|
||||
background-color: rgba(103, 194, 58, 0.05);
|
||||
background: linear-gradient(var(--color-card-bg-task), var(--color-card-bg-task)) padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border-color: #67c23a;
|
||||
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.4);
|
||||
background-color: rgba(103, 194, 58, 0.08);
|
||||
background: linear-gradient(
|
||||
var(--color-card-bg-task-hover),
|
||||
var(--color-card-bg-task-hover)
|
||||
)
|
||||
padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
box-shadow: var(--color-card-shadow-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-editing {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
@@ -241,42 +260,45 @@ const handleBranchKeydown = (event: KeyboardEvent) => {
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根节点样式
|
||||
.goal-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.goal-label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to right, var(--color-accent), var(--color-accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.goal-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-title-header);
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
// 任务节点样式
|
||||
.task-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--color-text-title-header);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--color-border-separate);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
min-height: 40px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.task-content-editing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
.agents-container {
|
||||
@@ -300,44 +322,11 @@ const handleBranchKeydown = (event: KeyboardEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
.input-outputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-weight: bold;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.agent-tooltip {
|
||||
padding: 8px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
// 编辑器样式
|
||||
.task-content-editor {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
background: transparent;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 节点包装器(用于容纳外部按钮)
|
||||
.task-node-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 外部添加按钮
|
||||
.external-add-btn {
|
||||
position: absolute;
|
||||
@@ -357,16 +346,6 @@ const handleBranchKeydown = (event: KeyboardEvent) => {
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
|
||||
.el-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
@@ -375,41 +354,6 @@ const handleBranchKeydown = (event: KeyboardEvent) => {
|
||||
&.cancel-btn {
|
||||
border-color: #f56c6c;
|
||||
box-shadow: 0 2px 8px rgba(245, 108, 108, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #f56c6c;
|
||||
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 外部输入容器
|
||||
.external-input-container {
|
||||
position: absolute;
|
||||
bottom: -80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 260px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #fff;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
border-top: 1px solid #dcdfe6;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,89 +368,29 @@ const handleBranchKeydown = (event: KeyboardEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
.add-branch-section {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--color-border-separate);
|
||||
// 节点删除按钮
|
||||
.node-delete-btn {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #2d2d2d;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-branch-btn {
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
z-index: 100;
|
||||
|
||||
// 左侧位置
|
||||
&.left {
|
||||
left: -100px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.branch-input-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.branch-input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: transparent;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c4cc;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交图标样式
|
||||
.submit-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active:not(.is-disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.is-adding-branch {
|
||||
.task-node-card {
|
||||
border-color: #409eff;
|
||||
background-color: #0c0c0c;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAgentsStore } from '@/stores'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import PlanModification from './TaskSyllabus/Branch/PlanModification.vue'
|
||||
import AgentAllocation from './TaskSyllabus/components/AgentAllocation.vue'
|
||||
import PlanTask from './TaskProcess/components/PlanTask.vue'
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 面板显示状态
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('outline')
|
||||
|
||||
// 判断是否有流程数据
|
||||
const planReady = computed(() => {
|
||||
return agentsStore.agentRawPlan.data !== undefined
|
||||
})
|
||||
|
||||
// 判断是否选中了任务
|
||||
const hasSelectedTask = computed(() => {
|
||||
return !!agentsStore.currentTask
|
||||
})
|
||||
|
||||
// 标签页配置
|
||||
const tabs = [
|
||||
{ key: 'outline', label: '任务大纲编排', icon: 'One', required: '' },
|
||||
{ key: 'agent', label: '专家智能体评选', icon: 'Two', required: 'selectedTask' },
|
||||
{ key: 'process', label: '执行过程编排', icon: 'Three', required: 'selectedTask' }
|
||||
]
|
||||
|
||||
// 判断标签是否可用
|
||||
const isTabDisabled = (tab: typeof tabs[0]) => {
|
||||
if (tab.required === 'selectedTask') {
|
||||
return !hasSelectedTask.value
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取禁用原因的提示文本
|
||||
const getDisabledReason = (tab: typeof tabs[0]) => {
|
||||
if (tab.required === 'selectedTask' && !hasSelectedTask.value) {
|
||||
return '请先在任务大纲中选中一个任务'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 打开面板
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
// 默认选中可用的第一个标签
|
||||
const firstAvailableTab = tabs.find(t => !isTabDisabled(t))
|
||||
if (firstAvailableTab) {
|
||||
activeTab.value = firstAvailableTab.key
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭面板
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 切换标签
|
||||
const handleTabChange = (tabKey: string) => {
|
||||
activeTab.value = tabKey
|
||||
}
|
||||
|
||||
// 暴露给父组件
|
||||
defineExpose({
|
||||
open,
|
||||
close
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- 面板主体 -->
|
||||
<Transition name="slide-up">
|
||||
<div v-if="visible" class="settings-panel">
|
||||
<!-- 标签栏 -->
|
||||
<div class="panel-header">
|
||||
<div class="tabs-container">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="[
|
||||
'tab-item',
|
||||
{ 'is-active': activeTab === tab.key },
|
||||
{ 'is-disabled': isTabDisabled(tab) }
|
||||
]"
|
||||
:title="getDisabledReason(tab)"
|
||||
@click="!isTabDisabled(tab) && handleTabChange(tab.key)"
|
||||
>
|
||||
<SvgIcon :icon-class="tab.icon" size="18px" />
|
||||
<span class="tab-label">{{ tab.label }}</span>
|
||||
<span v-if="isTabDisabled(tab)" class="tab-disabled-hint">
|
||||
{{ getDisabledReason(tab) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="close-btn" @click="close">
|
||||
<SvgIcon icon-class="close" size="20px" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="panel-content">
|
||||
<!-- 任务大纲 -->
|
||||
<div v-show="activeTab === 'outline'" class="tab-content first-tab">
|
||||
<PlanModification />
|
||||
</div>
|
||||
|
||||
<!-- 智能体 -->
|
||||
<div v-show="activeTab === 'agent'" class="tab-content">
|
||||
<AgentAllocation />
|
||||
</div>
|
||||
|
||||
<!-- 任务过程 -->
|
||||
<div v-show="activeTab === 'process'" class="tab-content">
|
||||
<PlanTask />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 面板主体
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
margin-left: -40%;
|
||||
width: 80%;
|
||||
max-width: 1200px;
|
||||
height: 70vh;
|
||||
background: var(--color-bg-three);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 -4px 30px rgba(0, 0, 0.4);
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 标题栏(包含标签栏)
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 10px 0 30px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
color: var(--color-text-secondary);
|
||||
transition: all 0.3s;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
margin: 0 4px;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-settings-panel-content-bg);
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
&:hover:not(.is-disabled)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
background: var(--color-bg-detail);
|
||||
border-radius: 40px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:deep(.svg-icon) {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&:hover:not(.is-disabled) :deep(.svg-icon) {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
&.is-active :deep(.svg-icon) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-disabled-hint {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-bg-detail);
|
||||
color: var(--color-text-secondary);
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tab-item.is-disabled:hover .tab-disabled-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--color-settings-panel-content-bg);
|
||||
margin: 0 10px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--color-text-secondary);
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
</style>
|
||||
@@ -60,6 +60,10 @@
|
||||
--color-card-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.17);
|
||||
// 任务大纲边框颜色
|
||||
--color-card-border-task: #E0E0E0; //边框颜色#FFFFFF60
|
||||
// 任务大纲节点边框颜色(固定#585858)
|
||||
--color-node-border: #CCCCCC;
|
||||
// 任务大纲节点连线高亮颜色(固定#00C7D2)
|
||||
--color-node-active: #00C7D2;
|
||||
// 执行结果卡片颜色
|
||||
--color-card-bg-result: #ececec;
|
||||
// 执行结果边框颜色
|
||||
@@ -118,6 +122,8 @@
|
||||
--color-task-edit-hover-border: #D6D6D6;
|
||||
// 编辑图标背景色
|
||||
--color-edit-icon-bg: rgba(255, 255, 255, 0.5);
|
||||
// 设置面板内容区背景颜色
|
||||
--color-settings-panel-content-bg: #f7f7f7;
|
||||
|
||||
}
|
||||
|
||||
@@ -179,6 +185,10 @@ html.dark {
|
||||
--color-card-bg-task-hover: #171b22;
|
||||
// 任务大纲边框颜色
|
||||
--color-card-border-task: #151515;
|
||||
// 任务大纲节点边框颜色(固定#585858)
|
||||
--color-node-border: #585858;
|
||||
// 任务大纲节点连线高亮颜色(固定#00C7D2)
|
||||
--color-node-active: #00C7D2;
|
||||
// 执行结果卡片颜色
|
||||
--color-card-bg-result: #1a1a1a;
|
||||
// 执行结果边框颜色
|
||||
@@ -241,6 +251,8 @@ html.dark {
|
||||
--color-task-edit-hover-border: #ADA9A7;
|
||||
// 编辑图标背景色
|
||||
--color-edit-icon-bg: rgba(255, 255, 255, 0.5);
|
||||
// 设置面板内容区背景颜色
|
||||
--color-settings-panel-content-bg: #1c222a;
|
||||
|
||||
--el-fill-color-blank: transparent !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user