Merge branch 'dev' of 192.168.0.100:smart_bench/cmvr-iot-ui into dev

This commit is contained in:
lixiaolong 2025-11-20 16:04:51 +08:00
commit 6558135b8e
24 changed files with 4736 additions and 3737 deletions

View File

@ -1,55 +1,63 @@
import request from '@/utils/request' import request from '@/utils/request'
// 发布流程 // 发布流程
export function flowDeploy(data) { export function flowDeploy(data) {
return request({ return request({
url: '/flow/publish', url: '/flow/publish',
method: 'post', method: 'post',
data: data data: data
}) })
} }
export function flowExecute(data) { export function flowExecute(data) {
return request({ return request({
url: '/flow/execute', url: '/flow/execute',
method: 'post', method: 'post',
data: data data: data
}) })
} }
export function flowExecuteTrial(data) { export function flowAction(data) {
return request({ return request({
url: '/flow/executeTrial', url: '/flow/action',
method: 'post', method: 'post',
data: data data: data
}) })
} }
export function flowView(data) { export function flowExecuteTrial(data) {
return request({ return request({
url: '/flow/view', url: '/flow/executeTrial',
method: 'post', method: 'post',
data: data data: data
}) })
} }
export function flowPause(instId) { export function flowView(data) {
return request({ return request({
url: `/flow/pause/${instId}`, url: '/flow/view',
method: 'post' method: 'post',
}) data: data
} })
}
export function flowResume(instId) {
return request({ export function flowPause(instId) {
url: `/flow/resume/${instId}`, return request({
method: 'post' url: `/flow/pause/${instId}`,
}) method: 'post'
} })
}
export function flowStop(instId) {
return request({ export function flowResume(instId) {
url: `/flow/stop/${instId}`, return request({
method: 'post' url: `/flow/resume/${instId}`,
}) method: 'post'
})
}
export function flowStop(instId) {
return request({
url: `/flow/stop/${instId}`,
method: 'post'
})
} }

View File

@ -0,0 +1,46 @@
import request from '@/utils/request'
// 开始录音
export function startMicrophoneApi(data) {
return request({
url: '/api/Microphone/start',
method: 'post',
params: data
})
}
// 暂停录音
export function pauseMicrophoneApi(data) {
return request({
url: '/api/Microphone/pause',
method: 'post',
params: data
})
}
// 恢复录音
export function resumeMicrophoneApi(data) {
return request({
url: '/api/Microphone/resume',
method: 'post',
params: data
})
}
// 停止录音
export function stopMicrophoneApi(data) {
return request({
url: '/api/Microphone/stop',
method: 'post',
params: data
})
}
// 播放音频
export function playSpeaker(data) {
return request({
url: '/api/Speaker/play',
method: 'post',
params: data
})
}

View File

@ -1,6 +1,7 @@
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
/* Layout */ /* Layout */
import Layout from "@/layout"; import Layout from "@/layout";
import path from "path";
/** /**
* Note: 路由配置项 * Note: 路由配置项
@ -92,7 +93,14 @@ export const constantRoutes = [
name: "灵巧手示教", name: "灵巧手示教",
meta: { title: "灵巧手详情" }, meta: { title: "灵巧手详情" },
hidden: true, hidden: true,
}, },{
path: "microphone/:id",
component: () =>
import("@/views/device/register/components/Microphone/index.vue"),
name: "麦克风示教",
meta: { title: "麦克风详情" },
hidden: true,
}
], ],
}, },
{ {

View File

@ -127,6 +127,34 @@ export const formatTableData = (arr, data = {}) => {
} }
export const addNewEdge = (nodeId) => { export const addNewEdge = (nodeId) => {
const { edges } = lf.getGraphData();
const ttt = edges.slice(); // 浅拷贝(比 JSON 深拷贝高效 10x+
const edgesToDelete = ttt.filter(_edge =>
_edge.sourceNodeId === nodeId || _edge.targetNodeId === nodeId
);
edgesToDelete.forEach(edge => lf.deleteEdge(edge.id));
if (window.addNewEdgeTimer) {
clearTimeout(window.addNewEdgeTimer); // 清理上一次未执行的定时器
}
window.addNewEdgeTimer = setTimeout(() => {
edgesToDelete.forEach(item => {
lf.addEdge({
type: "bezier",
sourceNodeId: item.sourceNodeId,
targetNodeId: item.targetNodeId,
sourceAnchorId: item.sourceAnchorId,
targetAnchorId: item.targetAnchorId
});
})
delete window.addNewEdgeTimer;
}, 50)
}
export const newEdge = (nodeId) => {
const { edges } = lf.getGraphData(); const { edges } = lf.getGraphData();
const arr = JSON.parse(JSON.stringify(edges)) const arr = JSON.parse(JSON.stringify(edges))
arr.forEach(_edge => { arr.forEach(_edge => {
@ -144,6 +172,22 @@ export const addNewEdge = (nodeId) => {
}) })
} }
export const initNodeZoom = (modelId, nodeZoom, className, init=false) => {
const nodes = document.getElementsByClassName(modelId)
if (nodes.length > 0) {
const node = nodes[0]
const parent = node.closest(className);
if (nodeZoom) {
parent.style.width = '680px'
} else {
parent.style.width = '380px'
}
if (!init) {
addNewEdge(modelId)
}
}
}
export const getInputNumber = (nodeId) => { export const getInputNumber = (nodeId) => {
const data = lf.getGraphData(); const data = lf.getGraphData();
let flag = false let flag = false

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,225 @@
<template>
<div class="microphone-container">
<div class="title">远程麦克风</div>
<div class="audiosList" v-if="audiosList.length > 0">
<div class="speaker-container">
<div class="speaker-title">选择扬声器</div>
<el-select v-model="speaker" size="small">
<el-option
v-for="item in speakerOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</div>
<div class="audioItem" v-for="item in audiosList">
<div>{{ item }}</div>
<div>
<el-button
circle
size="small"
:icon="VideoPlay"
@click="handlePlay(item)"
></el-button>
</div>
</div>
</div>
<div class="btn-container">
<el-tooltip
class="box-item"
effect="dark"
content="点击开始录音"
placement="top-start"
v-if="recordStatus === 'end'"
>
<el-icon class="btn" :size="40" @click="startRecording"><Microphone /></el-icon>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="点击暂停录音"
placement="top-start"
v-if="recordStatus === 'recording'"
>
<el-icon class="btn" :size="40" @click="pauseRecording"><VideoPause /></el-icon>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="点击恢复录音"
placement="top-start"
v-if="recordStatus === 'pause'"
>
<el-icon class="btn" :size="40" @click="recoverRecording"><Mic /></el-icon>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="点击结束录音"
placement="top-start"
v-if="recordStatus !== 'end'"
>
<el-icon class="btn" :size="40" @click="stopRecording"><Mute /></el-icon>
</el-tooltip>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { Microphone, VideoPause, Mute, Mic, VideoPlay } from "@element-plus/icons-vue";
import {
startMicrophoneApi,
pauseMicrophoneApi,
resumeMicrophoneApi,
stopMicrophoneApi,
playSpeaker
} from '@/api/device/microphone'
import { useRoute } from 'vue-router'
import { ElMessage } from "element-plus";
const route = useRoute()
const terminalId = route.query.terminalId
const deviceId = route.query.deviceId
const recordStatus = ref('end');
const isRecording = ref(false)
const list = []
const audiosList = ref([])
const speaker = ref('spk1')
const speakerOptions = ref(['spk1'])
/**
* 开始录音
*/
const startRecording = async () => {
const fileName = new Date().getTime() + '.wav'
isRecording.value = true
const res = await startMicrophoneApi({
terminalId,
deviceId,
filePath: `/home/share/record/audios/${terminalId}_${deviceId}_${fileName}`
})
if (res.code === 200) {
recordStatus.value = 'recording'
list.push(`${terminalId}_${deviceId}_${fileName}`)
}
};
/**
* 暂停录音
*/
const pauseRecording = async () => {
const res = await pauseMicrophoneApi({
terminalId,
deviceId
})
if (res.code === 200) {
recordStatus.value = 'pause'
}
};
/**
* 恢复录音
*/
const recoverRecording = async () => {
const res = await resumeMicrophoneApi({
terminalId,
deviceId
})
if (res.code === 200) {
recordStatus.value = 'recording'
}
}
/**
* 结束录音
*/
const stopRecording = async () => {
const res = await stopMicrophoneApi({
terminalId,
deviceId
})
if (res.code === 200) {
recordStatus.value = 'end'
audiosList.value.unshift(list[list.length - 1])
}
isRecording.value = false
};
/**
* 播放录音
*/
const handlePlay = async (audioPath) => {
const res = await playSpeaker({
terminalId,
deviceId: speaker.value,
audioPath: `/home/share/record/audios/${audioPath}`
})
if (res.code === 200) {
ElMessage.success('播放成功')
}
}
</script>
<style lang="scss" scoped>
.microphone-container {
display: flex;
flex: 1;
flex-direction: column;
max-width: 800px;
width: 100%;
height: calc(100vh - 125px);
background-color: #fff;
border-radius: 20px;
margin: auto;
.audiosList {
margin: 20px;
max-height: 280px;
overflow-y: auto;
.speaker-container {
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 12px;
.speaker-title {
width: 80px;
}
.el-select {
width: 200px;
}
}
.audioItem {
display: flex;
align-items: center;
gap: 20px;
}
}
.title {
padding-top: 20px;
font-size: 20px;
text-align: center;
color: #333;
}
.btn-container {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
.btn {
margin-right: 10px;
cursor: pointer;
}
}
}
</style>

View File

@ -378,6 +378,10 @@ function handleControl(row) {
const fullPath = `${row.deviceModel}/${row.id}`; // const fullPath = `${row.deviceModel}/${row.id}`; //
router.push({ router.push({
path: fullPath, path: fullPath,
query: {
terminalId: row.idDeDeviceTerminalConfig,
deviceId: row.deviceCode
}
}); // 使 }); // 使
// intoControlPage(`/${row.deviceModel}`, `${row.id}`) // intoControlPage(`/${row.deviceModel}`, `${row.id}`)
} }

View File

@ -1,171 +1,239 @@
<template> <template>
<div class="title__container"> <div class="title__container">
<div class="title"> <div class="title">
<div class="left"> <div class="left">
<img :src="icon" alt=""> <img :src="icon" alt="" />
<div class="text__container"> <div class="text__container">
<div class="text__title" v-if="showTextTitle"> <div class="text__title" v-if="showTextTitle">
<span class="text">{{ nodeName }}</span> <span class="text">{{ nodeName }}</span>
<el-icon class="editIcon" @click="showTextTitle = false"><EditPen /></el-icon> <el-icon class="editIcon" @click="showTextTitle = false"
</div> ><EditPen
<div class="input_name_container" v-else> /></el-icon>
<el-input v-model="nodeName" /> </div>
<el-button class="input_name_select" circle size="small" @click="confirmNodeName"> <div class="input_name_container" v-else>
<el-icon><Select /></el-icon> <el-input v-model="nodeName" />
</el-button> <el-button
<el-button class="input_name_closeBold" circle size="small" @click="cancelNodeName"> class="input_name_select"
<el-icon><CloseBold /></el-icon> circle
</el-button> size="small"
</div> @click="confirmNodeName"
</div> >
</div> <el-icon><Select /></el-icon>
<div class="right"> </el-button>
<el-button circle size="small" @click="deleteNode"> <el-button
<el-icon><Delete /></el-icon> class="input_name_closeBold"
</el-button> circle
</div> size="small"
</div> @click="cancelNodeName"
<div class="subTitle">{{ props.nodeDesc }}</div> >
</div> <el-icon><CloseBold /></el-icon>
</template> </el-button>
<script setup lang="js"> </div>
import { ref } from 'vue' </div>
import { EditPen, Select, CloseBold, Delete } from '@element-plus/icons-vue' </div>
import { ElMessageBox } from 'element-plus' <div class="right">
<el-tooltip
const props = defineProps({ class="box-item"
icon: { effect="dark"
type: String, content="执行"
default: '' placement="top"
}, >
nodeId: { <el-button v-if="nodeType && nodeType !=='NONE'" circle @click="execute">
type: String, <el-icon>
default: '' <VideoPlay />
}, </el-icon>
nodeProperties: { </el-button>
type: Object, </el-tooltip>
default: () => {} <el-tooltip
}, class="box-item"
nodeDesc: { effect="dark"
type: String, :content="zoomState ? '缩小' : '放大'"
default: '' placement="top"
}, >
nodeName: { <el-button v-if="showZoom" circle @click="zoom">
type: String, <el-icon>
default: '' <ZoomOut v-if="zoomState" />
}, <ZoomIn v-else />
nodeType: { </el-icon>
type: String, </el-button>
default: '' </el-tooltip>
} <el-tooltip
}) class="box-item"
effect="dark"
const showTextTitle = ref(true) content="删除"
const nodeName = ref(props.nodeProperties.name || props.nodeName) placement="top"
>
const confirmNodeName = () => { <el-button circle @click="deleteNode">
const properties = lf.getProperties(props.nodeId) <el-icon><Delete /></el-icon>
lf.setProperties(props.nodeId, { </el-button>
...properties, </el-tooltip>
name: nodeName.value </div>
}) </div>
showTextTitle.value = true <div class="subTitle">{{ props.nodeDesc }}</div>
} </div>
</template>
const cancelNodeName = () => { <script setup lang="js">
nodeName.value = props.nodeProperties?.name || props.nodeName import { ref } from 'vue'
showTextTitle.value = true import { EditPen, Select, CloseBold, Delete, ZoomOut, ZoomIn, VideoPlay } from '@element-plus/icons-vue'
} import { ElMessageBox } from 'element-plus'
const deleteNode = () => { const props = defineProps({
ElMessageBox.confirm( icon: {
'是否删除该节点?', type: String,
'警告', default: ''
{ },
confirmButtonText: '确定', nodeId: {
cancelButtonText: '取消', type: String,
type: 'warning', default: ''
} },
) nodeProperties: {
.then(() => { type: Object,
if (props.nodeType === 'selectArea') { default: () => {}
const children = lf.getNodeModelById(props.nodeId).children },
lf.getNodeModelById(props.nodeId).children = [] nodeDesc: {
children.forEach(item => { type: String,
lf.getNodeModelById(item).draggable = true default: ''
}) },
} nodeName: {
lf.deleteNode(props.nodeId); type: String,
}) default: ''
} },
</script> nodeType: {
<style lang="scss" scoped> type: String,
.title__container { default: ''
.title { },
display: flex; showZoom: {
align-items: center; type: Boolean,
justify-content: space-between; default: true
},
.left { zoomState: {
display: flex; type: Boolean,
default: false
img { }
width: 24px; })
height: 24px;
} const emits = defineEmits(['zoom'])
.text__container { const showTextTitle = ref(true)
.text__title { const nodeName = ref(props.nodeProperties.name || props.nodeName)
display: flex;
align-items: center; const confirmNodeName = () => {
const properties = lf.getProperties(props.nodeId)
.text { lf.setProperties(props.nodeId, {
font-size: 16px; ...properties,
font-weight: bold; name: nodeName.value
margin: 0 12px; })
} showTextTitle.value = true
}
.editIcon {
cursor: pointer; const cancelNodeName = () => {
} nodeName.value = props.nodeProperties?.name || props.nodeName
} showTextTitle.value = true
}
.input_name_container {
display: flex; const deleteNode = () => {
align-items: center; ElMessageBox.confirm(
'是否删除该节点?',
.el-input { '警告',
margin-left: 12px; {
} confirmButtonText: '确定',
cancelButtonText: '取消',
.input_name_select { type: 'warning',
color: #00b42a; }
margin-left: 4px; )
border: none; .then(() => {
} if (props.nodeType === 'selectArea') {
const children = lf.getNodeModelById(props.nodeId).children
.input_name_closeBold { lf.getNodeModelById(props.nodeId).children = []
color: #f53f3f; children.forEach(item => {
margin-left: 4px; lf.getNodeModelById(item).draggable = true
border: none; })
} }
} lf.deleteNode(props.nodeId);
} })
} }
.right { const zoom = () => {
cursor: pointer; emits('zoom', !props.zoomState)
}
.el-button {
border: none; const execute = () => {
} const myEvent = new CustomEvent('singleNodeExecution', {
} bubbles: false, // false
} cancelable: false, // preventDefault() false
detail: { ...props.nodeProperties } // event.detail
.subTitle { });
color: #737a87; document.dispatchEvent(myEvent);
font-size: 12px; }
margin: 8px 0; </script>
} <style lang="scss" scoped>
} .title__container {
</style> .title {
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
img {
width: 24px;
height: 24px;
}
.text__container {
.text__title {
display: flex;
align-items: center;
.text {
font-size: 16px;
font-weight: bold;
margin: 0 12px;
}
.editIcon {
cursor: pointer;
}
}
.input_name_container {
display: flex;
align-items: center;
.el-input {
margin-left: 12px;
}
.input_name_select {
color: #00b42a;
margin-left: 4px;
border: none;
}
.input_name_closeBold {
color: #f53f3f;
margin-left: 4px;
border: none;
}
}
}
}
.right {
cursor: pointer;
.el-button {
border: none;
font-size: 24px;
}
}
}
.subTitle {
color: #737a87;
font-size: 12px;
margin: 8px 0;
}
}
</style>

View File

@ -16,6 +16,8 @@ import planSvg from './icon/plan.svg'
import awakenSvg from './icon/awaken.svg' import awakenSvg from './icon/awaken.svg'
import dialogueSvg from './icon/dialogue.svg' import dialogueSvg from './icon/dialogue.svg'
import audioSvg from './icon/audio.svg' import audioSvg from './icon/audio.svg'
import touchSvg from './icon/touch.svg'
import expressionSvg from './icon/expression.svg'
import { v4 as randomUUID } from 'uuid' import { v4 as randomUUID } from 'uuid'
@ -40,6 +42,11 @@ export const lfConfig = {
// foldSize: 30, // 折叠后显示的图标尺寸 // foldSize: 30, // 折叠后显示的图标尺寸
// }, // },
edgeType: "bezier", edgeType: "bezier",
zoomConfig: {
min: 0.05, // 最小缩放比例(支持更小值,如 0.01,但不建议过小)
max: 3, // 最大缩放比例(可自定义)
step: 0.1 // 每次滚轮缩放的步长(默认 0.1
},
style: { style: {
anchor: { anchor: {
show: true, // 强制全局锚点显示 show: true, // 强制全局锚点显示
@ -73,11 +80,39 @@ export const registerCustomizeNode = (lf) => {
}; };
const deviceOptions = () => { const deviceOptions = () => {
return ['cam1', 'cam2', 'cam3', 'cam4'] return [{
value: 'cam1',
label: 'cam1'
}, {
value: 'cam2',
label: 'cam2'
}, {
value: 'cam3',
label: 'cam3'
}, {
value: 'cam4',
label: 'cam4'
}]
} }
const audioOptions = () => { const audioOptions = () => {
return ['spk1'] return [{
value: 'spk1',
label: 'spk1'
}]
}
const expressOptions = () => {
return [{
value: 1,
label: '高兴'
}, {
value: 2,
label: '惊讶'
}, {
value: 3,
label: '疲惫'
}]
} }
export const collapseList = [ export const collapseList = [
@ -90,6 +125,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "在获取相机照片时,需要先启动相机", desc: "在获取相机照片时,需要先启动相机",
action: 'CAMERA_START', action: 'CAMERA_START',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
], ],
@ -101,6 +137,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "获取相机拍摄的照片", desc: "获取相机拍摄的照片",
action: 'CAMERA_GETRGBIMAGE', action: 'CAMERA_GETRGBIMAGE',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
], ],
@ -113,6 +150,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "获取相机照片后,关闭该相机", desc: "获取相机照片后,关闭该相机",
action: 'CAMERA_STOP', action: 'CAMERA_STOP',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
], ],
@ -129,6 +167,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "开启摄像头,并开始录像", desc: "开启摄像头,并开始录像",
action: 'CAMERA_RECORDING_START', action: 'CAMERA_RECORDING_START',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
], ],
@ -140,6 +179,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "停止录像,并关闭摄像头", desc: "停止录像,并关闭摄像头",
action: 'CAMERA_RECORDING_STOP', action: 'CAMERA_RECORDING_STOP',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
], ],
@ -224,6 +264,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "用于启动麦克风的节点", desc: "用于启动麦克风的节点",
action: 'MICROPHONE_START', action: 'MICROPHONE_START',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
], ],
@ -235,6 +276,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "用于停止麦克风的节点", desc: "用于停止麦克风的节点",
action: 'MICROPHONE_START', action: 'MICROPHONE_START',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
], ],
@ -262,6 +304,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "用于获取触控坐标的节点", desc: "用于获取触控坐标的节点",
action: 'TOUCH_COORDINATES', action: 'TOUCH_COORDINATES',
nodeType: "LLM",
nodeParams: [ nodeParams: [
{ name: "targetFeature", type: "input", input: "运动模式", disabled: true }, { name: "targetFeature", type: "input", input: "运动模式", disabled: true },
{ name: "manufacturer", type: "input", input: "xiaomi", disabled: true }, { name: "manufacturer", type: "input", input: "xiaomi", disabled: true },
@ -275,6 +318,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "将指令文本(text)处理合成语音,输出语音路径(audioPath)", desc: "将指令文本(text)处理合成语音,输出语音路径(audioPath)",
action: 'GENERATE_ADVANCED_AUDIO', action: 'GENERATE_ADVANCED_AUDIO',
nodeType: "LLM",
outputType: 'json', outputType: 'json',
nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }], nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}] outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -284,6 +328,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "意图识别,输出类型(type) 1: 语音交互 2: 触控交互 3: 闲聊 4: 知识问答", desc: "意图识别,输出类型(type) 1: 语音交互 2: 触控交互 3: 闲聊 4: 知识问答",
action: 'INTENT_RECOGNITION', action: 'INTENT_RECOGNITION',
nodeType: "LLM",
outputType: 'json', outputType: 'json',
nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }], nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'type', type: 'string', desc: '1语音交互 2触控交互 3: 闲聊 4:知识问答', disabled: true}] outputParams: [{ name: 'type', type: 'string', desc: '1语音交互 2触控交互 3: 闲聊 4:知识问答', disabled: true}]
@ -312,6 +357,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "唤醒语料处理,输入唤醒语料(wakeCorpus),输出语音路径(audioPath)", desc: "唤醒语料处理,输入唤醒语料(wakeCorpus),输出语音路径(audioPath)",
action: 'VI_CORPUS_WAKE', action: 'VI_CORPUS_WAKE',
nodeType: "EDGE",
outputType: 'json', outputType: 'json',
nodeParams: [{ name: 'wakeCorpus', type: "input", input: "", disabled: true }], nodeParams: [{ name: 'wakeCorpus', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}] outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -322,6 +368,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "测试语料-单次对话语料处理,输入单次对话语料(testCorpus),输出语音路径(audioPath)", desc: "测试语料-单次对话语料处理,输入单次对话语料(testCorpus),输出语音路径(audioPath)",
action: 'VI_CORPUS_SINGLE', action: 'VI_CORPUS_SINGLE',
nodeType: "EDGE",
outputType: 'json', outputType: 'json',
nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }], nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}] outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -332,6 +379,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "测试语料-连续对话语料处理,输入连续对话语料(testCorpus),输出语音路径(audioPath)", desc: "测试语料-连续对话语料处理,输入连续对话语料(testCorpus),输出语音路径(audioPath)",
action: 'VI_CORPUS_CONTINUOUS', action: 'VI_CORPUS_CONTINUOUS',
nodeType: "EDGE",
outputType: 'json', outputType: 'json',
nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }], nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}] outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -342,6 +390,7 @@ export const collapseList = [
type: "serviceNode", type: "serviceNode",
desc: "用于播放语料的节点,需要输入语音路径(audioPath)和播放语音的设备id(deviceId)", desc: "用于播放语料的节点,需要输入语音路径(audioPath)和播放语音的设备id(deviceId)",
action: 'VI_PLAY_CORPUS', action: 'VI_PLAY_CORPUS',
nodeType: "EDGE",
outputType: 'json', outputType: 'json',
nodeParams: [ nodeParams: [
{ name: 'audioPath', type: "input", input: "", disabled: true }, { name: 'audioPath', type: "input", input: "", disabled: true },
@ -350,4 +399,59 @@ export const collapseList = [
} }
], ],
}, },
{
collapseTitle: "触控交互",
nodeList: [
{
icon: touchSvg,
name: "触控",
type: "serviceNode",
desc: "去触控坐标的点位",
action: 'TOUCH',
nodeType: "EDGE",
nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "touchParams", type: "input", input: "", disabled: true }
],
outputType: 'json',
},
{
icon: expressionSvg,
name: "开始说话",
type: "serviceNode",
desc: "控制机器人说话",
action: 'BIO_HEAD_SPEAK_START',
nodeType: "EDGE",
nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true }
],
outputType: 'json',
},
{
icon: expressionSvg,
name: "停止说话",
type: "serviceNode",
desc: "停止机器人说话",
action: 'BIO_HEAD_SPEAK_STOP',
nodeType: "EDGE",
nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true }
],
outputType: 'json',
},
{
icon: expressionSvg,
name: "表情",
type: "serviceNode",
desc: "控制机器人的面部表情",
action: 'BIO_HEAD_SPECIAL_EXPRESSION',
nodeType: "EDGE",
nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "expressKey", type: "input",componentType: 'select', selectOptions: expressOptions(), disabled: true },
],
outputType: 'json',
},
],
},
] ]

View File

@ -1 +1 @@
<svg t="1758526744847" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8113" width="200" height="200"><path d="M512 117.76c218.065455 0 395.636364 135.68 395.636364 302.545455s-177.570909 302.545455-395.636364 302.545454a144.058182 144.058182 0 0 0-50.501818 10.472727 791.272727 791.272727 0 0 0-162.909091 99.374546c-4.421818-128.232727-23.272727-168.96-56.087273-191.534546C162.909091 584.145455 116.363636 503.621818 116.363636 420.538182c0-167.098182 177.570909-302.545455 395.636364-302.545455m0-69.818182c-256 0-465.454545 166.632727-465.454545 372.363637 0 110.312727 59.810909 209.454545 155.22909 277.643636 23.272727 16.756364 27.694545 116.363636 27.694546 192.232727a41.192727 41.192727 0 0 0 40.494545 41.89091 40.029091 40.029091 0 0 0 23.272728-8.61091c58.414545-45.149091 141.730909-105.425455 192-124.50909a74.007273 74.007273 0 0 1 26.763636-6.05091c256 0 465.454545-166.865455 465.454545-372.363636s-209.454545-372.363636-465.454545-372.363636z" fill="#65BEE0" p-id="8114"></path><path d="M663.272727 325.818182h-302.545454a34.909091 34.909091 0 0 0 0 69.818182h302.545454a34.909091 34.909091 0 0 0 0-69.818182zM616.727273 488.727273h-209.454546a34.909091 34.909091 0 0 0 0 69.818182h209.454546a34.909091 34.909091 0 0 0 0-69.818182z" fill="#65BEE0" p-id="8115"></path></svg> <svg t="1762763163012" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="23408" width="200" height="200"><path d="M0 0m234.057143 0l555.885714 0q234.057143 0 234.057143 234.057143l0 555.885714q0 234.057143-234.057143 234.057143l-555.885714 0q-234.057143 0-234.057143-234.057143l0-555.885714q0-234.057143 234.057143-234.057143Z" fill="#65BEE0" p-id="23409" data-spm-anchor-id="a313x.search_index.0.i15.2cb83a81yhcKKA" class=""></path><path d="M735.319771 277.942857H376.451657A69.266286 69.266286 0 0 0 307.2 347.194514v197.953829A69.266286 69.266286 0 0 0 376.451657 614.4H446.171429s3.657143 43.885714-25.6 58.514286c0 0 72.206629-14.628571 94.6176-58.514286h220.130742A69.266286 69.266286 0 0 0 804.571429 545.148343V347.194514A69.266286 69.266286 0 0 0 735.319771 277.942857zM446.171429 482.742857a29.257143 29.257143 0 0 1-29.257143-29.257143 29.257143 29.257143 0 0 1 29.257143-29.257143 29.257143 29.257143 0 0 1 29.257142 29.257143 29.257143 29.257143 0 0 1-29.257142 29.257143z m109.714285 0a29.257143 29.257143 0 0 1-29.257143-29.257143 29.257143 29.257143 0 0 1 29.257143-29.257143 29.257143 29.257143 0 0 1 29.257143 29.257143 29.257143 29.257143 0 0 1-29.257143 29.257143z m109.714286 0a29.257143 29.257143 0 0 1-29.257143-29.257143 29.257143 29.257143 0 0 1 29.257143-29.257143 29.257143 29.257143 0 0 1 29.257143 29.257143 29.257143 29.257143 0 0 1-29.257143 29.257143z" fill="#ffffff" opacity=".34" p-id="23410" data-spm-anchor-id="a313x.search_index.0.i17.2cb83a81yhcKKA" class="selected"></path><path d="M662.176914 351.085714H303.3088A69.266286 69.266286 0 0 0 234.057143 420.337371v197.953829A69.266286 69.266286 0 0 0 303.3088 687.542857H373.028571s3.657143 43.885714-25.6 58.514286c0 0 72.206629-14.628571 94.6176-58.514286h220.130743A69.266286 69.266286 0 0 0 731.428571 618.2912V420.337371A69.266286 69.266286 0 0 0 662.176914 351.085714zM373.028571 555.885714a29.257143 29.257143 0 0 1-29.257142-29.257143 29.257143 29.257143 0 0 1 29.257142-29.257142 29.257143 29.257143 0 0 1 29.257143 29.257142 29.257143 29.257143 0 0 1-29.257143 29.257143z m109.714286 0a29.257143 29.257143 0 0 1-29.257143-29.257143 29.257143 29.257143 0 0 1 29.257143-29.257142 29.257143 29.257143 0 0 1 29.257143 29.257142 29.257143 29.257143 0 0 1-29.257143 29.257143z m109.714286 0a29.257143 29.257143 0 0 1-29.257143-29.257143 29.257143 29.257143 0 0 1 29.257143-29.257142 29.257143 29.257143 0 0 1 29.257143 29.257142 29.257143 29.257143 0 0 1-29.257143 29.257143z" fill="#ffffff" p-id="23411" data-spm-anchor-id="a313x.search_index.0.i14.2cb83a81yhcKKA" class="selected"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<svg t="1762999534829" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4760" width="200" height="200"><path d="M512 979C263.472 979 62 777.528 62 529S263.472 79 512 79s450 201.472 450 450-201.472 450-450 450zM337 479c41.421 0 75-33.579 75-75s-33.579-75-75-75-75 33.579-75 75 33.579 75 75 75z m350 0c41.421 0 75-33.579 75-75s-33.579-75-75-75-75 33.579-75 75 33.579 75 75 75zM312 629c0 110.457 89.543 200 200 200s200-89.543 200-200H312z" fill="#65BEE0" p-id="4761"></path></svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@ -0,0 +1 @@
<svg t="1762762024042" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7115" width="200" height="200"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#65BEE0" p-id="7116" data-spm-anchor-id="a313x.search_index.0.i4.2cb83a81yhcKKA" class="selected"></path><path d="M440.018824 310.452706c6.625882 0 13.703529 2.198588 20.781176 7.68 11.294118 8.884706 17.709176 22.588235 17.769412 36.954353l0.391529 108.242823 0.120471 43.248942 0.150588 31.442823s20.239059-27.648 49.935059-27.648c15.932235 0 34.484706 7.890824 54.091294 32.165647 0 0 18.191059-30.539294 47.435294-30.539294 15.36 0 33.731765 8.342588 54.091294 33.912471 0 0 17.588706-34.424471 41.562353-34.424471 8.071529 0 16.865882 3.915294 25.99153 14.486588 10.752 12.739765 16.564706 29.063529 16.564705 45.808941v230.098824s-0.722824 5.963294-7.168 14.938353a63.608471 63.608471 0 0 1-51.83247 26.352941l-239.555765 0.060235a73.396706 73.396706 0 0 1-57.313882-27.557647l-131.373177-164.111059a46.019765 46.019765 0 0 1-6.595764-46.983529c4.517647-10.390588 13.251765-19.275294 29.635764-19.275294 8.432941 0 18.763294 2.349176 31.62353 7.951059 42.736941 19.154824 58.006588 59.542588 58.006588 59.542588l0.692706-146.100706 0.210823-43.158588 0.512-105.682824c0.060235-14.546824 6.716235-28.400941 18.31153-37.255529 7.228235-5.571765 16.263529-10.149647 25.961412-10.149647zM436.705882 180.705882a165.647059 165.647059 0 0 1 123.723294 275.787294 38.068706 38.068706 0 0 1-29.455058 12.468706h-1.987765a8.794353 8.794353 0 0 1-8.734118-8.734117l-0.090353-15.450353a15.058824 15.058824 0 0 1 4.336942-10.541177l0.090352-0.090353a123.392 123.392 0 0 0 36.352-87.792941c0-33.189647-12.950588-64.391529-36.352-87.883294A123.663059 123.663059 0 0 0 436.705882 222.117647c-33.189647 0-64.391529 12.950588-87.883294 36.352a123.663059 123.663059 0 0 0-36.352 87.883294c0 33.189647 12.950588 64.391529 36.352 87.883294l0.090353 0.060236c3.162353 3.162353 4.969412 7.499294 4.909177 11.956705a21.353412 21.353412 0 0 1-36.74353 14.697412A165.647059 165.647059 0 0 1 436.705882 180.705882z" fill="#ffffff" p-id="7117" data-spm-anchor-id="a313x.search_index.0.i3.2cb83a81yhcKKA" class=""></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,46 @@
<template> <template>
<div class="node__container"> <div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus"> <NodeState :state="nodeOperatingStatus">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
<template #output> <template #output>
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" /> <JsonViewer
<el-image v-if="props.properties.outputType === 'json'"
v-if="props.properties.outputType === 'img'" :value="outputJsonData"
v-for="item in outputJsonData.imageUrl" copyable
style="width: 60px; height: 60px" boxed
:src="item" sort
:preview-src-list="outputJsonData.imageUrl" theme="light"
:preview-teleported="true" />
show-progress <el-image
fit="fill" v-if="props.properties.outputType === 'img'"
v-for="item in outputJsonData.imageUrl"
style="width: 60px; height: 60px"
:src="item"
:preview-src-list="outputJsonData.imageUrl"
:preview-teleported="true"
show-progress
fit="fill"
/>
<IPlayer
v-if="props.properties.outputType === 'video'"
v-for="item in outputJsonData.videoUrl"
:videoUrl="item"
/> />
<IPlayer v-if="props.properties.outputType === 'video'" v-for="item in outputJsonData.videoUrl" :videoUrl="item" />
</template> </template>
</NodeState> </NodeState>
<NodeTitle <NodeTitle
:icon="props.properties.icon" :icon="props.properties.icon"
:nodeId="props.model.id" :nodeId="props.model.id"
:nodeProperties="props.properties" :nodeProperties="props.properties"
:nodeType="props.properties.nodeType || 'NONE'"
:nodeName="props.properties.name" :nodeName="props.properties.name"
:nodeDesc="props.properties.desc" :nodeDesc="props.properties.desc"
:zoom-state="nodeZoom"
@zoom="zoom"
/> />
<div class="input__container"> <div class="input__container" v-show="nodeZoom">
<div class="title"> <div class="title">
<div class="left"> <div class="left">
<div class="tag"></div> <div class="tag"></div>
@ -57,11 +71,14 @@
<el-form-item <el-form-item
:label="index === 0 ? '参数名' : ''" :label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`" :prop="`nodeParams.${index}.name`"
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]" :rules="[
{ required: true, message: '请输入参数名', trigger: 'blur' },
]"
> >
<el-input <el-input
:disabled="property?.disabled || false" :disabled="property?.disabled || false"
v-model="property.name" v-model="property.name"
@keydown="handleInputKeydown"
placeholder="请输入" placeholder="请输入"
clearable clearable
/> />
@ -70,33 +87,76 @@
:label="index === 0 ? '参数值' : ''" :label="index === 0 ? '参数值' : ''"
:prop="`nodeParams.${index}.type`" :prop="`nodeParams.${index}.type`"
> >
<el-select v-model="property.type" @change="handleTypeChange(index)"> <el-select
v-model="property.type"
@change="handleTypeChange(index)"
>
<el-option label="引用" value="quote" /> <el-option label="引用" value="quote" />
<el-option label="输入" value="input" /> <el-option label="输入" value="input" />
</el-select> </el-select>
<el-form-item <el-form-item
v-if="property.type === 'input'" v-if="property.type === 'input'"
:rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]" :rules="[
{
required: true,
message: '请输入参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.input`" :prop="`nodeParams.${index}.input`"
> >
<el-input-number v-if="property.componentType === 'number'" v-model="property.input" :min="0" :controls="false" :step-strictly="true" placeholder="请输入" clearable /> <el-input-number
<el-select v-model="property.input" v-else-if="property.componentType === 'select'" > v-if="property.componentType === 'number'"
<el-option v-for="item in property.selectOptions" :key="item" :label="item" :value="item" /> v-model="property.input"
</el-select> :min="0"
<el-input v-else v-model="property.input" placeholder="请输入" clearable /> :controls="false"
:step-strictly="true"
placeholder="请输入"
clearable
/>
<el-select
v-model="property.input"
v-else-if="property.componentType === 'select'"
>
<el-option
v-for="item in property.selectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
@keydown="handleInputKeydown"
v-else
v-model="property.input"
placeholder="请输入"
clearable
/>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="property.type === 'quote'" v-if="property.type === 'quote'"
:rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]" :rules="[
{
required: true,
message: '请选择参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.quote`" :prop="`nodeParams.${index}.quote`"
> >
<el-cascader <el-cascader
:ref="el => { if (el) cascaderRefs[index] = el }" :ref="
(el) => {
if (el) cascaderRefs[index] = el;
}
"
v-model="property.quote" v-model="property.quote"
:checkStrictly="true" :checkStrictly="true"
:options="quoteOptions" :options="quoteOptions"
placeholder="请选择" placeholder="请选择"
@visible-change="(visible) => visibleChange(visible, index, property.quote)" @visible-change="
(visible) => visibleChange(visible, index, property.quote)
"
@change="(value) => cascaderChange(value, index)" @change="(value) => cascaderChange(value, index)"
/> />
</el-form-item> </el-form-item>
@ -106,51 +166,53 @@
</el-form> </el-form>
</div> </div>
</div> </div>
<div class="output__container" v-if="props.properties?.outputParams?.length > 0"> <div class="output__container" v-show="nodeZoom">
<div class="title"> <div v-if="props.properties?.outputParams?.length > 0">
<div class="left"> <div class="title">
<div class="tag"></div> <div class="left">
<div class="text">输出</div> <div class="tag"></div>
<div class="text">输出</div>
</div>
<div class="right">
<el-button
:disabled="flowStore.disableForm"
@click="addOutputFormItem"
class="addFormItem"
>
<el-icon :size="20"><Plus /></el-icon>
</el-button>
</div>
</div> </div>
<div class="right" >
<el-button
:disabled="flowStore.disableForm"
@click="addOutputFormItem"
class="addFormItem"
>
<el-icon :size="20"><Plus /></el-icon>
</el-button>
</div>
</div>
<div class="form__container"> <div class="form__container">
<el-form <el-form
:inline="true" :inline="true"
:model="formData" :model="formData"
label-position="top" label-position="top"
label-width="auto" label-width="auto"
:rules="outputRules" :rules="outputRules"
ref="outputFormRef" ref="outputFormRef"
> >
<FormItemRecursive <FormItemRecursive
formType="output" formType="output"
:current-list="formData.outputParams" :current-list="formData.outputParams"
prop-path="outputParams" prop-path="outputParams"
:depth="0" :depth="0"
:is-first-level="true" :is-first-level="true"
:endDepth="2" :endDepth="2"
:parent-path="[]" :parent-path="[]"
@delete-item="deleteTopLevelItem" @delete-item="deleteTopLevelItem"
/> />
</el-form> </el-form>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, onUnmounted } from "vue"; import { ref, reactive, onMounted, onUnmounted, nextTick } from "vue";
import { getInput } from "@/utils/flow"; import { getInput, initNodeZoom } from "@/utils/flow";
import { useFlowStore } from "@/store/modules/flow"; import { useFlowStore } from "@/store/modules/flow";
import { Plus } from "@element-plus/icons-vue"; import { Plus } from "@element-plus/icons-vue";
import NodeTitle from "../../components/NodeTitle.vue"; import NodeTitle from "../../components/NodeTitle.vue";
@ -171,7 +233,7 @@ const flowStore = useFlowStore();
const formData = reactive({ const formData = reactive({
nodeParams: [], nodeParams: [],
outputParams: [] outputParams: [],
}); });
const rules = reactive({}); const rules = reactive({});
@ -189,39 +251,40 @@ const handleTypeChange = (index) => {
} }
}; };
const cascaderRefs = ref([]) const cascaderRefs = ref([]);
const outputRules = reactive({}); const outputRules = reactive({});
const cascaderChange = (value, index) => { const cascaderChange = (value, index) => {
const selectedOptions = cascaderRefs.value[index].getCheckedNodes(true); const selectedOptions = cascaderRefs.value[index].getCheckedNodes(true);
formData.nodeParams[index].quote = value formData.nodeParams[index].quote = value;
formData.nodeParams[index].quoteType = selectedOptions[0].data.type formData.nodeParams[index].quoteType = selectedOptions[0].data.type;
} };
const addOutputFormItem = () => { const addOutputFormItem = () => {
formData.outputParams.push({ formData.outputParams.push({
name: '', name: "",
type: '', type: "",
desc: '', desc: "",
children: [] children: [],
}) });
} };
const dynamicForm = ref(); const dynamicForm = ref();
const outputFormRef = ref() const outputFormRef = ref();
const setNodeProperties = async () => { const setNodeProperties = async () => {
try { try {
const result = await dynamicForm.value.validate(); const result = await dynamicForm.value.validate();
let flag = true let flag = true;
if (outputFormRef.value) { if (outputFormRef.value) {
flag = await outputFormRef.value.validate() flag = await outputFormRef.value.validate();
} }
if (result && flag) { if (result && flag) {
const data = toRaw(formData); const data = toRaw(formData);
const properties = lf.getProperties(props.model.id); const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, { lf.setProperties(props.model.id, {
...properties, ...properties,
...data, ...data,
zoom: nodeZoom.value
}); });
emits("contentChange"); emits("contentChange");
} }
@ -286,20 +349,26 @@ const addFormItem = () => {
// //
const deleteTopLevelItem = (fullPath) => { const deleteTopLevelItem = (fullPath) => {
// //
let currentLevel = formData.outputParams; let currentLevel = formData.outputParams;
// //
for (let i = 0; i < fullPath.length - 1; i++) { for (let i = 0; i < fullPath.length - 1; i++) {
const index = fullPath[i]; const index = fullPath[i];
// //
currentLevel = currentLevel[index].children; currentLevel = currentLevel[index].children;
} }
// //
const lastIndex = fullPath[fullPath.length - 1]; const lastIndex = fullPath[fullPath.length - 1];
currentLevel.splice(lastIndex, 1); currentLevel.splice(lastIndex, 1);
emits("contentChange"); emits("contentChange");
};
const nodeZoom = ref(props?.properties?.zoom ?? true)
const zoom = (flag) => {
nodeZoom.value = flag
initNodeZoom(props.model.id, nodeZoom.value, '.node__box')
} }
watch( watch(
@ -311,8 +380,15 @@ watch(
if (option) { if (option) {
quoteOptions.value = option; quoteOptions.value = option;
} }
nextTick(() => {
initNodeZoom(props.model.id, props?.properties?.zoom ?? true, '.node__box', true)
})
} }
if (props.properties.outputParams && props.properties.outputParams.length > 0) { if (
props.properties.outputParams &&
props.properties.outputParams.length > 0
) {
formData.outputParams = props.properties.outputParams; formData.outputParams = props.properties.outputParams;
} }
}, },
@ -322,6 +398,15 @@ watch(
} }
); );
const handleInputKeydown = (e) => {
// Logic Flow
e.stopPropagation();
// Ctrl+V
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
e.returnValue = true; //
}
};
onMounted(() => { onMounted(() => {
emitter.on("changeNodeState", (data) => { emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) { if (data.nodeId === props.model.id) {
@ -499,13 +584,13 @@ defineExpose({
:deep(.el-row) { :deep(.el-row) {
align-items: end; align-items: end;
.el-input { .el-input {
--el-input-width: 148px; --el-input-width: 148px;
} }
.el-select { .el-select {
--el-select-width: 148px; --el-select-width: 148px;
} }
.el-cascader { .el-cascader {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="node__container"> <div class="node__container" :class="props.model.id">
<keep-alive> <keep-alive>
<NodeState :state="nodeOperatingStatus"> <NodeState :state="nodeOperatingStatus">
<template #input> <template #input>
@ -12,12 +12,29 @@
</keep-alive> </keep-alive>
<div class="title__container"> <div class="title__container">
<div class="title"> <div class="title">
<img src="../../icon/start.svg" alt="" /> <div class="left">
<span class="text">Start</span> <img src="../../icon/start.svg" alt="" />
<span class="text">Start</span>
</div>
<div class="right">
<el-tooltip
class="box-item"
effect="dark"
:content="nodeZoom ? '缩小' : '放大'"
placement="top"
>
<el-button circle @click="zoom">
<el-icon>
<ZoomOut v-if="nodeZoom" />
<ZoomIn v-else />
</el-icon>
</el-button>
</el-tooltip>
</div>
</div> </div>
<div class="subTitle">工作流的起始节点用于设定启动工作流需要的信息</div> <div class="subTitle">工作流的起始节点用于设定启动工作流需要的信息</div>
</div> </div>
<div class="input__container"> <div class="input__container" v-show="nodeZoom">
<div class="title"> <div class="title">
<div class="left"> <div class="left">
<div class="tag"></div> <div class="tag"></div>
@ -60,9 +77,10 @@
</template> </template>
<script setup> <script setup>
import { watch, reactive, toRaw, ref, onMounted, onUnmounted } from "vue"; import { watch, reactive, toRaw, ref, onMounted, onUnmounted, nextTick } from "vue";
import { Plus } from "@element-plus/icons-vue"; import { Plus, ZoomOut, ZoomIn } from "@element-plus/icons-vue";
import { useFlowStore } from "@/store/modules/flow"; import { useFlowStore } from "@/store/modules/flow";
import { initNodeZoom } from "@/utils/flow";
import NodeState from "../../components/NodeState.vue"; import NodeState from "../../components/NodeState.vue";
import "vue3-json-viewer/dist/index.css"; import "vue3-json-viewer/dist/index.css";
import { emitter } from "@/utils/eventBus"; import { emitter } from "@/utils/eventBus";
@ -112,6 +130,7 @@ const setNodeProperties = async () => {
lf.setProperties(props.model.id, { lf.setProperties(props.model.id, {
...props.properties, ...props.properties,
...data, ...data,
zoom: nodeZoom.value
}); });
emits("contentChange"); emits("contentChange");
} }
@ -129,6 +148,9 @@ watch(
() => { () => {
if (props.properties.inputParams && props.properties.inputParams.length > 0) { if (props.properties.inputParams && props.properties.inputParams.length > 0) {
formData.inputParams = props.properties.inputParams; formData.inputParams = props.properties.inputParams;
nextTick(() => {
initNodeZoom(props.model.id, props?.properties?.zoom ?? true, '.node__box', true)
})
} }
}, },
{ {
@ -200,6 +222,12 @@ const validateForm = async () => {
} }
}; };
const nodeZoom = ref(props?.properties?.zoom ?? true)
const zoom = () => {
nodeZoom.value = !nodeZoom.value
initNodeZoom(props.model.id, nodeZoom.value, '.node__box')
}
onUnmounted(() => { onUnmounted(() => {
emitter.off("changeNodeState"); emitter.off("changeNodeState");
emitter.off("contentChange"); emitter.off("contentChange");
@ -220,16 +248,26 @@ defineExpose({
.title { .title {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
img { .left {
width: 24px; img {
height: 24px; width: 24px;
height: 24px;
}
.text {
font-size: 16px;
font-weight: bold;
margin-left: 12px;
}
} }
.text { .right {
font-size: 16px; .el-button {
font-weight: bold; border: none;
margin-left: 12px; font-size: 24px;
}
} }
} }

View File

@ -1,107 +1,117 @@
<template> <template>
<div class="group__container" @mouseleave="setNodeProperties"> <div class="group__container" :class="props.model.id" @mouseleave="setNodeProperties">
<NodeTitle <NodeTitle
:icon="loop" :icon="loop"
:nodeId="props.model.id" :nodeId="props.model.id"
:nodeProperties="props.properties" :nodeProperties="props.properties"
:nodeName="props.properties?.name || '循环'" :nodeName="props.properties?.name || '循环'"
nodeDesc="循环执行一系列任务,直至输出所有结果" nodeDesc="循环执行一系列任务,直至输出所有结果"
:showZoom="false"
/> />
<div class="loop__container"> <div class="loop__container">
<div> <div>
<el-form <el-form
:inline="true" :inline="true"
:model="formData" :model="formData"
:rules="rules" :rules="rules"
ref="dynamicForm" ref="dynamicForm"
label-position="top" label-position="top"
label-width="auto" label-width="auto"
:disabled="flowStore.disableForm" :disabled="flowStore.disableForm"
> >
<div v-for="(property, index) in formData.nodeParams" :key="index"> <div v-for="(property, index) in formData.nodeParams" :key="index">
<el-row> <el-row>
<el-form-item
:label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`"
:rules="[
{ required: true, message: '请输入参数名', trigger: 'blur' },
]"
>
<el-input
:disabled="index === 0"
v-model="property.name"
placeholder="请输入"
clearable
/>
</el-form-item>
<el-form-item
:label="index === 0 ? '参数值' : ''"
:prop="`nodeParams.${index}.type`"
>
<el-select
v-model="property.type"
@change="handleTypeChange(index)"
>
<el-option label="引用" value="quote" />
<el-option label="输入" value="input" />
</el-select>
<el-form-item <el-form-item
v-if="property.type === 'input'" :label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`"
:rules="[ :rules="[
{ {
required: true, required: true,
message: '请输入参数', message: '请输入参数名',
trigger: 'blur', trigger: 'blur',
}, },
]" ]"
:prop="`nodeParams.${index}.input`"
> >
<el-input-number <el-input
v-model="property.input" :disabled="index === 0"
:min="0" v-model="property.name"
:controls="false"
:step-strictly="true"
placeholder="请输入" placeholder="请输入"
clearable clearable
/> />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="property.type === 'quote'" :label="index === 0 ? '参数值' : ''"
:rules="[ :prop="`nodeParams.${index}.type`"
{
required: true,
message: '请选择参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.quote`"
> >
<el-cascader <el-select
:ref=" v-model="property.type"
(el) => { @change="handleTypeChange(index)"
if (el) cascaderRefs[index] = el; >
} <el-option label="引用" value="quote" />
" <el-option label="输入" value="input" />
v-model="property.quote" </el-select>
:checkStrictly="true" <el-form-item
:options="quoteOptions" v-if="property.type === 'input'"
placeholder="请选择" :rules="[
@visible-change=" {
(visible) => visibleChange(visible, index, property.quote) required: true,
" message: '请输入参数值',
@change="(value) => cascaderChange(value, index)" trigger: 'blur',
/> },
]"
:prop="`nodeParams.${index}.input`"
>
<el-input-number
v-model="property.input"
:min="0"
:controls="false"
:step-strictly="true"
placeholder="请输入"
clearable
/>
</el-form-item>
<el-form-item
v-if="property.type === 'quote'"
:rules="[
{
required: true,
message: '请选择参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.quote`"
>
<el-cascader
:ref="
(el) => {
if (el) cascaderRefs[index] = el;
}
"
v-model="property.quote"
:checkStrictly="true"
:options="quoteOptions"
placeholder="请选择"
@visible-change="
(visible) =>
visibleChange(visible, index, property.quote)
"
@change="(value) => cascaderChange(value, index)"
/>
</el-form-item>
</el-form-item> </el-form-item>
</el-form-item> </el-row>
</el-row> </div>
</div> </el-form>
</el-form> </div>
</div>
<div>循环体</div>
<div
class="child__container"
@drop="handleDrop"
@dragover="handleDragover"
>
<slot></slot>
</div> </div>
</div>
<div>循环体</div>
<div class="child__container" @drop="handleDrop" @dragover="handleDragover">
<slot></slot>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>

View File

@ -1,425 +1,437 @@
<template> <template>
<div class="node__container"> <div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus"> <NodeState :state="nodeOperatingStatus">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
<template #output> <template #output>
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" /> <JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
<el-image <el-image
v-if="props.properties.outputType === 'img'" v-if="props.properties.outputType === 'img'"
v-for="item in outputJsonData.imageUrl" v-for="item in outputJsonData.imageUrl"
style="width: 60px; height: 60px" style="width: 60px; height: 60px"
:src="item" :src="item"
:preview-src-list="outputJsonData.imageUrl" :preview-src-list="outputJsonData.imageUrl"
:preview-teleported="true" :preview-teleported="true"
show-progress show-progress
fit="fill" fit="fill"
/> />
<IPlayer v-if="props.properties.outputType === 'video'" v-for="item in outputJsonData.videoUrl" :videoUrl="item" /> <IPlayer v-if="props.properties.outputType === 'video'" v-for="item in outputJsonData.videoUrl" :videoUrl="item" />
</template> </template>
</NodeState> </NodeState>
<NodeTitle <NodeTitle
:icon="props.properties.icon" :icon="props.properties.icon"
:nodeId="props.model.id" :nodeId="props.model.id"
:nodeProperties="props.properties" :nodeProperties="props.properties"
:nodeName="props.properties.name" :nodeName="props.properties.name"
:nodeDesc="props.properties.desc" :nodeDesc="props.properties.desc"
/> :zoom-state="nodeZoom"
<div class="input__container"> @zoom="zoom"
<div class="title"> />
<div class="left"> <div class="input__container" v-show="nodeZoom">
<div class="tag"></div> <div class="title">
<div class="text">输入</div> <div class="left">
</div> <div class="tag"></div>
<div class="right" v-if="properties.properties?.canAddFormItem"> <div class="text">输入</div>
<el-button </div>
:disabled="flowStore.disableForm" <div class="right" v-if="properties.properties?.canAddFormItem">
@click="addFormItem" <el-button
class="addFormItem" :disabled="flowStore.disableForm"
> @click="addFormItem"
<el-icon :size="20"><Plus /></el-icon> class="addFormItem"
</el-button> >
</div> <el-icon :size="20"><Plus /></el-icon>
</div> </el-button>
<div class="form__container"> </div>
<el-form </div>
:inline="true" <div class="form__container" v-show="nodeZoom">
:model="formData" <el-form
:rules="rules" :inline="true"
ref="dynamicForm" :model="formData"
label-position="top" :rules="rules"
label-width="auto" ref="dynamicForm"
:disabled="flowStore.disableForm" label-position="top"
> label-width="auto"
<div v-for="(property, index) in formData.nodeParams" :key="index"> :disabled="flowStore.disableForm"
<el-row> >
<el-form-item <div v-for="(property, index) in formData.nodeParams" :key="index">
:label="index === 0 ? '参数名' : ''" <el-row>
:prop="`nodeParams.${index}.name`" <el-form-item
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]" :label="index === 0 ? '参数名' : ''"
> :prop="`nodeParams.${index}.name`"
<el-input :rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]"
:disabled="property?.disabled || false" >
v-model="property.name" <el-input
placeholder="请输入" :disabled="property?.disabled || false"
clearable v-model="property.name"
/> placeholder="请输入"
</el-form-item> clearable
<el-form-item />
:label="index === 0 ? '参数值' : ''" </el-form-item>
:prop="`nodeParams.${index}.type`" <el-form-item
> :label="index === 0 ? '参数值' : ''"
<el-select v-model="property.type" @change="handleTypeChange(index)"> :prop="`nodeParams.${index}.type`"
<el-option label="引用" value="quote" /> >
<el-option label="输入" value="input" /> <el-select v-model="property.type" @change="handleTypeChange(index)">
</el-select> <el-option label="引用" value="quote" />
<el-form-item <el-option label="输入" value="input" />
v-if="property.type === 'input'" </el-select>
:rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]" <el-form-item
:prop="`nodeParams.${index}.input`" v-if="property.type === 'input'"
> :rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]"
<el-input-number v-if="property.componentType === 'number'" v-model="property.input" :min="0" :controls="false" :step-strictly="true" placeholder="请输入" clearable /> :prop="`nodeParams.${index}.input`"
<el-select v-model="property.input" v-else-if="property.componentType === 'select'" > >
<el-option v-for="item in property.selectOptions" :key="item" :label="item" :value="item" /> <el-input-number v-if="property.componentType === 'number'" v-model="property.input" :min="0" :controls="false" :step-strictly="true" placeholder="请输入" clearable />
</el-select> <el-select v-model="property.input" v-else-if="property.componentType === 'select'" >
<el-input v-else v-model="property.input" placeholder="请输入" clearable /> <el-option v-for="item in property.selectOptions" :key="item" :label="item" :value="item" />
</el-form-item> </el-select>
<el-form-item <el-input v-else v-model="property.input" placeholder="请输入" clearable />
v-if="property.type === 'quote'" </el-form-item>
:rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]" <el-form-item
:prop="`nodeParams.${index}.quote`" v-if="property.type === 'quote'"
> :rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]"
<el-cascader :prop="`nodeParams.${index}.quote`"
v-model="property.quote" >
:options="quoteOptions" <el-cascader
placeholder="请选择" v-model="property.quote"
@visible-change="visibleChange" :options="quoteOptions"
/> placeholder="请选择"
</el-form-item> @visible-change="visibleChange"
</el-form-item> />
</el-row> </el-form-item>
</div> </el-form-item>
</el-form> </el-row>
</div> </div>
</div> </el-form>
<div class="output__container" v-if="props.properties?.outputParams?.length > 0"> </div>
<div class="title"> </div>
<div class="tag"></div> <div class="output__container" v-if="props.properties?.outputParams?.length > 0">
<div class="text">输出</div> <div class="title">
</div> <div class="tag"></div>
<div class="text">输出</div>
<div class="form__container"> </div>
<el-form
:inline="true" <div class="form__container">
label-position="top" <el-form
label-width="auto" :inline="true"
:disabled="true" label-position="top"
> label-width="auto"
<el-row v-for="(item, index) in props.properties.outputParams"> :disabled="true"
<el-form-item label="参数名" > >
<el-input :value="item.name" /> <el-row v-for="(item, index) in props.properties.outputParams">
</el-form-item> <el-form-item label="参数名" >
<el-form-item label="参数类型" > <el-input :value="item.name" />
<el-input :value="item.type" /> </el-form-item>
</el-form-item> <el-form-item label="参数类型" >
<el-form-item label="描述" > <el-input :value="item.type" />
<el-input :value="item.desc" /> </el-form-item>
</el-form-item> <el-form-item label="描述" >
</el-row> <el-input :value="item.desc" />
</el-form> </el-form-item>
</div> </el-row>
</div> </el-form>
</div> </div>
</template> </div>
</div>
<script setup> </template>
import { ref, reactive, onMounted, onUnmounted } from "vue";
import { getInput } from "@/utils/flow"; <script setup>
import { useFlowStore } from "@/store/modules/flow"; import { ref, reactive, onMounted, onUnmounted, nextTick } from "vue";
import { Plus } from "@element-plus/icons-vue"; import { getInput, initNodeZoom } from "@/utils/flow";
import NodeTitle from "../../components/NodeTitle.vue"; import { useFlowStore } from "@/store/modules/flow";
import NodeState from "../../components/NodeState.vue"; import { Plus } from "@element-plus/icons-vue";
import "vue3-json-viewer/dist/index.css"; import NodeTitle from "../../components/NodeTitle.vue";
import { emitter } from "@/utils/eventBus"; import NodeState from "../../components/NodeState.vue";
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue"; import "vue3-json-viewer/dist/index.css";
import { emitter } from "@/utils/eventBus";
const props = defineProps({ import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
model: Object,
properties: Object, const props = defineProps({
}); model: Object,
properties: Object,
const emits = defineEmits(["contentChange"]); });
const flowStore = useFlowStore(); const emits = defineEmits(["contentChange"]);
const formData = reactive({ const flowStore = useFlowStore();
nodeParams: [],
}); const formData = reactive({
nodeParams: [],
const rules = reactive({}); });
const quoteOptions = ref([]); const rules = reactive({});
const handleTypeChange = (index) => {
if (formData.nodeParams[index].type === "input") { const quoteOptions = ref([]);
formData.nodeParams[index].quote = ""; const handleTypeChange = (index) => {
} else { if (formData.nodeParams[index].type === "input") {
formData.nodeParams[index].input = ""; formData.nodeParams[index].quote = "";
const option = getInput(props.model.id); } else {
if (option) { formData.nodeParams[index].input = "";
quoteOptions.value = option; const option = getInput(props.model.id);
} if (option) {
} quoteOptions.value = option;
}; }
}
const dynamicForm = ref(); };
const setNodeProperties = async () => { const dynamicForm = ref();
try {
const result = await dynamicForm.value.validate(); const setNodeProperties = async () => {
if (result) { try {
const data = toRaw(formData); const result = await dynamicForm.value.validate();
const properties = lf.getProperties(props.model.id); if (result) {
lf.setProperties(props.model.id, { const data = toRaw(formData);
...properties, const properties = lf.getProperties(props.model.id);
...data, lf.setProperties(props.model.id, {
}); ...properties,
emits("contentChange"); ...data,
} zoom: nodeZoom.value
} catch { });
dynamicForm.value.clearValidate(); emits("contentChange");
} }
}; } catch {
dynamicForm.value.clearValidate();
const nodeOperatingStatus = ref("NORMAL"); }
const inputJsonData = ref({}); };
const outputJsonData = ref({});
const nodeOperatingStatus = ref("NORMAL");
const visibleChange = (value) => { const inputJsonData = ref({});
if (value) { const outputJsonData = ref({});
const option = getInput(props.model.id);
if (option) { const visibleChange = (value) => {
quoteOptions.value = option; if (value) {
} const option = getInput(props.model.id);
} if (option) {
}; quoteOptions.value = option;
}
const validateForm = async () => { }
try { };
const result = await dynamicForm.value.validate();
if (result) { const validateForm = async () => {
return { valid: true, message: "验证通过" }; try {
} else { const result = await dynamicForm.value.validate();
return { valid: false, message: "验证失败" }; if (result) {
} return { valid: true, message: "验证通过" };
} catch { } else {
return { valid: false, message: "验证失败" }; return { valid: false, message: "验证失败" };
} }
}; } catch {
return { valid: false, message: "验证失败" };
const addFormItem = () => { }
formData.nodeParams.push({ };
name: "",
type: "", const addFormItem = () => {
desc: "", formData.nodeParams.push({
required: false, name: "",
children: [], type: "",
}); desc: "",
emits("contentChange"); required: false,
}; children: [],
});
watch( emits("contentChange");
() => props.properties, };
() => {
if (props.properties.nodeParams && props.properties.nodeParams.length > 0) { const nodeZoom = ref(props?.properties?.zoom ?? true)
formData.nodeParams = props.properties.nodeParams; const zoom = (flag) => {
const option = getInput(props.model.id); nodeZoom.value = flag
if (option) { initNodeZoom(props.model.id, nodeZoom.value, '.node__box')
quoteOptions.value = option; }
}
} watch(
}, () => props.properties,
{ () => {
immediate: true, if (props.properties.nodeParams && props.properties.nodeParams.length > 0) {
deep: true, formData.nodeParams = props.properties.nodeParams;
} const option = getInput(props.model.id);
); if (option) {
quoteOptions.value = option;
onMounted(() => { }
emitter.on("changeNodeState", (data) => { nextTick(() => {
if (data.nodeId === props.model.id) { initNodeZoom(props.model.id, props?.properties?.zoom ?? true, '.node__box', true)
nodeOperatingStatus.value = data.status; })
let inputData = {}; }
let output = {}; },
try { {
inputData = JSON.parse(data.paramsIn) || {}; immediate: true,
output = JSON.parse(data.paramsOut) || {}; deep: true,
} catch (error) { }
inputData = data.paramsIn || {}; );
output = data.paramsOut || {};
} onMounted(() => {
inputJsonData.value = inputData; emitter.on("changeNodeState", (data) => {
outputJsonData.value = output; if (data.nodeId === props.model.id) {
emits("contentChange"); nodeOperatingStatus.value = data.status;
} let inputData = {};
}); let output = {};
try {
emitter.on("contentChange", (data) => { inputData = JSON.parse(data.paramsIn) || {};
if (data.id === props.model.id) { output = JSON.parse(data.paramsOut) || {};
nodeOperatingStatus.value = "NORMAL"; } catch (error) {
inputJsonData.value = {}; inputData = data.paramsIn || {};
outputJsonData.value = {}; output = data.paramsOut || {};
emits("contentChange"); }
} inputJsonData.value = inputData;
}); outputJsonData.value = output;
}); emits("contentChange");
}
onUnmounted(() => { });
emitter.off("changeNodeState");
emitter.off("contentChange"); emitter.on("contentChange", (data) => {
}); if (data.id === props.model.id) {
nodeOperatingStatus.value = "NORMAL";
defineExpose({ inputJsonData.value = {};
validateForm, outputJsonData.value = {};
setNodeProperties, emits("contentChange");
}); }
</script> });
});
<style lang="scss" scoped>
.node__container { onUnmounted(() => {
width: 100%; emitter.off("changeNodeState");
height: auto; emitter.off("contentChange");
});
.input__container {
width: 100%; defineExpose({
background-color: #fafbfc; validateForm,
padding: 0 16px; setNodeProperties,
border-radius: 8px; });
box-sizing: border-box; </script>
.title { <style lang="scss" scoped>
height: 32px; .node__container {
display: flex; width: 100%;
align-items: center; height: auto;
justify-content: space-between;
.input__container {
.left { width: 100%;
display: flex; background-color: #fafbfc;
align-items: center; padding: 0 16px;
border-radius: 8px;
.tag { box-sizing: border-box;
width: 3px;
height: 16px; .title {
background: #1664ff; height: 32px;
border-radius: 0 4px 4px 0; display: flex;
margin-right: 12px; align-items: center;
} justify-content: space-between;
.text { .left {
font-size: 16px; display: flex;
font-weight: 700; align-items: center;
color: #0c0d0e;
} .tag {
} width: 3px;
height: 16px;
.right { background: #1664ff;
.addFormItem { border-radius: 0 4px 4px 0;
border: none; margin-right: 12px;
background: transparent; }
cursor: pointer;
} .text {
} font-size: 16px;
} font-weight: 700;
color: #0c0d0e;
.form__container { }
margin: 12px 0; }
.sub-properties { .right {
margin-left: 10px; .addFormItem {
.zw { border: none;
margin-left: 15px; background: transparent;
&::before { cursor: pointer;
content: ""; }
position: absolute; }
left: 0; }
top: -4px;
width: 10px; .form__container {
border-left: 1px solid gray; margin: 12px 0;
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px; .sub-properties {
background-color: transparent; margin-left: 10px;
height: 24px; .zw {
} margin-left: 15px;
} &::before {
} content: "";
position: absolute;
.gSon-properties { left: 0;
margin-left: 10px; top: -4px;
.zw { width: 10px;
margin-left: 15px; border-left: 1px solid gray;
&::before { border-bottom: 1px solid gray;
content: ""; border-bottom-left-radius: 4px;
position: absolute; background-color: transparent;
left: 0; height: 24px;
top: -4px; }
width: 10px; }
border-left: 1px solid gray; }
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px; .gSon-properties {
background-color: transparent; margin-left: 10px;
height: 24px; .zw {
} margin-left: 15px;
} &::before {
} content: "";
} position: absolute;
} left: 0;
top: -4px;
.output__container { width: 10px;
width: 100%; border-left: 1px solid gray;
background-color: #fafbfc; border-bottom: 1px solid gray;
padding: 0 16px; border-bottom-left-radius: 4px;
border-radius: 8px; background-color: transparent;
box-sizing: border-box; height: 24px;
}
.title { }
height: 32px; }
display: flex; }
align-items: center; }
.tag { .output__container {
width: 3px; width: 100%;
height: 16px; background-color: #fafbfc;
background: #1664ff; padding: 0 16px;
border-radius: 0 4px 4px 0; border-radius: 8px;
margin-right: 12px; box-sizing: border-box;
}
.title {
.text { height: 32px;
font-size: 16px; display: flex;
font-weight: 700; align-items: center;
color: #0c0d0e;
} .tag {
} width: 3px;
height: 16px;
.form__container { background: #1664ff;
margin: 12px 0; border-radius: 0 4px 4px 0;
} margin-right: 12px;
} }
:deep(.el-row) { .text {
align-items: end; font-size: 16px;
font-weight: 700;
.el-input { color: #0c0d0e;
--el-input-width: 170px; }
} }
.el-select { .form__container {
--el-select-width: 170px; margin: 12px 0;
} }
}
.el-cascader {
--el-form-inline-content-width: 170px; :deep(.el-row) {
} align-items: end;
}
} .el-input {
</style> --el-input-width: 170px;
}
.el-select {
--el-select-width: 170px;
}
.el-cascader {
--el-form-inline-content-width: 170px;
}
}
}
</style>

View File

@ -1,440 +1,558 @@
<template> <template>
<div class="node__container" ref="switchRef" @mouseleave="setNodeProperties"> <div class="node__container" ref="switchRef" @mouseleave="setNodeProperties">
<NodeState :state="nodeOperatingStatus"> <NodeState :state="nodeOperatingStatus">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
<template #output> <template #output>
<JsonViewer :value="outputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="outputJsonData" copyable boxed sort theme="light" />
</template> </template>
</NodeState> </NodeState>
<NodeTitle :icon="switchSvg" :nodeId="props.model.id" :nodeProperties="props.properties" :nodeName="props.properties?.name || '分支'" nodeDesc="连接多个下游分支,根据设定的条件按照顺序查找的方式来匹配运行的分支,如果匹配到某条件则只运行该条件对应的分支,否则继续匹配下一条件直至结束" /> <NodeTitle
<div class="condition_title_container"> :icon="switchSvg"
<div class="left"> :nodeId="props.model.id"
<div class="space"></div> :nodeProperties="props.properties"
<div class="title">所有条件</div> :nodeName="props.properties?.name || '分支'"
</div> nodeDesc="连接多个下游分支,根据设定的条件按照顺序查找的方式来匹配运行的分支,如果匹配到某条件则只运行该条件对应的分支,否则继续匹配下一条件直至结束"
<div class="right"> />
<el-button :disabled="flowStore.disableForm" @click="addFormItem" class="addFormItem"> <div class="condition_title_container">
<el-icon :size="20" ><Plus /></el-icon> <div class="left">
</el-button> <div class="space"></div>
</div> <div class="title">所有条件</div>
</div> </div>
<div> <div class="right">
<el-form :inline="true" :model="formData" :rules="rules" ref="dynamicForm" label-position="top" label-width="auto" :disabled="flowStore.disableForm"> <el-button
<div class="condition" v-for="(params, index) in formData.nodeParams" :id="params.id"> :disabled="flowStore.disableForm"
<div class="title__container"> @click="addFormItem"
<div class="left"> class="addFormItem"
<div class="space"></div> >
<div class="title">{{ index ===0 ? 'If' : 'Else If' }}</div> <el-icon :size="20"><Plus /></el-icon>
</div> </el-button>
<div class="right"> </div>
<el-button :disabled="flowStore.disableForm" circle size="small" @click="deleteFormItem(params.id, index)"> </div>
<el-icon :size="16" ><Minus /></el-icon> <div>
</el-button> <el-form
</div> :inline="true"
</div> :model="formData"
<div class="withOr"> :rules="rules"
<div class="title">条件</div> ref="dynamicForm"
<div> label-position="top"
<el-select v-model="params.withOr" placeholder="Select" style="width: 240px"> label-width="auto"
<el-option key="and" label="AND" value="and" /> :disabled="flowStore.disableForm"
<el-option key="or" label="OR" value="or" /> >
</el-select> <div
</div> class="condition"
</div> v-for="(params, index) in formData.nodeParams"
<div class="form__container"> :id="params.id"
<el-row v-for="(property, pIndex) in params.list"> >
<!-- :prop="`nodeParams.${index}.list.${pIndex}.name`" :rules="[{ required: true, message: '请输入变量名', trigger: 'blur' }]" --> <div class="title__container">
<el-form-item :label="pIndex === 0 ? '引用变量' : ''" :prop="`nodeParams.${index}.list.${pIndex}.nameType`"> <div class="left">
<!-- <el-input v-model="property.name" placeholder="请输入" clearable /> --> <div class="space"></div>
<el-select v-model="property.nameType" @change="handleTypeChange(index, pIndex)"> <div class="title">{{ index === 0 ? "If" : "Else If" }}</div>
<el-option label="引用" value="quote" /> </div>
<el-option label="输入" value="input" /> <div class="right">
</el-select> <el-button
<el-form-item v-if="property.nameType === 'input'" :rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.name`"> :disabled="flowStore.disableForm"
<el-input v-model="property.name" placeholder="请输入" clearable /> circle
</el-form-item> size="small"
<el-form-item v-if="property.nameType === 'quote'" :rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.nameQuote`"> @click="deleteFormItem(params.id, index)"
<el-cascader v-model="property.nameQuote" :options="quoteOptions" placeholder="请选择" @visible-change="visibleChange" /> >
</el-form-item> <el-icon :size="16"><Minus /></el-icon>
</el-form-item> </el-button>
<el-form-item :label="pIndex === 0 ? '选择条件' : ''" :prop="`nodeParams.${index}.list.${pIndex}.condition`" :rules="[{ required: true, message: '请输入变量名', trigger: 'blur' }]"> </div>
<el-select v-model="property.condition"> </div>
<el-option label="等于" value="equal" /> <div class="withOr">
<el-option label="不等于" value="notEqualTo" /> <div class="title">条件</div>
<el-option label="为空" value="null" /> <div>
<el-option label="不为空" value="notNull" /> <el-select
<el-option label="包含" value="include" /> v-model="params.withOr"
<el-option label="不包含" value="notInclude" /> placeholder="Select"
<el-option label="大于" value="greaterThan" /> style="width: 240px"
<el-option label="大于等于" value="greaterThanOrEqual" /> >
<el-option label="小于" value="lessThan" /> <el-option key="and" label="AND" value="and" />
<el-option label="小于等于" value="lessThanOrEqual" /> <el-option key="or" label="OR" value="or" />
</el-select> </el-select>
</el-form-item> </div>
<el-form-item :label="pIndex === 0 ? '比较值' : ''" :prop="`nodeParams.${index}.list.${pIndex}.type`" > </div>
<el-select v-model="property.type" @change="handleTypeChange(index, pIndex)"> <div class="form__container">
<el-option label="引用" value="quote" /> <el-row v-for="(property, pIndex) in params.list">
<el-option label="输入" value="input" /> <!-- :prop="`nodeParams.${index}.list.${pIndex}.name`" :rules="[{ required: true, message: '请输入变量名', trigger: 'blur' }]" -->
</el-select> <el-form-item
<el-form-item v-if="property.type === 'input'" :rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.input`"> :label="pIndex === 0 ? '引用变量' : ''"
<el-input v-model="property.input" placeholder="请输入" clearable /> :prop="`nodeParams.${index}.list.${pIndex}.nameType`"
</el-form-item> >
<el-form-item v-if="property.type === 'quote'" :rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.quote`"> <!-- <el-input v-model="property.name" placeholder="请输入" clearable /> -->
<el-cascader v-model="property.quote" :options="quoteOptions" placeholder="请选择" @visible-change="visibleChange" /> <el-select
</el-form-item> v-model="property.nameType"
</el-form-item> @change="handleTypeChange(index, pIndex)"
<el-form-item :label="pIndex === 0 ? '操作' : ''"> >
<el-button :disabled="flowStore.disableForm" circle size="small" @click="addListItem(index)"> <el-option label="引用" value="quote" />
<el-icon :size="12" ><Plus /></el-icon> <el-option label="输入" value="input" />
</el-button> </el-select>
<el-button :disabled="flowStore.disableForm" circle size="small" @click="removeListItem(index, pIndex)"> <el-form-item
<el-icon :size="12" ><Minus /></el-icon> v-if="property.nameType === 'input'"
</el-button> :rules="[
</el-form-item> {
required: true,
</el-row> message: '请输入参数值',
</div> trigger: 'blur',
</div> },
</el-form> ]"
</div> :prop="`nodeParams.${index}.list.${pIndex}.name`"
<div class="else" :id="''.concat(props.model.id, '_else')"> >
<div class="space"></div> <el-input
<div class="title">Else</div> v-model="property.name"
</div> placeholder="请输入"
</div> clearable
</template> />
<script setup> </el-form-item>
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue' <el-form-item
import NodeTitle from '../../components/NodeTitle.vue' v-if="property.nameType === 'quote'"
import NodeState from "../../components/NodeState.vue"; :rules="[
import switchSvg from '../../icon/switch.svg' {
import { v4 as randomUUID } from 'uuid' required: true,
import { useFlowStore } from '@/store/modules/flow' message: '请选择参数值',
import { Plus, Minus } from '@element-plus/icons-vue' trigger: 'blur',
import { getInput, addNewEdge, removeSwitchEdge } from '@/utils/flow' },
import { emitter } from "@/utils/eventBus"; ]"
:prop="`nodeParams.${index}.list.${pIndex}.nameQuote`"
const props = defineProps({ >
model: Object, <el-cascader
graphModel: Object, v-model="property.nameQuote"
properties: Object, :options="quoteOptions"
nowTime: Number, placeholder="请选择"
}); @visible-change="visibleChange"
/>
const emits = defineEmits(['addAnchor', 'removeAnchor', 'bindRef', 'changeAnchor']) </el-form-item>
</el-form-item>
const switchRef = ref() <el-form-item
const dynamicForm = ref() :label="pIndex === 0 ? '选择条件' : ''"
const flowStore = useFlowStore() :prop="`nodeParams.${index}.list.${pIndex}.condition`"
:rules="[
const formData = reactive({ { required: true, message: '请输入变量名', trigger: 'blur' },
nodeParams: props.properties?.conditions || [] ]"
}) >
<el-select v-model="property.condition">
const addFormItem = () => { <el-option label="等于" value="equal" />
const rect = switchRef.value.getBoundingClientRect(); <el-option label="不等于" value="notEqualTo" />
addCondition(rect.height - 60) <el-option label="为空" value="null" />
} <el-option label="不为空" value="notNull" />
<el-option label="包含" value="include" />
const deleteFormItem = (id, index) => { <el-option label="不包含" value="notInclude" />
if (index === 0) { <el-option label="大于" value="greaterThan" />
return <el-option label="大于等于" value="greaterThanOrEqual" />
} <el-option label="小于" value="lessThan" />
formData.nodeParams.splice(index, 1) <el-option label="小于等于" value="lessThanOrEqual" />
emits('removeAnchor', id) </el-select>
removeSwitchEdge(props.model.id, id) </el-form-item>
nextTick(() => { <el-form-item
changeFormItemHeight() :label="pIndex === 0 ? '比较值' : ''"
}) :prop="`nodeParams.${index}.list.${pIndex}.type`"
} >
<el-select
const quoteOptions = ref([]) v-model="property.type"
const handleTypeChange = (pIndex, index) => { @change="handleTypeChange(index, pIndex)"
if (formData.nodeParams[pIndex].list[index].type === 'input') { >
formData.nodeParams[pIndex].list[index].quote = "" <el-option label="引用" value="quote" />
} else { <el-option label="输入" value="input" />
formData.nodeParams[pIndex].list[index].input = "" </el-select>
const option = getInput(props.model.id, false) <el-form-item
if (option) { v-if="property.type === 'input'"
quoteOptions.value = option :rules="[
} {
} required: true,
} message: '请输入参数值',
trigger: 'blur',
const addCondition = (height) => { },
const id = randomUUID().replace(/-/g, '') ]"
formData.nodeParams.push({ :prop="`nodeParams.${index}.list.${pIndex}.input`"
id, >
withOr: 'and', <el-input
list: [{ v-model="property.input"
name: '', placeholder="请输入"
nameQuote: '', clearable
nameType: 'input', />
type: 'input', </el-form-item>
input: '', <el-form-item
quote: '' v-if="property.type === 'quote'"
}] :rules="[
}) {
emits('addAnchor', height, id) required: true,
message: '请选择参数值',
setTimeout(() => { trigger: 'blur',
addNewEdge(props.model.id) },
}, 50) ]"
:prop="`nodeParams.${index}.list.${pIndex}.quote`"
nextTick(() => { >
changeFormItemHeight() <el-cascader
}) v-model="property.quote"
} :options="quoteOptions"
placeholder="请选择"
const addListItem = (pIndex, cIndex) => { @visible-change="visibleChange"
formData.nodeParams[pIndex].list.push({ />
name: '', </el-form-item>
nameQuote: '', </el-form-item>
nameType: 'input', <el-form-item :label="pIndex === 0 ? '操作' : ''">
type: 'input', <el-button
input: '', :disabled="flowStore.disableForm"
quote: '' circle
}) size="small"
nextTick(() => { @click="addListItem(index)"
changeFormItemHeight() >
}) <el-icon :size="12"><Plus /></el-icon>
} </el-button>
<el-button
const removeListItem = (pIndex, cIndex) => { :disabled="flowStore.disableForm"
if (cIndex === 0) { circle
return size="small"
} @click="removeListItem(index, pIndex)"
formData.nodeParams[pIndex].list.splice(cIndex, 1) >
nextTick(() => { <el-icon :size="12"><Minus /></el-icon>
changeFormItemHeight() </el-button>
}) </el-form-item>
} </el-row>
</div>
const changeFormItemHeight = () => { </div>
formData.nodeParams.forEach(item => { </el-form>
const node = document.getElementById(item.id) </div>
emits('changeAnchor', node.offsetTop, item.id) <div class="else" :id="''.concat(props.model.id, '_else')">
}); <div class="space"></div>
<div class="title">Else</div>
const node = document.getElementById("".concat(props.model.id, "_else")) </div>
emits('changeAnchor', node.offsetTop, "".concat(props.model.id, "_else")) </div>
} </template>
<script setup>
const setNodeProperties = async () => { import { nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
try { import NodeTitle from "../../components/NodeTitle.vue";
const valid = await dynamicForm.value.validate() import NodeState from "../../components/NodeState.vue";
if (valid) { import switchSvg from "../../icon/switch.svg";
const data = toRaw(formData) import { v4 as randomUUID } from "uuid";
const properties = lf.getProperties(props.model.id) import { useFlowStore } from "@/store/modules/flow";
lf.setProperties(props.model.id, { import { Plus, Minus } from "@element-plus/icons-vue";
...properties, import { getInput, addNewEdge, removeSwitchEdge } from "@/utils/flow";
...data import { emitter } from "@/utils/eventBus";
})
emits('contentChange') const props = defineProps({
} model: Object,
} catch (error) { graphModel: Object,
dynamicForm.value.clearValidate() properties: Object,
} nowTime: Number,
} });
const visibleChange = (value) => { const emits = defineEmits([
if (value) { "addAnchor",
const option = getInput(props.model.id) "removeAnchor",
if (option) { "bindRef",
quoteOptions.value = option "changeAnchor",
} ]);
}
} const switchRef = ref();
const dynamicForm = ref();
const nodeOperatingStatus = ref("NORMAL"); const flowStore = useFlowStore();
const inputJsonData = ref({});
const outputJsonData = ref({}); const formData = reactive({
nodeParams: props.properties?.conditions || [],
watch( });
() => props.properties,
() => { const addFormItem = () => {
if (props.properties.nodeParams && props.properties.nodeParams.length > 0) { const rect = switchRef.value.getBoundingClientRect();
formData.nodeParams = props.properties.nodeParams addCondition(rect.height - 60);
} else { };
if (formData.nodeParams.length === 0) {
addCondition(100) const deleteFormItem = (id, index) => {
} if (index === 0) {
} return;
}, { }
immediate: true, formData.nodeParams.splice(index, 1);
deep: true emits("removeAnchor", id);
} removeSwitchEdge(props.model.id, id);
); nextTick(() => {
changeFormItemHeight();
onMounted(() => { });
emits('addAnchor', 280, "".concat(props.model.id, "_else")) };
emits('bindRef', dynamicForm.value) const quoteOptions = ref([]);
const handleTypeChange = (pIndex, index) => {
emitter.on("changeNodeState", (data) => { if (formData.nodeParams[pIndex].list[index].type === "input") {
if (data.nodeId === props.model.id) { formData.nodeParams[pIndex].list[index].quote = "";
nodeOperatingStatus.value = data.status; } else {
let inputData = {}; formData.nodeParams[pIndex].list[index].input = "";
let output = {}; const option = getInput(props.model.id, false);
try { if (option) {
inputData = JSON.parse(data.paramsIn) || {}; quoteOptions.value = option;
output = JSON.parse(data.paramsOut) || {}; }
} catch (error) { }
inputData = data.paramsIn || {}; };
output = data.paramsOut || {};
} const addCondition = (height) => {
inputJsonData.value = inputData; const id = randomUUID().replace(/-/g, "");
outputJsonData.value = output; formData.nodeParams.push({
id,
emits("contentChange"); withOr: "and",
} list: [
}); {
name: "",
emitter.on("contentChange", (data) => { nameQuote: "",
if (data.id === props.model.id) { nameType: "input",
nodeOperatingStatus.value = "NORMAL"; type: "input",
inputJsonData.value = {}; input: "",
outputJsonData.value = {}; quote: "",
},
emits("contentChange"); ],
} });
}); emits("addAnchor", height, id);
}) setTimeout(() => {
addNewEdge(props.model.id);
onUnmounted(() => { }, 50);
emitter.off("changeNodeState");
emitter.off("contentChange"); nextTick(() => {
}); changeFormItemHeight();
});
};
</script>
<style lang="scss" scoped> const addListItem = (pIndex, cIndex) => {
.node__container { formData.nodeParams[pIndex].list.push({
width: 720px; name: "",
height: auto; nameQuote: "",
background: #fff; nameType: "input",
padding: 12px; type: "input",
cursor: default; input: "",
border-radius: 12px; quote: "",
border: 2px solid white; });
box-shadow: 0 5px 15px 0#00000008; nextTick(() => {
position: relative; changeFormItemHeight();
});
.condition_title_container { };
display: flex;
align-items: center; const removeListItem = (pIndex, cIndex) => {
justify-content: space-between; if (cIndex === 0) {
return;
.left { }
display: flex; formData.nodeParams[pIndex].list.splice(cIndex, 1);
align-items: center; nextTick(() => {
changeFormItemHeight();
.space { });
width: 4px; };
height: 14px;
background: rgb(22, 100, 255); const changeFormItemHeight = () => {
border-radius: 0px 4px 4px 0px; formData.nodeParams.forEach((item) => {
} const node = document.getElementById(item.id);
emits("changeAnchor", node.offsetTop, item.id);
.title { });
margin-left: 8px;
font-size: 14px; const node = document.getElementById("".concat(props.model.id, "_else"));
font-weight: 500; emits("changeAnchor", node.offsetTop, "".concat(props.model.id, "_else"));
} };
}
const setNodeProperties = async () => {
try {
const valid = await dynamicForm.value.validate();
.el-button { if (valid) {
border: none; const data = toRaw(formData);
} const properties = lf.getProperties(props.model.id);
} lf.setProperties(props.model.id, {
...properties,
.condition { ...data,
width: 100%; });
height: auto; emits("contentChange");
margin-bottom: 10px; }
background-color: #fafbfc; } catch (error) {
padding: 8px 16px; dynamicForm.value.clearValidate();
border-radius: 8px; }
box-sizing: border-box; };
.title__container { const visibleChange = (value) => {
display: flex; if (value) {
align-items: center; const option = getInput(props.model.id);
justify-content: space-between; if (option) {
quoteOptions.value = option;
.left { }
display: flex; }
align-items: center; };
.space { const nodeOperatingStatus = ref("NORMAL");
width: 4px; const inputJsonData = ref({});
height: 14px; const outputJsonData = ref({});
background: rgb(22, 100, 255);
border-radius: 0px 4px 4px 0px; watch(
} () => props.properties,
() => {
.title { if (props.properties.nodeParams && props.properties.nodeParams.length > 0) {
margin-left: 8px; formData.nodeParams = props.properties.nodeParams;
font-size: 14px; } else {
font-weight: bold; if (formData.nodeParams.length === 0) {
} addCondition(100);
} }
} }
},
.withOr { {
margin-bottom: 12px; immediate: true,
deep: true,
.title { }
font-size: 14px; );
color: #737a87;
margin-bottom: 4px; onMounted(() => {
} emits("addAnchor", 280, "".concat(props.model.id, "_else"));
}
emits("bindRef", dynamicForm.value);
:deep(.form__container) {
emitter.on("changeNodeState", (data) => {
.el-form-item { if (data.nodeId === props.model.id) {
margin-right: 20px; nodeOperatingStatus.value = data.status;
} let inputData = {};
let output = {};
.el-input { try {
width: 92px; inputData = JSON.parse(data.paramsIn) || {};
} output = JSON.parse(data.paramsOut) || {};
} catch (error) {
.el-select { inputData = data.paramsIn || {};
width: 92px; output = data.paramsOut || {};
} }
inputJsonData.value = inputData;
.el-cascader { outputJsonData.value = output;
width: 92px;
} emits("contentChange");
} }
} });
.else { emitter.on("contentChange", (data) => {
display: flex; if (data.id === props.model.id) {
align-items: center; nodeOperatingStatus.value = "NORMAL";
background-color: #fafbfc; inputJsonData.value = {};
padding: 8px 16px; outputJsonData.value = {};
.space { emits("contentChange");
width: 4px; }
height: 14px; });
background: rgb(22, 100, 255); });
border-radius: 0px 4px 4px 0px;
} onUnmounted(() => {
emitter.off("changeNodeState");
.title { emitter.off("contentChange");
margin-left: 8px; });
font-size: 14px; </script>
font-weight: 500; <style lang="scss" scoped>
font-weight: bold; .node__container {
} width: 720px;
} height: auto;
} background: #fff;
</style> padding: 12px;
cursor: default;
border-radius: 12px;
border: 2px solid white;
box-shadow: 0 5px 15px 0#00000008;
position: relative;
.condition_title_container {
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
align-items: center;
.space {
width: 4px;
height: 14px;
background: rgb(22, 100, 255);
border-radius: 0px 4px 4px 0px;
}
.title {
margin-left: 8px;
font-size: 14px;
font-weight: 500;
}
}
.el-button {
border: none;
}
}
.condition {
width: 100%;
height: auto;
margin-bottom: 10px;
background-color: #fafbfc;
padding: 8px 16px;
border-radius: 8px;
box-sizing: border-box;
.title__container {
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
align-items: center;
.space {
width: 4px;
height: 14px;
background: rgb(22, 100, 255);
border-radius: 0px 4px 4px 0px;
}
.title {
margin-left: 8px;
font-size: 14px;
font-weight: bold;
}
}
}
.withOr {
margin-bottom: 12px;
.title {
font-size: 14px;
color: #737a87;
margin-bottom: 4px;
}
}
:deep(.form__container) {
.el-form-item {
margin-right: 20px;
}
.el-input {
width: 92px;
}
.el-select {
width: 92px;
}
.el-cascader {
width: 92px;
}
}
}
.else {
display: flex;
align-items: center;
background-color: #fafbfc;
padding: 8px 16px;
.space {
width: 4px;
height: 14px;
background: rgb(22, 100, 255);
border-radius: 0px 4px 4px 0px;
}
.title {
margin-left: 8px;
font-size: 14px;
font-weight: 500;
font-weight: bold;
}
}
}
</style>

View File

@ -1,452 +1,461 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch"> <div ref="topContainerRef">
<el-form-item label="菜单名称" prop="menuName"> <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-input <el-form-item label="菜单名称" prop="menuName">
v-model="queryParams.menuName" <el-input
placeholder="请输入菜单名称" v-model="queryParams.menuName"
clearable placeholder="请输入菜单名称"
style="width: 200px" clearable
@keyup.enter="handleQuery" style="width: 200px"
/> @keyup.enter="handleQuery"
</el-form-item> />
<el-form-item label="状态" prop="status"> </el-form-item>
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable style="width: 200px"> <el-form-item label="状态" prop="status">
<el-option <el-select v-model="queryParams.status" placeholder="菜单状态" clearable style="width: 200px">
v-for="dict in sys_normal_disable" <el-option
:key="dict.value" v-for="dict in sys_normal_disable"
:label="dict.label" :key="dict.value"
:value="dict.value" :label="dict.label"
/> :value="dict.value"
</el-select> />
</el-form-item> </el-select>
<el-form-item> </el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-form-item>
<el-button icon="Refresh" @click="resetQuery">重置</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
</el-form-item> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form> </el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-row :gutter="10" class="mb8">
<el-button <el-col :span="1.5">
type="primary" <el-button
plain type="primary"
icon="Plus" plain
@click="handleAdd" icon="Plus"
v-hasPermi="['system:menu:add']" @click="handleAdd"
>新增</el-button> v-hasPermi="['system:menu:add']"
</el-col> >新增</el-button>
<el-col :span="1.5"> </el-col>
<el-button <el-col :span="1.5">
type="info" <el-button
plain type="info"
icon="Sort" plain
@click="toggleExpandAll" icon="Sort"
>展开/折叠</el-button> @click="toggleExpandAll"
</el-col> >展开/折叠</el-button>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> </el-col>
</el-row> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table </div>
v-if="refreshTable"
v-loading="loading" <div :style="containerHeight">
:data="menuList" <el-table
row-key="menuId" v-if="refreshTable"
:default-expand-all="isExpandAll" height="100%"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" v-loading="loading"
> :data="menuList"
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column> row-key="menuId"
<el-table-column prop="icon" label="图标" align="center" width="100"> :default-expand-all="isExpandAll"
<template #default="scope"> :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
<svg-icon :icon-class="scope.row.icon" /> >
</template> <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
</el-table-column> <el-table-column prop="icon" label="图标" align="center" width="100">
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column> <template #default="scope">
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column> <svg-icon :icon-class="scope.row.icon" />
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column> </template>
<el-table-column prop="status" label="状态" width="80"> </el-table-column>
<template #default="scope"> <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
<dict-tag :options="sys_normal_disable" :value="scope.row.status" /> <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
</template> <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
</el-table-column> <el-table-column prop="status" label="状态" width="80">
<el-table-column label="创建时间" align="center" width="160" prop="createTime"> <template #default="scope">
<template #default="scope"> <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
<span>{{ parseTime(scope.row.createTime) }}</span> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="创建时间" align="center" width="160" prop="createTime">
<el-table-column label="操作" align="center" width="210" class-name="small-padding fixed-width"> <template #default="scope">
<template #default="scope"> <span>{{ parseTime(scope.row.createTime) }}</span>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']">修改</el-button> </template>
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']">新增</el-button> </el-table-column>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']">删除</el-button> <el-table-column label="操作" align="center" width="210" class-name="small-padding fixed-width">
</template> <template #default="scope">
</el-table-column> <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']">修改</el-button>
</el-table> <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']">新增</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']">删除</el-button>
<!-- 添加或修改菜单对话框 --> </template>
<el-dialog :title="title" v-model="open" width="680px" append-to-body> </el-table-column>
<el-form ref="menuRef" :model="form" :rules="rules" label-width="100px"> </el-table>
<el-row> </div>
<el-col :span="24">
<el-form-item label="上级菜单"> <!-- 添加或修改菜单对话框 -->
<el-tree-select <el-dialog :title="title" v-model="open" width="680px" append-to-body>
v-model="form.parentId" <el-form ref="menuRef" :model="form" :rules="rules" label-width="100px">
:data="menuOptions" <el-row>
:props="{ value: 'menuId', label: 'menuName', children: 'children' }" <el-col :span="24">
value-key="menuId" <el-form-item label="上级菜单">
placeholder="选择上级菜单" <el-tree-select
check-strictly v-model="form.parentId"
/> :data="menuOptions"
</el-form-item> :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
</el-col> value-key="menuId"
<el-col :span="24"> placeholder="选择上级菜单"
<el-form-item label="菜单类型" prop="menuType"> check-strictly
<el-radio-group v-model="form.menuType"> />
<el-radio value="M">目录</el-radio> </el-form-item>
<el-radio value="C">菜单</el-radio> </el-col>
<el-radio value="F">按钮</el-radio> <el-col :span="24">
</el-radio-group> <el-form-item label="菜单类型" prop="menuType">
</el-form-item> <el-radio-group v-model="form.menuType">
</el-col> <el-radio value="M">目录</el-radio>
<el-col :span="12" v-if="form.menuType != 'F'"> <el-radio value="C">菜单</el-radio>
<el-form-item label="菜单图标" prop="icon"> <el-radio value="F">按钮</el-radio>
<el-popover </el-radio-group>
placement="bottom-start" </el-form-item>
:width="540" </el-col>
trigger="click" <el-col :span="12" v-if="form.menuType != 'F'">
> <el-form-item label="菜单图标" prop="icon">
<template #reference> <el-popover
<el-input v-model="form.icon" placeholder="点击选择图标" @blur="showSelectIcon" readonly> placement="bottom-start"
<template #prefix> :width="540"
<svg-icon trigger="click"
v-if="form.icon" >
:icon-class="form.icon" <template #reference>
class="el-input__icon" <el-input v-model="form.icon" placeholder="点击选择图标" @blur="showSelectIcon" readonly>
style="height: 32px;width: 16px;" <template #prefix>
/> <svg-icon
<el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon> v-if="form.icon"
</template> :icon-class="form.icon"
</el-input> class="el-input__icon"
</template> style="height: 32px;width: 16px;"
<icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" /> />
</el-popover> <el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon>
</el-form-item> </template>
</el-col> </el-input>
<el-col :span="12"> </template>
<el-form-item label="显示排序" prop="orderNum"> <icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" /> </el-popover>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="菜单名称" prop="menuName"> <el-form-item label="显示排序" prop="orderNum">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" /> <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" v-if="form.menuType == 'C'"> <el-col :span="12">
<el-form-item prop="routeName"> <el-form-item label="菜单名称" prop="menuName">
<template #label> <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
<span> </el-form-item>
<el-tooltip content="默认不填则和路由地址相同:如地址为:`user`,则名称为`User`注意因为router会删除名称相同路由为避免名字的冲突特殊情况下请自定义保证唯一性" placement="top"> </el-col>
<el-icon><question-filled /></el-icon> <el-col :span="12" v-if="form.menuType == 'C'">
</el-tooltip> <el-form-item prop="routeName">
路由名称 <template #label>
</span> <span>
</template> <el-tooltip content="默认不填则和路由地址相同:如地址为:`user`,则名称为`User`注意因为router会删除名称相同路由为避免名字的冲突特殊情况下请自定义保证唯一性" placement="top">
<el-input v-model="form.routeName" placeholder="请输入路由名称" /> <el-icon><question-filled /></el-icon>
</el-form-item> </el-tooltip>
</el-col> 路由名称
<el-col :span="12" v-if="form.menuType != 'F'"> </span>
<el-form-item> </template>
<template #label> <el-input v-model="form.routeName" placeholder="请输入路由名称" />
<span> </el-form-item>
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top"> </el-col>
<el-icon><question-filled /></el-icon> <el-col :span="12" v-if="form.menuType != 'F'">
</el-tooltip> <el-form-item>
</span> <template #label>
</template> <span>
<el-radio-group v-model="form.isFrame"> <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<el-radio value="0"></el-radio> <el-icon><question-filled /></el-icon>
<el-radio value="1"></el-radio> </el-tooltip>
</el-radio-group> </span>
</el-form-item> </template>
</el-col> <el-radio-group v-model="form.isFrame">
<el-col :span="12" v-if="form.menuType != 'F'"> <el-radio value="0"></el-radio>
<el-form-item prop="path"> <el-radio value="1"></el-radio>
<template #label> </el-radio-group>
<span> </el-form-item>
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top"> </el-col>
<el-icon><question-filled /></el-icon> <el-col :span="12" v-if="form.menuType != 'F'">
</el-tooltip> <el-form-item prop="path">
路由地址 <template #label>
</span> <span>
</template> <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<el-input v-model="form.path" placeholder="请输入路由地址" /> <el-icon><question-filled /></el-icon>
</el-form-item> </el-tooltip>
</el-col> 路由地址
<el-col :span="12" v-if="form.menuType == 'C'"> </span>
<el-form-item prop="component"> </template>
<template #label> <el-input v-model="form.path" placeholder="请输入路由地址" />
<span> </el-form-item>
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top"> </el-col>
<el-icon><question-filled /></el-icon> <el-col :span="12" v-if="form.menuType == 'C'">
</el-tooltip> <el-form-item prop="component">
组件路径 <template #label>
</span> <span>
</template> <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<el-input v-model="form.component" placeholder="请输入组件路径" /> <el-icon><question-filled /></el-icon>
</el-form-item> </el-tooltip>
</el-col> 组件路径
<el-col :span="12" v-if="form.menuType != 'M'"> </span>
<el-form-item> </template>
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" /> <el-input v-model="form.component" placeholder="请输入组件路径" />
<template #label> </el-form-item>
<span> </el-col>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top"> <el-col :span="12" v-if="form.menuType != 'M'">
<el-icon><question-filled /></el-icon> <el-form-item>
</el-tooltip> <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
权限字符 <template #label>
</span> <span>
</template> <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
</el-form-item> <el-icon><question-filled /></el-icon>
</el-col> </el-tooltip>
<el-col :span="12" v-if="form.menuType == 'C'"> 权限字符
<el-form-item> </span>
<el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" /> </template>
<template #label> </el-form-item>
<span> </el-col>
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top"> <el-col :span="12" v-if="form.menuType == 'C'">
<el-icon><question-filled /></el-icon> <el-form-item>
</el-tooltip> <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
路由参数 <template #label>
</span> <span>
</template> <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
</el-form-item> <el-icon><question-filled /></el-icon>
</el-col> </el-tooltip>
<el-col :span="12" v-if="form.menuType == 'C'"> 路由参数
<el-form-item> </span>
<template #label> </template>
<span> </el-form-item>
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top"> </el-col>
<el-icon><question-filled /></el-icon> <el-col :span="12" v-if="form.menuType == 'C'">
</el-tooltip> <el-form-item>
是否缓存 <template #label>
</span> <span>
</template> <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<el-radio-group v-model="form.isCache"> <el-icon><question-filled /></el-icon>
<el-radio value="0">缓存</el-radio> </el-tooltip>
<el-radio value="1">不缓存</el-radio> 是否缓存
</el-radio-group> </span>
</el-form-item> </template>
</el-col> <el-radio-group v-model="form.isCache">
<el-col :span="12" v-if="form.menuType != 'F'"> <el-radio value="0">缓存</el-radio>
<el-form-item> <el-radio value="1">不缓存</el-radio>
<template #label> </el-radio-group>
<span> </el-form-item>
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top"> </el-col>
<el-icon><question-filled /></el-icon> <el-col :span="12" v-if="form.menuType != 'F'">
</el-tooltip> <el-form-item>
显示状态 <template #label>
</span> <span>
</template> <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<el-radio-group v-model="form.visible"> <el-icon><question-filled /></el-icon>
<el-radio </el-tooltip>
v-for="dict in sys_show_hide" 显示状态
:key="dict.value" </span>
:value="dict.value" </template>
>{{ dict.label }}</el-radio> <el-radio-group v-model="form.visible">
</el-radio-group> <el-radio
</el-form-item> v-for="dict in sys_show_hide"
</el-col> :key="dict.value"
<el-col :span="12"> :value="dict.value"
<el-form-item> >{{ dict.label }}</el-radio>
<template #label> </el-radio-group>
<span> </el-form-item>
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top"> </el-col>
<el-icon><question-filled /></el-icon> <el-col :span="12">
</el-tooltip> <el-form-item>
菜单状态 <template #label>
</span> <span>
</template> <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<el-radio-group v-model="form.status"> <el-icon><question-filled /></el-icon>
<el-radio </el-tooltip>
v-for="dict in sys_normal_disable" 菜单状态
:key="dict.value" </span>
:value="dict.value" </template>
>{{ dict.label }}</el-radio> <el-radio-group v-model="form.status">
</el-radio-group> <el-radio
</el-form-item> v-for="dict in sys_normal_disable"
</el-col> :key="dict.value"
</el-row> :value="dict.value"
</el-form> >{{ dict.label }}</el-radio>
<template #footer> </el-radio-group>
<div class="dialog-footer"> </el-form-item>
<el-button type="primary" @click="submitForm"> </el-button> </el-col>
<el-button @click="cancel"> </el-button> </el-row>
</div> </el-form>
</template> <template #footer>
</el-dialog> <div class="dialog-footer">
</div> <el-button type="primary" @click="submitForm"> </el-button>
</template> <el-button @click="cancel"> </el-button>
</div>
<script setup name="Menu"> </template>
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu"; </el-dialog>
import SvgIcon from "@/components/SvgIcon"; </div>
import IconSelect from "@/components/IconSelect"; </template>
const { proxy } = getCurrentInstance(); <script setup name="Menu">
const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable"); import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon";
const menuList = ref([]); import IconSelect from "@/components/IconSelect";
const open = ref(false); import { useContainerHeight } from "@/hooks/tableHeight";
const loading = ref(true);
const showSearch = ref(true); const topContainerRef = ref();
const title = ref(""); const containerHeight = useContainerHeight(topContainerRef);
const menuOptions = ref([]);
const isExpandAll = ref(false); const { proxy } = getCurrentInstance();
const refreshTable = ref(true); const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable");
const iconSelectRef = ref(null);
const menuList = ref([]);
const data = reactive({ const open = ref(false);
form: {}, const loading = ref(true);
queryParams: { const showSearch = ref(true);
menuName: undefined, const title = ref("");
visible: undefined const menuOptions = ref([]);
}, const isExpandAll = ref(false);
rules: { const refreshTable = ref(true);
menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }], const iconSelectRef = ref(null);
orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }] const data = reactive({
}, form: {},
}); queryParams: {
menuName: undefined,
const { queryParams, form, rules } = toRefs(data); visible: undefined
},
/** 查询菜单列表 */ rules: {
function getList() { menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
loading.value = true; orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
listMenu(queryParams.value).then(response => { path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
menuList.value = proxy.handleTree(response.data, "menuId"); },
loading.value = false; });
});
} const { queryParams, form, rules } = toRefs(data);
/** 查询菜单下拉树结构 */ /** 查询菜单列表 */
function getTreeselect() { function getList() {
menuOptions.value = []; loading.value = true;
listMenu().then(response => { listMenu(queryParams.value).then(response => {
const menu = { menuId: 0, menuName: "主类目", children: [] }; menuList.value = proxy.handleTree(response.data, "menuId");
menu.children = proxy.handleTree(response.data, "menuId"); loading.value = false;
menuOptions.value.push(menu); });
}); }
}
/** 查询菜单下拉树结构 */
/** 取消按钮 */ function getTreeselect() {
function cancel() { menuOptions.value = [];
open.value = false; listMenu().then(response => {
reset(); const menu = { menuId: 0, menuName: "主类目", children: [] };
} menu.children = proxy.handleTree(response.data, "menuId");
menuOptions.value.push(menu);
/** 表单重置 */ });
function reset() { }
form.value = {
menuId: undefined, /** 取消按钮 */
parentId: 0, function cancel() {
menuName: undefined, open.value = false;
icon: undefined, reset();
menuType: "M", }
orderNum: undefined,
isFrame: "1", /** 表单重置 */
isCache: "0", function reset() {
visible: "0", form.value = {
status: "0" menuId: undefined,
}; parentId: 0,
proxy.resetForm("menuRef"); menuName: undefined,
} icon: undefined,
menuType: "M",
/** 展示下拉图标 */ orderNum: undefined,
function showSelectIcon() { isFrame: "1",
iconSelectRef.value.reset(); isCache: "0",
} visible: "0",
status: "0"
/** 选择图标 */ };
function selected(name) { proxy.resetForm("menuRef");
form.value.icon = name; }
}
/** 展示下拉图标 */
/** 搜索按钮操作 */ function showSelectIcon() {
function handleQuery() { iconSelectRef.value.reset();
getList(); }
}
/** 选择图标 */
/** 重置按钮操作 */ function selected(name) {
function resetQuery() { form.value.icon = name;
proxy.resetForm("queryRef"); }
handleQuery();
} /** 搜索按钮操作 */
function handleQuery() {
/** 新增按钮操作 */ getList();
function handleAdd(row) { }
reset();
getTreeselect(); /** 重置按钮操作 */
if (row != null && row.menuId) { function resetQuery() {
form.value.parentId = row.menuId; proxy.resetForm("queryRef");
} else { handleQuery();
form.value.parentId = 0; }
}
open.value = true; /** 新增按钮操作 */
title.value = "添加菜单"; function handleAdd(row) {
} reset();
getTreeselect();
/** 展开/折叠操作 */ if (row != null && row.menuId) {
function toggleExpandAll() { form.value.parentId = row.menuId;
refreshTable.value = false; } else {
isExpandAll.value = !isExpandAll.value; form.value.parentId = 0;
nextTick(() => { }
refreshTable.value = true; open.value = true;
}); title.value = "添加菜单";
} }
/** 修改按钮操作 */ /** 展开/折叠操作 */
async function handleUpdate(row) { function toggleExpandAll() {
reset(); refreshTable.value = false;
await getTreeselect(); isExpandAll.value = !isExpandAll.value;
getMenu(row.menuId).then(response => { nextTick(() => {
form.value = response.data; refreshTable.value = true;
open.value = true; });
title.value = "修改菜单"; }
});
} /** 修改按钮操作 */
async function handleUpdate(row) {
/** 提交按钮 */ reset();
function submitForm() { await getTreeselect();
proxy.$refs["menuRef"].validate(valid => { getMenu(row.menuId).then(response => {
if (valid) { form.value = response.data;
if (form.value.menuId != undefined) { open.value = true;
updateMenu(form.value).then(response => { title.value = "修改菜单";
proxy.$modal.msgSuccess("修改成功"); });
open.value = false; }
getList();
}); /** 提交按钮 */
} else { function submitForm() {
addMenu(form.value).then(response => { proxy.$refs["menuRef"].validate(valid => {
proxy.$modal.msgSuccess("新增成功"); if (valid) {
open.value = false; if (form.value.menuId != undefined) {
getList(); updateMenu(form.value).then(response => {
}); proxy.$modal.msgSuccess("修改成功");
} open.value = false;
} getList();
}); });
} } else {
addMenu(form.value).then(response => {
/** 删除按钮操作 */ proxy.$modal.msgSuccess("新增成功");
function handleDelete(row) { open.value = false;
proxy.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() { getList();
return delMenu(row.menuId); });
}).then(() => { }
getList(); }
proxy.$modal.msgSuccess("删除成功"); });
}).catch(() => {}); }
}
/** 删除按钮操作 */
getList(); function handleDelete(row) {
</script> proxy.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
return delMenu(row.menuId);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
getList();
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@
<el-table-column prop="value" label="入参值"> <el-table-column prop="value" label="入参值">
<template #default="{ row, column, $index }"> <template #default="{ row, column, $index }">
<el-form-item v-if="row.type !== 'object' && !row.type.includes('array')" label="" :prop="`formData.tableData.${pIndex}.config.${row.propPath}.value`" :rules="row.required ? [{ required: true, message: '请输入', trigger: 'blur' }] : []"> <el-form-item v-if="row.type !== 'object' && !row.type.includes('array')" label="" :prop="`formData.tableData.${pIndex}.config${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请输入', trigger: 'blur' }] : []">
<el-input v-if="row.type === 'string'" v-model="row.value" /> <el-input v-if="row.type === 'string'" v-model="row.value" />
<el-input-number v-if="row.type === 'number'" :controls="false" v-model="row.value" /> <el-input-number v-if="row.type === 'number'" :controls="false" v-model="row.value" />
<el-switch v-if="row.type === 'boolean'" v-model="row.value" /> <el-switch v-if="row.type === 'boolean'" v-model="row.value" />

View File

@ -142,88 +142,91 @@
<div :style="containerHeight"> <div :style="containerHeight">
<el-table <el-table
v-loading="loading" v-loading="loading"
height="100%" height="100%"
:data="projectList" :data="projectList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="项目ID" align="center" prop="projectId" width="150" show-overflow-tooltip />
<el-table-column label="项目名称" align="center" prop="projectName" width="150" show-overflow-tooltip />
<el-table-column label="测试人员" align="center" prop="tester" />
<el-table-column
label="计划测试开始时间"
align="center"
prop="planStart"
width="180"
> >
<template #default="{ row }"> <el-table-column type="selection" width="55" align="center" />
<span>{{ parseTime(row.planStart) }}</span> <el-table-column label="项目ID" align="center" prop="projectId" width="150" show-overflow-tooltip />
</template> <el-table-column label="项目名称" align="center" prop="projectName" width="150" show-overflow-tooltip />
</el-table-column> <el-table-column label="测试人员" align="center" prop="tester" />
<el-table-column label="计划测试结束时间" align="center" prop="planEnd" width="180"> <el-table-column
<template #default="{ row }"> label="计划测试开始时间"
<span>{{ parseTime(row.planEnd) }}</span> align="center"
</template> prop="planStart"
</el-table-column> width="180"
<el-table-column label="项目描述" width="200" show-overflow-tooltip align="center" prop="description" /> >
<el-table-column label="样品名称" align="center" prop="sampleName" width="150" show-overflow-tooltip /> <template #default="{ row }">
<el-table-column label="商标" align="center" prop="trademark" width="150" show-overflow-tooltip /> <span>{{ parseTime(row.planStart) }}</span>
<el-table-column label="型号规格" align="center" prop="modelSpec" width="150" show-overflow-tooltip /> </template>
<el-table-column label="数量" align="center" prop="quantity" /> </el-table-column>
<el-table-column label="项目状态" align="center" prop="status"> <el-table-column label="计划测试结束时间" align="center" prop="planEnd" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ row.status == 0 ? '已执行': '未执行' }} <span>{{ parseTime(row.planEnd) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" width="300px" class-name="small-padding fixed-width" fixed="right"> <el-table-column label="项目描述" width="200" show-overflow-tooltip align="center" prop="description" />
<template #default="{ row }"> <el-table-column label="样品名称" align="center" prop="sampleName" width="150" show-overflow-tooltip />
<el-button <el-table-column label="商标" align="center" prop="trademark" width="150" show-overflow-tooltip />
size="mini" <el-table-column label="型号规格" align="center" prop="modelSpec" width="150" show-overflow-tooltip />
type="text" <el-table-column label="数量" align="center" prop="quantity" />
@click="handleSetting(row)" <el-table-column label="项目状态" align="center" prop="status">
>配置 <template #default="{ row }">
</el-button> {{ row.status == 0 ? '已执行': '未执行' }}
<el-button </template>
size="mini" </el-table-column>
type="text" <el-table-column label="操作" align="center" width="300px" class-name="small-padding fixed-width" fixed="right">
v-if="row.status == 1" <template #default="{ row }">
@click="handleExecute(row)" <el-button
>执行 size="mini"
</el-button> type="text"
<el-button @click="handleSetting(row)"
icon="DataLine" v-hasPermi="['system:project:add','system:project:edit']"
v-if="row.status == 0" >配置
link </el-button>
type="primary" <el-button
@click="handleOpenDashboard(row)" size="mini"
>可视化 type="text"
</el-button> v-if="row.status == 1"
<el-button @click="handleExecute(row)"
size="mini" v-hasPermi="['system:project:add','system:project:edit']"
type="text" >执行
@click="handleUpdate(row)" </el-button>
v-hasPermi="['system:project:edit']" <el-button
>修改</el-button icon="DataLine"
> v-if="row.status == 0"
<el-button link
size="mini" type="primary"
type="text" v-hasPermi="['system:project:add','system:project:edit']"
@click="handleDelete(row)" @click="handleOpenDashboard(row)"
v-hasPermi="['system:project:remove']" >可视化
>删除</el-button </el-button>
> <el-button
</template> size="mini"
</el-table-column> type="text"
</el-table> @click="handleUpdate(row)"
v-hasPermi="['system:project:edit']"
>修改</el-button
>
<el-button
size="mini"
type="text"
@click="handleDelete(row)"
v-hasPermi="['system:project:remove']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<pagination <pagination
v-show="total > 0" v-show="total > 0"
:total="total" :total="total"
:page.sync="queryParams.pageNum" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" :limit.sync="queryParams.pageSize"
@pagination="getList" @pagination="getList"
/> />
</div> </div>
<!-- 添加或修改语料库对话框 --> <!-- 添加或修改语料库对话框 -->

View File

@ -35,8 +35,8 @@ export default defineConfig(({mode, command}) => {
//杨 http://192.168.0.10:13080 //杨 http://192.168.0.10:13080
//赵 http://10.148.108.58:13080 //赵 http://10.148.108.58:13080
// dev http://10.148.20.34:13080 // dev http://10.148.20.34:13080
// target: command === 'build' ? VITE_API_URL : 'http://10.148.20.34:13080', target: VITE_API_URL,
target: command === 'build' ? VITE_API_URL : 'http://192.168.0.10:13080', // target: command === 'build' ? VITE_API_URL : 'http://192.168.0.10:13080',
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '') rewrite: (p) => p.replace(/^\/dev-api/, '')
} }