feat: 新增设备和设备基础信息

This commit is contained in:
100011797 2023-01-30 18:11:11 +08:00
parent c1adddbb3b
commit 56ca9f1dcf
11 changed files with 416 additions and 53 deletions

View File

@ -99,4 +99,24 @@ export const batchDeleteDevice = (data: string[]) => server.put(`/device-instanc
* @returns
*/
export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? '/' + productId : ''}/export.${type}`
/**
* ID是否重复
* @param id id
* @returns
*/
export const isExists = (id: string) => server.get(`/device-instance/${id}/exists`)
/**
*
* @param data
* @returns
*/
export const update = (data: Partial<DeviceInstance>) => data.id ? server.patch(`/device-instance`, data) : server.post(`/device-instance`, data)
/**
*
* @param id id
* @returns
*/
export const getConfigMetadata = (id: string) => server.get(`/device-instance/${id}/config-metadata`)

View File

@ -15,14 +15,14 @@
v-bind="props"
>
<div class="upload-image-content" :style="props.style">
<template v-if="myValue">
<template v-if="imageUrl">
<!-- <div class="upload-image"
:style="{
backgroundSize: props.backgroundSize,
backgroundImage: `url(${imageUrl})`
}"
></div> -->
<img :src="myValue" class="upload-image" />
<img :src="imageUrl" class="upload-image" />
<div class="upload-image-mask">点击修改</div>
</template>
<template v-else>
@ -32,7 +32,7 @@
</div>
</a-upload>
<div class="upload-loading-mask" v-if="props.disabled"></div>
<div class="upload-loading-mask" v-if="myValue && loading">
<div class="upload-loading-mask" v-if="imageUrl && loading">
<AIcon type="LoadingOutlined" style="font-size: 20px" />
</div>
</div>
@ -56,7 +56,6 @@ interface JUploadProps extends UploadProps {
errorMessage?: string;
size?: number;
style?: CSSProperties;
backgroundSize?: string;
}
const emit = defineEmits<Emits>();
@ -76,24 +75,23 @@ const loading = ref<boolean>(false)
const imageUrl = ref<string>(props?.modelValue || '')
const imageTypes = props.types ? props.types : ['image/jpeg', 'image/png'];
const myValue = computed({
get: () => {
return props.modelValue;
},
set: (val: any) => {
imageUrl.value = val;
emit('update:modelValue', val);
},
});
watch(() => props.modelValue,
(newValue)=> {
console.log(newValue)
imageUrl.value = newValue
}, {
deep: true,
immediate: true
})
const handleChange = (info: UploadChangeParam) => {
if (info.file.status === 'uploading') {
loading.value = true;
}
if (info.file.status === 'done') {
myValue.value = info.file.response?.result
imageUrl.value = info.file.response?.result
loading.value = false;
emit('update:modelValue', imageUrl.value)
emit('update:modelValue', info.file.response?.result)
}
if (info.file.status === 'error') {
loading.value = false;

View File

@ -68,9 +68,13 @@ export default [
// 设备管理
{
path: '/device/Instance',
path: '/device/instance',
component: () => import('@/views/device/Instance/index.vue')
},
{
path: '/device/instance/detail/:id',
component: () => import('@/views/device/Instance/Detail/index.vue')
},
// link 运维管理
{
path: '/link/log',

View File

@ -1,13 +1,28 @@
import { DeviceInstance, InstanceModel } from "@/views/device/Instance/typings"
import { defineStore } from "pinia";
import { defineStore } from "pinia"
import { detail } from '@/api/device/instance'
export const useInstanceStore = defineStore({
id: 'device',
state: () => ({} as InstanceModel),
state: () => ({
current: {} as Partial<DeviceInstance>,
detail: {} as Partial<DeviceInstance>,
tabActiveKey: 'Info'
}),
actions: {
setCurrent(current: Partial<DeviceInstance>) {
this.current = current
this.detail = current
}
},
async refresh(id: string) {
const resp = await detail(id)
if(resp.status === 200){
this.current = resp.result
this.detail = resp.result
}
},
setTabActiveKey(key: string) {
this.tabActiveKey = key
},
}
})

View File

@ -0,0 +1,57 @@
<template>
<div style="margin-top: 20px" v-if="config.length">
<div style="display: flex;">
<div style="font-size: 16px; font-weight: 700">配置</div>
<a-space>
<a-button type="link" @click="visible = true"><AIcon type="EditOutlined" />编辑</a-button>
<a-button type="link" v-if="instanceStore.detail.current?.value !== 'notActive'"><AIcon type="CheckOutlined" />应用配置<a-tooltip title="修改配置后需重新应用后才能生效。"><AIcon type="QuestionCircleOutlined" /></a-tooltip></a-button>
<a-button type="link" v-if="instanceStore.detail.aloneConfiguration"><AIcon type="SyncOutlined" />恢复默认<a-tooltip title="该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。"><AIcon type="QuestionCircleOutlined" /></a-tooltip></a-button>
</a-space>
</div>
<a-descriptions bordered size="small" v-for="i in config" :key="i.name">
<template #title><h4>{{i.name}}</h4></template>
<a-descriptions-item v-for="item in i.properties" :key="item.property">
<template #label>
<a-tooltip v-if="item.description" :title="item.description"><AIcon type="QuestionCircleOutlined" /></a-tooltip>
<span>{{item.name}}</span>
</template>
<span v-if="item.type.type === 'password' && instanceStore.current?.configuration?.[item.property]?.length > 0">******</span>
<span v-else>
<span>{{ instanceStore.current?.configuration?.[item.property] || '' }}</span>
<a-tooltip v-if="isExit(item.property)" :title="`有效值:${instanceStore.current?.configuration?.[item.property]}`"><AIcon type="QuestionCircleOutlined" /></a-tooltip>
</span>
</a-descriptions-item>
</a-descriptions>
</div>
</template>
<script lang="ts" setup>
import { useInstanceStore } from "@/store/instance"
import { ConfigMetadata } from "@/views/device/Product/typings"
import { getConfigMetadata } from '@/api/device/instance'
const instanceStore = useInstanceStore()
const visible = ref<boolean>(false)
const config = ref<ConfigMetadata[]>([])
watchEffect(() => {
if(instanceStore.current.id){
// getConfigMetadata(instanceStore.current.id).then(resp => {
// if(resp.status === 200){
// config.value = resp?.result as ConfigMetadata[]
// }
// })
}
})
const isExit = (property: string) => {
return (
instanceStore.current?.cachedConfiguration &&
instanceStore.current?.cachedConfiguration[property] !== undefined &&
instanceStore.current?.configuration &&
instanceStore.current?.configuration[property] !==
instanceStore.current?.cachedConfiguration[property]
);
}
</script>

View File

@ -0,0 +1,23 @@
<template>
<div style="margin-top: 20px">
<a-descriptions bordered>
<template #title>
关系信息
<a-button type="link" @click="visible = true"><AIcon type="EditOutlined" />编辑<a-tooltip title="管理设备与其他业务的关联关系,关系来源于关系配置"><AIcon type="QuestionCircleOutlined" /></a-tooltip></a-button>
</template>
<a-descriptions-item :span="1" v-for="item in dataSource" :key="item.objectId" :label="item.relationName">{{ item?.related ? (item?.related || []).map(i => i.name).join(',') : '' }}</a-descriptions-item>
</a-descriptions>
</div>
</template>
<script lang="ts" setup>
import { useInstanceStore } from "@/store/instance"
const instanceStore = useInstanceStore()
const dataSource = ref<Record<any, any>[]>([])
watchEffect(() => {
const arr = (instanceStore.current?.relations || []).reverse()
dataSource.value = arr as Record<any, any>[]
})
</script>

View File

@ -0,0 +1,3 @@
<template>
tags
</template>

View File

@ -0,0 +1,45 @@
<template>
<a-card>
<a-descriptions bordered>
<template #title>
设备信息
<a-button type="link" @click="visible = true"><AIcon type="EditOutlined" />编辑</a-button>
</template>
<a-descriptions-item label="设备ID">{{ instanceStore.current.id }}</a-descriptions-item>
<a-descriptions-item label="产品名称">{{ instanceStore.current.productName }}</a-descriptions-item>
<a-descriptions-item label="产品分类">{{ instanceStore.current.classifiedName }}</a-descriptions-item>
<a-descriptions-item label="设备类型">{{ instanceStore.current.deviceType?.text }}</a-descriptions-item>
<a-descriptions-item label="固件版本">{{ instanceStore.current.firmwareInfo?.version }}</a-descriptions-item>
<a-descriptions-item label="连接协议">{{ instanceStore.current.protocolName }}</a-descriptions-item>
<a-descriptions-item label="消息协议">{{ instanceStore.current.transport }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ instanceStore.current.createTime ? moment(instanceStore.current.createTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</a-descriptions-item>
<a-descriptions-item label="注册时间">{{ instanceStore.current.registerTime ? moment(instanceStore.current.registerTime).format('YYYY-MM-DD HH:mm:ss') : ''}}</a-descriptions-item>
<a-descriptions-item label="最后上线时间">{{ instanceStore.current.onlineTime ? moment(instanceStore.current.onlineTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</a-descriptions-item>
<a-descriptions-item label="父设备" v-if="instanceStore.current.deviceType?.value === 'childrenDevice'">{{ instanceStore.current.parentId }}</a-descriptions-item>
<a-descriptions-item label="说明">{{ instanceStore.current.description }}</a-descriptions-item>
</a-descriptions>
<Config />
<Tags v-if="instanceStore.current?.tags && instanceStore.current?.tags.length > 0 " />
<Relation v-if="instanceStore.current?.relations && instanceStore.current?.relations.length > 0" />
<Save v-if="visible" :data="instanceStore.current" @close="visible = false" @save="saveBtn" />
</a-card>
</template>
<script lang="ts" setup>
import { useInstanceStore } from '@/store/instance'
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 moment from 'moment'
const visible = ref<boolean>(false)
const instanceStore = useInstanceStore()
const saveBtn = () => {
if(instanceStore.current?.id){
instanceStore.refresh(instanceStore.current?.id)
}
visible.value = false
}
</script>

View File

@ -0,0 +1,49 @@
<template>
<page-container :tabList="list" @back="onBack" :tabActiveKey="instanceStore.active" @tabChange="onTabChange">
<template #subTitle><div>{{instanceStore.current.name}}</div></template>
<component :is="instanceStore.tabActiveKey" />
</page-container>
</template>
<script lang="ts" setup>
import { useInstanceStore } from '@/store/instance';
import Info from './Info/index.vue';
import Metadata from '../../components/Metadata/index.vue';
const route = useRoute();
const instanceStore = useInstanceStore()
const list = [
{
key: 'Info',
tab: '实例信息'
},
{
key: 'Metadata',
tab: '物模型'
}
]
const tabs = {
Info,
Metadata
}
watch(
() => route.params.id,
(newId) => {
if(newId){
instanceStore.tabActiveKey = 'Info'
instanceStore.refresh(newId as string)
}
},
{immediate: true, deep: true}
);
const onBack = () => {
}
const onTabChange = (e: string) => {
instanceStore.tabActiveKey = e
}
</script>

View File

@ -1,29 +1,83 @@
<template>
<a-modal :maskClosable="false" width="650px" :visible="true" title="新增" @ok="handleCancel" @cancel="handleCancel">
<a-modal
:maskClosable="false"
width="650px"
:visible="true"
:title="!!props.data.id ? '编辑' : '新增'"
@ok="handleSave"
@cancel="handleCancel"
:confirmLoading="loading"
>
<div style="margin-top: 10px">
<a-form :layout="'vertical'">
<a-form
:layout="'vertical'"
ref="formRef"
:rules="rules"
:model="modelRef"
>
<a-row type="flex">
<a-col flex="180px">
<a-form-item required name="photoUrl">
<JUpload v-model:value="modelRef.photoUrl" />
<a-form-item name="photoUrl">
<JUpload v-model="modelRef.photoUrl" />
</a-form-item>
</a-col>
<a-col flex="auto">
<a-form-item label="ID">
<a-input v-model:value="modelRef.id" placeholder="请输入ID" />
<a-form-item name="id">
<template #label>
<span>
ID
<a-tooltip title="若不填写系统将自动生成唯一ID">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
</a-tooltip>
</span>
</template>
<a-input
v-model:value="modelRef.id"
placeholder="请输入ID"
:disabled="!!props.data.id"
/>
</a-form-item>
<a-form-item label="名称" required>
<a-input v-model:value="modelRef.name" placeholder="请输入名称" />
<a-form-item label="名称" name="name">
<a-input
v-model:value="modelRef.name"
placeholder="请输入名称"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="产品" required>
<a-select showSearch v-model:value="modelRef.productId" placeholder="请选择产品">
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option>
<a-form-item name="productId">
<template #label>
<span>所属产品
<a-tooltip title="只能选择“正常”状态的产品">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px" />
</a-tooltip>
</span>
</template>
<a-select
showSearch
v-model:value="modelRef.productId"
placeholder="请选择所属产品"
>
<a-select-option
:value="item.id"
v-for="item in productList"
:key="item.id"
:title="item.name"
:disabled="!!props.data.id"
></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="说明">
<a-textarea v-model:value="modelRef.describe" placeholder="请输入说明" />
<a-form-item label="说明" name="describe">
<a-textarea
v-model:value="modelRef.describe"
placeholder="请输入说明"
showCount
:maxlength="200"
/>
</a-form-item>
</a-form>
</div>
@ -31,41 +85,130 @@
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product'
import { queryNoPagingPost } from '@/api/device/product';
import { isExists, update } from '@/api/device/instance';
import { getImage } from '@/utils/comm';
import { Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
const emit = defineEmits(['close', 'save'])
const emit = defineEmits(['close', 'save']);
const props = defineProps({
data: {
type: Object,
default: undefined
}
})
const productList = ref<Record<string, any>[]>([])
const useForm = Form.useForm;
default: undefined,
},
});
const productList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const formRef = ref();
const modelRef = reactive({
productId: undefined,
id: '',
name: '',
describe: '',
photoUrl: getImage('/device/instance/device-card.png')
photoUrl: getImage('/device/instance/device-card.png'),
});
const vailId = async (_: Record<string, any>, value: string) => {
if (!props?.data?.id && value) {
const resp = await isExists(value);
if (resp.status === 200 && resp.result) {
return Promise.reject('ID重复');
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
};
const rules = {
name: [
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
],
photoUrl: [
{
required: true,
message: '请上传图标',
},
],
productId: [
{
required: true,
message: '请选择所属产品',
},
],
id: [
{
max: 64,
message: '最多输入64个字符',
},
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
{
validator: vailId,
trigger: 'blur',
},
],
};
watch(
() => props.data,
() => {
queryNoPagingPost({paging: false}).then(resp => {
if(resp.status === 200){
productList.value = resp.result as Record<string, any>[]
(newValue) => {
queryNoPagingPost({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{
terms: [
{
termType: 'eq',
column: 'state',
value: 1,
},
],
},
],
}).then((resp) => {
if (resp.status === 200) {
productList.value = resp.result as Record<string, any>[];
}
})
});
Object.assign(modelRef, newValue);
},
{immediate: true, deep: true}
)
{ immediate: true, deep: true },
);
const handleCancel = () => {
emit('close')
}
emit('close');
formRef.value.resetFields();
};
const handleSave = () => {
formRef.value
.validate()
.then(async () => {
loading.value = true;
const resp = await update(toRaw(modelRef));
loading.value = false;
if (resp.status === 200) {
message.success('操作成功!');
emit('save');
formRef.value.resetFields();
}
})
.catch((err: any) => {
console.log('error', err);
});
};
</script>

View File

@ -151,7 +151,7 @@
<Import v-if="importVisible" @close="importVisible = false" />
<Export v-if="exportVisible" @close="exportVisible = false" :data="params" />
<Process v-if="operationVisible" @close="operationVisible = false" :api="api" :type="type" />
<Save v-if="visible" :data="current" />
<Save v-if="visible" :data="current" @close="visible = false" @save="saveBtn" />
</template>
<script setup lang="ts">
@ -165,6 +165,7 @@ import Process from './Process/index.vue'
import Save from './Save/index.vue'
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
const router = useRouter();
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({})
const _selectedRowKeys = ref<string[]>([])
@ -266,7 +267,7 @@ const handleAdd = () => {
* 查看
*/
const handleView = (id: string) => {
message.warn(id + '暂未开发')
router.push('/device/instance/detail/' + id)
}
const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'): ActionsType[] => {
@ -403,4 +404,9 @@ const disabledSelectedDevice = async () => {
instanceRef.value?.reload()
}
}
const saveBtn = () => {
visible.value = false
instanceRef.value?.reload()
}
</script>