refactor:【antd】【iot】代码优化

This commit is contained in:
haohao
2025-12-22 17:30:59 +08:00
parent 13f81b3130
commit 6bf9acbfb2
26 changed files with 178 additions and 529 deletions

View File

@@ -38,8 +38,10 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入分类排序',
class: 'w-full',
min: 0,
precision: 0,
},
rules: z.number().min(0, '分类排序不能为空'),
defaultValue: 0,
rules: z.number().min(0, '分类排序不能小于 0'),
},
{
fieldName: 'status',

View File

@@ -68,13 +68,6 @@ const [Modal, modalApi] = useVbenModal({
// 加载数据
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
if (!data || !data.id) {
// 新增模式:设置默认值
// TODO @AI可以参考部门进一步简化代码通过 defaultValue 在 schema 里设置默认值
formData.value = undefined;
await formApi.setValues({
sort: 0,
status: 1,
});
return;
}
// 编辑模式:加载数据

View File

@@ -16,161 +16,6 @@ import { getSimpleProductCategoryList } from '#/api/iot/product/category';
let categoryList: IotProductCategoryApi.ProductCategory[] = [];
getSimpleProductCategoryList().then((data) => (categoryList = data));
/** 新增/修改产品的表单 */
export function useFormSchema(
formApi?: any,
generateProductKey?: () => string,
): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'productKey',
label: 'ProductKey',
component: 'Input',
componentProps: {
placeholder: '请输入 ProductKey',
},
dependencies: {
triggerFields: ['id'],
if(values) {
return !values.id;
},
},
rules: z
.string()
.min(1, 'ProductKey 不能为空')
.max(32, 'ProductKey 长度不能超过 32 个字符'),
suffix: () => {
// 创建时的 ProductKey 字段(带生成按钮)
return h(
Button,
{
type: 'default',
onClick: () => {
if (generateProductKey) {
formApi?.setFieldValue('productKey', generateProductKey());
}
},
},
{ default: () => '重新生成' },
);
},
},
{
fieldName: 'productKey',
label: 'ProductKey',
component: 'Input',
componentProps: {
placeholder: '请输入 ProductKey',
disabled: true, // 编辑时的 ProductKey 字段(禁用,无按钮)
},
dependencies: {
triggerFields: ['id'],
if(values) {
return !!values.id;
},
},
rules: z
.string()
.min(1, 'ProductKey 不能为空')
.max(32, 'ProductKey 长度不能超过 32 个字符'),
},
{
fieldName: 'name',
label: '产品名称',
component: 'Input',
componentProps: {
placeholder: '请输入产品名称',
},
rules: z
.string()
.min(1, '产品名称不能为空')
.max(64, '产品名称长度不能超过 64 个字符'),
},
{
fieldName: 'categoryId',
label: '产品分类',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductCategoryList,
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品分类',
},
rules: 'required',
},
{
fieldName: 'deviceType',
label: '设备类型',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: 'required',
},
{
fieldName: 'netType',
label: '联网方式',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_NET_TYPE, 'number'),
placeholder: '请选择联网方式',
},
rules: 'required',
},
{
fieldName: 'codecType',
label: '数据格式',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_CODEC_TYPE, 'string'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: 'required',
},
{
fieldName: 'status',
label: '产品状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: 'required',
},
{
fieldName: 'icon',
label: '产品图标',
component: 'ImageUpload',
},
{
fieldName: 'picUrl',
label: '产品图片',
component: 'ImageUpload',
},
{
fieldName: 'description',
label: '产品描述',
component: 'Textarea',
componentProps: {
placeholder: '请输入产品描述',
rows: 3,
},
},
];
}
/** 基础表单字段(不含图标、图片、描述) */
export function useBasicFormSchema(
formApi?: any,

View File

@@ -119,8 +119,8 @@ function handleUnpublish(product: IotProductApi.Product) {
<Descriptions.Item label="ProductKey">
{{ product.productKey }}
<Button
size="small"
class="ml-2"
size="small"
@click="copyToClipboard(product.productKey || '')"
>
复制

View File

@@ -22,7 +22,7 @@ function formatDate(date?: Date | string) {
<template>
<Card title="产品信息">
<Descriptions bordered :column="3" size="small">
<Descriptions :column="3" bordered size="small">
<Descriptions.Item label="产品名称">
{{ product.name }}
</Descriptions.Item>
@@ -57,7 +57,7 @@ function formatDate(date?: Date | string) {
>
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
</Descriptions.Item>
<Descriptions.Item label="产品描述" :span="3">
<Descriptions.Item :span="3" label="产品描述">
{{ product.description || '-' }}
</Descriptions.Item>
</Descriptions>

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category';
import type { IotProductApi } from '#/api/iot/product/product';
@@ -107,19 +107,19 @@ function handleCreate() {
}
/** 编辑产品 */
function handleEdit(row: any) {
function handleEdit(row: IotProductApi.Product) {
formModalApi.setData(row).open();
}
/** 删除产品 */
async function handleDelete(row: any) {
async function handleDelete(row: IotProductApi.Product) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteProduct(row.id!);
message.success($t('ui.actionMessage.deleteSuccess'));
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
@@ -153,7 +153,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
} as VxeTableGridOptions<IotProductApi.Product>,
});
// 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口
/** 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口 */
const originalQuery = gridApi.query.bind(gridApi);
gridApi.query = async (params?: Record<string, any>) => {
if (viewMode.value === 'list') {
@@ -180,9 +180,9 @@ onMounted(() => {
<div class="mb-3 flex items-center gap-3">
<Input
v-model:value="queryParams.name"
placeholder="请输入产品名称"
allow-clear
class="w-[220px]"
placeholder="请输入产品名称"
@press-enter="handleSearch"
>
<template #prefix>
@@ -191,9 +191,9 @@ onMounted(() => {
</Input>
<Input
v-model:value="queryParams.productKey"
placeholder="请输入产品标识"
allow-clear
class="w-[220px]"
placeholder="请输入产品标识"
@press-enter="handleSearch"
>
<template #prefix>
@@ -201,11 +201,11 @@ onMounted(() => {
</template>
</Input>
<Button type="primary" @click="handleSearch">
<IconifyIcon icon="ant-design:search-outlined" class="mr-1" />
<IconifyIcon class="mr-1" icon="ant-design:search-outlined" />
搜索
</Button>
<Button @click="handleReset">
<IconifyIcon icon="ant-design:reload-outlined" class="mr-1" />
<IconifyIcon class="mr-1" icon="ant-design:reload-outlined" />
重置
</Button>
</div>
@@ -214,13 +214,13 @@ onMounted(() => {
<TableAction
:actions="[
{
label: '新增产品',
label: $t('ui.actionTitle.create', ['产品']),
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
{
label: '导出',
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
onClick: handleExport,
@@ -245,12 +245,12 @@ onMounted(() => {
</div>
</Card>
<Grid table-title="产品列表" v-show="viewMode === 'list'">
<Grid v-show="viewMode === 'list'" table-title="产品列表">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '详情',
label: $t('common.detail'),
type: 'link',
onClick: openProductDetail.bind(null, row.id!),
},
@@ -272,7 +272,7 @@ onMounted(() => {
icon: ACTION_ICON.DELETE,
disabled: row.status === ProductStatusEnum.PUBLISHED,
popConfirm: {
title: `确认删除产品 ${row.name} 吗?`,
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
@@ -288,16 +288,10 @@ onMounted(() => {
:category-list="categoryList"
:search-params="queryParams"
@create="handleCreate"
@edit="handleEdit"
@delete="handleDelete"
@detail="openProductDetail"
@edit="handleEdit"
@thing-model="openThingModel"
/>
</Page>
</template>
<style scoped>
/* 隐藏 VxeGrid 自带的搜索表单区域 */
:deep(.vxe-grid--form-wrapper) {
display: none !important;
}
</style>

View File

@@ -2,7 +2,6 @@
import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import {
@@ -14,10 +13,10 @@ import {
Pagination,
Popconfirm,
Row,
Tag,
Tooltip,
} from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag';
import { getProductPage } from '#/api/iot/product/product';
interface Props {
@@ -74,15 +73,6 @@ function handlePageChange(page: number, pageSize: number) {
getList();
}
/** 获取设备类型颜色 */
function getDeviceTypeColor(deviceType: number) {
const colors: Record<number, string> = {
0: 'blue',
1: 'green',
};
return colors[deviceType] || 'default';
}
defineExpose({
reload: getList,
query: () => {
@@ -137,18 +127,11 @@ onMounted(() => {
</div>
<div class="info-item">
<span class="info-label">产品类型</span>
<!-- TODO @AI这个要不完全用字典的 dict-tag -->
<Tag
:color="getDeviceTypeColor(item.deviceType)"
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
:value="item.deviceType"
class="info-tag m-0"
>
{{
getDictLabel(
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
item.deviceType,
)
}}
</Tag>
/>
</div>
<div class="info-item">
<span class="info-label">产品标识</span>
@@ -267,8 +250,7 @@ onMounted(() => {
width: 36px;
height: 36px;
color: white;
// TODO @haohao这里的紫色和下面的紫色按钮看看能不能换下。嘿嘿感觉 AI 比较喜欢用紫色,但是放现有的后台,有点突兀
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
border-radius: 8px;
}
@@ -337,8 +319,8 @@ onMounted(() => {
justify-content: center;
width: 80px;
height: 80px;
color: #667eea;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
color: #1890ff;
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
border-radius: 8px;
}
@@ -378,12 +360,12 @@ onMounted(() => {
}
&.action-btn-model {
color: #722ed1;
border-color: #722ed1;
color: #fa8c16;
border-color: #fa8c16;
&:hover {
color: white;
background: #722ed1;
background: #fa8c16;
}
}
@@ -419,8 +401,8 @@ html.dark {
}
.product-image {
color: #8b9cff;
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
color: #69c0ff;
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
}
}
}

View File

@@ -106,7 +106,8 @@ const [Modal, modalApi] = useVbenModal({
// 加载数据
const data = modalApi.getData<IotProductApi.Product>();
if (!data || !data.id) {
// 新增:设置默认值status 通过 schema 中的 defaultValue 自动设置为 0
// 新增:确保 Collapse 折叠,并设置默认值
activeKey.value = [];
await formApi.setValues({
productKey: generateProductKey(),
});
@@ -117,10 +118,19 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = await getProduct(data.id);
await formApi.setValues(formData.value);
// 设置高级表单(如果已挂载)
await nextTick();
if (advancedFormApi.isMounted) {
await advancedFormApi.setValues(formData.value);
// 如果存在高级字段数据,自动展开 Collapse
if (
formData.value?.icon ||
formData.value?.picUrl ||
formData.value?.description
) {
activeKey.value = ['advanced'];
// 等待 Collapse 展开后表单挂载
await nextTick();
await nextTick();
if (advancedFormApi.isMounted) {
await advancedFormApi.setValues(formData.value);
}
}
} finally {
modalApi.unlock();