385 lines
18 KiB
Vue
385 lines
18 KiB
Vue
<template>
|
|
<page-container>
|
|
<a-card>
|
|
<a-row :gutter="24">
|
|
<a-col :span="16">
|
|
<TitleComponent data="基本信息" />
|
|
<a-form
|
|
:layout="'vertical'"
|
|
ref="formRef"
|
|
:model="modelRef"
|
|
>
|
|
<a-row :gutter="24">
|
|
<a-col :span="24">
|
|
<a-form-item label="名称" name="name" :rules=" [
|
|
{
|
|
required: true,
|
|
message: '请输入名称',
|
|
},
|
|
{
|
|
max: 64,
|
|
message: '最多输入64个字符',
|
|
},
|
|
]">
|
|
<a-input placeholder="请输入名称" v-model:value="modelRef.name" />
|
|
</a-form-item>
|
|
</a-col>
|
|
<a-col :span="12">
|
|
<a-form-item label="产品" name="id" :rules="[{
|
|
required: true,
|
|
message: '请选择产品',
|
|
}]">
|
|
<a-select :disabled="modelRef.id !== ':id'" placeholder="请选择产品" v-model:value="modelRef.id" show-search :filter-option="filterOption" @change="productChange">
|
|
<a-select-option v-for="item in productList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
</a-col>
|
|
<a-col :span="12">
|
|
<a-form-item name="applianceType" :rules="{
|
|
required: true,
|
|
message: '请选择设备类型',
|
|
}">
|
|
<template #label>
|
|
<span>
|
|
设备类型
|
|
<a-tooltip title="DuerOS平台拟定的规范">
|
|
<AIcon
|
|
type="QuestionCircleOutlined"
|
|
style="margin-left: 2px;" />
|
|
</a-tooltip>
|
|
</span>
|
|
</template>
|
|
<a-select placeholder="请选择设备类型" v-model:value="modelRef.applianceType" show-search :filter-option="filterOption" @change="typeChange">
|
|
<a-select-option v-for="item in typeList" :key="item.id" :value="item.id" :label="item.name">{{item.name}}</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
<a-form-item name="productName" v-show="false" label="产品名称">
|
|
<a-input v-model:value="modelRef.productName" />
|
|
</a-form-item>
|
|
</a-col>
|
|
<a-col :span="24">
|
|
<p>动作映射</p>
|
|
<a-collapse v-if="modelRef.actionMappings.length" :activeKey="modelRef.actionMappings.map((_, _index) => _index)">
|
|
<a-collapse-panel v-for="(item, index) in modelRef.actionMappings" :key="index" :header="item.action ? getTypesActions(item.action).find(i => i.id === item.action)?.name : `动作映射${index + 1}`">
|
|
<template #extra><AIcon type="DeleteOutlined" @click="delItem(index)" /></template>
|
|
<a-row :gutter="24">
|
|
<a-col :span="12">
|
|
<a-form-item :name="['actionMappings', index, 'action']" :rules="{
|
|
required: true,
|
|
message: '请选择动作',
|
|
}">
|
|
<template #label>
|
|
<span>
|
|
动作
|
|
<a-tooltip title="DuerOS平台拟定的设备类型具有的相关动作">
|
|
<AIcon type="QuestionCircleOutlined" />
|
|
</a-tooltip>
|
|
</span>
|
|
</template>
|
|
<a-select placeholder="请选择动作" v-model:value="item.action" show-search :filter-option="filterOption">
|
|
<a-select-option v-for="i in getTypesActions(item.action)" :key="i.id" :value="i.id" :label="i.name">{{i.name}}</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
</a-col>
|
|
<a-col :span="12">
|
|
<a-form-item :name="['actionMappings', index, 'actionType']" :rules="{
|
|
required: true,
|
|
message: '请选择操作',
|
|
}">
|
|
<template #label>
|
|
<span>
|
|
操作
|
|
<a-tooltip title="映射物联网平台中所选产品具备的动作">
|
|
<AIcon type="QuestionCircleOutlined" />
|
|
</a-tooltip>
|
|
</span>
|
|
</template>
|
|
<a-select placeholder="请选择操作" v-model:value="item.actionType" show-search :filter-option="filterOption">
|
|
<a-select-option value="command">下发指令</a-select-option>
|
|
<a-select-option value="latestData">获取历史数据</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
</a-col>
|
|
<a-col :span="24" v-if="item.actionType">
|
|
<a-form-item :name="['actionMappings', index, 'command']">
|
|
<Command ref="command" :metadata="findProductMetadata" v-model:modelValue="item.command" :actionType="item.actionType" />
|
|
</a-form-item>
|
|
</a-col>
|
|
</a-row>
|
|
</a-collapse-panel>
|
|
</a-collapse>
|
|
</a-col>
|
|
<a-col :span="24">
|
|
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addItem">
|
|
<AIcon
|
|
type="PlusOutlined"
|
|
style="margin-left: 2px;" />新增动作
|
|
</a-button>
|
|
</a-col>
|
|
<a-col :span="24">
|
|
<p style="margin-top: 20px">属性映射</p>
|
|
<a-collapse v-if="modelRef.propertyMappings.length" :activeKey="modelRef.propertyMappings.map((_, _index) => _index)">
|
|
<a-collapse-panel v-for="(item, index) in modelRef.propertyMappings" :key="index" :header="item.source ? getDuerOSProperties(item.source).find(i => i.id === item.source)?.name : `属性映射${index + 1}`">
|
|
<template #extra><AIcon type="DeleteOutlined" @click="delPropertyItem(index)" /></template>
|
|
<a-row :gutter="24">
|
|
<a-col :span="12">
|
|
<a-form-item label="DuerOS属性" :name="['propertyMappings', index, 'source']" :rules="{
|
|
required: true,
|
|
message: '请选择DuerOS属性',
|
|
}">
|
|
<a-select placeholder="请选择DuerOS属性" v-model:value="item.source" show-search :filter-option="filterOption">
|
|
<a-select-option v-for="i in getDuerOSProperties(item.source)" :key="i.id" :value="i.id">{{i.name}}</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
</a-col>
|
|
<a-col :span="12">
|
|
<a-form-item label="平台属性" :name="['propertyMappings', index, 'target']" :rules="{
|
|
required: true,
|
|
message: '请选择平台属性',
|
|
}">
|
|
<a-select placeholder="请选择平台属性" v-model:value="item.target" mode="tags" show-search :filter-option="filterOption">
|
|
<a-select-option v-for="i in getProductProperties(item.target)" :key="i.id" :value="item.id">{{i.name}}</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
</a-col>
|
|
</a-row>
|
|
</a-collapse-panel>
|
|
</a-collapse>
|
|
</a-col>
|
|
<a-col :span="24">
|
|
<a-button type="dashed" style="width: 100%; margin-top: 10px" @click="addPropertyItem">
|
|
<AIcon
|
|
type="PlusOutlined"
|
|
style="margin-left: 2px;" />新增属性
|
|
</a-button>
|
|
</a-col>
|
|
<a-col :span="24" style="margin-top: 20px">
|
|
<a-form-item label="说明" name="description" :rules="{
|
|
max: 200,
|
|
message: '最多输入200个字符',
|
|
}">
|
|
<a-textarea
|
|
v-model:value="modelRef.description"
|
|
placeholder="请输入说明"
|
|
showCount
|
|
:maxlength="200"
|
|
/>
|
|
</a-form-item>
|
|
</a-col>
|
|
</a-row>
|
|
</a-form>
|
|
<div v-if="type === 'edit'">
|
|
<a-button :loading="loading" type="primary" @click="saveBtn">保存</a-button>
|
|
</div>
|
|
</a-col>
|
|
<a-col :span="8">
|
|
<Doc />
|
|
</a-col>
|
|
</a-row>
|
|
</a-card>
|
|
</page-container>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import Doc from './doc.vue'
|
|
import Command from './command/index.vue'
|
|
import { queryProductList, queryTypes, savePatch, detail } from '@/api/northbound/dueros'
|
|
import _ from 'lodash';
|
|
import { message } from 'ant-design-vue';
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
const formRef = ref();
|
|
|
|
const modelRef = reactive({
|
|
id: undefined,
|
|
name: undefined,
|
|
applianceType: undefined,
|
|
productName: undefined,
|
|
actionMappings: [{
|
|
actionType: undefined,
|
|
action: undefined,
|
|
command: {
|
|
messageType: undefined,
|
|
message: {
|
|
properties: undefined,
|
|
functionId: undefined,
|
|
inputs: []
|
|
}
|
|
}
|
|
}],
|
|
propertyMappings: [{
|
|
source: undefined,
|
|
target: []
|
|
}],
|
|
description: undefined
|
|
});
|
|
|
|
const addItem = () => {
|
|
modelRef.actionMappings.push({
|
|
actionType: undefined,
|
|
action: undefined,
|
|
command: {
|
|
messageType: undefined,
|
|
message: {
|
|
properties: undefined,
|
|
functionId: undefined,
|
|
inputs: []
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const productList = ref<Record<string, any>[]>([])
|
|
const typeList = ref<Record<string, any>[]>([])
|
|
const command = ref([])
|
|
const loading = ref<boolean>(false)
|
|
const type = ref<'edit' | 'view'>('view')
|
|
|
|
const filterOption = (input: string, option: any) => {
|
|
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
|
};
|
|
|
|
const delItem = (index: number) => {
|
|
modelRef.actionMappings.splice(index, 1)
|
|
}
|
|
|
|
const addPropertyItem = () => {
|
|
modelRef.propertyMappings.push({
|
|
source: undefined,
|
|
target: []
|
|
})
|
|
}
|
|
|
|
const delPropertyItem = (index: number) => {
|
|
modelRef.propertyMappings.splice(index, 1)
|
|
}
|
|
|
|
const productChange = (value: string) => {
|
|
modelRef.propertyMappings = modelRef.propertyMappings.map(item => {
|
|
return {source: item.source, target: []}
|
|
})
|
|
const item = productList.value.find(item => item.id === value)
|
|
if(item){
|
|
modelRef.productName = item.name
|
|
}
|
|
}
|
|
|
|
const typeChange = () => {
|
|
modelRef.propertyMappings = modelRef.propertyMappings.map(item => {
|
|
return {source: undefined, target: item.target}
|
|
})
|
|
modelRef.actionMappings = modelRef.actionMappings.map(item => {
|
|
return {...item, action: undefined}
|
|
})
|
|
}
|
|
|
|
const findApplianceType = computed(() => {
|
|
if(!modelRef.applianceType) return
|
|
return typeList.value.find(item => item.id === modelRef.applianceType)
|
|
})
|
|
|
|
const findProductMetadata = computed(() => {
|
|
if(!modelRef.id) return
|
|
const _product = productList.value?.find((item: any) => item.id === modelRef.id)
|
|
return _product?.metadata && JSON.parse(_product.metadata || '{}')
|
|
})
|
|
|
|
// 查询产品列表
|
|
const getProduct = async (id?: string) => {
|
|
const resp = await queryProductList(id)
|
|
if(resp.status === 200){
|
|
productList.value = (resp?.result as Record<string, any>[])
|
|
}
|
|
}
|
|
|
|
const getTypes = async () => {
|
|
const resp = await queryTypes()
|
|
if(resp.status === 200){
|
|
typeList.value = (resp?.result as Record<string, any>[])
|
|
}
|
|
}
|
|
|
|
const getDuerOSProperties = (val: string) => {
|
|
const arr = modelRef.propertyMappings.map(item => item?.source) || []
|
|
const checked = _.cloneDeep(arr)
|
|
const _index = checked.findIndex(i => i === val)
|
|
// 去掉重复的
|
|
checked.splice(_index, 1)
|
|
const targetList = findApplianceType.value?.properties;
|
|
const list = targetList?.filter((i: {id: string}) => !checked.includes(i?.id as any))
|
|
return list || []
|
|
}
|
|
|
|
const getProductProperties = (val: string[]) => {
|
|
const items = modelRef.propertyMappings.map((item: {target: string[]}) => item?.target.map(j => j)) || []
|
|
const checked = _.flatMap(items)
|
|
const _checked: any[] = []
|
|
checked.map(_item => {
|
|
if(!val.includes(_item)){
|
|
_checked.push(_item)
|
|
}
|
|
})
|
|
const sourceList = findProductMetadata.value?.properties
|
|
const list = sourceList?.filter((i: { id: string }) => !_checked.includes(i.id))
|
|
return list || []
|
|
}
|
|
|
|
const getTypesActions = (val: string) => {
|
|
const items = modelRef.actionMappings.map((item) => item?.action) || []
|
|
const checked = _.cloneDeep(items)
|
|
const _index = checked.findIndex(i => i === val)
|
|
checked.splice(_index, 1)
|
|
const actionsList = findApplianceType.value?.actions || []
|
|
const list = actionsList?.filter((i: { id: string, name: string }) => !checked.includes(i?.id as any))
|
|
return list || []
|
|
}
|
|
const saveBtn = async () => {
|
|
const tasks = []
|
|
for(let i = 0; i < command.value.length; i++){
|
|
const res = await (command.value[i] as any)?.saveBtn()
|
|
tasks.push(res)
|
|
if(!res) break
|
|
}
|
|
const data = await formRef.value.validate()
|
|
if(tasks.every(item => item) && data){
|
|
loading.value = true;
|
|
const resp = await savePatch(toRaw(modelRef));
|
|
loading.value = false;
|
|
if (resp.status === 200) {
|
|
message.success('操作成功!');
|
|
formRef.value.resetFields();
|
|
router.push('/iot/northbound/DuerOS/');
|
|
}
|
|
}
|
|
|
|
}
|
|
watch(
|
|
() => route.params?.id,
|
|
async (newId) => {
|
|
if(newId){
|
|
getProduct(newId as string)
|
|
getTypes()
|
|
if (newId === ':id') return;
|
|
const resp = await detail(newId as string)
|
|
const _data: any = resp.result;
|
|
if (_data) {
|
|
_data.applianceType = _data?.applianceType?.value;
|
|
}
|
|
Object.assign(modelRef, _data)
|
|
}
|
|
},
|
|
{immediate: true, deep: true}
|
|
);
|
|
|
|
watch(
|
|
() => route.query.type,
|
|
(newVal) => {
|
|
if(newVal){
|
|
type.value = newVal as 'edit' | 'view'
|
|
}
|
|
},
|
|
{immediate: true, deep: true}
|
|
);
|
|
</script> |