cmvr-iot-common/src/views/flow/nodes/common/serviceNode.vue

228 lines
6.3 KiB
Vue
Raw Normal View History

2026-08-25 10:15:37 +08:00
<!--
* 文件说明:通用服务节点并响应运行状态。
* 作用范围:仅服务于流程设计器模块。
-->
<template>
<div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #actions>
<el-button
v-if="audioCorrectionAvailable"
class="node-audio-correction-button nodrag nopan"
type="primary"
link
size="small"
@click="audioCorrectionVisible = true"
>
{{ outputJsonData.manualCorrected ? '重新判断' : '人工判断' }}
</el-button>
</template>
2026-08-25 10:15:37 +08:00
<template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template>
<template #output>
<div v-if="nodeOperatingStatus != 'FAILED'">
<JsonViewer
v-if="!['img', 'video', 'audio'].includes(props.properties.outputType)"
: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"
/>
<audio
v-for="item in audioUrls"
:key="item"
class="node-audio-player"
:src="item"
controls
preload="metadata"
/>
</div>
<div v-else>{{ errorInfoData }}</div>
</template>
</NodeState>
<AudioCorrectionDialog
v-model="audioCorrectionVisible"
:node-inst-id="nodeInstId"
:value="outputJsonData"
@saved="applyAudioCorrection"
/>
2026-08-25 10:15:37 +08:00
<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"
@setNodeName="setNodeName"
/>
<!-- @zoom="zoom" -->
</div>
</template>
<script setup>
import { computed, ref, onMounted } from "vue";
import NodeTitle from "../../components/NodeTitle.vue";
import NodeState from "../../components/NodeState.vue";
import "vue3-json-viewer/dist/index.css";
import { useFlowEvents } from "../../composables/useFlowEvents";
import IPlayer from "@/components/IPlayer/index.vue";
import AudioCorrectionDialog from "../../components/AudioCorrectionDialog.vue";
2026-08-25 10:15:37 +08:00
const { onFlowEvent } = useFlowEvents();
const props = defineProps({
model: Object,
properties: Object,
});
const emits = defineEmits(["contentChange"]);
/**
* 设置 setNodeName 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} name 名称或事件名称
*/
const setNodeName = (name) => {
const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, {
...properties,
name
});
emits("contentChange");
}
const nodeOperatingStatus = ref("NORMAL");
const runtimes = ref(0);
const inputJsonData = ref({});
const outputJsonData = ref({});
const errorInfoData = ref('');
const executionLog = ref(null);
const audioCorrectionVisible = ref(false);
const nodeInstIdOf = (log) => log?.nodeInstId
|| log?.nodeInstanceId
|| log?.executionNodeInstId
|| log?.id
|| '';
const nodeInstId = computed(() => nodeInstIdOf(executionLog.value));
const audioCorrectionAvailable = computed(() => (
String(props.properties?.action || '').toUpperCase() === 'AUDIO_EVENT_CLASSIFY'
&& nodeOperatingStatus.value !== 'NORMAL'
&& Boolean(nodeInstId.value)
));
const applyAudioCorrection = (correction) => {
outputJsonData.value = { ...outputJsonData.value, ...correction };
if (!executionLog.value) return;
const currentOutput = parseRuntimeValue(executionLog.value.paramsOut);
executionLog.value.paramsOut = JSON.stringify({ ...currentOutput, ...correction });
};
const parseRuntimeValue = (value) => {
if (!value) return {};
if (typeof value === 'object' && !Array.isArray(value)) return value;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
};
2026-08-25 10:15:37 +08:00
const audioUrls = computed(() => {
const value = outputJsonData.value?.audioUrl;
if (Array.isArray(value)) return value.filter(Boolean);
if (typeof value === "string" && value) return [value];
return [];
});
const nodeStateRef = ref(null);
/**
* 关闭 closePopover 对应的数据或交互;作用范围仅限当前组件或模块。
*/
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => {
onFlowEvent("changeNodeState", (data) => {
if (data.nodeId === props.model.id) {
executionLog.value = data;
2026-08-25 10:15:37 +08:00
nodeOperatingStatus.value = data.status;
if (data.endTime && data.startTime) {
runtimes.value = data.endTime - data.startTime;
}
let inputData = {};
let output = {};
errorInfoData.value = data?.message || ''
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");
}
});
onFlowEvent("contentChange", (data) => {
if (data.id === props.model.id) {
nodeOperatingStatus.value = "NORMAL";
inputJsonData.value = {};
outputJsonData.value = {};
executionLog.value = null;
2026-08-25 10:15:37 +08:00
emits("contentChange");
}
});
onFlowEvent("setProperties", (data) => {
if (data.id === props.model.id) {
emits("contentChange");
}
});
});
defineExpose({ closePopover })
</script>
<style lang="scss" scoped>
.node__container {
width: 100%;
height: auto;
}
.node-audio-player {
width: min(320px, 100%);
height: 36px;
}
.node-audio-correction-button {
margin-left: 4px;
}
2026-08-25 10:15:37 +08:00
</style>