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

View File

@ -3,12 +3,16 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { useRouter } from 'vue-router';
import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui';
import { ProductStatusEnum } from '@vben/constants';
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';
@ -26,6 +30,7 @@ const emit = defineEmits<{
}>();
const router = useRouter();
const { hasAccessByCodes } = useAccess();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
@ -44,8 +49,9 @@ async function copyToClipboard(text: string) {
/** 跳转到设备管理 */
function goToDeviceList(productId: number) {
// TODO @AIvben 使 name
router.push({
path: '/iot/device/device',
name: 'IoTDevice',
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>
<template>
@ -92,25 +110,38 @@ function handleUnpublish(product: IotProductApi.Product) {
</div>
<div class="flex gap-2">
<Button
v-if="hasAccessByCodes(['iot:product:update'])"
:disabled="product.status === ProductStatusEnum.PUBLISHED"
@click="openEditForm(product)"
>
编辑
</Button>
<Button
v-if="product.status === ProductStatusEnum.UNPUBLISHED"
v-if="
product.status === ProductStatusEnum.UNPUBLISHED &&
hasAccessByCodes(['iot:product:update'])
"
type="primary"
@click="handlePublish(product)"
>
发布
</Button>
<Button
v-if="product.status === ProductStatusEnum.PUBLISHED"
v-if="
product.status === ProductStatusEnum.PUBLISHED &&
hasAccessByCodes(['iot:product:update'])
"
danger
@click="handleUnpublish(product)"
>
撤销发布
</Button>
<Button
v-if="hasAccessByCodes(['iot:product:update'])"
@click="handleSyncPropertyTable(product)"
>
同步物模型表结构
</Button>
</div>
</div>

View File

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

View File

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