feat: 完善插件管理
4
build.sh
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:1.0.0 .
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:1.0.0
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1 .
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1
|
||||
|
|
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
|
@ -573,4 +573,17 @@ export const queryLogsType = () => server.get(`/dictionary/device-log-type/items
|
|||
|
||||
export const getDeviceNumber = (data?:any) => server.post<number>('/device-instance/_count', data)
|
||||
|
||||
/**
|
||||
* 导入映射设备
|
||||
* @param productId
|
||||
* @param data
|
||||
*/
|
||||
export const importDeviceByPlugin = (productId: string, data: any[]) => server.post(`/device/instance/plugin/${productId}/import`, data)
|
||||
|
||||
export const metadateMapById = (productId: string, data: ant[]) => server.patch(`/device/metadata/mapping/product/${productId}`, data)
|
||||
|
||||
export const getMetadateMapById = (productId: string) => server.get(`/device/metadata/mapping/product/${productId}`)
|
||||
|
||||
export const getInkingDevices = (data: string[]) => server.post('/plugin/mapping/device/_all', data)
|
||||
|
||||
|
||||
|
|
|
@ -63,11 +63,12 @@ export const put = function <T>(url: string, data = {}) {
|
|||
* @param {Object} [data]
|
||||
* @returns {AxiosInstance}
|
||||
*/
|
||||
export const patch = function <T>(url: string, data = {}) {
|
||||
export const patch = function <T>(url: string, data = {}, ext: any = {}) {
|
||||
return request<any, AxiosResponseRewrite<T>>({
|
||||
method: 'PATCH',
|
||||
url,
|
||||
data
|
||||
data,
|
||||
...ext
|
||||
})
|
||||
}
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Inkling from './index.vue'
|
||||
|
||||
export default Inkling
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<j-modal
|
||||
:width="800"
|
||||
:mask-closable="false"
|
||||
:visible="true"
|
||||
title="设备ID映射"
|
||||
:confirmLoading="loading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<InklingDevice
|
||||
v-model:value='checkKey'
|
||||
:accessId='accessId'
|
||||
/>
|
||||
</j-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='InklingModal'>
|
||||
import InklingDevice from '@/views/device/components/InklingDevice'
|
||||
import { onlyMessage } from '@/utils/comm'
|
||||
import { savePluginData } from '@/api/link/plugin'
|
||||
|
||||
type Emit = {
|
||||
(e: 'cancel'): void
|
||||
(e: 'submit', data: string): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
accessId: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
channelId: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
const checkKey = ref(props.id)
|
||||
const loading = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
const handleOk = async () => {
|
||||
if (checkKey.value) {
|
||||
const res = await savePluginData(
|
||||
'device',
|
||||
props.channelId!,
|
||||
route.params.id as string,
|
||||
checkKey.value
|
||||
).catch(() => ({ success: false }))
|
||||
if (res.success) {
|
||||
emit('submit', checkKey.value)
|
||||
}
|
||||
} else {
|
||||
onlyMessage('请选择设备', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -14,12 +14,36 @@
|
|||
<j-descriptions-item label="设备ID">{{
|
||||
instanceStore.current?.id
|
||||
}}</j-descriptions-item>
|
||||
<j-descriptions-item v-if='instanceStore.current?.accessProvider === "plugin_gateway"'>
|
||||
<template #label>
|
||||
<div>
|
||||
第三方系统设备ID
|
||||
<j-tooltip>
|
||||
<template #title>
|
||||
<p>通过调用SDK或HTTP请求的方式接入第三方系统设备数据时,第三方系统与平台当前设备对应的设备ID。</p>
|
||||
如双方ID值一致,则无需填写
|
||||
</template>
|
||||
<a-icon type='QuestionCircleOutlined' />
|
||||
</j-tooltip>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<j-button v-if='!inklingDeviceId' type="link" @click='giveAnInkling'>映射</j-button>
|
||||
<div v-else style='display: flex;justify-content: space-between;align-items: center;'>
|
||||
<div style='flex: 1 1 auto;'>
|
||||
<j-ellipsis>{{ inklingDeviceId }}</j-ellipsis>
|
||||
</div>
|
||||
<j-button type='link'>
|
||||
<a-icon
|
||||
type='EditOutlined'
|
||||
@click='inkingVisible = true'
|
||||
/>
|
||||
</j-button>
|
||||
</div>
|
||||
</j-descriptions-item>
|
||||
<j-descriptions-item label="产品名称">{{
|
||||
instanceStore.current?.productName
|
||||
}}</j-descriptions-item>
|
||||
<!-- <j-descriptions-item label="产品分类">{{-->
|
||||
<!-- instanceStore.current?.classifiedName-->
|
||||
<!-- }}</j-descriptions-item>-->
|
||||
<j-descriptions-item label="设备类型">{{
|
||||
instanceStore.current?.deviceType?.text
|
||||
}}</j-descriptions-item>
|
||||
|
@ -83,6 +107,14 @@
|
|||
@close="visible = false"
|
||||
@save="saveBtn"
|
||||
/>
|
||||
<InkingModal
|
||||
v-if='inkingVisible'
|
||||
:id='inklingDeviceId'
|
||||
:channelId='channelId'
|
||||
:accessId='instanceStore.current.accessId'
|
||||
@cancel="inkingVisible = false"
|
||||
@submit='saveInkling'
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -91,10 +123,16 @@ import Save from '../../Save/index.vue';
|
|||
import Config from './components/Config/index.vue';
|
||||
import Tags from './components/Tags/index.vue';
|
||||
import Relation from './components/Relation/index.vue';
|
||||
import InkingModal from './components/InklingModal'
|
||||
import moment from 'moment';
|
||||
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig'
|
||||
import { getPluginData } from '@/api/link/plugin'
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const inkingVisible = ref<boolean>(false);
|
||||
const instanceStore = useInstanceStore();
|
||||
const inklingDeviceId = ref()
|
||||
const channelId = ref()
|
||||
|
||||
const saveBtn = () => {
|
||||
if (instanceStore.current?.id) {
|
||||
|
@ -102,4 +140,37 @@ const saveBtn = () => {
|
|||
}
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const saveInkling = (id: string) => {
|
||||
if (instanceStore.current?.id) {
|
||||
instanceStore.refresh(instanceStore.current?.id);
|
||||
}
|
||||
channelId.value = id
|
||||
giveAnInkling()
|
||||
}
|
||||
|
||||
const giveAnInkling = () => {
|
||||
inkingVisible.value = true
|
||||
}
|
||||
|
||||
const queryInkling = () => {
|
||||
if (instanceStore.current?.accessProvider === 'plugin_gateway') {
|
||||
queryPluginAccessDetail(instanceStore.current?.accessId).then(async res => {
|
||||
if (res.success) {
|
||||
channelId.value = res.result.channelId
|
||||
const pluginRes = await getPluginData('device', res.result.channelId, instanceStore.current?.id)
|
||||
if (pluginRes.success) {
|
||||
inklingDeviceId.value = pluginRes.result?.externalId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => instanceStore.current?.id, () => {
|
||||
if (instanceStore.current?.id) {
|
||||
queryInkling()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<div class='file'>
|
||||
<j-form layout='vertical'>
|
||||
<j-form-item label='文件格式' >
|
||||
<div class='file-type-label'>
|
||||
<a-radio-group class='file-type-radio' v-model:value="modelRef.file.fileType" >
|
||||
<a-radio-button value="xlsx">xlsx</a-radio-button>
|
||||
<a-radio-button value="csv">csv</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-checkbox v-model:checked="modelRef.file.autoDeploy">自动启用</a-checkbox>
|
||||
</div>
|
||||
</j-form-item>
|
||||
<j-form-item label="文件上传">
|
||||
<j-upload
|
||||
v-model:fileList="modelRef.upload"
|
||||
name="file"
|
||||
:action="FILE_UPLOAD"
|
||||
:headers="{
|
||||
'X-Access-Token': LocalStore.get(TOKEN_KEY),
|
||||
}"
|
||||
:maxCount="1"
|
||||
:showUploadList="false"
|
||||
@change="uploadChange"
|
||||
:accept="
|
||||
modelRef?.file?.fileType ? `.${modelRef?.file?.fileType}` : '.xlsx'
|
||||
"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<j-button style='width: 760px;'>
|
||||
<template #icon><AIcon type="UploadOutlined" /></template>
|
||||
上传文件
|
||||
</j-button>
|
||||
</j-upload>
|
||||
</j-form-item>
|
||||
<j-form-item label='下载模板'>
|
||||
<div class='file-download'>
|
||||
<j-button @click="downFile('xlsx')">.xlsx</j-button>
|
||||
<j-button @click="downFile('csv')">.csv</j-button>
|
||||
</div>
|
||||
</j-form-item>
|
||||
</j-form>
|
||||
<div v-if="importLoading">
|
||||
<j-badge v-if="flag" status="processing" text="进行中" />
|
||||
<j-badge v-else status="success" text="已完成" />
|
||||
<span>总数量:{{ count }}</span>
|
||||
<p style="color: red">{{ errMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='DeviceImportFile'>
|
||||
import { FILE_UPLOAD } from '@/api/comm';
|
||||
import { TOKEN_KEY } from '@/utils/variable';
|
||||
import { LocalStore, onlyMessage } from '@/utils/comm';
|
||||
import { downloadFileByUrl } from '@/utils/utils';
|
||||
import {
|
||||
deviceImport,
|
||||
templateDownload,
|
||||
} from '@/api/device/instance';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { message } from 'jetlinks-ui-components'
|
||||
|
||||
const props = defineProps({
|
||||
product: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const modelRef = reactive({
|
||||
product: props.product,
|
||||
upload: [],
|
||||
file: {
|
||||
fileType: 'xlsx',
|
||||
autoDeploy: false,
|
||||
},
|
||||
});
|
||||
|
||||
const importLoading = ref<boolean>(false);
|
||||
const flag = ref<boolean>(false);
|
||||
const count = ref<number>(0);
|
||||
const errMessage = ref<string>('');
|
||||
|
||||
const downFile = async (type: string) => {
|
||||
const res: any = await templateDownload(props.product!, type);
|
||||
if (res) {
|
||||
const blob = new Blob([res], { type: type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
downloadFileByUrl(url, `设备导入模板`, type);
|
||||
}
|
||||
};
|
||||
|
||||
const beforeUpload = (_file: any) => {
|
||||
const fileType = modelRef.file?.fileType === 'csv' ? 'csv' : 'xlsx';
|
||||
const isCsv = _file.type === 'text/csv';
|
||||
const isXlsx = _file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
if (!isCsv && fileType !== 'xlsx') {
|
||||
onlyMessage('请上传.csv格式文件', 'warning');
|
||||
}
|
||||
if (!isXlsx && fileType !== 'csv') {
|
||||
onlyMessage('请上传.xlsx格式文件', 'warning');
|
||||
}
|
||||
return (isCsv && fileType !== 'xlsx') || (isXlsx && fileType !== 'csv');
|
||||
};
|
||||
|
||||
const submitData = async (fileUrl: string) => {
|
||||
if (!!fileUrl) {
|
||||
count.value = 0;
|
||||
errMessage.value = '';
|
||||
flag.value = true;
|
||||
const autoDeploy = !!modelRef?.file?.autoDeploy || false;
|
||||
importLoading.value = true;
|
||||
let dt = 0;
|
||||
const source = new EventSourcePolyfill(
|
||||
deviceImport(props.product!, fileUrl, autoDeploy),
|
||||
);
|
||||
source.onmessage = (e: any) => {
|
||||
const res = JSON.parse(e.data);
|
||||
if (res.success) {
|
||||
const temp = res.result.total;
|
||||
dt += temp;
|
||||
count.value = dt;
|
||||
} else {
|
||||
errMessage.value = res.message || '失败';
|
||||
}
|
||||
};
|
||||
source.onerror = (e: { status: number }) => {
|
||||
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员';
|
||||
flag.value = false;
|
||||
source.close();
|
||||
};
|
||||
source.onopen = () => {};
|
||||
} else {
|
||||
message.error('请先上传文件');
|
||||
}
|
||||
};
|
||||
|
||||
const uploadChange = async (info: Record<string, any>) => {
|
||||
if (info.file.status === 'done') {
|
||||
const resp: any = info.file.response || { result: '' };
|
||||
await submitData(resp?.result || '');
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.file {
|
||||
.file-type-label {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.file-type-radio {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
:deep(.ant-radio-button-wrapper) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-download {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
>button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,17 +1,39 @@
|
|||
<template>
|
||||
<j-modal
|
||||
:maskClosable="false"
|
||||
:visible="true"
|
||||
:visible="visible"
|
||||
width="800px"
|
||||
title="导入"
|
||||
title="批量导入"
|
||||
@cancel='cancel'
|
||||
>
|
||||
<div>
|
||||
<!-- 选择产品 -->
|
||||
<div v-if='steps === 0'>
|
||||
<Product
|
||||
v-model:rowKey='importData.productId'
|
||||
@change='productChange'
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if='steps === 1'>
|
||||
<j-form :layout="'vertical'">
|
||||
<j-form-item required label='选择导入方式'>
|
||||
<j-card-select
|
||||
:value="[importData.type]"
|
||||
:column='typeOptions.length'
|
||||
:options="typeOptions"
|
||||
@change='typeChange'
|
||||
>
|
||||
<template #image='{image}'>
|
||||
<img :src='image' />
|
||||
</template>
|
||||
</j-card-select>
|
||||
</j-form-item>
|
||||
</j-form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<File v-if='importData.type ==="file"' :product='importData.productId' />
|
||||
<Plugin v-else :accessId='productDetail.accessId' @change='pluginChange'/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<j-button v-if='steps === 0' @click='cancel' >取消</j-button>
|
||||
|
@ -20,26 +42,79 @@
|
|||
<j-button v-if='steps === 2' @click='save' type='primary'>确认</j-button>
|
||||
</template>
|
||||
</j-modal>
|
||||
<j-modal
|
||||
:maskClosable="false"
|
||||
:visible="importVisible"
|
||||
width="400px"
|
||||
title="导入完成"
|
||||
@cancel='importCancel'
|
||||
@ok='importCancel'
|
||||
>
|
||||
<a-icon type='CheckOutlined' style='color: #2F54EB;' /> 已完成 新增设备 <span style='color: #2F54EB;'>{{count}}</span>
|
||||
</j-modal>
|
||||
</template>
|
||||
|
||||
<script lang='ts' setup name='DeviceImport'>
|
||||
import Product from './product.vue'
|
||||
import { onlyMessage } from '@/utils/comm'
|
||||
|
||||
import { queryList } from '@/api/device/product';
|
||||
import { getImage, onlyMessage } from '@/utils/comm'
|
||||
import File from './file.vue'
|
||||
import Plugin from './plugin.vue'
|
||||
import { importDeviceByPlugin } from '@/api/device/instance'
|
||||
|
||||
const emit = defineEmits(['cancel', 'save']);
|
||||
|
||||
const steps = ref(0) // 步骤
|
||||
|
||||
const importData = reactive({
|
||||
const importData = reactive<{productId?: string, type?: string}>({
|
||||
productId: undefined,
|
||||
type: undefined,
|
||||
})
|
||||
|
||||
const productDetail = ref()
|
||||
const deviceList = ref<any[]>([])
|
||||
const visible = ref(true)
|
||||
const importVisible = ref(false)
|
||||
const count = ref(0)
|
||||
|
||||
const typeOptions = computed(() => {
|
||||
const array = [
|
||||
{
|
||||
value: 'file',
|
||||
label: '文件导入',
|
||||
subLabel: '支持上传XLSX、CSV格式文件',
|
||||
iconUrl: getImage('/device/import1.png'),
|
||||
},
|
||||
]
|
||||
if (productDetail.value?.accessProvider === 'plugin_gateway') {
|
||||
array.push({
|
||||
value: 'plugin',
|
||||
label: '插件导入',
|
||||
subLabel: '读取插件中的设备信息同步至平台',
|
||||
iconUrl: getImage('/device/import2.png'),
|
||||
})
|
||||
}
|
||||
return array
|
||||
})
|
||||
|
||||
const typeChange = (types: string[]) => {
|
||||
importData.type = types[0]
|
||||
}
|
||||
|
||||
const productChange = (detail: any) => {
|
||||
productDetail.value = detail
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
if (steps.value === 0 && !importData.productId) {
|
||||
return onlyMessage('请选择产品', 'error')
|
||||
if (steps.value === 0) {
|
||||
if (!importData.productId) {
|
||||
return onlyMessage('请选择产品', 'error')
|
||||
}
|
||||
if (productDetail.value?.accessProvider !== 'plugin_gateway') {
|
||||
importData.type = 'file'
|
||||
importData.productId = productDetail.value?.id
|
||||
steps.value = 2
|
||||
return
|
||||
}
|
||||
}
|
||||
if (steps.value === 1 && !importData.type) {
|
||||
return onlyMessage('请选择导入方式', 'error')
|
||||
|
@ -48,7 +123,7 @@ const next = () => {
|
|||
}
|
||||
|
||||
const prev = () => {
|
||||
if (steps.value === 2 && importData.type) {
|
||||
if (productDetail.value?.accessProvider !== 'plugin_gateway') {
|
||||
steps.value = 0
|
||||
} else {
|
||||
steps.value -= 1
|
||||
|
@ -59,7 +134,33 @@ const cancel = () => {
|
|||
emit('cancel')
|
||||
}
|
||||
|
||||
const pluginChange = (options: any[]) => {
|
||||
deviceList.value = options
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
if (importData.type === 'file') {
|
||||
cancel()
|
||||
emit('save')
|
||||
} else {
|
||||
if (deviceList.value.length) {
|
||||
importDeviceByPlugin(importData.productId!, deviceList.value).then(res => {
|
||||
if (res.success) {
|
||||
onlyMessage('操作成功')
|
||||
// cancel()
|
||||
visible.value = false
|
||||
importVisible.value = true
|
||||
count.value = res.result?.[0]?.result?.updated
|
||||
}
|
||||
})
|
||||
} else {
|
||||
onlyMessage('请选择设备', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importCancel = () => {
|
||||
importVisible.value = false
|
||||
emit('save')
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div style=''>
|
||||
<InklingDevice
|
||||
:accessId='accessId'
|
||||
:multiple='true'
|
||||
@change='change'
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='DeviceImportPlugin'>
|
||||
import InklingDevice from '@/views/device/components/InklingDevice'
|
||||
|
||||
type Emit = {
|
||||
(e: 'change', data: any[]): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
accessId: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const change = (options: any[]) => {
|
||||
emit('change', options)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
:deep(.device-import-product) {
|
||||
margin-bottom: 0;
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
|
@ -3,56 +3,72 @@
|
|||
:columns="columns"
|
||||
type='simple'
|
||||
@search="handleSearch"
|
||||
class="scene-search"
|
||||
class="device-import-product"
|
||||
target="device-import-product"
|
||||
/>
|
||||
<j-divider style='margin: 0' />
|
||||
<j-pro-table
|
||||
model='CARD'
|
||||
:columns='columns'
|
||||
:params='params'
|
||||
:request='productQuery'
|
||||
:gridColumn='2'
|
||||
:gridColumns='[2,2,2]'
|
||||
:bodyStyle='{
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0
|
||||
}'
|
||||
>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value='slotProps'
|
||||
:active="rowKey === slotProps.id"
|
||||
:status="slotProps.state"
|
||||
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
|
||||
:statusNames="{ 1: 'processing', 0: 'error', }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img width='80' height='80' :src="slotProps.photoUrl || getImage('/device-product.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<div style='width: calc(100% - 100px)'>
|
||||
<Ellipsis>
|
||||
<span style="font-size: 16px;font-weight: 600" >
|
||||
{{ slotProps.name }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
</div>
|
||||
<j-row>
|
||||
<j-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
设备类型
|
||||
</div>
|
||||
<div>直连设备</div>
|
||||
</j-col>
|
||||
</j-row>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
</j-pro-table>
|
||||
<j-scrollbar :height='400'>
|
||||
<j-pro-table
|
||||
model='CARD'
|
||||
:columns='columns'
|
||||
:params='params'
|
||||
:request='queryProductList'
|
||||
:gridColumn='2'
|
||||
:defaultParams="{
|
||||
terms: [
|
||||
{
|
||||
column: 'state',
|
||||
value: '1',
|
||||
type: 'and'
|
||||
},
|
||||
{
|
||||
column: 'accessProvider',
|
||||
value: props?.type
|
||||
}
|
||||
],
|
||||
sorts: [{ name: 'createTime', order: 'desc' }]
|
||||
}"
|
||||
:gridColumns='[2,2,2]'
|
||||
:bodyStyle='{
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0
|
||||
}'
|
||||
>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value='slotProps'
|
||||
:active="rowKey === slotProps.id"
|
||||
:status="slotProps.state"
|
||||
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
|
||||
:statusNames="{ 1: 'processing', 0: 'error', }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img width='80' height='80' :src="slotProps.photoUrl || getImage('/device-product.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<div style='width: calc(100% - 100px)'>
|
||||
<Ellipsis>
|
||||
<span style="font-size: 16px;font-weight: 600" >
|
||||
{{ slotProps.name }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
</div>
|
||||
<j-row>
|
||||
<j-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
设备类型
|
||||
</div>
|
||||
<div>直连设备</div>
|
||||
</j-col>
|
||||
</j-row>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
</j-pro-table>
|
||||
</j-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='Product'>
|
||||
|
@ -112,32 +128,18 @@ const columns = [
|
|||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '接入方式',
|
||||
dataIndex: 'accessName',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () => queryGatewayList().then((resp: any) =>
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.name, value: item.id
|
||||
}))
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
dataIndex: 'deviceType',
|
||||
width: 150,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '直连设备', value: 'device' },
|
||||
{ label: '网关子设备', value: 'childrenDevice' },
|
||||
{ label: '网关设备', value: 'gateway' },
|
||||
]
|
||||
}
|
||||
// search: {
|
||||
// type: 'select',
|
||||
// options: [
|
||||
// { label: '直连设备', value: 'device' },
|
||||
// { label: '网关子设备', value: 'childrenDevice' },
|
||||
// { label: '网关设备', value: 'gateway' },
|
||||
// ]
|
||||
// }
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
|
@ -156,88 +158,16 @@ const columns = [
|
|||
dataIndex: 'describe',
|
||||
ellipsis: true,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataIndex: 'classifiedId',
|
||||
title: '分类',
|
||||
hideInTable: true,
|
||||
search: {
|
||||
type: 'treeSelect',
|
||||
options: () => {
|
||||
return new Promise((res => {
|
||||
queryTree({ paging: false }).then(resp => {
|
||||
res(resp.result)
|
||||
})
|
||||
}))
|
||||
},
|
||||
componentProps: {
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
}
|
||||
}
|
||||
type: 'string',
|
||||
}
|
||||
},
|
||||
{
|
||||
dataIndex: 'id$dim-assets',
|
||||
title: '所属组织',
|
||||
hideInTable: true,
|
||||
search: {
|
||||
type: 'treeSelect',
|
||||
options: () => new Promise((resolve) => {
|
||||
getTreeData_api({ paging: false }).then((resp: any) => {
|
||||
const formatValue = (list: any[]) => {
|
||||
return list.map((item: any) => {
|
||||
if (item.children) {
|
||||
item.children = formatValue(item.children);
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
value: JSON.stringify({
|
||||
assetType: 'product',
|
||||
targets: [
|
||||
{
|
||||
type: 'org',
|
||||
id: item.id,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
resolve(formatValue(resp.result) || [])
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const handleSearch = (p: any) => {
|
||||
params.value = p
|
||||
}
|
||||
|
||||
const productQuery = async (p: any) => {
|
||||
const sorts: any = [];
|
||||
|
||||
if (props.rowKey) {
|
||||
sorts.push({
|
||||
name: 'id',
|
||||
value: props.rowKey,
|
||||
});
|
||||
}
|
||||
sorts.push({ name: 'createTime', order: 'desc' });
|
||||
p.sorts = sorts
|
||||
const resp = await queryProductList(p)
|
||||
if (resp.success && props.rowKey && firstFind.value) {
|
||||
const productItem = (resp.result as { data: any[]}).data.find((item: any) => item.id === props.rowKey)
|
||||
emit('update:detail', productItem)
|
||||
firstFind.value = false
|
||||
}
|
||||
return {
|
||||
...resp
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (detail: any) => {
|
||||
emit('update:rowKey', detail.id)
|
||||
emit('change', detail)
|
||||
|
@ -246,7 +176,7 @@ const handleClick = (detail: any) => {
|
|||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.search {
|
||||
:deep(.device-import-product) {
|
||||
margin-bottom: 0;
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
|
|
|
@ -271,7 +271,7 @@
|
|||
</page-container>
|
||||
<Import
|
||||
v-if="importVisible"
|
||||
@close="importVisible = false"
|
||||
@cancel="importVisible = false"
|
||||
@save="onRefresh"
|
||||
/>
|
||||
<Export
|
||||
|
@ -308,7 +308,7 @@ import {
|
|||
} from '@/api/device/instance';
|
||||
import { getImage, LocalStore } from '@/utils/comm';
|
||||
import { message } from 'jetlinks-ui-components';
|
||||
import Import from './Import/index.vue';
|
||||
import Import from './Import/modal.vue';
|
||||
import Export from './Export/index.vue';
|
||||
import Process from './Process/index.vue';
|
||||
import Save from './Save/index.vue';
|
||||
|
|
|
@ -129,6 +129,7 @@ import { getImage } from '@/utils/comm';
|
|||
import { queryList, getAccessConfig } from '@/api/device/product'
|
||||
import { message } from 'jetlinks-ui-components'
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { getProductByPluginId } from '@/api/link/plugin'
|
||||
|
||||
type Emit = {
|
||||
(e: 'submit', data: any): void
|
||||
|
@ -255,16 +256,25 @@ const findProvidersByProvider = (provider: string) => {
|
|||
*/
|
||||
const submitData = async () => {
|
||||
if (selectedRowKeys.value.length) {
|
||||
loading.value= true
|
||||
const resp = await getAccessConfig(props.productId!, checkData.value.id).catch(() => ({ success: false, result: {}}))
|
||||
// 返回外部组件需要的数据
|
||||
loading.value = false
|
||||
if (resp.success) {
|
||||
// const providers = findProvidersByProvider((resp.result as any)[0]?.provider)
|
||||
if (checkData.value.channel === 'plugin') {
|
||||
const resp = await getProductByPluginId(checkData.value.channelId).catch(() => ({ success: false, result: []}))
|
||||
|
||||
emit('submit', {
|
||||
access: {...checkData.value},
|
||||
metadata: resp.result
|
||||
productTypes: resp.result
|
||||
})
|
||||
} else {
|
||||
loading.value= true
|
||||
const resp = await getAccessConfig(props.productId!, checkData.value.id).catch(() => ({ success: false, result: {}}))
|
||||
// 返回外部组件需要的数据
|
||||
loading.value = false
|
||||
if (resp.success) {
|
||||
// const providers = findProvidersByProvider((resp.result as any)[0]?.provider)
|
||||
emit('submit', {
|
||||
access: {...checkData.value},
|
||||
metadata: resp.result
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error('请选择接入方式');
|
||||
|
|
|
@ -73,6 +73,19 @@
|
|||
</div>
|
||||
<div v-else>{{ '暂无连接信息' }}</div>
|
||||
</div>
|
||||
<!-- 产品类型 -->
|
||||
<j-form ref="pluginFormRef" :model="productData" layout="vertical" v-if='productTypes.length'>
|
||||
<j-form-item name='id' label='产品类型' :rules='[{ required: true, message: "请选择产品类型"}]'>
|
||||
<j-select
|
||||
v-model:value='productData.id'
|
||||
:options='productTypes'
|
||||
@change='productTypeChange'
|
||||
placeholder='请选择产品类型'
|
||||
/>
|
||||
</j-form-item>
|
||||
|
||||
</j-form>
|
||||
<!-- 其它接入配置 -->
|
||||
<Title
|
||||
v-if="metadata?.name"
|
||||
:data="metadata?.name"
|
||||
|
@ -160,6 +173,7 @@
|
|||
type="primary"
|
||||
@click="submitDevice"
|
||||
hasPermission="device/Instance:update"
|
||||
:loading='submitLoading'
|
||||
>保存</PermissionButton
|
||||
>
|
||||
</j-col>
|
||||
|
@ -246,6 +260,15 @@
|
|||
@cancel=' visible = false'
|
||||
@submit='checkAccess'
|
||||
/>
|
||||
<!-- 物模型处理方式 -->
|
||||
<MetaDataModal
|
||||
v-if='metadataVisible'
|
||||
:metadata='productData.metadata'
|
||||
:access='access'
|
||||
:data='metadataModalCacheData'
|
||||
@cancel=' () => { metadataVisible = false, metadataModalCacheData = {}}'
|
||||
@submit='MetaDataModalSubmit'
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name='AccessConfig'>
|
||||
|
@ -257,20 +280,20 @@ import { usePermissionStore } from '@/store/permission';
|
|||
import { steps, steps1 } from './util';
|
||||
import './index.less';
|
||||
import {
|
||||
getProviders,
|
||||
_deploy,
|
||||
_undeploy,
|
||||
queryList,
|
||||
getConfigView,
|
||||
getConfigMetadata,
|
||||
productGuide,
|
||||
productGuideSave,
|
||||
getStoragList,
|
||||
saveDevice,
|
||||
updateDevice,
|
||||
detail,
|
||||
modify,
|
||||
} from '@/api/device/product';
|
||||
getProviders,
|
||||
_deploy,
|
||||
_undeploy,
|
||||
queryList,
|
||||
getConfigView,
|
||||
getConfigMetadata,
|
||||
productGuide,
|
||||
productGuideSave,
|
||||
getStoragList,
|
||||
saveDevice,
|
||||
updateDevice,
|
||||
detail,
|
||||
modify, getAccessConfig
|
||||
} from '@/api/device/product'
|
||||
|
||||
import Driver from 'driver.js';
|
||||
import 'driver.js/dist/driver.min.css';
|
||||
|
@ -280,6 +303,9 @@ import { useMenuStore } from '@/store/menu';
|
|||
import _ from 'lodash';
|
||||
import { accessConfigTypeFilter } from '@/utils/setting';
|
||||
import AccessModal from './accessModal.vue'
|
||||
import MetaDataModal from './metadataModal.vue'
|
||||
import { getPluginData, getProductByPluginId, savePluginData } from '@/api/link/plugin'
|
||||
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig'
|
||||
|
||||
const productStore = useProductStore();
|
||||
const tableRef = ref();
|
||||
|
@ -319,6 +345,17 @@ const form = reactive<Record<string, any>>({
|
|||
const formData = reactive<Record<string, any>>({
|
||||
data: productStore.current?.configuration || {},
|
||||
});
|
||||
// 产品类型
|
||||
const productTypes = ref([])
|
||||
const productData = reactive({
|
||||
id: undefined,
|
||||
metadata: {} // 物模型
|
||||
})
|
||||
const pluginFormRef = ref()
|
||||
const metadataVisible = ref(false)
|
||||
const metadataModalCacheData = ref()
|
||||
|
||||
const submitLoading = ref(false)
|
||||
/**
|
||||
* 显示弹窗
|
||||
*/
|
||||
|
@ -571,11 +608,27 @@ const checkAccess = async (data: any) => {
|
|||
visible.value = false
|
||||
accessId.value = data.access.id
|
||||
access.value = data.access
|
||||
metadata.value = data.metadata[0]
|
||||
config.value = data.access?.transportDetail || {}
|
||||
handleColumns()
|
||||
markdownToHtml.value = config.value?.document ? marked(config.value.document) : '';
|
||||
getGuide(!!data.metadata.length); //
|
||||
productTypes.value = []
|
||||
productData.id = undefined
|
||||
productData.metadata = {}
|
||||
if (data.access.channel === 'plugin') { // 插件设备
|
||||
markdownToHtml.value = ''
|
||||
productTypes.value = data.productTypes.map(item => ({ ...item, label: item.name, value: item.id}))
|
||||
} else {
|
||||
metadata.value = data.metadata[0]
|
||||
handleColumns()
|
||||
markdownToHtml.value = config.value?.document ? marked(config.value.document) : '';
|
||||
getGuide(!!data.metadata.length); //
|
||||
|
||||
if (data.access?.transportDetail?.metadata) {
|
||||
productData.metadata = JSON.parse(data.access?.transportDetail?.metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const productTypeChange = (id: string, items: any) => {
|
||||
productData.metadata = items?.metadata || {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -623,20 +676,32 @@ const getData = async (accessId?: string) => {
|
|||
// if (metadataResp.success) {
|
||||
// metadata.value = (metadataResp.result?.[0] as ConfigMetadata[]) || [];
|
||||
// }
|
||||
queryAccessDetail(_accessId);
|
||||
queryAccessDetail(_accessId);
|
||||
if (productStore.current?.accessProvider === 'plugin_gateway') {
|
||||
queryPluginAccessDetail(_accessId).then(async res => { //
|
||||
if (res.success) {
|
||||
const pluginRes = await getPluginData('product', res.result.channelId, productStore.current?.id)
|
||||
const resp = await getProductByPluginId(res.result.channelId).catch(() => ({ success: false, result: []}))
|
||||
if (resp.success) {
|
||||
productTypes.value = resp.result.map(item => {
|
||||
if (pluginRes?.result?.externalId === item.id) {
|
||||
productData.id = pluginRes?.result?.externalId
|
||||
productData.metadata = JSON.stringify(item.metadata || {})
|
||||
}
|
||||
return { ...item, label: item.name, value: item.id }
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
getConfigDetail(
|
||||
productStore.current?.messageProtocol || '',
|
||||
productStore.current?.transportProtocol || '',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
// else {
|
||||
// if (productStore.current?.id) {
|
||||
// getConfigMetadata(productStore.current?.id).then((resp: any) => {
|
||||
// metadata.value = resp?.result[0] as ConfigMetadata[];
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
getStoragList().then((resp: any) => {
|
||||
if (resp.status === 200) {
|
||||
storageList.value = resp.result;
|
||||
|
@ -648,47 +713,80 @@ const getData = async (accessId?: string) => {
|
|||
* 保存设备接入
|
||||
*/
|
||||
const submitDevice = async () => {
|
||||
const res = await formRef.value.validate();
|
||||
const values = { storePolicy: form.storePolicy, ...formData.data };
|
||||
const result: any = {};
|
||||
flatObj(values, result);
|
||||
const { storePolicy, ...extra } = result;
|
||||
const id = productStore.current?.id;
|
||||
//TODO 二次确认是否覆盖物模型
|
||||
// 更新选择设备(设备接入)
|
||||
const accessObj = {
|
||||
...productStore.current,
|
||||
transportProtocol: access.value?.transport,
|
||||
protocolName: access.value?.protocolDetail?.name,
|
||||
accessId: access.value?.id,
|
||||
accessName: access.value?.name,
|
||||
accessProvider: access.value?.provider,
|
||||
messageProtocol: access.value?.protocol,
|
||||
if (pluginFormRef.value) { // 插件
|
||||
const pluginRef = await pluginFormRef.value.validate();
|
||||
if (!pluginRef) return
|
||||
}
|
||||
const updateDeviceResp = await updateDevice(accessObj)
|
||||
|
||||
if (!updateDeviceResp.success) return
|
||||
|
||||
// 更新产品配置信息
|
||||
const resp = await modify(id || '', {
|
||||
id: id,
|
||||
configuration: { ...extra },
|
||||
storePolicy: storePolicy,
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
productStore.current!.storePolicy = storePolicy;
|
||||
if ((window as any).onTabSaveSuccess) {
|
||||
if (resp.result) {
|
||||
(window as any).onTabSaveSuccess(resp);
|
||||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
} else {
|
||||
getDetailInfo();
|
||||
}
|
||||
const res = await formRef.value.validate();
|
||||
if (!res) return
|
||||
const values = { storePolicy: form.storePolicy, ...formData.data };
|
||||
const id = productStore.current?.id;
|
||||
// 该产品是否有物模型,有则弹窗进行处理
|
||||
const _metadata = JSON.parse(productStore.current?.metadata || '{}')
|
||||
if (_metadata.properties?.length || _metadata.events?.length || _metadata.functions?.length || _metadata.tags?.length) {
|
||||
metadataModalCacheData.value = {
|
||||
id,
|
||||
values,
|
||||
productTypeId: productData.id
|
||||
}
|
||||
metadataVisible.value = true
|
||||
} else {
|
||||
updateAccessData(id, values)
|
||||
}
|
||||
};
|
||||
|
||||
const updateAccessData = async (id: string, values: any) => {
|
||||
const result: any = {};
|
||||
flatObj(values, result);
|
||||
const { storePolicy, ...extra } = result;
|
||||
// 更新选择设备(设备接入)
|
||||
const accessObj = {
|
||||
...productStore.current,
|
||||
metadata: JSON.stringify(productData.metadata || "{}"),
|
||||
transportProtocol: access.value?.transport,
|
||||
protocolName: access.value?.protocolDetail?.name,
|
||||
accessId: access.value?.id,
|
||||
accessName: access.value?.name,
|
||||
accessProvider: access.value?.provider,
|
||||
messageProtocol: access.value?.protocol,
|
||||
}
|
||||
submitLoading.value = true
|
||||
const updateDeviceResp = await updateDevice(accessObj).catch(() => { success: false})
|
||||
|
||||
if (!updateDeviceResp.success) {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
if (access.value?.provider === "plugin_gateway") {
|
||||
await savePluginData(
|
||||
'product',
|
||||
access.value?.channelId,
|
||||
productStore.current.id,
|
||||
productData.id
|
||||
).catch(() => ({}))
|
||||
}
|
||||
// 更新产品配置信息
|
||||
const resp = await modify(id || '', {
|
||||
id: id,
|
||||
configuration: { ...extra },
|
||||
storePolicy: storePolicy,
|
||||
});
|
||||
submitLoading.value = false
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
productStore.current!.storePolicy = storePolicy;
|
||||
if ((window as any).onTabSaveSuccess) {
|
||||
if (resp.result) {
|
||||
(window as any).onTabSaveSuccess(resp);
|
||||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
} else {
|
||||
getDetailInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flatObj = (obj: any, result: any) => {
|
||||
Object.keys(obj).forEach((key: string) => {
|
||||
if (typeof obj[key] === 'string') {
|
||||
|
@ -699,8 +797,15 @@ const flatObj = (obj: any, result: any) => {
|
|||
});
|
||||
};
|
||||
|
||||
const getDetailInfo = () => {};
|
||||
const getDetailInfo = async () => {
|
||||
await productStore.getDetail(productStore.detail.id)
|
||||
MetaDataModalSubmit()
|
||||
};
|
||||
|
||||
const MetaDataModalSubmit = () => {
|
||||
// 跳转物模型标签
|
||||
productStore.tabActiveKey = 'Metadata'
|
||||
}
|
||||
|
||||
getProvidersList()
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
<template>
|
||||
<j-modal
|
||||
title="选择处理方式"
|
||||
visible
|
||||
width="900px"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
:confirmLoading='loading'
|
||||
@ok="submitData"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<div class='tip'>
|
||||
<a-icon type='ExclamationCircleOutlined'/>
|
||||
平台
|
||||
<span style='font-weight: bold;padding:0 4px;'>物模型</span>
|
||||
中已有数据,请选择处理方式。
|
||||
<j-tooltip title='默认采用覆盖的方式处理功能、事件、标签下的数据'>
|
||||
<a-icon type='QuestionCircleOutlined' />
|
||||
</j-tooltip>
|
||||
</div>
|
||||
<j-form :layout="'vertical'" ref='formRef' :model='handleData'>
|
||||
<j-form-item label='处理方式' :rules='[{ required: true, message: "请选择处理方式"}]' >
|
||||
<j-card-select
|
||||
v-model:value="handleData.type"
|
||||
:column='4'
|
||||
:options="options"
|
||||
>
|
||||
<template #image='{image}'>
|
||||
<img :src='image' />
|
||||
</template>
|
||||
</j-card-select>
|
||||
</j-form-item>
|
||||
</j-form>
|
||||
</j-modal>
|
||||
</template>
|
||||
|
||||
<script lang='ts' setup name='MetadataModal'>
|
||||
import { useProductStore } from '@/store/product';
|
||||
import { getImage } from '@/utils/comm'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { modify, updateDevice } from '@/api/device/product'
|
||||
import { message } from 'jetlinks-ui-components'
|
||||
import { savePluginData } from '@/api/link/plugin'
|
||||
|
||||
type Emit = {
|
||||
(e: 'submit'): void
|
||||
(e: 'cancel'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const props = defineProps({
|
||||
metadata: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
access: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const productStore = useProductStore();
|
||||
const { current: productDetail } = storeToRefs(productStore)
|
||||
const formRef = ref()
|
||||
const handleData = reactive({
|
||||
type: undefined
|
||||
})
|
||||
const loading = ref(false)
|
||||
const options = [
|
||||
{
|
||||
value: 'intersection',
|
||||
label: '取交集',
|
||||
subLabel: '仅保留标识一致的属性',
|
||||
iconUrl: getImage('/device/intersection.png'),
|
||||
},
|
||||
{
|
||||
value: 'union',
|
||||
label: '取并集',
|
||||
subLabel: '保留平台、插件中的所有属性',
|
||||
iconUrl: getImage('/device/union.png'),
|
||||
},
|
||||
{
|
||||
value: 'ignore',
|
||||
label: '忽略',
|
||||
subLabel: '仅保留平台中的属性',
|
||||
iconUrl: getImage('/device/ignore.png'),
|
||||
},
|
||||
{
|
||||
value: 'cover',
|
||||
label: '覆盖',
|
||||
subLabel: '仅保留插件中的属性',
|
||||
iconUrl: getImage('/device/cover.png'),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const flatObj = (obj: any, result: any) => {
|
||||
Object.keys(obj).forEach((key: string) => {
|
||||
if (typeof obj[key] === 'string') {
|
||||
result[key] = obj[key];
|
||||
} else {
|
||||
flatObj(obj[key], result);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const updateAccessData = async (id: string, values: any, metadata: string) => {
|
||||
const result: any = {};
|
||||
flatObj(values, result);
|
||||
const { storePolicy, ...extra } = result;
|
||||
// 更新选择设备(设备接入)
|
||||
const accessObj = {
|
||||
...productDetail.value,
|
||||
metadata: JSON.stringify(metadata),
|
||||
transportProtocol: props.access?.transport,
|
||||
protocolName: props.access?.protocolDetail?.name,
|
||||
accessId: props.access?.id,
|
||||
accessName: props.access?.name,
|
||||
accessProvider: props.access?.provider,
|
||||
messageProtocol: props.access?.protocol,
|
||||
}
|
||||
loading.value = true
|
||||
const updateDeviceResp = await updateDevice(accessObj)
|
||||
|
||||
if (!updateDeviceResp.success) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (props.access?.provider === 'plugin_gateway') {
|
||||
await savePluginData(
|
||||
'product',
|
||||
props.access.channelId,
|
||||
props.data.id,
|
||||
props.data.productTypeId
|
||||
).catch(() => ({}))
|
||||
}
|
||||
|
||||
// 更新产品配置信息
|
||||
const resp = await modify(id || '', {
|
||||
id: id,
|
||||
configuration: { ...extra },
|
||||
storePolicy: storePolicy,
|
||||
});
|
||||
loading.value = false
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
productStore.current!.storePolicy = storePolicy;
|
||||
if ((window as any).onTabSaveSuccess) {
|
||||
if (resp.result) {
|
||||
(window as any).onTabSaveSuccess(resp);
|
||||
setTimeout(() => window.close(), 300);
|
||||
}
|
||||
} else {
|
||||
await productStore.getDetail(productDetail.value.id)
|
||||
emit('submit')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const submitData = () => {
|
||||
formRef.value.validate().then((res) => {
|
||||
if (res) {
|
||||
let metadata = JSON.parse(productDetail.value?.metadata || '{}') // 产品物模型
|
||||
switch (handleData.type![0]) {
|
||||
case 'intersection': // 交集
|
||||
metadata.properties = IntersectionFn(metadata.properties, props.metadata.properties)
|
||||
metadata.events = IntersectionFn(metadata.events, props.metadata.events)
|
||||
metadata.functions = IntersectionFn(metadata.functions, props.metadata.functions)
|
||||
metadata.tags = IntersectionFn(metadata.tags, props.metadata.tags)
|
||||
break;
|
||||
case 'union': // 并集
|
||||
metadata.properties = UnionFn(metadata.properties, props.metadata.properties)
|
||||
metadata.functions = UnionFn(metadata.functions, props.metadata.functions)
|
||||
metadata.events = UnionFn(metadata.events, props.metadata.events)
|
||||
metadata.tags = UnionFn(metadata.tags, props.metadata.tags)
|
||||
break;
|
||||
case 'cover': // 覆盖
|
||||
metadata = props.metadata
|
||||
break;
|
||||
default:
|
||||
break
|
||||
}
|
||||
updateAccessData(
|
||||
props.data.id,
|
||||
props.data.values,
|
||||
metadata
|
||||
)
|
||||
}
|
||||
}).catch(() => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
/**
|
||||
* 交集处理函数, 只保留来自插件中的属性
|
||||
* @param DataA 产品物模型
|
||||
* @param DataB 插件物模型
|
||||
* @constructor
|
||||
*/
|
||||
const IntersectionFn = (DataA: any[] = [], DataB: any[] = []): any[] => {
|
||||
const newData: any[] = []
|
||||
if (!DataA.length) return []
|
||||
DataB.forEach((item) => {
|
||||
console.log(item, item.id)
|
||||
if (DataA.some((aItem) => aItem.id === item.id)) {
|
||||
newData.push(item)
|
||||
}
|
||||
})
|
||||
return newData
|
||||
}
|
||||
|
||||
/**
|
||||
* 并集函数处理,保留平台、插件中的所有属性,ID重复时,只保留来自插件中的1条属性。
|
||||
* @param DataA 产品物模型
|
||||
* @param DataB 插件物模型
|
||||
* @constructor
|
||||
*/
|
||||
const UnionFn = (DataA: any[] = [], DataB: any[] = []): any[] => {
|
||||
const dataMap = new Map()
|
||||
|
||||
DataB.forEach((item) => {
|
||||
dataMap.set(item.id, item)
|
||||
})
|
||||
|
||||
DataA.forEach((item) => {
|
||||
if (!dataMap.has(item.id)) {
|
||||
dataMap.set(item.id, item)
|
||||
}
|
||||
})
|
||||
console.log(DataA, DataB, [...dataMap.values()])
|
||||
return [...dataMap.values()]
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.tip {
|
||||
background: #F6F6F6;
|
||||
color: #999;
|
||||
padding: 10px 26px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
:deep(.j-card-item) {
|
||||
padding: 16px !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
import Index from './index.vue'
|
||||
|
||||
export default Index
|
|
@ -0,0 +1,225 @@
|
|||
<template>
|
||||
<div class='metadata-map'>
|
||||
<div class='left'>
|
||||
<j-input-search
|
||||
style='width: 350px;margin-bottom:24px;'
|
||||
placeholder='搜索平台属性名称'
|
||||
@search='search'
|
||||
/>
|
||||
<j-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:pagination='false'
|
||||
:rowSelection='{
|
||||
selectedRowKeys: selectedKeys,
|
||||
hideSelectAll: true,
|
||||
columnWidth: 0
|
||||
}'
|
||||
rowKey='id'
|
||||
>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if='column.dataIndex === "name"'>
|
||||
<span class='metadata-title'>{{ text }} ({{ record.id }})</span>
|
||||
</template>
|
||||
<template v-if='column.dataIndex === "plugin"'>
|
||||
<j-select
|
||||
v-model:value='record.plugin'
|
||||
style='width: 100%'
|
||||
@change='(id) => pluginChange(record, id)'
|
||||
>
|
||||
<j-select-option
|
||||
v-for='(item, index) in pluginOptions'
|
||||
:key='index + "_" + item.id'
|
||||
:value='item.value'
|
||||
:disabled='selectedPluginKeys.includes(item.id)'
|
||||
>{{ item.label }} ({{ item.id }})</j-select-option>
|
||||
</j-select>
|
||||
</template>
|
||||
</template>
|
||||
</j-table>
|
||||
</div>
|
||||
<div class='right'>
|
||||
<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.png")' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='MetadataMap'>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useProductStore } from '@/store/product';
|
||||
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig'
|
||||
import { getPluginData, getProductByPluginId } from '@/api/link/plugin'
|
||||
import { getImage, onlyMessage } from '@/utils/comm'
|
||||
import { getMetadateMapById, metadateMapById } from '@/api/device/instance'
|
||||
|
||||
const productStore = useProductStore();
|
||||
const { current: productDetail } = storeToRefs(productStore)
|
||||
const dataSource = ref([])
|
||||
const pluginOptions = ref<any[]>([])
|
||||
|
||||
const tableFilter = (value: string, record: any) => {
|
||||
console.log(value, record)
|
||||
return true
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '平台属性',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: '目标属性',
|
||||
dataIndex: 'plugin',
|
||||
filters: [
|
||||
{ text: '置顶已映射数据', value: 'already' },
|
||||
{ text: '置顶未映射数据', value: 'not' },
|
||||
],
|
||||
onFilter: tableFilter
|
||||
}
|
||||
]
|
||||
|
||||
const selectedKeys = computed(() => {
|
||||
return dataSource.value.filter(item => !!item?.plugin).map(item => item.id)
|
||||
})
|
||||
|
||||
const selectedPluginKeys = computed(() => {
|
||||
return dataSource.value.filter(item => !!item?.plugin).map(item => item.plugin)
|
||||
})
|
||||
|
||||
const getMetadataMapData = () => {
|
||||
return new Promise(resolve => {
|
||||
getMetadateMapById(productDetail.value?.id).then(res => {
|
||||
if (res.success) {
|
||||
resolve(res.result?.filter(item => item.customMapping)?.map(item => {
|
||||
return {
|
||||
id: item.metadataId,
|
||||
pluginId: item.originalId
|
||||
}
|
||||
}) || [])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getDefaultMetadata = async () => {
|
||||
const metadata = JSON.parse(productDetail.value?.metadata || '{}')
|
||||
const properties = metadata.properties
|
||||
const pluginMedata = await getPluginMetadata()
|
||||
const pluginProperties = pluginMedata?.properties || []
|
||||
const metadataMap = await getMetadataMapData()
|
||||
pluginOptions.value = pluginProperties.map(item => ({...item, label: item.name, value: item.id}))
|
||||
|
||||
const concatProperties = [ ...pluginProperties.map(item => ({ id: item.id, pluginId: item.id})), ...metadataMap]
|
||||
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,
|
||||
plugin: _m?.pluginId, // 插件物模型id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getPluginMetadata = (): Promise<{ properties: any[]}> => {
|
||||
return new Promise(resolve => {
|
||||
queryPluginAccessDetail(productDetail.value?.accessId!).then(async res => {
|
||||
if (res.success) {
|
||||
const _channelId = (res.result as any)!.channelId
|
||||
const pluginRes = await getPluginData('product', _channelId, productDetail.value?.id).catch(() => ({ success: false, result: {}}))
|
||||
const resp = await getProductByPluginId(_channelId).catch(() => ({ success: false, result: []}))
|
||||
if (resp.success) {
|
||||
const _item = (resp.result as any[])?.find((item: any) => item.id === (pluginRes?.result as any)?.externalId)
|
||||
|
||||
resolve(_item ? _item.metadata : { properties: [] })
|
||||
}
|
||||
}
|
||||
resolve({ properties: [] })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const pluginChange = async (value: any, id: string) => {
|
||||
const res = await metadateMapById(productDetail.value?.id, [{
|
||||
metadataType: 'property',
|
||||
metadataId: value.id,
|
||||
originalId: id
|
||||
}])
|
||||
if (res.success) {
|
||||
onlyMessage('操作成功')
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultMetadata()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.metadata-map {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
|
||||
.left {
|
||||
margin-right: 424px;
|
||||
}
|
||||
|
||||
.right {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
height: 100%;
|
||||
width: 400px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
color: rgba(#000, .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>
|
|
@ -112,6 +112,7 @@ import Info from './BasicInfo/indev.vue';
|
|||
import Device from './DeviceAccess/index.vue';
|
||||
import Metadata from '../../../device/components/Metadata/index.vue';
|
||||
import DataAnalysis from './DataAnalysis/index.vue';
|
||||
import MetadataMap from './MetadataMap'
|
||||
// import Metadata from '../../../components/Metadata/index.vue';
|
||||
import {
|
||||
_deploy,
|
||||
|
@ -163,6 +164,7 @@ const tabs = {
|
|||
Metadata,
|
||||
Device,
|
||||
DataAnalysis,
|
||||
MetadataMap
|
||||
};
|
||||
|
||||
watch(
|
||||
|
@ -280,6 +282,9 @@ const getProtocol = async () => {
|
|||
];
|
||||
}
|
||||
}
|
||||
if (productStore.current?.accessProvider === 'plugin_gateway') {
|
||||
list.value.push({ key: 'MetadataMap', tab: '物模型映射'})
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
import InklingDevice from './index.vue'
|
||||
export default InklingDevice
|
|
@ -0,0 +1,305 @@
|
|||
<template>
|
||||
<div class='inkling-device'>
|
||||
<j-spin :spinning='spinning'>
|
||||
<div class='search-box'>
|
||||
<div class='search-warp'>
|
||||
<j-advanced-search
|
||||
v-if='!spinning'
|
||||
:columns='columns'
|
||||
type='simple'
|
||||
@search='handleSearch'
|
||||
class='device-inkling'
|
||||
target='device-inkling'
|
||||
/>
|
||||
</div>
|
||||
<div class='multiple' v-if='multiple'>
|
||||
<j-checkbox @change='checkChange'>全选</j-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class='device-list-warp'>
|
||||
<j-scrollbar v-if='deviceList.length'>
|
||||
<j-spin :spinning='deviceSpinning'>
|
||||
<div class='device-list-items'>
|
||||
<div
|
||||
v-for='item in deviceList'
|
||||
:class='{
|
||||
"device-list-item": true,
|
||||
"active": checkKeys.includes(item.id),
|
||||
"disabled": disabledKeys.includes(item.id)
|
||||
}'
|
||||
@click='() => deviceClick(item.id, item)'
|
||||
>
|
||||
<template v-if='disabledKeys.includes(item.id)'>
|
||||
<j-tooltip
|
||||
title='该设备已绑定平台设备'
|
||||
>
|
||||
<span class='item-title'>{{ item.id }}</span>
|
||||
</j-tooltip>
|
||||
</template>
|
||||
<span v-else class='item-title'>
|
||||
{{ item.id }}
|
||||
</span>
|
||||
<a-icon
|
||||
v-if='checkKeys.includes(item.id)'
|
||||
type='CheckOutlined'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</j-spin>
|
||||
</j-scrollbar>
|
||||
<j-empty
|
||||
v-else
|
||||
description='暂无数据'
|
||||
style='padding-top: 24px'
|
||||
/>
|
||||
<div class='device-list-pagination'>
|
||||
<j-pagination
|
||||
v-if='showPage'
|
||||
:total='pageData.total'
|
||||
:current='pageData.pageIndex + 1'
|
||||
:pageSize='pageData.pageSize'
|
||||
:show-total='() => {
|
||||
const minSize = pageData.pageIndex * pageData.pageSize + 1;
|
||||
const MaxSize = (pageData.pageIndex + 1) * pageData.pageSize;
|
||||
return `第 ${minSize} - ${MaxSize > pageData.total ? pageData.total : MaxSize } 条/总共 ${pageData.total} 条`;
|
||||
}'
|
||||
@change='pageChange'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</j-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='InklingDevice'>
|
||||
|
||||
import { getCommandsByAccess, getCommandsDevicesByAccessId } from '@/api/link/accessConfig'
|
||||
import { getInkingDevices } from '@/api/device/instance'
|
||||
import { isArray } from 'lodash-es'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: string | string[]): void
|
||||
(e: 'change', data: any | any[]): void
|
||||
}
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: undefined
|
||||
},
|
||||
accessId: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const spinning = ref(true)
|
||||
const deviceSpinning = ref(false)
|
||||
const deviceList = ref([])
|
||||
const disabledKeys = ref<string[]>([])
|
||||
const checkKeys = ref<string[]>([])
|
||||
const checkCache = ref<Map<string, any>>(new Map())
|
||||
const showPage = ref(false)
|
||||
const pageData = reactive({
|
||||
pageSize: 10,
|
||||
pageIndex: 0,
|
||||
total: 0
|
||||
})
|
||||
const params = ref({
|
||||
terms: []
|
||||
})
|
||||
|
||||
const columns = ref([])
|
||||
|
||||
const queryInkingDevices = (data: string[]) => {
|
||||
return new Promise(async (resolve) => {
|
||||
if (!data.length) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
const res = await getInkingDevices(data)
|
||||
if (res) {
|
||||
disabledKeys.value = res.result?.map(item => item.externalId)
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
|
||||
const getDeviceList = async () => {
|
||||
const resp = await getCommandsDevicesByAccessId(props.accessId!, {
|
||||
pageIndex: pageData.pageIndex,
|
||||
pageSize: pageData.pageSize,
|
||||
terms: params.value.terms
|
||||
}).catch(() => ({ success: false }))
|
||||
if (resp.success) {
|
||||
await queryInkingDevices(resp.result?.data.map(item => item.id) || [])
|
||||
deviceList.value = resp.result?.data || []
|
||||
pageData.total = resp.result?.total || 0
|
||||
}
|
||||
}
|
||||
|
||||
const checkChange = (e: any) => { // 全选
|
||||
if (e.target.checked) {
|
||||
const keys = deviceList.value.filter(item => {
|
||||
// 过滤已选中和已绑定
|
||||
const type = !checkKeys.value.includes(item.id) && !disabledKeys.value.includes(item.id)
|
||||
if (type && checkCache.value.has(item.id)) {
|
||||
checkCache.value.set(item.id, item)
|
||||
}
|
||||
return type
|
||||
}).map(item => item.id)
|
||||
checkKeys.value = [...checkKeys.value, ...keys]
|
||||
emit('update:value', checkKeys.value)
|
||||
emit('change', [...checkCache.value.values()])
|
||||
} else {
|
||||
checkCache.value.clear()
|
||||
checkKeys.value = []
|
||||
emit('update:value', [])
|
||||
emit('change', [])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (p: any) => { // 查询
|
||||
pageData.pageIndex = 0
|
||||
params.value = p
|
||||
getDeviceList()
|
||||
}
|
||||
|
||||
const pageChange = (page: number, pageSize: number) => { // 分页变化
|
||||
pageData.pageSize = pageSize
|
||||
pageData.pageIndex = page - 1
|
||||
getDeviceList()
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if (props.accessId) {
|
||||
const resp = await getCommandsByAccess(props.accessId)
|
||||
if (resp.success) {
|
||||
const item = resp.result?.[0]
|
||||
if (item) {
|
||||
showPage.value = item.id === 'QueryDevicePage' // 分页
|
||||
columns.value = item.expands?.terms?.map(t => ({
|
||||
title: t.name,
|
||||
dataIndex: t.id,
|
||||
search: {
|
||||
type: t.valueType.type
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
spinning.value = false
|
||||
await getDeviceList()
|
||||
}
|
||||
}
|
||||
|
||||
const deviceClick = (id: string, option: any) => {
|
||||
if (option.disabled || disabledKeys.value.includes(id)) return
|
||||
|
||||
const _check = new Set(checkKeys.value)
|
||||
|
||||
if (props.multiple) { // 多选
|
||||
if (_check.has(id)) {
|
||||
_check.delete(id)
|
||||
checkCache.value.delete(id)
|
||||
} else {
|
||||
checkCache.value.set(id, option)
|
||||
_check.add(id)
|
||||
}
|
||||
checkKeys.value = [..._check.values()]
|
||||
emit('update:value', checkKeys.value)
|
||||
emit('change', [...checkCache.value.values()])
|
||||
} else {
|
||||
checkKeys.value = [id]
|
||||
emit('update:value', id)
|
||||
emit('change', option)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.value, (newValue) => {
|
||||
if (!newValue) {
|
||||
checkKeys.value = []
|
||||
return
|
||||
}
|
||||
if (isArray(newValue)) {
|
||||
checkKeys.value = newValue
|
||||
} else {
|
||||
checkKeys.value = [newValue as string]
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.inkling-device {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
|
||||
:deep(.device-inkling) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.search-warp {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.multiple {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.device-list-warp {
|
||||
.device-list-items {
|
||||
.device-list-item {
|
||||
padding: 10px 16px;
|
||||
color: #4F4F4F;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> .item-title {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(47, 84, 235, 0.06);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(153, 153, 153, 0.06);
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: rgba(153, 153, 153, 0.06);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-list-pagination {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -469,7 +469,7 @@ export default {
|
|||
id: '1-4-9',
|
||||
parentId: '1-4',
|
||||
path: 'T4zX-A0TC-BFum',
|
||||
sortIndex: 9999,
|
||||
sortIndex: 9998,
|
||||
level: 1,
|
||||
name: '远程升级',
|
||||
code: 'device/Firmware',
|
||||
|
@ -493,6 +493,34 @@ export default {
|
|||
accessDescription: '此菜单不支持数据权限控制',
|
||||
granted: true,
|
||||
},
|
||||
{
|
||||
id: '1-4-10',
|
||||
parentId: '1-4',
|
||||
path: 'T4zX-A0TC-BFum',
|
||||
sortIndex: 9999,
|
||||
level: 1,
|
||||
name: '插件管理',
|
||||
code: 'link/plugin',
|
||||
icon: 'BoxPlotOutlined',
|
||||
url: '/iot/link/plugin',
|
||||
buttons: [
|
||||
{ id: 'view', name: '查看', enabled: true, granted: true },
|
||||
{ id: 'update', name: '编辑', enabled: true, granted: true },
|
||||
{ id: 'delete', name: '删除', enabled: true, granted: true },
|
||||
{
|
||||
id: 'add',
|
||||
name: '新增',
|
||||
enabled: true,
|
||||
granted: true,
|
||||
},
|
||||
],
|
||||
accessSupport: { text: '不支持', value: 'unsupported' },
|
||||
assetAccesses: [],
|
||||
options: {},
|
||||
createTime: 1659344075524,
|
||||
accessDescription: '此菜单不支持数据权限控制',
|
||||
granted: true,
|
||||
},
|
||||
],
|
||||
[ROLEKEYS.complex]: [
|
||||
{
|
||||
|
|
|
@ -1436,6 +1436,63 @@ export default [
|
|||
supportDataAccess: false,
|
||||
indirectMenus: ['8ddbb67de5f65514105d47b448bfd70e']
|
||||
},
|
||||
{
|
||||
code: 'link/plugin',
|
||||
name: '插件管理',
|
||||
owner: 'iot',
|
||||
//parentId: '1-4',
|
||||
id: 'a20354876e9519e48f5ed6710ba6efb3',
|
||||
sortIndex: 10,
|
||||
url: '/iot/link/plugin',
|
||||
icon: 'BoxPlotOutlined',
|
||||
showPage: ['plugin-driver'],
|
||||
permissions: [],
|
||||
buttons: [
|
||||
{
|
||||
id: 'view',
|
||||
name: '查看',
|
||||
permissions: [
|
||||
{
|
||||
permission: 'plugin-driver',
|
||||
actions: ['save'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'update',
|
||||
name: '编辑',
|
||||
permissions: [
|
||||
{
|
||||
permission: 'plugin-driver',
|
||||
actions: ['save'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
name: '删除',
|
||||
permissions: [
|
||||
{
|
||||
permission: 'plugin-driver',
|
||||
actions: ['delete'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'add',
|
||||
name: '新增',
|
||||
permissions: [
|
||||
{
|
||||
permission: 'plugin-driver',
|
||||
actions: ['save'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
accessSupport: { text: "不支持", value: "unsupported" },
|
||||
supportDataAccess: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -135,11 +135,11 @@ const submitData = async () => {
|
|||
const judgeInitSet = async () => {
|
||||
const resp: any = await getInit();
|
||||
if (resp.status === 200 && resp.result.length) {
|
||||
window.location.href = '/';
|
||||
// window.location.href = '/';
|
||||
}
|
||||
};
|
||||
onBeforeMount(() => {
|
||||
// judgeInitSet();
|
||||
judgeInitSet();
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -90,7 +90,6 @@
|
|||
{
|
||||
max: 64,
|
||||
message: '最多可输入64个字符',
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
name='name'
|
||||
|
@ -325,10 +324,10 @@ const saveData = () => {
|
|||
loading.value = true
|
||||
const resp =
|
||||
paramsId === ':id'
|
||||
? await save(params)
|
||||
: await update({ ...params, id: paramsId });
|
||||
? await save(params).catch(() => { success: false})
|
||||
: await update({ ...params, id: paramsId }).catch(() => { success: false});
|
||||
loading.value = false
|
||||
if (resp.status === 200) {
|
||||
if (resp.success) {
|
||||
onlyMessage('操作成功', 'success');
|
||||
history.back();
|
||||
if ((window as any).onTabSaveSuccess) {
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
import { ID_Rule, Max_Length_64, Max_Length_200, RequiredStringFn } from '@/components/Form/rules'
|
||||
import UploadFile from './UploadFile.vue'
|
||||
import { FileUploadResult } from '@/views/link/plugin/typings'
|
||||
import { add, vailIdFn } from '@/api/link/plugin'
|
||||
import { add, update, vailIdFn } from '@/api/link/plugin'
|
||||
import { message } from 'jetlinks-ui-components'
|
||||
import { TypeMap } from './util'
|
||||
|
||||
|
@ -92,7 +92,7 @@ const fileType = ref(props.data.type)
|
|||
const loading = ref(false)
|
||||
|
||||
const vailId = async (_: any, value: string) => {
|
||||
if (!!props.data.id && value) { // 新增校验
|
||||
if (!props.data.id && value) { // 新增校验
|
||||
const resp = await vailIdFn(value)
|
||||
if (resp.success && resp.result) {
|
||||
return Promise.reject('ID重复');
|
||||
|
@ -135,7 +135,7 @@ const handleSave = async () => {
|
|||
const data = await formRef.value.validate()
|
||||
if (data) {
|
||||
loading.value = true
|
||||
const resp = await add(modelRef).catch(() => { success: false })
|
||||
const resp = props.data.id ? await update(modelRef).catch(() => { success: false }) : await add(modelRef).catch(() => { success: false })
|
||||
loading.value = false
|
||||
if (resp.success) {
|
||||
message.success('操作成功!');
|
||||
|
|