feat: 新增设备批量导出-映射设备

This commit is contained in:
xieyonghong 2023-04-19 17:25:07 +08:00
parent 875e7c00f7
commit 677f65ff8c
8 changed files with 424 additions and 29 deletions

View File

@ -24,4 +24,67 @@ export const ID_Rule = [
message: '请输入英文或者数字或者-或者_',
},
Max_Length_64[0]
]
]
export const CreteRuleByType = (type: string) => {
switch (type){
case 'int':
return [
{
validator: (_: any, value: number) => {
const baseNumber = 2147483648
if (value < -baseNumber) {
return Promise.reject(`最小仅输入-${baseNumber}`);
}
if (value > baseNumber) {
return Promise.reject(`最大可输入${baseNumber}`);
}
return Promise.resolve();
}
}
]
case'long':
return [
{
validator: (_: any, value: number) => {
const baseNumber = 340282346638528860000000000000000000000
if (value < -baseNumber) {
return Promise.reject(`最小仅输入-${baseNumber}`);
}
if (value > baseNumber) {
return Promise.reject(`最大可输入${baseNumber}`);
}
return Promise.resolve();
}
}
]
case'float':
return [
{
validator: (_: any, value: number) => {
const baseNumber = 9223372036854775807
if (value < -baseNumber) {
return Promise.reject(`最小仅输入-${baseNumber}`);
}
if (value > baseNumber) {
return Promise.reject(`最大可输入${baseNumber}`);
}
return Promise.resolve();
}
}
]
// case'double':
// return [
// {
// max: 1.7976931348623157,
// message: '最大可输入64位字符'
// }
// ]
case 'string':
return [MaxLengthStringFn()];
case 'description':
return [MaxLengthStringFn(200)]
default:
return []
}
}

View File

@ -0,0 +1,69 @@
<template>
<j-modal
:maskClosable="false"
:visible="true"
width="800px"
title="导入"
>
<div>
<!-- 选择产品 -->
<div v-if='steps === 0'>
<Product
v-model:rowKey='importData.productId'
/>
</div>
</div>
<template #footer>
<j-button v-if='steps === 0' @click='cancel' >取消</j-button>
<j-button v-if='steps !== 0' @click='prev' >上一步</j-button>
<j-button v-if='steps !== 2' @click='next' type='primary'>下一步</j-button>
<j-button v-if='steps === 2' @click='save' type='primary'>确认</j-button>
</template>
</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';
const emit = defineEmits(['cancel', 'save']);
const steps = ref(0) //
const importData = reactive({
productId: undefined,
type: undefined,
})
const next = () => {
if (steps.value === 0 && !importData.productId) {
return onlyMessage('请选择产品', 'error')
}
if (steps.value === 1 && !importData.type) {
return onlyMessage('请选择导入方式', 'error')
}
steps.value += 1
}
const prev = () => {
if (steps.value === 2 && importData.type) {
steps.value = 0
} else {
steps.value -= 1
}
}
const cancel = () => {
emit('cancel')
}
const save = () => {
emit('save')
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,254 @@
<template>
<j-advanced-search
:columns="columns"
type='simple'
@search="handleSearch"
class="scene-search"
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>
</template>
<script setup lang='ts' name='Product'>
import { getProviders, queryGatewayList, queryProductList } from '@/api/device/product'
import { queryTree } from '@/api/device/category'
import { getTreeData_api } from '@/api/system/department'
import { getImage } from '@/utils/comm'
import { accessConfigTypeFilter } from '@/utils/setting'
type Emit = {
(e: 'update:rowKey', data: string): void
(e: 'change', data: string): void
}
const params = ref({})
const props = defineProps({
rowKey: {
type: String,
default: ''
},
})
const emit = defineEmits<Emit>()
const firstFind = ref(true)
const columns = [
{
title: 'ID',
dataIndex: 'id',
width: 300,
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
search: {
type: 'string',
first: true
}
},
{
title: '网关类型',
dataIndex: 'accessProvider',
width: 150,
ellipsis: true,
hideInTable: true,
search: {
type: 'select',
options: () => getProviders().then((resp: any) => {
return accessConfigTypeFilter(resp.result || [])
})
}
},
{
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' },
]
}
},
{
title: '状态',
dataIndex: 'state',
width: '90px',
search: {
type: 'select',
options: [
{ label: '禁用', value: 0 },
{ label: '正常', value: 1 },
]
}
},
{
title: '说明',
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',
}
}
}
},
{
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)
}
</script>
<style scoped lang='less'>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>

View File

@ -90,7 +90,7 @@
<j-form ref="formRef" :model="formData.data" layout="vertical">
<j-form-item
:name="item.property"
v-for="item in metadata.properties"
v-for="item in metadata?.properties || []"
:key="item"
:label="item.name"
:rules="[

View File

@ -173,6 +173,7 @@ import {
import AccessCard from '../AccessCard/index.vue';
import { useMenuStore } from 'store/menu'
import { onlyMessage } from '@/utils/comm';
import { CreteRuleByType } from 'components/Form/rules'
const props = defineProps({
provider: {
@ -208,6 +209,7 @@ const config = ref<any>([])
const queryPlugin = (params = {}) => {
getPluginList({
...params,
sorts: [{ name: 'createTime', order: 'desc' }],
paging: false
}).then(res => {
pluginList.value = []
@ -219,26 +221,23 @@ const queryPlugin = (params = {}) => {
const getRules = (item: any) => {
let typeName = '输入'
let rules: any[] = []
if (['select', 'date'].includes(item.type?.type || 'string')) {
typeName = '选择'
}
const rules = [
{
required: true,
message: `${typeName}${item.name}`
}
]
if (['select', 'date'].includes(item.type?.type || 'string')) {
rules.push({
max: 64,
message: `最多输入64个字符`
})
if (item.required) {
rules.push(
{
required: true,
message: `${typeName}${item.name}`
}
)
}
const typeRules = CreteRuleByType(item.type?.type)
rules = [...rules, ...typeRules]
return rules
}

View File

@ -41,7 +41,7 @@
name='version'
:rules='[{ required: true, message: "请上传文件" }]'
>
<UploadFile v-model:modelValue='modelRef.version' @change='uploadChange' :disabled='data.id' />
<UploadFile v-model:modelValue='modelRef.version' @change='uploadChange' />
</j-form-item>
<div v-if='modelRef.version' class='file-detail'>
<div>
@ -77,7 +77,6 @@ import { FileUploadResult } from '@/views/link/plugin/typings'
import { add, vailIdFn } from '@/api/link/plugin'
import { message } from 'jetlinks-ui-components'
import { TypeMap } from './util'
import { start } from '@/api/link/type'
const props = defineProps({
data: {

View File

@ -9,12 +9,12 @@
@change="handleChange"
class="upload-box"
:before-upload="beforeUpload"
:disabled='disabled || loading'
:disabled='loading'
:maxCount='1'
>
<div>
<j-button :disabled='disabled'>上传文件</j-button>
<span class='upload-tip'>格式要求{文件名}.jar</span>
<j-button>上传文件</j-button>
<span class='upload-tip'>格式要求{文件名}.jar/{文件名}.zip</span>
</div>
</j-upload>
@ -51,9 +51,9 @@ const loading = ref(false);
const beforeUpload: UploadProps['beforeUpload'] = (file, fl) => {
const arr = file.name.split('.');
const isFile = ['jar'].includes(arr[arr.length - 1]); // file.type === 'application/zip' || file.type === 'application/javj-archive'
const isFile = ['jar', 'zip'].includes(arr[arr.length - 1]); // file.type === 'application/zip' || file.type === 'application/javj-archive'
if (!isFile) {
onlyMessage('请上传.jar格式的文件', 'error');
onlyMessage('请上传.jar,.zip格式的文件', 'error');
loading.value = false;
}
return isFile;

View File

@ -29,8 +29,12 @@
:value="slotProps"
:showStatus='false'
:actions='getActions(slotProps)'
:statusNames='{
processing: "processing"
}'
status='processing'
>
<template #img>
<img
:width="80"
@ -39,11 +43,16 @@
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px); margin-bottom: 18px;">
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
<div>
<div>
<j-tag class='plugin-version'>{{ slotProps.version }}</j-tag>
</div>
<Ellipsis style="width: calc(100% - 100px); margin-bottom: 18px;">
<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">
@ -258,6 +267,8 @@ onMounted(() => {
</script>
<style scoped>
<style scoped lang='less'>
.plugin-version {
border-radius: 4px;
}
</style>