feat: 完成设备详情、运行状态、设备模拟、日志管理页面
- 新增日志管理组件 - 重构运行状态组件,采用标签页布局 - 新增事件面板组件 - 优化参数模态框,支持数据类型和表单类型联动 - 调整设备模拟组件名称
This commit is contained in:
parent
1ac9339eb3
commit
b2a8aa545e
|
@ -0,0 +1,69 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { TabPane, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import FunctionSimulation from './simulation/FunctionSimulation.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
deviceInfo: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// 当前激活的tab
|
||||||
|
const activeTab = ref('function');
|
||||||
|
|
||||||
|
// 获取物模型数据,优先使用设备信息中的物模型,否则使用模拟数据
|
||||||
|
const getMetadata = () => {
|
||||||
|
try {
|
||||||
|
const raw = props.deviceInfo?.productObj?.metadata;
|
||||||
|
if (!raw) return { functions: [] } as any;
|
||||||
|
const obj = JSON.parse(raw || '{}');
|
||||||
|
return {
|
||||||
|
functions: obj?.functions || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('parse metadata error', error);
|
||||||
|
return { functions: [] } as any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前物模型数据
|
||||||
|
const metadata = computed(() => getMetadata());
|
||||||
|
|
||||||
|
// 功能列表
|
||||||
|
const functionList = computed(() => {
|
||||||
|
return metadata.value.functions || [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="device-simulation">
|
||||||
|
<Tabs v-model:active-key="activeTab">
|
||||||
|
<TabPane key="property" tab="属性">
|
||||||
|
<div>属性功能开发中...</div>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key="function" tab="功能">
|
||||||
|
<FunctionSimulation
|
||||||
|
:device-id="deviceId"
|
||||||
|
:device-info="deviceInfo"
|
||||||
|
:function-list="functionList"
|
||||||
|
/>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key="event" tab="事件">
|
||||||
|
<div>事件功能开发中...</div>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key="online" tab="上下线">
|
||||||
|
<div>上下线功能开发中...</div>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-simulation {
|
||||||
|
// 样式可以根据需要添加
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,257 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Empty,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
deviceInfo: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// 查询条件
|
||||||
|
const logType = 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 logTypeOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '上报', value: 'upload' },
|
||||||
|
{ label: '下发', value: 'download' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表格列
|
||||||
|
const columns = [
|
||||||
|
{ 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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
|
const viewVisible = ref(false);
|
||||||
|
const viewRecord = ref<any>(null);
|
||||||
|
|
||||||
|
const openView = (record: any) => {
|
||||||
|
viewRecord.value = record;
|
||||||
|
viewVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeView = () => {
|
||||||
|
viewVisible.value = false;
|
||||||
|
viewRecord.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载日志列表
|
||||||
|
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'),
|
||||||
|
logType: logType.value || undefined,
|
||||||
|
pageNo: pagination.value.current,
|
||||||
|
pageSize: pagination.value.pageSize,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.value.current = 1;
|
||||||
|
loadList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
logType.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="log-management">
|
||||||
|
<!-- 查询区 -->
|
||||||
|
<div class="query-bar">
|
||||||
|
<Space>
|
||||||
|
<span>日志类型:</span>
|
||||||
|
<Select
|
||||||
|
v-model:value="logType"
|
||||||
|
:options="logTypeOptions"
|
||||||
|
allow-clear
|
||||||
|
placeholder="请选择日志类型"
|
||||||
|
style="width: 220px"
|
||||||
|
/>
|
||||||
|
<span>时间范围:</span>
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
v-model:value="timeRange"
|
||||||
|
:show-time="true"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 360px"
|
||||||
|
/>
|
||||||
|
<Button @click="handleReset">重置</Button>
|
||||||
|
<Button type="primary" @click="handleSearch">查询</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="dataSource"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
row-key="id"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'logType'">
|
||||||
|
<Tag :color="record.logType === '上报' ? 'blue' : 'green'">
|
||||||
|
{{ record.logType }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<Button type="link" size="small" @click="openView(record)">
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div v-if="!loading && dataSource.length === 0" class="empty-wrap">
|
||||||
|
<Empty />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 查看内容弹窗 -->
|
||||||
|
<Modal
|
||||||
|
v-model:open="viewVisible"
|
||||||
|
title="日志详情"
|
||||||
|
width="720px"
|
||||||
|
@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>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.log-management {
|
||||||
|
.query-bar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-wrap {
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail {
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-view {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 300px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,26 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { EllipsisText } from '@vben/common-ui';
|
import { TabPane, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
import { LineChartOutlined, UnorderedListOutlined } from '@ant-design/icons-vue';
|
import EventsPanel from './running/EventsPanel.vue';
|
||||||
import {
|
import RealtimePanel from './running/RealtimePanel.vue';
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Checkbox,
|
|
||||||
Col,
|
|
||||||
DatePicker,
|
|
||||||
Empty,
|
|
||||||
Modal,
|
|
||||||
RadioButton,
|
|
||||||
RadioGroup,
|
|
||||||
Row,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Tabs,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
@ -29,543 +13,41 @@ interface Props {
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const loading = ref(false);
|
// 一次性解析物模型并向下传递
|
||||||
const selectedGroup = ref('all');
|
const metadata = computed(() => {
|
||||||
const selectedTypes = ref(['R', 'RW']); // 默认选中读写和只读
|
|
||||||
const metadata = ref({ properties: [], propertyGroups: [] });
|
|
||||||
|
|
||||||
// // 物模型数据
|
|
||||||
// const metadata = computed(() => {
|
|
||||||
// if (props.deviceInfo?.productObj?.metadata) {
|
|
||||||
// try {
|
|
||||||
// return JSON.parse(props.deviceInfo.productObj.metadata);
|
|
||||||
// } catch {
|
|
||||||
// return { properties: [], propertyGroups: [] };
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return { properties: [], propertyGroups: [] };
|
|
||||||
// });
|
|
||||||
|
|
||||||
const getMetadata = () => {
|
|
||||||
let metadataObj = { properties: [], propertyGroups: [] };
|
|
||||||
if (props.deviceInfo?.productObj?.metadata) {
|
|
||||||
metadataObj = JSON.parse(props.deviceInfo.productObj.metadata);
|
|
||||||
}
|
|
||||||
metadataObj.properties = metadataObj.properties.map((prop: any) => ({
|
|
||||||
...prop,
|
|
||||||
value: null,
|
|
||||||
timestamp: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
}));
|
|
||||||
metadata.value = metadataObj;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 属性分组选项
|
|
||||||
const groupOptions = computed(() => {
|
|
||||||
const groups = metadata.value.propertyGroups || [];
|
|
||||||
return [
|
|
||||||
{ label: '全部', value: 'all' },
|
|
||||||
...groups.map((group: any) => ({
|
|
||||||
label: group.name,
|
|
||||||
value: group.id,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 过滤后的属性列表
|
|
||||||
const filteredProperties = computed(() => {
|
|
||||||
let properties = metadata.value.properties || [];
|
|
||||||
|
|
||||||
// 按分组筛选
|
|
||||||
if (selectedGroup.value !== 'all') {
|
|
||||||
const group = metadata.value.propertyGroups?.find((g: any) => g.id === selectedGroup.value);
|
|
||||||
if (group?.properties) {
|
|
||||||
const groupPropertyIds = new Set(group.properties.map((p: any) => p.id));
|
|
||||||
properties = properties.filter((p: any) => groupPropertyIds.has(p.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按读写类型筛选
|
|
||||||
properties = properties.filter((p: any) => {
|
|
||||||
const type = p.expands?.type || 'R';
|
|
||||||
return selectedTypes.value.includes(type);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 为每个属性添加value字段
|
|
||||||
return properties;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 属性日志弹窗
|
|
||||||
const logModalVisible = ref(false);
|
|
||||||
const currentProperty = ref<any>(null);
|
|
||||||
const logTabActiveKey = ref('list');
|
|
||||||
const activeQuickTime = ref('today');
|
|
||||||
const logDateRange = ref([dayjs().startOf('day'), dayjs()]);
|
|
||||||
const logLoading = ref(false);
|
|
||||||
const logData = ref<any[]>([]);
|
|
||||||
|
|
||||||
// 日志表格列配置
|
|
||||||
const logColumns = [
|
|
||||||
{
|
|
||||||
title: '时间',
|
|
||||||
dataIndex: 'timestamp',
|
|
||||||
key: 'timestamp',
|
|
||||||
width: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '值',
|
|
||||||
dataIndex: 'value',
|
|
||||||
key: 'value',
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 打开属性日志弹窗
|
|
||||||
const openPropertyLog = (property: any) => {
|
|
||||||
currentProperty.value = property;
|
|
||||||
logModalVisible.value = true;
|
|
||||||
loadPropertyLog();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载属性日志数据
|
|
||||||
const loadPropertyLog = async () => {
|
|
||||||
if (!currentProperty.value) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logLoading.value = true;
|
const raw = props.deviceInfo?.productObj?.metadata;
|
||||||
// TODO: 调用API获取属性日志数据
|
if (!raw) return { properties: [], propertyGroups: [], events: [] } as any;
|
||||||
// const res = await getPropertyLog({
|
const obj = JSON.parse(raw || '{}');
|
||||||
// deviceId: props.deviceId,
|
return {
|
||||||
// propertyId: currentProperty.value.id,
|
properties: obj?.properties || [],
|
||||||
// startTime: logDateRange.value[0]?.format('YYYY-MM-DD HH:mm:ss'),
|
propertyGroups: obj?.propertyGroups || [],
|
||||||
// endTime: logDateRange.value[1]?.format('YYYY-MM-DD HH:mm:ss')
|
events: obj?.events || [],
|
||||||
// });
|
};
|
||||||
// logData.value = res.data || [];
|
|
||||||
|
|
||||||
// 模拟数据
|
|
||||||
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' },
|
|
||||||
];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取属性日志失败:', error);
|
console.warn('parse metadata error', error);
|
||||||
} finally {
|
return { properties: [], propertyGroups: [], events: [] } as any;
|
||||||
logLoading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 时间范围变化
|
|
||||||
const handleDateRangeChange = (dates: any) => {
|
|
||||||
logDateRange.value = dates;
|
|
||||||
if (dates && dates.length === 2) {
|
|
||||||
loadPropertyLog();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 快速时间选择
|
|
||||||
const handleQuickTimeSelect = (type: string) => {
|
|
||||||
console.log(type);
|
|
||||||
const now = dayjs();
|
|
||||||
switch (type.target.value) {
|
|
||||||
case 'month': {
|
|
||||||
logDateRange.value = [now.subtract(1, 'month'), now];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'today': {
|
|
||||||
logDateRange.value = [now.startOf('day'), now];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'week': {
|
|
||||||
logDateRange.value = [now.subtract(7, 'day'), now];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadPropertyLog();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 格式化数值显示
|
|
||||||
const formatValue = (property: any) => {
|
|
||||||
const { value, valueParams } = property;
|
|
||||||
if (value === undefined || value === null) return '--';
|
|
||||||
|
|
||||||
let displayValue = value;
|
|
||||||
|
|
||||||
if (valueParams?.formType === 'input' && valueParams?.length) {
|
|
||||||
displayValue = value.slice(0, valueParams.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据数据类型格式化
|
|
||||||
if (valueParams?.formType === 'switch') {
|
|
||||||
return value.toString() === valueParams.trueValue ? valueParams.trueText : valueParams.falseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueParams?.formType === 'select') {
|
|
||||||
return valueParams.enumConf.find((item: any) => item.value === value)?.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueParams?.formType === 'time') {
|
|
||||||
return dayjs(value).format(valueParams.format);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueParams?.formType === 'number' || valueParams?.formType === 'progress') {
|
|
||||||
if (valueParams?.scale) {
|
|
||||||
return value.toFixed(valueParams.scale);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// displayValue = `${value}`;
|
|
||||||
|
|
||||||
return displayValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 判断是否显示图表
|
|
||||||
const canShowChart = (property: any) => {
|
|
||||||
const { valueParams } = property;
|
|
||||||
const numericTypes = ['int', 'float', 'double'];
|
|
||||||
return numericTypes.includes(valueParams?.dataType);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭日志弹窗
|
|
||||||
const closeLogModal = () => {
|
|
||||||
logModalVisible.value = false;
|
|
||||||
currentProperty.value = null;
|
|
||||||
logData.value = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 定时刷新数据
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
const generateRandomString = (length: number) => {
|
|
||||||
// 定义字符池
|
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
// 从字符池中随机选取一个字符
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startRefreshTimer = () => {
|
|
||||||
refreshTimer = setInterval(() => {
|
|
||||||
// TODO: 刷新属性值
|
|
||||||
console.log('刷新属性值');
|
|
||||||
metadata.value.properties.forEach((item: any) => {
|
|
||||||
switch (item.valueParams?.dataType) {
|
|
||||||
case 'boolean': {
|
|
||||||
item.value = Math.random() > 0.5 ? 'true' : 'false';
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'date': {
|
|
||||||
item.value = Date.now();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'double':
|
|
||||||
case 'float':
|
|
||||||
case 'int':
|
|
||||||
case 'long': {
|
|
||||||
item.value = Math.random() * 100;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'enum': {
|
|
||||||
item.value = item.valueParams.enumConf[Math.floor(Math.random() * item.valueParams.enumConf.length)].value;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
item.value = generateRandomString(Math.floor(Math.random() * 1_000_000));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
item.timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
});
|
|
||||||
console.log(metadata.value.properties);
|
|
||||||
}, 3000); // 30秒刷新一次
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopRefreshTimer = () => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer);
|
|
||||||
refreshTimer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
startRefreshTimer();
|
|
||||||
getMetadata();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopRefreshTimer();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听设备信息变化
|
|
||||||
watch(
|
|
||||||
() => props.deviceInfo,
|
|
||||||
() => {
|
|
||||||
// 设备信息变化时重新加载数据
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="running-status">
|
<div class="running-status">
|
||||||
<!-- 控制面板 -->
|
<Tabs type="card">
|
||||||
<div class="control-panel">
|
<TabPane key="realtime" tab="实时数据">
|
||||||
<div class="control-item">
|
<RealtimePanel :device-id="props.deviceId" :metadata="metadata" />
|
||||||
<span class="control-label">属性分组:</span>
|
</TabPane>
|
||||||
<Select
|
<TabPane key="events" tab="事件">
|
||||||
v-model:value="selectedGroup"
|
<EventsPanel :device-id="props.deviceId" :metadata="metadata" />
|
||||||
:options="groupOptions"
|
</TabPane>
|
||||||
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="property-grid">
|
|
||||||
<Row :gutter="16">
|
|
||||||
<Col v-for="property in filteredProperties" :key="property.id" :span="6" class="property-col">
|
|
||||||
<Card class="property-card" :loading="loading">
|
|
||||||
<div class="property-header">
|
|
||||||
<h4 class="property-title">{{ property.name }}</h4>
|
|
||||||
<Button type="text" size="small" class="property-log-btn" @click="openPropertyLog(property)">
|
|
||||||
<UnorderedListOutlined />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="property-content">
|
|
||||||
<EllipsisText class="property-value" :line="3" expand>
|
|
||||||
{{ formatValue(property) }}
|
|
||||||
</EllipsisText>
|
|
||||||
<div class="property-unit">
|
|
||||||
{{ property.valueParams?.unit || '' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="property-footer">
|
|
||||||
<span class="property-timestamp">{{ property.timestamp }}</span>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<div v-if="filteredProperties.length === 0" class="empty-state">
|
|
||||||
<Empty description="暂无属性数据" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 属性日志弹窗 -->
|
|
||||||
<Modal
|
|
||||||
v-model:open="logModalVisible"
|
|
||||||
:title="`${currentProperty?.name || ''} - 详情`"
|
|
||||||
width="800px"
|
|
||||||
@cancel="closeLogModal"
|
|
||||||
:footer="null"
|
|
||||||
>
|
|
||||||
<div class="log-modal-content">
|
|
||||||
<!-- 时间选择 -->
|
|
||||||
<div class="time-selection">
|
|
||||||
<Space>
|
|
||||||
<RadioGroup v-model:value="activeQuickTime" @change="handleQuickTimeSelect" button-style="solid">
|
|
||||||
<RadioButton value="today">今日</RadioButton>
|
|
||||||
<RadioButton value="week">近一周</RadioButton>
|
|
||||||
<RadioButton value="month">近一月</RadioButton>
|
|
||||||
</RadioGroup>
|
|
||||||
<!-- <Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="handleQuickTimeSelect('today')"
|
|
||||||
>
|
|
||||||
今日
|
|
||||||
</Button>
|
|
||||||
<Button size="small" @click="handleQuickTimeSelect('week')">
|
|
||||||
近一周
|
|
||||||
</Button>
|
|
||||||
<Button size="small" @click="handleQuickTimeSelect('month')">
|
|
||||||
近一月
|
|
||||||
</Button> -->
|
|
||||||
<DatePicker.RangePicker
|
|
||||||
v-model:value="logDateRange"
|
|
||||||
:show-time="true"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
@change="handleDateRangeChange"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 标签页 -->
|
|
||||||
<Tabs v-model:active-key="logTabActiveKey" class="log-tabs">
|
|
||||||
<Tabs.TabPane key="list" tab="列表">
|
|
||||||
<Table
|
|
||||||
:columns="logColumns"
|
|
||||||
:data-source="logData"
|
|
||||||
:loading="logLoading"
|
|
||||||
:pagination="false"
|
|
||||||
size="small"
|
|
||||||
row-key="timestamp"
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column }">
|
|
||||||
<template v-if="column.key === 'action'">
|
|
||||||
<Button type="link" size="small">查看</Button>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<Tabs.TabPane v-if="currentProperty && canShowChart(currentProperty)" key="chart" tab="图表">
|
|
||||||
<div class="chart-container">
|
|
||||||
<div class="chart-placeholder">
|
|
||||||
<LineChartOutlined style="font-size: 48px; color: #d9d9d9" />
|
|
||||||
<p>图表功能开发中...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.running-status {
|
.running-status {
|
||||||
.control-panel {
|
:deep(.ant-tabs-content-holder) {
|
||||||
display: flex;
|
padding-top: 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-grid {
|
|
||||||
.property-col {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-card {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.property-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.property-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #262626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-log-btn {
|
|
||||||
padding: 4px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-content {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.property-value {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 80px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-unit {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-info {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-footer {
|
|
||||||
.property-timestamp {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
padding: 60px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-modal-content {
|
|
||||||
.time-selection {
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-tabs {
|
|
||||||
.chart-container {
|
|
||||||
.chart-placeholder {
|
|
||||||
padding: 60px 0;
|
|
||||||
color: #8c8c8c;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 16px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-card-body) {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Empty,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
metadata: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// 查询条件
|
||||||
|
const messageId = ref<string>('');
|
||||||
|
const timeRange = ref<[Dayjs, Dayjs] | undefined>();
|
||||||
|
const selectedEventId = ref<string | undefined>();
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
const loading = ref(false);
|
||||||
|
const dataSource = ref<any[]>([]);
|
||||||
|
const pagination = ref({ current: 1, pageSize: 10, total: 0 });
|
||||||
|
|
||||||
|
// 事件映射(id -> name)
|
||||||
|
const eventNameMap = computed<Record<string, string>>(() => {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
(props.metadata?.events || []).forEach((e: any) => {
|
||||||
|
map[e.id] = e.name || e.id;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下拉选项(来自物模型)
|
||||||
|
const eventOptions = computed(() => {
|
||||||
|
return (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: '操作', key: 'action', width: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
|
const viewVisible = ref(false);
|
||||||
|
const viewRecord = ref<any>(null);
|
||||||
|
|
||||||
|
const openView = (record: any) => {
|
||||||
|
viewRecord.value = record;
|
||||||
|
viewVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeView = () => {
|
||||||
|
viewVisible.value = false;
|
||||||
|
viewRecord.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载事件列表(占位,后续接入API)
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.value.current = 1;
|
||||||
|
loadList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
messageId.value = '';
|
||||||
|
timeRange.value = undefined;
|
||||||
|
selectedEventId.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="events-panel">
|
||||||
|
<!-- 查询区 -->
|
||||||
|
<div class="query-bar">
|
||||||
|
<Space>
|
||||||
|
<span>消息ID:</span>
|
||||||
|
<Input
|
||||||
|
v-model:value="messageId"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 220px"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
<span>事件:</span>
|
||||||
|
<Select
|
||||||
|
v-model:value="selectedEventId"
|
||||||
|
:options="eventOptions"
|
||||||
|
allow-clear
|
||||||
|
placeholder="请选择事件"
|
||||||
|
style="width: 220px"
|
||||||
|
/>
|
||||||
|
<span>时间范围:</span>
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
v-model:value="timeRange"
|
||||||
|
:show-time="true"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 360px"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div v-if="!loading && dataSource.length === 0" class="empty-wrap">
|
||||||
|
<Empty />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 查看内容弹窗 -->
|
||||||
|
<Modal
|
||||||
|
v-model:open="viewVisible"
|
||||||
|
title="事件内容"
|
||||||
|
width="720px"
|
||||||
|
@cancel="closeView"
|
||||||
|
:footer="null"
|
||||||
|
>
|
||||||
|
<pre class="json-view">{{
|
||||||
|
JSON.stringify(viewRecord?.payload ?? {}, null, 2)
|
||||||
|
}}</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>
|
|
@ -0,0 +1,481 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EllipsisText } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LineChartOutlined,
|
||||||
|
UnorderedListOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Col,
|
||||||
|
DatePicker,
|
||||||
|
Empty,
|
||||||
|
Modal,
|
||||||
|
RadioButton,
|
||||||
|
RadioGroup,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tabs,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
metadata: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const selectedGroup = ref('all');
|
||||||
|
const selectedTypes = ref(['R', 'RW']);
|
||||||
|
|
||||||
|
// 本地运行态属性(不修改入参metadata)
|
||||||
|
const runtimeProperties = ref<any[]>([]);
|
||||||
|
|
||||||
|
const initRuntime = () => {
|
||||||
|
const properties = (props.metadata?.properties || []).map((prop: any) => ({
|
||||||
|
...prop,
|
||||||
|
value: null,
|
||||||
|
timestamp: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
}));
|
||||||
|
runtimeProperties.value = properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 属性分组选项
|
||||||
|
const groupOptions = computed(() => {
|
||||||
|
const groups = props.metadata?.propertyGroups || [];
|
||||||
|
return [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
...groups.map((group: any) => ({ label: group.name, value: group.id })),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤后的属性列表
|
||||||
|
const filteredProperties = computed(() => {
|
||||||
|
let properties = [...runtimeProperties.value];
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 属性日志弹窗
|
||||||
|
const logModalVisible = ref(false);
|
||||||
|
const currentProperty = ref<any>(null);
|
||||||
|
const logTabActiveKey = ref('list');
|
||||||
|
const activeQuickTime = ref('today');
|
||||||
|
const logDateRange = ref([dayjs().startOf('day'), dayjs()]);
|
||||||
|
const logLoading = ref(false);
|
||||||
|
const logData = ref<any[]>([]);
|
||||||
|
|
||||||
|
const logColumns = [
|
||||||
|
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 200 },
|
||||||
|
{ title: '值', dataIndex: 'value', key: 'value', width: 150 },
|
||||||
|
{ title: '操作', key: 'action', width: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const openPropertyLog = (property: any) => {
|
||||||
|
currentProperty.value = property;
|
||||||
|
logModalVisible.value = true;
|
||||||
|
loadPropertyLog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPropertyLog = async () => {
|
||||||
|
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' },
|
||||||
|
];
|
||||||
|
} finally {
|
||||||
|
logLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (dates: any) => {
|
||||||
|
logDateRange.value = dates;
|
||||||
|
if (dates && dates.length === 2) loadPropertyLog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickTimeSelect = (e: any) => {
|
||||||
|
const now = dayjs();
|
||||||
|
switch (e?.target?.value) {
|
||||||
|
case 'month': {
|
||||||
|
logDateRange.value = [now.subtract(1, 'month'), now];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'week': {
|
||||||
|
logDateRange.value = [now.subtract(7, 'day'), now];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
logDateRange.value = [now.startOf('day'), now];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadPropertyLog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValue = (property: any) => {
|
||||||
|
const { value, valueParams } = property;
|
||||||
|
if (value === undefined || value === null) return '--';
|
||||||
|
|
||||||
|
let displayValue = value;
|
||||||
|
|
||||||
|
if (valueParams?.formType === 'input' && valueParams?.length) {
|
||||||
|
displayValue = value.slice(0, valueParams.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueParams?.formType === 'switch') {
|
||||||
|
return value.toString() === valueParams.trueValue
|
||||||
|
? valueParams.trueText
|
||||||
|
: valueParams.falseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueParams?.formType === 'select') {
|
||||||
|
return valueParams.enumConf.find((item: any) => item.value === value)?.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueParams?.formType === 'time') {
|
||||||
|
return dayjs(value).format(valueParams.format);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
valueParams?.formType === 'number' ||
|
||||||
|
valueParams?.formType === 'progress'
|
||||||
|
) {
|
||||||
|
if (valueParams?.scale) return value.toFixed(valueParams.scale);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const canShowChart = (property: any) => {
|
||||||
|
const { valueParams } = property;
|
||||||
|
const numericTypes = ['int', 'float', 'double'];
|
||||||
|
return numericTypes.includes(valueParams?.dataType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeLogModal = () => {
|
||||||
|
logModalVisible.value = false;
|
||||||
|
currentProperty.value = null;
|
||||||
|
logData.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
let refreshTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const generateRandomString = (length: number) => {
|
||||||
|
const chars =
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRefreshTimer = () => {
|
||||||
|
console.log('开始生成随机数据');
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
runtimeProperties.value.forEach((item: any) => {
|
||||||
|
switch (item.valueParams?.dataType) {
|
||||||
|
case 'boolean': {
|
||||||
|
item.value = Math.random() > 0.5 ? 'true' : 'false';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'date': {
|
||||||
|
item.value = Date.now();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'double':
|
||||||
|
case 'float':
|
||||||
|
case 'int':
|
||||||
|
case 'long': {
|
||||||
|
item.value = Math.random() * 100;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'enum': {
|
||||||
|
item.value =
|
||||||
|
item.valueParams.enumConf[
|
||||||
|
Math.floor(Math.random() * item.valueParams.enumConf.length)
|
||||||
|
].value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'string': {
|
||||||
|
item.value = generateRandomString(
|
||||||
|
Math.floor(Math.random() * 1_000_000),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// No default
|
||||||
|
}
|
||||||
|
item.timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRefreshTimer = () => {
|
||||||
|
console.log('停止生成随机数据');
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initRuntime();
|
||||||
|
startRefreshTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopRefreshTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.metadata,
|
||||||
|
() => initRuntime(),
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="realtime-panel">
|
||||||
|
<!-- 控制面板 -->
|
||||||
|
<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="property-grid">
|
||||||
|
<Row :gutter="16">
|
||||||
|
<Col
|
||||||
|
v-for="property in filteredProperties"
|
||||||
|
:key="property.id"
|
||||||
|
:span="6"
|
||||||
|
class="property-col"
|
||||||
|
>
|
||||||
|
<Card class="property-card" :loading="loading">
|
||||||
|
<div class="property-header">
|
||||||
|
<h4 class="property-title">{{ property.name }}</h4>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="property-log-btn"
|
||||||
|
@click="openPropertyLog(property)"
|
||||||
|
>
|
||||||
|
<UnorderedListOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="property-content">
|
||||||
|
<EllipsisText class="property-value" :line="3" expand>
|
||||||
|
{{ formatValue(property) }}
|
||||||
|
</EllipsisText>
|
||||||
|
<div class="property-unit">
|
||||||
|
{{ property.valueParams?.unit || '' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="property-footer">
|
||||||
|
<span class="property-timestamp">{{ property.timestamp }}</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div v-if="filteredProperties.length === 0" class="empty-state">
|
||||||
|
<Empty description="暂无属性数据" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 属性日志弹窗 -->
|
||||||
|
<Modal
|
||||||
|
v-model:open="logModalVisible"
|
||||||
|
:title="`${currentProperty?.name || ''} - 详情`"
|
||||||
|
width="800px"
|
||||||
|
@cancel="closeLogModal"
|
||||||
|
:footer="null"
|
||||||
|
>
|
||||||
|
<div class="log-modal-content">
|
||||||
|
<div class="time-selection">
|
||||||
|
<Space>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="activeQuickTime"
|
||||||
|
@change="handleQuickTimeSelect"
|
||||||
|
button-style="solid"
|
||||||
|
>
|
||||||
|
<RadioButton value="today">今日</RadioButton>
|
||||||
|
<RadioButton value="week">近一周</RadioButton>
|
||||||
|
<RadioButton value="month">近一月</RadioButton>
|
||||||
|
</RadioGroup>
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
v-model:value="logDateRange"
|
||||||
|
:show-time="true"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
@change="handleDateRangeChange"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs v-model:active-key="logTabActiveKey" class="log-tabs">
|
||||||
|
<Tabs.TabPane key="list" tab="列表">
|
||||||
|
<Table
|
||||||
|
:columns="logColumns"
|
||||||
|
:data-source="logData"
|
||||||
|
:loading="logLoading"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
row-key="timestamp"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column }">
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<Button type="link" size="small">查看</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane
|
||||||
|
v-if="currentProperty && canShowChart(currentProperty)"
|
||||||
|
key="chart"
|
||||||
|
tab="图表"
|
||||||
|
>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-placeholder">
|
||||||
|
<LineChartOutlined style="font-size: 48px; color: #d9d9d9" />
|
||||||
|
<p>图表功能开发中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.realtime-panel {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-grid {
|
||||||
|
.property-col {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-card {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.property-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.property-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-log-btn {
|
||||||
|
padding: 4px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-content {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.property-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 80px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-unit {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,975 @@
|
||||||
|
<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,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
Slider,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
TabPane,
|
||||||
|
Tabs,
|
||||||
|
TimePicker,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
||||||
|
import { dataTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deviceId: string;
|
||||||
|
deviceInfo: any;
|
||||||
|
functionList: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// 当前选中的功能
|
||||||
|
const selectedFunctionId = ref('');
|
||||||
|
// 当前模式:simple(精简模式) / advanced(高级模式)
|
||||||
|
const currentMode = ref('simple');
|
||||||
|
// 表单引用
|
||||||
|
const formRef = ref();
|
||||||
|
// JSON编辑器内容
|
||||||
|
const jsonContent = ref('{}');
|
||||||
|
// 参数框内容
|
||||||
|
const parameterContent = ref('');
|
||||||
|
// 提交结果
|
||||||
|
const submitResult = ref('');
|
||||||
|
|
||||||
|
// 当前选中的功能详情
|
||||||
|
const currentFunction = ref();
|
||||||
|
|
||||||
|
// 当前功能的输入参数
|
||||||
|
const currentInputs = ref([]);
|
||||||
|
|
||||||
|
// 选中的参数行
|
||||||
|
const selectedRowKeys = ref([]);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref({});
|
||||||
|
|
||||||
|
// 预检查弹窗相关
|
||||||
|
const preCheckVisible = ref(false);
|
||||||
|
const preCheckType = ref('');
|
||||||
|
const preCheckForm = ref({
|
||||||
|
password: '',
|
||||||
|
phone: '',
|
||||||
|
smsCode: '',
|
||||||
|
});
|
||||||
|
const preCheckFormRef = ref();
|
||||||
|
const smsCodeLoading = ref(false);
|
||||||
|
const smsCountdown = ref(0);
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
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 currentInputs.value.map((input) => ({
|
||||||
|
key: input.id,
|
||||||
|
id: input.id,
|
||||||
|
name: input.name,
|
||||||
|
dataType:
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(option) => option.value === input.valueParams.dataType,
|
||||||
|
)?.label || input.valueParams.dataType,
|
||||||
|
required: input.required,
|
||||||
|
formType: input.valueParams.formType,
|
||||||
|
valueParams: input.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 = currentInputs.value
|
||||||
|
.filter((input) => input.required)
|
||||||
|
.map((input) => input.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化表单默认值
|
||||||
|
const initializeFormData = () => {
|
||||||
|
const defaultData = {};
|
||||||
|
currentInputs.value.forEach((input) => {
|
||||||
|
const { formType } = input.valueParams;
|
||||||
|
|
||||||
|
// 为不同类型的组件设置默认值
|
||||||
|
switch (formType) {
|
||||||
|
// case 'number': {
|
||||||
|
// // 数字输入框默认为最小值或0
|
||||||
|
// defaultData[input.id] = input.valueParams.min || 0;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
case 'progress': {
|
||||||
|
// 滚动条默认为最小值
|
||||||
|
defaultData[input.id] = 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 'switch': {
|
||||||
|
// 开关默认值根据数据类型设置
|
||||||
|
defaultData[input.id] = input.valueParams.dataType === 'boolean' ? false : input.valueParams.falseValue || 'false' ;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// 其他类型保持undefined,让用户手动输入
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
formData.value = { ...defaultData };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取短信验证码
|
||||||
|
const getSmsCode = async () => {
|
||||||
|
if (!preCheckForm.value.phone) {
|
||||||
|
message.error('请输入手机号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号格式验证
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||||
|
if (!phoneRegex.test(preCheckForm.value.phone)) {
|
||||||
|
message.error('请输入正确的手机号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
smsCodeLoading.value = true;
|
||||||
|
// 模拟发送短信验证码
|
||||||
|
console.log('发送短信验证码到:', preCheckForm.value.phone);
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
smsCountdown.value = 60;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
smsCountdown.value--;
|
||||||
|
if (smsCountdown.value <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
message.success('验证码已发送');
|
||||||
|
} catch {
|
||||||
|
message.error('发送验证码失败');
|
||||||
|
} finally {
|
||||||
|
smsCodeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听功能切换,重置表单和内容
|
||||||
|
watch(selectedFunctionId, (newFunctionId) => {
|
||||||
|
if (newFunctionId) {
|
||||||
|
// 更新当前功能和输入参数
|
||||||
|
const selectedFunc = props.functionList.find(
|
||||||
|
(func) => func.id === newFunctionId,
|
||||||
|
);
|
||||||
|
if (selectedFunc) {
|
||||||
|
currentFunction.value = selectedFunc;
|
||||||
|
currentInputs.value = selectedFunc.inputs || [];
|
||||||
|
// 初始化选中行
|
||||||
|
initializeSelectedRows();
|
||||||
|
// 初始化表单默认值
|
||||||
|
initializeFormData();
|
||||||
|
} else {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDefaultContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听模式切换,重置表单
|
||||||
|
watch(currentMode, () => {
|
||||||
|
resetForm();
|
||||||
|
generateDefaultContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
}
|
||||||
|
formData.value = {};
|
||||||
|
submitResult.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成默认内容
|
||||||
|
const generateDefaultContent = () => {
|
||||||
|
if (!currentFunction.value) return;
|
||||||
|
|
||||||
|
if (currentMode.value === 'advanced') {
|
||||||
|
// 高级模式:所有属性key:null
|
||||||
|
const params = {};
|
||||||
|
currentInputs.value.forEach((item) => {
|
||||||
|
params[item.id] = null;
|
||||||
|
});
|
||||||
|
jsonContent.value = JSON.stringify(params, null, 2);
|
||||||
|
parameterContent.value = '';
|
||||||
|
} else {
|
||||||
|
// 精简模式:清空参数框
|
||||||
|
parameterContent.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理JSON编辑器变化
|
||||||
|
const handleJsonChange = (value: string) => {
|
||||||
|
jsonContent.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否需要预检查
|
||||||
|
const checkPreCheck = () => {
|
||||||
|
const currentFunc = currentFunction.value;
|
||||||
|
if (currentFunc?.expands?.preCheck) {
|
||||||
|
preCheckType.value = currentFunc.expands.checkType || 'userPassword';
|
||||||
|
preCheckForm.value = {
|
||||||
|
password: '',
|
||||||
|
phone: '',
|
||||||
|
smsCode: '',
|
||||||
|
};
|
||||||
|
preCheckVisible.value = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行实际提交
|
||||||
|
const executeSubmit = async (checkValue?: string) => {
|
||||||
|
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 input of currentInputs.value) {
|
||||||
|
// if (
|
||||||
|
// input.required &&
|
||||||
|
// selectedRowKeys.value.includes(input.id) &&
|
||||||
|
// (formValues[input.id] === undefined ||
|
||||||
|
// formValues[input.id] === null ||
|
||||||
|
// formValues[input.id] === '' ||
|
||||||
|
// (typeof formValues[input.id] === 'string' &&
|
||||||
|
// formValues[input.id].trim() === ''))
|
||||||
|
// ) {
|
||||||
|
// message.error(`请填写必填参数:${input.name}`);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (const input of currentInputs.value) {
|
||||||
|
if (selectedRowKeys.value.includes(input.id)) {
|
||||||
|
const value = formValues[input.id];
|
||||||
|
const { formType } = input.valueParams;
|
||||||
|
|
||||||
|
// 根据不同的表单类型判断是否为空值
|
||||||
|
let isEmpty = false;
|
||||||
|
|
||||||
|
isEmpty = formType === 'switch'
|
||||||
|
? value === undefined || value === null // if 分支
|
||||||
|
: value === undefined || value === null || value === '' || // else 分支
|
||||||
|
(typeof value === 'string' && value.trim() === '');
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
message.error(`请填写参数:${input.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 只提交选中的参数,构造key:value对象
|
||||||
|
parameters = {};
|
||||||
|
selectedRowKeys.value.forEach((key) => {
|
||||||
|
const value = formValues[key];
|
||||||
|
// 对于开关类型,false 也是有效值,不应该被过滤掉
|
||||||
|
const input = currentInputs.value.find((item) => item.id === key);
|
||||||
|
const isSwitch = input?.valueParams?.formType === 'switch';
|
||||||
|
|
||||||
|
if (
|
||||||
|
(value !== undefined && value !== null) ||
|
||||||
|
(isSwitch && value === false)
|
||||||
|
) {
|
||||||
|
if (input) {
|
||||||
|
const { dataType, formType } = input.valueParams;
|
||||||
|
let processedValue = value;
|
||||||
|
|
||||||
|
// 根据dataType和formType处理数据类型转换
|
||||||
|
if (formType === 'switch') {
|
||||||
|
if (dataType === 'boolean') {
|
||||||
|
// boolean类型的开关,提交布尔值
|
||||||
|
processedValue = Boolean(value);
|
||||||
|
} else if (dataType === 'string') {
|
||||||
|
// string类型的开关,提交字符串值
|
||||||
|
processedValue = value
|
||||||
|
? input.valueParams.trueValue || 'true'
|
||||||
|
: input.valueParams.falseValue || 'false';
|
||||||
|
}
|
||||||
|
} else if (formType === 'time' && dataType === 'date') {
|
||||||
|
// date类型的时间选择器,提交时间戳
|
||||||
|
if (value && typeof value === 'object' && value.valueOf) {
|
||||||
|
processedValue = value.valueOf(); // 转换为时间戳
|
||||||
|
console.log(
|
||||||
|
`时间字段 ${key} (date): ${value} -> ${processedValue}`,
|
||||||
|
);
|
||||||
|
} 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: any = {
|
||||||
|
deviceId: props.deviceId,
|
||||||
|
functionId: selectedFunctionId.value,
|
||||||
|
parameter: parameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果有预检查值,添加到提交数据中
|
||||||
|
if (checkValue) {
|
||||||
|
submitData.checkValue = checkValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新参数框内容
|
||||||
|
parameterContent.value = JSON.stringify(submitData, null, 2);
|
||||||
|
|
||||||
|
// 模拟API调用
|
||||||
|
console.log('执行功能:', submitData);
|
||||||
|
console.log('选中的参数keys:', selectedRowKeys.value);
|
||||||
|
console.log('提交的参数对象:', parameters);
|
||||||
|
|
||||||
|
// 模拟返回结果
|
||||||
|
const mockResult = {
|
||||||
|
success: true,
|
||||||
|
message: '执行成功',
|
||||||
|
data: {
|
||||||
|
deviceId: props.deviceId,
|
||||||
|
functionId: selectedFunctionId.value,
|
||||||
|
executeTime: new Date().toISOString(),
|
||||||
|
result: 'OK',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
submitResult.value = JSON.stringify(mockResult, null, 2);
|
||||||
|
message.success('执行成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('执行失败');
|
||||||
|
console.error('执行错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主要的提交函数
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 检查是否需要预检查
|
||||||
|
if (checkPreCheck()) {
|
||||||
|
return; // 显示预检查弹窗,等待用户输入
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接执行提交
|
||||||
|
await executeSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预检查确认
|
||||||
|
const handlePreCheckConfirm = async () => {
|
||||||
|
try {
|
||||||
|
await preCheckFormRef.value.validate();
|
||||||
|
|
||||||
|
let checkValue = '';
|
||||||
|
|
||||||
|
// 根据预检查类型构造checkValue
|
||||||
|
switch (preCheckType.value) {
|
||||||
|
case 'staticPassword': {
|
||||||
|
checkValue = preCheckForm.value.password;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userPassword': {
|
||||||
|
checkValue = preCheckForm.value.password;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userSms': {
|
||||||
|
checkValue = preCheckForm.value.smsCode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭预检查弹窗
|
||||||
|
preCheckVisible.value = false;
|
||||||
|
|
||||||
|
// 执行实际提交
|
||||||
|
await executeSubmit(checkValue);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('预检查验证失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消预检查
|
||||||
|
const handlePreCheckCancel = () => {
|
||||||
|
preCheckVisible.value = false;
|
||||||
|
preCheckForm.value = {
|
||||||
|
password: '',
|
||||||
|
phone: '',
|
||||||
|
smsCode: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取预检查弹窗标题
|
||||||
|
const getPreCheckTitle = () => {
|
||||||
|
switch (preCheckType.value) {
|
||||||
|
case 'staticPassword': {
|
||||||
|
return '固定密码验证';
|
||||||
|
}
|
||||||
|
case 'userPassword': {
|
||||||
|
return '用户密码验证';
|
||||||
|
}
|
||||||
|
case 'userSms': {
|
||||||
|
return '短信验证';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return '身份验证';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化选中第一个功能
|
||||||
|
if (props.functionList.length > 0 && !selectedFunctionId.value) {
|
||||||
|
selectedFunctionId.value = props.functionList[0].id;
|
||||||
|
currentFunction.value = props.functionList[0];
|
||||||
|
currentInputs.value = props.functionList[0].inputs || [];
|
||||||
|
// 初始化选中行
|
||||||
|
initializeSelectedRows();
|
||||||
|
// 初始化表单默认值
|
||||||
|
initializeFormData();
|
||||||
|
// 初始化
|
||||||
|
generateDefaultContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="function-simulation">
|
||||||
|
<div class="function-content">
|
||||||
|
<!-- 左侧功能列表 -->
|
||||||
|
<div class="function-sidebar">
|
||||||
|
<div class="sidebar-header">功能列表</div>
|
||||||
|
<div
|
||||||
|
v-for="func in functionList"
|
||||||
|
:key="func.id"
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ active: selectedFunctionId === func.id }"
|
||||||
|
@click="selectedFunctionId = func.id"
|
||||||
|
>
|
||||||
|
<div class="function-name">{{ func.name }}</div>
|
||||||
|
<div class="function-id">{{ func.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧内容区域 -->
|
||||||
|
<div class="function-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"
|
||||||
|
: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="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
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- 预检查弹窗 -->
|
||||||
|
<Modal
|
||||||
|
:open="preCheckVisible"
|
||||||
|
:title="getPreCheckTitle()"
|
||||||
|
@ok="handlePreCheckConfirm"
|
||||||
|
@cancel="handlePreCheckCancel"
|
||||||
|
>
|
||||||
|
<Form ref="preCheckFormRef" :model="preCheckForm" layout="vertical">
|
||||||
|
<!-- 用户密码 -->
|
||||||
|
<Form.Item
|
||||||
|
v-if="preCheckType === 'userPassword'"
|
||||||
|
label="请输入用户密码"
|
||||||
|
name="password"
|
||||||
|
:rules="[{ required: true, message: '请输入密码' }]"
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
v-model:value="preCheckForm.password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<!-- 固定密码 -->
|
||||||
|
<Form.Item
|
||||||
|
v-if="preCheckType === 'staticPassword'"
|
||||||
|
label="请输入固定密码"
|
||||||
|
name="password"
|
||||||
|
:rules="[{ required: true, message: '请输入密码' }]"
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
v-model:value="preCheckForm.password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<!-- 用户短信 -->
|
||||||
|
<template v-if="preCheckType === 'userSms'">
|
||||||
|
<Form.Item
|
||||||
|
label="手机号"
|
||||||
|
name="phone"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: '请输入手机号' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model:value="preCheckForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="验证码"
|
||||||
|
name="smsCode"
|
||||||
|
:rules="[{ required: true, message: '请输入验证码' }]"
|
||||||
|
>
|
||||||
|
<div style="display: flex; gap: 8px">
|
||||||
|
<Input
|
||||||
|
v-model:value="preCheckForm.smsCode"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:loading="smsCodeLoading"
|
||||||
|
:disabled="smsCountdown > 0"
|
||||||
|
@click="getSmsCode"
|
||||||
|
>
|
||||||
|
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.function-simulation {
|
||||||
|
.function-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 600px;
|
||||||
|
|
||||||
|
.function-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-id {
|
||||||
|
color: rgb(255 255 255 / 80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-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;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
|
@ -12,7 +12,7 @@ import { deviceStateOptions } from '#/constants/dicts';
|
||||||
import { useDeviceStore } from '#/store/device';
|
import { useDeviceStore } from '#/store/device';
|
||||||
|
|
||||||
import BasicInfo from './components/BasicInfo.vue';
|
import BasicInfo from './components/BasicInfo.vue';
|
||||||
import DeviceFunction from './components/DeviceFunction.vue';
|
import DeviceSimulation from './components/DeviceSimulation.vue';
|
||||||
import LogManagement from './components/LogManagement.vue';
|
import LogManagement from './components/LogManagement.vue';
|
||||||
import RunningStatus from './components/RunningStatus.vue';
|
import RunningStatus from './components/RunningStatus.vue';
|
||||||
|
|
||||||
|
@ -36,12 +36,12 @@ const loadDeviceInfo = async () => {
|
||||||
// 处理状态变更
|
// 处理状态变更
|
||||||
const handleStatusChange = async (checked: boolean) => {
|
const handleStatusChange = async (checked: boolean) => {
|
||||||
try {
|
try {
|
||||||
console.log('checked', checked);
|
|
||||||
// await deviceUpdateStatus({
|
// await deviceUpdateStatus({
|
||||||
// id: deviceId.value,
|
// id: deviceId.value,
|
||||||
// enabled: checked,
|
// enabled: checked,
|
||||||
// });
|
// });
|
||||||
// await loadDeviceInfo();
|
// await loadDeviceInfo();
|
||||||
|
console.log("checked",checked)
|
||||||
} catch {
|
} catch {
|
||||||
message.error('状态更新失败');
|
message.error('状态更新失败');
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,11 @@ onUnmounted(() => {
|
||||||
:class="deviceStateOptions.find((item) => item.value === currentDevice.deviceState)?.type"
|
:class="deviceStateOptions.find((item) => item.value === currentDevice.deviceState)?.type"
|
||||||
>•</span>
|
>•</span>
|
||||||
<span class="status-text">
|
<span class="status-text">
|
||||||
{{ deviceStateOptions.find((item) => item.value === currentDevice.deviceState)?.label }}
|
{{
|
||||||
|
deviceStateOptions.find(
|
||||||
|
(item) => item.value === currentDevice.deviceState,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
:checked="currentDevice.enabled === '1'"
|
:checked="currentDevice.enabled === '1'"
|
||||||
|
@ -138,15 +142,23 @@ 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"
|
||||||
|
:destroy-inactive-tab-pane="true"
|
||||||
|
>
|
||||||
<TabPane key="BasicInfo" tab="实例信息">
|
<TabPane key="BasicInfo" tab="实例信息">
|
||||||
<BasicInfo :device-info="currentDevice" @refresh="loadDeviceInfo" />
|
<BasicInfo :device-info="currentDevice" @refresh="loadDeviceInfo" />
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="RunningStatus" tab="运行状态">
|
<TabPane key="RunningStatus" tab="运行状态">
|
||||||
<RunningStatus :device-id="deviceId" :device-info="currentDevice" />
|
<RunningStatus :device-id="deviceId" :device-info="currentDevice" />
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="DeviceFunction" tab="设备功能">
|
<TabPane key="DeviceSimulation" tab="设备模拟">
|
||||||
<DeviceFunction :device-id="deviceId" :device-info="currentDevice" />
|
<DeviceSimulation
|
||||||
|
:device-id="deviceId"
|
||||||
|
:device-info="currentDevice"
|
||||||
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="LogManagement" tab="日志管理">
|
<TabPane key="LogManagement" tab="日志管理">
|
||||||
<LogManagement :device-id="deviceId" :device-info="currentDevice" />
|
<LogManagement :device-id="deviceId" :device-info="currentDevice" />
|
||||||
|
@ -264,11 +276,5 @@ onUnmounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-tabs {
|
|
||||||
:deep(.ant-tabs-content-holder) {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,20 +13,43 @@ import {
|
||||||
Modal,
|
Modal,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
SelectOption,
|
|
||||||
Switch,
|
Switch,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { dataTypeOptions, formTypeOptions } from '#/constants/dicts';
|
import {
|
||||||
|
dataTypeOptions,
|
||||||
|
formTypeOptions,
|
||||||
|
timeOptions,
|
||||||
|
} from '#/constants/dicts';
|
||||||
|
|
||||||
import EnumListModal from './EnumListModal.vue';
|
import EnumListModal from './EnumListModal.vue';
|
||||||
|
|
||||||
interface ParameterItem {
|
interface ParameterItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
dataType: string;
|
sort: number;
|
||||||
|
description: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
|
expands: {
|
||||||
|
source: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
valueParams: {
|
||||||
|
dataType: string;
|
||||||
|
enumConf: any[];
|
||||||
|
falseText?: string;
|
||||||
|
falseValue?: string;
|
||||||
|
format?: string;
|
||||||
formType: string;
|
formType: string;
|
||||||
|
length: any;
|
||||||
|
max?: number;
|
||||||
|
min?: number;
|
||||||
|
scale?: number;
|
||||||
|
trueText?: string;
|
||||||
|
trueValue?: string;
|
||||||
|
unit: string;
|
||||||
|
viewType: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -48,8 +71,66 @@ const visible = computed({
|
||||||
set: (value) => emit('update:open', value),
|
set: (value) => emit('update:open', value),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 根据数据类型过滤表单类型选项
|
||||||
|
const filterFormTypeOptions = computed(() => {
|
||||||
|
switch (formData?.value?.valueParams?.dataType) {
|
||||||
|
case 'boolean': {
|
||||||
|
return formTypeOptions.filter((item) => {
|
||||||
|
return item.value === 'switch';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'date': {
|
||||||
|
return formTypeOptions.filter((item) => {
|
||||||
|
return item.value === 'time';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'double':
|
||||||
|
case 'float':
|
||||||
|
case 'int':
|
||||||
|
case 'long': {
|
||||||
|
return formTypeOptions.filter((item) => {
|
||||||
|
return item.value === 'number' || item.value === 'progress';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'enum': {
|
||||||
|
return formTypeOptions.filter((item) => {
|
||||||
|
return item.value === 'select';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'string': {
|
||||||
|
return formTypeOptions.filter((item) => {
|
||||||
|
return (
|
||||||
|
item.value === 'input' ||
|
||||||
|
item.value === 'switch' ||
|
||||||
|
item.value === 'select' ||
|
||||||
|
item.value === 'time'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return formTypeOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据数据类型过滤时间格式选项
|
||||||
|
const filterTimeOptions = computed(() => {
|
||||||
|
switch (formData?.value?.valueParams?.dataType) {
|
||||||
|
case 'date': {
|
||||||
|
return timeOptions.filter((item) => {
|
||||||
|
return (
|
||||||
|
item.value === 'YYYY-MM-DD' || item.value === 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return timeOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 默认数据
|
// 默认数据
|
||||||
const defaultPropertyData: PropertyData = {
|
const defaultPropertyData: ParameterItem = {
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
sort: 1,
|
sort: 1,
|
||||||
|
@ -114,6 +195,36 @@ const modalTitle = computed(() => {
|
||||||
return props.parameterType === 'add' ? '新增参数' : '编辑参数';
|
return props.parameterType === 'add' ? '新增参数' : '编辑参数';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 处理数据类型变化
|
||||||
|
const handleDataTypeChange = (value: string) => {
|
||||||
|
switch (value) {
|
||||||
|
case 'boolean': {
|
||||||
|
formData.value.valueParams.formType = 'switch';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'date': {
|
||||||
|
formData.value.valueParams.formType = 'time';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'double':
|
||||||
|
case 'float':
|
||||||
|
case 'int':
|
||||||
|
case 'long': {
|
||||||
|
formData.value.valueParams.formType = 'number';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'enum': {
|
||||||
|
formData.value.valueParams.formType = 'select';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'string': {
|
||||||
|
formData.value.valueParams.formType = 'input';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// No default
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 验证数据
|
// 验证数据
|
||||||
const validateData = (): boolean => {
|
const validateData = (): boolean => {
|
||||||
try {
|
try {
|
||||||
|
@ -162,6 +273,19 @@ watch(
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 监听表单类型变化,设置开关默认值
|
||||||
|
watch(
|
||||||
|
() => formData.value.valueParams.formType,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal === 'switch') {
|
||||||
|
formData.value.valueParams.trueText = '是';
|
||||||
|
formData.value.valueParams.trueValue = 'true';
|
||||||
|
formData.value.valueParams.falseText = '否';
|
||||||
|
formData.value.valueParams.falseValue = 'false';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -192,32 +316,17 @@ watch(
|
||||||
<Select
|
<Select
|
||||||
v-model:value="formData.valueParams.dataType"
|
v-model:value="formData.valueParams.dataType"
|
||||||
placeholder="请选择数据类型"
|
placeholder="请选择数据类型"
|
||||||
style="width: 100%"
|
:options="dataTypeOptions"
|
||||||
>
|
@change="handleDataTypeChange"
|
||||||
<SelectOption
|
/>
|
||||||
v-for="option in dataTypeOptions"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</SelectOption>
|
|
||||||
</Select>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="表单类型" name="valueParams.formType">
|
<FormItem label="表单类型" name="valueParams.formType">
|
||||||
<Select
|
<Select
|
||||||
v-model:value="formData.valueParams.formType"
|
v-model:value="formData.valueParams.formType"
|
||||||
placeholder="请选择表单类型"
|
placeholder="请选择表单类型"
|
||||||
style="width: 100%"
|
:options="filterFormTypeOptions"
|
||||||
>
|
/>
|
||||||
<SelectOption
|
|
||||||
v-for="option in formTypeOptions"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</SelectOption>
|
|
||||||
</Select>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<!-- 开关类型配置 -->
|
<!-- 开关类型配置 -->
|
||||||
|
@ -312,6 +421,12 @@ watch(
|
||||||
<InputNumber
|
<InputNumber
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-model:value="formData.valueParams.min"
|
v-model:value="formData.valueParams.min"
|
||||||
|
:precision="
|
||||||
|
formData.valueParams.dataType === 'double' ||
|
||||||
|
formData.valueParams.dataType === 'float'
|
||||||
|
? undefined
|
||||||
|
: 0
|
||||||
|
"
|
||||||
placeholder="请输入最小值"
|
placeholder="请输入最小值"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -321,6 +436,12 @@ watch(
|
||||||
<InputNumber
|
<InputNumber
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-model:value="formData.valueParams.max"
|
v-model:value="formData.valueParams.max"
|
||||||
|
:precision="
|
||||||
|
formData.valueParams.dataType === 'double' ||
|
||||||
|
formData.valueParams.dataType === 'float'
|
||||||
|
? undefined
|
||||||
|
: 0
|
||||||
|
"
|
||||||
placeholder="请输入最大值"
|
placeholder="请输入最大值"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -330,8 +451,8 @@ watch(
|
||||||
label="小数位"
|
label="小数位"
|
||||||
name="valueParams.scale"
|
name="valueParams.scale"
|
||||||
v-if="
|
v-if="
|
||||||
formData.valueParams.formType === 'number' ||
|
formData.valueParams.dataType === 'double' ||
|
||||||
formData.valueParams.formType === 'progress'
|
formData.valueParams.dataType === 'float'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
@ -359,7 +480,7 @@ watch(
|
||||||
<Select
|
<Select
|
||||||
v-model:value="formData.valueParams.format"
|
v-model:value="formData.valueParams.format"
|
||||||
placeholder="请选择时间格式"
|
placeholder="请选择时间格式"
|
||||||
:options="timeOptions"
|
:options="filterTimeOptions"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue