10 changed files with 677 additions and 3 deletions
@ -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}`, |
||||
}) |
||||
} |
@ -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> |
@ -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> |
@ -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> |
@ -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, |
||||
} |
||||
} |
@ -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 +1,2 @@
|
||||
export { default as TopicManage } from './TopicManage.vue' |
||||
export { default as Model } from './Model.vue' |
||||
|
Loading…
Reference in new issue