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'
// 发布流程
export function flowDeploy(data) {
return request({
url: '/flow/publish',
method: 'post',
data: data
})
}
export function flowExecute(data) {
return request({
url: '/flow/execute',
method: 'post',
data: data
})
}
export function flowExecuteTrial(data) {
return request({
url: '/flow/executeTrial',
method: 'post',
data: data
})
}
export function flowView(data) {
return request({
url: '/flow/view',
method: 'post',
data: data
})
}
export function flowPause(instId) {
return request({
url: `/flow/pause/${instId}`,
method: 'post'
})
}
export function flowResume(instId) {
return request({
url: `/flow/resume/${instId}`,
method: 'post'
})
}
export function flowStop(instId) {
return request({
url: `/flow/stop/${instId}`,
method: 'post'
})
import request from '@/utils/request'
// 发布流程
export function flowDeploy(data) {
return request({
url: '/flow/publish',
method: 'post',
data: data
})
}
export function flowExecute(data) {
return request({
url: '/flow/execute',
method: 'post',
data: data
})
}
export function flowAction(data) {
return request({
url: '/flow/action',
method: 'post',
data: data
})
}
export function flowExecuteTrial(data) {
return request({
url: '/flow/executeTrial',
method: 'post',
data: data
})
}
export function flowView(data) {
return request({
url: '/flow/view',
method: 'post',
data: data
})
}
export function flowPause(instId) {
return request({
url: `/flow/pause/${instId}`,
method: 'post'
})
}
export function flowResume(instId) {
return request({
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";
/* Layout */
import Layout from "@/layout";
import path from "path";
/**
* Note: 路由配置项
@ -92,7 +93,14 @@ export const constantRoutes = [
name: "灵巧手示教",
meta: { title: "灵巧手详情" },
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) => {
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 arr = JSON.parse(JSON.stringify(edges))
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) => {
const data = lf.getGraphData();
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}`; //
router.push({
path: fullPath,
query: {
terminalId: row.idDeDeviceTerminalConfig,
deviceId: row.deviceCode
}
}); // 使
// intoControlPage(`/${row.deviceModel}`, `${row.id}`)
}

View File

@ -1,171 +1,239 @@
<template>
<div class="title__container">
<div class="title">
<div class="left">
<img :src="icon" alt="">
<div class="text__container">
<div class="text__title" v-if="showTextTitle">
<span class="text">{{ nodeName }}</span>
<el-icon class="editIcon" @click="showTextTitle = false"><EditPen /></el-icon>
</div>
<div class="input_name_container" v-else>
<el-input v-model="nodeName" />
<el-button class="input_name_select" circle size="small" @click="confirmNodeName">
<el-icon><Select /></el-icon>
</el-button>
<el-button class="input_name_closeBold" circle size="small" @click="cancelNodeName">
<el-icon><CloseBold /></el-icon>
</el-button>
</div>
</div>
</div>
<div class="right">
<el-button circle size="small" @click="deleteNode">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
<div class="subTitle">{{ props.nodeDesc }}</div>
</div>
</template>
<script setup lang="js">
import { ref } from 'vue'
import { EditPen, Select, CloseBold, Delete } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
const props = defineProps({
icon: {
type: String,
default: ''
},
nodeId: {
type: String,
default: ''
},
nodeProperties: {
type: Object,
default: () => {}
},
nodeDesc: {
type: String,
default: ''
},
nodeName: {
type: String,
default: ''
},
nodeType: {
type: String,
default: ''
}
})
const showTextTitle = ref(true)
const nodeName = ref(props.nodeProperties.name || props.nodeName)
const confirmNodeName = () => {
const properties = lf.getProperties(props.nodeId)
lf.setProperties(props.nodeId, {
...properties,
name: nodeName.value
})
showTextTitle.value = true
}
const cancelNodeName = () => {
nodeName.value = props.nodeProperties?.name || props.nodeName
showTextTitle.value = true
}
const deleteNode = () => {
ElMessageBox.confirm(
'是否删除该节点?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
if (props.nodeType === 'selectArea') {
const children = lf.getNodeModelById(props.nodeId).children
lf.getNodeModelById(props.nodeId).children = []
children.forEach(item => {
lf.getNodeModelById(item).draggable = true
})
}
lf.deleteNode(props.nodeId);
})
}
</script>
<style lang="scss" scoped>
.title__container {
.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;
}
}
}
.subTitle {
color: #737a87;
font-size: 12px;
margin: 8px 0;
}
}
</style>
<template>
<div class="title__container">
<div class="title">
<div class="left">
<img :src="icon" alt="" />
<div class="text__container">
<div class="text__title" v-if="showTextTitle">
<span class="text">{{ nodeName }}</span>
<el-icon class="editIcon" @click="showTextTitle = false"
><EditPen
/></el-icon>
</div>
<div class="input_name_container" v-else>
<el-input v-model="nodeName" />
<el-button
class="input_name_select"
circle
size="small"
@click="confirmNodeName"
>
<el-icon><Select /></el-icon>
</el-button>
<el-button
class="input_name_closeBold"
circle
size="small"
@click="cancelNodeName"
>
<el-icon><CloseBold /></el-icon>
</el-button>
</div>
</div>
</div>
<div class="right">
<el-tooltip
class="box-item"
effect="dark"
content="执行"
placement="top"
>
<el-button v-if="nodeType && nodeType !=='NONE'" circle @click="execute">
<el-icon>
<VideoPlay />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
:content="zoomState ? '缩小' : '放大'"
placement="top"
>
<el-button v-if="showZoom" circle @click="zoom">
<el-icon>
<ZoomOut v-if="zoomState" />
<ZoomIn v-else />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="删除"
placement="top"
>
<el-button circle @click="deleteNode">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
<div class="subTitle">{{ props.nodeDesc }}</div>
</div>
</template>
<script setup lang="js">
import { ref } from 'vue'
import { EditPen, Select, CloseBold, Delete, ZoomOut, ZoomIn, VideoPlay } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
const props = defineProps({
icon: {
type: String,
default: ''
},
nodeId: {
type: String,
default: ''
},
nodeProperties: {
type: Object,
default: () => {}
},
nodeDesc: {
type: String,
default: ''
},
nodeName: {
type: String,
default: ''
},
nodeType: {
type: String,
default: ''
},
showZoom: {
type: Boolean,
default: true
},
zoomState: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['zoom'])
const showTextTitle = ref(true)
const nodeName = ref(props.nodeProperties.name || props.nodeName)
const confirmNodeName = () => {
const properties = lf.getProperties(props.nodeId)
lf.setProperties(props.nodeId, {
...properties,
name: nodeName.value
})
showTextTitle.value = true
}
const cancelNodeName = () => {
nodeName.value = props.nodeProperties?.name || props.nodeName
showTextTitle.value = true
}
const deleteNode = () => {
ElMessageBox.confirm(
'是否删除该节点?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
if (props.nodeType === 'selectArea') {
const children = lf.getNodeModelById(props.nodeId).children
lf.getNodeModelById(props.nodeId).children = []
children.forEach(item => {
lf.getNodeModelById(item).draggable = true
})
}
lf.deleteNode(props.nodeId);
})
}
const zoom = () => {
emits('zoom', !props.zoomState)
}
const execute = () => {
const myEvent = new CustomEvent('singleNodeExecution', {
bubbles: false, // false
cancelable: false, // preventDefault() false
detail: { ...props.nodeProperties } // event.detail
});
document.dispatchEvent(myEvent);
}
</script>
<style lang="scss" scoped>
.title__container {
.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 dialogueSvg from './icon/dialogue.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'
@ -40,6 +42,11 @@ export const lfConfig = {
// foldSize: 30, // 折叠后显示的图标尺寸
// },
edgeType: "bezier",
zoomConfig: {
min: 0.05, // 最小缩放比例(支持更小值,如 0.01,但不建议过小)
max: 3, // 最大缩放比例(可自定义)
step: 0.1 // 每次滚轮缩放的步长(默认 0.1
},
style: {
anchor: {
show: true, // 强制全局锚点显示
@ -73,11 +80,39 @@ export const registerCustomizeNode = (lf) => {
};
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 = () => {
return ['spk1']
return [{
value: 'spk1',
label: 'spk1'
}]
}
const expressOptions = () => {
return [{
value: 1,
label: '高兴'
}, {
value: 2,
label: '惊讶'
}, {
value: 3,
label: '疲惫'
}]
}
export const collapseList = [
@ -90,6 +125,7 @@ export const collapseList = [
type: "serviceNode",
desc: "在获取相机照片时,需要先启动相机",
action: 'CAMERA_START',
nodeType: 'EDGE',
nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
],
@ -101,6 +137,7 @@ export const collapseList = [
type: "serviceNode",
desc: "获取相机拍摄的照片",
action: 'CAMERA_GETRGBIMAGE',
nodeType: 'EDGE',
nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
],
@ -113,6 +150,7 @@ export const collapseList = [
type: "serviceNode",
desc: "获取相机照片后,关闭该相机",
action: 'CAMERA_STOP',
nodeType: 'EDGE',
nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
],
@ -129,6 +167,7 @@ export const collapseList = [
type: "serviceNode",
desc: "开启摄像头,并开始录像",
action: 'CAMERA_RECORDING_START',
nodeType: 'EDGE',
nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
],
@ -140,6 +179,7 @@ export const collapseList = [
type: "serviceNode",
desc: "停止录像,并关闭摄像头",
action: 'CAMERA_RECORDING_STOP',
nodeType: 'EDGE',
nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
],
@ -224,6 +264,7 @@ export const collapseList = [
type: "serviceNode",
desc: "用于启动麦克风的节点",
action: 'MICROPHONE_START',
nodeType: 'EDGE',
nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
],
@ -235,6 +276,7 @@ export const collapseList = [
type: "serviceNode",
desc: "用于停止麦克风的节点",
action: 'MICROPHONE_START',
nodeType: 'EDGE',
nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
],
@ -262,6 +304,7 @@ export const collapseList = [
type: "serviceNode",
desc: "用于获取触控坐标的节点",
action: 'TOUCH_COORDINATES',
nodeType: "LLM",
nodeParams: [
{ name: "targetFeature", type: "input", input: "运动模式", disabled: true },
{ name: "manufacturer", type: "input", input: "xiaomi", disabled: true },
@ -275,6 +318,7 @@ export const collapseList = [
type: "serviceNode",
desc: "将指令文本(text)处理合成语音,输出语音路径(audioPath)",
action: 'GENERATE_ADVANCED_AUDIO',
nodeType: "LLM",
outputType: 'json',
nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -284,6 +328,7 @@ export const collapseList = [
type: "serviceNode",
desc: "意图识别,输出类型(type) 1: 语音交互 2: 触控交互 3: 闲聊 4: 知识问答",
action: 'INTENT_RECOGNITION',
nodeType: "LLM",
outputType: 'json',
nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'type', type: 'string', desc: '1语音交互 2触控交互 3: 闲聊 4:知识问答', disabled: true}]
@ -312,6 +357,7 @@ export const collapseList = [
type: "serviceNode",
desc: "唤醒语料处理,输入唤醒语料(wakeCorpus),输出语音路径(audioPath)",
action: 'VI_CORPUS_WAKE',
nodeType: "EDGE",
outputType: 'json',
nodeParams: [{ name: 'wakeCorpus', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -322,6 +368,7 @@ export const collapseList = [
type: "serviceNode",
desc: "测试语料-单次对话语料处理,输入单次对话语料(testCorpus),输出语音路径(audioPath)",
action: 'VI_CORPUS_SINGLE',
nodeType: "EDGE",
outputType: 'json',
nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -332,6 +379,7 @@ export const collapseList = [
type: "serviceNode",
desc: "测试语料-连续对话语料处理,输入连续对话语料(testCorpus),输出语音路径(audioPath)",
action: 'VI_CORPUS_CONTINUOUS',
nodeType: "EDGE",
outputType: 'json',
nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }],
outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
@ -342,6 +390,7 @@ export const collapseList = [
type: "serviceNode",
desc: "用于播放语料的节点,需要输入语音路径(audioPath)和播放语音的设备id(deviceId)",
action: 'VI_PLAY_CORPUS',
nodeType: "EDGE",
outputType: 'json',
nodeParams: [
{ 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>
<div class="node__container">
<div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus">
<template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template>
<template #output>
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
<el-image
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"
<JsonViewer
v-if="props.properties.outputType === 'json'"
:value="outputJsonData"
copyable
boxed
sort
theme="light"
/>
<el-image
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>
</NodeState>
<NodeTitle
:icon="props.properties.icon"
:nodeId="props.model.id"
:nodeProperties="props.properties"
:nodeType="props.properties.nodeType || 'NONE'"
:nodeName="props.properties.name"
: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="left">
<div class="tag"></div>
@ -57,11 +71,14 @@
<el-form-item
:label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`"
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]"
:rules="[
{ required: true, message: '请输入参数名', trigger: 'blur' },
]"
>
<el-input
:disabled="property?.disabled || false"
v-model="property.name"
@keydown="handleInputKeydown"
placeholder="请输入"
clearable
/>
@ -70,33 +87,76 @@
:label="index === 0 ? '参数值' : ''"
: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="input" />
</el-select>
<el-form-item
v-if="property.type === 'input'"
:rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]"
:rules="[
{
required: true,
message: '请输入参数值',
trigger: 'blur',
},
]"
: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-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-select>
<el-input v-else v-model="property.input" placeholder="请输入" clearable />
<el-input-number
v-if="property.componentType === 'number'"
v-model="property.input"
:min="0"
: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
v-if="property.type === 'quote'"
:rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]"
:rules="[
{
required: true,
message: '请选择参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.quote`"
>
<el-cascader
:ref="el => { if (el) cascaderRefs[index] = el }"
:ref="
(el) => {
if (el) cascaderRefs[index] = el;
}
"
v-model="property.quote"
:checkStrictly="true"
:options="quoteOptions"
placeholder="请选择"
@visible-change="(visible) => visibleChange(visible, index, property.quote)"
@visible-change="
(visible) => visibleChange(visible, index, property.quote)
"
@change="(value) => cascaderChange(value, index)"
/>
</el-form-item>
@ -106,51 +166,53 @@
</el-form>
</div>
</div>
<div class="output__container" v-if="props.properties?.outputParams?.length > 0">
<div class="title">
<div class="left">
<div class="tag"></div>
<div class="text">输出</div>
<div class="output__container" v-show="nodeZoom">
<div v-if="props.properties?.outputParams?.length > 0">
<div class="title">
<div class="left">
<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 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">
<el-form
:inline="true"
:model="formData"
label-position="top"
label-width="auto"
:rules="outputRules"
ref="outputFormRef"
>
<FormItemRecursive
formType="output"
:current-list="formData.outputParams"
prop-path="outputParams"
:depth="0"
:is-first-level="true"
:endDepth="2"
:parent-path="[]"
@delete-item="deleteTopLevelItem"
/>
</el-form>
<div class="form__container">
<el-form
:inline="true"
:model="formData"
label-position="top"
label-width="auto"
:rules="outputRules"
ref="outputFormRef"
>
<FormItemRecursive
formType="output"
:current-list="formData.outputParams"
prop-path="outputParams"
:depth="0"
:is-first-level="true"
:endDepth="2"
:parent-path="[]"
@delete-item="deleteTopLevelItem"
/>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from "vue";
import { getInput } from "@/utils/flow";
import { ref, reactive, onMounted, onUnmounted, nextTick } from "vue";
import { getInput, initNodeZoom } from "@/utils/flow";
import { useFlowStore } from "@/store/modules/flow";
import { Plus } from "@element-plus/icons-vue";
import NodeTitle from "../../components/NodeTitle.vue";
@ -171,7 +233,7 @@ const flowStore = useFlowStore();
const formData = reactive({
nodeParams: [],
outputParams: []
outputParams: [],
});
const rules = reactive({});
@ -189,39 +251,40 @@ const handleTypeChange = (index) => {
}
};
const cascaderRefs = ref([])
const cascaderRefs = ref([]);
const outputRules = reactive({});
const cascaderChange = (value, index) => {
const selectedOptions = cascaderRefs.value[index].getCheckedNodes(true);
formData.nodeParams[index].quote = value
formData.nodeParams[index].quoteType = selectedOptions[0].data.type
}
formData.nodeParams[index].quote = value;
formData.nodeParams[index].quoteType = selectedOptions[0].data.type;
};
const addOutputFormItem = () => {
formData.outputParams.push({
name: '',
type: '',
desc: '',
children: []
})
}
name: "",
type: "",
desc: "",
children: [],
});
};
const dynamicForm = ref();
const outputFormRef = ref()
const outputFormRef = ref();
const setNodeProperties = async () => {
try {
const result = await dynamicForm.value.validate();
let flag = true
let flag = true;
if (outputFormRef.value) {
flag = await outputFormRef.value.validate()
flag = await outputFormRef.value.validate();
}
if (result && flag) {
const data = toRaw(formData);
const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, {
...properties,
...data,
zoom: nodeZoom.value
});
emits("contentChange");
}
@ -286,20 +349,26 @@ const addFormItem = () => {
//
const deleteTopLevelItem = (fullPath) => {
//
//
let currentLevel = formData.outputParams;
//
for (let i = 0; i < fullPath.length - 1; i++) {
const index = fullPath[i];
//
currentLevel = currentLevel[index].children;
}
//
const lastIndex = fullPath[fullPath.length - 1];
currentLevel.splice(lastIndex, 1);
emits("contentChange");
};
const nodeZoom = ref(props?.properties?.zoom ?? true)
const zoom = (flag) => {
nodeZoom.value = flag
initNodeZoom(props.model.id, nodeZoom.value, '.node__box')
}
watch(
@ -311,8 +380,15 @@ watch(
if (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;
}
},
@ -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(() => {
emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) {
@ -499,13 +584,13 @@ defineExpose({
:deep(.el-row) {
align-items: end;
.el-input {
--el-input-width: 148px;
--el-input-width: 148px;
}
.el-select {
--el-select-width: 148px;
--el-select-width: 148px;
}
.el-cascader {

View File

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

View File

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

View File

@ -1,425 +1,437 @@
<template>
<div class="node__container">
<NodeState :state="nodeOperatingStatus">
<template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template>
<template #output>
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
<el-image
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" />
</template>
</NodeState>
<NodeTitle
:icon="props.properties.icon"
:nodeId="props.model.id"
:nodeProperties="props.properties"
:nodeName="props.properties.name"
:nodeDesc="props.properties.desc"
/>
<div class="input__container">
<div class="title">
<div class="left">
<div class="tag"></div>
<div class="text">输入</div>
</div>
<div class="right" v-if="properties.properties?.canAddFormItem">
<el-button
:disabled="flowStore.disableForm"
@click="addFormItem"
class="addFormItem"
>
<el-icon :size="20"><Plus /></el-icon>
</el-button>
</div>
</div>
<div class="form__container">
<el-form
:inline="true"
:model="formData"
:rules="rules"
ref="dynamicForm"
label-position="top"
label-width="auto"
:disabled="flowStore.disableForm"
>
<div v-for="(property, index) in formData.nodeParams" :key="index">
<el-row>
<el-form-item
:label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`"
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]"
>
<el-input
:disabled="property?.disabled || false"
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
v-if="property.type === 'input'"
:rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]"
: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-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-select>
<el-input v-else v-model="property.input" 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
v-model="property.quote"
:options="quoteOptions"
placeholder="请选择"
@visible-change="visibleChange"
/>
</el-form-item>
</el-form-item>
</el-row>
</div>
</el-form>
</div>
</div>
<div class="output__container" v-if="props.properties?.outputParams?.length > 0">
<div class="title">
<div class="tag"></div>
<div class="text">输出</div>
</div>
<div class="form__container">
<el-form
:inline="true"
label-position="top"
label-width="auto"
:disabled="true"
>
<el-row v-for="(item, index) in props.properties.outputParams">
<el-form-item label="参数名" >
<el-input :value="item.name" />
</el-form-item>
<el-form-item label="参数类型" >
<el-input :value="item.type" />
</el-form-item>
<el-form-item label="描述" >
<el-input :value="item.desc" />
</el-form-item>
</el-row>
</el-form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from "vue";
import { getInput } from "@/utils/flow";
import { useFlowStore } from "@/store/modules/flow";
import { Plus } from "@element-plus/icons-vue";
import NodeTitle from "../../components/NodeTitle.vue";
import NodeState from "../../components/NodeState.vue";
import "vue3-json-viewer/dist/index.css";
import { emitter } from "@/utils/eventBus";
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
const props = defineProps({
model: Object,
properties: Object,
});
const emits = defineEmits(["contentChange"]);
const flowStore = useFlowStore();
const formData = reactive({
nodeParams: [],
});
const rules = reactive({});
const quoteOptions = ref([]);
const handleTypeChange = (index) => {
if (formData.nodeParams[index].type === "input") {
formData.nodeParams[index].quote = "";
} else {
formData.nodeParams[index].input = "";
const option = getInput(props.model.id);
if (option) {
quoteOptions.value = option;
}
}
};
const dynamicForm = ref();
const setNodeProperties = async () => {
try {
const result = await dynamicForm.value.validate();
if (result) {
const data = toRaw(formData);
const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, {
...properties,
...data,
});
emits("contentChange");
}
} catch {
dynamicForm.value.clearValidate();
}
};
const nodeOperatingStatus = ref("NORMAL");
const inputJsonData = ref({});
const outputJsonData = ref({});
const visibleChange = (value) => {
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) {
return { valid: true, message: "验证通过" };
} else {
return { valid: false, message: "验证失败" };
}
} catch {
return { valid: false, message: "验证失败" };
}
};
const addFormItem = () => {
formData.nodeParams.push({
name: "",
type: "",
desc: "",
required: false,
children: [],
});
emits("contentChange");
};
watch(
() => props.properties,
() => {
if (props.properties.nodeParams && props.properties.nodeParams.length > 0) {
formData.nodeParams = props.properties.nodeParams;
const option = getInput(props.model.id);
if (option) {
quoteOptions.value = option;
}
}
},
{
immediate: true,
deep: true,
}
);
onMounted(() => {
emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) {
nodeOperatingStatus.value = data.status;
let inputData = {};
let output = {};
try {
inputData = JSON.parse(data.paramsIn) || {};
output = JSON.parse(data.paramsOut) || {};
} catch (error) {
inputData = data.paramsIn || {};
output = data.paramsOut || {};
}
inputJsonData.value = inputData;
outputJsonData.value = output;
emits("contentChange");
}
});
emitter.on("contentChange", (data) => {
if (data.id === props.model.id) {
nodeOperatingStatus.value = "NORMAL";
inputJsonData.value = {};
outputJsonData.value = {};
emits("contentChange");
}
});
});
onUnmounted(() => {
emitter.off("changeNodeState");
emitter.off("contentChange");
});
defineExpose({
validateForm,
setNodeProperties,
});
</script>
<style lang="scss" scoped>
.node__container {
width: 100%;
height: auto;
.input__container {
width: 100%;
background-color: #fafbfc;
padding: 0 16px;
border-radius: 8px;
box-sizing: border-box;
.title {
height: 32px;
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
align-items: center;
.tag {
width: 3px;
height: 16px;
background: #1664ff;
border-radius: 0 4px 4px 0;
margin-right: 12px;
}
.text {
font-size: 16px;
font-weight: 700;
color: #0c0d0e;
}
}
.right {
.addFormItem {
border: none;
background: transparent;
cursor: pointer;
}
}
}
.form__container {
margin: 12px 0;
.sub-properties {
margin-left: 10px;
.zw {
margin-left: 15px;
&::before {
content: "";
position: absolute;
left: 0;
top: -4px;
width: 10px;
border-left: 1px solid gray;
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px;
background-color: transparent;
height: 24px;
}
}
}
.gSon-properties {
margin-left: 10px;
.zw {
margin-left: 15px;
&::before {
content: "";
position: absolute;
left: 0;
top: -4px;
width: 10px;
border-left: 1px solid gray;
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px;
background-color: transparent;
height: 24px;
}
}
}
}
}
.output__container {
width: 100%;
background-color: #fafbfc;
padding: 0 16px;
border-radius: 8px;
box-sizing: border-box;
.title {
height: 32px;
display: flex;
align-items: center;
.tag {
width: 3px;
height: 16px;
background: #1664ff;
border-radius: 0 4px 4px 0;
margin-right: 12px;
}
.text {
font-size: 16px;
font-weight: 700;
color: #0c0d0e;
}
}
.form__container {
margin: 12px 0;
}
}
:deep(.el-row) {
align-items: end;
.el-input {
--el-input-width: 170px;
}
.el-select {
--el-select-width: 170px;
}
.el-cascader {
--el-form-inline-content-width: 170px;
}
}
}
</style>
<template>
<div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus">
<template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template>
<template #output>
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
<el-image
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" />
</template>
</NodeState>
<NodeTitle
:icon="props.properties.icon"
:nodeId="props.model.id"
:nodeProperties="props.properties"
:nodeName="props.properties.name"
:nodeDesc="props.properties.desc"
:zoom-state="nodeZoom"
@zoom="zoom"
/>
<div class="input__container" v-show="nodeZoom">
<div class="title">
<div class="left">
<div class="tag"></div>
<div class="text">输入</div>
</div>
<div class="right" v-if="properties.properties?.canAddFormItem">
<el-button
:disabled="flowStore.disableForm"
@click="addFormItem"
class="addFormItem"
>
<el-icon :size="20"><Plus /></el-icon>
</el-button>
</div>
</div>
<div class="form__container" v-show="nodeZoom">
<el-form
:inline="true"
:model="formData"
:rules="rules"
ref="dynamicForm"
label-position="top"
label-width="auto"
:disabled="flowStore.disableForm"
>
<div v-for="(property, index) in formData.nodeParams" :key="index">
<el-row>
<el-form-item
:label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`"
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]"
>
<el-input
:disabled="property?.disabled || false"
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
v-if="property.type === 'input'"
:rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]"
: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-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-select>
<el-input v-else v-model="property.input" 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
v-model="property.quote"
:options="quoteOptions"
placeholder="请选择"
@visible-change="visibleChange"
/>
</el-form-item>
</el-form-item>
</el-row>
</div>
</el-form>
</div>
</div>
<div class="output__container" v-if="props.properties?.outputParams?.length > 0">
<div class="title">
<div class="tag"></div>
<div class="text">输出</div>
</div>
<div class="form__container">
<el-form
:inline="true"
label-position="top"
label-width="auto"
:disabled="true"
>
<el-row v-for="(item, index) in props.properties.outputParams">
<el-form-item label="参数名" >
<el-input :value="item.name" />
</el-form-item>
<el-form-item label="参数类型" >
<el-input :value="item.type" />
</el-form-item>
<el-form-item label="描述" >
<el-input :value="item.desc" />
</el-form-item>
</el-row>
</el-form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, nextTick } from "vue";
import { getInput, initNodeZoom } from "@/utils/flow";
import { useFlowStore } from "@/store/modules/flow";
import { Plus } from "@element-plus/icons-vue";
import NodeTitle from "../../components/NodeTitle.vue";
import NodeState from "../../components/NodeState.vue";
import "vue3-json-viewer/dist/index.css";
import { emitter } from "@/utils/eventBus";
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
const props = defineProps({
model: Object,
properties: Object,
});
const emits = defineEmits(["contentChange"]);
const flowStore = useFlowStore();
const formData = reactive({
nodeParams: [],
});
const rules = reactive({});
const quoteOptions = ref([]);
const handleTypeChange = (index) => {
if (formData.nodeParams[index].type === "input") {
formData.nodeParams[index].quote = "";
} else {
formData.nodeParams[index].input = "";
const option = getInput(props.model.id);
if (option) {
quoteOptions.value = option;
}
}
};
const dynamicForm = ref();
const setNodeProperties = async () => {
try {
const result = await dynamicForm.value.validate();
if (result) {
const data = toRaw(formData);
const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, {
...properties,
...data,
zoom: nodeZoom.value
});
emits("contentChange");
}
} catch {
dynamicForm.value.clearValidate();
}
};
const nodeOperatingStatus = ref("NORMAL");
const inputJsonData = ref({});
const outputJsonData = ref({});
const visibleChange = (value) => {
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) {
return { valid: true, message: "验证通过" };
} else {
return { valid: false, message: "验证失败" };
}
} catch {
return { valid: false, message: "验证失败" };
}
};
const addFormItem = () => {
formData.nodeParams.push({
name: "",
type: "",
desc: "",
required: false,
children: [],
});
emits("contentChange");
};
const nodeZoom = ref(props?.properties?.zoom ?? true)
const zoom = (flag) => {
nodeZoom.value = flag
initNodeZoom(props.model.id, nodeZoom.value, '.node__box')
}
watch(
() => props.properties,
() => {
if (props.properties.nodeParams && props.properties.nodeParams.length > 0) {
formData.nodeParams = props.properties.nodeParams;
const option = getInput(props.model.id);
if (option) {
quoteOptions.value = option;
}
nextTick(() => {
initNodeZoom(props.model.id, props?.properties?.zoom ?? true, '.node__box', true)
})
}
},
{
immediate: true,
deep: true,
}
);
onMounted(() => {
emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) {
nodeOperatingStatus.value = data.status;
let inputData = {};
let output = {};
try {
inputData = JSON.parse(data.paramsIn) || {};
output = JSON.parse(data.paramsOut) || {};
} catch (error) {
inputData = data.paramsIn || {};
output = data.paramsOut || {};
}
inputJsonData.value = inputData;
outputJsonData.value = output;
emits("contentChange");
}
});
emitter.on("contentChange", (data) => {
if (data.id === props.model.id) {
nodeOperatingStatus.value = "NORMAL";
inputJsonData.value = {};
outputJsonData.value = {};
emits("contentChange");
}
});
});
onUnmounted(() => {
emitter.off("changeNodeState");
emitter.off("contentChange");
});
defineExpose({
validateForm,
setNodeProperties,
});
</script>
<style lang="scss" scoped>
.node__container {
width: 100%;
height: auto;
.input__container {
width: 100%;
background-color: #fafbfc;
padding: 0 16px;
border-radius: 8px;
box-sizing: border-box;
.title {
height: 32px;
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
align-items: center;
.tag {
width: 3px;
height: 16px;
background: #1664ff;
border-radius: 0 4px 4px 0;
margin-right: 12px;
}
.text {
font-size: 16px;
font-weight: 700;
color: #0c0d0e;
}
}
.right {
.addFormItem {
border: none;
background: transparent;
cursor: pointer;
}
}
}
.form__container {
margin: 12px 0;
.sub-properties {
margin-left: 10px;
.zw {
margin-left: 15px;
&::before {
content: "";
position: absolute;
left: 0;
top: -4px;
width: 10px;
border-left: 1px solid gray;
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px;
background-color: transparent;
height: 24px;
}
}
}
.gSon-properties {
margin-left: 10px;
.zw {
margin-left: 15px;
&::before {
content: "";
position: absolute;
left: 0;
top: -4px;
width: 10px;
border-left: 1px solid gray;
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px;
background-color: transparent;
height: 24px;
}
}
}
}
}
.output__container {
width: 100%;
background-color: #fafbfc;
padding: 0 16px;
border-radius: 8px;
box-sizing: border-box;
.title {
height: 32px;
display: flex;
align-items: center;
.tag {
width: 3px;
height: 16px;
background: #1664ff;
border-radius: 0 4px 4px 0;
margin-right: 12px;
}
.text {
font-size: 16px;
font-weight: 700;
color: #0c0d0e;
}
}
.form__container {
margin: 12px 0;
}
}
:deep(.el-row) {
align-items: end;
.el-input {
--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>
<div class="node__container" ref="switchRef" @mouseleave="setNodeProperties">
<NodeState :state="nodeOperatingStatus">
<template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template>
<template #output>
<JsonViewer :value="outputJsonData" copyable boxed sort theme="light" />
</template>
</NodeState>
<NodeTitle :icon="switchSvg" :nodeId="props.model.id" :nodeProperties="props.properties" :nodeName="props.properties?.name || '分支'" nodeDesc="连接多个下游分支,根据设定的条件按照顺序查找的方式来匹配运行的分支,如果匹配到某条件则只运行该条件对应的分支,否则继续匹配下一条件直至结束" />
<div class="condition_title_container">
<div class="left">
<div class="space"></div>
<div class="title">所有条件</div>
</div>
<div class="right">
<el-button :disabled="flowStore.disableForm" @click="addFormItem" class="addFormItem">
<el-icon :size="20" ><Plus /></el-icon>
</el-button>
</div>
</div>
<div>
<el-form :inline="true" :model="formData" :rules="rules" ref="dynamicForm" label-position="top" label-width="auto" :disabled="flowStore.disableForm">
<div class="condition" v-for="(params, index) in formData.nodeParams" :id="params.id">
<div class="title__container">
<div class="left">
<div class="space"></div>
<div class="title">{{ index ===0 ? 'If' : 'Else If' }}</div>
</div>
<div class="right">
<el-button :disabled="flowStore.disableForm" circle size="small" @click="deleteFormItem(params.id, index)">
<el-icon :size="16" ><Minus /></el-icon>
</el-button>
</div>
</div>
<div class="withOr">
<div class="title">条件</div>
<div>
<el-select v-model="params.withOr" placeholder="Select" style="width: 240px">
<el-option key="and" label="AND" value="and" />
<el-option key="or" label="OR" value="or" />
</el-select>
</div>
</div>
<div class="form__container">
<el-row v-for="(property, pIndex) in params.list">
<!-- :prop="`nodeParams.${index}.list.${pIndex}.name`" :rules="[{ required: true, message: '请输入变量名', trigger: 'blur' }]" -->
<el-form-item :label="pIndex === 0 ? '引用变量' : ''" :prop="`nodeParams.${index}.list.${pIndex}.nameType`">
<!-- <el-input v-model="property.name" placeholder="请输入" clearable /> -->
<el-select v-model="property.nameType" @change="handleTypeChange(index, pIndex)">
<el-option label="引用" value="quote" />
<el-option label="输入" value="input" />
</el-select>
<el-form-item v-if="property.nameType === 'input'" :rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.name`">
<el-input v-model="property.name" placeholder="请输入" clearable />
</el-form-item>
<el-form-item v-if="property.nameType === 'quote'" :rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.nameQuote`">
<el-cascader v-model="property.nameQuote" :options="quoteOptions" placeholder="请选择" @visible-change="visibleChange" />
</el-form-item>
</el-form-item>
<el-form-item :label="pIndex === 0 ? '选择条件' : ''" :prop="`nodeParams.${index}.list.${pIndex}.condition`" :rules="[{ required: true, message: '请输入变量名', trigger: 'blur' }]">
<el-select v-model="property.condition">
<el-option label="等于" value="equal" />
<el-option label="不等于" value="notEqualTo" />
<el-option label="为空" value="null" />
<el-option label="不为空" value="notNull" />
<el-option label="包含" value="include" />
<el-option label="不包含" value="notInclude" />
<el-option label="大于" value="greaterThan" />
<el-option label="大于等于" value="greaterThanOrEqual" />
<el-option label="小于" value="lessThan" />
<el-option label="小于等于" value="lessThanOrEqual" />
</el-select>
</el-form-item>
<el-form-item :label="pIndex === 0 ? '比较值' : ''" :prop="`nodeParams.${index}.list.${pIndex}.type`" >
<el-select v-model="property.type" @change="handleTypeChange(index, pIndex)">
<el-option label="引用" value="quote" />
<el-option label="输入" value="input" />
</el-select>
<el-form-item v-if="property.type === 'input'" :rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.input`">
<el-input v-model="property.input" placeholder="请输入" clearable />
</el-form-item>
<el-form-item v-if="property.type === 'quote'" :rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]" :prop="`nodeParams.${index}.list.${pIndex}.quote`">
<el-cascader v-model="property.quote" :options="quoteOptions" placeholder="请选择" @visible-change="visibleChange" />
</el-form-item>
</el-form-item>
<el-form-item :label="pIndex === 0 ? '操作' : ''">
<el-button :disabled="flowStore.disableForm" circle size="small" @click="addListItem(index)">
<el-icon :size="12" ><Plus /></el-icon>
</el-button>
<el-button :disabled="flowStore.disableForm" circle size="small" @click="removeListItem(index, pIndex)">
<el-icon :size="12" ><Minus /></el-icon>
</el-button>
</el-form-item>
</el-row>
</div>
</div>
</el-form>
</div>
<div class="else" :id="''.concat(props.model.id, '_else')">
<div class="space"></div>
<div class="title">Else</div>
</div>
</div>
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
import NodeTitle from '../../components/NodeTitle.vue'
import NodeState from "../../components/NodeState.vue";
import switchSvg from '../../icon/switch.svg'
import { v4 as randomUUID } from 'uuid'
import { useFlowStore } from '@/store/modules/flow'
import { Plus, Minus } from '@element-plus/icons-vue'
import { getInput, addNewEdge, removeSwitchEdge } from '@/utils/flow'
import { emitter } from "@/utils/eventBus";
const props = defineProps({
model: Object,
graphModel: Object,
properties: Object,
nowTime: Number,
});
const emits = defineEmits(['addAnchor', 'removeAnchor', 'bindRef', 'changeAnchor'])
const switchRef = ref()
const dynamicForm = ref()
const flowStore = useFlowStore()
const formData = reactive({
nodeParams: props.properties?.conditions || []
})
const addFormItem = () => {
const rect = switchRef.value.getBoundingClientRect();
addCondition(rect.height - 60)
}
const deleteFormItem = (id, index) => {
if (index === 0) {
return
}
formData.nodeParams.splice(index, 1)
emits('removeAnchor', id)
removeSwitchEdge(props.model.id, id)
nextTick(() => {
changeFormItemHeight()
})
}
const quoteOptions = ref([])
const handleTypeChange = (pIndex, index) => {
if (formData.nodeParams[pIndex].list[index].type === 'input') {
formData.nodeParams[pIndex].list[index].quote = ""
} else {
formData.nodeParams[pIndex].list[index].input = ""
const option = getInput(props.model.id, false)
if (option) {
quoteOptions.value = option
}
}
}
const addCondition = (height) => {
const id = randomUUID().replace(/-/g, '')
formData.nodeParams.push({
id,
withOr: 'and',
list: [{
name: '',
nameQuote: '',
nameType: 'input',
type: 'input',
input: '',
quote: ''
}]
})
emits('addAnchor', height, id)
setTimeout(() => {
addNewEdge(props.model.id)
}, 50)
nextTick(() => {
changeFormItemHeight()
})
}
const addListItem = (pIndex, cIndex) => {
formData.nodeParams[pIndex].list.push({
name: '',
nameQuote: '',
nameType: 'input',
type: 'input',
input: '',
quote: ''
})
nextTick(() => {
changeFormItemHeight()
})
}
const removeListItem = (pIndex, cIndex) => {
if (cIndex === 0) {
return
}
formData.nodeParams[pIndex].list.splice(cIndex, 1)
nextTick(() => {
changeFormItemHeight()
})
}
const changeFormItemHeight = () => {
formData.nodeParams.forEach(item => {
const node = document.getElementById(item.id)
emits('changeAnchor', node.offsetTop, item.id)
});
const node = document.getElementById("".concat(props.model.id, "_else"))
emits('changeAnchor', node.offsetTop, "".concat(props.model.id, "_else"))
}
const setNodeProperties = async () => {
try {
const valid = await dynamicForm.value.validate()
if (valid) {
const data = toRaw(formData)
const properties = lf.getProperties(props.model.id)
lf.setProperties(props.model.id, {
...properties,
...data
})
emits('contentChange')
}
} catch (error) {
dynamicForm.value.clearValidate()
}
}
const visibleChange = (value) => {
if (value) {
const option = getInput(props.model.id)
if (option) {
quoteOptions.value = option
}
}
}
const nodeOperatingStatus = ref("NORMAL");
const inputJsonData = ref({});
const outputJsonData = ref({});
watch(
() => props.properties,
() => {
if (props.properties.nodeParams && props.properties.nodeParams.length > 0) {
formData.nodeParams = props.properties.nodeParams
} else {
if (formData.nodeParams.length === 0) {
addCondition(100)
}
}
}, {
immediate: true,
deep: true
}
);
onMounted(() => {
emits('addAnchor', 280, "".concat(props.model.id, "_else"))
emits('bindRef', dynamicForm.value)
emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) {
nodeOperatingStatus.value = data.status;
let inputData = {};
let output = {};
try {
inputData = JSON.parse(data.paramsIn) || {};
output = JSON.parse(data.paramsOut) || {};
} catch (error) {
inputData = data.paramsIn || {};
output = data.paramsOut || {};
}
inputJsonData.value = inputData;
outputJsonData.value = output;
emits("contentChange");
}
});
emitter.on("contentChange", (data) => {
if (data.id === props.model.id) {
nodeOperatingStatus.value = "NORMAL";
inputJsonData.value = {};
outputJsonData.value = {};
emits("contentChange");
}
});
})
onUnmounted(() => {
emitter.off("changeNodeState");
emitter.off("contentChange");
});
</script>
<style lang="scss" scoped>
.node__container {
width: 720px;
height: auto;
background: #fff;
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>
<template>
<div class="node__container" ref="switchRef" @mouseleave="setNodeProperties">
<NodeState :state="nodeOperatingStatus">
<template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template>
<template #output>
<JsonViewer :value="outputJsonData" copyable boxed sort theme="light" />
</template>
</NodeState>
<NodeTitle
:icon="switchSvg"
:nodeId="props.model.id"
:nodeProperties="props.properties"
:nodeName="props.properties?.name || '分支'"
nodeDesc="连接多个下游分支,根据设定的条件按照顺序查找的方式来匹配运行的分支,如果匹配到某条件则只运行该条件对应的分支,否则继续匹配下一条件直至结束"
/>
<div class="condition_title_container">
<div class="left">
<div class="space"></div>
<div class="title">所有条件</div>
</div>
<div class="right">
<el-button
:disabled="flowStore.disableForm"
@click="addFormItem"
class="addFormItem"
>
<el-icon :size="20"><Plus /></el-icon>
</el-button>
</div>
</div>
<div>
<el-form
:inline="true"
:model="formData"
:rules="rules"
ref="dynamicForm"
label-position="top"
label-width="auto"
:disabled="flowStore.disableForm"
>
<div
class="condition"
v-for="(params, index) in formData.nodeParams"
:id="params.id"
>
<div class="title__container">
<div class="left">
<div class="space"></div>
<div class="title">{{ index === 0 ? "If" : "Else If" }}</div>
</div>
<div class="right">
<el-button
:disabled="flowStore.disableForm"
circle
size="small"
@click="deleteFormItem(params.id, index)"
>
<el-icon :size="16"><Minus /></el-icon>
</el-button>
</div>
</div>
<div class="withOr">
<div class="title">条件</div>
<div>
<el-select
v-model="params.withOr"
placeholder="Select"
style="width: 240px"
>
<el-option key="and" label="AND" value="and" />
<el-option key="or" label="OR" value="or" />
</el-select>
</div>
</div>
<div class="form__container">
<el-row v-for="(property, pIndex) in params.list">
<!-- :prop="`nodeParams.${index}.list.${pIndex}.name`" :rules="[{ required: true, message: '请输入变量名', trigger: 'blur' }]" -->
<el-form-item
:label="pIndex === 0 ? '引用变量' : ''"
:prop="`nodeParams.${index}.list.${pIndex}.nameType`"
>
<!-- <el-input v-model="property.name" placeholder="请输入" clearable /> -->
<el-select
v-model="property.nameType"
@change="handleTypeChange(index, pIndex)"
>
<el-option label="引用" value="quote" />
<el-option label="输入" value="input" />
</el-select>
<el-form-item
v-if="property.nameType === 'input'"
:rules="[
{
required: true,
message: '请输入参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.list.${pIndex}.name`"
>
<el-input
v-model="property.name"
placeholder="请输入"
clearable
/>
</el-form-item>
<el-form-item
v-if="property.nameType === 'quote'"
:rules="[
{
required: true,
message: '请选择参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.list.${pIndex}.nameQuote`"
>
<el-cascader
v-model="property.nameQuote"
:options="quoteOptions"
placeholder="请选择"
@visible-change="visibleChange"
/>
</el-form-item>
</el-form-item>
<el-form-item
:label="pIndex === 0 ? '选择条件' : ''"
:prop="`nodeParams.${index}.list.${pIndex}.condition`"
:rules="[
{ required: true, message: '请输入变量名', trigger: 'blur' },
]"
>
<el-select v-model="property.condition">
<el-option label="等于" value="equal" />
<el-option label="不等于" value="notEqualTo" />
<el-option label="为空" value="null" />
<el-option label="不为空" value="notNull" />
<el-option label="包含" value="include" />
<el-option label="不包含" value="notInclude" />
<el-option label="大于" value="greaterThan" />
<el-option label="大于等于" value="greaterThanOrEqual" />
<el-option label="小于" value="lessThan" />
<el-option label="小于等于" value="lessThanOrEqual" />
</el-select>
</el-form-item>
<el-form-item
:label="pIndex === 0 ? '比较值' : ''"
:prop="`nodeParams.${index}.list.${pIndex}.type`"
>
<el-select
v-model="property.type"
@change="handleTypeChange(index, pIndex)"
>
<el-option label="引用" value="quote" />
<el-option label="输入" value="input" />
</el-select>
<el-form-item
v-if="property.type === 'input'"
:rules="[
{
required: true,
message: '请输入参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.list.${pIndex}.input`"
>
<el-input
v-model="property.input"
placeholder="请输入"
clearable
/>
</el-form-item>
<el-form-item
v-if="property.type === 'quote'"
:rules="[
{
required: true,
message: '请选择参数值',
trigger: 'blur',
},
]"
:prop="`nodeParams.${index}.list.${pIndex}.quote`"
>
<el-cascader
v-model="property.quote"
:options="quoteOptions"
placeholder="请选择"
@visible-change="visibleChange"
/>
</el-form-item>
</el-form-item>
<el-form-item :label="pIndex === 0 ? '操作' : ''">
<el-button
:disabled="flowStore.disableForm"
circle
size="small"
@click="addListItem(index)"
>
<el-icon :size="12"><Plus /></el-icon>
</el-button>
<el-button
:disabled="flowStore.disableForm"
circle
size="small"
@click="removeListItem(index, pIndex)"
>
<el-icon :size="12"><Minus /></el-icon>
</el-button>
</el-form-item>
</el-row>
</div>
</div>
</el-form>
</div>
<div class="else" :id="''.concat(props.model.id, '_else')">
<div class="space"></div>
<div class="title">Else</div>
</div>
</div>
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
import NodeTitle from "../../components/NodeTitle.vue";
import NodeState from "../../components/NodeState.vue";
import switchSvg from "../../icon/switch.svg";
import { v4 as randomUUID } from "uuid";
import { useFlowStore } from "@/store/modules/flow";
import { Plus, Minus } from "@element-plus/icons-vue";
import { getInput, addNewEdge, removeSwitchEdge } from "@/utils/flow";
import { emitter } from "@/utils/eventBus";
const props = defineProps({
model: Object,
graphModel: Object,
properties: Object,
nowTime: Number,
});
const emits = defineEmits([
"addAnchor",
"removeAnchor",
"bindRef",
"changeAnchor",
]);
const switchRef = ref();
const dynamicForm = ref();
const flowStore = useFlowStore();
const formData = reactive({
nodeParams: props.properties?.conditions || [],
});
const addFormItem = () => {
const rect = switchRef.value.getBoundingClientRect();
addCondition(rect.height - 60);
};
const deleteFormItem = (id, index) => {
if (index === 0) {
return;
}
formData.nodeParams.splice(index, 1);
emits("removeAnchor", id);
removeSwitchEdge(props.model.id, id);
nextTick(() => {
changeFormItemHeight();
});
};
const quoteOptions = ref([]);
const handleTypeChange = (pIndex, index) => {
if (formData.nodeParams[pIndex].list[index].type === "input") {
formData.nodeParams[pIndex].list[index].quote = "";
} else {
formData.nodeParams[pIndex].list[index].input = "";
const option = getInput(props.model.id, false);
if (option) {
quoteOptions.value = option;
}
}
};
const addCondition = (height) => {
const id = randomUUID().replace(/-/g, "");
formData.nodeParams.push({
id,
withOr: "and",
list: [
{
name: "",
nameQuote: "",
nameType: "input",
type: "input",
input: "",
quote: "",
},
],
});
emits("addAnchor", height, id);
setTimeout(() => {
addNewEdge(props.model.id);
}, 50);
nextTick(() => {
changeFormItemHeight();
});
};
const addListItem = (pIndex, cIndex) => {
formData.nodeParams[pIndex].list.push({
name: "",
nameQuote: "",
nameType: "input",
type: "input",
input: "",
quote: "",
});
nextTick(() => {
changeFormItemHeight();
});
};
const removeListItem = (pIndex, cIndex) => {
if (cIndex === 0) {
return;
}
formData.nodeParams[pIndex].list.splice(cIndex, 1);
nextTick(() => {
changeFormItemHeight();
});
};
const changeFormItemHeight = () => {
formData.nodeParams.forEach((item) => {
const node = document.getElementById(item.id);
emits("changeAnchor", node.offsetTop, item.id);
});
const node = document.getElementById("".concat(props.model.id, "_else"));
emits("changeAnchor", node.offsetTop, "".concat(props.model.id, "_else"));
};
const setNodeProperties = async () => {
try {
const valid = await dynamicForm.value.validate();
if (valid) {
const data = toRaw(formData);
const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, {
...properties,
...data,
});
emits("contentChange");
}
} catch (error) {
dynamicForm.value.clearValidate();
}
};
const visibleChange = (value) => {
if (value) {
const option = getInput(props.model.id);
if (option) {
quoteOptions.value = option;
}
}
};
const nodeOperatingStatus = ref("NORMAL");
const inputJsonData = ref({});
const outputJsonData = ref({});
watch(
() => props.properties,
() => {
if (props.properties.nodeParams && props.properties.nodeParams.length > 0) {
formData.nodeParams = props.properties.nodeParams;
} else {
if (formData.nodeParams.length === 0) {
addCondition(100);
}
}
},
{
immediate: true,
deep: true,
}
);
onMounted(() => {
emits("addAnchor", 280, "".concat(props.model.id, "_else"));
emits("bindRef", dynamicForm.value);
emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) {
nodeOperatingStatus.value = data.status;
let inputData = {};
let output = {};
try {
inputData = JSON.parse(data.paramsIn) || {};
output = JSON.parse(data.paramsOut) || {};
} catch (error) {
inputData = data.paramsIn || {};
output = data.paramsOut || {};
}
inputJsonData.value = inputData;
outputJsonData.value = output;
emits("contentChange");
}
});
emitter.on("contentChange", (data) => {
if (data.id === props.model.id) {
nodeOperatingStatus.value = "NORMAL";
inputJsonData.value = {};
outputJsonData.value = {};
emits("contentChange");
}
});
});
onUnmounted(() => {
emitter.off("changeNodeState");
emitter.off("contentChange");
});
</script>
<style lang="scss" scoped>
.node__container {
width: 720px;
height: auto;
background: #fff;
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>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="菜单名称" prop="menuName">
<el-input
v-model="queryParams.menuName"
placeholder="请输入菜单名称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable style="width: 200px">
<el-option
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['system:menu:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Sort"
@click="toggleExpandAll"
>展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
v-if="refreshTable"
v-loading="loading"
:data="menuList"
row-key="menuId"
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100">
<template #default="scope">
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" width="160" prop="createTime">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="210" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']">修改</el-button>
<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-table-column>
</el-table>
<!-- 添加或修改菜单对话框 -->
<el-dialog :title="title" v-model="open" width="680px" append-to-body>
<el-form ref="menuRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="上级菜单">
<el-tree-select
v-model="form.parentId"
:data="menuOptions"
:props="{ value: 'menuId', label: 'menuName', children: 'children' }"
value-key="menuId"
placeholder="选择上级菜单"
check-strictly
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio value="M">目录</el-radio>
<el-radio value="C">菜单</el-radio>
<el-radio value="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item label="菜单图标" prop="icon">
<el-popover
placement="bottom-start"
:width="540"
trigger="click"
>
<template #reference>
<el-input v-model="form.icon" placeholder="点击选择图标" @blur="showSelectIcon" readonly>
<template #prefix>
<svg-icon
v-if="form.icon"
:icon-class="form.icon"
class="el-input__icon"
style="height: 32px;width: 16px;"
/>
<el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon>
</template>
</el-input>
</template>
<icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
</el-popover>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="routeName">
<template #label>
<span>
<el-tooltip content="默认不填则和路由地址相同:如地址为:`user`,则名称为`User`注意因为router会删除名称相同路由为避免名字的冲突特殊情况下请自定义保证唯一性" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由名称
</span>
</template>
<el-input v-model="form.routeName" placeholder="请输入路由名称" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.isFrame">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item prop="path">
<template #label>
<span>
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由地址
</span>
</template>
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="component">
<template #label>
<span>
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
组件路径
</span>
</template>
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'M'">
<el-form-item>
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
<template #label>
<span>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
权限字符
</span>
</template>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item>
<el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
<template #label>
<span>
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由参数
</span>
</template>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
是否缓存
</span>
</template>
<el-radio-group v-model="form.isCache">
<el-radio value="0">缓存</el-radio>
<el-radio value="1">不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
显示状态
</span>
</template>
<el-radio-group v-model="form.visible">
<el-radio
v-for="dict in sys_show_hide"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
菜单状态
</span>
</template>
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Menu">
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect";
const { proxy } = getCurrentInstance();
const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable");
const menuList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const title = ref("");
const menuOptions = ref([]);
const isExpandAll = ref(false);
const refreshTable = ref(true);
const iconSelectRef = ref(null);
const data = reactive({
form: {},
queryParams: {
menuName: undefined,
visible: undefined
},
rules: {
menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
},
});
const { queryParams, form, rules } = toRefs(data);
/** 查询菜单列表 */
function getList() {
loading.value = true;
listMenu(queryParams.value).then(response => {
menuList.value = proxy.handleTree(response.data, "menuId");
loading.value = false;
});
}
/** 查询菜单下拉树结构 */
function getTreeselect() {
menuOptions.value = [];
listMenu().then(response => {
const menu = { menuId: 0, menuName: "主类目", children: [] };
menu.children = proxy.handleTree(response.data, "menuId");
menuOptions.value.push(menu);
});
}
/** 取消按钮 */
function cancel() {
open.value = false;
reset();
}
/** 表单重置 */
function reset() {
form.value = {
menuId: undefined,
parentId: 0,
menuName: undefined,
icon: undefined,
menuType: "M",
orderNum: undefined,
isFrame: "1",
isCache: "0",
visible: "0",
status: "0"
};
proxy.resetForm("menuRef");
}
/** 展示下拉图标 */
function showSelectIcon() {
iconSelectRef.value.reset();
}
/** 选择图标 */
function selected(name) {
form.value.icon = name;
}
/** 搜索按钮操作 */
function handleQuery() {
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
/** 新增按钮操作 */
function handleAdd(row) {
reset();
getTreeselect();
if (row != null && row.menuId) {
form.value.parentId = row.menuId;
} else {
form.value.parentId = 0;
}
open.value = true;
title.value = "添加菜单";
}
/** 展开/折叠操作 */
function toggleExpandAll() {
refreshTable.value = false;
isExpandAll.value = !isExpandAll.value;
nextTick(() => {
refreshTable.value = true;
});
}
/** 修改按钮操作 */
async function handleUpdate(row) {
reset();
await getTreeselect();
getMenu(row.menuId).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改菜单";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["menuRef"].validate(valid => {
if (valid) {
if (form.value.menuId != undefined) {
updateMenu(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addMenu(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
proxy.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
return delMenu(row.menuId);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
getList();
</script>
<template>
<div class="app-container">
<div ref="topContainerRef">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="菜单名称" prop="menuName">
<el-input
v-model="queryParams.menuName"
placeholder="请输入菜单名称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable style="width: 200px">
<el-option
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['system:menu:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Sort"
@click="toggleExpandAll"
>展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</div>
<div :style="containerHeight">
<el-table
v-if="refreshTable"
height="100%"
v-loading="loading"
:data="menuList"
row-key="menuId"
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100">
<template #default="scope">
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" width="160" prop="createTime">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="210" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']">修改</el-button>
<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-table-column>
</el-table>
</div>
<!-- 添加或修改菜单对话框 -->
<el-dialog :title="title" v-model="open" width="680px" append-to-body>
<el-form ref="menuRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="上级菜单">
<el-tree-select
v-model="form.parentId"
:data="menuOptions"
:props="{ value: 'menuId', label: 'menuName', children: 'children' }"
value-key="menuId"
placeholder="选择上级菜单"
check-strictly
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio value="M">目录</el-radio>
<el-radio value="C">菜单</el-radio>
<el-radio value="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item label="菜单图标" prop="icon">
<el-popover
placement="bottom-start"
:width="540"
trigger="click"
>
<template #reference>
<el-input v-model="form.icon" placeholder="点击选择图标" @blur="showSelectIcon" readonly>
<template #prefix>
<svg-icon
v-if="form.icon"
:icon-class="form.icon"
class="el-input__icon"
style="height: 32px;width: 16px;"
/>
<el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon>
</template>
</el-input>
</template>
<icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
</el-popover>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="routeName">
<template #label>
<span>
<el-tooltip content="默认不填则和路由地址相同:如地址为:`user`,则名称为`User`注意因为router会删除名称相同路由为避免名字的冲突特殊情况下请自定义保证唯一性" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由名称
</span>
</template>
<el-input v-model="form.routeName" placeholder="请输入路由名称" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.isFrame">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item prop="path">
<template #label>
<span>
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由地址
</span>
</template>
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="component">
<template #label>
<span>
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
组件路径
</span>
</template>
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'M'">
<el-form-item>
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
<template #label>
<span>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
权限字符
</span>
</template>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item>
<el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
<template #label>
<span>
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由参数
</span>
</template>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
是否缓存
</span>
</template>
<el-radio-group v-model="form.isCache">
<el-radio value="0">缓存</el-radio>
<el-radio value="1">不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
显示状态
</span>
</template>
<el-radio-group v-model="form.visible">
<el-radio
v-for="dict in sys_show_hide"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
菜单状态
</span>
</template>
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Menu">
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect";
import { useContainerHeight } from "@/hooks/tableHeight";
const topContainerRef = ref();
const containerHeight = useContainerHeight(topContainerRef);
const { proxy } = getCurrentInstance();
const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable");
const menuList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const title = ref("");
const menuOptions = ref([]);
const isExpandAll = ref(false);
const refreshTable = ref(true);
const iconSelectRef = ref(null);
const data = reactive({
form: {},
queryParams: {
menuName: undefined,
visible: undefined
},
rules: {
menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
},
});
const { queryParams, form, rules } = toRefs(data);
/** 查询菜单列表 */
function getList() {
loading.value = true;
listMenu(queryParams.value).then(response => {
menuList.value = proxy.handleTree(response.data, "menuId");
loading.value = false;
});
}
/** 查询菜单下拉树结构 */
function getTreeselect() {
menuOptions.value = [];
listMenu().then(response => {
const menu = { menuId: 0, menuName: "主类目", children: [] };
menu.children = proxy.handleTree(response.data, "menuId");
menuOptions.value.push(menu);
});
}
/** 取消按钮 */
function cancel() {
open.value = false;
reset();
}
/** 表单重置 */
function reset() {
form.value = {
menuId: undefined,
parentId: 0,
menuName: undefined,
icon: undefined,
menuType: "M",
orderNum: undefined,
isFrame: "1",
isCache: "0",
visible: "0",
status: "0"
};
proxy.resetForm("menuRef");
}
/** 展示下拉图标 */
function showSelectIcon() {
iconSelectRef.value.reset();
}
/** 选择图标 */
function selected(name) {
form.value.icon = name;
}
/** 搜索按钮操作 */
function handleQuery() {
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
/** 新增按钮操作 */
function handleAdd(row) {
reset();
getTreeselect();
if (row != null && row.menuId) {
form.value.parentId = row.menuId;
} else {
form.value.parentId = 0;
}
open.value = true;
title.value = "添加菜单";
}
/** 展开/折叠操作 */
function toggleExpandAll() {
refreshTable.value = false;
isExpandAll.value = !isExpandAll.value;
nextTick(() => {
refreshTable.value = true;
});
}
/** 修改按钮操作 */
async function handleUpdate(row) {
reset();
await getTreeselect();
getMenu(row.menuId).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改菜单";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["menuRef"].validate(valid => {
if (valid) {
if (form.value.menuId != undefined) {
updateMenu(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addMenu(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
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="入参值">
<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-number v-if="row.type === 'number'" :controls="false" v-model="row.value" />
<el-switch v-if="row.type === 'boolean'" v-model="row.value" />

View File

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

View File

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