Initial commit: Multi-Agent Coordination Platform
- Vue 3 + TypeScript + Vite project structure - Element Plus UI components with dark theme - Pinia state management for agents and tasks - JSPlumb integration for visual workflow editing - SVG icon system for agent roles - Axios request layer with API proxy configuration - Tailwind CSS for styling - Docker deployment with Caddy web server - Complete development toolchain (ESLint, Prettier, Vitest) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
253
src/layout/components/Main/TaskTemplate/AgentRepo/index.vue
Normal file
253
src/layout/components/Main/TaskTemplate/AgentRepo/index.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<script setup lang="ts">
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
import api from '@/api/index.ts'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import {
|
||||
agentMapDuty,
|
||||
getActionTypeDisplay,
|
||||
getAgentMapIcon,
|
||||
} from '@/layout/components/config.ts'
|
||||
import { type TaskProcess, useAgentsStore } from '@/stores'
|
||||
import { onMounted } from 'vue'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(el: 'resetAgentRepoLine'): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
const handleScroll = () => {
|
||||
emit('resetAgentRepoLine')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 如果已上传就重新发送给后端
|
||||
if (agentsStore.agents.length) {
|
||||
void api.setAgents(agentsStore.agents.map((item) => pick(item, ['Name', 'Profile'])))
|
||||
}
|
||||
})
|
||||
|
||||
// 自定义提示框鼠标移入小红点时显示
|
||||
const tooltipVisibleKey = ref('')
|
||||
const tooltipPosition = ref({ x: 0, y: 0 })
|
||||
|
||||
const showTooltip = (event: MouseEvent, item: TaskProcess & { key: string }) => {
|
||||
tooltipVisibleKey.value = item.key
|
||||
const rect = (event.target as HTMLElement).getBoundingClientRect()
|
||||
tooltipPosition.value = {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top - 10,
|
||||
}
|
||||
}
|
||||
|
||||
const hideTooltip = () => {
|
||||
tooltipVisibleKey.value = ''
|
||||
}
|
||||
|
||||
// 上传agent文件
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
const triggerFileSelect = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0]
|
||||
readFileContent(file)
|
||||
}
|
||||
}
|
||||
|
||||
const readFileContent = async (file: File) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e) => {
|
||||
if (!e.target?.result) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(e.target.result?.toString?.() ?? '{}')
|
||||
// 处理 JSON 数据
|
||||
if (Array.isArray(json)) {
|
||||
const isValid = json.every(
|
||||
(item) =>
|
||||
typeof item.Name === 'string' &&
|
||||
typeof item.Icon === 'string' &&
|
||||
typeof item.Profile === 'string',
|
||||
)
|
||||
if (isValid) {
|
||||
// 处理有效的 JSON 数据
|
||||
agentsStore.setAgents(
|
||||
json.map((item) => ({
|
||||
Name: item.Name,
|
||||
Icon: item.Icon.replace(/\.png$/, ''),
|
||||
Profile: item.Profile,
|
||||
})),
|
||||
)
|
||||
await api.setAgents(json.map((item) => pick(item, ['Name', 'Profile'])))
|
||||
} else {
|
||||
ElNotification.error({
|
||||
title: '错误',
|
||||
message: 'JSON 格式错误',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.error('JSON is not an array')
|
||||
ElNotification.error({
|
||||
title: '错误',
|
||||
message: 'JSON 格式错误',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const taskProcess = computed(() => {
|
||||
const list = agentsStore.currentTask?.TaskProcess ?? []
|
||||
return list.map((item) => ({
|
||||
...item,
|
||||
key: uuidv4(),
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-repo h-full flex flex-col">
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[18px] font-bold">智能体库</span>
|
||||
<!-- 上传文件 -->
|
||||
<input type="file" accept=".json" @change="handleFileSelect" class="hidden" ref="fileInput" />
|
||||
<div class="plus-button" @click="triggerFileSelect">
|
||||
<svg-icon icon-class="plus" color="var(--color-text)" size="18px" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 人员列表 -->
|
||||
<div class="mt-[18px] flex-1 overflow-y-auto relative" @scroll="handleScroll">
|
||||
<div
|
||||
class="flex items-center justify-between user-item relative"
|
||||
v-for="item in agentsStore.agents"
|
||||
:key="item.Name"
|
||||
>
|
||||
<!-- 右侧链接点 -->
|
||||
<div
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2"
|
||||
:id="`agent-repo-${item.Name}`"
|
||||
></div>
|
||||
<div
|
||||
class="w-[41px] h-[41px] rounded-full flex items-center justify-center"
|
||||
:style="{ background: getAgentMapIcon(item.Name).color }"
|
||||
>
|
||||
<svg-icon
|
||||
:icon-class="getAgentMapIcon(item.Name).icon"
|
||||
color="var(--color-text)"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-[14px] flex flex-col items-end justify-end">
|
||||
<div class="flex items-center gap-[7px]">
|
||||
<div
|
||||
v-for="item1 in taskProcess.filter((i) => i.AgentName === item.Name)"
|
||||
:key="item1.key"
|
||||
class="relative inline-block"
|
||||
>
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
:width="200"
|
||||
trigger="click"
|
||||
:content="item1.Description"
|
||||
:title="getActionTypeDisplay(item1.ActionType)?.name"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="group relative inline-block">
|
||||
<!-- 小圆点 -->
|
||||
<div
|
||||
class="w-[6px] h-[6px] rounded-full"
|
||||
:style="{ background: getActionTypeDisplay(item1.ActionType)?.color }"
|
||||
@mouseenter="(el) => showTooltip(el, item1)"
|
||||
@mouseleave="hideTooltip"
|
||||
></div>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="tooltipVisibleKey === item1.key"
|
||||
class="fixed transform -translate-x-1/2 -translate-y-full mb-2 p-2 bg-[var(--el-bg-color-overlay)] text-sm rounded-[8px] z-50"
|
||||
:style="{
|
||||
left: tooltipPosition.x + 'px',
|
||||
top: tooltipPosition.y + 'px',
|
||||
}"
|
||||
>
|
||||
{{ getActionTypeDisplay(item1.ActionType)?.name }}
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
<span class="mb-1">{{ item.Name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部提示栏 -->
|
||||
<div class="w-full grid grid-cols-3 gap-x-[10px] bg-[#1d222b] rounded-[20px] p-[8px] mt-[10px]">
|
||||
<div
|
||||
v-for="item in Object.values(agentMapDuty)"
|
||||
:key="item.key"
|
||||
class="flex items-center justify-center gap-x-1"
|
||||
>
|
||||
<span class="text-[12px]">{{ item.name }}</span>
|
||||
<div class="w-[8px] h-[8px] rounded-full" :style="{ background: item.color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-repo {
|
||||
padding: 0 8px;
|
||||
.plus-button {
|
||||
background: #1d2128;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #374151;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.user-item {
|
||||
background: #1d222b;
|
||||
border-radius: 20px;
|
||||
padding-right: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
color: #969696;
|
||||
|
||||
& + .user-item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||
color: #b8b8b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import type { IExecuteRawResponse } from '@/api'
|
||||
import { computed } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import DOMPurify from 'dompurify'
|
||||
import Iod from './Iod.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
executePlans: IExecuteRawResponse[]
|
||||
nodeId?: string
|
||||
actionId?: string
|
||||
}>()
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
})
|
||||
|
||||
const data = computed(() => {
|
||||
for (const result of props.executePlans) {
|
||||
if (result.NodeId === props.nodeId && result.ActionHistory) {
|
||||
for (const action of result.ActionHistory) {
|
||||
if (action.ID === props.actionId) {
|
||||
return action
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const action_Result = computed(() => {
|
||||
const html = md.render(data.value?.Action_Result ?? '')
|
||||
return DOMPurify.sanitize(html)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="data" class="card-item w-full pl-[56px] pr-[41px]">
|
||||
<!-- 分割线 -->
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div class="text-[16px] flex items-center gap-1 text-[var(--color-text-secondary)]">
|
||||
{{ data.Description }}
|
||||
<Iod />
|
||||
</div>
|
||||
<div class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-quaternary)] mt-1">
|
||||
<div class="markdown-content max-h-[240px] overflow-y-auto" v-html="action_Result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
76
src/layout/components/Main/TaskTemplate/TaskResult/Iod.vue
Normal file
76
src/layout/components/Main/TaskTemplate/TaskResult/Iod.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { readConfig } from '@/utils/readJson.ts'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
interface Iod {
|
||||
name: string
|
||||
data_space: string
|
||||
doId: string
|
||||
fromRepo: string
|
||||
}
|
||||
|
||||
const data = ref<Iod[]>([])
|
||||
const displayIndex = ref(0)
|
||||
|
||||
const displayIod = computed(() => {
|
||||
return data.value[displayIndex.value]!
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await readConfig<{ data: Iod[] }>('iodConfig.json')
|
||||
data.value = res.data
|
||||
})
|
||||
|
||||
function handleNext() {
|
||||
if (displayIndex.value === data.value.length - 1) {
|
||||
displayIndex.value = 0
|
||||
} else {
|
||||
displayIndex.value++
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover trigger="hover" width="440">
|
||||
<template #reference>
|
||||
<div
|
||||
class="rounded-full w-[20px] h-[20px] bg-[var(--color-bg-quaternary)] flex justify-center items-center cursor-pointer"
|
||||
>
|
||||
{{ data.length }}
|
||||
</div>
|
||||
</template>
|
||||
<template #default v-if="data.length">
|
||||
<div>
|
||||
<div class="flex justify-between items-center p-2 pb-0 rounded-[8px] text-[16px] font-bold">
|
||||
<span>数联网搜索结果</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<div>{{ `${displayIndex + 1}/${data.length}` }}</div>
|
||||
<el-button type="primary" size="small" @click="handleNext">下一个</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div class="p-2 pt-0">
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">名称:</div>
|
||||
<div class="text-[var(--color-text-secondary)] flex-1 break-words">{{ displayIod.name }}</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">数据空间:</div>
|
||||
<div class="text-[var(--color-text-secondary)] lex-1 break-words">{{ displayIod.data_space }}</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">DOID:</div>
|
||||
<div class="text-[var(--color-text-secondary)] lex-1 break-words">{{ displayIod.doId }}</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">来源仓库:</div>
|
||||
<div class="text-[var(--color-text-secondary)] flex-1 break-words break-al">{{ displayIod.fromRepo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
365
src/layout/components/Main/TaskTemplate/TaskResult/index.vue
Normal file
365
src/layout/components/Main/TaskTemplate/TaskResult/index.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<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, { type IExecuteRawResponse } from '@/api'
|
||||
|
||||
import ExecutePlan from './ExecutePlan.vue'
|
||||
import Iod from './Iod.vue'
|
||||
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refreshLine'): void
|
||||
(el: 'setCurrentTask', task: IRawStepTask): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
const collaborationProcess = computed(() => {
|
||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
||||
})
|
||||
|
||||
const jsplumb = new Jsplumb('task-results-main', {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: { curviness: 30, stub: 10 },
|
||||
},
|
||||
})
|
||||
|
||||
// 操作折叠面板时要实时的刷新连线
|
||||
let timer: number
|
||||
function handleCollapse() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
jsplumb.repaintEverything()
|
||||
emit('refreshLine')
|
||||
}, 1)
|
||||
|
||||
// 默认三秒后已经完全打开
|
||||
const timer1 = setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
}, 3000)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (timer1) {
|
||||
clearInterval(timer1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建内部连线
|
||||
function createInternalLine(id?: string) {
|
||||
const arr: ConnectArg[] = []
|
||||
jsplumb.reset()
|
||||
collaborationProcess.value.forEach((item) => {
|
||||
// 创建左侧流程与产出的连线
|
||||
// jsplumb.connect(`task-results-${item.Id}-0`, `task-results-${item.Id}-1`, [
|
||||
// AnchorLocations.Left,
|
||||
// AnchorLocations.Left,
|
||||
// ])
|
||||
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 ?? '')) {
|
||||
// jsplumb.connect(
|
||||
// `task-results-${jitem.Id}-1`,
|
||||
// `task-results-${item.Id}-0`,
|
||||
// [AnchorLocations.Left, AnchorLocations.Left],
|
||||
// {
|
||||
// type: 'output',
|
||||
// },
|
||||
// )
|
||||
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-${jitem.Id}-0-${i.ID}`
|
||||
const targetId = `task-results-${item.Id}-1`
|
||||
// jsplumb.connect(sourceId, targetId, [AnchorLocations.Right, AnchorLocations.Right], {
|
||||
// stops: [
|
||||
// [0, color],
|
||||
// [1, color],
|
||||
// ],
|
||||
// transparent: sourceId !== id,
|
||||
// })
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color],
|
||||
],
|
||||
transparent: sourceId !== 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-${i.ID}`
|
||||
const targetId = `task-results-${item.Id}-0-${i2.ID}`
|
||||
// jsplumb.connect(sourceId, targetId, [AnchorLocations.Right, AnchorLocations.Right], {
|
||||
// stops: [
|
||||
// [0, color],
|
||||
// [1, color],
|
||||
// ],
|
||||
// transparent: sourceId !== id,
|
||||
// })
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color],
|
||||
],
|
||||
transparent: sourceId !== id,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
jsplumb.connects(arr)
|
||||
jsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
const results = ref<IExecuteRawResponse[]>([])
|
||||
const loading = ref(false)
|
||||
async function handleRun() {
|
||||
try {
|
||||
loading.value = true
|
||||
results.value = await api.executePlan(agentsStore.agentRawPlan.data!)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加滚动状态标识
|
||||
const isScrolling = ref(false)
|
||||
let scrollTimer: number
|
||||
|
||||
// 修改滚动处理函数
|
||||
function handleScroll() {
|
||||
isScrolling.value = true
|
||||
emit('refreshLine')
|
||||
// 清除之前的定时器
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer)
|
||||
}
|
||||
jsplumb.repaintEverything()
|
||||
|
||||
// 设置滚动结束检测
|
||||
scrollTimer = setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 修改鼠标事件处理函数
|
||||
const handleMouseEnter = throttle((id) => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine(id)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const handleMouseLeave = throttle(() => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
defineExpose({
|
||||
createInternalLine,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col relative" id="task-results">
|
||||
<!-- 标题与执行按钮 -->
|
||||
<div class="text-[18px] font-bold mb-[18px] flex justify-between items-center px-[20px]">
|
||||
<span>执行结果</span>
|
||||
<div class="flex items-center gap-[14px]">
|
||||
<el-button circle :color="variables.tertiary" disabled title="点击刷新">
|
||||
<svg-icon icon-class="refresh" />
|
||||
</el-button>
|
||||
<el-button circle :color="variables.tertiary" title="点击运行" @click="handleRun">
|
||||
<svg-icon icon-class="action" />
|
||||
</el-button>
|
||||
</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"
|
||||
shadow="hover"
|
||||
:id="`task-results-${item.Id}-0`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<div class="text-[18px] mb-[15px]">{{ item.StepName }}</div>
|
||||
<!-- 折叠面板 -->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<!-- <el-tooltip-->
|
||||
<!-- :disabled="Boolean(true || results.length || loading)"-->
|
||||
<!-- placement="top"-->
|
||||
<!-- effect="light"-->
|
||||
<!-- content="请点击右上角的循行按钮,查看运行结果"-->
|
||||
<!-- >-->
|
||||
<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(!results.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="!results.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-[15px]">
|
||||
<!-- 右侧链接点 -->
|
||||
<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="var(--color-text)"
|
||||
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="results"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
<!-- </el-tooltip>-->
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="card-item w-full relative"
|
||||
shadow="hover"
|
||||
:id="`task-results-${item.Id}-1`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<div class="text-[18px]">{{ item.OutputObject }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#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-secondary);
|
||||
min-height: 41px;
|
||||
line-height: 41px;
|
||||
border-radius: 20px;
|
||||
transition: border-radius 1ms;
|
||||
position: relative;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
border: none;
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card) {
|
||||
.el-card__body {
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
235
src/layout/components/Main/TaskTemplate/TaskSyllabus/index.vue
Normal file
235
src/layout/components/Main/TaskTemplate/TaskSyllabus/index.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import {
|
||||
type ConnectArg,
|
||||
Jsplumb,
|
||||
type JsplumbConfig,
|
||||
} from '@/layout/components/Main/TaskTemplate/utils.ts'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import { computed } from 'vue'
|
||||
import { AnchorLocations } from '@jsplumb/browser-ui'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(el: 'resetAgentRepoLine'): void
|
||||
(el: 'setCurrentTask', task: IRawStepTask): void
|
||||
}>()
|
||||
|
||||
const jsplumb = new Jsplumb('task-syllabus')
|
||||
|
||||
const handleScroll = () => {
|
||||
emit('resetAgentRepoLine')
|
||||
}
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
const collaborationProcess = computed(() => {
|
||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
||||
})
|
||||
|
||||
function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg[] {
|
||||
// 创建当前流程与产出的连线
|
||||
const arr: ConnectArg[] = [
|
||||
{
|
||||
sourceId: `task-syllabus-flow-${task.Id}`,
|
||||
targetId: `task-syllabus-output-object-${task.Id}`,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Left],
|
||||
config: {
|
||||
transparent,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// jsplumb.connect(
|
||||
// `task-syllabus-flow-${task.Id}`,
|
||||
// `task-syllabus-output-object-${task.Id}`,
|
||||
// [AnchorLocations.Right, AnchorLocations.Left],
|
||||
// {
|
||||
// transparent,
|
||||
// },
|
||||
// )
|
||||
// 创建当前产出与流程的连线
|
||||
task.InputObject_List?.forEach((item) => {
|
||||
const id = collaborationProcess.value.find((i) => i.OutputObject === item)?.Id
|
||||
if (id) {
|
||||
arr.push({
|
||||
sourceId: `task-syllabus-output-object-${id}`,
|
||||
targetId: `task-syllabus-flow-${task.Id}`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Right],
|
||||
config: {
|
||||
type: 'output',
|
||||
transparent,
|
||||
}
|
||||
})
|
||||
// jsplumb.connect(
|
||||
// `task-syllabus-output-object-${id}`,
|
||||
// `task-syllabus-flow-${task.Id}`,
|
||||
// [AnchorLocations.Left, AnchorLocations.Right],
|
||||
// {
|
||||
// type: 'output',
|
||||
// transparent,
|
||||
// },
|
||||
// )
|
||||
}
|
||||
})
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
function changeTask(task?: IRawStepTask, isEmit?: boolean) {
|
||||
jsplumb.reset()
|
||||
const arr: ConnectArg[] = []
|
||||
agentsStore.agentRawPlan.data?.['Collaboration Process']?.forEach((item) => {
|
||||
arr.push(...handleCurrentTask(item, item.Id !== task?.Id))
|
||||
})
|
||||
jsplumb.connects(arr)
|
||||
if (isEmit && task) {
|
||||
emit('setCurrentTask', task)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
changeTask,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="text-[18px] font-bold mb-[18px]">任务大纲</div>
|
||||
|
||||
<div
|
||||
v-loading="agentsStore.agentRawPlan.loading"
|
||||
class="flex-1 w-full overflow-y-auto relative"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div class="flex items-center gap-[14%] w-full px-5 relative" id="task-syllabus">
|
||||
<!-- 流程 -->
|
||||
<div
|
||||
v-if="agentsStore.agentRawPlan.data"
|
||||
class="w-[43%] min-h-full relative flex justify-center"
|
||||
>
|
||||
<!-- 流程内容 -->
|
||||
<div class="relative min-h-full z-10 flex flex-col items-center card-box pb-[100px]">
|
||||
<!-- 背景那一根线 -->
|
||||
<div
|
||||
class="absolute h-full left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[5px] z-[1]"
|
||||
>
|
||||
<!-- 线底部的小圆球 -->
|
||||
<div
|
||||
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
<!-- 固定的标题 -->
|
||||
<div
|
||||
class="card-item w-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]"
|
||||
>
|
||||
流程
|
||||
</div>
|
||||
|
||||
<el-card
|
||||
v-for="item in collaborationProcess"
|
||||
:key="item.Id"
|
||||
class="card-item w-full h-[158px] overflow-y-auto active-card relative z-99"
|
||||
shadow="hover"
|
||||
:id="`task-syllabus-flow-${item.Id}`"
|
||||
@click="changeTask(item, true)"
|
||||
>
|
||||
<div class="text-[18px] font-bold text-center">{{ item.StepName }}</div>
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div class="text-[14px] line-clamp-3 text-[var(--color-text-secondary)]">
|
||||
{{ item.TaskContent }}
|
||||
</div>
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div class="flex items-center gap-2 flex-wrap relative">
|
||||
<!-- 连接到智能体库的连接点 -->
|
||||
<div
|
||||
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
|
||||
:id="`task-syllabus-flow-agents-${item.Id}`"
|
||||
></div>
|
||||
<el-tooltip
|
||||
v-for="agentSelection in item.AgentSelection"
|
||||
:key="agentSelection"
|
||||
effect="light"
|
||||
placement="right"
|
||||
>
|
||||
<template #content>
|
||||
<div class="w-[150px]">
|
||||
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div>
|
||||
{{
|
||||
item.TaskProcess.find((i) => i.AgentName === agentSelection)?.Description
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
|
||||
:style="{ background: getAgentMapIcon(agentSelection).color }"
|
||||
>
|
||||
<svg-icon
|
||||
:icon-class="getAgentMapIcon(agentSelection).icon"
|
||||
color="var(--color-text)"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 产出 -->
|
||||
<div
|
||||
v-if="agentsStore.agentRawPlan.data"
|
||||
class="w-[43%] h-full relative flex justify-center"
|
||||
>
|
||||
<div class="min-h-full w-full relative">
|
||||
<!-- 产出内容 -->
|
||||
<div class="min-h-full relative z-10 flex flex-col items-center card-box pb-[100px]">
|
||||
<!-- 背景那一根线 -->
|
||||
<div
|
||||
class="absolute h-full left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[5px] z-[1]"
|
||||
>
|
||||
<!-- 线底部的小圆球 -->
|
||||
<div
|
||||
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
<!-- 固定的标题 -->
|
||||
<div
|
||||
class="card-item w-[45%] h-[41px] flex justify-center items-center rounded-[20px] bg-[var(--color-bg-tertiary)] relative z-99"
|
||||
>
|
||||
产出
|
||||
</div>
|
||||
<div
|
||||
v-for="item in collaborationProcess"
|
||||
:key="item.Id"
|
||||
class="h-[158px] overflow-y-auto flex items-center w-full card-item relative z-99"
|
||||
>
|
||||
<el-card
|
||||
class="card-item w-full relative"
|
||||
shadow="hover"
|
||||
:id="`task-syllabus-output-object-${item.Id}`"
|
||||
>
|
||||
<div class="text-[18px] font-bold text-center">{{ item.OutputObject }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-box {
|
||||
.active-card {
|
||||
border: 2px solid transparent;
|
||||
$bg: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background:
|
||||
linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
109
src/layout/components/Main/TaskTemplate/index.vue
Normal file
109
src/layout/components/Main/TaskTemplate/index.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import AgentRepo from './AgentRepo/index.vue'
|
||||
import TaskSyllabus from './TaskSyllabus/index.vue'
|
||||
import TaskResult from './TaskResult/index.vue'
|
||||
import { Jsplumb } from './utils.ts'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 智能体库
|
||||
const agentRepoJsplumb = new Jsplumb('task-template', {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: {
|
||||
curviness: 30, // 曲线弯曲程度
|
||||
stub: 20, // 添加连接点与端点的距离
|
||||
alwaysRespectStubs: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
// 任务流程
|
||||
const taskSyllabusRef = ref<{ changeTask: (task?: IRawStepTask, isEmit?: boolean) => void }>()
|
||||
// 执行结果
|
||||
const taskResultRef = ref<{ createInternalLine: () => void }>()
|
||||
const taskResultJsplumb = new Jsplumb('task-template')
|
||||
|
||||
function setCurrentTask(task: IRawStepTask) {
|
||||
// 智能体库画线
|
||||
agentRepoJsplumb.reset()
|
||||
task.AgentSelection?.forEach((item) => {
|
||||
agentRepoJsplumb.connect(
|
||||
`agent-repo-${item}`,
|
||||
`task-syllabus-flow-agents-${task.Id}`,
|
||||
[AnchorLocations.Left, AnchorLocations.Right],
|
||||
{ type: 'input' },
|
||||
)
|
||||
})
|
||||
agentRepoJsplumb.repaintEverything()
|
||||
// 执行结果画线
|
||||
taskResultJsplumb.reset()
|
||||
taskResultJsplumb.connect(`task-syllabus-output-object-${task.Id}`, `task-results-${task.Id}-0`, [AnchorLocations.Right, AnchorLocations.Left])
|
||||
taskResultJsplumb.connect(`task-syllabus-output-object-${task.Id}`, `task-results-${task.Id}-1`, [AnchorLocations.Right, AnchorLocations.Left])
|
||||
taskResultJsplumb.repaintEverything()
|
||||
agentsStore.setCurrentTask(task)
|
||||
// 更新任务大纲内部的线
|
||||
taskSyllabusRef.value?.changeTask(task, false)
|
||||
}
|
||||
|
||||
function changeTask() {
|
||||
taskResultRef.value?.createInternalLine()
|
||||
taskSyllabusRef.value?.changeTask()
|
||||
}
|
||||
|
||||
function resetAgentRepoLine() {
|
||||
agentRepoJsplumb.repaintEverything()
|
||||
taskResultJsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
changeTask,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="task-template flex gap-6 items-center h-[calc(100%-67px)] relative overflow-hidden"
|
||||
id="task-template"
|
||||
>
|
||||
<!-- 智能体库 -->
|
||||
<div class="w-[9.5%] min-w-[179px] h-full relative">
|
||||
<AgentRepo @resetAgentRepoLine="agentRepoJsplumb.repaintEverything" />
|
||||
</div>
|
||||
<!-- 任务大纲 -->
|
||||
<div class="w-[40.5%] min-w-[600px] h-full px-[20px]">
|
||||
<TaskSyllabus
|
||||
ref="taskSyllabusRef"
|
||||
@resetAgentRepoLine="resetAgentRepoLine"
|
||||
@set-current-task="setCurrentTask"
|
||||
/>
|
||||
</div>
|
||||
<!-- 执行结果 -->
|
||||
<div class="flex-1 h-full">
|
||||
<TaskResult
|
||||
ref="taskResultRef"
|
||||
@refresh-line="taskResultJsplumb.repaintEverything"
|
||||
@set-current-task="setCurrentTask"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-template {
|
||||
& > div {
|
||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.8);
|
||||
border-radius: 24px;
|
||||
border: 1px solid #414752;
|
||||
background: #29303c;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
:root {
|
||||
--gradient: linear-gradient(to right, #0093eb, #00d2d1);
|
||||
}
|
||||
</style>
|
||||
168
src/layout/components/Main/TaskTemplate/utils.ts
Normal file
168
src/layout/components/Main/TaskTemplate/utils.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type {
|
||||
AnchorSpec,
|
||||
ConnectorSpec,
|
||||
ConnectParams,
|
||||
EndpointOptions,
|
||||
JsPlumbInstance,
|
||||
} from '@jsplumb/browser-ui'
|
||||
import { BezierConnector, DotEndpoint, newInstance } from '@jsplumb/browser-ui'
|
||||
|
||||
export interface JsplumbConfig {
|
||||
connector?: ConnectorSpec
|
||||
type?: 'input' | 'output'
|
||||
stops?: [[number, string], [number, string]]
|
||||
// 连接线条是否变透明一些
|
||||
transparent?: boolean
|
||||
}
|
||||
|
||||
export interface ConnectArg {
|
||||
sourceId: string
|
||||
targetId: string
|
||||
anchor: AnchorSpec
|
||||
config?: JsplumbConfig
|
||||
}
|
||||
|
||||
const defaultConfig: JsplumbConfig = {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: {
|
||||
curviness: 70,
|
||||
stub: 10,
|
||||
},
|
||||
},
|
||||
type: 'input',
|
||||
}
|
||||
|
||||
export class Jsplumb {
|
||||
instance!: JsPlumbInstance
|
||||
containerId: string
|
||||
config: JsplumbConfig
|
||||
|
||||
constructor(eleId: string, config = {} as JsplumbConfig) {
|
||||
this.containerId = eleId
|
||||
this.config = { ...defaultConfig, ...config }
|
||||
onMounted(() => {
|
||||
this.init()
|
||||
})
|
||||
}
|
||||
|
||||
init = () => {
|
||||
if (this.instance) {
|
||||
return
|
||||
}
|
||||
this.instance = newInstance({
|
||||
container: document.querySelector(`#${this.containerId}`)!, // 或指定共同的父容器
|
||||
})
|
||||
}
|
||||
|
||||
getStops = (type?: 'input' | 'output'): [[number, string], [number, string]] => {
|
||||
if (type === 'input') {
|
||||
return [
|
||||
[0, '#0093EB'],
|
||||
[1, '#00D2D1'],
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
[0, '#FF6161'],
|
||||
[1, '#D76976'],
|
||||
]
|
||||
}
|
||||
|
||||
_connect = (
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
anchor: AnchorSpec,
|
||||
_config = {} as JsplumbConfig,
|
||||
) => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
...this.config,
|
||||
..._config,
|
||||
}
|
||||
this.init()
|
||||
// 连接两个元素
|
||||
const sourceElement = document.querySelector(`#${sourceId}`)
|
||||
const targetElement = document.querySelector(`#${targetId}`)
|
||||
const stops = _config.stops ?? this.getStops(config.type)
|
||||
// 如果config.transparent为true,则将stops都加一些透明度
|
||||
if (config.transparent) {
|
||||
stops[0][1] = stops[0][1] + '30'
|
||||
stops[1][1] = stops[1][1] + '30'
|
||||
}
|
||||
if (targetElement && sourceElement) {
|
||||
this.instance.connect({
|
||||
source: sourceElement,
|
||||
target: targetElement,
|
||||
connector: config.connector,
|
||||
anchor: anchor,
|
||||
paintStyle: {
|
||||
stroke: stops[0][1],
|
||||
strokeWidth: 2.5,
|
||||
dashstyle: '0',
|
||||
zIndex: 100,
|
||||
opacity: 0.9,
|
||||
gradient: {
|
||||
stops: stops,
|
||||
type: 'linear',
|
||||
},
|
||||
},
|
||||
sourceEndpointStyle: { fill: stops[0][1] },
|
||||
|
||||
endpoint: {
|
||||
type: DotEndpoint.type,
|
||||
options: { radius: 5 },
|
||||
},
|
||||
} as unknown as ConnectParams<unknown>)
|
||||
|
||||
// 为源元素添加端点
|
||||
this.instance.addEndpoint(sourceElement, {
|
||||
anchor: (anchor as [AnchorSpec, AnchorSpec])[0],
|
||||
paintStyle: { fill: stops[0][1], zIndex: 100 }, // source端点颜色
|
||||
} as unknown as EndpointOptions)
|
||||
|
||||
// 为目标元素添加端点
|
||||
this.instance.addEndpoint(targetElement, {
|
||||
anchor: (anchor as [AnchorSpec, AnchorSpec])[1],
|
||||
paintStyle: { fill: stops[1][1], zIndex: 100 }, // target端点颜色
|
||||
} as unknown as EndpointOptions)
|
||||
}
|
||||
}
|
||||
|
||||
connect = (
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
anchor: AnchorSpec,
|
||||
config = {} as JsplumbConfig,
|
||||
) => {
|
||||
this._connect(sourceId, targetId, anchor, config)
|
||||
}
|
||||
|
||||
connects = (args: ConnectArg[]) => {
|
||||
this.instance.batch(() => {
|
||||
args.forEach(({ sourceId, targetId, anchor, config }) => {
|
||||
this._connect(sourceId, targetId, anchor, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
repaintEverything = () => {
|
||||
// 重新验证元素位置
|
||||
const container = document.querySelector(`#${this.containerId}`)
|
||||
if (container) {
|
||||
const elements = container.querySelectorAll('[id^="task-results-"]')
|
||||
elements.forEach((element) => {
|
||||
this.instance.revalidate(element)
|
||||
})
|
||||
}
|
||||
this.instance.repaintEverything()
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.instance.deleteEveryConnection()
|
||||
const allEndpoints = this.instance.selectEndpoints()
|
||||
allEndpoints.each((endpoint) => {
|
||||
this.instance.deleteEndpoint(endpoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user