feat: 添加设备日志查询功能

- 新增设备日志查询 API
- 实现事件、功能、上下线日志查询
- 优化属性日志查询接口
- 修复事件和功能模拟相关问题
This commit is contained in:
fhysy 2025-09-11 15:08:01 +08:00
parent 8d0b69cd4c
commit 2925fce7fb
13 changed files with 1194 additions and 258 deletions

View File

@ -34,6 +34,46 @@ export function deviceInfo(id: ID) {
return requestClient.get<DeviceVO>(`/device/device/${id}`);
}
/**
* (/)
* @param params
* @returns
*/
export function deviceLogList(params?: DeviceQuery) {
return requestClient.post('/device/device/data/field', params);
}
/**
*
* @param params
* @returns
*/
export function functionLogList(params?: DeviceQuery) {
return requestClient.get('/device/functionLog/list', {
params,
});
}
/**
* 线
* @param params
* @returns 线
*/
export function onlineLogList(params?: DeviceQuery) {
return requestClient.get('/device/onlineLog/list', {
params,
});
}
/**
* (/)
* @param params
* @returns
*/
export function deviceUpDownLogList(params?: DeviceQuery) {
return requestClient.post('/device/device/log', params);
}
/**
*
* @param data

View File

@ -4,14 +4,15 @@ import { onMounted, ref } from 'vue';
import {
Button,
DatePicker,
Empty,
Modal,
Select,
Space,
Table,
Tag,
} from 'ant-design-vue';
import { Dayjs } from 'dayjs';
import dayjs, { Dayjs } from 'dayjs';
import { deviceUpDownLogList } from '#/api/device/device';
interface Props {
deviceId: string;
@ -32,15 +33,15 @@ const pagination = ref({ current: 1, pageSize: 10, total: 0 });
//
const logTypeOptions = [
{ label: '全部', value: '' },
{ label: '上报', value: 'upload' },
{ label: '下发', value: 'download' },
{ label: '上报', value: 'up' },
{ label: '下发', value: 'down' },
];
//
const columns = [
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 200 },
{ title: '类型', dataIndex: 'logType', key: 'logType', width: 120 },
{ title: '名称内容', dataIndex: 'content', key: 'content', ellipsis: true },
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 200 },
{ title: '操作', key: 'action', width: 100 },
];
@ -49,7 +50,13 @@ const viewVisible = ref(false);
const viewRecord = ref<any>(null);
const openView = (record: any) => {
viewRecord.value = record;
let content = '';
try {
content = JSON.stringify(JSON.parse(record.content), null, 2);
} catch {
content = record.content;
}
viewRecord.value = content;
viewVisible.value = true;
};
@ -62,53 +69,52 @@ const closeView = () => {
const loadList = async () => {
try {
loading.value = true;
const [start, end] = timeRange.value || [];
let startTime = null;
let endTime = null;
if (timeRange.value?.length === 2) {
startTime = timeRange.value[0].valueOf();
endTime = timeRange.value[1].valueOf();
}
const params = {
deviceId: props.deviceId,
startTime: start?.format('YYYY-MM-DD HH:mm:ss'),
endTime: end?.format('YYYY-MM-DD HH:mm:ss'),
logType: logType.value || undefined,
pageNo: pagination.value.current,
pageSize: pagination.value.pageSize,
deviceKey: props.deviceInfo.deviceKey,
productKey: props.deviceInfo.productObj.productKey,
// direction: logType.value,
orderType: 2,
current: pagination.value.current,
size: pagination.value.pageSize,
listWhere: [],
};
if (startTime && endTime) {
params.startTime = startTime;
params.endTime = endTime;
}
if (logType.value) {
params.listWhere.push({
field: 'direction',
val: logType.value,
operator: 'eq',
valType: 'string',
});
}
// if (messageId.value) {
// params.listWhere.push({
// field: 'msg_id',
// val: messageId.value?.trim(),
// operator: 'eq',
// valType: 'string',
// });
// }
console.log('query logs with', params);
// TODO:
const mock = [
{
id: 1,
timestamp: '2025-01-20 10:30:15',
logType: '上报',
content: '设备启动成功固件版本v1.0.0',
},
{
id: 2,
timestamp: '2025-01-20 10:30:20',
logType: '下发',
content: '{"switch": "on"}',
},
{
id: 3,
timestamp: '2025-01-20 10:30:25',
logType: '上报',
content: '网络连接不稳定重试次数3',
},
{
id: 4,
timestamp: '2025-01-20 10:30:30',
logType: '下发',
content:
'功能执行失败错误码E001错误信息参数无效功能执行失败错误码E001错误信息参数无效功能执行失败错误码E001错误信息参数无效功能执行失败错误码E001错误信息参数无效',
},
{
id: 5,
timestamp: '2025-01-20 10:30:35',
logType: '上报',
content: '调试信息:设备响应时间 150ms',
},
];
dataSource.value = mock;
pagination.value.total = mock.length;
const data = await deviceUpDownLogList(params);
dataSource.value = data?.records.map((item: any) => {
return {
messageId: item.msg_id,
logType: item.direction,
timestamp: dayjs(item.time).format('YYYY-MM-DD HH:mm:ss'),
content: item.log_data,
};
});
pagination.value.total = data.total;
} finally {
loading.value = false;
}
@ -141,7 +147,7 @@ onMounted(() => {
<div class="log-management">
<!-- 查询区 -->
<div class="query-bar">
<Space>
<Space wrap>
<span>日志类型:</span>
<Select
v-model:value="logType"
@ -173,8 +179,11 @@ onMounted(() => {
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'logType'">
<Tag :color="record.logType === '上报' ? 'blue' : 'green'">
{{ record.logType }}
<Tag :color="record.logType === 'up' ? 'blue' : 'green'">
{{
logTypeOptions.find((item) => item.value === record.logType)
?.label
}}
</Tag>
</template>
<template v-if="column.key === 'action'">
@ -185,10 +194,6 @@ onMounted(() => {
</template>
</Table>
<div v-if="!loading && dataSource.length === 0" class="empty-wrap">
<Empty />
</div>
<!-- 查看内容弹窗 -->
<Modal
v-model:open="viewVisible"
@ -197,22 +202,7 @@ onMounted(() => {
@cancel="closeView"
:footer="null"
>
<div class="log-detail">
<div class="detail-item">
<span class="label">类型:</span>
<Tag :color="viewRecord?.logType === '上报' ? 'blue' : 'green'">
{{ viewRecord?.logType }}
</Tag>
</div>
<div class="detail-item">
<span class="label">时间:</span>
<span>{{ viewRecord?.timestamp }}</span>
</div>
<div class="detail-item">
<span class="label">内容:</span>
<pre class="content-view">{{ viewRecord?.content }}</pre>
</div>
</div>
<pre class="json-view">{{ viewRecord }}</pre>
</Modal>
</div>
</template>
@ -253,4 +243,15 @@ onMounted(() => {
}
}
}
.json-view {
max-height: 420px;
padding: 12px;
margin: 0;
overflow: auto;
word-break: break-all;
white-space: pre-wrap;
background: #fafafa;
border-radius: 4px;
}
</style>

View File

@ -4,6 +4,9 @@ import { computed } from 'vue';
import { TabPane, Tabs } from 'ant-design-vue';
import EventsPanel from './running/EventsPanel.vue';
import FunctionPanel from './running/FunctionPanel.vue';
import OnlineStatusPanel from './running/OnlineStatusPanel.vue';
import PropertyPanel from './running/PropertyPanel.vue';
import RealtimePanel from './running/RealtimePanel.vue';
interface Props {
@ -17,23 +20,35 @@ const props = defineProps<Props>();
const metadata = computed(() => {
try {
const raw = props.deviceInfo?.productObj?.metadata;
if (!raw) return { properties: [], propertyGroups: [], events: [] } as any;
if (!raw)
return {
properties: [],
propertyGroups: [],
events: [],
functions: [],
} as any;
const obj = JSON.parse(raw || '{}');
return {
properties: obj?.properties || [],
propertyGroups: obj?.propertyGroups || [],
events: obj?.events || [],
functions: obj?.functions || [],
};
} catch (error) {
console.warn('parse metadata error', error);
return { properties: [], propertyGroups: [], events: [] } as any;
return {
properties: [],
propertyGroups: [],
events: [],
functions: [],
} as any;
}
});
</script>
<template>
<div class="running-status">
<Tabs type="card">
<Tabs type="card" :destroy-inactive-tab-pane="true">
<TabPane key="realtime" tab="实时数据">
<RealtimePanel
:device-id="props.deviceId"
@ -41,8 +56,33 @@ const metadata = computed(() => {
:device-info="deviceInfo"
/>
</TabPane>
<TabPane key="property" tab="属性">
<PropertyPanel
:device-id="props.deviceId"
:metadata="metadata"
:device-info="deviceInfo"
/>
</TabPane>
<TabPane key="events" tab="事件">
<EventsPanel :device-id="props.deviceId" :metadata="metadata" />
<EventsPanel
:device-id="props.deviceId"
:metadata="metadata"
:device-info="deviceInfo"
/>
</TabPane>
<TabPane key="functions" tab="功能">
<FunctionPanel
:device-id="props.deviceId"
:metadata="metadata"
:device-info="deviceInfo"
/>
</TabPane>
<TabPane key="online" tab="上下线">
<OnlineStatusPanel
:device-id="props.deviceId"
:metadata="metadata"
:device-info="deviceInfo"
/>
</TabPane>
</Tabs>
</div>

View File

@ -4,7 +4,6 @@ import { computed, onMounted, ref } from 'vue';
import {
Button,
DatePicker,
Empty,
Input,
Modal,
Select,
@ -13,9 +12,12 @@ import {
} from 'ant-design-vue';
import dayjs, { Dayjs } from 'dayjs';
import { deviceLogList } from '#/api/device/device';
interface Props {
deviceId: string;
metadata: any;
deviceInfo: any;
}
const props = defineProps<Props>();
@ -41,18 +43,21 @@ const eventNameMap = computed<Record<string, string>>(() => {
//
const eventOptions = computed(() => {
return (props.metadata?.events || []).map((e: any) => ({
label: e.name || e.id,
value: e.id,
}));
return [
{ label: '全部', value: '' },
...(props.metadata?.events || []).map((e: any) => ({
label: e.name || e.id,
value: e.id,
})),
];
});
//
const columns = [
{ title: '消息ID', dataIndex: 'messageId', key: 'messageId', width: 220 },
{ title: '名称', dataIndex: 'eventName', key: 'eventName', width: 160 },
{ title: '标识', dataIndex: 'eventId', key: 'eventId', width: 160 },
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 200 },
{ title: '消息ID', dataIndex: 'messageId', key: 'messageId', width: 140 },
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 160 },
{ title: '事件', dataIndex: 'eventName', key: 'eventName', width: 200 },
{ title: '内容', dataIndex: 'content', key: 'content', ellipsis: true },
{ title: '操作', key: 'action', width: 100 },
];
@ -61,7 +66,13 @@ const viewVisible = ref(false);
const viewRecord = ref<any>(null);
const openView = (record: any) => {
viewRecord.value = record;
let content = '';
try {
content = JSON.stringify(JSON.parse(record.content), null, 2);
} catch {
content = record.content;
}
viewRecord.value = content;
viewVisible.value = true;
};
@ -74,30 +85,53 @@ const closeView = () => {
const loadList = async () => {
try {
loading.value = true;
const [start, end] = timeRange.value || [];
const params = {
deviceId: props.deviceId,
startTime: start?.format('YYYY-MM-DD HH:mm:ss'),
endTime: end?.format('YYYY-MM-DD HH:mm:ss'),
messageId: messageId.value?.trim() || undefined,
eventId: selectedEventId.value || undefined,
pageNo: pagination.value.current,
pageSize: pagination.value.pageSize,
let startTime = null;
let endTime = null;
if (timeRange.value?.length === 2) {
startTime = timeRange.value[0].valueOf();
endTime = timeRange.value[1].valueOf();
}
const params: any = {
deviceKey: props.deviceInfo.deviceKey,
productKey: props.deviceInfo.productObj.productKey,
fields: selectedEventId.value || undefined,
orderType: 2,
current: pagination.value.current,
size: pagination.value.pageSize,
metaDataType: 'event',
listWhere: [],
};
console.log('query events with', params);
// TODO:
const eid =
selectedEventId.value || props.metadata?.events?.[0]?.id || 'temple';
const mock = Array.from({ length: 5 }).map((_, i) => ({
messageId: `${Date.now()}_${i}`,
eventId: eid,
eventName: eventNameMap.value[eid] || '事件',
payload: { level: 'info', msg: '模拟事件' },
timestamp: dayjs().format('YYYY-MM-DD HH:mm:ss.SSS'),
}));
dataSource.value = mock;
pagination.value.total = mock.length;
if (startTime && endTime) {
params.startTime = startTime;
params.endTime = endTime;
}
if (messageId.value) {
params.listWhere.push({
field: 'msg_id',
val: messageId.value?.trim(),
operator: 'eq',
valType: 'string',
});
}
if (selectedEventId.value) {
params.listWhere.push({
field: 'eventId',
val: selectedEventId.value?.trim(),
operator: 'eq',
valType: 'string',
});
}
const data = await deviceLogList(params);
console.log('eventNameMap', eventNameMap);
dataSource.value = data.records.map((item: any) => {
return {
messageId: item.msg_id,
eventName: eventNameMap.value[item.eventId],
timestamp: dayjs(item.time).format('YYYY-MM-DD HH:mm:ss'),
content: item.raw_data,
};
});
pagination.value.total = data.total;
} finally {
loading.value = false;
}
@ -131,7 +165,7 @@ onMounted(() => {
<div class="events-panel">
<!-- 查询区 -->
<div class="query-bar">
<Space>
<Space wrap>
<span>消息ID:</span>
<Input
v-model:value="messageId"
@ -146,6 +180,7 @@ onMounted(() => {
allow-clear
placeholder="请选择事件"
style="width: 220px"
@change="handleSearch"
/>
<span>时间范围:</span>
<DatePicker.RangePicker
@ -153,6 +188,7 @@ onMounted(() => {
:show-time="true"
format="YYYY-MM-DD HH:mm:ss"
style="width: 360px"
@change="handleSearch"
/>
<Button @click="handleReset">重置</Button>
<Button type="primary" @click="handleSearch">查询</Button>
@ -177,10 +213,6 @@ onMounted(() => {
</template>
</Table>
<div v-if="!loading && dataSource.length === 0" class="empty-wrap">
<Empty />
</div>
<!-- 查看内容弹窗 -->
<Modal
v-model:open="viewVisible"
@ -189,9 +221,7 @@ onMounted(() => {
@cancel="closeView"
:footer="null"
>
<pre class="json-view">{{
JSON.stringify(viewRecord?.payload ?? {}, null, 2)
}}</pre>
<pre class="json-view">{{ viewRecord }}</pre>
</Modal>
</div>
</template>
@ -206,14 +236,14 @@ onMounted(() => {
padding: 40px 0;
text-align: center;
}
}
.json-view {
max-height: 420px;
padding: 12px;
margin: 0;
overflow: auto;
background: #fafafa;
border-radius: 4px;
}
.json-view {
max-height: 420px;
padding: 12px;
margin: 0;
overflow: auto;
background: #fafafa;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,241 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import {
Button,
DatePicker,
Input,
Modal,
Select,
Space,
Table,
} from 'ant-design-vue';
import { Dayjs } from 'dayjs';
import { functionLogList } from '#/api/device/device';
interface Props {
deviceId: string;
metadata: any;
deviceInfo: any;
}
const props = defineProps<Props>();
//
const messageId = ref<string>('');
const timeRange = ref<[Dayjs, Dayjs] | undefined>();
const selectedFuncId = ref<string | undefined>();
//
const loading = ref(false);
const dataSource = ref<any[]>([]);
const pagination = ref({ current: 1, pageSize: 10, total: 0 });
// id -> name
const funcNameMap = computed<Record<string, string>>(() => {
const map: Record<string, string> = {};
(props.metadata?.functions || []).forEach((f: any) => {
map[f.id] = f.name || f.id;
});
return {
defaultWrite: '设备写入',
defaultRead: '设备读取',
...map,
};
});
//
const funcOptions = computed(() => {
return [
{ label: '全部', value: '' },
{ label: '设备写入', value: 'defaultWrite' },
{ label: '设备读取', value: 'defaultRead' },
...(props.metadata?.functions || []).map((f: any) => ({
label: f.name || f.id,
value: f.id,
})),
];
});
//
const columns = [
{ title: '消息ID', dataIndex: 'messageId', key: 'messageId', width: 180 },
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 160 },
{ title: '功能', dataIndex: 'funcName', key: 'funcName', width: 200 },
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 120 },
{ title: '内容', dataIndex: 'content', key: 'content', ellipsis: true },
{ title: '操作', key: 'action', width: 100 },
];
//
const viewVisible = ref(false);
const viewRecord = ref<any>(null);
const openView = (record: any) => {
let content = '';
try {
content = JSON.stringify(JSON.parse(record.content), null, 2);
} catch {
content = record.content;
}
viewRecord.value = content;
viewVisible.value = true;
};
const closeView = () => {
viewVisible.value = false;
viewRecord.value = null;
};
//
const loadList = async () => {
try {
loading.value = true;
let startTime = null;
let endTime = null;
if (timeRange.value?.length === 2) {
startTime = timeRange.value[0].format('YYYY-MM-DD HH:mm:ss');
endTime = timeRange.value[1].format('YYYY-MM-DD HH:mm:ss');
}
const params: any = {
deviceKey: props.deviceInfo.deviceKey,
productKey: props.deviceInfo.productObj.productKey,
msgId: messageId.value?.trim() || undefined,
funcId: selectedFuncId.value || undefined,
isAsc: 'desc',
pageNum: pagination.value.current,
pageSize: pagination.value.pageSize,
listWhere: [],
params: {
beginTime: undefined,
endTime: undefined,
},
};
if (startTime && endTime) {
params.params.beginTime = startTime;
params.params.endTime = endTime;
}
const data = await functionLogList(params);
dataSource.value = (data.rows || []).map((item: any) => {
return {
messageId: item.msgId,
funcName: funcNameMap.value[item.funcId] || item.funcId,
timestamp: item.serverTime,
operator: item.operatorName || '-',
content: item.funcParams || '-',
};
});
pagination.value.total = data.total || 0;
} finally {
loading.value = false;
}
};
const handleSearch = () => {
pagination.value.current = 1;
loadList();
};
const handleReset = () => {
messageId.value = '';
timeRange.value = undefined;
selectedFuncId.value = undefined;
pagination.value.current = 1;
loadList();
};
const handleTableChange = (page: any) => {
pagination.value.current = page.current;
pagination.value.pageSize = page.pageSize;
loadList();
};
onMounted(() => {
loadList();
});
</script>
<template>
<div class="function-panel">
<!-- 查询区 -->
<div class="query-bar">
<Space wrap>
<span>消息ID:</span>
<Input
v-model:value="messageId"
placeholder="请输入"
style="width: 220px"
allow-clear
/>
<span>功能:</span>
<Select
v-model:value="selectedFuncId"
:options="funcOptions"
allow-clear
placeholder="请选择功能"
style="width: 220px"
@change="handleSearch"
/>
<span>时间范围:</span>
<DatePicker.RangePicker
v-model:value="timeRange"
:show-time="true"
format="YYYY-MM-DD HH:mm:ss"
style="width: 360px"
@change="handleSearch"
/>
<Button @click="handleReset">重置</Button>
<Button type="primary" @click="handleSearch">查询</Button>
</Space>
</div>
<!-- 列表 -->
<Table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="messageId"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<Button type="link" size="small" @click="openView(record)">
查看
</Button>
</template>
</template>
</Table>
<!-- 查看内容弹窗 -->
<Modal
v-model:open="viewVisible"
title="功能日志内容"
width="720px"
@cancel="closeView"
:footer="null"
>
<pre class="json-view">{{ viewRecord }}</pre>
</Modal>
</div>
</template>
<style lang="scss" scoped>
.function-panel {
.query-bar {
margin-bottom: 16px;
}
}
.json-view {
max-height: 420px;
padding: 12px;
margin: 0;
overflow: auto;
word-break: break-all;
white-space: pre-wrap;
background: #fafafa;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,204 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import {
Button,
DatePicker,
Input,
Modal,
Space,
Table,
Tag,
} from 'ant-design-vue';
import { Dayjs } from 'dayjs';
import { onlineLogList } from '#/api/device/device';
interface Props {
deviceId: string;
metadata: any;
deviceInfo: any;
}
const props = defineProps<Props>();
//
const messageId = ref<string>('');
const timeRange = ref<[Dayjs, Dayjs] | undefined>();
//
const loading = ref(false);
const dataSource = ref<any[]>([]);
const pagination = ref({ current: 1, pageSize: 10, total: 0 });
//
const columns = [
{ title: '消息ID', dataIndex: 'messageId', key: 'messageId', width: 180 },
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 160 },
{ title: '类型', dataIndex: 'type', key: 'type', width: 120 },
];
//
const viewVisible = ref(false);
const viewRecord = ref<any>(null);
const openView = (record: any) => {
let content = '';
try {
content = JSON.stringify(JSON.parse(record.content), null, 2);
} catch {
content = record.content;
}
viewRecord.value = content;
viewVisible.value = true;
};
const closeView = () => {
viewVisible.value = false;
viewRecord.value = null;
};
// 线
const loadList = async () => {
try {
loading.value = true;
let startTime = null;
let endTime = null;
if (timeRange.value?.length === 2) {
startTime = timeRange.value[0].format('YYYY-MM-DD HH:mm:ss');
endTime = timeRange.value[1].format('YYYY-MM-DD HH:mm:ss');
}
const params: any = {
deviceKey: props.deviceInfo.deviceKey,
productKey: props.deviceInfo.productObj.productKey,
msgId: messageId.value?.trim() || undefined,
isAsc: 'desc',
pageNum: pagination.value.current,
pageSize: pagination.value.pageSize,
params: {
beginTime: undefined,
endTime: undefined,
},
};
if (startTime && endTime) {
params.params.beginTime = startTime;
params.params.endTime = endTime;
}
const data = await onlineLogList(params);
dataSource.value = (data.rows || []).map((item: any) => {
return {
messageId: item.msgId,
timestamp: item.createTime,
type: item.deviceStatus,
content: item.raw_data ?? '-',
};
});
pagination.value.total = data.total || 0;
} finally {
loading.value = false;
}
};
const handleSearch = () => {
pagination.value.current = 1;
loadList();
};
const handleReset = () => {
messageId.value = '';
timeRange.value = undefined;
pagination.value.current = 1;
loadList();
};
const handleTableChange = (page: any) => {
pagination.value.current = page.current;
pagination.value.pageSize = page.pageSize;
loadList();
};
onMounted(() => {
loadList();
});
</script>
<template>
<div class="online-status-panel">
<!-- 查询区 -->
<div class="query-bar">
<Space wrap>
<span>消息ID:</span>
<Input
v-model:value="messageId"
placeholder="请输入"
style="width: 220px"
allow-clear
/>
<span>时间范围:</span>
<DatePicker.RangePicker
v-model:value="timeRange"
:show-time="true"
format="YYYY-MM-DD HH:mm:ss"
style="width: 360px"
@change="handleSearch"
/>
<Button @click="handleReset">重置</Button>
<Button type="primary" @click="handleSearch">查询</Button>
</Space>
</div>
<!-- 列表 -->
<Table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="messageId"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<Tag color="processing" v-if="record.type === 'online'">上线</Tag>
<Tag color="error" v-else-if="record.type === 'offline'">下线</Tag>
<span v-else>--</span>
</template>
<template v-if="column.key === 'action'">
<Button type="link" size="small" @click="openView(record)">
查看
</Button>
</template>
</template>
</Table>
<!-- 查看内容弹窗 -->
<Modal
v-model:open="viewVisible"
title="上下线日志内容"
width="720px"
@cancel="closeView"
:footer="null"
>
<pre class="json-view">{{ viewRecord }}</pre>
</Modal>
</div>
</template>
<style lang="scss" scoped>
.online-status-panel {
.query-bar {
margin-bottom: 16px;
}
.json-view {
max-height: 420px;
padding: 12px;
margin: 0;
overflow: auto;
background: #fafafa;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,202 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { Button, DatePicker, Input, Modal, Space, Table } from 'ant-design-vue';
import dayjs, { Dayjs } from 'dayjs';
import { deviceLogList } from '#/api/device/device';
interface Props {
deviceId: string;
metadata: any;
deviceInfo: any;
}
const props = defineProps<Props>();
//
const messageId = ref<string>('');
const timeRange = ref<[Dayjs, Dayjs] | undefined>();
//
const selectedGroup = ref('all');
//
const loading = ref(false);
const dataSource = ref<any[]>([]);
const pagination = ref({ current: 1, pageSize: 10, total: 0 });
//
const columns = [
{ title: '消息ID', dataIndex: 'messageId', key: 'messageId', width: 140 },
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 160 },
{ title: '内容', dataIndex: 'content', key: 'content', ellipsis: true },
{ title: '操作', key: 'action', width: 100 },
];
//
const viewVisible = ref(false);
const viewRecord = ref<any>(null);
const openView = (record: any) => {
let content = '';
try {
content = JSON.stringify(JSON.parse(record.content), null, 2);
} catch {
content = record.content;
}
viewRecord.value = content;
viewVisible.value = true;
};
const closeView = () => {
viewVisible.value = false;
viewRecord.value = null;
};
// API
const loadList = async () => {
try {
loading.value = true;
let startTime = null;
let endTime = null;
if (timeRange.value?.length === 2) {
startTime = timeRange.value[0].valueOf();
endTime = timeRange.value[1].valueOf();
}
const params: any = {
deviceKey: props.deviceInfo.deviceKey,
productKey: props.deviceInfo.productObj.productKey,
orderType: 2,
current: pagination.value.current,
size: pagination.value.pageSize,
metaDataType: 'property',
listWhere: [],
};
if (startTime && endTime) {
params.startTime = startTime;
params.endTime = endTime;
}
if (messageId.value) {
params.listWhere.push({
field: 'msg_id',
val: messageId.value?.trim(),
operator: 'eq',
valType: 'string',
});
}
const data = await deviceLogList(params);
dataSource.value = data.records.map((item: any) => {
return {
messageId: item.msg_id,
timestamp: dayjs(item.time).format('YYYY-MM-DD HH:mm:ss'),
content: item.raw_data,
};
});
pagination.value.total = data.total;
} finally {
loading.value = false;
}
};
const handleSearch = () => {
pagination.value.current = 1;
loadList();
};
const handleReset = () => {
messageId.value = '';
timeRange.value = undefined;
selectedGroup.value = 'all';
pagination.value.current = 1;
loadList();
};
const handleTableChange = (page: any) => {
pagination.value.current = page.current;
pagination.value.pageSize = page.pageSize;
loadList();
};
onMounted(() => {
loadList();
});
</script>
<template>
<div class="events-panel">
<!-- 查询区 -->
<div class="query-bar">
<Space wrap>
<span>消息ID:</span>
<Input
v-model:value="messageId"
placeholder="请输入"
style="width: 220px"
allow-clear
/>
<span>时间范围:</span>
<DatePicker.RangePicker
v-model:value="timeRange"
:show-time="true"
format="YYYY-MM-DD HH:mm:ss"
style="width: 360px"
@change="handleSearch"
/>
<Button @click="handleReset">重置</Button>
<Button type="primary" @click="handleSearch">查询</Button>
</Space>
</div>
<!-- 列表 -->
<Table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="messageId"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<Button type="link" size="small" @click="openView(record)">
查看
</Button>
</template>
</template>
</Table>
<!-- 查看内容弹窗 -->
<Modal
v-model:open="viewVisible"
title="属性内容"
width="720px"
@cancel="closeView"
:footer="null"
>
<pre class="json-view">{{ viewRecord }}</pre>
</Modal>
</div>
</template>
<style lang="scss" scoped>
.events-panel {
.query-bar {
margin-bottom: 16px;
}
.empty-wrap {
padding: 40px 0;
text-align: center;
}
}
.json-view {
max-height: 420px;
padding: 12px;
margin: 0;
overflow: auto;
background: #fafafa;
border-radius: 4px;
}
</style>

View File

@ -9,11 +9,11 @@ import {
Card,
Checkbox,
Col,
DatePicker,
Empty,
Modal,
RadioButton,
RadioGroup,
RangePicker,
Row,
Select,
Space,
@ -22,6 +22,7 @@ import {
} from 'ant-design-vue';
import dayjs from 'dayjs';
import { deviceLogList } from '#/api/device/device';
import { getWebSocket } from '#/utils/websocket';
import RealtimeChart from './RealtimeChart.vue';
@ -129,10 +130,33 @@ const logModalVisible = ref(false);
const currentProperty = ref<any>(null);
const logTabActiveKey = ref('list');
const activeQuickTime = ref('today');
const dates = ref<any>();
const logDateRange = ref([dayjs().startOf('day'), dayjs()]);
const logLoading = ref(false);
const logData = ref<any[]>([]);
const disabledDate = (current: Dayjs) => {
const now = dayjs();
//
if (current.isAfter(now, 'second')) return true;
if (!dates.value || (dates.value as any).length === 0) {
return false;
}
const tooLate = dates.value[0] && current.diff(dates.value[0], 'days') > 60;
const tooEarly = dates.value[1] && dates.value[1].diff(current, 'days') > 60;
return tooEarly || tooLate;
};
const onOpenChange = (open) => {
if (open) {
dates.value = [];
}
};
const onCalendarChange = (val: any) => {
dates.value = val;
};
const logColumns = [
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 200 },
{
@ -147,46 +171,52 @@ const logColumns = [
const openPropertyLog = (property: any) => {
currentProperty.value = property;
logTabActiveKey.value = 'list';
activeQuickTime.value = 'today';
logModalVisible.value = true;
loadPropertyLog();
const startTime = dayjs().startOf('day').valueOf();
const endTime = dayjs().valueOf();
loadPropertyLog(property.id, startTime, endTime);
};
const loadPropertyLog = async () => {
const loadPropertyLog = async (field, startTime, endTime) => {
if (!currentProperty.value) return;
try {
logLoading.value = true;
// TODO:
logData.value = [
{ timestamp: '2025-08-19 16:19:40', value: '0.5' },
{ timestamp: '2025-08-19 16:18:40', value: '0.4' },
{ timestamp: '2025-08-19 16:17:40', value: '0.3' },
{ timestamp: '2025-08-20 16:19:40', value: '0.5' },
{ timestamp: '2025-08-20 16:18:40', value: '0.4' },
{ timestamp: '2025-08-20 16:17:40', value: '0.3' },
{ timestamp: '2025-08-21 14:19:40', value: '0.5' },
{ timestamp: '2025-08-21 16:18:40', value: '0.4' },
{ timestamp: '2025-08-21 18:17:40', value: '0.3' },
{ timestamp: '2025-08-22 16:19:40', value: '0.5' },
{ timestamp: '2025-08-22 16:18:40', value: '0.4' },
{ timestamp: '2025-08-22 16:17:40', value: '0.3' },
{ timestamp: '2025-08-23 16:19:40', value: '0.5' },
{ timestamp: '2025-08-23 16:18:40', value: '0.4' },
{ timestamp: '2025-08-23 16:17:40', value: '0.3' },
{ timestamp: '2025-08-24 16:19:40', value: '0.5' },
{ timestamp: '2025-08-24 16:18:40', value: '0.4' },
{ timestamp: '2025-08-24 16:17:40', value: '0.3' },
{ timestamp: '2025-08-25 16:19:40', value: '0.5' },
{ timestamp: '2025-08-25 16:18:40', value: '0.4' },
{ timestamp: '2025-08-25 16:17:40', value: '0.3' },
];
const data = await deviceLogList({
deviceKey: props.deviceInfo.deviceKey,
productKey: props.deviceInfo.productObj.productKey,
fields: field,
startTime,
endTime,
orderType: 2,
metaDataType: 'property',
});
logData.value = data.records.map((item: any) => {
const itemValue = formatValue({
...currentProperty.value,
value: item[field],
});
return {
timestamp: dayjs(item.time).format('YYYY-MM-DD HH:mm:ss'),
value: itemValue,
};
});
} finally {
logLoading.value = false;
}
};
const handleDateRangeChange = (dates: any) => {
console.log('选择日期范围', dates);
logDateRange.value = dates;
if (dates && dates.length === 2) loadPropertyLog();
const [start, end] = dates;
if (dates && dates.length === 2) {
const startTime = start.valueOf();
const endTime = end.valueOf();
loadPropertyLog(currentProperty.value.id, startTime, endTime);
}
};
const handleQuickTimeSelect = (e: any) => {
@ -204,7 +234,9 @@ const handleQuickTimeSelect = (e: any) => {
logDateRange.value = [now.startOf('day'), now];
}
}
loadPropertyLog();
const startTime = logDateRange.value[0].valueOf();
const endTime = logDateRange.value[1].valueOf();
loadPropertyLog(currentProperty.value.id, startTime, endTime);
};
const formatValue = (property: any) => {
@ -361,11 +393,14 @@ watch(
<RadioButton value="week">近一周</RadioButton>
<RadioButton value="month">近一月</RadioButton>
</RadioGroup>
<DatePicker.RangePicker
<RangePicker
v-model:value="logDateRange"
:show-time="true"
format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disabledDate"
@change="handleDateRangeChange"
@open-change="onOpenChange"
@calendar-change="onCalendarChange"
/>
</Space>
</div>

View File

@ -132,18 +132,22 @@ const initializeSelectedRows = () => {
const initializeFormData = () => {
const defaultData = {};
currentOutputs.value.forEach((output) => {
const { formType } = output.valueParams;
const { formType, scale } = output.valueParams;
//
switch (formType) {
case 'number': {
// 0
defaultData[output.id] = output.valueParams.min || 0;
defaultData[output.id] = scale
? Number.parseFloat((output.valueParams.min || 0).toFixed(scale))
: output.valueParams.min || 0;
break;
}
case 'progress': {
//
defaultData[output.id] = output.valueParams.min || 0;
defaultData[output.id] = scale
? Number.parseFloat((output.valueParams.min || 0).toFixed(scale))
: output.valueParams.min || 0;
break;
}
case 'select': {
@ -184,17 +188,20 @@ watch(selectedEventId, (newEventId) => {
currentOutputs.value = selectedEvt.outputs || [];
//
initializeSelectedRows();
resetForm();
//
initializeFormData();
}
} else {
resetForm();
}
resetForm();
generateDefaultContent();
});
//
watch(currentMode, () => {
resetForm();
initializeFormData();
generateDefaultContent();
});
@ -285,7 +292,7 @@ const executeSubmit = async () => {
(isSwitch && value === false)
) {
if (output) {
const { dataType, formType } = output.valueParams;
const { dataType, formType, format } = output.valueParams;
let processedValue = value;
// dataTypeformType
@ -295,16 +302,18 @@ const executeSubmit = async () => {
processedValue = Boolean(value);
} else if (dataType === 'string') {
// string
processedValue = value
? output.valueParams.trueValue || 'true'
: output.valueParams.falseValue || 'false';
processedValue = value;
}
} 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(); //
let timeValue = value;
if (format === 'YYYY-MM-DD') {
timeValue += ' 00:00:00';
}
processedValue = new Date(timeValue).getTime(); //
}
}
@ -500,9 +509,15 @@ onMounted(() => {
record.valueParams.dataType === 'string'
"
v-model:checked="formData[record.id]"
:checked-value="
record?.valueParams?.trueValue || 'true'
"
:checked-children="
record?.valueParams?.trueText || '是'
"
:un-checked-value="
record?.valueParams?.falseValue || 'false'
"
:un-checked-children="
record?.valueParams?.falseText || '否'
"
@ -517,6 +532,7 @@ onMounted(() => {
record?.valueParams?.format?.includes('HH:mm:ss')
"
:format="record?.valueParams?.format"
:value-format="record?.valueParams?.format"
style="width: 100%"
/>
<TimePicker
@ -526,6 +542,7 @@ onMounted(() => {
"
v-model:value="formData[record.id]"
:format="record?.valueParams?.format"
:value-format="record?.valueParams?.format"
style="width: 100%"
/>
</Form.Item>

View File

@ -146,30 +146,34 @@ const initializeSelectedRows = () => {
const initializeFormData = () => {
const defaultData = {};
currentInputs.value.forEach((input) => {
const { formType } = input.valueParams;
const { formType, scale } = input.valueParams;
//
switch (formType) {
// case 'number': {
// // 0
// defaultData[input.id] = input.valueParams.min || 0;
// break;
// }
case 'progress': {
//
defaultData[input.id] = input.valueParams.min || 0;
case 'number': {
// 0
defaultData[input.id] = scale
? Number.parseFloat((input.valueParams.min || 0).toFixed(scale))
: input.valueParams.min || 0;
break;
}
case 'progress': {
//
defaultData[input.id] = scale
? Number.parseFloat((input.valueParams.min || 0).toFixed(scale))
: input.valueParams.min || 0;
break;
}
case 'select': {
//
if (
input.valueParams.enumConf &&
input.valueParams.enumConf.length > 0
) {
defaultData[input.id] = input.valueParams.enumConf[0].value;
}
break;
}
// case 'select': {
// //
// if (
// input.valueParams.enumConf &&
// input.valueParams.enumConf.length > 0
// ) {
// defaultData[input.id] = input.valueParams.enumConf[0].value;
// }
// break;
// }
case 'switch': {
//
defaultData[input.id] =
@ -236,10 +240,9 @@ watch(selectedFunctionId, (newFunctionId) => {
currentInputs.value = selectedFunc.inputs || [];
//
initializeSelectedRows();
resetForm();
//
initializeFormData();
} else {
resetForm();
}
} else {
resetForm();
@ -251,6 +254,7 @@ watch(selectedFunctionId, (newFunctionId) => {
//
watch(currentMode, () => {
resetForm();
initializeFormData();
generateDefaultContent();
});
@ -370,7 +374,7 @@ const executeSubmit = async (checkValue?: string) => {
(isSwitch && value === false)
) {
if (input) {
const { dataType, formType } = input.valueParams;
const { dataType, formType, format } = input.valueParams;
let processedValue = value;
// dataTypeformType
@ -380,9 +384,7 @@ const executeSubmit = async (checkValue?: string) => {
processedValue = Boolean(value);
} else if (dataType === 'string') {
// string
processedValue = value
? input.valueParams.trueValue || 'true'
: input.valueParams.falseValue || 'false';
processedValue = value;
}
} else if (formType === 'time' && dataType === 'date') {
// date
@ -392,7 +394,11 @@ const executeSubmit = async (checkValue?: string) => {
`时间字段 ${key} (date): ${value} -> ${processedValue}`,
);
} else if (value && typeof value === 'string') {
processedValue = new Date(value).getTime(); //
let timeValue = value;
if (format === 'YYYY-MM-DD') {
timeValue += ' 00:00:00';
}
processedValue = new Date(timeValue).getTime(); //
}
}

View File

@ -130,7 +130,7 @@ const generateDefaultContent = () => {
if (currentMode.value === 'advanced') {
// key:null
const params = {
status: null,
status: '1',
};
jsonContent.value = JSON.stringify(params, null, 2);
parameterContent.value = '';

View File

@ -156,6 +156,22 @@ const columns = [
},
];
//
const readColumns = [
{
title: '属性名称',
dataIndex: 'name',
key: 'name',
width: 150,
},
{
title: '数据类型',
dataIndex: 'dataType',
key: 'dataType',
width: 100,
},
];
//
const tableDataSource = computed(() => {
return filteredProperties.value.map((property) => ({
@ -225,6 +241,13 @@ const initializeSelectedRows = () => {
.map((row) => row.key);
};
const sidebarClick = (item: any) => {
if (selectedPropertyFunction.value !== item.value) {
selectedPropertyFunction.value = item.value;
selectedGroup.value = 'all';
}
};
//
const initializeFormData = () => {
const defaultData = {};
@ -232,13 +255,32 @@ const initializeFormData = () => {
const property = filteredProperties.value.find((p) => p.id === row.id);
if (!property) return;
const { formType } = property.valueParams;
const { formType, scale } = property.valueParams;
//
switch (formType) {
case 'number': {
// 0
defaultData[row.id] = scale
? Number.parseFloat((property.valueParams.min || 0).toFixed(scale))
: property.valueParams.min || 0;
break;
}
case 'progress': {
//
defaultData[row.id] = property.valueParams?.min || 0;
defaultData[row.id] = scale
? Number.parseFloat((property.valueParams.min || 0).toFixed(scale))
: property.valueParams.min || 0;
break;
}
case 'select': {
//
if (
property.valueParams.enumConf &&
property.valueParams.enumConf.length > 0
) {
defaultData[row.id] = property.valueParams.enumConf[0].value;
}
break;
}
case 'switch': {
@ -262,6 +304,7 @@ const initializeFormData = () => {
//
watch(selectedPropertyFunction, () => {
resetForm();
initializeFormData();
initializeSelectedRows();
generateDefaultContent();
});
@ -269,6 +312,7 @@ watch(selectedPropertyFunction, () => {
//
watch(currentMode, () => {
resetForm();
initializeFormData();
generateDefaultContent();
});
@ -298,10 +342,24 @@ const generateDefaultContent = () => {
if (currentMode.value === 'advanced') {
// key:null
const params = {};
filteredProperties.value.forEach((property) => {
params[property.id] = null;
});
jsonContent.value = JSON.stringify(params, null, 2);
if (selectedPropertyFunction.value === 'read') {
const propertyStr = filteredProperties.value
.map((item) => item.id)
.join(',');
jsonContent.value = JSON.stringify(
{
propertyStr,
},
null,
2,
);
} else {
filteredProperties.value.forEach((property) => {
params[property.id] = null;
});
jsonContent.value = JSON.stringify(params, null, 2);
}
parameterContent.value = '';
} else {
//
@ -321,72 +379,83 @@ const executeSubmit = async () => {
return;
}
//
await formRef.value.validate();
const formValues = formData.value;
console.log('表单值:', formValues);
if (selectedPropertyFunction.value === 'read') {
const value = selectedRowKeys.value.join(',');
parameters = {
propertyStr: value,
};
} else {
//
await formRef.value.validate();
const formValues = formData.value;
console.log('表单值:', selectedRowKeys.value);
//
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;
//
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;
if (!property) return;
const value = formValues[row.id];
const { formType } = property.valueParams;
const { dataType, formType, format } = property.valueParams;
let processedValue = value;
//
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;
// dataTypeformType
if (formType === 'switch') {
if (dataType === 'boolean') {
processedValue = Boolean(value);
} else if (dataType === 'string') {
processedValue = value;
}
} else if (formType === 'time' && dataType === 'date') {
if (value && typeof value === 'object' && value.valueOf) {
processedValue = value.valueOf();
} else if (value && typeof value === 'string') {
let timeValue = value;
if (format === 'YYYY-MM-DD') {
timeValue += ' 00:00:00';
}
processedValue = new Date(timeValue).getTime();
}
}
}
parameters[row.id] = processedValue;
});
}
// 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;
// dataTypeformType
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 {
@ -477,7 +546,7 @@ onMounted(() => {
:key="func.value"
class="sidebar-item"
:class="{ active: selectedPropertyFunction === func.value }"
@click="selectedPropertyFunction = func.value"
@click="sidebarClick(func)"
>
<div class="function-name">{{ func.label }}</div>
</div>
@ -487,7 +556,7 @@ onMounted(() => {
<div class="property-main">
<!-- 筛选控制面板 -->
<div class="control-panel">
<div class="control-item">
<div class="control-item" v-if="selectedPropertyFunction !== 'read'">
<span class="control-label">属性分组:</span>
<Select
v-model:value="selectedGroup"
@ -516,12 +585,18 @@ onMounted(() => {
<div class="mode-info">
<ExclamationCircleOutlined />
精简模式下属性只支持输入框的方式录入
<span v-if="selectedPropertyFunction === 'read'">
请选择要读取的属性
</span>
</div>
</TabPane>
<TabPane key="advanced" tab="高级模式">
<div class="mode-info">
<ExclamationCircleOutlined />
高级模式下支持JSON格式直接编辑
<span v-if="selectedPropertyFunction === 'read'">
填写读取属性id,使用逗号分开
</span>
</div>
</TabPane>
</Tabs>
@ -530,9 +605,10 @@ onMounted(() => {
<div class="content-area">
<!-- 左侧属性输入 -->
<div class="input-section">
<div v-if="currentMode === 'simple'" class="simple-form">
<div class="simple-form" v-if="currentMode === 'simple'">
<Form ref="formRef" :model="formData" layout="vertical">
<Table
v-if="selectedPropertyFunction !== 'read'"
:columns="columns"
:data-source="tableDataSource"
:row-selection="rowSelection"
@ -666,9 +742,52 @@ onMounted(() => {
</template>
</template>
</Table>
<Table
v-else-if="selectedPropertyFunction === 'read'"
:columns="readColumns"
: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>
</Table>
</Form>
</div>
<!-- <div
v-else-if="
currentMode === 'simple' && selectedPropertyFunction === 'read'
"
class="simple-form"
>
<Form ref="formRef" :model="formData" layout="vertical">
<Table
:columns="readColumns"
: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>
</Table>
</Form>
</div> -->
<div v-else class="advanced-form">
<MonacoEditor
:model-value="jsonContent"

View File

@ -21,6 +21,7 @@ import { columns, querySchema } from './data';
import productCategoryDrawer from './productCategory-drawer.vue';
const formOptions: VbenFormProps = {
collapsed: true,
commonConfig: {
labelWidth: 80,
componentProps: {