|
|
@@ -0,0 +1,538 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="4" class="sidebar-wrapper">
|
|
|
+ <el-input
|
|
|
+ v-model="modelQueryParams.name"
|
|
|
+ placeholder="请输入模型名称"
|
|
|
+ clearable
|
|
|
+ prefix-icon="Search"
|
|
|
+ style="margin-bottom: 10px"
|
|
|
+ @change="getModelList()"
|
|
|
+ />
|
|
|
+ <el-radio-group style="flex-wrap: nowrap;" v-model="modelQueryParams.isPublic" @change="getModelList">
|
|
|
+ <el-radio-button label="已发布" value="1"/>
|
|
|
+ <el-radio-button label="未发布" value="0"/>
|
|
|
+ </el-radio-group>
|
|
|
+ <div class="tool-container">
|
|
|
+ <div :class="{'active': modelId === item.mdid}" v-for="(item, index) in modelOptions" :key="index"
|
|
|
+ class="tool-item" @click="handleModelClick(item.mdid)">
|
|
|
+ {{ item.name }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <!-- v-loading="loading" -->
|
|
|
+ <el-col :span="16" style="position: relative;">
|
|
|
+ <el-button-group class="flow-button-group">
|
|
|
+ <el-button type="primary" :icon="Promotion" @click="saveStep">保存</el-button>
|
|
|
+ <!-- <el-button type="primary" :icon="Check">测试</el-button>-->
|
|
|
+ </el-button-group>
|
|
|
+ <VueFlow :nodes="nodes" :edges="edges" @drop="onDrop" @dragover="onDragOver" @dragleave="onDragLeave"
|
|
|
+ @connect="onConnect" fit-view-on-init>
|
|
|
+ <template #node-special="specialNodeProps">
|
|
|
+ <SpecialNode :id="specialNodeProps.id" :position="specialNodeProps.position" :data="specialNodeProps.data"/>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- bind your custom edge type to a component by using slots, slot names are always `edge-<type>` -->
|
|
|
+ <template #edge-special="specialEdgeProps">
|
|
|
+ <SpecialEdge v-bind="specialEdgeProps"/>
|
|
|
+ </template>
|
|
|
+ </VueFlow>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4" class="sidebar-wrapper">
|
|
|
+ <el-input
|
|
|
+ v-model="toolQueryParams.name"
|
|
|
+ placeholder="请输入工具名称"
|
|
|
+ clearable
|
|
|
+ prefix-icon="Search"
|
|
|
+ style="margin-bottom: 10px"
|
|
|
+ @change="getServiceList()"
|
|
|
+ />
|
|
|
+ <el-radio-group style="flex-wrap: nowrap;" v-model="toolType">
|
|
|
+ <el-radio-button label="工具类" value="1"/>
|
|
|
+ <el-radio-button label="服务" value="0"/>
|
|
|
+ </el-radio-group>
|
|
|
+ <div v-show="toolType === '1'" class="tool-container">
|
|
|
+ <div></div>
|
|
|
+ </div>
|
|
|
+ <div v-show="toolType === '0'" class="tool-container">
|
|
|
+ <div v-for="(item, index) in serviceList" :key="index" class="tool-item" :draggable="true"
|
|
|
+ @dragstart="onDragStart($event, item)">
|
|
|
+ {{ item.label }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 添加或修改部门对话框 -->
|
|
|
+ <el-dialog :title="title" v-model="open" width="600px" append-to-body>
|
|
|
+ <template #header="{ close, titleId, titleClass }">
|
|
|
+ <div style="display: flex;align-items: center;">
|
|
|
+ <el-tag>{{ form.type }}</el-tag>
|
|
|
+ <h4 style="margin: 0 0 0 5px;" :id="titleId" :class="titleClass">{{ form.label }}</h4>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px" label-position="top">
|
|
|
+ <el-form-item label="API">
|
|
|
+ <el-row style="width: 100%;" :gutter="20">
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-select v-model="form.config.method" style="width: 100px">
|
|
|
+ <el-option label="GET" value="GET"></el-option>
|
|
|
+ <el-option label="POST" value="POST"></el-option>
|
|
|
+ <el-option label="HEAD" value="HEAD"></el-option>
|
|
|
+ <el-option label="PATCH" value="PATCH"></el-option>
|
|
|
+ <el-option label="PUT" value="PUT"></el-option>
|
|
|
+ <el-option label="DELETE" value="DELETE"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="20">
|
|
|
+ <el-input v-model="form.config.url" placeholder="请输入URL"/>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="HEADERS">
|
|
|
+ <dynamic-map v-model="form.config.headers"></dynamic-map>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="PARAMS">
|
|
|
+ <dynamic-map v-model="form.config.params"></dynamic-map>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="BODY">
|
|
|
+ <!-- 单选 -->
|
|
|
+ <el-radio-group v-model="form.bodyType">
|
|
|
+ <el-radio value="none">none</el-radio>
|
|
|
+ <el-radio value="form-data">form-data</el-radio>
|
|
|
+ <el-radio value="x-www-form-urlencoded">x-www-form-urlencoded</el-radio>
|
|
|
+ <el-radio value="JSON">JSON</el-radio>
|
|
|
+ <el-radio value="raw">raw</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ <dynamic-map v-if="form.bodyType === 'x-www-form-urlencoded'" v-model="form.config.body"></dynamic-map>
|
|
|
+ </el-form-item>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="失败处理">
|
|
|
+ <el-select v-model="form.errorPolicy" style="width: 100px">
|
|
|
+ <el-option label="报错" value="ABORT"></el-option>
|
|
|
+ <el-option label="忽视" value="IGNORE"></el-option>
|
|
|
+ <el-option label="重连" value="RETRY"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item v-if="form.errorPolicy === 'RETRY'" label="失败重连次数">
|
|
|
+ <el-input-number v-model="form.retryCount" :min="1" :max="30"/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitForm">确 定</el-button>
|
|
|
+ <el-button @click="cancel">取 消</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script setup>
|
|
|
+import DynamicMap from '@/components/DynamicMap/index.vue'
|
|
|
+import {Promotion} from '@element-plus/icons-vue'
|
|
|
+import {ref} from 'vue'
|
|
|
+import {useVueFlow, VueFlow} from '@vue-flow/core'
|
|
|
+import SpecialNode from './components/SpecialNode.vue'
|
|
|
+import SpecialEdge from './components/SpecialEdge.vue'
|
|
|
+import {getPtServiceList} from "@/api/service/info.js";
|
|
|
+import {getModelList2} from "@/api/register/regCom.js";
|
|
|
+import {copyObject} from "@/utils/index.js";
|
|
|
+import {getWorkflowByModelId, saveWorkflow} from "@/api/standardization/workflow.js";
|
|
|
+
|
|
|
+const {
|
|
|
+ onInit,
|
|
|
+ findNode,
|
|
|
+ fitView,
|
|
|
+ snapToGrid,
|
|
|
+ addEdges,
|
|
|
+ addNodes,
|
|
|
+ toObject,
|
|
|
+ screenToFlowCoordinate,
|
|
|
+ onNodesInitialized,
|
|
|
+ updateNode,
|
|
|
+ onNodeClick,
|
|
|
+ onEdgeClick,
|
|
|
+ getNodes,
|
|
|
+ getEdges,
|
|
|
+ removeNodes,
|
|
|
+ removeEdges,
|
|
|
+} = useVueFlow()
|
|
|
+// to enable snapping to grid
|
|
|
+snapToGrid.value = true
|
|
|
+
|
|
|
+const {proxy} = getCurrentInstance();
|
|
|
+const modelQueryParams = ref({
|
|
|
+ name: undefined,
|
|
|
+ isPublic: '0',
|
|
|
+});
|
|
|
+const toolQueryParams = ref({
|
|
|
+ name: undefined,
|
|
|
+});
|
|
|
+const modelOptions = ref(undefined);
|
|
|
+const modelId = ref(undefined);
|
|
|
+const loading = ref(true);
|
|
|
+const toolType = ref('0');
|
|
|
+const serviceList = ref([]);
|
|
|
+
|
|
|
+const draggedData = ref(undefined);
|
|
|
+const isDragging = ref(false);
|
|
|
+const isDragOver = ref(false);
|
|
|
+const nodes = ref([]);
|
|
|
+const edges = ref([]);
|
|
|
+
|
|
|
+const title = ref('')
|
|
|
+const open = ref(false)
|
|
|
+
|
|
|
+
|
|
|
+function handleModelClick(mid) {
|
|
|
+ modelId.value = mid
|
|
|
+ removeNodes(getNodes.value)
|
|
|
+ removeEdges(getEdges.value)
|
|
|
+ getWorkflow(mid)
|
|
|
+}
|
|
|
+
|
|
|
+onInit((instance) => {
|
|
|
+ fitView()
|
|
|
+ const node = findNode('1')
|
|
|
+ if (node) {
|
|
|
+ node.position = {x: 100, y: 100}
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onNodeClick(({event, node}) => {
|
|
|
+ form.value = node.data
|
|
|
+ open.value = true
|
|
|
+ title.value = node.data.label
|
|
|
+ nodeId.value = node.id
|
|
|
+});
|
|
|
+
|
|
|
+// Edge click event handler
|
|
|
+onEdgeClick(({event, edge}) => {
|
|
|
+ console.log('Edge clicked:', edge, event);
|
|
|
+});
|
|
|
+
|
|
|
+const data = reactive({
|
|
|
+ form: {config: {}},
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ userName: undefined,
|
|
|
+ phonenumber: undefined,
|
|
|
+ status: undefined,
|
|
|
+ deptId: undefined
|
|
|
+ },
|
|
|
+ rules: {
|
|
|
+ userName: [{required: true, message: "用户名称不能为空", trigger: "blur"}, {
|
|
|
+ min: 2,
|
|
|
+ max: 20,
|
|
|
+ message: "用户名称长度必须介于 2 和 20 之间",
|
|
|
+ trigger: "blur"
|
|
|
+ }],
|
|
|
+ nickName: [{required: true, message: "用户昵称不能为空", trigger: "blur"}],
|
|
|
+ password: [{required: true, message: "用户密码不能为空", trigger: "blur"}, {
|
|
|
+ min: 5,
|
|
|
+ max: 20,
|
|
|
+ message: "用户密码长度必须介于 5 和 20 之间",
|
|
|
+ trigger: "blur"
|
|
|
+ }, {pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur"}],
|
|
|
+ email: [{type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"]}],
|
|
|
+ phonenumber: [{pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur"}]
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const {queryParams, form, rules} = toRefs(data);
|
|
|
+const nodeId = ref(null)
|
|
|
+
|
|
|
+/**
|
|
|
+ * 开始拖拽选项的事件
|
|
|
+ * @param event
|
|
|
+ * @param data
|
|
|
+ */
|
|
|
+function onDragStart(event, data) {
|
|
|
+ if (event.dataTransfer) {
|
|
|
+ event.dataTransfer.setData('application/vueflow', data)
|
|
|
+ event.dataTransfer.effectAllowed = 'move'
|
|
|
+ }
|
|
|
+
|
|
|
+ draggedData.value = data
|
|
|
+ isDragging.value = true
|
|
|
+
|
|
|
+ document.addEventListener('drop', onDragEnd)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 拖拽到画布vueflow的事件
|
|
|
+ * @param event
|
|
|
+ */
|
|
|
+function onDragOver(event) {
|
|
|
+ event.preventDefault()
|
|
|
+
|
|
|
+ if (draggedData.value) {
|
|
|
+ isDragOver.value = true
|
|
|
+
|
|
|
+ if (event.dataTransfer) {
|
|
|
+ event.dataTransfer.dropEffect = 'move'
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 拖拽放下的事件
|
|
|
+ * @param event
|
|
|
+ */
|
|
|
+function onDrop(event) {
|
|
|
+ const position = screenToFlowCoordinate({
|
|
|
+ x: event.clientX,
|
|
|
+ y: event.clientY,
|
|
|
+ })
|
|
|
+
|
|
|
+ const nodeId = Math.random() + "id";
|
|
|
+
|
|
|
+ const data = copyObject(draggedData.value)
|
|
|
+ const newNode = {
|
|
|
+ id: nodeId,
|
|
|
+ type: 'special',
|
|
|
+ position,
|
|
|
+ data,
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新位置到鼠标中心
|
|
|
+ const {off} = onNodesInitialized(() => {
|
|
|
+ updateNode(nodeId, (node) => ({
|
|
|
+ position: {x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2},
|
|
|
+ }))
|
|
|
+
|
|
|
+ off()
|
|
|
+ })
|
|
|
+
|
|
|
+ addNodes(newNode)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 拖拽到画布外面的的事件
|
|
|
+ */
|
|
|
+function onDragLeave() {
|
|
|
+ isDragOver.value = false
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 拖拽结束
|
|
|
+ */
|
|
|
+function onDragEnd() {
|
|
|
+ isDragging.value = false
|
|
|
+ isDragOver.value = false
|
|
|
+ draggedData.value = null
|
|
|
+ document.removeEventListener('drop', onDragEnd)
|
|
|
+}
|
|
|
+
|
|
|
+function onConnect(params) {
|
|
|
+ console.log('on connect', params)
|
|
|
+ addEdges(params)
|
|
|
+}
|
|
|
+
|
|
|
+/** 查询模型列表 */
|
|
|
+function getModelList() {
|
|
|
+ getModelList2(modelQueryParams.value).then(res => {
|
|
|
+ loading.value = false;
|
|
|
+ modelOptions.value = res.data;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** 通过条件过滤节点 */
|
|
|
+const filterNode = (value, data) => {
|
|
|
+ if (!value) return true;
|
|
|
+ return data.label.indexOf(value) !== -1;
|
|
|
+};
|
|
|
+
|
|
|
+/** 节点单击事件 */
|
|
|
+function handleNodeClick(data) {
|
|
|
+ queryParams.value.modeId = 1 // data.id;
|
|
|
+ handleQuery();
|
|
|
+}
|
|
|
+
|
|
|
+/** 搜索按钮操作 */
|
|
|
+function handleQuery() {
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+}
|
|
|
+
|
|
|
+/** 查询流程图 */
|
|
|
+function getList() {
|
|
|
+ loading.value = true;
|
|
|
+ // listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {
|
|
|
+ // loading.value = false;
|
|
|
+ // userList.value = res.rows;
|
|
|
+ // total.value = res.total;
|
|
|
+ // });
|
|
|
+}
|
|
|
+
|
|
|
+/** 查询服务列表 */
|
|
|
+function getServiceList() {
|
|
|
+ getPtServiceList(toolQueryParams.value).then(res => {
|
|
|
+ serviceList.value = res.data.map(item => {
|
|
|
+ return {
|
|
|
+ id: item.id,
|
|
|
+ label: item.name,
|
|
|
+ type: 'API',
|
|
|
+ config: {
|
|
|
+ url: item.url,
|
|
|
+ method: item.rqtype,
|
|
|
+ params: item.params,
|
|
|
+ headers: item.headers,
|
|
|
+ body: item.body,
|
|
|
+ },
|
|
|
+ errorPolicy: 'ABORT',
|
|
|
+ retryCount: 0,
|
|
|
+ outputMapping: null,
|
|
|
+ data: item,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function submitForm() {
|
|
|
+ updateNode(nodeId.value, (node) => ({
|
|
|
+ data: form.value,
|
|
|
+ }))
|
|
|
+ cancel()
|
|
|
+}
|
|
|
+
|
|
|
+function cancel() {
|
|
|
+ form.value = {config: {}};
|
|
|
+ open.value = false;
|
|
|
+ title.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+function saveStep() {
|
|
|
+
|
|
|
+ if (!modelId.value) {
|
|
|
+ proxy.$modal.msgError("请选择模型");
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const nodes = getNodes.value.map(res => {
|
|
|
+ return {
|
|
|
+ id: res.id,
|
|
|
+ type: res.type,
|
|
|
+ position: res.position,
|
|
|
+ data: res.data,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const edges = getEdges.value.map(res => {
|
|
|
+ return {
|
|
|
+ source: res.source,
|
|
|
+ target: res.target,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const data = {
|
|
|
+ mdid: modelId.value,
|
|
|
+ graph: JSON.stringify({
|
|
|
+ nodes: nodes,
|
|
|
+ edges: edges,
|
|
|
+ }),
|
|
|
+ }
|
|
|
+ saveWorkflow(data).then(res => {
|
|
|
+ // 测试
|
|
|
+ proxy.$modal.msgError("请输入建表语句");
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function getWorkflow(modelId) {
|
|
|
+ getWorkflowByModelId(modelId).then(res => {
|
|
|
+ if (res.data && res.data.graph) {
|
|
|
+ const {nodes, edges} = JSON.parse(res.data.graph)
|
|
|
+ addNodes(nodes)
|
|
|
+ addEdges(edges)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+getModelList()
|
|
|
+getServiceList()
|
|
|
+</script>
|
|
|
+<style>
|
|
|
+@import '@vue-flow/core/dist/style.css';
|
|
|
+@import '@vue-flow/core/dist/theme-default.css';
|
|
|
+</style>
|
|
|
+<style scoped>
|
|
|
+.app-container {
|
|
|
+ height: 100%;
|
|
|
+
|
|
|
+ & > div {
|
|
|
+ height: 100%;
|
|
|
+
|
|
|
+ & > div {
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-wrapper {
|
|
|
+ padding: 10px 0;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid var(--el-border-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .tool-container {
|
|
|
+ overflow: auto;
|
|
|
+ height: calc(100% - 74px);
|
|
|
+
|
|
|
+ .tool-item {
|
|
|
+ padding: 5px 10px;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover, &.active {
|
|
|
+ background-color: var(--el-color-primary);
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ .flow-button-group {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ left: 10px;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-radio-group) {
|
|
|
+ display: flex;
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-radio-button) {
|
|
|
+ flex: 1;
|
|
|
+ border: var(--el-border);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-radio-button.is-active) {
|
|
|
+ background-color: var(--el-color-primary);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-radio-button:first-child) {
|
|
|
+ border-top-left-radius: 5px;
|
|
|
+ border-bottom-left-radius: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-radio-button:last-child) {
|
|
|
+ border-top-right-radius: 5px;
|
|
|
+ border-bottom-right-radius: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-radio-button__inner) {
|
|
|
+ transition: none;
|
|
|
+ border: none !important;
|
|
|
+}
|
|
|
+</style>
|