feat: 新增产品物模型编辑功能

- 添加物模型编辑组件,包括属性(完成)、功能和事件定义
- 实现物模型数据的加载、保存和重置功能
- 增加枚举列表编辑弹窗
- 优化物模型表格展示和操作
This commit is contained in:
fhysy 2025-08-14 16:57:25 +08:00
parent b86957758a
commit fde2ec9bae
5 changed files with 1380 additions and 2 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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();