feat: 完善插件管理

This commit is contained in:
xieyonghong 2023-04-21 18:49:42 +08:00
parent 4b2b8c1728
commit 5772c050f8
31 changed files with 1643 additions and 241 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

View File

@ -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
})
}
/**

View File

@ -0,0 +1,3 @@
import Inkling from './index.vue'
export default Inkling

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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('请选择接入方式');

View File

@ -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()
/**

View File

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

View File

@ -0,0 +1,3 @@
import Index from './index.vue'
export default Index

View File

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

View File

@ -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: '物模型映射'})
}
}
};
/**

View File

@ -0,0 +1,2 @@
import InklingDevice from './index.vue'
export default InklingDevice

View File

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

View File

@ -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]: [
{

View File

@ -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,
},
],
},
{

View File

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

View File

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

View File

@ -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('操作成功!');