feat(flow): 更新声音类型检测功能并添加系统错误状态清除
- 修改声音事件识别为声音类型检测,调整相关参数配置 - 添加目标声音类型选择功能和预期标签匹配逻辑 - 新增机械臂故障清除API接口和操作按钮 - 实现工作流中清除机器人错误状态的功能和对话框 - 更新任务执行日志中声音分析结果显示逻辑 - 优化声音类型检测的输出参数和验证规则
This commit is contained in:
parent
f944d33d58
commit
eb056f097c
@ -15,4 +15,12 @@ export function status(data) {
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearFault(params) {
|
||||||
|
return request({
|
||||||
|
url: '/api/arm/clearFault',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, toRefs, onUnmounted, ref, onMounted } from "vue";
|
import { computed, reactive, toRefs, onUnmounted, ref, onMounted } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { CircleClose } from "@element-plus/icons-vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { clearFault as clearArmFault } from "@/api/device/arm";
|
||||||
import UrdfViewer from "./components/UrdfViewer.vue";
|
import UrdfViewer from "./components/UrdfViewer.vue";
|
||||||
import JointControl from "./components/JointControl.vue";
|
import JointControl from "./components/JointControl.vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const robotId = computed(() => String(route.query.robotId || ""));
|
||||||
|
const deviceId = computed(() => String(route.query.deviceId || ""));
|
||||||
|
const clearingFault = ref(false);
|
||||||
|
|
||||||
|
const handleClearFault = async () => {
|
||||||
|
if (!robotId.value || !deviceId.value) {
|
||||||
|
ElMessage.warning("缺少机器人或机械臂设备信息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearingFault.value = true;
|
||||||
|
try {
|
||||||
|
await clearArmFault({ robotId: robotId.value, deviceId: deviceId.value });
|
||||||
|
ElMessage.success("机械臂故障状态已清除");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("清除机械臂故障失败:", error);
|
||||||
|
} finally {
|
||||||
|
clearingFault.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
form: {
|
form: {
|
||||||
x: 11,
|
x: 11,
|
||||||
@ -160,6 +185,15 @@ onUnmounted(() => {
|
|||||||
>零位姿态</el-button
|
>零位姿态</el-button
|
||||||
>
|
>
|
||||||
<el-button @click="handleResets('init')">初始位置</el-button>
|
<el-button @click="handleResets('init')">初始位置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:loading="clearingFault"
|
||||||
|
:disabled="!robotId || !deviceId"
|
||||||
|
@click="handleClearFault"
|
||||||
|
>
|
||||||
|
<el-icon><CircleClose /></el-icon>清除故障
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pose-info flex align-center">
|
<div class="pose-info flex align-center">
|
||||||
<div class="pose-item">X: {{ form.x.toFixed(6) }} m</div>
|
<div class="pose-item">X: {{ form.x.toFixed(6) }} m</div>
|
||||||
|
|||||||
@ -17,8 +17,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-log__analysis-grid">
|
<div class="task-log__analysis-grid">
|
||||||
|
<div v-if="analysisOf(log).targetLabel" class="task-log__analysis-field">
|
||||||
|
<span>目标声音</span>
|
||||||
|
<strong>{{ analysisOf(log).targetLabel }}</strong>
|
||||||
|
</div>
|
||||||
<div v-if="analysisOf(log).label" class="task-log__analysis-field">
|
<div v-if="analysisOf(log).label" class="task-log__analysis-field">
|
||||||
<span>识别结果</span>
|
<span>{{ analysisOf(log).isAudio ? '实际声音' : '识别结果' }}</span>
|
||||||
<strong>{{ analysisOf(log).label }}</strong>
|
<strong>{{ analysisOf(log).label }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="analysisOf(log).matchLabel" class="task-log__analysis-field">
|
<div v-if="analysisOf(log).matchLabel" class="task-log__analysis-field">
|
||||||
@ -144,10 +148,11 @@ const analysisOf = (log) => {
|
|||||||
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
||||||
'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
||||||
'confidence', 'passed', 'summary', 'conclusion', 'events', '_executionContext',
|
'confidence', 'passed', 'summary', 'conclusion', 'events', '_executionContext',
|
||||||
|
'expectedLabel', 'expectedLabelName', 'decisionReason',
|
||||||
].forEach((key) => delete detailSource[key])
|
].forEach((key) => delete detailSource[key])
|
||||||
const hasDetails = Object.keys(detailSource).length > 0
|
const hasDetails = Object.keys(detailSource).length > 0
|
||||||
const result = {
|
const result = {
|
||||||
title: isAudio ? '声音分析' : '视频分析',
|
title: isAudio ? '声音类型检测' : '视频分析',
|
||||||
statusLabel,
|
statusLabel,
|
||||||
tagType: status === 'FAILED' || status === 'ERROR' || passed === false
|
tagType: status === 'FAILED' || status === 'ERROR' || passed === false
|
||||||
? 'danger'
|
? 'danger'
|
||||||
@ -157,10 +162,12 @@ const analysisOf = (log) => {
|
|||||||
: output.analysisModel?.name || '',
|
: output.analysisModel?.name || '',
|
||||||
timingMs: Number.isFinite(Number(output.analysisTimingMs)) ? Number(output.analysisTimingMs) : null,
|
timingMs: Number.isFinite(Number(output.analysisTimingMs)) ? Number(output.analysisTimingMs) : null,
|
||||||
label: output.labelName || output.label || '',
|
label: output.labelName || output.label || '',
|
||||||
matchLabel: matched === true ? '匹配' : matched === false ? '未匹配' : '',
|
targetLabel: output.expectedLabelName || output.expectedLabel || '',
|
||||||
|
matchLabel: isAudio ? '' : matched === true ? '匹配' : matched === false ? '未匹配' : '',
|
||||||
confidence,
|
confidence,
|
||||||
passedLabel: passed === true ? '通过' : passed === false ? '未通过' : '',
|
passedLabel: passed === true ? '通过' : passed === false ? '未通过' : '',
|
||||||
summary: output.summary || output.conclusion || '',
|
summary: output.decisionReason || output.summary || output.conclusion || '',
|
||||||
|
isAudio,
|
||||||
events: Array.isArray(output.events)
|
events: Array.isArray(output.events)
|
||||||
? output.events.slice(0, 20).map((event) => typeof event === 'string' ? event : JSON.stringify(event))
|
? output.events.slice(0, 20).map((event) => typeof event === 'string' ? event : JSON.stringify(event))
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@ -29,6 +29,26 @@
|
|||||||
<div class="field-hint">场景决定识别标签、判断阈值和分析要求。</div>
|
<div class="field-hint">场景决定识别标签、判断阈值和分析要求。</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="!isVideo" label="目标声音类型" prop="expectedLabel">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.expectedLabel"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="请选择目标声音类型"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleExpectedLabelChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in soundTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div class="field-hint">只有实际声音与目标类型一致时才通过;新增类型可直接输入编码或名称。</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item :label="mediaLabel" prop="mediaMode">
|
<el-form-item :label="mediaLabel" prop="mediaMode">
|
||||||
<el-segmented v-model="formData.mediaMode" :options="mediaModes" />
|
<el-segmented v-model="formData.mediaMode" :options="mediaModes" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -126,6 +146,8 @@ const analysisPresets = {
|
|||||||
}
|
}
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
profileCode: '',
|
profileCode: '',
|
||||||
|
expectedLabel: '',
|
||||||
|
expectedLabelName: '',
|
||||||
mediaMode: 'quote',
|
mediaMode: 'quote',
|
||||||
mediaQuote: [],
|
mediaQuote: [],
|
||||||
mediaQuoteType: 'output',
|
mediaQuoteType: 'output',
|
||||||
@ -136,10 +158,10 @@ const formData = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isVideo = computed(() => props.data?.properties?.action === 'VIDEO_ANALYZE')
|
const isVideo = computed(() => props.data?.properties?.action === 'VIDEO_ANALYZE')
|
||||||
const title = computed(() => isVideo.value ? '视频智能分析' : '声音事件识别')
|
const title = computed(() => isVideo.value ? '视频智能分析' : '声音类型检测')
|
||||||
const description = computed(() => isVideo.value
|
const description = computed(() => isVideo.value
|
||||||
? '对录像抽帧并按所选场景生成结构化分析结果。'
|
? '对录像抽帧并按所选场景生成结构化分析结果。'
|
||||||
: '将录音与场景中的多类参考声音比较,低于阈值时返回“其他声音”。')
|
: '判断录音是否属于指定的目标声音类型,并输出通过或不通过。')
|
||||||
const mediaLabel = computed(() => isVideo.value ? '视频来源' : '音频来源')
|
const mediaLabel = computed(() => isVideo.value ? '视频来源' : '音频来源')
|
||||||
const mediaName = computed(() => isVideo.value ? 'videoUrl' : 'audioUrl')
|
const mediaName = computed(() => isVideo.value ? 'videoUrl' : 'audioUrl')
|
||||||
const mediaModes = [
|
const mediaModes = [
|
||||||
@ -151,6 +173,13 @@ const analysisModes = [
|
|||||||
{ label: '均衡', value: 'AUTO' },
|
{ label: '均衡', value: 'AUTO' },
|
||||||
{ label: '精确', value: 'ACCURATE' },
|
{ label: '精确', value: 'ACCURATE' },
|
||||||
]
|
]
|
||||||
|
const soundTypeOptions = [
|
||||||
|
{ label: '开机', value: 'POWER_ON' },
|
||||||
|
{ label: '关机', value: 'POWER_OFF' },
|
||||||
|
{ label: '设防', value: 'ARMED' },
|
||||||
|
{ label: '解防', value: 'DISARMED' },
|
||||||
|
{ label: '鸣笛寻车', value: 'FIND_VEHICLE_HORN' },
|
||||||
|
]
|
||||||
const modeDescriptions = {
|
const modeDescriptions = {
|
||||||
FAST: '使用轻量模型直接判断,适合动作明显、批量执行的场景。',
|
FAST: '使用轻量模型直接判断,适合动作明显、批量执行的场景。',
|
||||||
AUTO: '先快速判断,低置信度时自动切换精确分析。',
|
AUTO: '先快速判断,低置信度时自动切换精确分析。',
|
||||||
@ -163,7 +192,7 @@ const isTuned = computed(() => {
|
|||||||
})
|
})
|
||||||
const profileOptions = computed(() => isVideo.value
|
const profileOptions = computed(() => isVideo.value
|
||||||
? [{ label: '爱玛测试视频分析', value: 'aima.power_video.v1' }]
|
? [{ label: '爱玛测试视频分析', value: 'aima.power_video.v1' }]
|
||||||
: [{ label: '爱玛车辆声音事件识别', value: 'aima.power_state.v1' }])
|
: [{ label: '爱玛车辆声音类型检测', value: 'aima.power_state.v1' }])
|
||||||
const outputSummary = computed(() => isVideo.value
|
const outputSummary = computed(() => isVideo.value
|
||||||
? [
|
? [
|
||||||
{ name: 'passed', desc: '是否通过' },
|
{ name: 'passed', desc: '是否通过' },
|
||||||
@ -176,15 +205,23 @@ const outputSummary = computed(() => isVideo.value
|
|||||||
{ name: 'fallback', desc: '是否切换到精确分析' },
|
{ name: 'fallback', desc: '是否切换到精确分析' },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ name: 'label', desc: '场景分类编码,未匹配时为 UNKNOWN' },
|
{ name: 'passed', desc: '是否检测到目标声音' },
|
||||||
{ name: 'labelName', desc: '结果名称' },
|
{ name: 'expectedLabelName', desc: '目标声音类型' },
|
||||||
{ name: 'matched', desc: '是否达到匹配阈值' },
|
{ name: 'labelName', desc: '实际声音类型' },
|
||||||
|
{ name: 'matched', desc: '是否识别到已知声音' },
|
||||||
{ name: 'confidence', desc: '最高匹配度' },
|
{ name: 'confidence', desc: '最高匹配度' },
|
||||||
|
{ name: 'decisionReason', desc: '检测结论' },
|
||||||
{ name: 'scores', desc: '各分类匹配度' },
|
{ name: 'scores', desc: '各分类匹配度' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
profileCode: [{ required: true, message: '请选择或输入分析场景', trigger: 'change' }],
|
profileCode: [{ required: true, message: '请选择或输入分析场景', trigger: 'change' }],
|
||||||
|
expectedLabel: [{
|
||||||
|
validator: (_, value) => isVideo.value || String(value || '').trim()
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error('请选择目标声音类型')),
|
||||||
|
trigger: 'change',
|
||||||
|
}],
|
||||||
mediaQuote: [{
|
mediaQuote: [{
|
||||||
validator: (_, value) => formData.mediaMode !== 'quote' || (Array.isArray(value) && value.length)
|
validator: (_, value) => formData.mediaMode !== 'quote' || (Array.isArray(value) && value.length)
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
@ -211,6 +248,11 @@ const restorePreset = () => {
|
|||||||
|
|
||||||
const handleAnalysisModeChange = () => restorePreset()
|
const handleAnalysisModeChange = () => restorePreset()
|
||||||
|
|
||||||
|
const handleExpectedLabelChange = (value) => {
|
||||||
|
const option = soundTypeOptions.find(item => item.value === value)
|
||||||
|
formData.expectedLabelName = option?.label || String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
const parseStoredTuning = (value) => {
|
const parseStoredTuning = (value) => {
|
||||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value
|
if (value && typeof value === 'object' && !Array.isArray(value)) return value
|
||||||
if (typeof value !== 'string' || !value.trim()) return {}
|
if (typeof value !== 'string' || !value.trim()) return {}
|
||||||
@ -251,11 +293,18 @@ const handleQuoteChange = () => {
|
|||||||
const initData = () => {
|
const initData = () => {
|
||||||
const params = props.data?.properties?.nodeParams || []
|
const params = props.data?.properties?.nodeParams || []
|
||||||
const profile = params.find(item => item.name === 'profileCode')
|
const profile = params.find(item => item.name === 'profileCode')
|
||||||
|
const expectedLabel = params.find(item => item.name === 'expectedLabel')
|
||||||
|
const expectedLabelName = params.find(item => item.name === 'expectedLabelName')
|
||||||
const media = params.find(item => item.name === mediaName.value)
|
const media = params.find(item => item.name === mediaName.value)
|
||||||
const instruction = params.find(item => item.name === 'instruction')
|
const instruction = params.find(item => item.name === 'instruction')
|
||||||
const analysisMode = params.find(item => item.name === 'analysisMode')
|
const analysisMode = params.find(item => item.name === 'analysisMode')
|
||||||
const analysisTuning = params.find(item => item.name === 'analysisTuning')
|
const analysisTuning = params.find(item => item.name === 'analysisTuning')
|
||||||
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
||||||
|
formData.expectedLabel = expectedLabel?.input || ''
|
||||||
|
formData.expectedLabelName = expectedLabelName?.input || ''
|
||||||
|
if (formData.expectedLabel && !formData.expectedLabelName) {
|
||||||
|
handleExpectedLabelChange(formData.expectedLabel)
|
||||||
|
}
|
||||||
formData.mediaMode = media?.type === 'input' ? 'input' : 'quote'
|
formData.mediaMode = media?.type === 'input' ? 'input' : 'quote'
|
||||||
formData.mediaQuote = Array.isArray(media?.quote) ? [...media.quote] : []
|
formData.mediaQuote = Array.isArray(media?.quote) ? [...media.quote] : []
|
||||||
formData.mediaQuoteType = media?.quoteType || 'output'
|
formData.mediaQuoteType = media?.quoteType || 'output'
|
||||||
@ -292,6 +341,26 @@ const validateAndSave = async () => {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
if (!isVideo.value) {
|
||||||
|
nodeParams.splice(1, 0,
|
||||||
|
{
|
||||||
|
name: 'expectedLabel',
|
||||||
|
type: 'input',
|
||||||
|
input: formData.expectedLabel.trim(),
|
||||||
|
required: true,
|
||||||
|
disabled: true,
|
||||||
|
componentType: 'select',
|
||||||
|
selectOptions: soundTypeOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expectedLabelName',
|
||||||
|
type: 'input',
|
||||||
|
input: formData.expectedLabelName.trim() || formData.expectedLabel.trim(),
|
||||||
|
required: false,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
if (isVideo.value) {
|
if (isVideo.value) {
|
||||||
nodeParams.push({
|
nodeParams.push({
|
||||||
name: 'instruction',
|
name: 'instruction',
|
||||||
|
|||||||
@ -516,21 +516,27 @@ function handler(params) {
|
|||||||
nodeList: [
|
nodeList: [
|
||||||
{
|
{
|
||||||
icon: microphoneSvg,
|
icon: microphoneSvg,
|
||||||
name: "声音事件识别",
|
name: "声音类型检测",
|
||||||
type: "mediaAnalysis",
|
type: "mediaAnalysis",
|
||||||
desc: "将录音与分析场景中的多类参考声音进行匹配",
|
desc: "判断录音是否属于指定的声音类型",
|
||||||
action: "AUDIO_EVENT_CLASSIFY",
|
action: "AUDIO_EVENT_CLASSIFY",
|
||||||
nodeType: "LLM",
|
nodeType: "LLM",
|
||||||
nodeParams: [
|
nodeParams: [
|
||||||
{ name: "profileCode", type: "input", input: "aima.power_state.v1", required: true, disabled: true },
|
{ name: "profileCode", type: "input", input: "aima.power_state.v1", required: true, disabled: true },
|
||||||
|
{ name: "expectedLabel", type: "input", input: "", required: true, disabled: true },
|
||||||
|
{ name: "expectedLabelName", type: "input", input: "", required: false, disabled: true },
|
||||||
{ name: "audioUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
{ name: "audioUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
||||||
],
|
],
|
||||||
outputType: "json",
|
outputType: "json",
|
||||||
outputParams: [
|
outputParams: [
|
||||||
{ name: "label", type: "string", desc: "场景分类编码,未匹配时为UNKNOWN", disabled: true },
|
{ name: "passed", type: "boolean", desc: "是否检测到目标声音", disabled: true },
|
||||||
{ name: "labelName", type: "string", desc: "分类名称", disabled: true },
|
{ name: "expectedLabel", type: "string", desc: "目标声音类型编码", disabled: true },
|
||||||
{ name: "matched", type: "boolean", desc: "是否达到匹配阈值", disabled: true },
|
{ name: "expectedLabelName", type: "string", desc: "目标声音类型名称", disabled: true },
|
||||||
|
{ name: "label", type: "string", desc: "实际声音类型编码,未匹配时为UNKNOWN", disabled: true },
|
||||||
|
{ name: "labelName", type: "string", desc: "实际声音类型名称", disabled: true },
|
||||||
|
{ name: "matched", type: "boolean", desc: "是否识别到已知声音", disabled: true },
|
||||||
{ name: "confidence", type: "number", desc: "最高匹配度", disabled: true },
|
{ name: "confidence", type: "number", desc: "最高匹配度", disabled: true },
|
||||||
|
{ name: "decisionReason", type: "string", desc: "检测结论", disabled: true },
|
||||||
{ name: "scores", type: "object", desc: "各分类匹配度", children: [], disabled: true },
|
{ name: "scores", type: "object", desc: "各分类匹配度", children: [], disabled: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,6 +21,15 @@
|
|||||||
<el-button v-if="hasRunResult" @click="clearRun">
|
<el-button v-if="hasRunResult" @click="clearRun">
|
||||||
<el-icon><Delete /></el-icon>清除结果
|
<el-icon><Delete /></el-icon>清除结果
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
:loading="recoverSafetyLoading"
|
||||||
|
:disabled="flowState === 'testRunning'"
|
||||||
|
@click="openRecoverSafetyDialog"
|
||||||
|
>
|
||||||
|
<el-icon><Warning /></el-icon>清除错误状态
|
||||||
|
</el-button>
|
||||||
<el-button :type="groupSelectionPending ? 'primary' : 'default'" @click="group">
|
<el-button :type="groupSelectionPending ? 'primary' : 'default'" @click="group">
|
||||||
<el-icon><CollectionTag /></el-icon>{{ groupSelectionPending ? "取消分组" : "分组" }}
|
<el-icon><CollectionTag /></el-icon>{{ groupSelectionPending ? "取消分组" : "分组" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -163,6 +172,39 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="recoverSafetyDialogVisible" title="清除错误状态" width="480" append-to-body>
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="机器人">
|
||||||
|
<el-select
|
||||||
|
v-model="recoverSafetyRobotIds"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
|
:loading="recoverRobotLoading"
|
||||||
|
placeholder="请选择需要恢复的在线机器人"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in recoverRobotOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="recoverSafetyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
:loading="recoverSafetyLoading"
|
||||||
|
:disabled="recoverSafetyRobotIds.length === 0"
|
||||||
|
@click="confirmRecoverSafetyState"
|
||||||
|
>确认清除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="visible" :width="nodeAction === 'SYSTEM_EXECUTE_ACTION_QUEUE' ? '620' : '500'" class="single-node-dialog" append-to-body>
|
<el-dialog v-model="visible" :width="nodeAction === 'SYSTEM_EXECUTE_ACTION_QUEUE' ? '620' : '500'" class="single-node-dialog" append-to-body>
|
||||||
<template #header="{ close, titleId, titleClass }">
|
<template #header="{ close, titleId, titleClass }">
|
||||||
<div class="my-header">
|
<div class="my-header">
|
||||||
@ -279,7 +321,7 @@ import TestRun from "./components/TestRun.vue";
|
|||||||
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
||||||
import Aside from "./components/Aside.vue";
|
import Aside from "./components/Aside.vue";
|
||||||
import ActionQueueSingleRun from "./components/ActionQueueSingleRun.vue";
|
import ActionQueueSingleRun from "./components/ActionQueueSingleRun.vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { useFlowStore } from "@/store/modules/flow";
|
import { useFlowStore } from "@/store/modules/flow";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import { convertToTree } from "@/utils/flow";
|
import { convertToTree } from "@/utils/flow";
|
||||||
@ -308,6 +350,7 @@ import {
|
|||||||
} from "@/api/flow/flow";
|
} from "@/api/flow/flow";
|
||||||
import { getDetect } from "@/api/test/detect";
|
import { getDetect } from "@/api/test/detect";
|
||||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||||
|
import { recoverSafetyState } from '@/api/device/system'
|
||||||
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
||||||
import {
|
import {
|
||||||
CircleClose,
|
CircleClose,
|
||||||
@ -321,6 +364,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
VideoPause,
|
VideoPause,
|
||||||
VideoPlay,
|
VideoPlay,
|
||||||
|
Warning,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const flowContainerRef = ref(null);
|
const flowContainerRef = ref(null);
|
||||||
@ -333,6 +377,76 @@ const queueDropTargetId = ref("");
|
|||||||
const { onFlowEvent } = useFlowEvents();
|
const { onFlowEvent } = useFlowEvents();
|
||||||
window.lf = lf;
|
window.lf = lf;
|
||||||
|
|
||||||
|
const recoverSafetyDialogVisible = ref(false);
|
||||||
|
const recoverRobotLoading = ref(false);
|
||||||
|
const recoverSafetyLoading = ref(false);
|
||||||
|
const recoverRobotOptions = ref([]);
|
||||||
|
const recoverSafetyRobotIds = ref([]);
|
||||||
|
|
||||||
|
const openRecoverSafetyDialog = async () => {
|
||||||
|
recoverSafetyDialogVisible.value = true;
|
||||||
|
recoverRobotLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
||||||
|
recoverRobotOptions.value = (response.rows || []).filter((robot) => robot.robotId).map((robot) => ({
|
||||||
|
value: robot.robotId,
|
||||||
|
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
||||||
|
}));
|
||||||
|
recoverSafetyRobotIds.value = recoverSafetyRobotIds.value.filter((robotId) => (
|
||||||
|
recoverRobotOptions.value.some((option) => option.value === robotId)
|
||||||
|
));
|
||||||
|
if (!recoverSafetyRobotIds.value.length && recoverRobotOptions.value.length === 1) {
|
||||||
|
recoverSafetyRobotIds.value = [recoverRobotOptions.value[0].value];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载在线机器人失败:", error);
|
||||||
|
recoverRobotOptions.value = [];
|
||||||
|
} finally {
|
||||||
|
recoverRobotLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmRecoverSafetyState = async () => {
|
||||||
|
if (!recoverSafetyRobotIds.value.length) {
|
||||||
|
ElMessage.warning("请选择需要恢复的机器人");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`将清除 ${recoverSafetyRobotIds.value.length} 台机器人的可恢复错误状态,确认继续?`,
|
||||||
|
"安全状态确认",
|
||||||
|
{ type: "warning", confirmButtonText: "确认清除", cancelButtonText: "取消" },
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recoverSafetyLoading.value = true;
|
||||||
|
const robotIds = [...recoverSafetyRobotIds.value];
|
||||||
|
try {
|
||||||
|
const results = await Promise.allSettled(robotIds.map((robotId) => recoverSafetyState({
|
||||||
|
robotId,
|
||||||
|
reason: "工作流设计器手动清除错误状态",
|
||||||
|
timeoutMs: 15000,
|
||||||
|
})));
|
||||||
|
const failedRobotIds = results.flatMap((result, index) => (
|
||||||
|
result.status === "rejected" ? [robotIds[index]] : []
|
||||||
|
));
|
||||||
|
if (!failedRobotIds.length) {
|
||||||
|
ElMessage.success(`已清除 ${robotIds.length} 台机器人的可恢复错误状态`);
|
||||||
|
recoverSafetyDialogVisible.value = false;
|
||||||
|
recoverSafetyRobotIds.value = [];
|
||||||
|
} else {
|
||||||
|
const successCount = robotIds.length - failedRobotIds.length;
|
||||||
|
recoverSafetyRobotIds.value = failedRobotIds;
|
||||||
|
ElMessage.error(successCount
|
||||||
|
? `${successCount} 台成功,${failedRobotIds.length} 台失败,请重试失败项`
|
||||||
|
: "清除错误状态失败,请检查机器人状态");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
recoverSafetyLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const defaultEdgeOptions = {
|
const defaultEdgeOptions = {
|
||||||
type: "flow-edge",
|
type: "flow-edge",
|
||||||
animated: true,
|
animated: true,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user