feat: 实现设备模拟功能并优化相关页面
- 新增设备模拟相关的 API 接口 - 实现设备属性上报、功能下发和事件上报 - 优化设备详情页面,增加实时数据订阅和设备状态订阅 - 改进物模型编辑页面,支持步长设置和固定密码校验 - 重构部分组件以支持新功能
This commit is contained in:
parent
b2a8aa545e
commit
a465fa498a
|
@ -22,4 +22,7 @@ VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuP
|
||||||
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||||
|
|
||||||
# 开启SSE
|
# 开启SSE
|
||||||
VITE_GLOB_SSE_ENABLE=true
|
VITE_GLOB_SSE_ENABLE=false
|
||||||
|
#开始websocket
|
||||||
|
VITE_APP_WEBSOCKET=true
|
||||||
|
|
||||||
|
|
|
@ -28,5 +28,7 @@ VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuP
|
||||||
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||||
|
|
||||||
# 开启SSE
|
# 开启SSE
|
||||||
VITE_GLOB_SSE_ENABLE=true
|
VITE_GLOB_SSE_ENABLE=false
|
||||||
|
#开始websocket
|
||||||
|
VITE_APP_WEBSOCKET=true
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
|
"rxjs": "^7.8.2",
|
||||||
"tinymce": "^7.3.0",
|
"tinymce": "^7.3.0",
|
||||||
"unplugin-vue-components": "^0.27.3",
|
"unplugin-vue-components": "^0.27.3",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
|
|
|
@ -60,3 +60,32 @@ export function deviceUpdate(data: DeviceForm) {
|
||||||
export function deviceRemove(id: ID | IDS) {
|
export function deviceRemove(id: ID | IDS) {
|
||||||
return requestClient.deleteWithMsg<void>(`/device/device/${id}`);
|
return requestClient.deleteWithMsg<void>(`/device/device/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设备模拟
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备属性上报
|
||||||
|
* @param data
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function deviceOperateReport(data: any) {
|
||||||
|
return requestClient.postWithMsg<void>('/device/operate/mockReport', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备功能下发
|
||||||
|
* @param data
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function deviceOperateFunc(data: any) {
|
||||||
|
return requestClient.postWithMsg<void>('/device/operate/func', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备事件上报
|
||||||
|
* @param data
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function deviceOperateEvent(data: any) {
|
||||||
|
return requestClient.postWithMsg<void>('/device/operate/mockEvent', data);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
import { SvgMessageUrl } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { notification } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { useNotifyStore } from '#/store/notify';
|
||||||
|
|
||||||
|
// 全局变量(原生 WebSocket 单例)
|
||||||
|
let ws: null | WebSocket = null;
|
||||||
|
let reconnectCount = 0; // 重连计数
|
||||||
|
let reconnectTimer: any = null;
|
||||||
|
let heartTimer: any = null;
|
||||||
|
let lockReconnect = false; // 避免重复连接
|
||||||
|
const maxReconnect = 100; // 重连总次数
|
||||||
|
const backoffBaseMs = 5000;
|
||||||
|
|
||||||
|
// 订阅管理:requestId -> subscriber handlers
|
||||||
|
const subs: Record<
|
||||||
|
string,
|
||||||
|
Array<{ complete: () => void; next: (v: any) => void }>
|
||||||
|
> = {};
|
||||||
|
// 未连接时缓存要发送的消息
|
||||||
|
const tempQueue: string[] = [];
|
||||||
|
|
||||||
|
// 初始化 socket(原生 WebSocket)
|
||||||
|
export const initWebSocket = () => {
|
||||||
|
if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ws) {
|
||||||
|
return ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiURL, clientId } = useAppConfig(
|
||||||
|
import.meta.env,
|
||||||
|
import.meta.env.PROD,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const token = encodeURIComponent(accessStore.accessToken || '');
|
||||||
|
|
||||||
|
const url = `${document.location.protocol.replace('http', 'ws')}//${document.location.host}${apiURL}/resource/websocket?Authorization=Bearer ${token}&clientid=${clientId}`;
|
||||||
|
|
||||||
|
if (reconnectCount >= maxReconnect) return;
|
||||||
|
reconnectCount += 1;
|
||||||
|
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
|
||||||
|
ws.addEventListener('open', () => {
|
||||||
|
reconnectCount = 0;
|
||||||
|
// 心跳
|
||||||
|
heartTimer && clearInterval(heartTimer);
|
||||||
|
heartTimer = setInterval(() => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ping' }));
|
||||||
|
}
|
||||||
|
}, 10_000);
|
||||||
|
|
||||||
|
// flush queue
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN && tempQueue.length > 0) {
|
||||||
|
while (tempQueue.length > 0) {
|
||||||
|
const payload = tempQueue.pop();
|
||||||
|
if (payload) ws.send(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('close', () => {
|
||||||
|
ws = null;
|
||||||
|
reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('error', () => {
|
||||||
|
ws = null;
|
||||||
|
reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('message', (e: MessageEvent<string>) => {
|
||||||
|
const raw = e.data;
|
||||||
|
if (typeof raw === 'string' && raw.includes('ping')) return;
|
||||||
|
|
||||||
|
let data: any;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.type === 'error') {
|
||||||
|
notification.error({
|
||||||
|
key: 'ws-error',
|
||||||
|
message: data.message || 'WebSocket错误',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知类消息(可选)
|
||||||
|
if (data?.type === 'notification') {
|
||||||
|
useNotifyStore().notificationList.unshift({
|
||||||
|
message: data.message || raw,
|
||||||
|
avatar: SvgMessageUrl,
|
||||||
|
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
isRead: false,
|
||||||
|
title: $t('component.notice.title'),
|
||||||
|
userId: userStore.userInfo?.userId || '0',
|
||||||
|
});
|
||||||
|
notification.success({
|
||||||
|
description: data.message || raw,
|
||||||
|
duration: 3,
|
||||||
|
message: $t('component.notice.received'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId: string =
|
||||||
|
typeof data?.requestId === 'string' ? data.requestId : '';
|
||||||
|
if (!requestId) return;
|
||||||
|
|
||||||
|
const list = subs[requestId];
|
||||||
|
if (!Array.isArray(list) || list.length === 0) return;
|
||||||
|
|
||||||
|
if (data.type === 'complete') {
|
||||||
|
list.forEach((s) => s.complete());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.type === 'result') {
|
||||||
|
list.forEach((s) => s.next(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
|
||||||
|
function reconnect() {
|
||||||
|
heartTimer && clearInterval(heartTimer);
|
||||||
|
if (lockReconnect) return;
|
||||||
|
lockReconnect = true;
|
||||||
|
|
||||||
|
reconnectTimer && clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
initWebSocket();
|
||||||
|
lockReconnect = false;
|
||||||
|
}, backoffBaseMs * reconnectCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWebSocket = (
|
||||||
|
id: string,
|
||||||
|
topic: string,
|
||||||
|
parameter: Record<string, any>,
|
||||||
|
) =>
|
||||||
|
new Observable<any>(
|
||||||
|
(subscriber: { complete: () => void; next: (v: any) => void }) => {
|
||||||
|
if (!subs[id]) subs[id] = [];
|
||||||
|
const handle = {
|
||||||
|
next: (val: any) => subscriber.next(val),
|
||||||
|
complete: () => subscriber.complete(),
|
||||||
|
};
|
||||||
|
subs[id].push(handle);
|
||||||
|
|
||||||
|
const msg = JSON.stringify({ id, topic, parameter, type: 'sub' });
|
||||||
|
initWebSocket();
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(msg);
|
||||||
|
} else {
|
||||||
|
tempQueue.push(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const unsub = JSON.stringify({ id, type: 'unsub' });
|
||||||
|
const list = subs[id];
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
const idx = list.indexOf(handle);
|
||||||
|
if (idx !== -1) list.splice(idx, 1);
|
||||||
|
if (list.length === 0) subs[id] = [];
|
||||||
|
}
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(unsub);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const closeWs = () => {
|
||||||
|
heartTimer && clearInterval(heartTimer);
|
||||||
|
reconnectTimer && clearTimeout(reconnectTimer);
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -55,7 +55,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '所属产品',
|
title: '所属产品',
|
||||||
field: 'productName',
|
field: 'productObj.productName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '设备类型',
|
title: '设备类型',
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { TabPane, Tabs } from 'ant-design-vue';
|
import { TabPane, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import EventSimulation from './simulation/EventSimulation.vue';
|
||||||
import FunctionSimulation from './simulation/FunctionSimulation.vue';
|
import FunctionSimulation from './simulation/FunctionSimulation.vue';
|
||||||
|
import PropertySimulation from './simulation/PropertySimulation.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
@ -13,20 +15,34 @@ interface Props {
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
// 当前激活的tab
|
// 当前激活的tab
|
||||||
const activeTab = ref('function');
|
const activeTab = ref('property');
|
||||||
|
|
||||||
// 获取物模型数据,优先使用设备信息中的物模型,否则使用模拟数据
|
// 获取物模型数据,优先使用设备信息中的物模型,否则使用模拟数据
|
||||||
const getMetadata = () => {
|
const getMetadata = () => {
|
||||||
try {
|
try {
|
||||||
const raw = props.deviceInfo?.productObj?.metadata;
|
const raw = props.deviceInfo?.productObj?.metadata;
|
||||||
if (!raw) return { functions: [] } as any;
|
if (!raw)
|
||||||
|
return {
|
||||||
|
functions: [],
|
||||||
|
properties: [],
|
||||||
|
propertyGroups: [],
|
||||||
|
events: [],
|
||||||
|
} as any;
|
||||||
const obj = JSON.parse(raw || '{}');
|
const obj = JSON.parse(raw || '{}');
|
||||||
return {
|
return {
|
||||||
functions: obj?.functions || [],
|
functions: obj?.functions || [],
|
||||||
|
properties: obj?.properties || [],
|
||||||
|
propertyGroups: obj?.propertyGroups || [],
|
||||||
|
events: obj?.events || [],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('parse metadata error', error);
|
console.warn('parse metadata error', error);
|
||||||
return { functions: [] } as any;
|
return {
|
||||||
|
functions: [],
|
||||||
|
properties: [],
|
||||||
|
propertyGroups: [],
|
||||||
|
events: [],
|
||||||
|
} as any;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,13 +53,22 @@ const metadata = computed(() => getMetadata());
|
||||||
const functionList = computed(() => {
|
const functionList = computed(() => {
|
||||||
return metadata.value.functions || [];
|
return metadata.value.functions || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 事件列表
|
||||||
|
const eventList = computed(() => {
|
||||||
|
return metadata.value.events || [];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="device-simulation">
|
<div class="device-simulation">
|
||||||
<Tabs v-model:active-key="activeTab">
|
<Tabs v-model:active-key="activeTab">
|
||||||
<TabPane key="property" tab="属性">
|
<TabPane key="property" tab="属性">
|
||||||
<div>属性功能开发中...</div>
|
<PropertySimulation
|
||||||
|
:device-id="deviceId"
|
||||||
|
:device-info="deviceInfo"
|
||||||
|
:metadata="metadata"
|
||||||
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="function" tab="功能">
|
<TabPane key="function" tab="功能">
|
||||||
<FunctionSimulation
|
<FunctionSimulation
|
||||||
|
@ -53,7 +78,11 @@ const functionList = computed(() => {
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="event" tab="事件">
|
<TabPane key="event" tab="事件">
|
||||||
<div>事件功能开发中...</div>
|
<EventSimulation
|
||||||
|
:device-id="deviceId"
|
||||||
|
:device-info="deviceInfo"
|
||||||
|
:event-list="eventList"
|
||||||
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="online" tab="上下线">
|
<TabPane key="online" tab="上下线">
|
||||||
<div>上下线功能开发中...</div>
|
<div>上下线功能开发中...</div>
|
||||||
|
|
|
@ -35,7 +35,11 @@ const metadata = computed(() => {
|
||||||
<div class="running-status">
|
<div class="running-status">
|
||||||
<Tabs type="card">
|
<Tabs type="card">
|
||||||
<TabPane key="realtime" tab="实时数据">
|
<TabPane key="realtime" tab="实时数据">
|
||||||
<RealtimePanel :device-id="props.deviceId" :metadata="metadata" />
|
<RealtimePanel
|
||||||
|
:device-id="props.deviceId"
|
||||||
|
:metadata="metadata"
|
||||||
|
:device-info="deviceInfo"
|
||||||
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="events" tab="事件">
|
<TabPane key="events" tab="事件">
|
||||||
<EventsPanel :device-id="props.deviceId" :metadata="metadata" />
|
<EventsPanel :device-id="props.deviceId" :metadata="metadata" />
|
||||||
|
|
|
@ -25,9 +25,12 @@ import {
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { getWebSocket } from '#/utils/websocket';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
metadata: any;
|
metadata: any;
|
||||||
|
deviceInfo: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
@ -39,6 +42,48 @@ const selectedTypes = ref(['R', 'RW']);
|
||||||
// 本地运行态属性(不修改入参metadata)
|
// 本地运行态属性(不修改入参metadata)
|
||||||
const runtimeProperties = ref<any[]>([]);
|
const runtimeProperties = ref<any[]>([]);
|
||||||
|
|
||||||
|
// WebSocket订阅相关
|
||||||
|
let realtimeDataSubscription: any = null;
|
||||||
|
|
||||||
|
// 订阅设备实时数据
|
||||||
|
const subscribeRealtimeData = () => {
|
||||||
|
const productKey = props.deviceInfo.productObj.productKey;
|
||||||
|
const deviceKey = props.deviceInfo.deviceKey;
|
||||||
|
if (!deviceKey || !props.metadata?.properties?.length) return;
|
||||||
|
|
||||||
|
const id = `device-realtime-${deviceKey}`;
|
||||||
|
const topic = `/device/${productKey}/report`;
|
||||||
|
|
||||||
|
realtimeDataSubscription = getWebSocket(id, topic, {
|
||||||
|
deviceKey,
|
||||||
|
properties: props.metadata.properties.map((p: any) => p.id),
|
||||||
|
interval: '3s',
|
||||||
|
}).subscribe((data: any) => {
|
||||||
|
if (data.payload?.deviceKey === deviceKey && data.payload?.properties) {
|
||||||
|
// 更新实时属性数据
|
||||||
|
data.payload.properties.forEach((propData: any) => {
|
||||||
|
const property = runtimeProperties.value.find(
|
||||||
|
(p: any) => p.id === propData.id,
|
||||||
|
);
|
||||||
|
if (property) {
|
||||||
|
property.value = propData.value;
|
||||||
|
property.timestamp = dayjs(propData.timestamp).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
const unsubscribeRealtimeData = () => {
|
||||||
|
if (realtimeDataSubscription) {
|
||||||
|
realtimeDataSubscription.unsubscribe();
|
||||||
|
realtimeDataSubscription = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const initRuntime = () => {
|
const initRuntime = () => {
|
||||||
const properties = (props.metadata?.properties || []).map((prop: any) => ({
|
const properties = (props.metadata?.properties || []).map((prop: any) => ({
|
||||||
...prop,
|
...prop,
|
||||||
|
@ -248,15 +293,22 @@ const stopRefreshTimer = () => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initRuntime();
|
initRuntime();
|
||||||
startRefreshTimer();
|
startRefreshTimer();
|
||||||
|
subscribeRealtimeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopRefreshTimer();
|
stopRefreshTimer();
|
||||||
|
unsubscribeRealtimeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.metadata,
|
() => props.metadata,
|
||||||
() => initRuntime(),
|
() => {
|
||||||
|
initRuntime();
|
||||||
|
// 重新订阅实时数据
|
||||||
|
unsubscribeRealtimeData();
|
||||||
|
subscribeRealtimeData();
|
||||||
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,729 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
Slider,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
TabPane,
|
||||||
|
Tabs,
|
||||||
|
TimePicker,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { deviceOperateEvent } from '#/api/device/device';
|
||||||
|
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
||||||
|
import { dataTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
deviceInfo: any;
|
||||||
|
eventList: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// 当前选中的事件
|
||||||
|
const selectedEventId = ref('');
|
||||||
|
// 当前模式:simple(精简模式) / advanced(高级模式)
|
||||||
|
const currentMode = ref('simple');
|
||||||
|
// 表单引用
|
||||||
|
const formRef = ref();
|
||||||
|
// JSON编辑器内容
|
||||||
|
const jsonContent = ref('{}');
|
||||||
|
// 参数框内容
|
||||||
|
const parameterContent = ref('');
|
||||||
|
// 提交结果
|
||||||
|
const submitResult = ref('');
|
||||||
|
|
||||||
|
// 当前选中的事件详情
|
||||||
|
const currentEvent = ref();
|
||||||
|
|
||||||
|
// 当前事件的输出参数
|
||||||
|
const currentOutputs = ref([]);
|
||||||
|
|
||||||
|
// 选中的参数行
|
||||||
|
const selectedRowKeys = ref([]);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref({});
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '参数名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '输入类型',
|
||||||
|
dataIndex: 'dataType',
|
||||||
|
key: 'dataType',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '值',
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表格数据源
|
||||||
|
const tableDataSource = computed(() => {
|
||||||
|
return currentOutputs.value.map((output) => ({
|
||||||
|
key: output.id,
|
||||||
|
id: output.id,
|
||||||
|
name: output.name,
|
||||||
|
dataType:
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(option) => option.value === output.valueParams.dataType,
|
||||||
|
)?.label || output.valueParams.dataType,
|
||||||
|
required: output.required,
|
||||||
|
formType: output.valueParams.formType,
|
||||||
|
valueParams: output.valueParams,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格行选择配置
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
|
onChange: (selectedKeys: string[]) => {
|
||||||
|
selectedRowKeys.value = selectedKeys;
|
||||||
|
},
|
||||||
|
onSelectAll: (selected: boolean) => {
|
||||||
|
selectedRowKeys.value = selected
|
||||||
|
? tableDataSource.value.map((row) => row.key)
|
||||||
|
: tableDataSource.value
|
||||||
|
.filter((row) => row.required)
|
||||||
|
.map((row) => row.key);
|
||||||
|
},
|
||||||
|
onSelect: (record: any, selected: boolean) => {
|
||||||
|
if (record.required && !selected) {
|
||||||
|
// 必填项不能取消选择
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRowKeys.value = selected
|
||||||
|
? [...selectedRowKeys.value, record.key]
|
||||||
|
: selectedRowKeys.value.filter((key) => key !== record.key);
|
||||||
|
},
|
||||||
|
getCheckboxProps: (record: any) => ({
|
||||||
|
disabled: record.required, // 必填项禁用取消选择
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 初始化选中行(默认选中必填项)
|
||||||
|
const initializeSelectedRows = () => {
|
||||||
|
selectedRowKeys.value = currentOutputs.value
|
||||||
|
.filter((output) => output.required)
|
||||||
|
.map((output) => output.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化表单默认值
|
||||||
|
const initializeFormData = () => {
|
||||||
|
const defaultData = {};
|
||||||
|
currentOutputs.value.forEach((output) => {
|
||||||
|
const { formType } = output.valueParams;
|
||||||
|
|
||||||
|
// 为不同类型的组件设置默认值
|
||||||
|
switch (formType) {
|
||||||
|
case 'number': {
|
||||||
|
// 数字输入框默认为最小值或0
|
||||||
|
defaultData[output.id] = output.valueParams.min || 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'progress': {
|
||||||
|
// 滚动条默认为最小值
|
||||||
|
defaultData[output.id] = output.valueParams.min || 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'select': {
|
||||||
|
// 下拉框默认为第一个选项
|
||||||
|
if (
|
||||||
|
output.valueParams.enumConf &&
|
||||||
|
output.valueParams.enumConf.length > 0
|
||||||
|
) {
|
||||||
|
defaultData[output.id] = output.valueParams.enumConf[0].value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'switch': {
|
||||||
|
// 开关默认值根据数据类型设置
|
||||||
|
defaultData[output.id] =
|
||||||
|
output.valueParams.dataType === 'boolean'
|
||||||
|
? false
|
||||||
|
: output.valueParams.falseValue || 'false';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// 其他类型保持undefined,让用户手动输入
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
formData.value = { ...defaultData };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听事件切换,重置表单和内容
|
||||||
|
watch(selectedEventId, (newEventId) => {
|
||||||
|
if (newEventId) {
|
||||||
|
// 更新当前事件和输出参数
|
||||||
|
const selectedEvt = props.eventList.find((evt) => evt.id === newEventId);
|
||||||
|
if (selectedEvt) {
|
||||||
|
currentEvent.value = selectedEvt;
|
||||||
|
currentOutputs.value = selectedEvt.outputs || [];
|
||||||
|
// 初始化选中行
|
||||||
|
initializeSelectedRows();
|
||||||
|
// 初始化表单默认值
|
||||||
|
initializeFormData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
generateDefaultContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听模式切换,重置表单
|
||||||
|
watch(currentMode, () => {
|
||||||
|
resetForm();
|
||||||
|
generateDefaultContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
}
|
||||||
|
formData.value = {};
|
||||||
|
submitResult.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成默认内容
|
||||||
|
const generateDefaultContent = () => {
|
||||||
|
if (!currentEvent.value) return;
|
||||||
|
|
||||||
|
if (currentMode.value === 'advanced') {
|
||||||
|
// 高级模式:所有属性key:null
|
||||||
|
const params = {};
|
||||||
|
currentOutputs.value.forEach((output) => {
|
||||||
|
params[output.id] = null;
|
||||||
|
});
|
||||||
|
jsonContent.value = JSON.stringify(params, null, 2);
|
||||||
|
parameterContent.value = '';
|
||||||
|
} else {
|
||||||
|
// 精简模式:清空参数框
|
||||||
|
parameterContent.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理JSON编辑器变化
|
||||||
|
const handleJsonChange = (value: string) => {
|
||||||
|
jsonContent.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行实际提交
|
||||||
|
const executeSubmit = async () => {
|
||||||
|
try {
|
||||||
|
let parameters = {};
|
||||||
|
|
||||||
|
if (currentMode.value === 'simple') {
|
||||||
|
// 检查是否有选中的参数
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.error('请至少选择一个参数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 精简模式:验证表单并获取值
|
||||||
|
await formRef.value.validate();
|
||||||
|
const formValues = formData.value;
|
||||||
|
console.log('表单值:', formValues);
|
||||||
|
|
||||||
|
// 验证选中的参数
|
||||||
|
for (const output of currentOutputs.value) {
|
||||||
|
if (selectedRowKeys.value.includes(output.id)) {
|
||||||
|
const value = formValues[output.id];
|
||||||
|
const { formType } = output.valueParams;
|
||||||
|
|
||||||
|
// 根据不同的表单类型判断是否为空值
|
||||||
|
let isEmpty = false;
|
||||||
|
|
||||||
|
// 使用三元表达式简化 isEmpty 的赋值
|
||||||
|
isEmpty =
|
||||||
|
formType === 'switch'
|
||||||
|
? value === undefined || value === null
|
||||||
|
: value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === '' ||
|
||||||
|
(typeof value === 'string' && value.trim() === '');
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
message.error(`请填写参数:${output.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只提交选中的参数,构造key:value对象
|
||||||
|
parameters = {};
|
||||||
|
selectedRowKeys.value.forEach((key) => {
|
||||||
|
const value = formValues[key];
|
||||||
|
// 对于开关类型,false 也是有效值,不应该被过滤掉
|
||||||
|
const output = currentOutputs.value.find((item) => item.id === key);
|
||||||
|
const isSwitch = output?.valueParams?.formType === 'switch';
|
||||||
|
|
||||||
|
if (
|
||||||
|
(value !== undefined && value !== null) ||
|
||||||
|
(isSwitch && value === false)
|
||||||
|
) {
|
||||||
|
if (output) {
|
||||||
|
const { dataType, formType } = output.valueParams;
|
||||||
|
let processedValue = value;
|
||||||
|
|
||||||
|
// 根据dataType和formType处理数据类型转换
|
||||||
|
if (formType === 'switch') {
|
||||||
|
if (dataType === 'boolean') {
|
||||||
|
// boolean类型的开关,提交布尔值
|
||||||
|
processedValue = Boolean(value);
|
||||||
|
} else if (dataType === 'string') {
|
||||||
|
// string类型的开关,提交字符串值
|
||||||
|
processedValue = value
|
||||||
|
? output.valueParams.trueValue || 'true'
|
||||||
|
: output.valueParams.falseValue || 'false';
|
||||||
|
}
|
||||||
|
} else if (formType === 'time' && dataType === 'date') {
|
||||||
|
// date类型的时间选择器,提交时间戳
|
||||||
|
if (value && typeof value === 'object' && value.valueOf) {
|
||||||
|
processedValue = value.valueOf(); // 转换为时间戳
|
||||||
|
} else if (value && typeof value === 'string') {
|
||||||
|
processedValue = new Date(value).getTime(); // 字符串转时间戳
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters[key] = processedValue;
|
||||||
|
} else {
|
||||||
|
parameters[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 高级模式:验证JSON格式
|
||||||
|
try {
|
||||||
|
parameters = JSON.parse(jsonContent.value);
|
||||||
|
} catch {
|
||||||
|
message.error('JSON格式错误,请检查格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造提交数据格式
|
||||||
|
const submitData = {
|
||||||
|
productKey: props.deviceInfo.productObj.productKey,
|
||||||
|
deviceKey: props.deviceInfo.deviceKey,
|
||||||
|
eventId: selectedEventId.value,
|
||||||
|
params: parameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新参数框内容
|
||||||
|
parameterContent.value = JSON.stringify(submitData, null, 2);
|
||||||
|
|
||||||
|
// 调用设备操作事件接口
|
||||||
|
console.log('触发事件:', submitData);
|
||||||
|
const result = await deviceOperateEvent(submitData);
|
||||||
|
|
||||||
|
submitResult.value = JSON.stringify(result, null, 2);
|
||||||
|
// message.success('事件触发成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('事件触发失败');
|
||||||
|
console.error('触发错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主要的提交函数
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 直接执行提交
|
||||||
|
await executeSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化选中第一个事件
|
||||||
|
if (props.eventList.length > 0 && !selectedEventId.value) {
|
||||||
|
selectedEventId.value = props.eventList[0].id;
|
||||||
|
currentEvent.value = props.eventList[0];
|
||||||
|
currentOutputs.value = props.eventList[0].outputs || [];
|
||||||
|
// 初始化选中行
|
||||||
|
initializeSelectedRows();
|
||||||
|
// 初始化表单默认值
|
||||||
|
initializeFormData();
|
||||||
|
// 初始化
|
||||||
|
generateDefaultContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="event-simulation">
|
||||||
|
<div class="event-content">
|
||||||
|
<!-- 左侧事件列表 -->
|
||||||
|
<div class="event-sidebar">
|
||||||
|
<div class="sidebar-header">事件列表</div>
|
||||||
|
<div
|
||||||
|
v-for="evt in eventList"
|
||||||
|
:key="evt.id"
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ active: selectedEventId === evt.id }"
|
||||||
|
@click="selectedEventId = evt.id"
|
||||||
|
>
|
||||||
|
<div class="event-name">{{ evt.name }}</div>
|
||||||
|
<div class="event-id">{{ evt.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧内容区域 -->
|
||||||
|
<div class="event-main">
|
||||||
|
<!-- 模式切换 -->
|
||||||
|
<div class="mode-tabs">
|
||||||
|
<Tabs v-model:active-key="currentMode">
|
||||||
|
<TabPane key="simple" tab="精简模式">
|
||||||
|
<div class="mode-info">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
精简模式下参数只支持输入框的方式录入
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key="advanced" tab="高级模式">
|
||||||
|
<div class="mode-info">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
高级模式下支持JSON格式直接编辑
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-area">
|
||||||
|
<!-- 左侧参数输入 -->
|
||||||
|
<div class="input-section">
|
||||||
|
<div v-if="currentMode === 'simple'" class="simple-form">
|
||||||
|
<Form ref="formRef" :model="formData" layout="vertical">
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableDataSource"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ y: 300 }"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'name'">
|
||||||
|
<span>{{ record.name }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'value'">
|
||||||
|
<Form.Item
|
||||||
|
:name="record.id"
|
||||||
|
:rules="
|
||||||
|
record.required
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: `请输入${record.name}`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
"
|
||||||
|
style="margin-bottom: 0"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-if="record?.valueParams?.formType === 'input'"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:maxlength="record?.valueParams?.length || null"
|
||||||
|
:placeholder="`请输入${record.name}`"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
v-else-if="record?.valueParams?.formType === 'number'"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
type="number"
|
||||||
|
:min="record?.valueParams?.min || null"
|
||||||
|
:max="record?.valueParams?.max || null"
|
||||||
|
:step="record?.valueParams?.step || 1"
|
||||||
|
:precision="record?.valueParams?.scale || 0"
|
||||||
|
:placeholder="`请输入${record.name}`"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'progress'
|
||||||
|
"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:min="record?.valueParams?.min || 0"
|
||||||
|
:max="record?.valueParams?.max || 100"
|
||||||
|
style="width: 99%"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-else-if="record?.valueParams?.formType === 'select'"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:placeholder="`请选择${record.name}`"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
v-for="option in record?.valueParams?.enumConf ||
|
||||||
|
[]"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.text }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
<Switch
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'switch' &&
|
||||||
|
record.valueParams.dataType === 'boolean'
|
||||||
|
"
|
||||||
|
v-model:checked="formData[record.id]"
|
||||||
|
:checked-children="
|
||||||
|
record?.valueParams?.trueText || '是'
|
||||||
|
"
|
||||||
|
:un-checked-children="
|
||||||
|
record?.valueParams?.falseText || '否'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'switch' &&
|
||||||
|
record.valueParams.dataType === 'string'
|
||||||
|
"
|
||||||
|
v-model:checked="formData[record.id]"
|
||||||
|
:checked-children="
|
||||||
|
record?.valueParams?.trueText || '是'
|
||||||
|
"
|
||||||
|
:un-checked-children="
|
||||||
|
record?.valueParams?.falseText || '否'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'time' &&
|
||||||
|
record?.valueParams?.format?.includes('YYYY-MM-DD')
|
||||||
|
"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:show-time="
|
||||||
|
record?.valueParams?.format?.includes('HH:mm:ss')
|
||||||
|
"
|
||||||
|
:format="record?.valueParams?.format"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<TimePicker
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'time' &&
|
||||||
|
record?.valueParams?.format?.includes('HH:mm:ss')
|
||||||
|
"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:format="record?.valueParams?.format"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="advanced-form">
|
||||||
|
<MonacoEditor
|
||||||
|
v-model="jsonContent"
|
||||||
|
lang="json"
|
||||||
|
theme="vs-dark"
|
||||||
|
style="
|
||||||
|
height: 300px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
"
|
||||||
|
@update:model-value="handleJsonChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="execute-button">
|
||||||
|
<Button type="primary" @click="handleSubmit">触发事件</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧参数框和结果 -->
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="parameter-box">
|
||||||
|
<div class="parameter-header">参数:</div>
|
||||||
|
<div class="parameter-content">
|
||||||
|
<pre>{{ parameterContent || '点击触发后显示参数' }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.event-simulation {
|
||||||
|
.event-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 600px;
|
||||||
|
|
||||||
|
.event-sidebar {
|
||||||
|
width: 200px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-bottom: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: white;
|
||||||
|
background: #1890ff;
|
||||||
|
|
||||||
|
.event-id {
|
||||||
|
color: rgb(255 255 255 / 80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-main {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.mode-tabs {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.mode-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.input-section {
|
||||||
|
flex: 5;
|
||||||
|
min-height: 300px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.simple-form {
|
||||||
|
:deep(.ant-table) {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
font-weight: 500;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.execute-button {
|
||||||
|
margin: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
display: flex;
|
||||||
|
flex: 2;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 300px;
|
||||||
|
min-height: 300px;
|
||||||
|
|
||||||
|
.parameter-box {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-box,
|
||||||
|
.result-box {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.parameter-header,
|
||||||
|
.result-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-content,
|
||||||
|
.result-content {
|
||||||
|
min-height: calc(100% - 32px);
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #262626;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-box {
|
||||||
|
.result-content {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -19,7 +19,9 @@ import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TimePicker,
|
TimePicker,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
import MD5 from 'crypto-js/md5';
|
||||||
|
|
||||||
|
import { deviceOperateFunc } from '#/api/device/device';
|
||||||
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
||||||
import { dataTypeOptions } from '#/constants/dicts';
|
import { dataTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
@ -112,9 +114,11 @@ const rowSelection = computed(() => ({
|
||||||
selectedRowKeys.value = selectedKeys;
|
selectedRowKeys.value = selectedKeys;
|
||||||
},
|
},
|
||||||
onSelectAll: (selected: boolean) => {
|
onSelectAll: (selected: boolean) => {
|
||||||
selectedRowKeys.value = selected ? tableDataSource.value.map((row) => row.key) : tableDataSource.value
|
selectedRowKeys.value = selected
|
||||||
.filter((row) => row.required)
|
? tableDataSource.value.map((row) => row.key)
|
||||||
.map((row) => row.key);
|
: tableDataSource.value
|
||||||
|
.filter((row) => row.required)
|
||||||
|
.map((row) => row.key);
|
||||||
},
|
},
|
||||||
onSelect: (record: any, selected: boolean) => {
|
onSelect: (record: any, selected: boolean) => {
|
||||||
if (record.required && !selected) {
|
if (record.required && !selected) {
|
||||||
|
@ -168,7 +172,10 @@ const initializeFormData = () => {
|
||||||
// }
|
// }
|
||||||
case 'switch': {
|
case 'switch': {
|
||||||
// 开关默认值根据数据类型设置
|
// 开关默认值根据数据类型设置
|
||||||
defaultData[input.id] = input.valueParams.dataType === 'boolean' ? false : input.valueParams.falseValue || 'false' ;
|
defaultData[input.id] =
|
||||||
|
input.valueParams.dataType === 'boolean'
|
||||||
|
? false
|
||||||
|
: input.valueParams.falseValue || 'false';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
@ -336,10 +343,13 @@ const executeSubmit = async (checkValue?: string) => {
|
||||||
// 根据不同的表单类型判断是否为空值
|
// 根据不同的表单类型判断是否为空值
|
||||||
let isEmpty = false;
|
let isEmpty = false;
|
||||||
|
|
||||||
isEmpty = formType === 'switch'
|
isEmpty =
|
||||||
? value === undefined || value === null // if 分支
|
formType === 'switch'
|
||||||
: value === undefined || value === null || value === '' || // else 分支
|
? value === undefined || value === null // if 分支
|
||||||
(typeof value === 'string' && value.trim() === '');
|
: value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === '' || // else 分支
|
||||||
|
(typeof value === 'string' && value.trim() === '');
|
||||||
|
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
message.error(`请填写参数:${input.name}`);
|
message.error(`请填写参数:${input.name}`);
|
||||||
|
@ -404,9 +414,10 @@ const executeSubmit = async (checkValue?: string) => {
|
||||||
|
|
||||||
// 构造提交数据格式
|
// 构造提交数据格式
|
||||||
const submitData: any = {
|
const submitData: any = {
|
||||||
deviceId: props.deviceId,
|
productKey: props.deviceInfo.productObj.productKey,
|
||||||
functionId: selectedFunctionId.value,
|
deviceKey: props.deviceInfo.deviceKey,
|
||||||
parameter: parameters,
|
funcId: selectedFunctionId.value,
|
||||||
|
params: parameters,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果有预检查值,添加到提交数据中
|
// 如果有预检查值,添加到提交数据中
|
||||||
|
@ -417,27 +428,33 @@ const executeSubmit = async (checkValue?: string) => {
|
||||||
// 更新参数框内容
|
// 更新参数框内容
|
||||||
parameterContent.value = JSON.stringify(submitData, null, 2);
|
parameterContent.value = JSON.stringify(submitData, null, 2);
|
||||||
|
|
||||||
|
await deviceOperateFunc(submitData);
|
||||||
|
|
||||||
// 模拟API调用
|
// 模拟API调用
|
||||||
console.log('执行功能:', submitData);
|
console.log('执行功能:', submitData);
|
||||||
console.log('选中的参数keys:', selectedRowKeys.value);
|
console.log('选中的参数keys:', selectedRowKeys.value);
|
||||||
console.log('提交的参数对象:', parameters);
|
console.log('提交的参数对象:', parameters);
|
||||||
|
|
||||||
// 模拟返回结果
|
// // 模拟返回结果
|
||||||
const mockResult = {
|
// const mockResult = {
|
||||||
success: true,
|
// success: true,
|
||||||
message: '执行成功',
|
// message: '执行成功',
|
||||||
data: {
|
// data: {
|
||||||
deviceId: props.deviceId,
|
// deviceKey: props.deviceInfo.deviceKey,
|
||||||
functionId: selectedFunctionId.value,
|
// funcId: selectedFunctionId.value,
|
||||||
executeTime: new Date().toISOString(),
|
// executeTime: new Date().toISOString(),
|
||||||
result: 'OK',
|
// result: 'OK',
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
|
|
||||||
submitResult.value = JSON.stringify(mockResult, null, 2);
|
// submitResult.value = JSON.stringify(mockResult, null, 2);
|
||||||
message.success('执行成功');
|
// message.success('执行成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('执行失败');
|
if (error.errorFields[0].errors[0]) {
|
||||||
|
message.error(error.errorFields[0].errors[0]);
|
||||||
|
} else {
|
||||||
|
message.error('执行失败');
|
||||||
|
}
|
||||||
console.error('执行错误:', error);
|
console.error('执行错误:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -463,7 +480,8 @@ const handlePreCheckConfirm = async () => {
|
||||||
// 根据预检查类型构造checkValue
|
// 根据预检查类型构造checkValue
|
||||||
switch (preCheckType.value) {
|
switch (preCheckType.value) {
|
||||||
case 'staticPassword': {
|
case 'staticPassword': {
|
||||||
checkValue = preCheckForm.value.password;
|
const hashResult = MD5(preCheckForm.value.password).toString(); // 得到 MD5 哈希值
|
||||||
|
checkValue = hashResult;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'userPassword': {
|
case 'userPassword': {
|
||||||
|
@ -613,6 +631,7 @@ onMounted(() => {
|
||||||
type="number"
|
type="number"
|
||||||
:min="record?.valueParams?.min || null"
|
:min="record?.valueParams?.min || null"
|
||||||
:max="record?.valueParams?.max || null"
|
:max="record?.valueParams?.max || null"
|
||||||
|
:step="record?.valueParams?.step || 1"
|
||||||
:precision="record?.valueParams?.scale || 0"
|
:precision="record?.valueParams?.scale || 0"
|
||||||
:placeholder="`请输入${record.name}`"
|
:placeholder="`请输入${record.name}`"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
@ -624,6 +643,7 @@ onMounted(() => {
|
||||||
v-model:value="formData[record.id]"
|
v-model:value="formData[record.id]"
|
||||||
:min="record?.valueParams?.min || 0"
|
:min="record?.valueParams?.min || 0"
|
||||||
:max="record?.valueParams?.max || 100"
|
:max="record?.valueParams?.max || 100"
|
||||||
|
:step="record?.valueParams?.step || 1"
|
||||||
style="width: 99%"
|
style="width: 99%"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -0,0 +1,882 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
Slider,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
TabPane,
|
||||||
|
Tabs,
|
||||||
|
TimePicker,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { deviceOperateFunc, deviceOperateReport } from '#/api/device/device';
|
||||||
|
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
||||||
|
import { dataTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
deviceInfo: any;
|
||||||
|
metadata: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// 当前选中的属性功能
|
||||||
|
const selectedPropertyFunction = ref('report'); // report(上报), write(写入), read(读取)
|
||||||
|
// 当前模式:simple(精简模式) / advanced(高级模式)
|
||||||
|
const currentMode = ref('simple');
|
||||||
|
// 表单引用
|
||||||
|
const formRef = ref();
|
||||||
|
// JSON编辑器内容
|
||||||
|
const jsonContent = ref('{}');
|
||||||
|
// 参数框内容
|
||||||
|
const parameterContent = ref('');
|
||||||
|
// 提交结果
|
||||||
|
const submitResult = ref('');
|
||||||
|
|
||||||
|
// 属性分组选择
|
||||||
|
const selectedGroup = ref('all');
|
||||||
|
// 去掉访问权限过滤
|
||||||
|
// const selectedTypes = ref(['R', 'RW']);
|
||||||
|
|
||||||
|
// 当前选中的属性详情 - 删除未使用的变量
|
||||||
|
// const currentProperty = ref();
|
||||||
|
|
||||||
|
// 选中的属性行
|
||||||
|
const selectedRowKeys = ref([]);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref({});
|
||||||
|
|
||||||
|
// 属性功能选项
|
||||||
|
const propertyFunctionOptions = [
|
||||||
|
{ label: '上报', value: 'report' },
|
||||||
|
{ label: '写入', value: 'write' },
|
||||||
|
{ label: '读取', value: 'read' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 属性分组选项
|
||||||
|
const groupOptions = computed(() => {
|
||||||
|
const groups = props.metadata?.propertyGroups || [];
|
||||||
|
return [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
...groups.map((group: any) => ({ label: group.name, value: group.id })),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 读写权限选项 - 删除
|
||||||
|
// const typeOptions = [
|
||||||
|
// { label: '只读', value: 'R' },
|
||||||
|
// { label: '只写', value: 'W' },
|
||||||
|
// { label: '读写', value: 'RW' },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// 过滤后的属性列表
|
||||||
|
const filteredProperties = computed(() => {
|
||||||
|
let properties = [...(props.metadata?.properties || [])];
|
||||||
|
|
||||||
|
// 根据分组过滤
|
||||||
|
if (selectedGroup.value !== 'all') {
|
||||||
|
const group = (props.metadata?.propertyGroups || []).find(
|
||||||
|
(g: any) => g.id === selectedGroup.value,
|
||||||
|
);
|
||||||
|
if (group?.properties) {
|
||||||
|
const ids = new Set(group.properties.map((p: any) => p.id));
|
||||||
|
properties = properties.filter((p: any) => ids.has(p.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉读写权限过滤
|
||||||
|
// properties = properties.filter((p: any) => {
|
||||||
|
// const type = p.expands?.type || 'R';
|
||||||
|
// return selectedTypes.value.includes(type);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 根据选中的功能过滤
|
||||||
|
switch (selectedPropertyFunction.value) {
|
||||||
|
case 'read': {
|
||||||
|
// 读取:只读和读写属性
|
||||||
|
properties = properties.filter((p: any) => {
|
||||||
|
const type = p.expands?.type || 'R';
|
||||||
|
return type === 'R' || type === 'RW';
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'report': {
|
||||||
|
// 上报:包含所有属性
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'write': {
|
||||||
|
// 写入:只写和读写属性
|
||||||
|
properties = properties.filter((p: any) => {
|
||||||
|
const type = p.expands?.type || 'R';
|
||||||
|
return type === 'W' || type === 'RW';
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '属性名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据类型',
|
||||||
|
dataIndex: 'dataType',
|
||||||
|
key: 'dataType',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: '访问权限',
|
||||||
|
// dataIndex: 'accessType',
|
||||||
|
// key: 'accessType',
|
||||||
|
// width: 100,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: '值',
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表格数据源
|
||||||
|
const tableDataSource = computed(() => {
|
||||||
|
return filteredProperties.value.map((property) => ({
|
||||||
|
key: property.id,
|
||||||
|
id: property.id,
|
||||||
|
name: property.name,
|
||||||
|
dataType:
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(option) => option.value === property.valueParams?.dataType,
|
||||||
|
)?.label || property.valueParams?.dataType,
|
||||||
|
accessType: (() => {
|
||||||
|
const type = property.expands?.type || 'R';
|
||||||
|
switch (type) {
|
||||||
|
case 'R': {
|
||||||
|
return '只读';
|
||||||
|
}
|
||||||
|
case 'RW': {
|
||||||
|
return '读写';
|
||||||
|
}
|
||||||
|
case 'W': {
|
||||||
|
return '只写';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return '只读';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
required: property.required,
|
||||||
|
formType: property.valueParams?.formType,
|
||||||
|
valueParams: property.valueParams,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格行选择配置
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
|
onChange: (selectedKeys: string[]) => {
|
||||||
|
selectedRowKeys.value = selectedKeys;
|
||||||
|
},
|
||||||
|
onSelectAll: (selected: boolean) => {
|
||||||
|
selectedRowKeys.value = selected
|
||||||
|
? tableDataSource.value.map((row) => row.key)
|
||||||
|
: tableDataSource.value
|
||||||
|
.filter((row) => row.required)
|
||||||
|
.map((row) => row.key);
|
||||||
|
},
|
||||||
|
onSelect: (record: any, selected: boolean) => {
|
||||||
|
if (record.required && !selected) {
|
||||||
|
// 必填项不能取消选择
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRowKeys.value = selected
|
||||||
|
? [...selectedRowKeys.value, record.key]
|
||||||
|
: selectedRowKeys.value.filter((key) => key !== record.key);
|
||||||
|
},
|
||||||
|
getCheckboxProps: (record: any) => ({
|
||||||
|
disabled: record.required, // 必填项禁用取消选择
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 初始化选中行(默认选中必填项)
|
||||||
|
const initializeSelectedRows = () => {
|
||||||
|
console.log('初始化选中行', tableDataSource.value);
|
||||||
|
selectedRowKeys.value = tableDataSource.value
|
||||||
|
.filter((row) => row.required)
|
||||||
|
.map((row) => row.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化表单默认值
|
||||||
|
const initializeFormData = () => {
|
||||||
|
const defaultData = {};
|
||||||
|
tableDataSource.value.forEach((row) => {
|
||||||
|
const property = filteredProperties.value.find((p) => p.id === row.id);
|
||||||
|
if (!property) return;
|
||||||
|
|
||||||
|
const { formType } = property.valueParams;
|
||||||
|
|
||||||
|
// 为不同类型的组件设置默认值
|
||||||
|
switch (formType) {
|
||||||
|
case 'progress': {
|
||||||
|
// 滚动条默认为最小值
|
||||||
|
defaultData[row.id] = property.valueParams?.min || 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'switch': {
|
||||||
|
// 开关默认值根据数据类型设置
|
||||||
|
defaultData[row.id] =
|
||||||
|
property.valueParams?.dataType === 'boolean'
|
||||||
|
? false
|
||||||
|
: property.valueParams?.falseValue || 'false';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// 其他类型保持undefined,让用户手动输入
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
formData.value = { ...defaultData };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听属性功能切换,重置表单和内容
|
||||||
|
watch(selectedPropertyFunction, () => {
|
||||||
|
resetForm();
|
||||||
|
initializeSelectedRows();
|
||||||
|
generateDefaultContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听模式切换,重置表单
|
||||||
|
watch(currentMode, () => {
|
||||||
|
resetForm();
|
||||||
|
generateDefaultContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听筛选条件变化,重新初始化
|
||||||
|
watch([selectedGroup], () => {
|
||||||
|
// 延迟执行,确保 filteredProperties 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeSelectedRows();
|
||||||
|
initializeFormData();
|
||||||
|
generateDefaultContent();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
}
|
||||||
|
formData.value = {};
|
||||||
|
submitResult.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成默认内容
|
||||||
|
const generateDefaultContent = () => {
|
||||||
|
if (filteredProperties.value.length === 0) return;
|
||||||
|
|
||||||
|
if (currentMode.value === 'advanced') {
|
||||||
|
// 高级模式:所有属性key:null
|
||||||
|
const params = {};
|
||||||
|
filteredProperties.value.forEach((property) => {
|
||||||
|
params[property.id] = null;
|
||||||
|
});
|
||||||
|
jsonContent.value = JSON.stringify(params, null, 2);
|
||||||
|
parameterContent.value = '';
|
||||||
|
} else {
|
||||||
|
// 精简模式:清空参数框
|
||||||
|
parameterContent.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行实际提交
|
||||||
|
const executeSubmit = async () => {
|
||||||
|
try {
|
||||||
|
let parameters = {};
|
||||||
|
|
||||||
|
if (currentMode.value === 'simple') {
|
||||||
|
// 检查是否有选中的属性
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.error('请至少选择一个属性');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 精简模式:验证表单并获取值
|
||||||
|
await formRef.value.validate();
|
||||||
|
const formValues = formData.value;
|
||||||
|
console.log('表单值:', formValues);
|
||||||
|
|
||||||
|
// 验证选中的属性
|
||||||
|
for (const row of tableDataSource.value) {
|
||||||
|
if (selectedRowKeys.value.includes(row.key)) {
|
||||||
|
const property = filteredProperties.value.find(
|
||||||
|
(p) => p.id === row.id,
|
||||||
|
);
|
||||||
|
if (!property) continue;
|
||||||
|
|
||||||
|
const value = formValues[row.id];
|
||||||
|
const { formType } = property.valueParams;
|
||||||
|
|
||||||
|
// 根据不同的表单类型判断是否为空值
|
||||||
|
let isEmpty = false;
|
||||||
|
|
||||||
|
isEmpty =
|
||||||
|
formType === 'switch'
|
||||||
|
? value === undefined || value === null
|
||||||
|
: value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === '' ||
|
||||||
|
(typeof value === 'string' && value.trim() === '');
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
message.error(`请填写属性:${row.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只提交选中的属性,构造key:value对象
|
||||||
|
parameters = {};
|
||||||
|
selectedRowKeys.value.forEach((key) => {
|
||||||
|
const row = tableDataSource.value.find((r) => r.key === key);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const property = filteredProperties.value.find((p) => p.id === row.id);
|
||||||
|
if (!property) return;
|
||||||
|
|
||||||
|
const value = formValues[row.id];
|
||||||
|
const { dataType, formType } = property.valueParams;
|
||||||
|
let processedValue = value;
|
||||||
|
|
||||||
|
// 根据dataType和formType处理数据类型转换
|
||||||
|
if (formType === 'switch') {
|
||||||
|
if (dataType === 'boolean') {
|
||||||
|
processedValue = Boolean(value);
|
||||||
|
} else if (dataType === 'string') {
|
||||||
|
processedValue = value
|
||||||
|
? property.valueParams?.trueValue || 'true'
|
||||||
|
: property.valueParams?.falseValue || 'false';
|
||||||
|
}
|
||||||
|
} else if (formType === 'time' && dataType === 'date') {
|
||||||
|
if (value && typeof value === 'object' && value.valueOf) {
|
||||||
|
processedValue = value.valueOf();
|
||||||
|
} else if (value && typeof value === 'string') {
|
||||||
|
processedValue = new Date(value).getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters[row.id] = processedValue;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 高级模式:验证JSON格式
|
||||||
|
try {
|
||||||
|
parameters = JSON.parse(jsonContent.value);
|
||||||
|
} catch {
|
||||||
|
message.error('JSON格式错误,请检查格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造提交数据格式
|
||||||
|
const submitData: any = {
|
||||||
|
productKey: props.deviceInfo.productObj.productKey,
|
||||||
|
deviceKey: props.deviceInfo.deviceKey,
|
||||||
|
params: parameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedPropertyFunction.value === 'read') {
|
||||||
|
submitData.funcId = 'defaultRead';
|
||||||
|
} else if (selectedPropertyFunction.value === 'write') {
|
||||||
|
submitData.funcId = 'defaultWrite';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新参数框内容
|
||||||
|
parameterContent.value = JSON.stringify(submitData, null, 2);
|
||||||
|
|
||||||
|
await (selectedPropertyFunction.value === 'report'
|
||||||
|
? deviceOperateReport(submitData)
|
||||||
|
: deviceOperateFunc(submitData));
|
||||||
|
|
||||||
|
// 模拟API调用
|
||||||
|
// console.log('执行属性操作:', submitData);
|
||||||
|
// console.log('选中的属性keys:', selectedRowKeys.value);
|
||||||
|
// console.log('提交的参数对象:', parameters);
|
||||||
|
|
||||||
|
// 模拟返回结果
|
||||||
|
// const mockResult = {
|
||||||
|
// success: true,
|
||||||
|
// message: '执行成功',
|
||||||
|
// data: {
|
||||||
|
// deviceKey: props.deviceInfo.deviceKey,
|
||||||
|
// executeTime: new Date().toISOString(),
|
||||||
|
// result: 'OK',
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// submitResult.value = JSON.stringify(mockResult, null, 2);
|
||||||
|
// message.success('执行成功');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.errorFields[0].errors[0]) {
|
||||||
|
message.error(error.errorFields[0].errors[0]);
|
||||||
|
} else {
|
||||||
|
message.error('执行失败');
|
||||||
|
}
|
||||||
|
console.error('执行错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主要的提交函数
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 直接执行提交
|
||||||
|
await executeSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化选中第一个功能
|
||||||
|
if (propertyFunctionOptions.length > 0 && !selectedPropertyFunction.value) {
|
||||||
|
selectedPropertyFunction.value = propertyFunctionOptions[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟初始化,确保 filteredProperties 已计算
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeSelectedRows();
|
||||||
|
initializeFormData();
|
||||||
|
generateDefaultContent();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="property-simulation">
|
||||||
|
<div class="property-content">
|
||||||
|
<!-- 左侧属性功能列表 -->
|
||||||
|
<div class="property-sidebar">
|
||||||
|
<div class="sidebar-header">属性功能</div>
|
||||||
|
<div
|
||||||
|
v-for="func in propertyFunctionOptions"
|
||||||
|
:key="func.value"
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ active: selectedPropertyFunction === func.value }"
|
||||||
|
@click="selectedPropertyFunction = func.value"
|
||||||
|
>
|
||||||
|
<div class="function-name">{{ func.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧内容区域 -->
|
||||||
|
<div class="property-main">
|
||||||
|
<!-- 筛选控制面板 -->
|
||||||
|
<div class="control-panel">
|
||||||
|
<div class="control-item">
|
||||||
|
<span class="control-label">属性分组:</span>
|
||||||
|
<Select
|
||||||
|
v-model:value="selectedGroup"
|
||||||
|
:options="groupOptions"
|
||||||
|
style="width: 200px"
|
||||||
|
placeholder="请选择属性分组"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 访问权限过滤 - 删除 -->
|
||||||
|
<!--
|
||||||
|
<div class="control-item">
|
||||||
|
<span class="control-label">访问权限:</span>
|
||||||
|
<Checkbox.Group v-model:value="selectedTypes">
|
||||||
|
<Checkbox value="RW">读写</Checkbox>
|
||||||
|
<Checkbox value="R">只读</Checkbox>
|
||||||
|
<Checkbox value="W">只写</Checkbox>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模式切换 -->
|
||||||
|
<div class="mode-tabs">
|
||||||
|
<Tabs v-model:active-key="currentMode">
|
||||||
|
<TabPane key="simple" tab="精简模式">
|
||||||
|
<div class="mode-info">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
精简模式下属性只支持输入框的方式录入
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key="advanced" tab="高级模式">
|
||||||
|
<div class="mode-info">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
高级模式下支持JSON格式直接编辑
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-area">
|
||||||
|
<!-- 左侧属性输入 -->
|
||||||
|
<div class="input-section">
|
||||||
|
<div v-if="currentMode === 'simple'" class="simple-form">
|
||||||
|
<Form ref="formRef" :model="formData" layout="vertical">
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableDataSource"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ y: 300 }"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'name'">
|
||||||
|
<span>{{ record.name }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'dataType'">
|
||||||
|
<span>{{ record.dataType }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'accessType'">
|
||||||
|
<span>{{ record.accessType }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'value'">
|
||||||
|
<Form.Item
|
||||||
|
:name="record.id"
|
||||||
|
:rules="
|
||||||
|
record.required
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: `请输入${record.name}`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
"
|
||||||
|
style="margin-bottom: 0"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-if="record?.valueParams?.formType === 'input'"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:maxlength="record?.valueParams?.length || null"
|
||||||
|
:placeholder="`请输入${record.name}`"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
v-else-if="record?.valueParams?.formType === 'number'"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
type="number"
|
||||||
|
:min="record?.valueParams?.min || null"
|
||||||
|
:max="record?.valueParams?.max || null"
|
||||||
|
:step="record?.valueParams?.step || 1"
|
||||||
|
:precision="record?.valueParams?.scale || 0"
|
||||||
|
:placeholder="`请输入${record.name}`"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'progress'
|
||||||
|
"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:min="record?.valueParams?.min || 0"
|
||||||
|
:max="record?.valueParams?.max || 100"
|
||||||
|
:step="record?.valueParams?.step || 1"
|
||||||
|
style="width: 99%"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-else-if="record?.valueParams?.formType === 'select'"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:placeholder="`请选择${record.name}`"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
v-for="item in record.valueParams.enumConf"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
<Switch
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'switch' &&
|
||||||
|
record.valueParams.dataType === 'boolean'
|
||||||
|
"
|
||||||
|
v-model:checked="formData[record.id]"
|
||||||
|
:checked-children="
|
||||||
|
record?.valueParams?.trueText || '是'
|
||||||
|
"
|
||||||
|
:un-checked-children="
|
||||||
|
record?.valueParams?.falseText || '否'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'switch' &&
|
||||||
|
record.valueParams.dataType === 'string'
|
||||||
|
"
|
||||||
|
v-model:checked="formData[record.id]"
|
||||||
|
:checked-children="
|
||||||
|
record?.valueParams?.trueText || '是'
|
||||||
|
"
|
||||||
|
:checked-value="
|
||||||
|
record?.valueParams?.trueValue || true
|
||||||
|
"
|
||||||
|
:un-checked-children="
|
||||||
|
record?.valueParams?.falseText || '否'
|
||||||
|
"
|
||||||
|
:un-checked-value="
|
||||||
|
record?.valueParams?.falseValue || false
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'time' &&
|
||||||
|
record?.valueParams?.format?.includes('YYYY-MM-DD')
|
||||||
|
"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:show-time="
|
||||||
|
record?.valueParams?.format?.includes('HH:mm:ss')
|
||||||
|
"
|
||||||
|
:format="record?.valueParams?.format"
|
||||||
|
:value-format="record?.valueParams?.format"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<TimePicker
|
||||||
|
v-else-if="
|
||||||
|
record?.valueParams?.formType === 'time' &&
|
||||||
|
record?.valueParams?.format?.includes('HH:mm:ss')
|
||||||
|
"
|
||||||
|
v-model:value="formData[record.id]"
|
||||||
|
:format="record?.valueParams?.format"
|
||||||
|
:value-format="record?.valueParams?.format"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="advanced-form">
|
||||||
|
<MonacoEditor
|
||||||
|
:model-value="jsonContent"
|
||||||
|
lang="json"
|
||||||
|
theme="vs-dark"
|
||||||
|
style="
|
||||||
|
height: 300px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
"
|
||||||
|
@update:model-value="(value) => (jsonContent = value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="execute-button">
|
||||||
|
<Button type="primary" @click="handleSubmit">执行</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧参数框 -->
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="parameter-box">
|
||||||
|
<div class="parameter-header">参数:</div>
|
||||||
|
<div class="parameter-content">
|
||||||
|
<pre>{{ parameterContent || '点击执行后显示执行参数' }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.property-simulation {
|
||||||
|
.property-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 600px;
|
||||||
|
|
||||||
|
.property-sidebar {
|
||||||
|
width: 200px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-bottom: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: white;
|
||||||
|
background: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-main {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.control-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tabs {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.mode-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.input-section {
|
||||||
|
flex: 5;
|
||||||
|
min-height: 300px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.simple-form {
|
||||||
|
:deep(.ant-table) {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
font-weight: 500;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-form {
|
||||||
|
.json-editor-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.execute-button {
|
||||||
|
margin: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
display: flex;
|
||||||
|
flex: 2;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 300px;
|
||||||
|
min-height: 300px;
|
||||||
|
|
||||||
|
.parameter-box {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.parameter-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-content {
|
||||||
|
min-height: calc(100% - 32px);
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #262626;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -10,6 +10,7 @@ import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||||
import { deviceStateOptions } from '#/constants/dicts';
|
import { deviceStateOptions } from '#/constants/dicts';
|
||||||
// import { deviceUpdateStatus } from '#/api/device/device';
|
// import { deviceUpdateStatus } from '#/api/device/device';
|
||||||
import { useDeviceStore } from '#/store/device';
|
import { useDeviceStore } from '#/store/device';
|
||||||
|
import { getWebSocket } from '#/utils/websocket';
|
||||||
|
|
||||||
import BasicInfo from './components/BasicInfo.vue';
|
import BasicInfo from './components/BasicInfo.vue';
|
||||||
import DeviceSimulation from './components/DeviceSimulation.vue';
|
import DeviceSimulation from './components/DeviceSimulation.vue';
|
||||||
|
@ -24,10 +25,39 @@ const deviceId = computed(() => route.params.id as string);
|
||||||
const activeTab = computed(() => deviceStore.getTabActiveKey);
|
const activeTab = computed(() => deviceStore.getTabActiveKey);
|
||||||
const currentDevice = computed(() => deviceStore.getCurrentDevice);
|
const currentDevice = computed(() => deviceStore.getCurrentDevice);
|
||||||
|
|
||||||
|
// WebSocket订阅相关
|
||||||
|
let deviceStatusSubscription: any = null;
|
||||||
|
|
||||||
|
// 订阅设备在线状态
|
||||||
|
const subscribeDeviceStatus = () => {
|
||||||
|
if (!currentDevice.value) return;
|
||||||
|
|
||||||
|
const id = `device-status-${currentDevice.value.deviceKey}`;
|
||||||
|
const topic = `/device/${currentDevice.value.deviceKey}/status`;
|
||||||
|
|
||||||
|
deviceStatusSubscription = getWebSocket(id, topic, {
|
||||||
|
deviceId: currentDevice.value.deviceKey,
|
||||||
|
}).subscribe((data: any) => {
|
||||||
|
if (data.payload?.deviceKey === currentDevice.value.deviceKey) {
|
||||||
|
// 更新设备状态
|
||||||
|
deviceStore.updateDeviceStatus(data.payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
const unsubscribeDeviceStatus = () => {
|
||||||
|
if (deviceStatusSubscription) {
|
||||||
|
deviceStatusSubscription.unsubscribe();
|
||||||
|
deviceStatusSubscription = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 加载设备信息
|
// 加载设备信息
|
||||||
const loadDeviceInfo = async () => {
|
const loadDeviceInfo = async () => {
|
||||||
try {
|
try {
|
||||||
await deviceStore.getDetail(deviceId.value);
|
await deviceStore.getDetail(deviceId.value);
|
||||||
|
subscribeDeviceStatus();
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载设备信息失败');
|
message.error('加载设备信息失败');
|
||||||
}
|
}
|
||||||
|
@ -41,7 +71,7 @@ const handleStatusChange = async (checked: boolean) => {
|
||||||
// enabled: checked,
|
// enabled: checked,
|
||||||
// });
|
// });
|
||||||
// await loadDeviceInfo();
|
// await loadDeviceInfo();
|
||||||
console.log("checked",checked)
|
console.log('checked', checked);
|
||||||
} catch {
|
} catch {
|
||||||
message.error('状态更新失败');
|
message.error('状态更新失败');
|
||||||
}
|
}
|
||||||
|
@ -64,12 +94,17 @@ const handleProductClick = () => {
|
||||||
router.push(`/device/product/detail/${currentDevice.value.productId}`);
|
router.push(`/device/product/detail/${currentDevice.value.productId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeviceClick = () => {
|
||||||
|
router.push(`/device/device/detail/${currentDevice.value.parentId}`);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDeviceInfo();
|
loadDeviceInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件销毁时重置 store
|
// 组件销毁时重置 store 和取消订阅
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
unsubscribeDeviceStatus();
|
||||||
deviceStore.reset();
|
deviceStore.reset();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -139,6 +174,18 @@ onUnmounted(() => {
|
||||||
currentDevice?.productObj?.productName || '未知产品'
|
currentDevice?.productObj?.productName || '未知产品'
|
||||||
}}</a>
|
}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="basic-item"
|
||||||
|
v-if="
|
||||||
|
currentDevice.parentId &&
|
||||||
|
currentDevice.deviceType === 'childrenDevice'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="basic-label">父设备:</span>
|
||||||
|
<a class="basic-value product-link" @click="handleDeviceClick">{{
|
||||||
|
currentDevice?.parentName || '未知设备'
|
||||||
|
}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签页内容 -->
|
<!-- 标签页内容 -->
|
||||||
|
|
|
@ -137,6 +137,14 @@ const handleAddOutputParam = () => {
|
||||||
outputParamModalVisible.value = true;
|
outputParamModalVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 编辑输入参数
|
||||||
|
const handleEditOutputParam = (record: any, index: number) => {
|
||||||
|
parameterDataIndex.value = index;
|
||||||
|
parameterType.value = 'edit';
|
||||||
|
outputParamForm.value = JSON.parse(JSON.stringify(record));
|
||||||
|
outputParamModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
// 删除输出参数
|
// 删除输出参数
|
||||||
const handleDeleteOutputParam = (index: number) => {
|
const handleDeleteOutputParam = (index: number) => {
|
||||||
formData.value.outputs.splice(index, 1);
|
formData.value.outputs.splice(index, 1);
|
||||||
|
@ -197,7 +205,12 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Drawer v-model:open="visible" :title="drawerTitle" width="800px" @close="handleDrawerClose">
|
<Drawer
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="drawerTitle"
|
||||||
|
width="800px"
|
||||||
|
@close="handleDrawerClose"
|
||||||
|
>
|
||||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||||
<Row :gutter="24">
|
<Row :gutter="24">
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
|
@ -216,10 +229,18 @@ watch(
|
||||||
<Col :span="24">
|
<Col :span="24">
|
||||||
<FormItem label="输出参数" name="outputs">
|
<FormItem label="输出参数" name="outputs">
|
||||||
<div class="parameter-preview">
|
<div class="parameter-preview">
|
||||||
<Table :columns="outputColumns" :data-source="formData.outputs" :pagination="false">
|
<Table
|
||||||
|
:columns="outputColumns"
|
||||||
|
:data-source="formData.outputs"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record, index }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'dataType'">
|
<template v-if="column.key === 'dataType'">
|
||||||
{{ dataTypeOptions.find((item) => item.value === record.valueParams.dataType)?.label }}
|
{{
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.dataType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'required'">
|
<template v-if="column.key === 'required'">
|
||||||
<Tag color="processing">
|
<Tag color="processing">
|
||||||
|
@ -227,12 +248,26 @@ watch(
|
||||||
</Tag>
|
</Tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'formType'">
|
<template v-if="column.key === 'formType'">
|
||||||
{{ formTypeOptions.find((item) => item.value === record.valueParams.formType)?.label }}
|
{{
|
||||||
|
formTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.formType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" size="small" @click="handleEditOutputParam(record, index)"> 编辑 </Button>
|
<Button
|
||||||
<Popconfirm placement="left" title="确认删除?" @confirm="handleDeleteOutputParam(index)">
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleEditOutputParam(record, index)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
placement="left"
|
||||||
|
title="确认删除?"
|
||||||
|
@confirm="handleDeleteOutputParam(index)"
|
||||||
|
>
|
||||||
<Button type="link" size="small" danger> 删除 </Button>
|
<Button type="link" size="small" danger> 删除 </Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
|
@ -240,7 +275,11 @@ watch(
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
<div class="add-button-container">
|
<div class="add-button-container">
|
||||||
<a-button @click="handleAddOutputParam" type="primary" class="add-button">
|
<a-button
|
||||||
|
@click="handleAddOutputParam"
|
||||||
|
type="primary"
|
||||||
|
class="add-button"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
@ -253,13 +292,21 @@ watch(
|
||||||
|
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="排序" name="sort">
|
<FormItem label="排序" name="sort">
|
||||||
<InputNumber style="width: 100%" v-model:value="formData.sort" placeholder="请输入排序" />
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.sort"
|
||||||
|
placeholder="请输入排序"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col :span="24">
|
<Col :span="24">
|
||||||
<FormItem label="描述" name="description">
|
<FormItem label="描述" name="description">
|
||||||
<Textarea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
<Textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -268,7 +315,9 @@ watch(
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Space>
|
<Space>
|
||||||
<Button @click="handleDrawerClose">取消</Button>
|
<Button @click="handleDrawerClose">取消</Button>
|
||||||
<Button type="primary" @click="handleSave" :loading="saveLoading"> 确认 </Button>
|
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {
|
||||||
Tag,
|
Tag,
|
||||||
Textarea,
|
Textarea,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
// 引入 MD5 模块
|
||||||
|
import MD5 from 'crypto-js/md5';
|
||||||
|
|
||||||
import { dataTypeOptions, formTypeOptions } from '#/constants/dicts';
|
import { dataTypeOptions, formTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
@ -153,6 +155,17 @@ const formRules = {
|
||||||
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
||||||
trigger: 'blur',
|
trigger: 'blur',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
validator: (_rule: any, value: string) => {
|
||||||
|
if (value === 'defaultWrite' || value === 'defaultRead') {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error('标识符不能为 defaultWrite 或 defaultRead'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||||
|
@ -200,7 +213,27 @@ const handleInputParamsConfirm = (params: any) => {
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate();
|
await formRef.value.validate();
|
||||||
|
// 新增校验
|
||||||
|
if (
|
||||||
|
formData.value.expands.preCheck &&
|
||||||
|
formData.value.expands.checkType === 'staticPassword' &&
|
||||||
|
!formData.value.expands.staticPasswordValue
|
||||||
|
) {
|
||||||
|
message.error('请填写固定密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
saveLoading.value = true;
|
saveLoading.value = true;
|
||||||
|
if (
|
||||||
|
formData.value.expands.preCheck &&
|
||||||
|
formData.value.expands.checkType === 'staticPassword' &&
|
||||||
|
formData.value.expands.staticPasswordValue
|
||||||
|
) {
|
||||||
|
// 前置校验开启且类型为固定密码时,进行MD5加密
|
||||||
|
const hashResult = MD5(
|
||||||
|
formData.value.expands.staticPasswordValue,
|
||||||
|
).toString(); // 得到 MD5 哈希值
|
||||||
|
formData.value.expands.staticPasswordValue = hashResult;
|
||||||
|
}
|
||||||
|
|
||||||
emit('save', { ...formData.value });
|
emit('save', { ...formData.value });
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
|
@ -220,7 +253,10 @@ const handleDrawerClose = () => {
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
Object.assign(formData.value, JSON.parse(JSON.stringify(defaultFunctionData)));
|
Object.assign(
|
||||||
|
formData.value,
|
||||||
|
JSON.parse(JSON.stringify(defaultFunctionData)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听数据变化
|
// 监听数据变化
|
||||||
|
@ -238,7 +274,12 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Drawer v-model:open="visible" :title="drawerTitle" width="800px" @close="handleDrawerClose">
|
<Drawer
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="drawerTitle"
|
||||||
|
width="800px"
|
||||||
|
@close="handleDrawerClose"
|
||||||
|
>
|
||||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||||
<Row :gutter="24">
|
<Row :gutter="24">
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
|
@ -255,10 +296,18 @@ watch(
|
||||||
<Col :span="24">
|
<Col :span="24">
|
||||||
<FormItem label="输入参数" name="inputs">
|
<FormItem label="输入参数" name="inputs">
|
||||||
<div class="parameter-preview">
|
<div class="parameter-preview">
|
||||||
<Table :columns="inputColumns" :data-source="formData.inputs" :pagination="false">
|
<Table
|
||||||
|
:columns="inputColumns"
|
||||||
|
:data-source="formData.inputs"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record, index }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'dataType'">
|
<template v-if="column.key === 'dataType'">
|
||||||
{{ dataTypeOptions.find((item) => item.value === record.valueParams.dataType)?.label }}
|
{{
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.dataType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'required'">
|
<template v-if="column.key === 'required'">
|
||||||
<Tag color="processing">
|
<Tag color="processing">
|
||||||
|
@ -266,12 +315,26 @@ watch(
|
||||||
</Tag>
|
</Tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'formType'">
|
<template v-if="column.key === 'formType'">
|
||||||
{{ formTypeOptions.find((item) => item.value === record.valueParams.formType)?.label }}
|
{{
|
||||||
|
formTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.formType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" size="small" @click="handleEditInputParam(record, index)"> 编辑 </Button>
|
<Button
|
||||||
<Popconfirm placement="left" title="确认删除?" @confirm="handleDeleteInputParam(index)">
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleEditInputParam(record, index)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
placement="left"
|
||||||
|
title="确认删除?"
|
||||||
|
@confirm="handleDeleteInputParam(index)"
|
||||||
|
>
|
||||||
<Button type="link" size="small" danger> 删除 </Button>
|
<Button type="link" size="small" danger> 删除 </Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
|
@ -279,7 +342,11 @@ watch(
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
<div class="add-button-container">
|
<div class="add-button-container">
|
||||||
<a-button @click="handleAddInputParam" type="primary" class="add-button">
|
<a-button
|
||||||
|
@click="handleAddInputParam"
|
||||||
|
type="primary"
|
||||||
|
class="add-button"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
@ -294,7 +361,11 @@ watch(
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="是否异步" name="async">
|
<FormItem label="是否异步" name="async">
|
||||||
<RadioGroup v-model:value="formData.async" button-style="solid">
|
<RadioGroup v-model:value="formData.async" button-style="solid">
|
||||||
<RadioButton :value="item.value" v-for="item in asyncOptions" :key="item.value">
|
<RadioButton
|
||||||
|
:value="item.value"
|
||||||
|
v-for="item in asyncOptions"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
@ -303,8 +374,15 @@ watch(
|
||||||
|
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="是否开启前置校验" name="expands.preCheck">
|
<FormItem label="是否开启前置校验" name="expands.preCheck">
|
||||||
<RadioGroup v-model:value="formData.expands.preCheck" button-style="solid">
|
<RadioGroup
|
||||||
<RadioButton :value="item.value" v-for="item in preCheckOptions" :key="item.value">
|
v-model:value="formData.expands.preCheck"
|
||||||
|
button-style="solid"
|
||||||
|
>
|
||||||
|
<RadioButton
|
||||||
|
:value="item.value"
|
||||||
|
v-for="item in preCheckOptions"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
@ -331,13 +409,21 @@ watch(
|
||||||
|
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="排序" name="sort">
|
<FormItem label="排序" name="sort">
|
||||||
<InputNumber style="width: 100%" v-model:value="formData.sort" placeholder="请输入排序" />
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.sort"
|
||||||
|
placeholder="请输入排序"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col :span="24">
|
<Col :span="24">
|
||||||
<FormItem label="描述" name="description">
|
<FormItem label="描述" name="description">
|
||||||
<Textarea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
<Textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -346,7 +432,9 @@ watch(
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Space>
|
<Space>
|
||||||
<Button @click="handleDrawerClose">取消</Button>
|
<Button @click="handleDrawerClose">取消</Button>
|
||||||
<Button type="primary" @click="handleSave" :loading="saveLoading"> 确认 </Button>
|
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
|
||||||
|
|
||||||
import { Button, Drawer, message, Modal, Space, TabPane, Tabs } from 'ant-design-vue';
|
import {
|
||||||
|
Button,
|
||||||
|
Drawer,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
TabPane,
|
||||||
|
Tabs,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { productUpdateById } from '#/api/device/product';
|
import { productUpdateById } from '#/api/device/product';
|
||||||
|
|
||||||
|
@ -241,7 +250,9 @@ const handleSave = async () => {
|
||||||
metadata.propertyGroups = (metadata.propertyGroups || [])
|
metadata.propertyGroups = (metadata.propertyGroups || [])
|
||||||
.map((g: any) => {
|
.map((g: any) => {
|
||||||
const next = stripPkDeep(g);
|
const next = stripPkDeep(g);
|
||||||
next.properties = (next.properties || []).map((item) => stripPkDeep(item));
|
next.properties = (next.properties || []).map((item) =>
|
||||||
|
stripPkDeep(item),
|
||||||
|
);
|
||||||
return next;
|
return next;
|
||||||
})
|
})
|
||||||
.sort((a: any, b: any) => (a.sort ?? 0) - (b.sort ?? 0));
|
.sort((a: any, b: any) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||||
|
@ -288,25 +299,26 @@ const handleMetadataChange = (data: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听路由变化,提示未保存
|
// 监听路由变化,提示未保存
|
||||||
// const handleBeforeRouteLeave = (to: any, from: any, next: any) => {
|
const handleBeforeRouteLeave = (next: any) => {
|
||||||
// if (metadataChanged.value) {
|
if (metadataChanged.value) {
|
||||||
// Modal.confirm({
|
Modal.confirm({
|
||||||
// title: '提示',
|
title: '提示',
|
||||||
// content: '物模型有未保存的修改,确认离开吗?',
|
content: '物模型有未保存的修改,确认离开吗?',
|
||||||
// onOk: () => next(),
|
onOk: () => next(),
|
||||||
// onCancel: () => next(false),
|
});
|
||||||
// });
|
} else {
|
||||||
// } else {
|
next();
|
||||||
// next();
|
}
|
||||||
// }
|
};
|
||||||
// };
|
|
||||||
|
|
||||||
// 监听物模型数据变化
|
// 监听物模型数据变化
|
||||||
watch(
|
watch(
|
||||||
currentMetadata,
|
currentMetadata,
|
||||||
() => {
|
() => {
|
||||||
if (originalMetadata.value) {
|
if (originalMetadata.value) {
|
||||||
metadataChanged.value = JSON.stringify(currentMetadata.value) !== JSON.stringify(originalMetadata.value);
|
metadataChanged.value =
|
||||||
|
JSON.stringify(currentMetadata.value) !==
|
||||||
|
JSON.stringify(originalMetadata.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
|
@ -314,6 +326,16 @@ watch(
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
loadMetadata();
|
loadMetadata();
|
||||||
|
|
||||||
|
onBeforeRouteUpdate((to, from, next) => {
|
||||||
|
// 设备管理内路由跳转
|
||||||
|
handleBeforeRouteLeave(next as Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeRouteLeave((to, from, next) => {
|
||||||
|
// 设备管理外路由跳转
|
||||||
|
handleBeforeRouteLeave(next as Function);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -321,16 +343,36 @@ loadMetadata();
|
||||||
<div class="metadata-header">
|
<div class="metadata-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h3>物模型</h3>
|
<h3>物模型</h3>
|
||||||
<span class="desc"> 设备会默认继承产品的物模型,继承的物模型不支持删改 </span>
|
<span class="desc">
|
||||||
|
设备会默认继承产品的物模型,继承的物模型不支持删改
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<Space>
|
<Space>
|
||||||
<Button v-if="metadataChanged" type="primary" @click="handleSave" v-access:code="['device:product:edit']">
|
<Button
|
||||||
|
v-if="metadataChanged"
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
v-access:code="['device:product:edit']"
|
||||||
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="showReset" @click="handleReset" v-access:code="['device:product:edit']"> 重置操作 </Button>
|
<Button
|
||||||
<Button @click="handleImport" v-access:code="['device:product:edit']"> 快速导入 </Button>
|
v-if="showReset"
|
||||||
<Button @click="handleViewTSL" v-access:code="['device:product:edit']"> 物模型 </Button>
|
@click="handleReset"
|
||||||
|
v-access:code="['device:product:edit']"
|
||||||
|
>
|
||||||
|
重置操作
|
||||||
|
</Button>
|
||||||
|
<Button @click="handleImport" v-access:code="['device:product:edit']">
|
||||||
|
快速导入
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
@click="handleViewTSL"
|
||||||
|
v-access:code="['device:product:edit']"
|
||||||
|
>
|
||||||
|
物模型
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -376,12 +418,22 @@ loadMetadata();
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<!-- 导入抽屉 -->
|
<!-- 导入抽屉 -->
|
||||||
<Drawer v-model:open="importVisible" title="快速导入" width="600px" @close="handleImportClose">
|
<Drawer
|
||||||
|
v-model:open="importVisible"
|
||||||
|
title="快速导入"
|
||||||
|
width="600px"
|
||||||
|
@close="handleImportClose"
|
||||||
|
>
|
||||||
<ImportForm @success="handleImportSuccess" />
|
<ImportForm @success="handleImportSuccess" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<!-- TSL查看抽屉 -->
|
<!-- TSL查看抽屉 -->
|
||||||
<Drawer v-model:open="tslVisible" title="物模型" width="800px" @close="handleTSLClose">
|
<Drawer
|
||||||
|
v-model:open="tslVisible"
|
||||||
|
title="物模型"
|
||||||
|
width="800px"
|
||||||
|
@close="handleTSLClose"
|
||||||
|
>
|
||||||
<TSLViewer
|
<TSLViewer
|
||||||
:product-id="productInfo.id"
|
:product-id="productInfo.id"
|
||||||
:product-info="productInfo"
|
:product-info="productInfo"
|
||||||
|
|
|
@ -45,6 +45,7 @@ interface ParameterItem {
|
||||||
max?: number;
|
max?: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
|
step?: number;
|
||||||
trueText?: string;
|
trueText?: string;
|
||||||
trueValue?: string;
|
trueValue?: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
@ -447,6 +448,20 @@ watch(
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<FormItem
|
||||||
|
label="步长"
|
||||||
|
name="valueParams.step"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.step"
|
||||||
|
placeholder="请输入步长"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
label="小数位"
|
label="小数位"
|
||||||
name="valueParams.scale"
|
name="valueParams.scale"
|
||||||
|
|
|
@ -52,6 +52,7 @@ interface PropertyData {
|
||||||
max?: number;
|
max?: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
|
step?: number;
|
||||||
trueText?: string;
|
trueText?: string;
|
||||||
trueValue?: string;
|
trueValue?: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
@ -105,7 +106,12 @@ const filterFormTypeOptions = computed(() => {
|
||||||
}
|
}
|
||||||
case 'string': {
|
case 'string': {
|
||||||
return formTypeOptions.filter((item) => {
|
return formTypeOptions.filter((item) => {
|
||||||
return item.value === 'input' || item.value === 'switch' || item.value === 'select' || item.value === 'time';
|
return (
|
||||||
|
item.value === 'input' ||
|
||||||
|
item.value === 'switch' ||
|
||||||
|
item.value === 'select' ||
|
||||||
|
item.value === 'time'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
@ -118,7 +124,9 @@ const filterTimeOptions = computed(() => {
|
||||||
switch (formData?.value?.valueParams?.dataType) {
|
switch (formData?.value?.valueParams?.dataType) {
|
||||||
case 'date': {
|
case 'date': {
|
||||||
return timeOptions.filter((item) => {
|
return timeOptions.filter((item) => {
|
||||||
return item.value === 'YYYY-MM-DD' || item.value === 'YYYY-MM-DD HH:mm:ss';
|
return (
|
||||||
|
item.value === 'YYYY-MM-DD' || item.value === 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
@ -212,7 +220,10 @@ const handleDataTypeChange = (value: string) => {
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
Object.assign(formData.value, JSON.parse(JSON.stringify(defaultPropertyData)));
|
Object.assign(
|
||||||
|
formData.value,
|
||||||
|
JSON.parse(JSON.stringify(defaultPropertyData)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开枚举列表编辑弹窗
|
// 打开枚举列表编辑弹窗
|
||||||
|
@ -275,7 +286,12 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Drawer v-model:open="visible" :title="drawerTitle" width="800px" @close="handleDrawerClose">
|
<Drawer
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="drawerTitle"
|
||||||
|
width="800px"
|
||||||
|
@close="handleDrawerClose"
|
||||||
|
>
|
||||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||||
<Row :gutter="24">
|
<Row :gutter="24">
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
|
@ -323,7 +339,13 @@ watch(
|
||||||
<Col :span="24" v-if="formData.valueParams.formType === 'switch'">
|
<Col :span="24" v-if="formData.valueParams.formType === 'switch'">
|
||||||
<Row
|
<Row
|
||||||
:gutter="24"
|
:gutter="24"
|
||||||
style="padding-top: 16px; margin: 0; margin-bottom: 16px; border: 1px solid #f0f0f0; border-radius: 4px"
|
style="
|
||||||
|
padding-top: 16px;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="开关打开名称" name="valueParams.trueText">
|
<FormItem label="开关打开名称" name="valueParams.trueText">
|
||||||
|
@ -368,9 +390,14 @@ watch(
|
||||||
<Col :span="24" v-if="formData.valueParams.formType === 'select'">
|
<Col :span="24" v-if="formData.valueParams.formType === 'select'">
|
||||||
<FormItem label="枚举列表" name="valueParams.enumConf">
|
<FormItem label="枚举列表" name="valueParams.enumConf">
|
||||||
<div class="enum-preview">
|
<div class="enum-preview">
|
||||||
<a-button type="primary" @click="handleEditEnum"> 编辑枚举列表 </a-button>
|
<a-button type="primary" @click="handleEditEnum">
|
||||||
|
编辑枚举列表
|
||||||
|
</a-button>
|
||||||
<Descriptions
|
<Descriptions
|
||||||
v-if="formData.valueParams.enumConf && formData.valueParams.enumConf.length > 0"
|
v-if="
|
||||||
|
formData.valueParams.enumConf &&
|
||||||
|
formData.valueParams.enumConf.length > 0
|
||||||
|
"
|
||||||
:column="4"
|
:column="4"
|
||||||
bordered
|
bordered
|
||||||
title=" "
|
title=" "
|
||||||
|
@ -390,14 +417,20 @@ watch(
|
||||||
<!-- 数字类型配置 -->
|
<!-- 数字类型配置 -->
|
||||||
<Col
|
<Col
|
||||||
:span="12"
|
:span="12"
|
||||||
v-if="formData.valueParams.formType === 'number' || formData.valueParams.formType === 'progress'"
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<FormItem label="最小值" name="valueParams.min">
|
<FormItem label="最小值" name="valueParams.min">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-model:value="formData.valueParams.min"
|
v-model:value="formData.valueParams.min"
|
||||||
:precision="
|
:precision="
|
||||||
formData.valueParams.dataType === 'double' || formData.valueParams.dataType === 'float' ? undefined : 0
|
formData.valueParams.dataType === 'double' ||
|
||||||
|
formData.valueParams.dataType === 'float'
|
||||||
|
? undefined
|
||||||
|
: 0
|
||||||
"
|
"
|
||||||
placeholder="请输入最小值"
|
placeholder="请输入最小值"
|
||||||
/>
|
/>
|
||||||
|
@ -405,19 +438,41 @@ watch(
|
||||||
</Col>
|
</Col>
|
||||||
<Col
|
<Col
|
||||||
:span="12"
|
:span="12"
|
||||||
v-if="formData.valueParams.formType === 'number' || formData.valueParams.formType === 'progress'"
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<FormItem label="最大值" name="valueParams.max">
|
<FormItem label="最大值" name="valueParams.max">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-model:value="formData.valueParams.max"
|
v-model:value="formData.valueParams.max"
|
||||||
:precision="
|
:precision="
|
||||||
formData.valueParams.dataType === 'double' || formData.valueParams.dataType === 'float' ? undefined : 0
|
formData.valueParams.dataType === 'double' ||
|
||||||
|
formData.valueParams.dataType === 'float'
|
||||||
|
? undefined
|
||||||
|
: 0
|
||||||
"
|
"
|
||||||
placeholder="请输入最大值"
|
placeholder="请输入最大值"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col
|
||||||
|
:span="12"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FormItem label="步长" name="valueParams.step">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.step"
|
||||||
|
placeholder="请输入步长"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col :span="12" v-if="formData.valueParams.formType === 'input'">
|
<Col :span="12" v-if="formData.valueParams.formType === 'input'">
|
||||||
<FormItem label="长度" name="valueParams.length">
|
<FormItem label="长度" name="valueParams.length">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
@ -428,14 +483,27 @@ watch(
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="12" v-if="formData.valueParams.dataType === 'double' || formData.valueParams.dataType === 'float'">
|
<Col
|
||||||
|
:span="12"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.dataType === 'double' ||
|
||||||
|
formData.valueParams.dataType === 'float'
|
||||||
|
"
|
||||||
|
>
|
||||||
<FormItem label="小数位" name="valueParams.scale">
|
<FormItem label="小数位" name="valueParams.scale">
|
||||||
<InputNumber style="width: 100%" v-model:value="formData.valueParams.scale" placeholder="请输入小数位" />
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.scale"
|
||||||
|
placeholder="请输入小数位"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="单位" name="valueParams.unit">
|
<FormItem label="单位" name="valueParams.unit">
|
||||||
<Input v-model:value="formData.valueParams.unit" placeholder="请输入单位" />
|
<Input
|
||||||
|
v-model:value="formData.valueParams.unit"
|
||||||
|
placeholder="请输入单位"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="12" v-if="formData.valueParams.formType === 'time'">
|
<Col :span="12" v-if="formData.valueParams.formType === 'time'">
|
||||||
|
@ -456,8 +524,15 @@ watch(
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="读写类型" name="type">
|
<FormItem label="读写类型" name="type">
|
||||||
<RadioGroup v-model:value="formData.expands.type" button-style="solid">
|
<RadioGroup
|
||||||
<RadioButton :value="item.value" v-for="item in readWriteTypeOptions" :key="item.value">
|
v-model:value="formData.expands.type"
|
||||||
|
button-style="solid"
|
||||||
|
>
|
||||||
|
<RadioButton
|
||||||
|
:value="item.value"
|
||||||
|
v-for="item in readWriteTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
@ -466,13 +541,21 @@ watch(
|
||||||
|
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="排序" name="sort">
|
<FormItem label="排序" name="sort">
|
||||||
<InputNumber style="width: 100%" v-model:value="formData.sort" placeholder="请输入排序" />
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.sort"
|
||||||
|
placeholder="请输入排序"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col :span="24">
|
<Col :span="24">
|
||||||
<FormItem label="描述" name="description">
|
<FormItem label="描述" name="description">
|
||||||
<Textarea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
<Textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -481,7 +564,9 @@ watch(
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Space>
|
<Space>
|
||||||
<Button @click="handleDrawerClose">取消</Button>
|
<Button @click="handleDrawerClose">取消</Button>
|
||||||
<Button type="primary" @click="handleSave" :loading="saveLoading"> 确认 </Button>
|
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,10 @@ import { Page } from '@vben/common-ui';
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||||
import { message, Modal, Switch, TabPane, Tabs } from 'ant-design-vue';
|
import { message, Modal, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
import { productPushMetadataById, productUpdateStatus } from '#/api/device/product';
|
import {
|
||||||
|
productPushMetadataById,
|
||||||
|
productUpdateStatus,
|
||||||
|
} from '#/api/device/product';
|
||||||
import { useProductStore } from '#/store/product';
|
import { useProductStore } from '#/store/product';
|
||||||
|
|
||||||
import BasicInfo from './components/BasicInfo.vue';
|
import BasicInfo from './components/BasicInfo.vue';
|
||||||
|
@ -145,15 +148,29 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签页内容 -->
|
<!-- 标签页内容 -->
|
||||||
<Tabs v-model:active-key="activeTab" class="detail-tabs" @change="handleTabChange">
|
<Tabs
|
||||||
|
v-model:active-key="activeTab"
|
||||||
|
class="detail-tabs"
|
||||||
|
@change="handleTabChange"
|
||||||
|
>
|
||||||
<TabPane key="BasicInfo" tab="配置信息">
|
<TabPane key="BasicInfo" tab="配置信息">
|
||||||
<BasicInfo :product-info="currentProduct" @refresh="loadProductInfo" />
|
<BasicInfo
|
||||||
|
:product-info="currentProduct"
|
||||||
|
@refresh="loadProductInfo"
|
||||||
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="Metadata" tab="物模型">
|
<TabPane key="Metadata" tab="物模型">
|
||||||
<Metadata :product-id="productId" :product-info="currentProduct" @refresh="loadProductInfo" />
|
<Metadata
|
||||||
|
:product-id="productId"
|
||||||
|
:product-info="currentProduct"
|
||||||
|
@refresh="loadProductInfo"
|
||||||
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="DeviceAccess" tab="设备接入">
|
<TabPane key="DeviceAccess" tab="设备接入">
|
||||||
<DeviceAccess :product-info="currentProduct" @refresh="loadProductInfo" />
|
<DeviceAccess
|
||||||
|
:product-info="currentProduct"
|
||||||
|
@refresh="loadProductInfo"
|
||||||
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue