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">
|
||||
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 {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Col,
|
||||
DatePicker,
|
||||
Empty,
|
||||
Modal,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import EventsPanel from './running/EventsPanel.vue';
|
||||
import RealtimePanel from './running/RealtimePanel.vue';
|
||||
|
||||
interface Props {
|
||||
deviceId: string;
|
||||
|
@ -29,543 +13,41 @@ interface Props {
|
|||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const loading = ref(false);
|
||||
const selectedGroup = ref('all');
|
||||
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;
|
||||
|
||||
// 一次性解析物模型并向下传递
|
||||
const metadata = computed(() => {
|
||||
try {
|
||||
logLoading.value = true;
|
||||
// TODO: 调用API获取属性日志数据
|
||||
// const res = await getPropertyLog({
|
||||
// deviceId: props.deviceId,
|
||||
// propertyId: currentProperty.value.id,
|
||||
// startTime: logDateRange.value[0]?.format('YYYY-MM-DD HH:mm:ss'),
|
||||
// endTime: logDateRange.value[1]?.format('YYYY-MM-DD HH:mm:ss')
|
||||
// });
|
||||
// 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' },
|
||||
];
|
||||
const raw = props.deviceInfo?.productObj?.metadata;
|
||||
if (!raw) return { properties: [], propertyGroups: [], events: [] } as any;
|
||||
const obj = JSON.parse(raw || '{}');
|
||||
return {
|
||||
properties: obj?.properties || [],
|
||||
propertyGroups: obj?.propertyGroups || [],
|
||||
events: obj?.events || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取属性日志失败:', error);
|
||||
} finally {
|
||||
logLoading.value = false;
|
||||
console.warn('parse metadata error', error);
|
||||
return { properties: [], propertyGroups: [], events: [] } as any;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间范围变化
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="running-status">
|
||||
<!-- 控制面板 -->
|
||||
<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>
|
||||
<!-- <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>
|
||||
</div>
|
||||
</Modal>
|
||||
<Tabs type="card">
|
||||
<TabPane key="realtime" tab="实时数据">
|
||||
<RealtimePanel :device-id="props.deviceId" :metadata="metadata" />
|
||||
</TabPane>
|
||||
<TabPane key="events" tab="事件">
|
||||
<EventsPanel :device-id="props.deviceId" :metadata="metadata" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.running-status {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
:deep(.ant-tabs-content-holder) {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
@ -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 BasicInfo from './components/BasicInfo.vue';
|
||||
import DeviceFunction from './components/DeviceFunction.vue';
|
||||
import DeviceSimulation from './components/DeviceSimulation.vue';
|
||||
import LogManagement from './components/LogManagement.vue';
|
||||
import RunningStatus from './components/RunningStatus.vue';
|
||||
|
||||
|
@ -36,12 +36,12 @@ const loadDeviceInfo = async () => {
|
|||
// 处理状态变更
|
||||
const handleStatusChange = async (checked: boolean) => {
|
||||
try {
|
||||
console.log('checked', checked);
|
||||
// await deviceUpdateStatus({
|
||||
// id: deviceId.value,
|
||||
// enabled: checked,
|
||||
// });
|
||||
// await loadDeviceInfo();
|
||||
console.log("checked",checked)
|
||||
} catch {
|
||||
message.error('状态更新失败');
|
||||
}
|
||||
|
@ -98,7 +98,11 @@ onUnmounted(() => {
|
|||
:class="deviceStateOptions.find((item) => item.value === currentDevice.deviceState)?.type"
|
||||
>•</span>
|
||||
<span class="status-text">
|
||||
{{ deviceStateOptions.find((item) => item.value === currentDevice.deviceState)?.label }}
|
||||
{{
|
||||
deviceStateOptions.find(
|
||||
(item) => item.value === currentDevice.deviceState,
|
||||
)?.label
|
||||
}}
|
||||
</span>
|
||||
<Switch
|
||||
:checked="currentDevice.enabled === '1'"
|
||||
|
@ -138,15 +142,23 @@ onUnmounted(() => {
|
|||
</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="实例信息">
|
||||
<BasicInfo :device-info="currentDevice" @refresh="loadDeviceInfo" />
|
||||
</TabPane>
|
||||
<TabPane key="RunningStatus" tab="运行状态">
|
||||
<RunningStatus :device-id="deviceId" :device-info="currentDevice" />
|
||||
</TabPane>
|
||||
<TabPane key="DeviceFunction" tab="设备功能">
|
||||
<DeviceFunction :device-id="deviceId" :device-info="currentDevice" />
|
||||
<TabPane key="DeviceSimulation" tab="设备模拟">
|
||||
<DeviceSimulation
|
||||
:device-id="deviceId"
|
||||
:device-info="currentDevice"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="LogManagement" tab="日志管理">
|
||||
<LogManagement :device-id="deviceId" :device-info="currentDevice" />
|
||||
|
@ -264,11 +276,5 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
:deep(.ant-tabs-content-holder) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,20 +13,43 @@ import {
|
|||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { dataTypeOptions, formTypeOptions } from '#/constants/dicts';
|
||||
import {
|
||||
dataTypeOptions,
|
||||
formTypeOptions,
|
||||
timeOptions,
|
||||
} from '#/constants/dicts';
|
||||
|
||||
import EnumListModal from './EnumListModal.vue';
|
||||
|
||||
interface ParameterItem {
|
||||
id: string;
|
||||
name: string;
|
||||
dataType: string;
|
||||
sort: number;
|
||||
description: string;
|
||||
required: boolean;
|
||||
formType: string;
|
||||
expands: {
|
||||
source: string;
|
||||
type: string;
|
||||
};
|
||||
valueParams: {
|
||||
dataType: string;
|
||||
enumConf: any[];
|
||||
falseText?: string;
|
||||
falseValue?: string;
|
||||
format?: string;
|
||||
formType: string;
|
||||
length: any;
|
||||
max?: number;
|
||||
min?: number;
|
||||
scale?: number;
|
||||
trueText?: string;
|
||||
trueValue?: string;
|
||||
unit: string;
|
||||
viewType: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -48,8 +71,66 @@ const visible = computed({
|
|||
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: '',
|
||||
name: '',
|
||||
sort: 1,
|
||||
|
@ -114,6 +195,36 @@ const modalTitle = computed(() => {
|
|||
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 => {
|
||||
try {
|
||||
|
@ -162,6 +273,19 @@ watch(
|
|||
},
|
||||
{ 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>
|
||||
|
||||
<template>
|
||||
|
@ -192,32 +316,17 @@ watch(
|
|||
<Select
|
||||
v-model:value="formData.valueParams.dataType"
|
||||
placeholder="请选择数据类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="option in dataTypeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
:options="dataTypeOptions"
|
||||
@change="handleDataTypeChange"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="表单类型" name="valueParams.formType">
|
||||
<Select
|
||||
v-model:value="formData.valueParams.formType"
|
||||
placeholder="请选择表单类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="option in formTypeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
:options="filterFormTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<!-- 开关类型配置 -->
|
||||
|
@ -312,6 +421,12 @@ watch(
|
|||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.min"
|
||||
:precision="
|
||||
formData.valueParams.dataType === 'double' ||
|
||||
formData.valueParams.dataType === 'float'
|
||||
? undefined
|
||||
: 0
|
||||
"
|
||||
placeholder="请输入最小值"
|
||||
/>
|
||||
</FormItem>
|
||||
|
@ -321,6 +436,12 @@ watch(
|
|||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.max"
|
||||
:precision="
|
||||
formData.valueParams.dataType === 'double' ||
|
||||
formData.valueParams.dataType === 'float'
|
||||
? undefined
|
||||
: 0
|
||||
"
|
||||
placeholder="请输入最大值"
|
||||
/>
|
||||
</FormItem>
|
||||
|
@ -330,8 +451,8 @@ watch(
|
|||
label="小数位"
|
||||
name="valueParams.scale"
|
||||
v-if="
|
||||
formData.valueParams.formType === 'number' ||
|
||||
formData.valueParams.formType === 'progress'
|
||||
formData.valueParams.dataType === 'double' ||
|
||||
formData.valueParams.dataType === 'float'
|
||||
"
|
||||
>
|
||||
<InputNumber
|
||||
|
@ -359,7 +480,7 @@ watch(
|
|||
<Select
|
||||
v-model:value="formData.valueParams.format"
|
||||
placeholder="请选择时间格式"
|
||||
:options="timeOptions"
|
||||
:options="filterTimeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
|
|
Loading…
Reference in New Issue