feat: 添加设备上下线模拟、产品json导入功能
- 新增设备上下线模拟接口和相关组件 - 实现设备状态模拟和高级模式下的 JSON 编辑 - 优化设备详情页面布局,添加设备图片显示 - 修复 websocket 取消订阅时的消息格式问题
This commit is contained in:
parent
a465fa498a
commit
f0a666fb1d
|
@ -52,6 +52,7 @@
|
|||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pinia": "catalog:",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"tinymce": "^7.3.0",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
|
|
|
@ -89,3 +89,12 @@ export function deviceOperateFunc(data: any) {
|
|||
export function deviceOperateEvent(data: any) {
|
||||
return requestClient.postWithMsg<void>('/device/operate/mockEvent', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下线模拟
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function deviceOperateStatus(data: any) {
|
||||
return requestClient.postWithMsg<void>('/device/operate/mockStatus', data);
|
||||
}
|
||||
|
|
|
@ -171,7 +171,7 @@ export const getWebSocket = (
|
|||
}
|
||||
|
||||
return () => {
|
||||
const unsub = JSON.stringify({ id, type: 'unsub' });
|
||||
const unsub = JSON.stringify({ id, topic, type: 'unsub' });
|
||||
const list = subs[id];
|
||||
if (Array.isArray(list)) {
|
||||
const idx = list.indexOf(handle);
|
||||
|
|
|
@ -5,6 +5,7 @@ import { TabPane, Tabs } from 'ant-design-vue';
|
|||
|
||||
import EventSimulation from './simulation/EventSimulation.vue';
|
||||
import FunctionSimulation from './simulation/FunctionSimulation.vue';
|
||||
import OnlineOfflineSimulation from './simulation/OnlineOfflineSimulation.vue';
|
||||
import PropertySimulation from './simulation/PropertySimulation.vue';
|
||||
|
||||
interface Props {
|
||||
|
@ -85,7 +86,10 @@ const eventList = computed(() => {
|
|||
/>
|
||||
</TabPane>
|
||||
<TabPane key="online" tab="上下线">
|
||||
<div>上下线功能开发中...</div>
|
||||
<OnlineOfflineSimulation
|
||||
:device-id="deviceId"
|
||||
:device-info="deviceInfo"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
@ -395,7 +395,7 @@ onMounted(() => {
|
|||
<TabPane key="simple" tab="精简模式">
|
||||
<div class="mode-info">
|
||||
<ExclamationCircleOutlined />
|
||||
精简模式下参数只支持输入框的方式录入
|
||||
精简模式下参数支持表单的方式录入
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="advanced" tab="高级模式">
|
||||
|
@ -550,7 +550,7 @@ onMounted(() => {
|
|||
</div>
|
||||
|
||||
<div class="execute-button">
|
||||
<Button type="primary" @click="handleSubmit">触发事件</Button>
|
||||
<Button type="primary" @click="handleSubmit">执行</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -574,7 +574,7 @@ onMounted(() => {
|
|||
<TabPane key="simple" tab="精简模式">
|
||||
<div class="mode-info">
|
||||
<ExclamationCircleOutlined />
|
||||
精简模式下参数只支持输入框的方式录入
|
||||
精简模式下参数支持表单的方式录入
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="advanced" tab="高级模式">
|
||||
|
|
|
@ -0,0 +1,414 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
message,
|
||||
Select,
|
||||
SelectOption,
|
||||
Table,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { deviceOperateStatus } from '#/api/device/device';
|
||||
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
||||
|
||||
interface Props {
|
||||
deviceId: string;
|
||||
deviceInfo: any;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
// 当前模式:simple(精简模式) / advanced(高级模式)
|
||||
const currentMode = ref('simple');
|
||||
// 表单引用
|
||||
const formRef = ref();
|
||||
// JSON编辑器内容
|
||||
const jsonContent = ref('{}');
|
||||
// 参数框内容
|
||||
const parameterContent = ref('');
|
||||
// 提交结果
|
||||
const submitResult = ref('');
|
||||
|
||||
// 选中的参数行
|
||||
const selectedRowKeys = ref(['status']);
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
status: '1', // 默认为上线状态
|
||||
});
|
||||
|
||||
// 固定的状态参数配置
|
||||
const statusParameter = {
|
||||
id: 'status',
|
||||
name: '状态',
|
||||
required: true,
|
||||
valueParams: {
|
||||
dataType: 'string',
|
||||
formType: 'select',
|
||||
enumConf: [
|
||||
{ value: '1', text: '上线' },
|
||||
{ value: '0', text: '下线' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
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 [
|
||||
{
|
||||
key: statusParameter.id,
|
||||
id: statusParameter.id,
|
||||
name: statusParameter.name,
|
||||
dataType: 'string',
|
||||
required: statusParameter.required,
|
||||
formType: statusParameter.valueParams.formType,
|
||||
valueParams: statusParameter.valueParams,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// 表格行选择配置
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (selectedKeys: string[]) => {
|
||||
// 状态参数必选,不允许取消选择
|
||||
if (!selectedKeys.includes('status')) {
|
||||
selectedKeys.push('status');
|
||||
}
|
||||
selectedRowKeys.value = selectedKeys;
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
disabled: record.id === 'status', // 状态参数禁用取消选择
|
||||
}),
|
||||
}));
|
||||
|
||||
// 监听模式切换,重置表单
|
||||
watch(currentMode, () => {
|
||||
resetForm();
|
||||
generateDefaultContent();
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
formData.value = {
|
||||
status: '1',
|
||||
};
|
||||
submitResult.value = '';
|
||||
};
|
||||
|
||||
// 生成默认内容
|
||||
const generateDefaultContent = () => {
|
||||
if (currentMode.value === 'advanced') {
|
||||
// 高级模式:状态参数key:null
|
||||
const params = {
|
||||
status: null,
|
||||
};
|
||||
jsonContent.value = JSON.stringify(params, null, 2);
|
||||
parameterContent.value = '';
|
||||
} else {
|
||||
// 精简模式:清空参数框
|
||||
parameterContent.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 处理JSON编辑器变化
|
||||
const handleJsonChange = (value: string) => {
|
||||
jsonContent.value = value;
|
||||
};
|
||||
|
||||
// 执行实际提交
|
||||
const executeSubmit = async () => {
|
||||
try {
|
||||
let parameters = {};
|
||||
|
||||
if (currentMode.value === 'simple') {
|
||||
// 精简模式:验证表单并获取值
|
||||
await formRef.value.validate();
|
||||
const formValues = formData.value;
|
||||
console.log('表单值:', formValues);
|
||||
|
||||
// 验证状态参数
|
||||
if (!formValues.status) {
|
||||
message.error('请选择状态');
|
||||
return;
|
||||
}
|
||||
|
||||
parameters = {
|
||||
status: formValues.status,
|
||||
};
|
||||
} else {
|
||||
// 高级模式:验证JSON格式
|
||||
try {
|
||||
parameters = JSON.parse(jsonContent.value);
|
||||
} catch {
|
||||
message.error('JSON格式错误,请检查格式');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 构造提交数据格式
|
||||
const submitData = {
|
||||
productKey: props.deviceInfo.productObj.productKey,
|
||||
deviceKey: props.deviceInfo.deviceKey,
|
||||
...parameters,
|
||||
};
|
||||
|
||||
// 更新参数框内容
|
||||
parameterContent.value = JSON.stringify(submitData, null, 2);
|
||||
|
||||
// 调用设备操作事件接口
|
||||
console.log('设备上下线:', submitData);
|
||||
const result = await deviceOperateStatus(submitData);
|
||||
|
||||
submitResult.value = JSON.stringify(result, null, 2);
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
console.error('操作错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 主要的提交函数
|
||||
const handleSubmit = async () => {
|
||||
// 直接执行提交
|
||||
await executeSubmit();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化默认值
|
||||
formData.value = {
|
||||
status: '1',
|
||||
};
|
||||
selectedRowKeys.value = ['status'];
|
||||
generateDefaultContent();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="online-offline-simulation">
|
||||
<div class="simulation-content">
|
||||
<!-- 模式切换 -->
|
||||
<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"
|
||||
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="[
|
||||
{
|
||||
required: true,
|
||||
message: `请选择${record.name}`,
|
||||
},
|
||||
]"
|
||||
style="margin-bottom: 0"
|
||||
>
|
||||
<Select
|
||||
v-model:value="formData[record.id]"
|
||||
:placeholder="`请选择${record.name}`"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="option in record?.valueParams?.enumConf || []"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.text }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.online-offline-simulation {
|
||||
.simulation-content {
|
||||
min-height: 600px;
|
||||
|
||||
.mode-tabs {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.mode-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.content-area {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.input-section {
|
||||
flex: 5;
|
||||
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-direction: column;
|
||||
gap: 16px;
|
||||
width: 300px;
|
||||
|
||||
.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: 120px;
|
||||
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>
|
|
@ -1,11 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
import { Image, message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
|
||||
import { deviceStateOptions } from '#/constants/dicts';
|
||||
// import { deviceUpdateStatus } from '#/api/device/device';
|
||||
|
@ -32,11 +33,13 @@ let deviceStatusSubscription: any = null;
|
|||
const subscribeDeviceStatus = () => {
|
||||
if (!currentDevice.value) return;
|
||||
|
||||
const productKey = currentDevice.value.productObj.productKey;
|
||||
|
||||
const id = `device-status-${currentDevice.value.deviceKey}`;
|
||||
const topic = `/device/${currentDevice.value.deviceKey}/status`;
|
||||
const topic = `/device/${productKey}/status`;
|
||||
|
||||
deviceStatusSubscription = getWebSocket(id, topic, {
|
||||
deviceId: currentDevice.value.deviceKey,
|
||||
deviceKey: currentDevice.value.deviceKey,
|
||||
}).subscribe((data: any) => {
|
||||
if (data.payload?.deviceKey === currentDevice.value.deviceKey) {
|
||||
// 更新设备状态
|
||||
|
@ -152,6 +155,16 @@ onUnmounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<QrcodeVue
|
||||
:value="currentDevice.deviceKey"
|
||||
:image-settings="{
|
||||
src: currentDevice.imgUrl,
|
||||
height: 20,
|
||||
width: 20,
|
||||
}"
|
||||
:size="80"
|
||||
level="H"
|
||||
/>
|
||||
<!-- <a-button
|
||||
type="primary"
|
||||
@click="handleStatusChange(!(currentDevice.enabled === '1'))"
|
||||
|
@ -164,6 +177,9 @@ onUnmounted(() => {
|
|||
|
||||
<!-- 设备基本信息 -->
|
||||
<div class="device-basic">
|
||||
<div class="basic-item" v-if="currentDevice.imgUrl">
|
||||
<Image :width="55" :src="currentDevice.imgUrl" />
|
||||
</div>
|
||||
<div class="basic-item">
|
||||
<span class="basic-label">ID:</span>
|
||||
<span class="basic-value">{{ deviceId }}</span>
|
||||
|
@ -221,6 +237,7 @@ onUnmounted(() => {
|
|||
background: #fff;
|
||||
|
||||
.detail-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
@ -290,12 +307,23 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.device-basic {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 16px 0;
|
||||
padding: 0;
|
||||
|
||||
.basic-item {
|
||||
display: flex;
|
||||
|
|
|
@ -91,7 +91,7 @@ const outputColumns = ref([
|
|||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
]);
|
||||
|
@ -233,6 +233,7 @@ watch(
|
|||
:columns="outputColumns"
|
||||
:data-source="formData.outputs"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 320 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
|
|
|
@ -110,6 +110,7 @@ const inputColumns = ref([
|
|||
dataIndex: 'required',
|
||||
key: 'required',
|
||||
scopedSlots: { customRender: 'required' },
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '表单类型',
|
||||
|
@ -120,8 +121,7 @@ const inputColumns = ref([
|
|||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -300,6 +300,7 @@ watch(
|
|||
:columns="inputColumns"
|
||||
:data-source="formData.inputs"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 280 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import MonacoEditor from '#/components/MonacoEditor/index.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
// 导出组件
|
||||
defineExpose({});
|
||||
|
||||
const formRef = ref();
|
||||
const importLoading = ref(false);
|
||||
|
||||
const formData = reactive({
|
||||
script: `{
|
||||
"properties": [],
|
||||
"functions": [],
|
||||
"events": [],
|
||||
"propertyGroups": []
|
||||
}`,
|
||||
options: ['validate'],
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
file: [{ required: true, message: '请选择文件' }],
|
||||
script: [{ required: true, message: '请输入脚本内容' }],
|
||||
};
|
||||
|
||||
// 是否可以导入
|
||||
const canImport = computed(() => {
|
||||
return formData.script.trim() !== '';
|
||||
});
|
||||
|
||||
// 文件导入处理
|
||||
const handleFileImport = async (file: File) => {
|
||||
const isValidType = /\.json$/.test(file.name);
|
||||
if (!isValidType) {
|
||||
message.error('只支持 JSON 格式的文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
message.error('文件大小不能超过 2MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFileContent(file);
|
||||
if (handleScriptValidate(content)) {
|
||||
Modal.confirm({
|
||||
title: '确认导入',
|
||||
content: '导入文件将覆盖当前脚本内容,是否继续?',
|
||||
onOk: () => {
|
||||
// 覆盖脚本内容
|
||||
formData.script = content;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error('当前脚本内容验证未通过,请修正后再导入');
|
||||
return false;
|
||||
}
|
||||
// 显示覆盖确认对话框
|
||||
} catch {
|
||||
message.error('文件读取失败');
|
||||
}
|
||||
|
||||
return false; // 阻止默认上传行为
|
||||
};
|
||||
|
||||
const checkJsonScript = () => {
|
||||
if (handleScriptValidate(formData.script)) {
|
||||
message.success('脚本验证成功');
|
||||
} else {
|
||||
message.error('当前脚本内容验证未通过,请修正后再导入');
|
||||
}
|
||||
};
|
||||
|
||||
// 验证脚本内容
|
||||
const handleScriptValidate = (content: any) => {
|
||||
try {
|
||||
parseFileContent(content, 'script.json');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 读取文件内容
|
||||
const readFileContent = (file: File): Promise<string> => {
|
||||
return file.text();
|
||||
};
|
||||
|
||||
// 解析文件内容
|
||||
const parseFileContent = (content: string, fileName: string) => {
|
||||
try {
|
||||
if (fileName.endsWith('.json')) {
|
||||
const data = JSON.parse(content);
|
||||
|
||||
// 验证JSON格式是否正确
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('JSON格式不正确');
|
||||
}
|
||||
|
||||
// 验证必要的字段结构
|
||||
const requiredFields = [
|
||||
'properties',
|
||||
'functions',
|
||||
'events',
|
||||
'propertyGroups',
|
||||
];
|
||||
const hasValidStructure = requiredFields.every(
|
||||
(field) => Array.isArray(data[field]) && data[field].length >= 0,
|
||||
);
|
||||
|
||||
if (!hasValidStructure) {
|
||||
throw new Error('物模型JSON格式不正确,缺少必要的属性、功能或事件字段');
|
||||
}
|
||||
|
||||
return {
|
||||
properties: data.properties || [],
|
||||
functions: data.functions || [],
|
||||
events: data.events || [],
|
||||
propertyGroups: data.propertyGroups || [],
|
||||
};
|
||||
}
|
||||
throw new Error('不支持的文件格式');
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new TypeError('JSON格式错误,请检查文件内容');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// const handleJsonChange = (value: string) => {
|
||||
// formData.script = value;
|
||||
// };
|
||||
|
||||
// 导入
|
||||
const handleImport = async () => {
|
||||
try {
|
||||
const metaDataObj = JSON.parse(formData.script);
|
||||
await formRef.value.validate();
|
||||
if (handleScriptValidate(formData.script)) {
|
||||
// 显示覆盖确认对话框
|
||||
Modal.confirm({
|
||||
title: '确认覆盖物模型',
|
||||
content: '将覆盖当前产品的物模型,是否继续?',
|
||||
onOk: () => {
|
||||
importLoading.value = true;
|
||||
emit('change', metaDataObj);
|
||||
emit('success');
|
||||
importLoading.value = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error('当前脚本内容验证未通过,请修正后再覆盖');
|
||||
}
|
||||
|
||||
// 这里调用导入API
|
||||
// const res = await importMetadata({
|
||||
// type: formData.importType,
|
||||
// data: formData,
|
||||
// options: formData.options,
|
||||
// });
|
||||
|
||||
// // 模拟导入过程
|
||||
// await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// message.success('导入成功');
|
||||
// emit('success');
|
||||
} catch {
|
||||
message.error('导入失败');
|
||||
} finally {
|
||||
importLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('success');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="import-form">
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<!-- 物模型脚本编辑器 -->
|
||||
<FormItem label="物模型脚本" name="script">
|
||||
<div class="script-header">
|
||||
<Space>
|
||||
<Upload
|
||||
:show-upload-list="false"
|
||||
:before-upload="handleFileImport"
|
||||
:custom-request="() => {}"
|
||||
accept=".json"
|
||||
>
|
||||
<Button>
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
导入JSON文件
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button @click="checkJsonScript"> 验证脚本 </Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div class="script-editor">
|
||||
<MonacoEditor
|
||||
v-model="formData.script"
|
||||
lang="json"
|
||||
theme="vs"
|
||||
style="height: 400px; border: 1px solid #d9d9d9; border-radius: 6px"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<div class="form-actions">
|
||||
<Space>
|
||||
<Button @click="handleCancel">取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleImport"
|
||||
:loading="importLoading"
|
||||
:disabled="!canImport"
|
||||
>
|
||||
覆盖物模型
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.import-form {
|
||||
.upload-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.preview-area {
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.preview-tips {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
background: #f0f9ff;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.category-preview {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 500;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.script-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.script-editor {
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -87,8 +87,6 @@ const handleViewTSL = () => {
|
|||
// 导入成功
|
||||
const handleImportSuccess = () => {
|
||||
importVisible.value = false;
|
||||
loadMetadata();
|
||||
message.success('导入成功');
|
||||
};
|
||||
|
||||
// 关闭导入抽屉
|
||||
|
@ -298,6 +296,12 @@ const handleMetadataChange = (data: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
// 物模型数据变更
|
||||
const handleMetadataImport = (data: any) => {
|
||||
console.log('data', data);
|
||||
currentMetadata.value = data;
|
||||
};
|
||||
|
||||
// 监听路由变化,提示未保存
|
||||
const handleBeforeRouteLeave = (next: any) => {
|
||||
if (metadataChanged.value) {
|
||||
|
@ -424,7 +428,10 @@ onBeforeRouteLeave((to, from, next) => {
|
|||
width="600px"
|
||||
@close="handleImportClose"
|
||||
>
|
||||
<ImportForm @success="handleImportSuccess" />
|
||||
<ImportForm
|
||||
@change="handleMetadataImport"
|
||||
@success="handleImportSuccess"
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<!-- TSL查看抽屉 -->
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
import { Image, message, Modal, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
productPushMetadataById,
|
||||
|
@ -144,7 +144,12 @@ onUnmounted(() => {
|
|||
|
||||
<!-- 产品统计信息 -->
|
||||
<div class="product-stats">
|
||||
<span>设备数量:</span><a @click="jumpToDevices">{{ deviceCount }}</a>
|
||||
<div class="basic-item" v-if="currentProduct.imgUrl">
|
||||
<Image :width="55" :src="currentProduct.imgUrl" />
|
||||
</div>
|
||||
<div class="basic-item">
|
||||
<span>设备数量:</span><a @click="jumpToDevices">{{ deviceCount }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
|
@ -218,7 +223,9 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
.product-stats {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
|
|
Loading…
Reference in New Issue