iot-ui-vue/src/views/device/components/Metadata/Import/index.vue

727 lines
27 KiB
Vue

<template>
<j-modal
:mask-closable="false"
title="导入物模型"
destroy-on-close
v-model:visible="_visible"
@cancel="close"
@ok="handleImport"
:confirm-loading="loading"
>
<div class="import-content">
<p class="import-tip">
<AIcon
type="ExclamationCircleOutlined"
style="margin-right: 5px"
/>
<template v-if="type === 'product'">
导入的物模型会覆盖原来的属性、功能、事件、标签,请谨慎操作。
</template>
<template v-else>
导入时会根据标识跳过继承自产品物模型的属性、功能、事件、标签。
</template>
</p>
</div>
<j-form layout="vertical" ref="formRef" :model="formModel">
<j-form-item
v-if="type === 'product'"
label="导入方式"
name="type"
:rules="[
{
required: true,
message: '请选择导入方式',
},
]"
>
<j-select v-model:value="formModel.type">
<j-select-option value="copy">拷贝产品</j-select-option>
<j-select-option value="import">导入物模型</j-select-option>
</j-select>
</j-form-item>
<j-form-item
label="选择产品"
:rules="[
{
required: true,
message: '请选择产品',
},
]"
name="copy"
v-if="formModel.type === 'copy'"
>
<j-select
:options="productList"
v-model:value="formModel.copy"
option-filter-prop="label"
placeholder="请选择产品"
showSearch
></j-select>
</j-form-item>
<j-form-item
label="物模型类型"
:rules="[
{
required: true,
message: '请选择物模型类型',
},
]"
name="metadata"
v-if="type === 'device' || formModel.type === 'import'"
>
<j-select v-model:value="formModel.metadata">
<j-select-option value="jetlinks"
>Jetlinks物模型</j-select-option
>
<j-select-option value="alink"
>阿里云物模型TSL</j-select-option
>
</j-select>
</j-form-item>
<j-form-item
label="导入类型"
:rules="[
{
required: true,
message: '请选择导入类型',
},
]"
name="metadataType"
v-if="type === 'device' || formModel.type === 'import'"
>
<j-select v-model:value="formModel.metadataType" @change="formModel.import = undefined">
<j-select-option value="file">文件上传</j-select-option>
<j-select-option value="script">脚本</j-select-option>
</j-select>
</j-form-item>
<j-form-item
v-if="
formModel.type === 'import' &&
formModel.metadataType === 'file'
"
label="文件上传"
name="import"
:rules="[
{
required: true,
message: '请上传文件',
},
]"
>
<!-- <j-input v-model:value="formModel.upload">
<template #addonAfter>
<j-upload
v-model:file-list="fileList"
:before-upload="beforeUpload"
accept=".json"
:show-upload-list="false"
:action="FILE_UPLOAD"
@change="fileChange"
:headers="{ 'X-Access-Token': getToken() }"
>
<AIcon
type="UploadOutlined"
class="upload-button"
/>
</j-upload>
</template>
</j-input> -->
<j-upload
v-model:file-list="fileList"
:before-upload="beforeUpload"
accept=".json"
:show-upload-list="false"
:action="FILE_UPLOAD"
@change="fileChange"
:headers="{ 'X-Access-Token': getToken() }"
>
<j-button>
<template #icon><AIcon type="UploadOutlined" /></template>
上传文件
</j-button>
</j-upload>
<div style="margin-left: 10px; color: rgba(0, 0, 0, .6);">支持扩展名:.json</div>
</j-form-item>
<j-form-item
:rules="[
{
required: true,
message: '请输入物模型',
},
]"
name="import"
v-if="
(type === 'device' || formModel.type === 'import') &&
formModel.metadataType === 'script'
"
>
<template #label>
<j-space>
物模型
<j-tooltip title="在线编辑器中编写物模型脚本">
<AIcon
type="QuestionCircleOutlined"
style="color: rgb(136, 136, 136)"
/>
</j-tooltip>
</j-space>
</template>
<JMonacoEditor
v-model:modelValue="formModel.import"
theme="vs"
style="height: 300px"
lang="json"
></JMonacoEditor>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang="ts" name="Import">
import { saveMetadata } from '@/api/device/instance';
import {
queryNoPagingPost,
convertMetadata,
modify,
} from '@/api/device/product';
import type { DefaultOptionType } from 'ant-design-vue/es/select';
import type {
UploadProps,
UploadFile,
UploadChangeParam,
} from 'ant-design-vue/es';
import type { DeviceMetadata } from '@/views/device/Product/typings';
import { useInstanceStore } from '@/store/instance';
import { useProductStore } from '@/store/product';
import { FILE_UPLOAD } from '@/api/comm';
import { getToken, onlyMessage } from '@/utils/comm';
import { useMetadataStore } from '@/store/metadata';
import { omit } from 'lodash-es';
import { Modal } from 'jetlinks-ui-components';
const route = useRoute();
const instanceStore = useInstanceStore();
const productStore = useProductStore();
interface Props {
visible: boolean;
type: 'device' | 'product';
}
interface Emits {
(e: 'update:visible', data: boolean): void;
(e: 'submit', data: any): void;
}
const props = defineProps<Props>();
const emits = defineEmits<Emits>();
const loading = ref(false);
const _visible = computed({
get: () => {
return props.visible;
},
set: (val: any) => {
emits('update:visible', val);
},
});
const close = () => {
emits('update:visible', false);
};
/** form表单 */
const formModel = reactive<Record<string, any>>({
type: 'import',
metadata: 'jetlinks',
metadataType: 'script',
});
// const { validate, validateInfos } = useForm(formModel, rules);
const fileList = ref<UploadFile[]>([]);
const hasVirtualRule = ref(false);
const formRef = ref();
const productList = ref<DefaultOptionType[]>([]);
const loadData = async () => {
const { id } = route.params || {};
const product = (await queryNoPagingPost({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [{ column: 'id$not', value: id }],
})) as any;
productList.value = product.result
.filter((i: any) => i?.metadata)
.map((item: any) => ({
label: item.name,
value: item.metadata,
key: item.id,
})) as DefaultOptionType[];
};
loadData();
// const propertiesSet = new Set(['id','name','expands','valueType']);
// const handleMadeDataNull = (data:any) =>{
// return data?.properties?.some?.((item:any,index:number)=>{
// if(!item?.id){
// onlyMessage(`属性定义第${index + 1}个数组中缺失id属性`,'error');
// return true
// }
// if(!item?.name){
// onlyMessage(`属性定义第${index + 1}个数组中缺失name属性`,'error');
// return
// }
// if(!item?.expands?.source){
// onlyMessage(`属性定义第${index + 1}个数组中缺失expands.source属性`,'error');
// return
// }
// if((item?.expands?.source === 'device' || item?.expands?.source === 'rule') && !item?.expands?.type){
// onlyMessage(`属性定义第${index + 1}个数组中缺失type属性`,'error');
// return
// }
// }) || false
// }
const requiredCheck = (data:any) =>{
let check:boolean = false;
if(data?.properties && !check){
data.properties.some((item:any,index:number)=>{
if(!item?.id){
onlyMessage(`属性定义第${index + 1}个数组中缺失id属性`,'error');
check = true
return
}
if(!item?.name){
onlyMessage(`属性定义第${index + 1}个数组中缺失name属性`,'error');
check = true
return
}
if(!item?.valueType?.type){
onlyMessage(`标签定义第${index + 1}个数组中缺失valueType.type属性`,'error');
check = true
return
}
if(!item?.expands?.source){
onlyMessage(`属性定义第${index + 1}个数组中缺失expands.source属性`,'error');
check = true
return
}
if((item?.expands?.source === 'device' || item?.expands?.source === 'rule') && !item?.expands?.type){
onlyMessage(`属性定义第${index + 1}个数组中缺失type属性`,'error');
check = true
return
}
})
}
if(data?.functions && !check){
data?.functions.forEach((item:any,index:number)=>{
if(!item?.id){
onlyMessage(`方法定义第${index + 1}个数组中缺失id属性`,'error');
check = true
return
}
if(!item?.name){
onlyMessage(`方法定义第${index + 1}个数组中缺失name属性`,'error');
check = true
return
}
if(!item?.async && item?.async !== false){
onlyMessage(`方法定义第${index + 1}个数组中缺失async属性`,'error');
check = true
return
}
})
}
if(data?.events && !check){
data?.events.forEach((item:any,index:number)=>{
if(!item?.id){
onlyMessage(`事件定义第${index + 1}个数组中缺失id属性`,'error');
check = true
return
}
if(!item?.name){
onlyMessage(`事件定义第${index + 1}个数组中缺失name属性`,'error');
check = true
return
}
if(!item?.async && item?.async !== false){
onlyMessage(`事件定义第${index + 1}个数组中缺失async属性`,'error');
check = true
return
}
if(!item?.valueType?.type){
onlyMessage(`事件定义第${index + 1}个数组中缺失valueType.type属性`,'error');
check = true
return
}
if(!item?.expands?.level){
onlyMessage(`事件定义第${index + 1}个数组中缺失expands.level属性`,'error');
check = true
return
}
if(!check){
if(item?.valueType?.properties){
item?.valueType?.properties.forEach((i:any,number:number)=>{
if(!i?.id){
onlyMessage(`事件定义第${index + 1}个数组中缺失valueType.properties数组第${number+1}项的id属性`,'error');
check = true
return
}
if(!i?.name){
onlyMessage(`事件定义第${index + 1}个数组中缺失valueType.properties数组第${number+1}项的name属性`,'error');
check = true
return
}
if(!i?.valueType?.type){
onlyMessage(`事件定义第${index + 1}个数组中缺失valueType.properties数组第${number+1}项的valueType.type属性`,'error');
check = true
return
}
})
}else{
onlyMessage(`事件定义第${index + 1}个数组中缺失valueType.properties数组`,'error');
check = true
return
}
}
})
}
if(data?.tags && !check){
data?.tags.forEach((item:any,index:number)=>{
if(!item?.id){
onlyMessage(`标签定义第${index + 1}个数组中缺失id属性`,'error');
check = true
return
}
if(!item?.name){
onlyMessage(`标签定义第${index + 1}个数组中缺失name属性`,'error');
check = true
return
}
if(!item?.valueType?.type){
onlyMessage(`标签定义第${index + 1}个数组中缺失valueType.type属性`,'error');
check = true
return
}
if(!item?.expands?.type){
onlyMessage(`标签定义第${index + 1}个数组中缺失expands.type属性`,'error');
check = true
return
}
})
}
return check
}
const aliCheck = (data:any) => {
let check:boolean = false;
if(data?.properties && !check){
data.properties.some((item:any,index:number)=>{
if(!item?.identifier){
onlyMessage(`属性定义第${index + 1}个数组中缺失identifier属性`,'error');
check = true
return
}
if(!item?.name){
onlyMessage(`属性定义第${index + 1}个数组中缺失name属性`,'error');
check = true
return
}
if(!item?.dataType?.type){
onlyMessage(`属性定义第${index + 1}个数组中缺失dataType.type属性`,'error');
check = true
return
}
})
}
if(data?.functions && !check){
data?.functions.forEach((item:any,index:number)=>{
if(!item?.identifier){
onlyMessage(`方法定义第${index + 1}个数组中缺失identifier属性`,'error');
check = true
return
}
if(!item?.name){
onlyMessage(`方法定义第${index + 1}个数组中缺失name属性`,'error');
check = true
return
}
if(!item?.callType){
onlyMessage(`方法定义第${index + 1}个数组中缺失callType属性`,'error');
check = true
return
}
})
}
if(data?.events && !check){
data?.events.forEach((item:any,index:number)=>{
if(!item?.identifier){
onlyMessage(`事件定义第${index + 1}个数组中缺失identifier属性`,'error');
check = true
return
}
if(!item?.name){
onlyMessage(`事件定义第${index + 1}个数组中缺失name属性`,'error');
check = true
return
}
if(!item?.type){
onlyMessage(`事件定义第${index + 1}个数组中缺失type属性`,'error');
check = true
return
}
if(!check){
if(item?.outputData){
item?.outputData?.forEach((i:any,number:number)=>{
if(!i?.identifier){
onlyMessage(`事件定义第${index + 1}个数组中缺失outputData数组第${number+1}项的id属性`,'error');
check = true
return
}
if(!i?.name){
onlyMessage(`事件定义第${index + 1}个数组中缺失outputData数组第${number+1}项的name属性`,'error');
check = true
return
}
if(!i?.dataType?.type){
onlyMessage(`事件定义第${index + 1}个数组中缺失outputData数组第${number+1}项的dataType.type属性`,'error');
check = true
return
}
if(!i?.dataType?.specs){
onlyMessage(`事件定义第${index + 1}个数组中缺失outputData数组第${number+1}项的dataType.specs属性`,'error');
check = true
return
}
})
}else{
onlyMessage(`事件定义第${index + 1}个数组中缺失outputData数组`,'error');
check = true
return
}
}
})
}
return check
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
if(file.type === 'application/json') {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = (json) => {
if(json.target?.result){
const data = JSON.parse(json.target?.result);
let check = formModel.metadata === 'jetlinks' ? requiredCheck(data) : aliCheck(data)
if(!check){
onlyMessage('操作成功!')
formModel.import = json.target?.result;
}
} else {
onlyMessage('文件内容不能为空', 'error')
}
};
} else {
onlyMessage('请上传json格式的文件', 'error')
}
};
const fileChange = (info: UploadChangeParam) => {
if (info.file.status === 'done') {
const { response } = info.file;
if (response.status === 200) {
formModel.upload = response.result;
}
}
};
const uniqArray = (arr: any[]) => {
const _map = new Map();
for(let item of arr) {
_map.set(item.id, item)
}
return [..._map.values()]
}
const operateLimits = (mdata: DeviceMetadata) => {
hasVirtualRule.value = false;
const obj: DeviceMetadata = { ...mdata };
const old = JSON.parse((props.type === 'device' ? instanceStore.detail?.metadata : productStore.detail?.metadata) || '{}');
const fid = instanceStore.detail?.features?.map((item) => item.id);
const _data: DeviceMetadata = {
properties: [],
events: [],
functions: [],
tags: []
}
_data.properties = uniqArray([...(old?.properties || []), ...uniqArray(obj?.properties || [])])
_data.events = uniqArray([...(old?.events || []), ...uniqArray(obj?.events || [])])
_data.functions = uniqArray([...(old?.functions || []), ...uniqArray(obj?.functions || [])])
_data.tags = uniqArray([...(old?.tags || []), ...uniqArray(obj?.tags || [])])
if (fid?.includes('eventNotModifiable')) {
_data.events = old?.events || [];
}
if (fid?.includes('propertyNotModifiable')) {
_data.properties = old?.properties || [];
}
(_data?.properties || []).map((item) => {
if (item.expands?.source === 'rule') {
hasVirtualRule.value = true;
item.expands = omit(item.expands, ['virtualRule']);
}
return item
});
return _data;
};
const metadataStore = useMetadataStore();
const handleImport = async () => {
formRef.value.validate().then(async (data: any) => {
const { id } = route.params || {};
if (data.metadata === 'alink') {
try {
const _import = JSON.parse(data.import);
Object.keys(_import).forEach((i:any)=>{
const map = new Map()
_import[i].forEach((item:any)=>(
map.set(item.id,item)
))
_import[i] = [...map.values()]
})
loading.value = true;
const res = await convertMetadata(
'from',
'alink',
_import,
).catch((err) => err);
if (res.status === 200) {
// const metadata = operateLimits(res.result); // 导入取并集逻辑
const metadata = res.result
let result;
if (props?.type === 'device') {
result = await saveMetadata(
id as string,
metadata,
).catch((err) => err);
} else {
result = await modify(id as string, {
id,
metadata: JSON.stringify(metadata),
}).catch((err) => err);
}
if (result.success) {
onlyMessage('导入成功');
}
loading.value = false;
} else {
loading.value = false;
return;
}
if (props?.type === 'device') {
await instanceStore.refresh(id as string);
} else {
await productStore.getDetail(id as string);
}
metadataStore.set('importMetadata', true);
close();
} catch (e) {
onlyMessage(
e === 'error'
? '物模型数据不正确'
: '上传json格式的物模型文件',
'error',
);
}
} else {
try {
const _object = JSON.parse(
data[data?.type === 'copy' ? 'copy' : 'import'] ||
'{}',
);
if(data?.type !== 'copy'){
Object.keys(_object).forEach((i:any)=>{
const map = new Map()
_object[i].forEach((item:any)=>(
map.set(item.id,item)
))
_object[i] = [...map.values()]
})
}
if (
!(
!!_object?.properties ||
!!_object?.events ||
!!_object?.functions ||
!!_object?.tags
)
) {
onlyMessage('物模型数据不正确', 'error');
loading.value = false;
return;
}
const { id } = route.params || {};
// const copyOperateLimits = operateLimits(
// _object as DeviceMetadata,
// );
// console.log(copyOperateLimits,_object); // 导入取并集逻辑
const params = {
id,
metadata: JSON.stringify(_object),
};
const paramsDevice = _object;
let resp = undefined;
loading.value = true;
if (props?.type === 'device') {
resp = await saveMetadata(id as string, paramsDevice);
} else {
resp = await modify(id as string, params);
}
loading.value = false;
if (resp.success) {
onlyMessage('导入成功');
if (hasVirtualRule.value) {
setTimeout(() => {
Modal.info({
title: '导入数据存在虚拟属性,请及时添加虚拟属性计算规则。',
okText: '确认',
});
}, 300);
}
}
if (props?.type === 'device') {
await instanceStore.refresh(id as string);
} else {
await productStore.getDetail(id as string);
}
metadataStore.set('importMetadata', true);
close();
} catch (e) {
loading.value = false;
onlyMessage(
e === 'error'
? '物模型数据不正确'
: '上传json格式的物模型文件',
'error',
);
}
}
});
};
// const showProduct = computed(() => formModel.type === 'copy')
</script>
<style scoped lang="less">
.import-content {
background: rgb(236, 237, 238);
.import-tip {
padding: 10px;
}
}
.upload-button {
width: 37px;
height: 30px;
line-height: 30px;
margin: 0 -11px;
}
</style>