430 lines
14 KiB
Vue
430 lines
14 KiB
Vue
<template>
|
|
<div class="metadata-map">
|
|
<div class="left">
|
|
<j-space style="margin-bottom: 24px">
|
|
<j-select
|
|
@change="onSearchChange"
|
|
show-search
|
|
allow-clear
|
|
placeholder="请选择属性名称"
|
|
style="width: 250px"
|
|
>
|
|
<j-select-option
|
|
:label="item.name"
|
|
v-for="item in dataSourceCache"
|
|
:value="item?.id"
|
|
:key="item?.id"
|
|
>{{ item?.name }}</j-select-option
|
|
>
|
|
</j-select>
|
|
<j-button type="primary" @click="onSearch"
|
|
><AIcon type="SearchOutlined"
|
|
/></j-button>
|
|
</j-space>
|
|
<div class="box">
|
|
<j-scrollbar height="100%">
|
|
<j-table
|
|
:columns="columns"
|
|
:data-source="dataSource"
|
|
:pagination="false"
|
|
:rowSelection="{
|
|
selectedRowKeys: selectedKeys,
|
|
hideSelectAll: true,
|
|
columnWidth: 0,
|
|
}"
|
|
rowKey="id"
|
|
:customRow="customRow"
|
|
>
|
|
<template #headerCell="{ column }">
|
|
<template v-if="column.dataIndex === 'original'">
|
|
<div
|
|
style="
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
"
|
|
>
|
|
<span>
|
|
目标属性<j-tooltip
|
|
title="协议包中物模型下的属性"
|
|
>
|
|
<AIcon
|
|
style="margin-left: 10px"
|
|
type="QuestionCircleOutlined"
|
|
/>
|
|
</j-tooltip>
|
|
</span>
|
|
<j-tag
|
|
v-if="filterValue !== undefined"
|
|
color="#87d068"
|
|
closable
|
|
@close="onClose"
|
|
><AIcon
|
|
type="ArrowUpOutlined"
|
|
/><span>{{
|
|
filterValue ? '已映射' : '未映射'
|
|
}}</span></j-tag
|
|
>
|
|
<j-dropdown v-else>
|
|
<AIcon type="FilterOutlined" />
|
|
<template #overlay>
|
|
<j-menu @click="onFilter">
|
|
<j-menu-item :key="true"
|
|
>置顶已映射数据</j-menu-item
|
|
>
|
|
<j-menu-item :key="false"
|
|
>置顶未映射数据</j-menu-item
|
|
>
|
|
</j-menu>
|
|
</template>
|
|
</j-dropdown>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template #bodyCell="{ column, text, record }">
|
|
<template v-if="column.dataIndex === 'name'">
|
|
<span class="metadata-title" ref="title">
|
|
<j-ellipsis>
|
|
{{ text }} ({{ record.id }})
|
|
</j-ellipsis>
|
|
</span>
|
|
</template>
|
|
<template v-if="column.dataIndex === 'original'">
|
|
<j-select
|
|
v-model:value="record.original"
|
|
style="width: 100%"
|
|
allowClear
|
|
@change="(id) => onChange(record, id)"
|
|
placeholder="请选择"
|
|
>
|
|
<j-select-option
|
|
v-for="(item, index) in targetOptions"
|
|
:key="index + '_' + item.id"
|
|
:value="item.value"
|
|
:disabled="
|
|
selectedOriginalKeys.includes(
|
|
item.id,
|
|
)
|
|
"
|
|
>
|
|
{{ item.label }} ({{
|
|
item.id
|
|
}})</j-select-option
|
|
>
|
|
</j-select>
|
|
</template>
|
|
</template>
|
|
</j-table>
|
|
</j-scrollbar>
|
|
</div>
|
|
</div>
|
|
<div class="right">
|
|
<j-scrollbar>
|
|
<div class="title">功能说明</div>
|
|
<p>
|
|
该功能用于将协议包中的
|
|
<b>物模型属性标识</b>与
|
|
<b>平台物模型属性标识</b
|
|
>进行映射,当两方属性标识不一致时,可在当前页面直接修改映射管理,系统将以映射后的物模型属性进行数据处理。
|
|
</p>
|
|
<p>
|
|
未完成映射的属性标识“目标属性”列数据为空,代表该属性值来源以在平台配置的来源为准。
|
|
</p>
|
|
<p>
|
|
数据条背景亮起代表<b>标识一致</b>或<b>已完成映射</b>的属性。
|
|
</p>
|
|
<div class="title">功能图示</div>
|
|
<div>
|
|
<img :src="getImage('/device/matadataMap.jpg')" />
|
|
</div>
|
|
</j-scrollbar>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang='ts' name='MetadataMap'>
|
|
import { storeToRefs } from 'pinia';
|
|
import { getImage, onlyMessage } from '@/utils/comm';
|
|
import {
|
|
getMetadataMapById,
|
|
metadataMapById,
|
|
getProtocolMetadata,
|
|
} from '@/api/device/instance';
|
|
import { useInstanceStore } from '@/store/instance';
|
|
import { cloneDeep } from 'lodash-es';
|
|
|
|
const deviceStore = useInstanceStore();
|
|
const { current: deviceDetail } = storeToRefs(deviceStore);
|
|
const dataSourceCache = ref([]);
|
|
const dataSource = ref([]);
|
|
const targetOptions = ref<any[]>([]);
|
|
|
|
const filterValue = ref<boolean | undefined>(undefined);
|
|
const originalData = ref([]);
|
|
|
|
const _value = ref<any>(undefined);
|
|
const searchValue = ref<any>(undefined);
|
|
const _delTag = ref<boolean>(false);
|
|
|
|
const columns = [
|
|
{
|
|
title: '序号',
|
|
dataIndex: 'index',
|
|
width: 100,
|
|
},
|
|
{
|
|
title: '平台属性',
|
|
dataIndex: 'name',
|
|
},
|
|
{
|
|
title: '目标属性',
|
|
dataIndex: 'original',
|
|
width: 250,
|
|
},
|
|
];
|
|
|
|
const selectedKeys = computed(() => {
|
|
return (
|
|
dataSource.value
|
|
?.filter((item: any) => !!item?.original)
|
|
.map((i: any) => i.id) || []
|
|
);
|
|
});
|
|
|
|
const selectedOriginalKeys = computed(() => {
|
|
return (
|
|
dataSource.value
|
|
?.filter((item: any) => !!item?.original)
|
|
.map((i: any) => i.original) || []
|
|
);
|
|
});
|
|
|
|
const metadata = computed(() => {
|
|
return JSON.parse(deviceDetail.value?.metadata || '{}');
|
|
});
|
|
|
|
const _id = deviceDetail.value?.id;
|
|
|
|
const getMetadataMapData = () => {
|
|
return new Promise((resolve) => {
|
|
getMetadataMapById('device', _id).then((res: any) => {
|
|
if (res.success) {
|
|
resolve(
|
|
res.result
|
|
?.filter((i: any) => i.customMapping)
|
|
?.map((item: any) => {
|
|
return {
|
|
id: item.metadataId,
|
|
originalId: item.originalId,
|
|
customMapping: item.customMapping,
|
|
};
|
|
}) || [],
|
|
);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const customRow = (record: any) => {
|
|
return {
|
|
id: record.id,
|
|
class: _value.value === record?.name ? 'metadata-search-row' : '',
|
|
};
|
|
};
|
|
|
|
const onSearchChange = (_: any, options: any) => {
|
|
searchValue.value = options?.label;
|
|
};
|
|
|
|
const onSearch = () => {
|
|
if (searchValue.value) {
|
|
const _item: any = dataSourceCache.value.find((item: any) => {
|
|
return searchValue.value === item?.name;
|
|
});
|
|
if (_item) {
|
|
_value.value = _item?.name;
|
|
document.getElementById(_item?.id)?.scrollIntoView(); // 滚动到可视区域
|
|
}
|
|
} else {
|
|
_value.value = undefined;
|
|
}
|
|
};
|
|
|
|
const getDefaultMetadata = async () => {
|
|
const properties = metadata.value?.properties;
|
|
if (!deviceDetail.value?.protocol) {
|
|
return;
|
|
}
|
|
const _metadata = await getMetadata();
|
|
const _properties = _metadata?.properties || [];
|
|
const metadataMap: any = await getMetadataMapData();
|
|
|
|
targetOptions.value = _properties.map((item) => ({
|
|
...item,
|
|
label: item?.name,
|
|
value: item.id,
|
|
}));
|
|
|
|
const concatProperties = [...metadataMap];
|
|
|
|
if (!concatProperties.length) {
|
|
_delTag.value = true;
|
|
const arr = concatProperties.map((item) => item.id);
|
|
const _arr = concatProperties.map((item) => item.originalId);
|
|
|
|
_properties.map((item) => {
|
|
// 添加默认映射,但是该选项还没被其他属性映射
|
|
if (!arr.includes(item.id) && !_arr.includes(item.id)) {
|
|
concatProperties.push({
|
|
id: item.id,
|
|
originalId: item.id,
|
|
customMapping: item?.customMapping,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
dataSource.value =
|
|
properties?.map((item: any, index: number) => {
|
|
const _m = concatProperties.find((p) => p.id === item.id);
|
|
return {
|
|
index: index + 1,
|
|
id: item.id, // 产品物模型id
|
|
name: item?.name,
|
|
type: item.valueType?.type,
|
|
customMapping: _m?.customMapping,
|
|
original: _m?.originalId, // 协议包物模型id
|
|
};
|
|
}) || [];
|
|
dataSourceCache.value = dataSource.value;
|
|
};
|
|
|
|
const getMetadata = (): Promise<{ properties: any[] }> => {
|
|
return new Promise((resolve) => {
|
|
const transport = deviceDetail.value?.transport;
|
|
getProtocolMetadata(deviceDetail.value?.protocol || '', transport).then(
|
|
(res: any) => {
|
|
if (res.success) {
|
|
resolve(JSON.parse(res?.result || '{}'));
|
|
}
|
|
resolve({ properties: [] });
|
|
},
|
|
);
|
|
});
|
|
};
|
|
|
|
const onMapData = async (arr: any[], flag?: boolean) => {
|
|
const res = await metadataMapById('device', _id, arr);
|
|
if (res.success && flag) {
|
|
onlyMessage('操作成功');
|
|
}
|
|
};
|
|
|
|
const onChange = async (value: any, id: string) => {
|
|
if ((!id && value?.customMapping) || id) {
|
|
// 映射 / 取消映射
|
|
const arr = [
|
|
{
|
|
metadataType: 'property',
|
|
metadataId: value.id,
|
|
originalId: id,
|
|
},
|
|
];
|
|
onMapData(arr, true);
|
|
}
|
|
};
|
|
|
|
const onFilter = ({ key }: any) => {
|
|
originalData.value = dataSource.value;
|
|
const _dataSource = cloneDeep(dataSource.value).sort((a: any, b: any) => {
|
|
if (!key) {
|
|
return (a.original ? 1 : -1) - (b.original ? 1 : -1);
|
|
} else {
|
|
return (b.original ? 1 : -1) - (a.original ? 1 : -1);
|
|
}
|
|
});
|
|
|
|
dataSource.value = _dataSource;
|
|
filterValue.value = key;
|
|
};
|
|
|
|
const onClose = () => {
|
|
filterValue.value = undefined;
|
|
dataSource.value = originalData.value;
|
|
};
|
|
|
|
onMounted(() => {
|
|
getDefaultMetadata();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (_delTag.value) {
|
|
// 保存数据
|
|
const arr = dataSourceCache.value.filter((i: any) => i?.original).map((item: any) => {
|
|
return {
|
|
metadataType: 'property',
|
|
metadataId: item.id,
|
|
originalId: item.original,
|
|
}
|
|
})
|
|
onMapData(arr)
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang='less'>
|
|
.metadata-map {
|
|
min-height: 100%;
|
|
display: flex;
|
|
gap: 24px;
|
|
|
|
.left {
|
|
flex: 1;
|
|
height: 100%;
|
|
|
|
.box {
|
|
height: calc(100vh - 388px);
|
|
overflow: hidden;
|
|
|
|
:deep(.metadata-search-row) {
|
|
td {
|
|
background: #ffff80 !important;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.right {
|
|
// position: absolute;
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
min-height: 100%;
|
|
min-width: 410px;
|
|
width: 33.33333%;
|
|
// top: 0;
|
|
// right: 0;
|
|
padding: 16px;
|
|
|
|
.title {
|
|
margin-bottom: 16px;
|
|
color: rgba(#000, 0.85);
|
|
font-weight: bold;
|
|
|
|
p {
|
|
initial-letter: 28px;
|
|
color: #666666;
|
|
}
|
|
}
|
|
}
|
|
|
|
.metadata-title {
|
|
color: #666666;
|
|
}
|
|
|
|
:deep(.ant-table-selection-column) {
|
|
padding: 0;
|
|
label {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
</style> |