feat(task): 添加任务过程编辑功能
- 新增 ProcessCard 组件用于展示和编辑任务流程 - 实现双击编辑任务描述功能 - 添加编辑状态下的卡片式输入界面 - 支持保存和取消编辑操作 - 实现鼠标悬停高亮效果 - 添加颜色处理函数用于界面美化 - 集成到 TaskResult 组件中展示任务过程 - 支持动态创建和管理任务流程连接线 - 添加额外产物编辑功能 - 实现按钮交互状态管理 - 添加滚动和折叠面板事件处理 - 集成 AgentAllocation 组件用于智能体分配 - 实现椭圆框交互效果展示选中状态 - 添加智能体等级颜色配置 - 支持智能体选中状态切换和排序
This commit is contained in:
parent
5dace5f788
commit
907310365a
@ -0,0 +1,283 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { getActionTypeDisplay } from '@/layout/components/config.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
step: {
|
||||
Id?: string
|
||||
TaskProcess: Array<{
|
||||
ID: string
|
||||
ActionType: string
|
||||
AgentName: string
|
||||
Description: string
|
||||
}>
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-edit', stepId: string, processId: string): void
|
||||
(e: 'save-edit', stepId: string, processId: string, value: string): void
|
||||
}>()
|
||||
|
||||
// 当前正在编辑的process ID
|
||||
const editingProcessId = ref<string | null>(null)
|
||||
// 编辑框的值
|
||||
const editValue = ref('')
|
||||
// 鼠标悬停的process ID
|
||||
const hoverProcessId = ref<string | null>(null)
|
||||
|
||||
// 获取颜色浅两号的函数
|
||||
function getLightColor(color: string, level: number = 2): string {
|
||||
if (!color || color.length !== 7 || color[0] !== '#') return color
|
||||
|
||||
const r = parseInt(color.substr(1, 2), 16)
|
||||
const g = parseInt(color.substr(3, 2), 16)
|
||||
const b = parseInt(color.substr(5, 2), 16)
|
||||
|
||||
// 增加亮度(浅两号)
|
||||
const lightenAmount = level * 20
|
||||
const newR = Math.min(255, r + lightenAmount)
|
||||
const newG = Math.min(255, g + lightenAmount)
|
||||
const newB = Math.min(255, b + lightenAmount)
|
||||
|
||||
return `#${Math.round(newR).toString(16).padStart(2, '0')}${Math.round(newG)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}${Math.round(newB).toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 处理鼠标进入
|
||||
function handleMouseEnter(processId: string) {
|
||||
hoverProcessId.value = processId
|
||||
}
|
||||
|
||||
// 处理鼠标离开
|
||||
function handleMouseLeave() {
|
||||
hoverProcessId.value = null
|
||||
}
|
||||
|
||||
// 处理双击编辑(针对单个process)
|
||||
function handleDblClick(processId: string, currentDescription: string) {
|
||||
editingProcessId.value = processId
|
||||
editValue.value = currentDescription
|
||||
emit('open-edit', props.step.Id || '', processId)
|
||||
}
|
||||
|
||||
// 处理保存编辑
|
||||
function handleSave(processId: string) {
|
||||
if (!editingProcessId.value) return
|
||||
|
||||
emit('save-edit', props.step.Id || '', processId, editValue.value)
|
||||
editingProcessId.value = null
|
||||
editValue.value = ''
|
||||
}
|
||||
|
||||
// 处理取消编辑
|
||||
function handleCancel() {
|
||||
editingProcessId.value = null
|
||||
editValue.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="process-card">
|
||||
<div class="process-content">
|
||||
<!-- 显示模式 -->
|
||||
<div class="display-content">
|
||||
<span
|
||||
v-for="process in step.TaskProcess"
|
||||
:key="process.ID"
|
||||
class="process-segment"
|
||||
@mouseenter="handleMouseEnter(process.ID)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<span
|
||||
class="agent-name"
|
||||
:style="{
|
||||
backgroundColor: getActionTypeDisplay(process.ActionType)?.color || '#909399',
|
||||
color: '#fff',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
marginRight: '4px'
|
||||
}"
|
||||
>
|
||||
{{ process.AgentName }}
|
||||
</span>
|
||||
|
||||
<!-- 编辑模式 - 修改为卡片样式 -->
|
||||
<div v-if="editingProcessId === process.ID" class="edit-container">
|
||||
<div class="edit-card">
|
||||
<el-input
|
||||
v-model="editValue"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="请输入描述内容"
|
||||
autofocus
|
||||
/>
|
||||
<div class="edit-buttons">
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
icon="Check"
|
||||
@click="handleSave(process.ID)"
|
||||
title="保存"
|
||||
>
|
||||
√
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="Close"
|
||||
@click="handleCancel"
|
||||
title="取消"
|
||||
>
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示模式 -->
|
||||
<span
|
||||
v-else
|
||||
class="process-description"
|
||||
:class="{ hovered: hoverProcessId === process.ID }"
|
||||
:style="{
|
||||
border: `1px solid ${getActionTypeDisplay(process.ActionType)?.border}`,
|
||||
backgroundColor:
|
||||
hoverProcessId === process.ID
|
||||
? getLightColor(getActionTypeDisplay(process.ActionType)?.color || '#909399')
|
||||
: 'transparent'
|
||||
}"
|
||||
@dblclick="handleDblClick(process.ID, process.Description)"
|
||||
>
|
||||
{{ process.Description }}
|
||||
</span>
|
||||
|
||||
<span class="separator" v-if="!process.Description.endsWith('。')">。</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.process-card {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-list);
|
||||
border: 1px solid var(--color-border-default);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.process-content {
|
||||
min-height: 20px;
|
||||
|
||||
.display-content {
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.process-segment {
|
||||
display: inline;
|
||||
position: relative;
|
||||
|
||||
.agent-name {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.edit-container {
|
||||
display: block; // 改为块级元素,使其换行显示
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.edit-card {
|
||||
position: relative;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
|
||||
:deep(.el-textarea) {
|
||||
width: 100%;
|
||||
|
||||
.el-textarea__inner {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-buttons {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.el-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
|
||||
&:first-child {
|
||||
background-color: #67c23a;
|
||||
border-color: #67c23a;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background-color: #f56c6c;
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-description {
|
||||
display: inline;
|
||||
white-space: normal;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&.hovered {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:last-child .separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-card:hover {
|
||||
border-color: var(--el-border-color);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,861 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
|
||||
import variables from '@/styles/variables.module.scss'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import api from '@/api'
|
||||
import ProcessCard from '../TaskProcess/ProcessCard.vue'
|
||||
import ExecutePlan from './ExecutePlan.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refreshLine'): void
|
||||
(el: 'setCurrentTask', task: IRawStepTask): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const drawerVisible = ref(false)
|
||||
const collaborationProcess = computed(() => {
|
||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
||||
})
|
||||
|
||||
// 编辑逻辑
|
||||
const editMode = ref(false) //全局编辑开关
|
||||
const editMap = reactive<Record<string, boolean>>({}) //行级编辑状态
|
||||
const editBuffer = reactive<Record<string, string | undefined>>({}) //临时输入
|
||||
|
||||
function getProcessDescription(stepId: string, processId: string) {
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
return process?.Description || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function save() {
|
||||
Object.keys(editMap).forEach(key => {
|
||||
if (editMap[key]) {
|
||||
const [stepId, processId] = key.split('-')
|
||||
const value = editBuffer[key]
|
||||
// 确保 value 是字符串类型
|
||||
if (value !== undefined && value !== null) {
|
||||
// @ts-ignore - TypeScript 无法正确推断类型,但运行时是安全的
|
||||
handleSaveEdit(stepId, processId, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
editMode.value = false
|
||||
}
|
||||
|
||||
function handleOpenEdit(stepId: string, processId: string) {
|
||||
if (!editMode.value) return
|
||||
const key = `${stepId}-${processId}`
|
||||
editMap[key] = true
|
||||
editBuffer[key] = getProcessDescription(stepId, processId)
|
||||
}
|
||||
|
||||
function handleSaveEdit(stepId: string, processId: string, value: string) {
|
||||
const key = `${stepId}-${processId}`
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
if (process) {
|
||||
process.Description = value
|
||||
}
|
||||
}
|
||||
editMap[key] = false
|
||||
ElMessage.success('已保存(前端内存)')
|
||||
}
|
||||
const jsplumb = new Jsplumb('task-results-main', {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: { curviness: 30, stub: 10 }
|
||||
}
|
||||
})
|
||||
|
||||
// 操作折叠面板时要实时的刷新连线
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
function handleCollapse() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
jsplumb.repaintEverything()
|
||||
emit('refreshLine')
|
||||
}, 1) as ReturnType<typeof setInterval>
|
||||
|
||||
// 默认三秒后已经完全打开
|
||||
const timer1 = setTimeout(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (timer1) {
|
||||
clearInterval(timer1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建内部连线
|
||||
function createInternalLine(id?: string) {
|
||||
const arr: ConnectArg[] = []
|
||||
jsplumb.reset()
|
||||
collaborationProcess.value.forEach(item => {
|
||||
// 创建左侧流程与产出的连线
|
||||
arr.push({
|
||||
sourceId: `task-results-${item.Id}-0`,
|
||||
targetId: `task-results-${item.Id}-1`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left]
|
||||
})
|
||||
collaborationProcess.value.forEach(jitem => {
|
||||
// 创建左侧产出与上一步流程的连线
|
||||
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
|
||||
arr.push({
|
||||
sourceId: `task-results-${jitem.Id}-1`,
|
||||
targetId: `task-results-${item.Id}-0`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left],
|
||||
config: {
|
||||
type: 'output'
|
||||
}
|
||||
})
|
||||
}
|
||||
// 创建右侧任务程序与InputObject字段的连线
|
||||
jitem.TaskProcess.forEach(i => {
|
||||
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-1`
|
||||
const targetId = `task-results-${jitem.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 创建右侧TaskProcess内部连线
|
||||
item.TaskProcess?.forEach(i => {
|
||||
if (!i.ImportantInput?.length) {
|
||||
return
|
||||
}
|
||||
item.TaskProcess?.forEach(i2 => {
|
||||
if (i.ImportantInput.includes(`ActionResult:${i2.ID}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-0-${i2.ID}`
|
||||
const targetId = `task-results-${item.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
jsplumb.connects(arr)
|
||||
jsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 额外产物编辑状态
|
||||
const editingOutputId = ref<string | null>(null)
|
||||
const editingOutputContent = ref('')
|
||||
|
||||
// 额外产物内容存储
|
||||
const additionalOutputContents = ref<Record<string, string>>({})
|
||||
|
||||
async function handleRun() {
|
||||
try {
|
||||
loading.value = true
|
||||
const d = await api.executePlan(agentsStore.agentRawPlan.data!)
|
||||
agentsStore.setExecutePlan(d)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务过程
|
||||
async function handleTaskProcess() {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
// 开始编辑额外产物内容
|
||||
function startOutputEditing(output: string) {
|
||||
editingOutputId.value = output
|
||||
editingOutputContent.value = getAdditionalOutputContent(output) || ''
|
||||
}
|
||||
|
||||
// 保存额外产物内容
|
||||
function saveOutputEditing() {
|
||||
if (editingOutputId.value && editingOutputContent.value.trim()) {
|
||||
additionalOutputContents.value[editingOutputId.value] = editingOutputContent.value.trim()
|
||||
}
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 取消编辑额外产物内容
|
||||
function cancelOutputEditing() {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 获取额外产物内容
|
||||
function getAdditionalOutputContent(output: string) {
|
||||
return additionalOutputContents.value[output] || ''
|
||||
}
|
||||
|
||||
// 处理额外产物的键盘事件
|
||||
function handleOutputKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveOutputEditing()
|
||||
} else if (event.key === 'Escape') {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 添加滚动状态标识
|
||||
const isScrolling = ref(false)
|
||||
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 修改滚动处理函数
|
||||
function handleScroll() {
|
||||
isScrolling.value = true
|
||||
emit('refreshLine')
|
||||
// 清除之前的定时器
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer)
|
||||
}
|
||||
jsplumb.repaintEverything()
|
||||
|
||||
// 设置滚动结束检测
|
||||
scrollTimer = setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 300) as ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
// 修改鼠标事件处理函数
|
||||
const handleMouseEnter = throttle(id => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine(id)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const handleMouseLeave = throttle(() => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
function clear() {
|
||||
jsplumb.reset()
|
||||
}
|
||||
|
||||
// ========== 按钮交互状态管理 ==========
|
||||
const buttonHoverState = ref(null) // null | 'process' | 'execute'
|
||||
|
||||
const handleProcessMouseEnter = () => {
|
||||
buttonHoverState.value = 'process'
|
||||
}
|
||||
|
||||
const handleExecuteMouseEnter = () => {
|
||||
if (agentsStore.agentRawPlan.data) {
|
||||
buttonHoverState.value = 'execute'
|
||||
}
|
||||
}
|
||||
|
||||
const handleButtonMouseLeave = () => {
|
||||
setTimeout(() => {
|
||||
buttonHoverState.value = null
|
||||
}, 150)
|
||||
}
|
||||
|
||||
// 计算按钮类名
|
||||
const processBtnClass = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
const executeBtnClass = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮变圆形
|
||||
if (buttonHoverState.value === 'process') {
|
||||
return 'circle'
|
||||
}
|
||||
// 其他情况:如果有任务数据就显示椭圆形,否则显示圆形
|
||||
return agentsStore.agentRawPlan.data ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
// 计算按钮是否显示文字
|
||||
const showProcessText = computed(() => {
|
||||
return buttonHoverState.value === 'process'
|
||||
})
|
||||
|
||||
const showExecuteText = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮不显示文字
|
||||
if (buttonHoverState.value === 'process') return false
|
||||
// 其他情况:如果有任务数据就显示文字,否则不显示
|
||||
return agentsStore.agentRawPlan.data
|
||||
})
|
||||
|
||||
// 计算按钮标题
|
||||
const processBtnTitle = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? '任务过程' : '点击查看任务过程'
|
||||
})
|
||||
|
||||
const executeBtnTitle = computed(() => {
|
||||
return showExecuteText.value ? '任务执行' : '点击运行'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
createInternalLine,
|
||||
clear
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full flex flex-col relative"
|
||||
id="task-results"
|
||||
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
|
||||
>
|
||||
<!-- 标题与执行按钮 -->
|
||||
<div class="text-[18px] font-bold mb-[18px] flex justify-between items-center px-[20px]">
|
||||
<span class="text-[var(--color-text-title-header)]">执行结果</span>
|
||||
<div
|
||||
class="flex items-center gap-[14px] task-button-group"
|
||||
@mouseleave="handleButtonMouseLeave"
|
||||
>
|
||||
<!-- 任务过程按钮 -->
|
||||
<el-button
|
||||
:class="processBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="processBtnTitle"
|
||||
@mouseenter="handleProcessMouseEnter"
|
||||
@click="handleTaskProcess"
|
||||
>
|
||||
<svg-icon icon-class="process" />
|
||||
<span v-if="showProcessText" class="btn-text">任务过程</span>
|
||||
</el-button>
|
||||
|
||||
<!-- 任务执行按钮 -->
|
||||
<el-popover
|
||||
:disabled="Boolean(agentsStore.agentRawPlan.data)"
|
||||
title="请先输入要执行的任务"
|
||||
:visible="showPopover"
|
||||
@hide="showPopover = false"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
:class="executeBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="executeBtnTitle"
|
||||
:disabled="!agentsStore.agentRawPlan.data"
|
||||
@mouseenter="handleExecuteMouseEnter"
|
||||
@click="handleRun"
|
||||
>
|
||||
<svg-icon icon-class="action" />
|
||||
<span v-if="showExecuteText" class="btn-text">任务执行</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="任务过程"
|
||||
direction="rtl"
|
||||
size="30%"
|
||||
:destroy-on-close="false"
|
||||
>
|
||||
<!-- 头部工具栏 -->
|
||||
<template #header>
|
||||
<div class="drawer-header">
|
||||
<span class="title">任务过程</span>
|
||||
<!-- <el-button v-if="!editMode" text icon="Edit" @click="editMode = true" />
|
||||
<el-button v-else text icon="Check" @click="save" /> -->
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar height="calc(100vh - 120px)">
|
||||
<el-empty v-if="!collaborationProcess.length" description="暂无任务过程" />
|
||||
<div v-else class="process-list">
|
||||
<!-- 使用ProcessCard组件显示每个AgentSelection -->
|
||||
<ProcessCard
|
||||
v-for="step in collaborationProcess"
|
||||
:key="step.Id"
|
||||
:step="step"
|
||||
@open-edit="handleOpenEdit"
|
||||
@save-edit="handleSaveEdit"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 内容 -->
|
||||
<div
|
||||
v-loading="agentsStore.agentRawPlan.loading"
|
||||
class="flex-1 overflow-auto relative"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div id="task-results-main" class="px-[40px] relative">
|
||||
<!-- 原有的流程和产物 -->
|
||||
<div v-for="item in collaborationProcess" :key="item.Id" class="card-item">
|
||||
<el-card
|
||||
class="card-item w-full relative"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:shadow="true"
|
||||
:id="`task-results-${item.Id}-0`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<div class="text-[18px] mb-[15px]">{{ item.StepName }}</div>
|
||||
<!-- 折叠面板 -->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
v-for="item1 in item.TaskProcess"
|
||||
:key="`task-results-${item.Id}-${item1.ID}`"
|
||||
:name="`task-results-${item.Id}-${item1.ID}`"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
|
||||
<div
|
||||
class="flex items-center gap-[15px] rounded-[20px]"
|
||||
:class="{
|
||||
'bg-[var(--color-bg-detail-list)]': !agentsStore.executePlan.length,
|
||||
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
|
||||
}"
|
||||
>
|
||||
<!-- 右侧链接点 -->
|
||||
<div
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2"
|
||||
:id="`task-results-${item.Id}-0-${item1.ID}`"
|
||||
></div>
|
||||
<div
|
||||
class="w-[41px] h-[41px] rounded-full flex items-center justify-center"
|
||||
:style="{ background: getAgentMapIcon(item1.AgentName).color }"
|
||||
>
|
||||
<svg-icon
|
||||
:icon-class="getAgentMapIcon(item1.AgentName).icon"
|
||||
color="#fff"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-[16px]">
|
||||
<span>{{ item1.AgentName }}: </span>
|
||||
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
|
||||
{{ getActionTypeDisplay(item1.ActionType)?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:action-id="item1.ID"
|
||||
:node-id="item.StepName"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card"
|
||||
:shadow="true"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:id="`task-results-${item.Id}-1`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
class="output-object"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="text-[18px]">{{ item.OutputObject }}</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:node-id="item.OutputObject"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<div
|
||||
v-for="(output, index) in agentsStore.additionalOutputs"
|
||||
:key="`additional-output-${index}`"
|
||||
class="card-item"
|
||||
>
|
||||
<!-- 空的流程卡片位置 -->
|
||||
<div class="w-full"></div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card additional-output-card"
|
||||
:shadow="false"
|
||||
:id="`additional-output-results-${index}`"
|
||||
>
|
||||
<!-- 产物名称行 -->
|
||||
<div class="text-[18px] mb-3">
|
||||
{{ output }}
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域行 -->
|
||||
<div class="additional-output-editor">
|
||||
<div v-if="editingOutputId === output" class="w-full">
|
||||
<!-- 编辑状态:输入框 + 按钮 -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<el-input
|
||||
v-model="editingOutputContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="请输入产物内容"
|
||||
@keydown="handleOutputKeydown"
|
||||
class="output-editor"
|
||||
size="small"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="saveOutputEditing" type="primary" size="small" class="px-3">
|
||||
√
|
||||
</el-button>
|
||||
<el-button @click="cancelOutputEditing" size="small" class="px-3">
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full">
|
||||
<!-- 非编辑状态:折叠区域 + 编辑按钮 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-3 bg-[var(--color-bg-quinary)] rounded-[8px]"
|
||||
>
|
||||
<div
|
||||
class="text-[14px] text-[var(--color-text-secondary)] output-content-display"
|
||||
>
|
||||
{{ getAdditionalOutputContent(output) || '暂无内容,点击编辑' }}
|
||||
</div>
|
||||
<el-button
|
||||
@click="startOutputEditing(output)"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<svg-icon icon-class="action" size="12px" />
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#task-results.is-running {
|
||||
--color-bg-detail-list: var(--color-bg-detail-list-run); // 直接指向 100 % 版本
|
||||
}
|
||||
#task-results {
|
||||
:deep(.el-collapse) {
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
.el-collapse-item + .el-collapse-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list-run);
|
||||
min-height: 41px;
|
||||
line-height: 41px;
|
||||
border-radius: 20px;
|
||||
transition: border-radius 1ms;
|
||||
position: relative;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: var(--color-bg-detail-list);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
background: var(--color-bg-icon-rotate);
|
||||
border-radius: 50px;
|
||||
color: #d8d8d8;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.output-object {
|
||||
.el-collapse-item__header {
|
||||
background: none;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: none;
|
||||
|
||||
.card-item {
|
||||
background: var(--color-bg-detail);
|
||||
padding: 25px;
|
||||
padding-top: 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list);
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card) {
|
||||
.el-card__body {
|
||||
padding-right: 40px;
|
||||
background-color: var(--color-bg-detail);
|
||||
&:hover {
|
||||
background-color: var(--color-card-bg-result-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.output-object-card {
|
||||
:deep(.el-card__body) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.active-card {
|
||||
background: linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
}
|
||||
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.additional-output-card {
|
||||
border: 1px dashed #dcdfe6;
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--color-agent-list-hover-shadow);
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 编辑区域样式调整
|
||||
.el-collapse {
|
||||
border: none;
|
||||
|
||||
.el-collapse-item {
|
||||
.el-collapse-item__header {
|
||||
background: var(--color-bg-detail);
|
||||
min-height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 8px;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: var(--color-bg-detail);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 额外产物编辑区域样式
|
||||
.additional-output-editor {
|
||||
.output-editor {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-detail);
|
||||
border: 1px solid #dcdfe6;
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.output-content-display {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.5;
|
||||
min-height: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑按钮样式
|
||||
.el-button {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 新增:按钮交互样式 ==========
|
||||
.task-button-group {
|
||||
.el-button {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
||||
overflow: hidden !important;
|
||||
white-space: nowrap !important;
|
||||
border: none !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
position: relative;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
&:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
|
||||
&:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 圆形状态
|
||||
.circle {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
min-width: 40px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
|
||||
.btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 椭圆形状态
|
||||
.ellipse {
|
||||
height: 40px !important;
|
||||
border-radius: 20px !important;
|
||||
padding: 0 16px !important;
|
||||
gap: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: inline-block !important;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,284 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAgentsStore } from '@/stores/modules/agents'
|
||||
import { getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 获取当前选中任务的智能体列表
|
||||
const currentTaskAgents = computed(() => {
|
||||
return agentsStore.currentTask?.AgentSelection || []
|
||||
})
|
||||
|
||||
// 获取智能体列表
|
||||
const agents = computed(() => agentsStore.agents)
|
||||
|
||||
// 选中状态管理 - 初始选中当前任务的智能体
|
||||
const selectedAgents = ref<string[]>([])
|
||||
|
||||
// 初始化选中当前任务的智能体
|
||||
const initializeSelectedAgents = () => {
|
||||
selectedAgents.value = [...currentTaskAgents.value]
|
||||
}
|
||||
|
||||
// 切换选中状态
|
||||
const toggleSelectAgent = (agentName: string) => {
|
||||
const index = selectedAgents.value.indexOf(agentName)
|
||||
if (index > -1) {
|
||||
// 如果已选中,则取消选中
|
||||
selectedAgents.value.splice(index, 1)
|
||||
} else {
|
||||
// 如果未选中,则添加到选中列表末尾
|
||||
selectedAgents.value.push(agentName)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否选中
|
||||
const isAgentSelected = (agentName: string) => {
|
||||
return selectedAgents.value.includes(agentName)
|
||||
}
|
||||
|
||||
// 获取排序后的智能体列表 - 选中的智能体排在前面,保持原有顺序
|
||||
const sortedAgents = computed(() => {
|
||||
const selected = agents.value.filter(agent => selectedAgents.value.includes(agent.Name))
|
||||
const unselected = agents.value.filter(agent => !selectedAgents.value.includes(agent.Name))
|
||||
return [...selected, ...unselected]
|
||||
})
|
||||
|
||||
// 颜色等级配置 - 从蓝色到白色的5个等级
|
||||
const colorLevels = [
|
||||
{ level: 5, color: '#1d59dc', textColor: '#FFFFFF' }, // 深蓝色
|
||||
{ level: 4, color: '#4a71c7', textColor: '#FFFFFF' }, // 中蓝色
|
||||
{ level: 3, color: '#6c8ed7', textColor: '#333333' }, // 浅蓝色
|
||||
{ level: 2, color: '#9cb7f0', textColor: '#333333' }, // 更浅蓝
|
||||
{ level: 1, color: '#c8d9fc', textColor: '#333333' } // 接近白色
|
||||
]
|
||||
|
||||
// 为每个智能体分配随机等级(实际应用中应该根据业务逻辑分配)
|
||||
const getAgentLevel = (agentIndex: number) => {
|
||||
// 这里使用简单的算法分配等级,实际应用中应该根据智能体的能力、经验等分配
|
||||
return ((agentIndex % 5) + 1) % 6
|
||||
}
|
||||
|
||||
// 获取对应等级的颜色配置
|
||||
const getLevelConfig = (level: number) => {
|
||||
return colorLevels.find(config => config.level === level) || colorLevels[0]
|
||||
}
|
||||
|
||||
// 组件挂载时初始化选中状态
|
||||
onMounted(() => {
|
||||
initializeSelectedAgents()
|
||||
})
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-allocation">
|
||||
<!-- 头部 -->
|
||||
<div class="allocation-header">
|
||||
<span class="header-title">智能体分配</span>
|
||||
<el-button class="close-button" text circle @click="handleClose" title="关闭">
|
||||
<span class="close-icon">❌️</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<el-divider class="header-divider" />
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="allocation-content">
|
||||
<!-- 智能体整体容器 -->
|
||||
<div class="agents-container selectagent">
|
||||
<div
|
||||
v-for="(agent, index) in agents"
|
||||
:key="agent.Name"
|
||||
class="agent-item"
|
||||
:title="agent.Name"
|
||||
@click="toggleSelectAgent(agent.Name)"
|
||||
>
|
||||
<!-- 头像部分(独立元素) -->
|
||||
<div class="agent-avatar" :style="{ background: getAgentMapIcon(agent.Name).color }">
|
||||
<SvgIcon :icon-class="getAgentMapIcon(agent.Name).icon" size="24px" color="#fff" />
|
||||
</div>
|
||||
<!-- 等级色值部分(独立元素,通过CSS定位跟随头像) -->
|
||||
<div
|
||||
class="color-level"
|
||||
:style="{
|
||||
backgroundColor: getLevelConfig(getAgentLevel(index)).color,
|
||||
color: getLevelConfig(getAgentLevel(index)).textColor
|
||||
}"
|
||||
>
|
||||
{{ getAgentLevel(index) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="agents.length === 0" class="empty-state">
|
||||
<el-empty description="暂无智能体数据" />
|
||||
<div class="empty-tip">请先在智能体库中上传智能体信息</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-allocation {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.allocation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-title);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
padding: 4px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-divider {
|
||||
margin: 0;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.allocation-content {
|
||||
flex: 1;
|
||||
padding: 30px 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; // 改为靠左对齐
|
||||
gap: 20px;
|
||||
|
||||
.agents-container {
|
||||
display: flex;
|
||||
gap: 0; // 确保容器内元素之间没有间隙
|
||||
justify-content: flex-start; // 靠左对齐
|
||||
align-items: flex-start; // 改为顶部对齐
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
|
||||
.agent-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 0px; // 确保item之间没有水平间距
|
||||
padding: 0; // 移除内边距
|
||||
|
||||
.agent-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--color-border);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
margin-top: 8px; // 头像和等级卡片之间的垂直间距
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
width: 100%;
|
||||
|
||||
.empty-tip {
|
||||
margin-top: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
padding: 20px 16px;
|
||||
gap: 16px;
|
||||
|
||||
.agents-container {
|
||||
.agent-item {
|
||||
margin: 0 6px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
margin-top: 8px; // 保持一致的垂直间距
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 480px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
.agents-container {
|
||||
.agent-item {
|
||||
margin: 0 4px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 12px;
|
||||
margin-top: 8px; // 保持一致的垂直间距
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,505 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useAgentsStore } from '@/stores/modules/agents'
|
||||
import { getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 获取当前选中任务的智能体列表
|
||||
const currentTaskAgents = computed(() => {
|
||||
return agentsStore.currentTask?.AgentSelection || []
|
||||
})
|
||||
|
||||
// 获取智能体列表
|
||||
const agents = computed(() => agentsStore.agents)
|
||||
|
||||
// 选中状态管理 - 初始选中当前任务的智能体
|
||||
const selectedAgents = ref<string[]>([])
|
||||
|
||||
// 初始化选中当前任务的智能体
|
||||
const initializeSelectedAgents = () => {
|
||||
selectedAgents.value = [...currentTaskAgents.value]
|
||||
}
|
||||
|
||||
// 切换选中状态
|
||||
const toggleSelectAgent = (agentName: string) => {
|
||||
const index = selectedAgents.value.indexOf(agentName)
|
||||
if (index > -1) {
|
||||
// 如果已选中,则取消选中
|
||||
selectedAgents.value.splice(index, 1)
|
||||
} else {
|
||||
// 如果未选中,则添加到选中列表末尾
|
||||
selectedAgents.value.push(agentName)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否选中
|
||||
const isAgentSelected = (agentName: string) => {
|
||||
return selectedAgents.value.includes(agentName)
|
||||
}
|
||||
|
||||
// 获取排序后的智能体列表 - 选中的智能体排在前面,保持原有顺序
|
||||
const sortedAgents = computed(() => {
|
||||
const selected = agents.value.filter(agent => selectedAgents.value.includes(agent.Name))
|
||||
const unselected = agents.value.filter(agent => !selectedAgents.value.includes(agent.Name))
|
||||
return [...selected, ...unselected]
|
||||
})
|
||||
|
||||
// 颜色等级配置 - 从蓝色到白色的5个等级
|
||||
const colorLevels = [
|
||||
{ level: 5, color: '#1d59dc', textColor: '#FFFFFF' }, // 深蓝色
|
||||
{ level: 4, color: '#4a71c7', textColor: '#FFFFFF' }, // 中蓝色
|
||||
{ level: 3, color: '#6c8ed7', textColor: '#333333' }, // 浅蓝色
|
||||
{ level: 2, color: '#9cb7f0', textColor: '#333333' }, // 更浅蓝
|
||||
{ level: 1, color: '#c8d9fc', textColor: '#333333' } // 接近白色
|
||||
]
|
||||
|
||||
// 为每个智能体分配随机等级(实际应用中应该根据业务逻辑分配)
|
||||
const getAgentLevel = (agentIndex: number) => {
|
||||
// 这里使用简单的算法分配等级,实际应用中应该根据智能体的能力、经验等分配
|
||||
return ((agentIndex % 5) + 1) % 6
|
||||
}
|
||||
|
||||
// 获取对应等级的颜色配置
|
||||
const getLevelConfig = (level: number) => {
|
||||
return colorLevels.find(config => config.level === level) || colorLevels[0]
|
||||
}
|
||||
|
||||
// ========== 新增:椭圆框交互逻辑 ==========
|
||||
const ellipseBoxRef = ref<HTMLElement>()
|
||||
const isEllipseVisible = ref(false)
|
||||
const ellipseStyle = ref({
|
||||
width: '0px',
|
||||
height: '0px',
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
opacity: 0
|
||||
})
|
||||
|
||||
// 椭圆框动画配置
|
||||
const ellipseAnimationConfig = {
|
||||
duration: 300,
|
||||
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
||||
}
|
||||
|
||||
// 更新椭圆框位置和大小
|
||||
const updateEllipseBox = () => {
|
||||
if (!ellipseBoxRef.value || selectedAgents.value.length === 0) {
|
||||
isEllipseVisible.value = false
|
||||
ellipseStyle.value.opacity = 0
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有选中头像的位置
|
||||
const selectedAvatars = document.querySelectorAll('.agent-item.selected .agent-avatar')
|
||||
|
||||
if (selectedAvatars.length === 0) {
|
||||
isEllipseVisible.value = false
|
||||
ellipseStyle.value.opacity = 0
|
||||
return
|
||||
}
|
||||
|
||||
// 计算椭圆框的位置和尺寸
|
||||
let minLeft = Infinity
|
||||
let maxRight = -Infinity
|
||||
let minTop = Infinity
|
||||
let maxBottom = -Infinity
|
||||
|
||||
selectedAvatars.forEach(avatar => {
|
||||
const rect = avatar.getBoundingClientRect()
|
||||
const containerRect = ellipseBoxRef.value!.parentElement!.getBoundingClientRect()
|
||||
|
||||
const left = rect.left - containerRect.left
|
||||
const right = rect.right - containerRect.left
|
||||
const top = rect.top - containerRect.top
|
||||
const bottom = rect.bottom - containerRect.top
|
||||
|
||||
minLeft = Math.min(minLeft, left)
|
||||
maxRight = Math.max(maxRight, right)
|
||||
minTop = Math.min(minTop, top)
|
||||
maxBottom = Math.max(maxBottom, bottom)
|
||||
})
|
||||
|
||||
// 计算椭圆框的尺寸(增加一些内边距)
|
||||
const padding = 8
|
||||
const width = maxRight - minLeft + padding * 2
|
||||
const height = maxBottom - minTop + padding * 2
|
||||
|
||||
ellipseStyle.value = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
left: `${minLeft - padding}px`,
|
||||
top: `${minTop - padding}px`,
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
isEllipseVisible.value = true
|
||||
}
|
||||
|
||||
// 监听选中状态变化,更新椭圆框
|
||||
watch(
|
||||
selectedAgents,
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
updateEllipseBox()
|
||||
}, 50) // 等待DOM更新
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 组件挂载时初始化选中状态
|
||||
onMounted(() => {
|
||||
initializeSelectedAgents()
|
||||
|
||||
// 初始渲染后更新椭圆框
|
||||
setTimeout(() => {
|
||||
updateEllipseBox()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-allocation">
|
||||
<!-- 头部 -->
|
||||
<div class="allocation-header">
|
||||
<span class="header-title">智能体分配</span>
|
||||
<el-button class="close-button" text circle @click="handleClose" title="关闭">
|
||||
<span class="close-icon">❌️</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<el-divider class="header-divider" />
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="allocation-content">
|
||||
<!-- 椭圆框容器 -->
|
||||
<div class="ellipse-box-container">
|
||||
<!-- 椭圆框(动画元素) -->
|
||||
<div
|
||||
ref="ellipseBoxRef"
|
||||
class="ellipse-box"
|
||||
:class="{ visible: isEllipseVisible }"
|
||||
:style="ellipseStyle"
|
||||
/>
|
||||
|
||||
<!-- 智能体整体容器 -->
|
||||
<div class="agents-container">
|
||||
<div
|
||||
v-for="(agent, index) in agents"
|
||||
:key="agent.Name"
|
||||
class="agent-item"
|
||||
:class="{ selected: isAgentSelected(agent.Name) }"
|
||||
:title="agent.Name"
|
||||
@click="toggleSelectAgent(agent.Name)"
|
||||
>
|
||||
<!-- 头像部分(独立元素) -->
|
||||
<div
|
||||
class="agent-avatar"
|
||||
:style="{
|
||||
background: getAgentMapIcon(agent.Name).color,
|
||||
borderColor: isAgentSelected(agent.Name) ? '#409eff' : 'var(--color-border)'
|
||||
}"
|
||||
>
|
||||
<SvgIcon :icon-class="getAgentMapIcon(agent.Name).icon" size="24px" color="#fff" />
|
||||
|
||||
<!-- 选中标记 -->
|
||||
<div v-if="isAgentSelected(agent.Name)" class="selected-indicator">
|
||||
<svg-icon icon-class="check" size="12px" color="#fff" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 等级色值部分(独立元素,通过CSS定位跟随头像) -->
|
||||
<div
|
||||
class="color-level"
|
||||
:style="{
|
||||
backgroundColor: getLevelConfig(getAgentLevel(index)).color,
|
||||
color: getLevelConfig(getAgentLevel(index)).textColor
|
||||
}"
|
||||
>
|
||||
{{ getAgentLevel(index) }}
|
||||
</div>
|
||||
|
||||
<!-- 智能体名称 -->
|
||||
<div class="agent-name">{{ agent.Name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="agents.length === 0" class="empty-state">
|
||||
<el-empty description="暂无智能体数据" />
|
||||
<div class="empty-tip">请先在智能体库中上传智能体信息</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-allocation {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.allocation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-title);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
padding: 4px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-divider {
|
||||
margin: 0;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.allocation-content {
|
||||
flex: 1;
|
||||
padding: 30px 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
// 椭圆框容器
|
||||
.ellipse-box-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
|
||||
// 椭圆框样式
|
||||
.ellipse-box {
|
||||
position: absolute;
|
||||
border: 2px solid #409eff;
|
||||
border-radius: 50px; // 椭圆形状
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(64, 158, 255, 0.1) 0%,
|
||||
rgba(64, 158, 255, 0.05) 100%
|
||||
);
|
||||
pointer-events: none; // 允许点击穿透
|
||||
z-index: 1;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
opacity: 0;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 椭圆框动画
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
border: 2px solid rgba(64, 158, 255, 0.3);
|
||||
border-radius: 50px;
|
||||
animation: pulse 2s infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agents-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
z-index: 2; // 确保头像在椭圆框上方
|
||||
|
||||
.agent-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 80px;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
cursor: pointer;
|
||||
|
||||
// 选中状态的动画效果
|
||||
&.selected {
|
||||
transform: scale(1.05);
|
||||
z-index: 10;
|
||||
|
||||
// 让选中头像移动到前面
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.agent-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3px solid var(--color-border);
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
// 选中指示器
|
||||
.selected-indicator {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid white;
|
||||
}
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
width: 100%;
|
||||
|
||||
.empty-tip {
|
||||
margin-top: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 椭圆框脉冲动画
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
padding: 20px 16px;
|
||||
gap: 16px;
|
||||
|
||||
.agents-container {
|
||||
gap: 12px;
|
||||
|
||||
.agent-item {
|
||||
width: 70px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 480px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
.agents-container {
|
||||
gap: 8px;
|
||||
|
||||
.agent-item {
|
||||
width: 65px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user