feat(iot):增加 product/product 模块的代码评审

pull/345/head
YunaiV 2026-05-18 08:54:44 +08:00
parent 179881bd3d
commit 89f75428d6
4 changed files with 92 additions and 39 deletions

View File

@ -1,6 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category';
import { h } from 'vue'; import { h } from 'vue';
@ -12,10 +11,6 @@ import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getSimpleProductCategoryList } from '#/api/iot/product/category'; import { getSimpleProductCategoryList } from '#/api/iot/product/category';
/** 产品分类列表缓存 */
let categoryList: IotProductCategoryApi.ProductCategory[] = [];
getSimpleProductCategoryList().then((data) => (categoryList = data));
/** 基础表单字段(不含图标、图片、描述) */ /** 基础表单字段(不含图标、图片、描述) */
export function useBasicFormSchema( export function useBasicFormSchema(
formApi?: any, formApi?: any,
@ -114,6 +109,15 @@ export function useBasicFormSchema(
buttonStyle: 'solid', buttonStyle: 'solid',
optionType: 'button', optionType: 'button',
}, },
// TODO @AI枚举值。或者这里不要枚举值对齐 vue3 + ep 版本
defaultValue: 0,
dependencies: {
triggerFields: ['id'],
componentProps: (values) => ({
// 编辑时设备类型不可改
disabled: !!values.id,
}),
},
rules: 'required', rules: 'required',
}, },
{ {
@ -124,6 +128,12 @@ export function useBasicFormSchema(
options: getDictOptions(DICT_TYPE.IOT_NET_TYPE, 'number'), options: getDictOptions(DICT_TYPE.IOT_NET_TYPE, 'number'),
placeholder: '请选择联网方式', placeholder: '请选择联网方式',
}, },
// 网关子设备走网关联网,不需要联网方式
dependencies: {
triggerFields: ['deviceType'],
// TODO @AI枚举值。或者这里不要枚举值也看看 vben 里,其它是不是也漏了枚举值。)
show: (values) => values.deviceType !== 2,
},
rules: 'required', rules: 'required',
}, },
{ {
@ -134,6 +144,7 @@ export function useBasicFormSchema(
options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'string'), options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'string'),
placeholder: '请选择协议类型', placeholder: '请选择协议类型',
}, },
defaultValue: 'mqtt',
rules: 'required', rules: 'required',
}, },
{ {
@ -144,6 +155,7 @@ export function useBasicFormSchema(
options: getDictOptions(DICT_TYPE.IOT_SERIALIZE_TYPE, 'string'), options: getDictOptions(DICT_TYPE.IOT_SERIALIZE_TYPE, 'string'),
placeholder: '请选择序列化类型', placeholder: '请选择序列化类型',
}, },
defaultValue: 'json',
help: 'iot-gateway-server 默认根据接入的协议类型确定数据格式,仅 MQTT、EMQX 协议支持自定义序列化类型', help: 'iot-gateway-server 默认根据接入的协议类型确定数据格式,仅 MQTT、EMQX 协议支持自定义序列化类型',
rules: 'required', rules: 'required',
}, },
@ -167,11 +179,7 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
{ {
fieldName: 'icon', fieldName: 'icon',
label: '产品图标', label: '产品图标',
component: 'IconPicker', component: 'ImageUpload',
componentProps: {
placeholder: '请选择产品图标',
prefix: 'carbon',
},
}, },
{ {
fieldName: 'picUrl', fieldName: 'picUrl',
@ -204,11 +212,10 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
minWidth: 150, minWidth: 150,
}, },
{ {
field: 'categoryId', field: 'categoryName',
title: '品类', title: '品类',
minWidth: 120, minWidth: 120,
formatter: ({ cellValue }) => formatter: ({ row }) => row.categoryName || '未分类',
categoryList.find((c) => c.id === cellValue)?.name || '未分类',
}, },
{ {
field: 'deviceType', field: 'deviceType',

View File

@ -3,12 +3,16 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { ProductStatusEnum } from '@vben/constants'; import { ProductStatusEnum } from '@vben/constants';
import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue'; import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue';
import { updateProductStatus } from '#/api/iot/product/product'; import {
syncProductPropertyTable,
updateProductStatus,
} from '#/api/iot/product/product';
import Form from '../../modules/form.vue'; import Form from '../../modules/form.vue';
@ -26,6 +30,7 @@ const emit = defineEmits<{
}>(); }>();
const router = useRouter(); const router = useRouter();
const { hasAccessByCodes } = useAccess();
const [FormModal, formModalApi] = useVbenModal({ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form, connectedComponent: Form,
@ -44,8 +49,9 @@ async function copyToClipboard(text: string) {
/** 跳转到设备管理 */ /** 跳转到设备管理 */
function goToDeviceList(productId: number) { function goToDeviceList(productId: number) {
// TODO @AIvben 使 name
router.push({ router.push({
path: '/iot/device/device', name: 'IoTDevice',
query: { productId: String(productId) }, query: { productId: String(productId) },
}); });
} }
@ -80,6 +86,18 @@ function handleUnpublish(product: IotProductApi.Product) {
}, },
}); });
} }
/** 同步物模型超级表结构 */
function handleSyncPropertyTable(product: IotProductApi.Product) {
Modal.confirm({
title: '确认同步',
content: `确认要同步产品「${product.name}」的物模型超级表结构吗?`,
async onOk() {
await syncProductPropertyTable(product.id!);
message.success('同步成功');
},
});
}
</script> </script>
<template> <template>
@ -92,25 +110,38 @@ function handleUnpublish(product: IotProductApi.Product) {
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
v-if="hasAccessByCodes(['iot:product:update'])"
:disabled="product.status === ProductStatusEnum.PUBLISHED" :disabled="product.status === ProductStatusEnum.PUBLISHED"
@click="openEditForm(product)" @click="openEditForm(product)"
> >
编辑 编辑
</Button> </Button>
<Button <Button
v-if="product.status === ProductStatusEnum.UNPUBLISHED" v-if="
product.status === ProductStatusEnum.UNPUBLISHED &&
hasAccessByCodes(['iot:product:update'])
"
type="primary" type="primary"
@click="handlePublish(product)" @click="handlePublish(product)"
> >
发布 发布
</Button> </Button>
<Button <Button
v-if="product.status === ProductStatusEnum.PUBLISHED" v-if="
product.status === ProductStatusEnum.PUBLISHED &&
hasAccessByCodes(['iot:product:update'])
"
danger danger
@click="handleUnpublish(product)" @click="handleUnpublish(product)"
> >
撤销发布 撤销发布
</Button> </Button>
<Button
v-if="hasAccessByCodes(['iot:product:update'])"
@click="handleSyncPropertyTable(product)"
>
同步物模型表结构
</Button>
</div> </div>
</div> </div>

View File

@ -217,12 +217,14 @@ onMounted(() => {
label: $t('ui.actionTitle.create', ['产品']), label: $t('ui.actionTitle.create', ['产品']),
type: 'primary', type: 'primary',
icon: ACTION_ICON.ADD, icon: ACTION_ICON.ADD,
auth: ['iot:product:create'],
onClick: handleCreate, onClick: handleCreate,
}, },
{ {
label: $t('ui.actionTitle.export'), label: $t('ui.actionTitle.export'),
type: 'primary', type: 'primary',
icon: ACTION_ICON.DOWNLOAD, icon: ACTION_ICON.DOWNLOAD,
auth: ['iot:product:export'],
onClick: handleExport, onClick: handleExport,
}, },
]" ]"
@ -252,17 +254,20 @@ onMounted(() => {
{ {
label: $t('common.detail'), label: $t('common.detail'),
type: 'link', type: 'link',
auth: ['iot:product:query'],
onClick: openProductDetail.bind(null, row.id!), onClick: openProductDetail.bind(null, row.id!),
}, },
{ {
label: '物模型', label: '物模型',
type: 'link', type: 'link',
auth: ['iot:thing-model:query'],
onClick: openThingModel.bind(null, row.id!), onClick: openThingModel.bind(null, row.id!),
}, },
{ {
label: $t('common.edit'), label: $t('common.edit'),
type: 'link', type: 'link',
icon: ACTION_ICON.EDIT, icon: ACTION_ICON.EDIT,
auth: ['iot:product:update'],
onClick: handleEdit.bind(null, row), onClick: handleEdit.bind(null, row),
}, },
{ {
@ -270,6 +275,7 @@ onMounted(() => {
type: 'link', type: 'link',
danger: true, danger: true,
icon: ACTION_ICON.DELETE, icon: ACTION_ICON.DELETE,
auth: ['iot:product:delete'],
disabled: row.status === ProductStatusEnum.PUBLISHED, disabled: row.status === ProductStatusEnum.PUBLISHED,
popConfirm: { popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]), title: $t('ui.actionMessage.deleteConfirm', [row.name]),

View File

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useAccess } from '@vben/access';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
@ -37,6 +38,8 @@ const emit = defineEmits<{
thingModel: [productId: number]; thingModel: [productId: number];
}>(); }>();
const { hasAccessByCodes } = useAccess();
const loading = ref(false); const loading = ref(false);
const list = ref<any[]>([]); const list = ref<any[]>([]);
const total = ref(0); const total = ref(0);
@ -160,6 +163,7 @@ onMounted(() => {
<!-- 按钮组 --> <!-- 按钮组 -->
<div class="action-buttons"> <div class="action-buttons">
<Button <Button
v-if="hasAccessByCodes(['iot:product:update'])"
size="small" size="small"
class="action-btn action-btn-edit" class="action-btn action-btn-edit"
@click="emit('edit', item)" @click="emit('edit', item)"
@ -168,6 +172,7 @@ onMounted(() => {
编辑 编辑
</Button> </Button>
<Button <Button
v-if="hasAccessByCodes(['iot:product:query'])"
size="small" size="small"
class="action-btn action-btn-detail" class="action-btn action-btn-detail"
@click="emit('detail', item.id)" @click="emit('detail', item.id)"
@ -176,6 +181,7 @@ onMounted(() => {
详情 详情
</Button> </Button>
<Button <Button
v-if="hasAccessByCodes(['iot:thing-model:query'])"
size="small" size="small"
class="action-btn action-btn-model" class="action-btn action-btn-model"
@click="emit('thingModel', item.id)" @click="emit('thingModel', item.id)"
@ -183,29 +189,32 @@ onMounted(() => {
<IconifyIcon icon="lucide:git-branch" class="mr-1" /> <IconifyIcon icon="lucide:git-branch" class="mr-1" />
物模型 物模型
</Button> </Button>
<Tooltip v-if="item.status === 1" title="已发布的产品不能删除"> <template v-if="hasAccessByCodes(['iot:product:delete'])">
<Button <!-- TODO @AI使用枚举 -->
size="small" <Tooltip v-if="item.status === 1" title="已发布的产品不能删除">
danger <Button
disabled size="small"
class="action-btn action-btn-delete !w-8" danger
disabled
class="action-btn action-btn-delete !w-8"
>
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
</Button>
</Tooltip>
<Popconfirm
v-else
:title="`确认删除产品 ${item.name} 吗?`"
@confirm="emit('delete', item)"
> >
<IconifyIcon icon="lucide:trash-2" class="text-sm" /> <Button
</Button> size="small"
</Tooltip> danger
<Popconfirm class="action-btn action-btn-delete !w-8"
v-else >
:title="`确认删除产品 ${item.name} 吗?`" <IconifyIcon icon="lucide:trash-2" class="text-sm" />
@confirm="emit('delete', item)" </Button>
> </Popconfirm>
<Button </template>
size="small"
danger
class="action-btn action-btn-delete !w-8"
>
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
</Button>
</Popconfirm>
</div> </div>
</Card> </Card>
</Col> </Col>