Browse Source

feat: 产品详情 - 物模型

main
刘凯 1 year ago
parent
commit
cb56482b64
  1. 58
      src/api/product/model.ts
  2. 51
      src/api/product/types.ts
  3. 1
      src/components/Form/src/hooks/useFormEvents.ts
  4. 133
      src/views/product/components/Model.vue
  5. 241
      src/views/product/components/ModelAttributeFormModal.vue
  6. 79
      src/views/product/components/ModelServiceFormModal.vue
  7. 73
      src/views/product/components/composables/useModelAttribute.ts
  8. 39
      src/views/product/components/composables/useModelService.ts
  9. 1
      src/views/product/components/index.ts
  10. 4
      src/views/product/detail.vue

58
src/api/product/model.ts

@ -0,0 +1,58 @@
import type { GetModelAttributeListParams, ModelAttribute, ModelAttributeWithForm, ModelService } from './types'
import { defHttp } from '@/utils/http/axios'
export function getAllModelServices(productId: string) {
return defHttp.get<ModelService[]>({
url: '/thingModel/select',
params: {
productId,
},
})
}
export function createModelService(data: Partial<ModelService>) {
return defHttp.post({
url: '/thingModel/save',
data,
})
}
export function updateModelService(data: Partial<ModelService>) {
return defHttp.post({
url: '/thingModel/update',
data,
})
}
export function deleteModelService(id: string) {
return defHttp.post({
url: `/thingModel/remove?id=${id}`,
})
}
export function getModelAttributeList(params: GetModelAttributeListParams) {
return defHttp.get<PageResult<ModelAttribute>>({
url: '/thingModel/item/page',
params,
})
}
export function createModelAttribute(data: ModelAttributeWithForm) {
return defHttp.post({
url: '/thingModel/item/save',
data,
})
}
export function updateModelAttribute(data: ModelAttributeWithForm) {
return defHttp.post({
url: '/thingModel/item/update',
data,
})
}
export function deleteModelAttribute(id: string) {
return defHttp.post({
url: `/thingModel/item/remove?id=${id}`,
})
}

51
src/api/product/types.ts

@ -43,3 +43,54 @@ export interface Topic {
topicDesc: string
productId: string
}
export interface ModelService {
id: string
productId: string
serviceId: string
tenantId: string
description: string
}
export interface GetModelAttributeListParams extends PageParam {
productId?: string
modelId?: string
}
export interface ModelAttribute {
id: string
name: string
itemType: number
identifier: string
modelId: string
modelName?: string
tenantId: string
method: string
sort: number
dataSpecs?: {
min?: number
max?: number
maxLength?: number
trueDesc?: string
falseDesc?: string
scale?: string
}
}
export interface ModelAttributeWithForm {
id: string
name: string
itemType: number
identifier: string
dataType: number
min: number
max: number
maxLength: number
scale: number
unit: string
trueDesc: string
falseDesc: string
method: string
sort: number
modelId: string
}

1
src/components/Form/src/hooks/useFormEvents.ts

@ -351,7 +351,6 @@ export function useFormEvents({
else
_nameList = nameList === Array.isArray(nameList) ? nameList : undefined
const values = await unref(formElRef)?.validate(_nameList)
return handleFormValues(values)
}

133
src/views/product/components/Model.vue

@ -0,0 +1,133 @@
<script setup lang='ts'>
import { Alert, Popconfirm } from 'ant-design-vue'
import { useRoute } from 'vue-router'
import { EyeOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons-vue'
import { useModelService } from './composables/useModelService'
import { useModelAttribute } from './composables/useModelAttribute'
import ModelServiceFormModal from './ModelServiceFormModal.vue'
import ModelAttributeFormModal from './ModelAttributeFormModal.vue'
import { BasicTable, TableAction } from '@/components/Table'
import type { ModelAttribute } from '@/api/product/types'
import JsonPreviewModal from '@/components/JsonPreviewModal'
defineProps<{ tsl?: string }>()
const route = useRoute()
const {
selectedModelId,
setSelectedModelId,
modelServiceList,
handleDeleteModelService,
registerModelServiceModal,
openModelServiceModal,
reloadModelService,
} = useModelService(route.params.id as string)
const {
registerModelAttributeTable,
registerModelAttributeModal,
openModelAttributeModal,
reloadModalAttribute,
handleDeleteModelAttribute,
} = useModelAttribute(route.params.id as string, selectedModelId)
</script>
<template>
<div>
<div mb="12px">
<JsonPreviewModal title="物模型 TSL" :data="tsl || ''">
<a-button type="primary">
<EyeOutlined />
物模型 TSL
</a-button>
</JsonPreviewModal>
</div>
<Alert icon type="info" show-icon class="py-4px text-13px" message="产品模型用于描述设备具备的能力和特性。通过定义产品模型,在平台构建一款设备的抽象模型,使平台理解该款设备支持的服务、属性、命令等信息,如颜色、开关等。当定义完一款产品模型后,在进行注册设备时,就可以使用在控制台上定义的产品模型。" />
<div flex="~ gap-12px" mt="12px">
<div w="20%">
<div flex="~ items-center justify-between" border="0 b-1 solid gray-200" class="mb-12px box-border h-40px px-10px">
<div font-bold>
服务列表
</div>
<div class="i-ant-design:plus-outlined cursor-pointer text-20px" @click="openModelServiceModal()" />
</div>
<div
v-for="item in modelServiceList" :key="item.id"
flex="~ items-center justify-between"
class="box-border h-60px cursor-pointer pl-10px hover:bg-gray-100"
border="0 b-1 solid gray-50"
:class="selectedModelId === item.id ? 'bg-gray-100' : ''"
@click="setSelectedModelId(item.id)"
>
<div w-0 flex-1>
<div truncate>
{{ item.serviceId }}
</div>
<div mt="5px" text="12px gray-500" truncate :title="item.description">
{{ item.description }}
</div>
</div>
<div v-if="item.serviceId !== 'DEFAULT'">
<a-button type="link" size="small" @click="openModelServiceModal(true, item)">
<span class="i-ant-design:edit-outlined" />
</a-button>
<Popconfirm title="是否要删除数据?" @confirm="handleDeleteModelService(item.id)">
<a-button type="link" size="small" danger>
<span class="i-ant-design:delete-outlined" />
</a-button>
</Popconfirm>
</div>
</div>
</div>
<div w-0 flex-1>
<BasicTable :api="async () => ([] as ModelAttribute[])" @register="registerModelAttributeTable">
<template #tableTitle>
<a-button type="primary" @click="openModelAttributeModal">
<PlusOutlined />
新增属性
</a-button>
</template>
<template #toolbar>
<a-button size="small" @click="reloadModalAttribute">
<template #icon>
<SyncOutlined />
</template>
刷新
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{
icon: 'i-ant-design:edit-outlined',
label: '编辑',
onClick: () => openModelAttributeModal(true, record),
},
{
icon: 'i-ant-design:delete-outlined',
danger: true,
label: '删除',
popConfirm: {
title: '是否要删除数据?',
placement: 'left',
confirm: () => handleDeleteModelAttribute(record.id),
},
},
]"
/>
</template>
</template>
</BasicTable>
</div>
</div>
<ModelServiceFormModal @register="registerModelServiceModal" @success="reloadModelService" />
<ModelAttributeFormModal :model-id="selectedModelId || ''" @register="registerModelAttributeModal" @success="reloadModalAttribute" />
</div>
</template>

241
src/views/product/components/ModelAttributeFormModal.vue

@ -0,0 +1,241 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { FormItem, Input, InputNumber } from 'ant-design-vue'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createModelAttribute, updateModelAttribute } from '@/api/product/model'
import type { ModelAttribute, ModelAttributeWithForm } from '@/api/product/types'
defineOptions({ name: 'ModelAttributeFormModal' })
const props = defineProps<{ modelId: string }>()
const emit = defineEmits(['success', 'register'])
enum DataTypesEnum {
Int32 = 'int32',
Float = 'float',
Bool = 'bool',
Text = 'text',
}
const isUpdate = ref(false)
const [registerForm, { setFieldsValue, validate }] = useForm({
labelWidth: 80,
baseColProps: { span: 24 },
schemas: [
{
field: 'id',
show: false,
component: 'Input',
},
{
field: 'itemType',
label: '功能类型',
required: true,
component: 'Select',
defaultValue: 1,
componentProps: {
options: [
{ label: '属性', value: 1 },
{ label: '命令', value: 2 },
],
},
dynamicDisabled: () => isUpdate.value,
},
{
field: 'name',
label: '功能名称',
required: true,
component: 'Input',
},
{
field: 'identifier',
label: '标识符',
required: true,
component: 'Input',
},
{
field: 'dataType',
label: '数据类型',
required: true,
component: 'Select',
componentProps: {
options: Object.values(DataTypesEnum).map(value => ({ label: value, value })),
},
dynamicDisabled: () => isUpdate.value,
},
// int32 / float schemas
{
field: 'valueScope',
fields: ['min', 'max'],
label: '取值范围',
slot: 'ValueScope',
required: true,
itemProps: {
autoLink: false,
},
defaultValue: '_', // skip the required check
ifShow: ({ values }) => [DataTypesEnum.Int32, DataTypesEnum.Float].includes(values.dataType),
},
{
field: 'scale',
label: '精度',
required: true,
helpMessage: '小数位',
component: 'InputNumber',
componentProps: {
precision: 0,
},
ifShow: ({ values }) => values.dataType === DataTypesEnum.Float,
},
{
field: 'unit',
label: '单位',
component: 'Input',
ifShow: ({ values }) => [DataTypesEnum.Int32, DataTypesEnum.Float].includes(values.dataType),
},
// bool schemas
{
field: 'bool',
fields: ['trueDesc', 'falseDesc'],
label: '布尔值',
slot: 'Bool',
required: true,
itemProps: {
autoLink: false,
},
defaultValue: '_', // skip the required check
ifShow: ({ values }) => values.dataType === DataTypesEnum.Bool,
},
// text schemas
{
field: 'maxLength',
label: '数据长度',
required: true,
component: 'InputNumber',
componentProps: {
precision: 0,
},
ifShow: ({ values }) => values.dataType === DataTypesEnum.Text,
},
{
field: 'method',
label: '读写',
component: 'Select',
defaultValue: 'r',
componentProps: {
options: [
{ label: '只读', value: 'r' },
{ label: '读写', value: 'rw' },
],
},
},
{
field: 'sort',
label: '排序',
component: 'InputNumber',
componentProps: {
precision: 0,
},
},
],
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: ModelAttribute) => {
isUpdate.value = true
setFieldsValue({ ...data, ...(data.dataSpecs || {}) })
})
async function handleSubmit() {
try {
const values = await validate<ModelAttributeWithForm>()
setModalProps({ confirmLoading: true })
values.modelId = props.modelId
await (isUpdate.value ? updateModelAttribute(values) : createModelAttribute(values))
closeModal()
emit('success')
useMessage().createMessage.success('保存成功')
}
catch {}
finally {
setModalProps({ confirmLoading: false })
}
}
function validatorValueScope(value1: number, value2: number) {
// eslint-disable-next-line prefer-promise-reject-errors
return value1 > value2 ? Promise.reject() : Promise.resolve()
}
</script>
<template>
<BasicModal
v-bind="$attrs"
width="40%"
:min-height="100"
:title="isUpdate ? '编辑' : '新增'"
:after-close="() => isUpdate = false"
@register="registerModal"
@ok="handleSubmit"
>
<BasicForm @register="registerForm">
<template #ValueScope="{ model }">
<div flex="~ justify-between gap-12px" class="custom-form-slot">
<FormItem
name="min"
:rules="[
{ required: true, message: '请填写最小值' },
{ validator: (_, min) => validatorValueScope(min, model.max), message: '最小值不能大于最大值' },
]"
>
<InputNumber v-model:value="model.min" placeholder="最小值" />
</FormItem>
<span mt="5px">-</span>
<FormItem
name="max"
:rules="[
{ required: true, message: '请填写最大值' },
{ validator: (_, max) => validatorValueScope(model.min, max), message: '最大值不能小于最小值' },
]"
>
<InputNumber v-model:value="model.max" placeholder="最大值" />
</FormItem>
</div>
</template>
<template #Bool="{ model }">
<div flex="~ justify-between gap-12px" class="custom-form-slot">
<FormItem name="trueDesc" :rules="[{ required: true, message: '请填写布尔值' }]">
<Input v-model:value="model.trueDesc" addon-before="0" placeholder="如:是" />
</FormItem>
<span mt="5px">-</span>
<FormItem name="falseDesc" :rules="[{ required: true, message: '请填写布尔值' }]">
<Input v-model:value="model.falseDesc" addon-before="1" placeholder="如:否" />
</FormItem>
</div>
</template>
</BasicForm>
</BasicModal>
</template>
<style scoped lang="less">
.custom-form-slot .ant-form-item {
margin-bottom: 0;
flex: 1;
:deep(.ant-form-item-explain-error) {
position: absolute;
}
}
</style>

79
src/views/product/components/ModelServiceFormModal.vue

@ -0,0 +1,79 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createModelService, updateModelService } from '@/api/product/model'
import type { ModelService } from '@/api/product/types'
defineOptions({ name: 'ModelServiceFormModal' })
const emit = defineEmits(['success', 'register'])
const isUpdate = ref(false)
const [registerForm, { setFieldsValue, validate }] = useForm({
labelWidth: 80,
baseColProps: { span: 24 },
schemas: [
{
field: 'id',
show: false,
component: 'Input',
},
{
field: 'serviceId',
label: '服务 ID',
required: true,
component: 'Input',
componentProps: {
maxlength: 30,
showCount: true,
},
},
{
field: 'description',
label: '服务描述',
required: true,
component: 'InputTextArea',
},
],
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: ModelService) => {
isUpdate.value = true
setFieldsValue({ ...data })
})
const route = useRoute()
async function handleSubmit() {
try {
const values = await validate<ModelService>()
setModalProps({ confirmLoading: true })
values.productId = route.params.id as string
await (isUpdate.value ? updateModelService(values) : createModelService(values))
closeModal()
emit('success')
useMessage().createMessage.success('保存成功')
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal
v-bind="$attrs"
width="30%"
:min-height="100"
:title="isUpdate ? '编辑' : '新增'"
:after-close="() => isUpdate = false"
@register="registerModal"
@ok="handleSubmit"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>

73
src/views/product/components/composables/useModelAttribute.ts

@ -0,0 +1,73 @@
import { unref, watch } from 'vue'
import type { MaybeRef, Ref } from 'vue'
import { useTable } from '@/components/Table'
import { deleteModelAttribute, getModelAttributeList } from '@/api/product/model'
import { useModal } from '@/components/Modal'
import type { ModelAttribute } from '@/api/product/types'
import { useMessage } from '@/hooks/web/useMessage'
export function useModelAttribute(productId: MaybeRef<string>, modelId: Ref<string | undefined>) {
const [registerModelAttributeTable, { reload, setPagination }] = useTable({
async api(params) {
if (!unref(modelId))
return []
return getModelAttributeList({
...params,
productId: unref(productId),
modelId: unref(modelId),
})
},
columns: [
{
title: '功能类型',
dataIndex: 'itemType',
customRender: ({ value }) => ({ 1: '属性', 2: '命令' }[value]),
},
{
title: '功能名称',
dataIndex: 'name',
},
{
title: '标识符',
dataIndex: 'identifier',
},
{
title: '数据类型',
dataIndex: 'dataType',
},
],
bordered: true,
inset: true,
canResize: false,
actionColumn: {
width: 150,
title: '操作',
dataIndex: 'action',
fixed: 'right',
},
})
watch(modelId, () => {
setPagination({ current: 1 })
reload()
})
const [registerModelAttributeModal, { openModal: openModelAttributeModal }] = useModal<ModelAttribute>()
async function handleDeleteModelAttribute(id: string) {
try {
await deleteModelAttribute(id)
useMessage().createMessage.success('删除成功')
reload()
}
catch {}
}
return {
registerModelAttributeTable,
registerModelAttributeModal,
openModelAttributeModal,
reloadModalAttribute: reload,
handleDeleteModelAttribute,
}
}

39
src/views/product/components/composables/useModelService.ts

@ -0,0 +1,39 @@
import { useAsyncState, watchOnce } from '@vueuse/core'
import type { MaybeRef } from 'vue'
import { ref, unref } from 'vue'
import { deleteModelService, getAllModelServices } from '@/api/product/model'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import type { ModelService } from '@/api/product/types'
export function useModelService(productId: MaybeRef<string>) {
const selectedModelId = ref<string>()
const { state, execute } = useAsyncState(() => getAllModelServices(unref(productId)), [], { resetOnExecute: false })
watchOnce(state, () => {
if (state.value.length > 0)
selectedModelId.value = state.value[0].id
})
const [registerModelServiceModal, { openModal }] = useModal<ModelService>()
async function handleDeleteModelService(id: string) {
try {
await deleteModelService(id)
useMessage().createMessage.success('删除成功')
execute()
}
catch {}
}
return {
selectedModelId,
setSelectedModelId: (id: string) => selectedModelId.value = id,
reloadModelService: execute,
modelServiceList: state,
handleDeleteModelService,
registerModelServiceModal,
openModelServiceModal: openModal,
}
}

1
src/views/product/components/index.ts

@ -1 +1,2 @@
export { default as TopicManage } from './TopicManage.vue'
export { default as Model } from './Model.vue'

4
src/views/product/detail.vue

@ -2,7 +2,7 @@
import { Card, Tabs } from 'ant-design-vue'
import { useAsyncState } from '@vueuse/core'
import { useRoute } from 'vue-router'
import { TopicManage } from './components'
import { Model, TopicManage } from './components'
import { Description } from '@/components/Description'
import { getProductDetail } from '@/api/product'
import type { DescItem } from '@/components/Description'
@ -78,7 +78,7 @@ const schema: DescItem[] = [
<TopicManage />
</Tabs.TabPane>
<Tabs.TabPane key="2" tab="物模型">
物模型
<Model :tsl="state?.tsl" />
</Tabs.TabPane>
<Tabs.TabPane key="3" tab="消息解析">
消息解析

Loading…
Cancel
Save