mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: 工作编排功能
This commit is contained in:
parent
c77b20c079
commit
5a29e83879
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "MaxKB",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
"@logicflow/core": "^1.2.27",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"axios": "^0.28.0",
|
||||
"cropperjs": "^1.6.2",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import Components from '@/components'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { HtmlNode, HtmlNodeModel } from '@logicflow/core'
|
||||
import { createApp, h } from 'vue'
|
||||
import directives from '@/directives'
|
||||
|
||||
class AppNode extends HtmlNode {
|
||||
isMounted
|
||||
r
|
||||
app
|
||||
|
||||
constructor(props: any, VueNode: any) {
|
||||
super(props)
|
||||
this.isMounted = false
|
||||
|
||||
this.r = h(VueNode, {
|
||||
properties: props.model.properties,
|
||||
nodeModel: props.model
|
||||
})
|
||||
|
||||
this.app = createApp({
|
||||
render: () => this.r
|
||||
})
|
||||
this.app.use(ElementPlus)
|
||||
this.app.use(Components)
|
||||
this.app.use(directives)
|
||||
}
|
||||
|
||||
setHtml(rootEl: HTMLElement) {
|
||||
if (!this.isMounted) {
|
||||
this.isMounted = true
|
||||
const node = document.createElement('div')
|
||||
rootEl.appendChild(node)
|
||||
this.app.mount(node)
|
||||
} else {
|
||||
if (this.r && this.r.component) {
|
||||
this.r.component.props.properties = this.props.model.getProperties()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppNodeModel extends HtmlNodeModel {
|
||||
/**
|
||||
* 给model自定义添加字段方法
|
||||
*/
|
||||
addField(item: any) {
|
||||
this.properties.fields.unshift(item)
|
||||
this.setAttributes()
|
||||
// 为了保持节点顶部位置不变,在节点变化后,对节点进行一个位移,位移距离为添加高度的一半。
|
||||
this.move(0, 24 / 2)
|
||||
// 更新节点连接边的path
|
||||
this.incoming.edges.forEach((egde: any) => {
|
||||
// 调用自定义的更新方案
|
||||
egde.updatePathByAnchor()
|
||||
})
|
||||
this.outgoing.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
}
|
||||
getOutlineStyle() {
|
||||
const style = super.getOutlineStyle()
|
||||
style.stroke = 'none'
|
||||
if (style.hover) {
|
||||
style.hover.stroke = 'none'
|
||||
}
|
||||
return style
|
||||
}
|
||||
// 如果不用修改锚地形状,可以重写颜色相关样式
|
||||
getAnchorStyle(anchorInfo: any) {
|
||||
const style = super.getAnchorStyle(anchorInfo)
|
||||
if (anchorInfo.type === 'left') {
|
||||
style.fill = 'red'
|
||||
style.hover.fill = 'transparent'
|
||||
style.hover.stroke = 'transpanrent'
|
||||
style.className = 'lf-hide-default'
|
||||
} else {
|
||||
style.fill = 'green'
|
||||
}
|
||||
return style
|
||||
}
|
||||
setHeight(height: number, inputContainerHeight: number, outputContainerHeight: number) {
|
||||
this.height = height + inputContainerHeight + outputContainerHeight + 100
|
||||
this.baseHeight = height
|
||||
this.inputContainerHeight = inputContainerHeight
|
||||
this.outputContainerHeight = outputContainerHeight
|
||||
|
||||
this.outgoing.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
this.incoming.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
}
|
||||
setAttributes() {
|
||||
this.width = 500
|
||||
|
||||
const circleOnlyAsTarget = {
|
||||
message: '只允许从右边的锚点连出',
|
||||
validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => {
|
||||
return sourceAnchor.type === 'right'
|
||||
}
|
||||
}
|
||||
this.sourceRules.push(circleOnlyAsTarget)
|
||||
this.targetRules.push({
|
||||
message: '只允许连接左边的锚点',
|
||||
validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
|
||||
return targetAnchor.type === 'left'
|
||||
}
|
||||
})
|
||||
}
|
||||
getDefaultAnchor() {
|
||||
const {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
properties: { input, output }
|
||||
} = this
|
||||
|
||||
if (this.baseHeight === undefined) {
|
||||
this.baseHeight = 0
|
||||
}
|
||||
if (this.inputContainerHeight === undefined) {
|
||||
this.inputContainerHeight = 0
|
||||
}
|
||||
if (this.height === undefined) {
|
||||
this.height = 200
|
||||
}
|
||||
if (this.outputContainerHeight === undefined) {
|
||||
this.outputContainerHeight = 0
|
||||
}
|
||||
|
||||
const anchors: any = []
|
||||
if (input) {
|
||||
input.forEach((feild: any, index: any) => {
|
||||
anchors.push({
|
||||
x: x - width / 2 + 10,
|
||||
y: y - height / 2 + this.baseHeight + 35 + index * 24,
|
||||
id: `${id}_${feild.key}_left`,
|
||||
edgeAddable: false,
|
||||
type: 'left'
|
||||
})
|
||||
})
|
||||
}
|
||||
if (output) {
|
||||
output.forEach((feild: any, index: any) => {
|
||||
anchors.push({
|
||||
x: x + width / 2 - 10,
|
||||
y: y - height / 2 + this.baseHeight + this.inputContainerHeight + 30 + index * 24,
|
||||
id: `${id}_${feild.key}_right`,
|
||||
type: 'right'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return anchors
|
||||
}
|
||||
}
|
||||
|
||||
export { AppNodeModel, AppNode }
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { BezierEdge, BezierEdgeModel } from '@logicflow/core'
|
||||
|
||||
class CustomEdge2 extends BezierEdge {}
|
||||
|
||||
class CustomEdgeModel2 extends BezierEdgeModel {
|
||||
getEdgeStyle() {
|
||||
const style = super.getEdgeStyle()
|
||||
|
||||
// svg属性
|
||||
style.strokeWidth = 1
|
||||
style.stroke = '#ababac'
|
||||
return style
|
||||
}
|
||||
/**
|
||||
* 重写此方法,使保存数据是能带上锚点数据。
|
||||
*/
|
||||
getData() {
|
||||
const data: any = super.getData()
|
||||
if (data) {
|
||||
data.sourceAnchorId = this.sourceAnchorId
|
||||
data.targetAnchorId = this.targetAnchorId
|
||||
}
|
||||
return data
|
||||
}
|
||||
/**
|
||||
* 给边自定义方案,使其支持基于锚点的位置更新边的路径
|
||||
*/
|
||||
updatePathByAnchor() {
|
||||
// TODO
|
||||
const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId)
|
||||
const sourceAnchor = sourceNodeModel
|
||||
.getDefaultAnchor()
|
||||
.find((anchor: any) => anchor.id === this.sourceAnchorId)
|
||||
|
||||
const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId)
|
||||
const targetAnchor = targetNodeModel
|
||||
.getDefaultAnchor()
|
||||
.find((anchor: any) => anchor.id === this.targetAnchorId)
|
||||
if (sourceAnchor && targetAnchor) {
|
||||
const startPoint = {
|
||||
x: sourceAnchor.x,
|
||||
y: sourceAnchor.y
|
||||
}
|
||||
this.updateStartPoint(startPoint)
|
||||
const endPoint = {
|
||||
x: targetAnchor.x,
|
||||
y: targetAnchor.y
|
||||
}
|
||||
this.updateEndPoint(endPoint)
|
||||
}
|
||||
|
||||
// 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
|
||||
this.pointsList = []
|
||||
this.initPoints()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
type: 'app-edge',
|
||||
view: CustomEdge2,
|
||||
model: CustomEdgeModel2
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,115 @@
|
|||
import LogicFlow from '@logicflow/core'
|
||||
import { shapeList } from './data'
|
||||
type ShapeItem = {
|
||||
type?: string
|
||||
text?: string
|
||||
icon?: string
|
||||
label?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
properties?: Record<string, any>
|
||||
callback?: (lf: LogicFlow, container: HTMLElement) => void
|
||||
}
|
||||
|
||||
class AppMenu {
|
||||
lf: LogicFlow
|
||||
shapeList: ShapeItem[]
|
||||
panelEl?: HTMLDivElement
|
||||
static pluginName = 'AppMenu'
|
||||
domContainer?: HTMLElement
|
||||
constructor({ lf }: { lf: LogicFlow }) {
|
||||
this.lf = lf
|
||||
this.lf.setPatternItems = (shapeList: Array<ShapeItem>) => {
|
||||
this.setPatternItems(shapeList)
|
||||
}
|
||||
this.shapeList = shapeList
|
||||
this.panelEl = undefined
|
||||
this.domContainer = undefined
|
||||
}
|
||||
render(lf: LogicFlow, domContainer: HTMLElement) {
|
||||
this.destroy()
|
||||
if (!this.shapeList || this.shapeList.length === 0) {
|
||||
// 首次render后失败后,后续调用setPatternItems支持渲染
|
||||
this.domContainer = domContainer
|
||||
return
|
||||
}
|
||||
this.panelEl = document.createElement('div')
|
||||
this.panelEl.className = 'lf-dndpanel'
|
||||
this.shapeList.forEach((shapeItem) => {
|
||||
this.panelEl?.appendChild(this.createDndItem(shapeItem))
|
||||
})
|
||||
domContainer.appendChild(this.panelEl)
|
||||
this.domContainer = domContainer
|
||||
}
|
||||
destroy() {
|
||||
if (this.domContainer && this.panelEl && this.domContainer.contains(this.panelEl)) {
|
||||
this.domContainer.removeChild(this.panelEl)
|
||||
}
|
||||
}
|
||||
setPatternItems(shapeList: Array<ShapeItem>) {
|
||||
this.shapeList = shapeList
|
||||
// 支持渲染后重新设置拖拽面板
|
||||
if (this.domContainer) {
|
||||
this.render(this.lf, this.domContainer)
|
||||
}
|
||||
}
|
||||
private createDndItem(shapeItem: ShapeItem): HTMLElement {
|
||||
const el = document.createElement('div')
|
||||
el.className = shapeItem.className ? `lf-dnd-item ${shapeItem.className}` : 'lf-dnd-item'
|
||||
const shape = document.createElement('div')
|
||||
shape.className = 'lf-dnd-shape'
|
||||
|
||||
if (shapeItem.icon) {
|
||||
shape.style.backgroundImage = `url(${shapeItem.icon})`
|
||||
}
|
||||
el.appendChild(shape)
|
||||
if (shapeItem.label) {
|
||||
const text = document.createElement('div')
|
||||
text.innerText = shapeItem.label
|
||||
text.className = 'lf-dnd-text'
|
||||
el.appendChild(text)
|
||||
}
|
||||
if (shapeItem.disabled) {
|
||||
el.classList.add('disabled')
|
||||
// 保留callback的执行,可用于界面提示当前shapeItem的禁用状态
|
||||
el.onmousedown = () => {
|
||||
if (shapeItem.callback && this.domContainer) {
|
||||
shapeItem.callback(this.lf, this.domContainer)
|
||||
}
|
||||
}
|
||||
return el
|
||||
}
|
||||
el.onmousedown = () => {
|
||||
if (shapeItem.type) {
|
||||
this.lf.dnd.startDrag({
|
||||
type: shapeItem.type,
|
||||
properties: shapeItem.properties
|
||||
})
|
||||
}
|
||||
if (shapeItem.callback && this.domContainer) {
|
||||
shapeItem.callback(this.lf, this.domContainer)
|
||||
}
|
||||
}
|
||||
el.ondblclick = (e) => {
|
||||
this.lf.graphModel.eventCenter.emit('dnd:panel-dbclick', {
|
||||
e,
|
||||
data: shapeItem
|
||||
})
|
||||
}
|
||||
el.onclick = (e) => {
|
||||
this.lf.graphModel.eventCenter.emit('dnd:panel-click', {
|
||||
e,
|
||||
data: shapeItem
|
||||
})
|
||||
}
|
||||
el.oncontextmenu = (e) => {
|
||||
this.lf.graphModel.eventCenter.emit('dnd:panel-contextmenu', {
|
||||
e,
|
||||
data: shapeItem
|
||||
})
|
||||
}
|
||||
return el
|
||||
}
|
||||
}
|
||||
|
||||
export { AppMenu }
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="step-container" :class="`step-color-${Math.ceil(Math.random() * 4)}`">
|
||||
<div v-resize="resizeStepContainer">
|
||||
<div class="step-name">{{ nodeModel.properties.stepName }}</div>
|
||||
<div style="padding: 10px"><slot></slot></div>
|
||||
</div>
|
||||
<div class="input-container" v-resize="resetInputContainer">
|
||||
<el-divider> </el-divider>
|
||||
<div v-for="item in nodeModel.properties.input" class="step-feild">
|
||||
<span>{{ item.key }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="outout-container" v-resize="resetOutputContainer">
|
||||
<el-divider> </el-divider>
|
||||
<div v-for="item in nodeModel.properties.output" class="out-step-feild">
|
||||
<span>{{ item.key }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const height = ref<{
|
||||
stepContanerHeight: number
|
||||
inputContainerHeight: number
|
||||
outputContainerHeight: number
|
||||
}>({
|
||||
stepContanerHeight: 0,
|
||||
inputContainerHeight: 0,
|
||||
outputContainerHeight: 0
|
||||
})
|
||||
|
||||
const resizeStepContainer = (wh: any) => {
|
||||
if (wh.height) {
|
||||
height.value.stepContanerHeight = wh.height
|
||||
props.nodeModel.setHeight(
|
||||
height.value.stepContanerHeight,
|
||||
height.value.inputContainerHeight,
|
||||
height.value.outputContainerHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const resetOutputContainer = (wh: { height: number; width: number }) => {
|
||||
if (wh.height) {
|
||||
height.value.outputContainerHeight = wh.height
|
||||
props.nodeModel.setHeight(
|
||||
height.value.stepContanerHeight,
|
||||
height.value.inputContainerHeight,
|
||||
height.value.outputContainerHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const resetInputContainer = (wh: { height: number; width: number }) => {
|
||||
if (wh.height) {
|
||||
height.value.inputContainerHeight = wh.height
|
||||
props.nodeModel.setHeight(
|
||||
height.value.stepContanerHeight,
|
||||
height.value.inputContainerHeight,
|
||||
height.value.outputContainerHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
nodeModel: any
|
||||
}>()
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.el-divider--horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
.step-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.step-container::before {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #d79b00;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.step-container.step-color-1::before {
|
||||
background: #9673a6;
|
||||
}
|
||||
|
||||
.step-container.step-color-2::before {
|
||||
background: #dae8fc;
|
||||
}
|
||||
|
||||
.step-container.step-color-3::before {
|
||||
background: #82b366;
|
||||
}
|
||||
|
||||
.step-container.step-color-4::before {
|
||||
background: #f8cecc;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
height: 28px;
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.step-feild {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.feild-type {
|
||||
color: #9f9c9f;
|
||||
}
|
||||
.out-step-feild {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
float: right;
|
||||
}
|
||||
/* 自定义锚点样式 */
|
||||
.custom-anchor {
|
||||
cursor: crosshair;
|
||||
fill: #d9d9d9;
|
||||
stroke: #999;
|
||||
stroke-width: 1;
|
||||
rx: 3;
|
||||
ry: 3;
|
||||
}
|
||||
|
||||
.custom-anchor:hover {
|
||||
fill: #ff7f0e;
|
||||
stroke: #ff7f0e;
|
||||
}
|
||||
|
||||
.lf-node-not-allow .custom-anchor:hover {
|
||||
cursor: not-allowed;
|
||||
fill: #d9d9d9;
|
||||
stroke: #999;
|
||||
}
|
||||
|
||||
.incomming-anchor {
|
||||
stroke: #d79b00;
|
||||
}
|
||||
|
||||
.outgoing-anchor {
|
||||
stroke: #82b366;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<button @click="validate">点击校验</button>
|
||||
<button @click="getGraphData">点击获取流程数据</button>
|
||||
<div className="helloworld-app sql" style="height: 100vh; width: 100vw" id="container"></div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import LogicFlow from '@logicflow/core'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import AiChatNode from '@/work-flow/nodes/ai-chat-node/index.ts'
|
||||
import AppEdge from '@/work-flow/common/edge/index'
|
||||
import { AppMenu } from '@/work-flow/common/menu/index'
|
||||
import '@logicflow/extension/lib/style/index.css'
|
||||
import '@logicflow/core/dist/style/index.css'
|
||||
|
||||
LogicFlow.use(AppMenu)
|
||||
|
||||
const graphData = {
|
||||
nodes: [
|
||||
{
|
||||
id: '92a94b25-453d-4a00-aa26-9fed9b487e08',
|
||||
type: 'ai-chat-node',
|
||||
x: -10,
|
||||
y: 239,
|
||||
properties: {
|
||||
height: 200,
|
||||
stepName: 'AI对话',
|
||||
input: [{ key: '输入' }],
|
||||
output: [{ key: '输出' }],
|
||||
node_data: { model: 'shanghai', name: '222' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '34902d3d-a3ff-497f-b8e1-0c34a44d7dd4',
|
||||
type: 'ai-chat-node',
|
||||
x: 143,
|
||||
y: 523,
|
||||
properties: {
|
||||
height: 200,
|
||||
stepName: 'AI对话',
|
||||
input: [{ key: '输入' }],
|
||||
output: [{ key: '输出' }],
|
||||
node_data: { model: 'shanghai', name: '222222' }
|
||||
}
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'bc7297fa-2409-4c85-9a4d-3d74c9c1e30f',
|
||||
type: 'app-edge',
|
||||
sourceNodeId: '92a94b25-453d-4a00-aa26-9fed9b487e08',
|
||||
targetNodeId: '34902d3d-a3ff-497f-b8e1-0c34a44d7dd4',
|
||||
startPoint: { x: 230, y: 333.000005 },
|
||||
endPoint: { x: -97, y: 596.111105 },
|
||||
properties: {},
|
||||
pointsList: [
|
||||
{ x: 230, y: 333.000005 },
|
||||
{ x: 340, y: 333.000005 },
|
||||
{ x: -207, y: 596.111105 },
|
||||
{ x: -97, y: 596.111105 }
|
||||
],
|
||||
sourceAnchorId: '92a94b25-453d-4a00-aa26-9fed9b487e08_输出_right',
|
||||
targetAnchorId: '34902d3d-a3ff-497f-b8e1-0c34a44d7dd4_输入_left'
|
||||
},
|
||||
{
|
||||
id: '9f5740ce-b55e-42d4-90a2-a06f34d6f5ef',
|
||||
type: 'app-edge',
|
||||
sourceNodeId: '92a94b25-453d-4a00-aa26-9fed9b487e08',
|
||||
targetNodeId: '34902d3d-a3ff-497f-b8e1-0c34a44d7dd4',
|
||||
startPoint: { x: 230, y: 333.000005 },
|
||||
endPoint: { x: -97, y: 596.111105 },
|
||||
properties: {},
|
||||
pointsList: [
|
||||
{ x: 230, y: 333.000005 },
|
||||
{ x: 340, y: 333.000005 },
|
||||
{ x: -207, y: 596.111105 },
|
||||
{ x: -97, y: 596.111105 }
|
||||
],
|
||||
sourceAnchorId: '92a94b25-453d-4a00-aa26-9fed9b487e08_输出_right',
|
||||
targetAnchorId: '34902d3d-a3ff-497f-b8e1-0c34a44d7dd4_输入_left'
|
||||
}
|
||||
]
|
||||
}
|
||||
const lf = ref()
|
||||
|
||||
onMounted(() => {
|
||||
const container: any = document.querySelector('#container')
|
||||
if (container) {
|
||||
lf.value = new LogicFlow({
|
||||
keyboard: {
|
||||
enabled: true,
|
||||
shortcuts: [
|
||||
{
|
||||
keys: ['backspace'],
|
||||
callback: () => {
|
||||
const elements = lf.value.getSelectElements(true)
|
||||
if (
|
||||
(elements.edges && elements.edges.length > 0) ||
|
||||
(elements.nodes && elements.nodes.length > 0)
|
||||
) {
|
||||
const r = window.confirm('确定要删除吗?')
|
||||
if (r) {
|
||||
lf.value.clearSelectElements()
|
||||
elements.edges.forEach((edge: any) => {
|
||||
lf.value.deleteEdge(edge.id)
|
||||
})
|
||||
elements.nodes.forEach((node: any) => {
|
||||
lf.value.deleteNode(node.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
isSilentMode: false,
|
||||
container: container
|
||||
})
|
||||
lf.value.setTheme({
|
||||
bezier: {
|
||||
stroke: '#afafaf',
|
||||
strokeWidth: 1
|
||||
}
|
||||
})
|
||||
|
||||
lf.value.register(AiChatNode)
|
||||
lf.value.register(AppEdge)
|
||||
lf.value.setDefaultEdgeType('app-edge')
|
||||
|
||||
lf.value.render(graphData)
|
||||
|
||||
lf.value.translateCenter()
|
||||
}
|
||||
})
|
||||
const validate = () => {
|
||||
lf.value.graphModel.nodes.forEach((element: any) => {
|
||||
element.validate()
|
||||
})
|
||||
}
|
||||
const getGraphData = () => {
|
||||
console.log(JSON.stringify(lf.value.getGraphData()))
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.lf-dnd-text {
|
||||
width: 200px;
|
||||
}
|
||||
.lf-dnd-shape {
|
||||
height: 50px;
|
||||
}
|
||||
.lf-node-selected {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import ChatNodeVue from '@/flow/nodes/ai-chat-node/index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/flow/common/app-node/index'
|
||||
class ChatNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, ChatNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'ai-chat-node',
|
||||
model: AppNodeModel,
|
||||
view: ChatNode
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<NodeContaner :nodeModel="nodeModel">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="chat_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
class="mb-24"
|
||||
label-width="auto"
|
||||
ref="aiChatNodeFormRef"
|
||||
>
|
||||
<el-form-item
|
||||
label="模型"
|
||||
prop="model"
|
||||
:rules="{
|
||||
message: '模型不能为空',
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
}"
|
||||
>
|
||||
<el-select v-model="chat_data.model" placeholder="请选择模型">
|
||||
<el-option label="Zone one" value="shanghai" />
|
||||
<el-option label="Zone two" value="beijing" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="提示词"
|
||||
:rules="{
|
||||
message: '提示词不能为空',
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
}"
|
||||
prop="name"
|
||||
>
|
||||
<el-input v-model="chat_data.name" @focus="handleFocus" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</NodeContaner>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import NodeContaner from '@/flow/common/node-container/index.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const chat_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
props.nodeModel.properties.node_data = { model: '', name: '' }
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
props.nodeModel.properties.node_data = value
|
||||
}
|
||||
})
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
const handleFocus = () => {
|
||||
props.nodeModel.isSelected = false
|
||||
}
|
||||
const aiChatNodeFormRef = ref<FormInstance>()
|
||||
|
||||
const validate = () => {
|
||||
aiChatNodeFormRef.value?.validate()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
props.nodeModel.validate = validate
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import SearchDatasetVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/flow/common/app-node/index'
|
||||
class SearchDatasetNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, SearchDatasetVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'search-dataset-node',
|
||||
model: AppNodeModel,
|
||||
view: SearchDatasetNode
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<NodeContaner :nodeModel="nodeModel">
|
||||
<el-form
|
||||
:model="chat_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
class="mb-24"
|
||||
label-width="auto"
|
||||
ref="aiChatNodeFormRef"
|
||||
>
|
||||
<el-form-item label="知识库">
|
||||
<el-select v-model="chat_data.model" placeholder="请选择模型">
|
||||
<el-option label="Zone one" value="shanghai" />
|
||||
<el-option label="Zone two" value="beijing" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="提示词">
|
||||
<el-input v-model="chat_data.name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</NodeContaner>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import NodeContaner from '@/flow/common/node-container/index.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const chat_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
}
|
||||
return {}
|
||||
},
|
||||
set: (value) => {
|
||||
props.nodeModel.properties.node_data = value
|
||||
}
|
||||
})
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const aiChatNodeFormRef = ref<FormInstance>()
|
||||
|
||||
const validate = () => {
|
||||
aiChatNodeFormRef.value?.validate()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
props.nodeModel.validate = validate
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
Loading…
Reference in New Issue