refactor:【antd】【iot】代码优化
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
// 编辑模式:加载数据
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || '')"
|
||||
>
|
||||
复制
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user