!290 refactor:【antd】【iot】产品管理问题修复

Merge pull request !290 from haohaoMT/dev
This commit is contained in:
xingyu
2025-12-04 03:07:26 +00:00
committed by Gitee
14 changed files with 316 additions and 703 deletions

View File

@@ -48,6 +48,12 @@ export enum CodecTypeEnum {
ALINK = 'Alink', // 阿里云 Alink 协议 ALINK = 'Alink', // 阿里云 Alink 协议
} }
/** IOT 产品状态枚举类 */
export enum ProductStatusEnum {
UNPUBLISHED = 0, // 开发中
PUBLISHED = 1, // 已发布
}
/** 查询产品分页 */ /** 查询产品分页 */
export function getProductPage(params: PageParam) { export function getProductPage(params: PageParam) {
return requestClient.get<PageResult<IotProductApi.Product>>( return requestClient.get<PageResult<IotProductApi.Product>>(

View File

@@ -11,12 +11,12 @@ import { deleteDeviceGroup, getDeviceGroupPage } from '#/api/iot/device/group';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
import DeviceGroupForm from './modules/device-group-form.vue'; import Form from './modules/form.vue';
defineOptions({ name: 'IoTDeviceGroup' }); defineOptions({ name: 'IoTDeviceGroup' });
const [FormModal, formModalApi] = useVbenModal({ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: DeviceGroupForm, connectedComponent: Form,
destroyOnClose: true, destroyOnClose: true,
}); });

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import type { IotDeviceGroupApi } from '#/api/iot/device/group'; import type { IotDeviceGroupApi } from '#/api/iot/device/group';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@@ -17,17 +17,9 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
defineOptions({ name: 'IoTDeviceGroupForm' }); const emit = defineEmits(['success']);
// TODO @haohaoweb-antd/src/views/iot/product/category/modules/product-category-form.vue
const emit = defineEmits<{
success: [];
}>();
const formData = ref<IotDeviceGroupApi.DeviceGroup>(); const formData = ref<IotDeviceGroupApi.DeviceGroup>();
const getTitle = computed(() => {
const modalTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['设备分组']) ? $t('ui.actionTitle.edit', ['设备分组'])
: $t('ui.actionTitle.create', ['设备分组']); : $t('ui.actionTitle.create', ['设备分组']);
@@ -40,11 +32,9 @@ const [Form, formApi] = useVbenForm({
}, },
}, },
schema: useFormSchema(), schema: useFormSchema(),
showCollapseButton: false,
showDefaultActions: false, showDefaultActions: false,
}); });
// TODO @haohao form12
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
@@ -52,17 +42,13 @@ const [Modal, modalApi] = useVbenModal({
return; return;
} }
modalApi.lock(); modalApi.lock();
//
const data = (await formApi.getValues()) as IotDeviceGroupApi.DeviceGroup;
try { try {
const values = await formApi.getValues();
await (formData.value?.id await (formData.value?.id
? updateDeviceGroup({ ? updateDeviceGroup(data)
...values, : createDeviceGroup(data));
id: formData.value.id, //
} as IotDeviceGroupApi.DeviceGroup)
: createDeviceGroup(values as IotDeviceGroupApi.DeviceGroup));
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
message.success($t('ui.actionMessage.operationSuccess')); message.success($t('ui.actionMessage.operationSuccess'));
@@ -70,28 +56,20 @@ const [Modal, modalApi] = useVbenModal({
modalApi.unlock(); modalApi.unlock();
} }
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = undefined;
await formApi.resetForm();
return; return;
} }
//
//
await formApi.resetForm();
const data = modalApi.getData<IotDeviceGroupApi.DeviceGroup>(); const data = modalApi.getData<IotDeviceGroupApi.DeviceGroup>();
// id
if (!data || !data.id) { if (!data || !data.id) {
formData.value = undefined;
return; return;
} }
//
modalApi.lock(); modalApi.lock();
try { try {
formData.value = await getDeviceGroup(data.id); formData.value = await getDeviceGroup(data.id);
// values
await formApi.setValues(formData.value); await formApi.setValues(formData.value);
} finally { } finally {
modalApi.unlock(); modalApi.unlock();
@@ -101,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal :title="modalTitle" class="w-2/5"> <Modal :title="getTitle">
<Form class="mx-4" /> <Form class="mx-4" />
</Modal> </Modal>
</template> </template>

View File

@@ -14,12 +14,12 @@ import {
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
import ProductCategoryForm from './modules/product-category-form.vue'; import Form from './modules/form.vue';
defineOptions({ name: 'IoTProductCategory' }); defineOptions({ name: 'IoTProductCategory' });
const [FormModal, formModalApi] = useVbenModal({ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ProductCategoryForm, connectedComponent: Form,
destroyOnClose: true, destroyOnClose: true,
}); });

View File

@@ -17,8 +17,6 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
// TODO @haohao form.vue
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<IotProductCategoryApi.ProductCategory>(); const formData = ref<IotProductCategoryApi.ProductCategory>();
const getTitle = computed(() => { const getTitle = computed(() => {
@@ -40,7 +38,6 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false, showDefaultActions: false,
}); });
// TODO @haohao apps/web-antd/src/views/system/dept/modules/form.vue useVbenModal
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
@@ -66,25 +63,19 @@ const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = undefined;
formApi.resetForm();
return; return;
} }
//
//
await formApi.resetForm();
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>(); const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
// id
if (!data || !data.id) { if (!data || !data.id) {
formData.value = undefined;
// //
formData.value = undefined;
await formApi.setValues({ await formApi.setValues({
sort: 0, sort: 0,
status: 1, status: 1,
}); });
return; return;
} }
// //
modalApi.lock(); modalApi.lock();
try { try {

View File

@@ -1,7 +1,8 @@
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, ref } from 'vue'; import { h } from 'vue';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
@@ -10,7 +11,17 @@ 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';
import { getProductPage } from '#/api/iot/product/product';
/** 产品分类列表缓存 */
let categoryList: IotProductCategoryApi.ProductCategory[] = [];
/** 加载产品分类数据 */
async function loadCategoryData() {
categoryList = await getSimpleProductCategoryList();
}
// 初始化加载分类数据
loadCategoryData();
/** 新增/修改产品的表单 */ /** 新增/修改产品的表单 */
export function useFormSchema(formApi?: any): VbenFormSchema[] { export function useFormSchema(formApi?: any): VbenFormSchema[] {
@@ -134,7 +145,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
label: '产品状态', label: '产品状态',
component: 'RadioGroup', component: 'RadioGroup',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
buttonStyle: 'solid', buttonStyle: 'solid',
optionType: 'button', optionType: 'button',
}, },
@@ -283,7 +294,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
label: '产品状态', label: '产品状态',
component: 'RadioGroup', component: 'RadioGroup',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
buttonStyle: 'solid', buttonStyle: 'solid',
optionType: 'button', optionType: 'button',
}, },
@@ -308,11 +319,10 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
{ {
fieldName: 'icon', fieldName: 'icon',
label: '产品图标', label: '产品图标',
component: 'IconPicker', // 用这个组件 产品卡片列表 可以根据这个显示 否则就显示默认的 component: 'IconPicker',
componentProps: { componentProps: {
placeholder: '请选择产品图标', placeholder: '请选择产品图标',
prefix: 'carbon', prefix: 'carbon',
autoFetchApi: false,
}, },
}, },
{ {
@@ -333,31 +343,6 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
]; ];
} }
/** 列表的搜索表单 */
// TODO @haohao貌似用不上
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '产品名称',
component: 'Input',
componentProps: {
placeholder: '请输入产品名称',
allowClear: true,
},
},
{
fieldName: 'productKey',
label: 'ProductKey',
component: 'Input',
componentProps: {
placeholder: '请输入产品标识',
allowClear: true,
},
},
];
}
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] { export function useGridColumns(): VxeTableGridOptions['columns'] {
return [ return [
@@ -375,7 +360,8 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'categoryId', field: 'categoryId',
title: '品类', title: '品类',
minWidth: 120, minWidth: 120,
slots: { default: 'category' }, formatter: ({ cellValue }) =>
categoryList.find((c) => c.id === cellValue)?.name || '未分类',
}, },
{ {
field: 'deviceType', field: 'deviceType',
@@ -390,13 +376,17 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'icon', field: 'icon',
title: '产品图标', title: '产品图标',
width: 100, width: 100,
slots: { default: 'icon' }, cellRender: {
name: 'CellImage',
},
}, },
{ {
field: 'picUrl', field: 'picUrl',
title: '产品图片', title: '产品图片',
width: 100, width: 100,
slots: { default: 'picUrl' }, cellRender: {
name: 'CellImage',
},
}, },
{ {
field: 'createTime', field: 'createTime',
@@ -413,35 +403,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
]; ];
} }
/** 查询产品列表 */
// TODO @haohao貌似可以删除
export async function queryProductList({ page }: any, searchParams: any) {
return await getProductPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...searchParams,
});
}
/** 创建图片预览状态 */
// TODO @haohao可能不一定用的上
export function useImagePreview() {
const previewVisible = ref(false);
const previewImage = ref('');
function handlePreviewImage(url: string) {
previewImage.value = url;
previewVisible.value = true;
}
return {
previewVisible,
previewImage,
handlePreviewImage,
};
}
// TODO @haohao放到对应的 form 里
/** 生成 ProductKey包含大小写字母和数字 */ /** 生成 ProductKey包含大小写字母和数字 */
export function generateProductKey(): string { export function generateProductKey(): string {
const chars = const chars =

View File

@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category';
import type { IotProductApi } from '#/api/iot/product/product';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -8,7 +10,7 @@ import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, Card, Image, Input, message, Space } from 'ant-design-vue'; import { Button, Card, Input, message, Space } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleProductCategoryList } from '#/api/iot/product/category'; import { getSimpleProductCategoryList } from '#/api/iot/product/category';
@@ -19,14 +21,14 @@ import {
} from '#/api/iot/product/product'; } from '#/api/iot/product/product';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useImagePreview } from './data'; import { useGridColumns } from './data';
import ProductCardView from './modules/product-card-view.vue'; import ProductCardView from './modules/card-view.vue';
import ProductForm from './modules/product-form.vue'; import Form from './modules/form.vue';
defineOptions({ name: 'IoTProduct' }); defineOptions({ name: 'IoTProduct' });
const router = useRouter(); const router = useRouter();
const categoryList = ref<any[]>([]); // TODO @haohaocategory 类型 const categoryList = ref<IotProductCategoryApi.ProductCategory[]>([]);
const viewMode = ref<'card' | 'list'>('card'); const viewMode = ref<'card' | 'list'>('card');
const cardViewRef = ref(); const cardViewRef = ref();
const searchParams = ref({ const searchParams = ref({
@@ -34,10 +36,8 @@ const searchParams = ref({
productKey: '', productKey: '',
}); // 搜索参数 }); // 搜索参数
const { previewVisible, previewImage, handlePreviewImage } = useImagePreview();
const [FormModal, formModalApi] = useVbenModal({ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ProductForm, connectedComponent: Form,
destroyOnClose: true, destroyOnClose: true,
}); });
@@ -46,13 +46,6 @@ async function loadCategories() {
categoryList.value = await getSimpleProductCategoryList(); categoryList.value = await getSimpleProductCategoryList();
} }
/** 获取分类名称 */
function getCategoryNameByValue(categoryId: number) {
const category = categoryList.value.find((c: any) => c.id === categoryId);
return category?.name || '未分类';
}
// TODO @haohao要不要改成 handleRefresh注释改成“刷新表格”更加统一。
/** 搜索产品 */ /** 搜索产品 */
function handleSearch() { function handleSearch() {
if (viewMode.value === 'list') { if (viewMode.value === 'list') {
@@ -128,10 +121,6 @@ async function handleDelete(row: any) {
} }
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
// TODO @haohao这个不用可以删除掉的
formOptions: {
schema: [],
},
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useGridColumns(),
height: 'auto', height: 'auto',
@@ -155,7 +144,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions, // TODO @haohao这里有个 <> 泛型 } as VxeTableGridOptions<IotProductApi.Product>,
}); });
/** 初始化 */ /** 初始化 */
@@ -172,24 +161,22 @@ onMounted(() => {
<Card :body-style="{ padding: '16px' }" class="mb-4"> <Card :body-style="{ padding: '16px' }" class="mb-4">
<!-- 搜索表单 --> <!-- 搜索表单 -->
<div class="mb-3 flex items-center gap-3"> <div class="mb-3 flex items-center gap-3">
<!-- TODO @haohaotindwind -->
<Input <Input
v-model:value="searchParams.name" v-model:value="searchParams.name"
placeholder="请输入产品名称" placeholder="请输入产品名称"
allow-clear allow-clear
style="width: 220px" class="w-[220px]"
@press-enter="handleSearch" @press-enter="handleSearch"
> >
<template #prefix> <template #prefix>
<span class="text-gray-400">产品名称</span> <span class="text-gray-400">产品名称</span>
</template> </template>
</Input> </Input>
<!-- TODO @haohaotindwind -->
<Input <Input
v-model:value="searchParams.productKey" v-model:value="searchParams.productKey"
placeholder="请输入产品标识" placeholder="请输入产品标识"
allow-clear allow-clear
style="width: 220px" class="w-[220px]"
@press-enter="handleSearch" @press-enter="handleSearch"
> >
<template #prefix> <template #prefix>
@@ -207,18 +194,22 @@ onMounted(() => {
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Space :size="12"> <TableAction
<Button type="primary" @click="handleCreate"> :actions="[
<!-- TODO @haohao按钮使用中立的ACTION_ICON.ADD --> {
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" /> label: '新增产品',
新增产品 type: 'primary',
</Button> icon: ACTION_ICON.ADD,
<Button type="primary" @click="handleExport"> onClick: handleCreate,
<!-- TODO @haohao按钮使用中立的ACTION_ICON.EXPORT --> },
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" /> {
导出 label: '导出',
</Button> type: 'primary',
</Space> icon: ACTION_ICON.DOWNLOAD,
onClick: handleExport,
},
]"
/>
<!-- 视图切换 --> <!-- 视图切换 -->
<Space :size="4"> <Space :size="4">
<Button <Button
@@ -238,53 +229,18 @@ onMounted(() => {
</Card> </Card>
<Grid v-show="viewMode === 'list'"> <Grid v-show="viewMode === 'list'">
<!-- TODO @haohao这里貌似可以删除掉 -->
<template #toolbar-tools>
<div></div>
</template>
<!-- 产品分类列 -->
<!-- TODO @haohao这里应该可以拿到 data.ts参考别的模块类似 apps/web-antd/src/views/ai/image/manager/data.ts 里面查询 category 和自己渲染-->
<template #category="{ row }">
<span>{{ getCategoryNameByValue(row.categoryId) }}</span>
</template>
<!-- 产品图标列 -->
<!-- TODO @haohao直接用 Image 组件 ok 了呀 data.ts -->
<template #icon="{ row }">
<Button
v-if="row.icon"
type="link"
size="small"
@click="handlePreviewImage(row.icon)"
>
<IconifyIcon icon="ant-design:eye-outlined" class="text-lg" />
</Button>
<span v-else class="text-gray-400">-</span>
</template>
<!-- TODO @haohao直接用 Image 组件 ok 了呀 data.ts -->
<!-- 产品图片列 -->
<template #picUrl="{ row }">
<Button
v-if="row.picUrl"
type="link"
size="small"
@click="handlePreviewImage(row.picUrl)"
>
<IconifyIcon icon="ant-design:eye-outlined" class="text-lg" />
</Button>
<span v-else class="text-gray-400">-</span>
</template>
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
:actions="[ :actions="[
{ {
label: '详情', label: '详情',
type: 'link', type: 'link',
onClick: openProductDetail.bind(null, row.id), onClick: openProductDetail.bind(null, row.id!),
}, },
{ {
label: '物模型', label: '物模型',
type: 'link', type: 'link',
onClick: openThingModel.bind(null, row.id), onClick: openThingModel.bind(null, row.id!),
}, },
{ {
label: $t('common.edit'), label: $t('common.edit'),
@@ -320,42 +276,11 @@ onMounted(() => {
@thing-model="openThingModel" @thing-model="openThingModel"
/> />
<!-- 图片预览 -->
<!-- TODO @haohaotindwind -->
<div style="display: none">
<!-- TODO @haohao是不是通过 Image 直接实现预览 -->
<Image.PreviewGroup
:preview="{
visible: previewVisible,
onVisibleChange: (visible) => (previewVisible = visible),
}"
>
<Image :src="previewImage" />
</Image.PreviewGroup>
</div>
</Page> </Page>
</template> </template>
<style scoped> <style scoped>
/** TODO @haohao貌似这 2 个 css 没啥用? */
:deep(.vxe-toolbar div) {
z-index: 1;
}
/* 隐藏 VxeGrid 自带的搜索表单区域 */ /* 隐藏 VxeGrid 自带的搜索表单区域 */
:deep(.vxe-grid--form-wrapper) { :deep(.vxe-grid--form-wrapper) {
display: none !important; display: none !important;
} }
</style> </style>
<style>
/* 控制图片预览的大小 */
.ant-image-preview-img {
max-width: 80% !important;
max-height: 80% !important;
object-fit: contain !important;
}
.ant-image-preview-operations {
background: rgb(0 0 0 / 70%) !important;
}
</style>

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
@@ -10,6 +10,7 @@ import {
Card, Card,
Col, Col,
Empty, Empty,
Image,
Pagination, Pagination,
Popconfirm, Popconfirm,
Row, Row,
@@ -19,10 +20,13 @@ import {
import { getProductPage } from '#/api/iot/product/product'; import { getProductPage } from '#/api/iot/product/product';
// TODO @haohao card-view.vue interface Props {
categoryList: any[];
// TODO @haohao searchParams?: {
defineOptions({ name: 'ProductCardView' }); name: string;
productKey: string;
};
}
const props = defineProps<Props>(); const props = defineProps<Props>();
@@ -34,14 +38,6 @@ const emit = defineEmits<{
thingModel: [productId: number]; thingModel: [productId: number];
}>(); }>();
interface Props {
categoryList: any[];
searchParams?: {
name: string;
productKey: string;
};
}
const loading = ref(false); const loading = ref(false);
const list = ref<any[]>([]); const list = ref<any[]>([]);
const total = ref(0); const total = ref(0);
@@ -50,14 +46,13 @@ const queryParams = ref({
pageSize: 12, pageSize: 12,
}); });
// TODO @haohao /** 获取分类名称 */
//
function getCategoryName(categoryId: number) { function getCategoryName(categoryId: number) {
const category = props.categoryList.find((c: any) => c.id === categoryId); const category = props.categoryList.find((c: any) => c.id === categoryId);
return category?.name || '未分类'; return category?.name || '未分类';
} }
// /** 获取产品列表 */
async function getList() { async function getList() {
loading.value = true; loading.value = true;
try { try {
@@ -72,14 +67,14 @@ async function getList() {
} }
} }
// /** 处理页码变化 */
function handlePageChange(page: number, pageSize: number) { function handlePageChange(page: number, pageSize: number) {
queryParams.value.pageNo = page; queryParams.value.pageNo = page;
queryParams.value.pageSize = pageSize; queryParams.value.pageSize = pageSize;
getList(); getList();
} }
// /** 获取设备类型颜色 */
function getDeviceTypeColor(deviceType: number) { function getDeviceTypeColor(deviceType: number) {
const colors: Record<number, string> = { const colors: Record<number, string> = {
0: 'blue', 0: 'blue',
@@ -114,17 +109,17 @@ onMounted(() => {
:sm="12" :sm="12"
:md="12" :md="12"
:lg="6" :lg="6"
class="mb-4"
> >
<!-- TODO @haohao卡片之间的上下距离太宽了 --> <Card
<Card :body-style="{ padding: '20px' }" class="product-card h-full"> :body-style="{ padding: '16px' }"
class="product-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
>
<!-- 顶部标题区域 --> <!-- 顶部标题区域 -->
<div class="mb-4 flex items-start"> <div class="mb-3 flex items-center">
<!-- TODO @haohao图标太大了看看是不是参考 vue3 + element-plus 搞小点然后标题居中 -->
<div class="product-icon"> <div class="product-icon">
<IconifyIcon <IconifyIcon
:icon="item.icon || 'ant-design:inbox-outlined'" :icon="item.icon || 'lucide:box'"
class="text-3xl" class="text-xl"
/> />
</div> </div>
<div class="ml-3 min-w-0 flex-1"> <div class="ml-3 min-w-0 flex-1">
@@ -132,7 +127,7 @@ onMounted(() => {
</div> </div>
</div> </div>
<!-- 内容区域 --> <!-- 内容区域 -->
<div class="mb-4 flex items-start"> <div class="mb-3 flex items-start">
<div class="info-list flex-1"> <div class="info-list flex-1">
<div class="info-item"> <div class="info-item">
<span class="info-label">产品分类</span> <span class="info-label">产品分类</span>
@@ -156,20 +151,25 @@ onMounted(() => {
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">产品标识</span> <span class="info-label">产品标识</span>
<!-- TODO @haohao展示 有点奇怪要不小手 -->
<Tooltip :title="item.productKey || item.id" placement="top"> <Tooltip :title="item.productKey || item.id" placement="top">
<span class="info-value product-key"> <span class="info-value product-key cursor-pointer">
{{ item.productKey || item.id }} {{ item.productKey || item.id }}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<!-- TODO @haohao这里是不是有 image然后默认 icon --> <!-- 产品图片 -->
<!-- TODO @haohao高度太高了建议和左侧产品分类 + 产品类型 + 产品标识高度保持一致 --> <div class="product-image">
<div class="product-3d-icon"> <Image
v-if="item.picUrl"
:src="item.picUrl"
:preview="true"
class="size-full rounded object-cover"
/>
<IconifyIcon <IconifyIcon
icon="ant-design:box-plot-outlined" v-else
class="text-2xl" icon="lucide:image"
class="text-2xl opacity-50"
/> />
</div> </div>
</div> </div>
@@ -180,8 +180,7 @@ onMounted(() => {
class="action-btn action-btn-edit" class="action-btn action-btn-edit"
@click="emit('edit', item)" @click="emit('edit', item)"
> >
<!-- TODO @haohao按钮尽量用中立的按钮方便迁移 ele --> <IconifyIcon icon="lucide:edit" class="mr-1" />
<IconifyIcon icon="ant-design:edit-outlined" class="mr-1" />
编辑 编辑
</Button> </Button>
<Button <Button
@@ -189,7 +188,7 @@ onMounted(() => {
class="action-btn action-btn-detail" class="action-btn action-btn-detail"
@click="emit('detail', item.id)" @click="emit('detail', item.id)"
> >
<IconifyIcon icon="ant-design:eye-outlined" class="mr-1" /> <IconifyIcon icon="lucide:eye" class="mr-1" />
详情 详情
</Button> </Button>
<Button <Button
@@ -197,23 +196,17 @@ onMounted(() => {
class="action-btn action-btn-model" class="action-btn action-btn-model"
@click="emit('thingModel', item.id)" @click="emit('thingModel', item.id)"
> >
<IconifyIcon <IconifyIcon icon="lucide:git-branch" class="mr-1" />
icon="ant-design:apartment-outlined"
class="mr-1"
/>
物模型 物模型
</Button> </Button>
<Tooltip v-if="item.status === 1" title="启用状态的产品不能删除"> <Tooltip v-if="item.status === 1" title="已发布的产品不能删除">
<Button <Button
size="small" size="small"
danger danger
disabled disabled
class="action-btn action-btn-delete !w-8" class="action-btn action-btn-delete !w-8"
> >
<IconifyIcon <IconifyIcon icon="lucide:trash-2" class="text-sm" />
icon="ant-design:delete-outlined"
class="text-sm"
/>
</Button> </Button>
</Tooltip> </Tooltip>
<Popconfirm <Popconfirm
@@ -226,10 +219,7 @@ onMounted(() => {
danger danger
class="action-btn action-btn-delete !w-8" class="action-btn action-btn-delete !w-8"
> >
<IconifyIcon <IconifyIcon icon="lucide:trash-2" class="text-sm" />
icon="ant-design:delete-outlined"
class="text-sm"
/>
</Button> </Button>
</Popconfirm> </Popconfirm>
</div> </div>
@@ -241,8 +231,7 @@ onMounted(() => {
</div> </div>
<!-- 分页 --> <!-- 分页 -->
<!-- TODO @haohao放到最右侧好点 --> <div v-if="list.length > 0" class="flex justify-end">
<div v-if="list.length > 0" class="flex justify-center">
<Pagination <Pagination
v-model:current="queryParams.pageNo" v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize" v-model:page-size="queryParams.pageSize"
@@ -258,18 +247,9 @@ onMounted(() => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/** TODO @haohao看看哪些可以 tindwind 掉 */
.product-card-view { .product-card-view {
.product-card { .product-card {
height: 100%;
overflow: hidden; overflow: hidden;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
transform: translateY(-2px);
}
:deep(.ant-card-body) { :deep(.ant-card-body) {
display: flex; display: flex;
@@ -283,8 +263,8 @@ onMounted(() => {
flex-shrink: 0; flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 48px; width: 36px;
height: 48px; height: 36px;
color: white; color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px; border-radius: 8px;
@@ -294,9 +274,9 @@ onMounted(() => {
.product-title { .product-title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
line-height: 1.5; line-height: 36px;
white-space: nowrap; white-space: nowrap;
} }
@@ -305,7 +285,7 @@ onMounted(() => {
.info-item { .info-item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 8px;
font-size: 13px; font-size: 13px;
&:last-child { &:last-child {
@@ -338,7 +318,6 @@ onMounted(() => {
font-size: 12px; font-size: 12px;
vertical-align: middle; vertical-align: middle;
white-space: nowrap; white-space: nowrap;
cursor: help;
opacity: 0.85; opacity: 0.85;
} }
@@ -348,18 +327,17 @@ onMounted(() => {
} }
} }
// 3D //
.product-3d-icon { .product-image {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100px; width: 80px;
height: 100px; height: 80px;
color: #667eea; color: #667eea;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-radius: 8px; border-radius: 8px;
opacity: 0.8;
} }
// //
@@ -420,10 +398,6 @@ onMounted(() => {
html.dark { html.dark {
.product-card-view { .product-card-view {
.product-card { .product-card {
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 30%);
}
.product-title { .product-title {
color: rgb(255 255 255 / 85%); color: rgb(255 255 255 / 85%);
} }
@@ -442,7 +416,7 @@ html.dark {
} }
} }
.product-3d-icon { .product-image {
color: #8b9cff; color: #8b9cff;
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%); background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
} }

View File

@@ -1,196 +0,0 @@
<!-- IoT 产品选择器使用弹窗展示 -->
<script setup lang="ts">
// TODO @haohao这个貌似暂时没看到在哪里用
import type { IotProductApi } from '#/api/iot/product/product';
import { reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Button, Form, Input, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductPage } from '#/api/iot/product/product';
defineOptions({ name: 'IoTProductTableSelect' });
const props = withDefaults(defineProps<Props>(), {
multiple: false,
});
const emit = defineEmits<{
success: [product: IotProductApi.Product | IotProductApi.Product[]];
}>();
interface Props {
multiple?: boolean;
}
const [Modal, modalApi] = useVbenModal({
title: '产品选择器',
// TODO @haohaohandleConfirm 直接放到这里,不用单独声明
onConfirm: handleConfirm,
});
const selectedProducts = ref<IotProductApi.Product[]>([]);
const selectedRowKeys = ref<number[]>([]);
// 搜索参数
const queryParams = reactive({
name: '',
productKey: '',
});
// TODO @haohao是不是 form 应该也在 Grid 里;
// 配置表格
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{
type: props.multiple ? 'checkbox' : 'radio',
width: 50,
},
{
field: 'name',
title: '产品名称',
minWidth: 150,
},
{
field: 'productKey',
title: 'ProductKey',
minWidth: 150,
},
{
field: 'categoryName',
title: '品类',
minWidth: 120,
},
{
field: 'deviceType',
title: '设备类型',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: 'iot_product_device_type' },
},
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
],
checkboxConfig: {
reserve: true,
highlight: true,
},
radioConfig: {
reserve: true,
highlight: true,
},
proxyConfig: {
ajax: {
query: async ({ page }: any) => {
return await getProductPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...queryParams,
});
},
},
},
},
});
// 打开选择器
async function open() {
selectedProducts.value = [];
selectedRowKeys.value = [];
modalApi.open();
gridApi.reload();
}
// 搜索
function handleSearch() {
gridApi.reload();
}
// 重置搜索
function handleReset() {
queryParams.name = '';
queryParams.productKey = '';
gridApi.reload();
}
// 确认选择
async function handleConfirm() {
const grid = gridApi.grid;
if (!grid) {
return false;
}
if (props.multiple) {
const checkboxRecords = grid.getCheckboxRecords();
if (checkboxRecords.length === 0) {
message.warning('请至少选择一个产品');
return false;
}
emit('success', checkboxRecords);
} else {
const radioRecord = grid.getRadioRecord();
if (!radioRecord) {
message.warning('请选择一个产品');
return false;
}
emit('success', radioRecord);
}
return true;
}
defineExpose({ open });
</script>
<template>
<Modal class="!w-[900px]">
<div class="mb-4">
<Form layout="inline" :model="queryParams">
<Form.Item label="产品名称">
<Input
v-model:value="queryParams.name"
placeholder="请输入产品名称"
allow-clear
class="!w-[200px]"
@press-enter="handleSearch"
/>
</Form.Item>
<Form.Item label="ProductKey">
<Input
v-model:value="queryParams.productKey"
placeholder="请输入产品标识"
allow-clear
class="!w-[200px]"
@press-enter="handleSearch"
/>
</Form.Item>
<Form.Item>
<Button type="primary" @click="handleSearch">
<template #icon>
<IconifyIcon icon="ant-design:search-outlined" />
</template>
搜索
</Button>
<Button class="ml-2" @click="handleReset">
<template #icon>
<IconifyIcon icon="ant-design:reload-outlined" />
</template>
重置
</Button>
</Form.Item>
</Form>
</div>
<Grid />
</Modal>
</template>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { IotProductApi } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
// TODO @haohaodetail 挪到 yudao-ui-admin-vben-v5/apps/web-antd/src/views/iot/product/product/detail 下。独立一个,不放在 modules 里。
import { onMounted, provide, ref } from 'vue'; import { onMounted, provide, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
@@ -13,8 +12,8 @@ import { getDeviceCount } from '#/api/iot/device/device';
import { getProduct } from '#/api/iot/product/product'; import { getProduct } from '#/api/iot/product/product';
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue'; import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
import ProductDetailsHeader from './product-details-header.vue'; import ProductDetailsHeader from './modules/header.vue';
import ProductDetailsInfo from './product-details-info.vue'; import ProductDetailsInfo from './modules/info.vue';
defineOptions({ name: 'IoTProductDetail' }); defineOptions({ name: 'IoTProductDetail' });

View File

@@ -1,15 +1,18 @@
<script setup lang="ts"> <script lang="ts" setup>
// TODO @haohao detail/modules header.vue
import type { IotProductApi } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Button, Card, Descriptions, message } from 'ant-design-vue'; import { useVbenModal } from '@vben/common-ui';
import { updateProductStatus } from '#/api/iot/product/product'; import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue';
import ProductForm from '../product-form.vue'; import {
ProductStatusEnum,
updateProductStatus,
} from '#/api/iot/product/product';
import Form from '../../form.vue';
interface Props { interface Props {
product: IotProductApi.Product; product: IotProductApi.Product;
@@ -25,7 +28,11 @@ const emit = defineEmits<{
}>(); }>();
const router = useRouter(); const router = useRouter();
const formRef = ref();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 复制到剪贴板 */ /** 复制到剪贴板 */
async function copyToClipboard(text: string) { async function copyToClipboard(text: string) {
@@ -46,59 +53,63 @@ function goToDeviceList(productId: number) {
} }
/** 打开编辑表单 */ /** 打开编辑表单 */
function openForm(type: string, id?: number) { function openEditForm(row: IotProductApi.Product) {
formRef.value?.open(type, id); formModalApi.setData(row).open();
} }
/** 发布产品 */ /** 发布产品 */
async function confirmPublish(id: number) { function handlePublish(product: IotProductApi.Product) {
// TODO @haohaoasync function handleDeleteBatch() { 1 confirm2 loading Modal.confirm({
try { title: '确认发布',
await updateProductStatus(id, 1); // TODO @1 0 content: `确认要发布产品「${product.name}」吗?`,
message.success('发布成功'); async onOk() {
emit('refresh'); await updateProductStatus(product.id!, ProductStatusEnum.PUBLISHED);
} catch { message.success('发布成功');
message.error('发布失败'); emit('refresh');
} },
});
} }
/** 撤销发布 */ /** 撤销发布 */
async function confirmUnpublish(id: number) { function handleUnpublish(product: IotProductApi.Product) {
// TODO @haohaoasync function handleDeleteBatch() { 1 confirm2 loading Modal.confirm({
try { title: '确认撤销发布',
await updateProductStatus(id, 0); content: `确认要撤销发布产品「${product.name}」吗?`,
message.success('撤销发布成功'); async onOk() {
emit('refresh'); await updateProductStatus(product.id!, ProductStatusEnum.UNPUBLISHED);
} catch { message.success('撤销发布成功');
message.error('撤销发布失败'); emit('refresh');
} },
});
} }
</script> </script>
<template> <template>
<div class="mb-4"> <div class="mb-4">
<FormModal @success="emit('refresh')" />
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
<h2 class="text-xl font-bold">{{ product.name }}</h2> <h2 class="text-xl font-bold">{{ product.name }}</h2>
</div> </div>
<div class="space-x-2"> <div class="space-x-2">
<Button <Button
:disabled="product.status === 1" :disabled="product.status === ProductStatusEnum.PUBLISHED"
@click="openForm('update', product.id)" @click="openEditForm(product)"
> >
编辑 编辑
</Button> </Button>
<Button <Button
v-if="product.status === 0" v-if="product.status === ProductStatusEnum.UNPUBLISHED"
type="primary" type="primary"
@click="confirmPublish(product.id!)" @click="handlePublish(product)"
> >
发布 发布
</Button> </Button>
<Button <Button
v-if="product.status === 1" v-if="product.status === ProductStatusEnum.PUBLISHED"
danger danger
@click="confirmUnpublish(product.id!)" @click="handleUnpublish(product)"
> >
撤销发布 撤销发布
</Button> </Button>
@@ -127,9 +138,5 @@ async function confirmUnpublish(id: number) {
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Card> </Card>
<!-- 表单弹窗 -->
<!-- TODO @haohao弹不出来另外应该用 index.vue Form 的声明方式哈 -->
<ProductForm ref="formRef" @success="emit('refresh')" />
</div> </div>
</template> </template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
// TODO @haohao detail/modules info.vue
import type { IotProductApi } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
@@ -24,7 +23,6 @@ function formatDate(date?: Date | string) {
<template> <template>
<Card title="产品信息"> <Card title="产品信息">
<!-- TODO @haohao看看是不是用 description 组件 -->
<Descriptions bordered :column="3" size="small"> <Descriptions bordered :column="3" size="small">
<Descriptions.Item label="产品名称"> <Descriptions.Item label="产品名称">
{{ product.name }} {{ product.name }}
@@ -48,7 +46,7 @@ function formatDate(date?: Date | string) {
{{ product.codecType || '-' }} {{ product.codecType || '-' }}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="产品状态"> <Descriptions.Item label="产品状态">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" /> <DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item <Descriptions.Item
v-if=" v-if="

View File

@@ -0,0 +1,142 @@
<script lang="ts" setup>
import type { IotProductApi } from '#/api/iot/product/product';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Collapse, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProduct,
getProduct,
updateProduct,
} from '#/api/iot/product/product';
import { $t } from '#/locales';
import {
generateProductKey,
useAdvancedFormSchema,
useBasicFormSchema,
} from '../data';
const emit = defineEmits(['success']);
const formData = ref<IotProductApi.Product>();
const activeKey = ref<string[]>([]);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['产品'])
: $t('ui.actionTitle.create', ['产品']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const [AdvancedForm, advancedFormApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
},
layout: 'horizontal',
schema: useAdvancedFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */
formApi.setState({ schema: useBasicFormSchema(formApi) });
const [Modal, modalApi] = useVbenModal({
/** 提交表单 */
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 合并两个表单的值
const basicValues = await formApi.getValues();
const advancedValues = activeKey.value.includes('advanced')
? await advancedFormApi.getValues()
: formData.value?.id
? {
icon: formData.value.icon,
picUrl: formData.value.picUrl,
description: formData.value.description,
}
: {};
const data = {
...basicValues,
...advancedValues,
} as IotProductApi.Product;
try {
await (formData.value?.id ? updateProduct(data) : createProduct(data));
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
/** 弹窗打开/关闭 */
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
activeKey.value = [];
return;
}
// 加载数据
const data = modalApi.getData<IotProductApi.Product>();
if (!data || !data.id) {
// 新增:设置默认值
await formApi.setValues({
productKey: generateProductKey(),
status: 0,
});
return;
}
// 编辑:加载数据
modalApi.lock();
try {
formData.value = await getProduct(data.id);
await formApi.setValues(formData.value);
// 设置高级表单(不等待)
advancedFormApi.setValues({
icon: formData.value.icon,
picUrl: formData.value.picUrl,
description: formData.value.description,
});
// 有高级字段时自动展开
if (
formData.value.icon ||
formData.value.picUrl ||
formData.value.description
) {
activeKey.value = ['advanced'];
}
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<div class="mx-4">
<Form />
<Collapse v-model:active-key="activeKey" class="mt-4">
<Collapse.Panel key="advanced" header="更多设置">
<AdvancedForm />
</Collapse.Panel>
</Collapse>
</div>
</Modal>
</template>

View File

@@ -1,172 +0,0 @@
<script setup lang="ts">
import type { IotProductApi } from '#/api/iot/product/product';
import { computed, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { Collapse, message } from 'ant-design-vue';
import {
createProduct,
getProduct,
updateProduct,
} from '#/api/iot/product/product';
import { $t } from '#/locales';
import {
generateProductKey,
useAdvancedFormSchema,
useBasicFormSchema,
} from '../data';
// TODO @haohao应该是 form.vue
defineOptions({ name: 'IoTProductForm' });
const emit = defineEmits(['success']);
const CollapsePanel = Collapse.Panel;
const formData = ref<any>();
const getTitle = computed(() => {
return formData.value?.id ? '编辑产品' : '新增产品';
});
const activeKey = ref<string[]>([]); // 折叠面板的激活 key默认不展开
// TODO @haohao每一行一个
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
wrapperClass: 'grid-cols-2',
layout: 'horizontal',
schema: [],
showDefaultActions: false,
});
// TODO @haohao每一行一个
const [AdvancedForm, advancedFormApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
wrapperClass: 'grid-cols-2',
layout: 'horizontal',
schema: [],
showDefaultActions: false,
});
// TODO @haohao看看是不是可以参考别的 form 模块,优化表单这块的逻辑;从 61 到 156 行。体感有点冗余、以及代码风格,不够统一;
formApi.setState({ schema: useBasicFormSchema(formApi) });
advancedFormApi.setState({ schema: useAdvancedFormSchema() });
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
// 只验证基础表单
const { valid: basicValid } = await formApi.validate();
if (!basicValid) {
return;
}
modalApi.lock();
try {
// 提交表单 - 合并两个表单的值
const basicValues = await formApi.getValues();
// 如果折叠面板展开,则获取高级表单的值,否则保留原有值(编辑时)或使用空值(新增时)
let advancedValues: any = {};
if (activeKey.value.includes('advanced')) {
advancedValues = await advancedFormApi.getValues();
} else if (formData.value?.id) {
// 编辑时保留原有的高级字段值
advancedValues = {
icon: formData.value.icon,
picUrl: formData.value.picUrl,
description: formData.value.description,
};
}
const values = {
...basicValues,
...advancedValues,
} as IotProductApi.Product;
const data = formData.value?.id
? { ...values, id: formData.value.id }
: values;
await (formData.value?.id ? updateProduct(data) : createProduct(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
// 重置折叠面板状态
activeKey.value = [];
return;
}
// 加载数据
const data = modalApi.getData<any>();
if (!data || !data.id) {
// 设置默认值
await formApi.setValues({
productKey: generateProductKey(), // 自动生成 ProductKey
// deviceType: 0, // 默认直连设备
// codecType: 'Alink', // 默认 Alink
// dataFormat: 1, // 默认 JSON
// validateType: 1, // 默认设备密钥
status: 0, // 默认启用
});
return;
}
try {
formData.value = await getProduct(data.id);
// 设置基础表单
await formApi.setValues(formData.value);
// 先设置高级表单的值(不等待)
advancedFormApi.setValues({
icon: formData.value.icon,
picUrl: formData.value.picUrl,
description: formData.value.description,
});
// 如果有图标、图片或描述,自动展开折叠面板以便显示
if (
formData.value.icon ||
formData.value.picUrl ||
formData.value.description
) {
activeKey.value = ['advanced'];
}
} catch (error) {
console.error('加载产品数据失败:', error);
message.error('加载产品数据失败,请重试');
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<div class="mx-4">
<Form />
<Collapse v-model:active-key="activeKey" class="mt-4">
<CollapsePanel key="advanced" header="更多设置">
<AdvancedForm />
</CollapsePanel>
</Collapse>
</div>
</Modal>
</template>