feat: 新增产品物模型编辑功能
- 添加物模型编辑组件,包括属性(完成)、功能和事件定义 - 实现物模型数据的加载、保存和重置功能 - 增加枚举列表编辑弹窗 - 优化物模型表格展示和操作
This commit is contained in:
parent
b86957758a
commit
fde2ec9bae
|
@ -0,0 +1,215 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { Empty, Input, message, Modal } from 'ant-design-vue';
|
||||
|
||||
interface EnumItem {
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
enumConf?: EnumItem[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:open', value: boolean): void;
|
||||
(e: 'confirm', value: EnumItem[]): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: (value) => emit('update:open', value),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const enumList = ref<EnumItem[]>([]);
|
||||
|
||||
// 监听enumConf变化,初始化数据
|
||||
watch(
|
||||
() => props.enumConf,
|
||||
(newVal) => {
|
||||
enumList.value = newVal && newVal.length > 0 ? [...newVal] : [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 添加枚举项
|
||||
const addEnumItem = () => {
|
||||
enumList.value.push({ value: '', text: '' });
|
||||
};
|
||||
|
||||
// 删除枚举项
|
||||
const removeEnumItem = (index: number) => {
|
||||
enumList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 验证数据
|
||||
const validateData = (): boolean => {
|
||||
// 检查是否有空值
|
||||
const hasEmptyFields = enumList.value.some(
|
||||
(item) => !item.value || !item.text,
|
||||
);
|
||||
if (hasEmptyFields) {
|
||||
message.error('请填写完整的枚举文本和枚举值');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有重复的value
|
||||
const values = enumList.value.map((item) => item.value);
|
||||
const uniqueValues = new Set(values);
|
||||
if (values.length !== uniqueValues.size) {
|
||||
message.error('枚举值不能重复');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 确认
|
||||
const handleOk = async () => {
|
||||
if (!validateData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 过滤掉空值
|
||||
const validList = enumList.value.filter((item) => item.value && item.text);
|
||||
emit('confirm', validList);
|
||||
visible.value = false;
|
||||
message.success('保存成功');
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="visible"
|
||||
title="枚举列表"
|
||||
width="600px"
|
||||
:mask-closable="false"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="loading"
|
||||
>
|
||||
<div class="enum-list-container">
|
||||
<!-- 表头 -->
|
||||
<div class="enum-header">
|
||||
<div class="header-item">枚举文本</div>
|
||||
<div class="header-item">枚举值</div>
|
||||
<div class="header-item">操作</div>
|
||||
</div>
|
||||
|
||||
<!-- 枚举项列表 -->
|
||||
<div class="enum-items">
|
||||
<div v-for="(item, index) in enumList" :key="index" class="enum-item">
|
||||
<div class="item-field">
|
||||
<Input
|
||||
v-model:value="item.text"
|
||||
placeholder="请输入"
|
||||
:status="item.text ? '' : 'error'"
|
||||
/>
|
||||
</div>
|
||||
<div class="item-field">
|
||||
<Input
|
||||
v-model:value="item.value"
|
||||
placeholder="请输入"
|
||||
:status="item.value ? '' : 'error'"
|
||||
/>
|
||||
</div>
|
||||
<div class="item-field">
|
||||
<a-button type="link" danger @click="removeEnumItem(index)">
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="enumList.length === 0">
|
||||
<div class="empty-container">
|
||||
<Empty description="暂无枚举项" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<div class="add-button-container">
|
||||
<a-button @click="addEnumItem" class="add-button">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加一行数据
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.enum-list-container {
|
||||
.enum-header {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
|
||||
.header-item {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
flex: 0 0 80px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enum-items {
|
||||
.enum-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.item-field {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
|
||||
&:last-child {
|
||||
flex: 0 0 80px;
|
||||
margin-right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-button-container {
|
||||
padding-top: 16px;
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
border-top: 1px dashed #d9d9d9;
|
||||
|
||||
.add-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,404 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { productUpdateById } from '#/api/device/product';
|
||||
|
||||
import ImportForm from './ImportForm.vue';
|
||||
import MetadataTable from './MetadataTable.vue';
|
||||
import TSLViewer from './TSLViewer.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
productId: string;
|
||||
productInfo: ProductVO;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const activeTab = ref('properties');
|
||||
const importVisible = ref(false);
|
||||
const tslVisible = ref(false);
|
||||
const showReset = ref(false);
|
||||
|
||||
// 物模型编辑状态
|
||||
const metadataChanged = ref(false);
|
||||
const originalMetadata = ref<any>(null);
|
||||
const currentMetadata = ref<any>({
|
||||
properties: [],
|
||||
functions: [],
|
||||
events: [],
|
||||
});
|
||||
|
||||
// 标签页切换
|
||||
const handleTabChange = (key: string) => {
|
||||
activeTab.value = key;
|
||||
};
|
||||
|
||||
// 重置操作
|
||||
const handleReset = () => {
|
||||
Modal.confirm({
|
||||
title: '确认重置',
|
||||
content: '重置后将使用产品的物模型配置,确认继续吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 这里调用重置API
|
||||
message.success('重置成功');
|
||||
loadMetadata();
|
||||
metadataChanged.value = false;
|
||||
} catch {
|
||||
message.error('重置失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 快速导入
|
||||
const handleImport = () => {
|
||||
importVisible.value = true;
|
||||
};
|
||||
|
||||
// 查看TSL
|
||||
const handleViewTSL = () => {
|
||||
tslVisible.value = true;
|
||||
};
|
||||
|
||||
// 导入成功
|
||||
const handleImportSuccess = () => {
|
||||
importVisible.value = false;
|
||||
loadMetadata();
|
||||
message.success('导入成功');
|
||||
};
|
||||
|
||||
// 关闭导入抽屉
|
||||
const handleImportClose = () => {
|
||||
importVisible.value = false;
|
||||
};
|
||||
|
||||
// 关闭TSL抽屉
|
||||
const handleTSLClose = () => {
|
||||
tslVisible.value = false;
|
||||
};
|
||||
|
||||
// 加载物模型数据
|
||||
const loadMetadata = async () => {
|
||||
console.log('加载物模型数据');
|
||||
const defaultMetadata = {
|
||||
properties: [],
|
||||
functions: [],
|
||||
events: [],
|
||||
};
|
||||
try {
|
||||
currentMetadata.value = props.productInfo.metadata
|
||||
? JSON.parse(props.productInfo.metadata)
|
||||
: JSON.parse(JSON.stringify(defaultMetadata));
|
||||
// 这里调用API加载物模型数据
|
||||
// const res = await getProductMetadata(props.productId);
|
||||
// currentMetadata.value = res.data || { properties: [], functions: [], events: [] };
|
||||
|
||||
// 模拟数据
|
||||
// currentMetadata.value = {
|
||||
// properties: [
|
||||
// {
|
||||
// id: 'switch',
|
||||
// name: '开关状态',
|
||||
// sort: 1,
|
||||
// description: '开关状态描述',
|
||||
// required: false,
|
||||
// expands: {
|
||||
// source: 'device',
|
||||
// type: 'R',
|
||||
// },
|
||||
// valueParams: {
|
||||
// dataType: 'boolean',
|
||||
// formType: 'switch',
|
||||
// min: 0,
|
||||
// max: 1,
|
||||
// length: 1,
|
||||
// viewType: 'switch',
|
||||
// enumConf: [],
|
||||
// arrayConf: { type: 'string' },
|
||||
// objectConf: [],
|
||||
// unit: '',
|
||||
// scale: 2,
|
||||
// format: 'yyyy-MM-dd HH:mm:ss',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// functions: [
|
||||
// {
|
||||
// id: 'changeswitch',
|
||||
// name: '控制开关',
|
||||
// sort: 1,
|
||||
// description: '开关状态描述',
|
||||
// async: false,
|
||||
// expands: {},
|
||||
// inputs: [
|
||||
// {
|
||||
// id: 'switch',
|
||||
// name: '开关',
|
||||
// required: true,
|
||||
// valueParams: {
|
||||
// dataType: 'boolean',
|
||||
// formType: 'switch',
|
||||
// min: 0,
|
||||
// max: 1,
|
||||
// length: 1,
|
||||
// viewType: 'switch',
|
||||
// enumConf: [],
|
||||
// objectConf: [],
|
||||
// unit: '',
|
||||
// scale: 2,
|
||||
// format: 'yyyy-MM-dd HH:mm:ss',
|
||||
// },
|
||||
// expands: {},
|
||||
// },
|
||||
// ],
|
||||
// outputs: [],
|
||||
// },
|
||||
// ],
|
||||
// events: [
|
||||
// {
|
||||
// id: 'event1',
|
||||
// name: '事件1',
|
||||
// sort: 1,
|
||||
// description: '事件1描述',
|
||||
// outputs: [
|
||||
// {
|
||||
// id: 'param1',
|
||||
// name: '参数1',
|
||||
// expands: {},
|
||||
// valueParams: {
|
||||
// type: 'string',
|
||||
// expands: {},
|
||||
// enumConf: [],
|
||||
// objectConf: [],
|
||||
// unit: '',
|
||||
// scale: 2,
|
||||
// format: 'yyyy-MM-dd HH:mm:ss',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
|
||||
// 保存原始数据用于比较
|
||||
originalMetadata.value = JSON.parse(JSON.stringify(currentMetadata.value));
|
||||
metadataChanged.value = false;
|
||||
} catch {
|
||||
currentMetadata.value = JSON.parse(JSON.stringify(defaultMetadata));
|
||||
originalMetadata.value = JSON.parse(JSON.stringify(defaultMetadata));
|
||||
metadataChanged.value = false;
|
||||
message.error('加载物模型数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存物模型
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 这里调用保存API
|
||||
const metadata = JSON.parse(JSON.stringify(currentMetadata.value));
|
||||
metadata.properties.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.functions.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.events.sort((a: any, b: any) => a.sort - b.sort);
|
||||
await productUpdateById(props.productInfo.id, {
|
||||
id: props.productInfo.id,
|
||||
metadata: JSON.stringify(metadata),
|
||||
});
|
||||
|
||||
message.success('保存成功');
|
||||
// 更新原始数据
|
||||
originalMetadata.value = JSON.parse(JSON.stringify(metadata));
|
||||
metadataChanged.value = false;
|
||||
emit('refresh');
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 物模型数据变更
|
||||
const handleMetadataChange = (data: any) => {
|
||||
console.log('data', data);
|
||||
// metadataChanged.value = true;
|
||||
switch (activeTab.value) {
|
||||
case 'events': {
|
||||
currentMetadata.value.events = data;
|
||||
break;
|
||||
}
|
||||
case 'functions': {
|
||||
currentMetadata.value.functions = data;
|
||||
break;
|
||||
}
|
||||
case 'properties': {
|
||||
currentMetadata.value.properties = data;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化,提示未保存
|
||||
// const handleBeforeRouteLeave = (to: any, from: any, next: any) => {
|
||||
// if (metadataChanged.value) {
|
||||
// Modal.confirm({
|
||||
// title: '提示',
|
||||
// content: '物模型有未保存的修改,确认离开吗?',
|
||||
// onOk: () => next(),
|
||||
// onCancel: () => next(false),
|
||||
// });
|
||||
// } else {
|
||||
// next();
|
||||
// }
|
||||
// };
|
||||
|
||||
// 监听物模型数据变化
|
||||
watch(
|
||||
currentMetadata,
|
||||
() => {
|
||||
if (originalMetadata.value) {
|
||||
metadataChanged.value =
|
||||
JSON.stringify(currentMetadata.value) !==
|
||||
JSON.stringify(originalMetadata.value);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 初始化
|
||||
loadMetadata();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="metadata">
|
||||
<div class="metadata-header">
|
||||
<div class="header-left">
|
||||
<h3>物模型</h3>
|
||||
<span class="desc">
|
||||
设备会默认继承产品的物模型,继承的物模型不支持删改
|
||||
</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Space>
|
||||
<Button
|
||||
v-if="metadataChanged"
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
v-if="showReset"
|
||||
@click="handleReset"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
重置操作
|
||||
</Button>
|
||||
<Button @click="handleImport" v-access:code="['device:product:edit']">
|
||||
快速导入
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleViewTSL"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
物模型TSL
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs v-model:active-key="activeTab" type="card" @change="handleTabChange">
|
||||
<TabPane key="properties" tab="属性定义">
|
||||
<MetadataTable
|
||||
type="properties"
|
||||
:product-id="productInfo.id"
|
||||
:metadata="currentMetadata.properties"
|
||||
@change="handleMetadataChange"
|
||||
@refresh="loadMetadata"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="functions" tab="功能定义">
|
||||
<MetadataTable
|
||||
type="functions"
|
||||
:product-id="productInfo.id"
|
||||
:metadata="currentMetadata.functions"
|
||||
@change="handleMetadataChange"
|
||||
@refresh="loadMetadata"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="events" tab="事件定义">
|
||||
<MetadataTable
|
||||
type="events"
|
||||
:product-id="productInfo.id"
|
||||
:metadata="currentMetadata.events"
|
||||
@change="handleMetadataChange"
|
||||
@refresh="loadMetadata"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
<!-- 导入抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="importVisible"
|
||||
title="快速导入"
|
||||
width="600px"
|
||||
@close="handleImportClose"
|
||||
>
|
||||
<ImportForm @success="handleImportSuccess" />
|
||||
</Drawer>
|
||||
|
||||
<!-- TSL查看抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="tslVisible"
|
||||
title="物模型TSL"
|
||||
width="800px"
|
||||
@close="handleTSLClose"
|
||||
>
|
||||
<TSLViewer :product-id="productInfo.id" />
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.metadata {
|
||||
.metadata-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,749 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Popconfirm,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import EnumListModal from './EnumListModal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
metadata: any[];
|
||||
productId: string;
|
||||
type: 'events' | 'functions' | 'properties' | 'tags';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [];
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const drawerVisible = ref(false);
|
||||
const saveLoading = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
// 枚举列表弹窗相关
|
||||
const enumModalVisible = ref(false);
|
||||
|
||||
const tableData = ref<any[]>([]);
|
||||
|
||||
const defaultPropertiesFormData = {
|
||||
id: '',
|
||||
name: '',
|
||||
sort: 1,
|
||||
description: '',
|
||||
required: false,
|
||||
expands: {
|
||||
source: 'device',
|
||||
type: 'R',
|
||||
},
|
||||
valueParams: {
|
||||
dataType: 'string',
|
||||
formType: 'input',
|
||||
length: null,
|
||||
viewType: 'input',
|
||||
unit: '',
|
||||
enumConf: [],
|
||||
},
|
||||
};
|
||||
|
||||
const formData = ref({});
|
||||
|
||||
const formRules = {
|
||||
id: [
|
||||
{ required: true, message: '请输入标识符', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-z]\w*$/i,
|
||||
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
valueParams: [{ required: true, message: '请选择数据类型', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
// 数据类型选项
|
||||
const dataTypeOptions = ref([
|
||||
{
|
||||
value: 'int',
|
||||
label: 'int(整数型)',
|
||||
},
|
||||
{
|
||||
value: 'long',
|
||||
label: 'long(长整数型)',
|
||||
},
|
||||
{
|
||||
value: 'float',
|
||||
label: 'float(单精度浮点型)',
|
||||
},
|
||||
{
|
||||
value: 'double',
|
||||
label: 'double(双精度浮点数)',
|
||||
},
|
||||
{
|
||||
value: 'string',
|
||||
label: 'text(字符串)',
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
label: 'boolean(布尔型)',
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: 'date(时间型)',
|
||||
},
|
||||
{
|
||||
value: 'enum',
|
||||
label: 'enum(枚举)',
|
||||
},
|
||||
// {
|
||||
// value: 'array',
|
||||
// label: 'array(数组)',
|
||||
// },
|
||||
// {
|
||||
// value: 'object',
|
||||
// label: 'object(结构体)',
|
||||
// },
|
||||
// {
|
||||
// value: 'file',
|
||||
// label: 'file(文件)',
|
||||
// },
|
||||
// {
|
||||
// value: 'password',
|
||||
// label: 'password(密码)',
|
||||
// },
|
||||
// {
|
||||
// value: 'geoPoint',
|
||||
// label: 'geoPoint(地理位置)',
|
||||
// }
|
||||
]);
|
||||
|
||||
// 表单类型选项
|
||||
const formTypeOptions = ref([
|
||||
{ label: '文本', value: 'input' },
|
||||
{ label: '开关', value: 'switch' },
|
||||
{ label: '数字', value: 'number' },
|
||||
{ label: '进度条', value: 'progress' },
|
||||
{ label: '选择器', value: 'select' },
|
||||
{ label: '时间选择器', value: 'time' },
|
||||
// { label: '文本域', value: 'textarea' },
|
||||
]);
|
||||
|
||||
const viewTypeOptions = ref([
|
||||
{ label: '文本', value: 'input' },
|
||||
{ label: '开关', value: 'switch' },
|
||||
{ label: '选择器', value: 'select' },
|
||||
{ label: '进度条', value: 'progress' },
|
||||
{ label: '图片', value: 'img' },
|
||||
{ label: '折线图', value: 'line' },
|
||||
{ label: '仪表盘', value: 'dashboard' },
|
||||
]);
|
||||
|
||||
// 读写类型选项
|
||||
const readWriteTypeOptions = ref([
|
||||
{ label: '读', value: 'R' },
|
||||
{ label: '写', value: 'W' },
|
||||
{ label: '读写', value: 'RW' },
|
||||
]);
|
||||
|
||||
const timeOptions = ref([
|
||||
{ label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
|
||||
{ label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
|
||||
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
|
||||
]);
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => {
|
||||
const baseColumns = [
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
key: 'sort',
|
||||
width: 80,
|
||||
sorter: (a: any, b: any) => a.sort - b.sort,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
// 功能定义特殊列
|
||||
if (props.type === 'properties') {
|
||||
baseColumns.splice(
|
||||
3,
|
||||
0,
|
||||
{
|
||||
title: '数据类型',
|
||||
dataIndex: 'dataType',
|
||||
key: 'dataType',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '读写类型',
|
||||
dataIndex: 'expands.type',
|
||||
key: 'expands.type',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 功能定义特殊列
|
||||
if (props.type === 'functions') {
|
||||
baseColumns.splice(
|
||||
3,
|
||||
0,
|
||||
{
|
||||
title: '是否异步',
|
||||
dataIndex: 'async',
|
||||
key: 'async',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '输入参数',
|
||||
dataIndex: 'inputs',
|
||||
key: 'inputs',
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 事件定义特殊列
|
||||
if (props.type === 'events') {
|
||||
baseColumns.splice(3, 0, {
|
||||
title: '输出参数',
|
||||
dataIndex: 'outputs',
|
||||
key: 'outputs',
|
||||
width: 100,
|
||||
});
|
||||
}
|
||||
|
||||
return baseColumns;
|
||||
});
|
||||
|
||||
// 获取类型标签
|
||||
const getTypeLabel = () => {
|
||||
const labels = {
|
||||
properties: '属性',
|
||||
functions: '功能',
|
||||
events: '事件',
|
||||
tags: '标签',
|
||||
};
|
||||
return labels[props.type];
|
||||
};
|
||||
|
||||
// 抽屉标题
|
||||
const drawerTitle = computed(() => {
|
||||
return isEdit.value ? `编辑${getTypeLabel()}` : `新增${getTypeLabel()}`;
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 使用传入的metadata数据
|
||||
tableData.value = [...(props.metadata || [])];
|
||||
} catch {
|
||||
message.error('加载数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
isEdit.value = true;
|
||||
formData.value = structuredClone(record.value);
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
// 本地删除
|
||||
const index = tableData.value.findIndex((item) => item.id === record.id);
|
||||
if (index !== -1) {
|
||||
tableData.value.splice(index, 1);
|
||||
emit('change', tableData.value);
|
||||
message.success('删除成功');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
saveLoading.value = true;
|
||||
|
||||
if (isEdit.value) {
|
||||
// 编辑模式
|
||||
const index = tableData.value.findIndex(
|
||||
(item) => item.id === formData.value.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
tableData.value[index] = { ...formData.value };
|
||||
}
|
||||
} else {
|
||||
// 新增模式
|
||||
tableData.value.push({ ...formData.value });
|
||||
}
|
||||
|
||||
message.success('保存成功');
|
||||
drawerVisible.value = false;
|
||||
emit('change', tableData.value);
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭抽屉
|
||||
const handleDrawerClose = () => {
|
||||
drawerVisible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData.value, structuredClone(defaultPropertiesFormData));
|
||||
};
|
||||
|
||||
// 打开枚举列表编辑弹窗
|
||||
const handleEditEnum = () => {
|
||||
enumModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 确认枚举列表
|
||||
const handleEnumConfirm = (enumList: any[]) => {
|
||||
formData.value.valueParams.enumConf = enumList;
|
||||
enumModalVisible.value = false;
|
||||
};
|
||||
|
||||
// 监听metadata变化
|
||||
watch(
|
||||
() => props.metadata,
|
||||
() => {
|
||||
loadData();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="metadata-table">
|
||||
<div class="table-header">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增{{ getTypeLabel() }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确认删除?" @confirm="handleDelete(record)">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataType'">
|
||||
<Tag color="processing">
|
||||
{{
|
||||
dataTypeOptions.find(
|
||||
(item) => item.value === record.valueParams?.dataType,
|
||||
)?.label
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'expands.type'">
|
||||
<Tag color="processing">
|
||||
{{
|
||||
readWriteTypeOptions.find(
|
||||
(item) => item.value === record.expands.type,
|
||||
)?.label
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'required'">
|
||||
<Tag :color="record.required ? 'red' : 'default'">
|
||||
{{ record.required ? '必填' : '可选' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'async'">
|
||||
<Tag :color="record.async ? 'blue' : 'default'">
|
||||
{{ record.async ? '异步' : '同步' }}
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- 新增/编辑抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
width="800px"
|
||||
@close="handleDrawerClose"
|
||||
>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<Row :gutter="24">
|
||||
<Col :span="12">
|
||||
<FormItem label="标识符" name="id">
|
||||
<Input v-model:value="formData.id" placeholder="请输入标识符" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="名称" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名称" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<!-- 值参数配置 -->
|
||||
<Col :span="24">
|
||||
<FormItem label="数据类型" name="dataType">
|
||||
<Select
|
||||
v-model:value="formData.valueParams.dataType"
|
||||
placeholder="请选择数据类型"
|
||||
:options="dataTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="表单类型" name="valueParams.formType">
|
||||
<Select
|
||||
v-model:value="formData.valueParams.formType"
|
||||
placeholder="请选择表单类型"
|
||||
:options="formTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="展示类型" name="valueParams.viewType">
|
||||
<Select
|
||||
v-model:value="formData.valueParams.viewType"
|
||||
placeholder="请选择展示类型"
|
||||
:options="viewTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="24" v-if="formData.valueParams.formType === 'switch'">
|
||||
<Row
|
||||
:gutter="24"
|
||||
style="
|
||||
padding-top: 16px;
|
||||
margin: 0;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<Col :span="12">
|
||||
<FormItem label="开关打开名称" name="valueParams.trueText">
|
||||
<Input
|
||||
v-model:value="formData.valueParams.trueText"
|
||||
default-value="是"
|
||||
placeholder="请输入开关打开名称"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="开关打开值" name="valueParams.trueValue">
|
||||
<Input
|
||||
v-model:value="formData.valueParams.trueValue"
|
||||
default-value="true"
|
||||
placeholder="请输入开关打开值"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="开关关闭名称" name="valueParams.falseText">
|
||||
<Input
|
||||
v-model:value="formData.valueParams.falseText"
|
||||
default-value="否"
|
||||
placeholder="请输入开关关闭名称"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="开关关闭值" name="valueParams.falseValue">
|
||||
<Input
|
||||
v-model:value="formData.valueParams.falseValue"
|
||||
default-value="false"
|
||||
placeholder="请输入开关关闭值"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Col :span="24" v-if="formData.valueParams.formType === 'select'">
|
||||
<FormItem label="枚举列表" name="valueParams.enumList">
|
||||
<a-button type="primary" @click="handleEditEnum">
|
||||
编辑枚举列表
|
||||
</a-button>
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="
|
||||
formData.valueParams.formType === 'number' ||
|
||||
formData.valueParams.formType === 'progress'
|
||||
"
|
||||
>
|
||||
<FormItem label="最小值" name="valueParams.min">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.min"
|
||||
placeholder="请输入最小值"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="
|
||||
formData.valueParams.formType === 'number' ||
|
||||
formData.valueParams.formType === 'progress'
|
||||
"
|
||||
>
|
||||
<FormItem label="最大值" name="valueParams.max">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.max"
|
||||
placeholder="请输入最大值"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12" v-if="formData.valueParams.formType === 'input'">
|
||||
<FormItem label="长度" name="valueParams.length">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.length"
|
||||
placeholder="请输入长度"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="
|
||||
formData.valueParams.formType === 'number' ||
|
||||
formData.valueParams.formType === 'progress'
|
||||
"
|
||||
>
|
||||
<FormItem label="小数位" name="valueParams.scale">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.valueParams.scale"
|
||||
placeholder="请输入小数位"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="单位" name="valueParams.unit">
|
||||
<Input
|
||||
v-model:value="formData.valueParams.unit"
|
||||
placeholder="请输入单位"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12" v-if="formData.valueParams.formType === 'time'">
|
||||
<FormItem label="时间格式" name="valueParams.format">
|
||||
<Select
|
||||
v-model:value="formData.valueParams.format"
|
||||
placeholder="请选择时间格式"
|
||||
:options="timeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<!-- 属性特有字段 -->
|
||||
<FormItem label="是否必填" name="required">
|
||||
<Switch v-model:checked="formData.required" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<!-- 属性特有字段 -->
|
||||
<FormItem label="读写类型" name="type">
|
||||
<RadioGroup
|
||||
v-model:value="formData.expands.type"
|
||||
button-style="solid"
|
||||
>
|
||||
<RadioButton
|
||||
:value="item.value"
|
||||
v-for="item in readWriteTypeOptions"
|
||||
:key="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
||||
<Col :span="12">
|
||||
<FormItem label="排序" name="sort">
|
||||
<InputNumber
|
||||
style="width: 100%"
|
||||
v-model:value="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
||||
<Col :span="24">
|
||||
<FormItem label="描述" name="description">
|
||||
<Textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入描述"
|
||||
:rows="3"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleDrawerClose">取消</Button>
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
|
||||
<!-- 枚举列表编辑弹窗 -->
|
||||
<EnumListModal
|
||||
v-model:open="enumModalVisible"
|
||||
:enum-conf="formData.valueParams?.enumConf"
|
||||
@confirm="handleEnumConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.metadata-table {
|
||||
.table-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.value-params-section {
|
||||
padding-top: 16px;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.enum-preview {
|
||||
.enum-items-preview {
|
||||
padding: 12px;
|
||||
margin-top: 12px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
|
||||
.preview-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.preview-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -151,7 +151,11 @@ onUnmounted(() => {
|
|||
/>
|
||||
</TabPane>
|
||||
<TabPane key="Metadata" tab="物模型">
|
||||
<Metadata :product-id="productId" />
|
||||
<Metadata
|
||||
:product-id="productId"
|
||||
:product-info="currentProduct"
|
||||
@refresh="loadProductInfo"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="DeviceAccess" tab="设备接入">
|
||||
<DeviceAccess
|
||||
|
|
|
@ -2,4 +2,10 @@
|
|||
|
||||
import { defineConfig } from '@vben/eslint-config';
|
||||
|
||||
export default defineConfig();
|
||||
export default defineConfig([{
|
||||
rules: {
|
||||
'unicorn/prefer-structured-clone': 'off',
|
||||
},
|
||||
}]);
|
||||
|
||||
// export default defineConfig();
|
||||
|
|
Loading…
Reference in New Issue