@@ -1,17 +1,19 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { h, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||
import { getProductPage } from '#/api/iot/product/product';
|
||||
|
||||
/** 新增/修改产品的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -21,6 +23,55 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
// 创建时的 ProductKey 字段(带生成按钮)
|
||||
{
|
||||
fieldName: 'productKey',
|
||||
label: 'ProductKey',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 ProductKey',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
if(values) {
|
||||
// 仅在创建时显示(没有 id)
|
||||
return !values.id;
|
||||
},
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, 'ProductKey 不能为空')
|
||||
.max(32, 'ProductKey 长度不能超过 32 个字符'),
|
||||
suffix: () => {
|
||||
return h(Button, {
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('productKey', generateProductKey());
|
||||
},
|
||||
}, { default: () => '重新生成' });
|
||||
},
|
||||
},
|
||||
// 编辑时的 ProductKey 字段(禁用,无按钮)
|
||||
{
|
||||
fieldName: 'productKey',
|
||||
label: 'ProductKey',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 ProductKey',
|
||||
disabled: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
if(values) {
|
||||
// 仅在编辑时显示(有 id)
|
||||
return !!values.id;
|
||||
},
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, 'ProductKey 不能为空')
|
||||
.max(32, 'ProductKey 长度不能超过 32 个字符'),
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '产品名称',
|
||||
@@ -67,41 +118,11 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'protocolType',
|
||||
label: '接入协议',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'number'),
|
||||
placeholder: '请选择接入协议',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'dataFormat',
|
||||
fieldName: 'codecType',
|
||||
label: '数据格式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_DATA_FORMAT, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '产品描述',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入产品描述',
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'validateType',
|
||||
label: '认证方式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_VALIDATE_TYPE, 'number'),
|
||||
options: getDictOptions(DICT_TYPE.IOT_CODEC_TYPE, 'string'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
@@ -118,6 +139,196 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
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): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
// 创建时的 ProductKey 字段(带生成按钮)
|
||||
{
|
||||
fieldName: 'productKey',
|
||||
label: 'ProductKey',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 ProductKey',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
if(values) {
|
||||
// 仅在创建时显示(没有 id)
|
||||
return !values.id;
|
||||
},
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, 'ProductKey 不能为空')
|
||||
.max(32, 'ProductKey 长度不能超过 32 个字符'),
|
||||
suffix: () => {
|
||||
return h(Button, {
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('productKey', generateProductKey());
|
||||
},
|
||||
}, { default: () => '重新生成' });
|
||||
},
|
||||
},
|
||||
// 编辑时的 ProductKey 字段(禁用,无按钮)
|
||||
{
|
||||
fieldName: 'productKey',
|
||||
label: 'ProductKey',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 ProductKey',
|
||||
disabled: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
if(values) {
|
||||
// 仅在编辑时显示(有 id)
|
||||
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.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'locationType',
|
||||
label: '定位类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_LOCATION_TYPE, 'number'),
|
||||
placeholder: '请选择定位类型',
|
||||
},
|
||||
rules: 'required',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/** 高级设置表单字段(图标、图片、产品描述) */
|
||||
export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'icon',
|
||||
label: '产品图标',
|
||||
component: 'IconPicker',//用这个组件 产品卡片列表 可以根据这个显示 否则就显示默认的
|
||||
componentProps: {
|
||||
placeholder: '请选择产品图标',
|
||||
prefix: 'carbon',
|
||||
autoFetchApi: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '产品图片',
|
||||
component: 'ImageUpload',
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '产品描述',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入产品描述',
|
||||
rows: 3,
|
||||
},
|
||||
formItemClass: 'col-span-2', // 让描述占满两列
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -226,3 +437,13 @@ export function useImagePreview() {
|
||||
handlePreviewImage,
|
||||
};
|
||||
}
|
||||
|
||||
/** 生成 ProductKey(包含大小写字母和数字) */
|
||||
export function generateProductKey(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
deleteProduct,
|
||||
exportProduct,
|
||||
getProductPage,
|
||||
} from '#/api/crm/product';
|
||||
} from '#/api/iot/product/product';
|
||||
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ defineExpose({
|
||||
<div class="mb-4 flex items-start">
|
||||
<div class="product-icon">
|
||||
<IconifyIcon
|
||||
icon="ant-design:inbox-outlined"
|
||||
:icon="item.icon || 'ant-design:inbox-outlined'"
|
||||
class="text-[32px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Collapse, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createProduct,
|
||||
@@ -14,16 +14,21 @@ import {
|
||||
} from '#/api/iot/product/product';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
import { generateProductKey, useBasicFormSchema, useAdvancedFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'IoTProductForm' });
|
||||
|
||||
const CollapsePanel = Collapse.Panel;
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<any>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id ? '编辑产品' : '新增产品';
|
||||
});
|
||||
|
||||
// 折叠面板的激活key,默认不展开
|
||||
const activeKey = ref<string[]>([]);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
@@ -32,20 +37,58 @@ const [Form, formApi] = useVbenForm({
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 创建高级设置表单
|
||||
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 在 formApi 创建后设置 schema
|
||||
formApi.setState({ schema: useBasicFormSchema(formApi) });
|
||||
advancedFormApi.setState({ schema: useAdvancedFormSchema() });
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
// 只验证基础表单
|
||||
const { valid: basicValid } = await formApi.validate();
|
||||
if (!basicValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as IotProductApi.Product;
|
||||
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();
|
||||
@@ -58,6 +101,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
// 重置折叠面板状态
|
||||
activeKey.value = [];
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
@@ -65,18 +110,34 @@ const [Modal, modalApi] = useVbenModal({
|
||||
if (!data || !data.id) {
|
||||
// 设置默认值
|
||||
await formApi.setValues({
|
||||
deviceType: 0, // 默认直连设备
|
||||
dataFormat: 1, // 默认 JSON
|
||||
validateType: 1, // 默认设备密钥
|
||||
productKey: generateProductKey(), // 自动生成 ProductKey
|
||||
// deviceType: 0, // 默认直连设备
|
||||
// codecType: 'Alink', // 默认 Alink
|
||||
// dataFormat: 1, // 默认 JSON
|
||||
// validateType: 1, // 默认设备密钥
|
||||
status: 0, // 默认启用
|
||||
});
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getProduct(data.id);
|
||||
// 设置到 values
|
||||
// 设置基础表单
|
||||
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();
|
||||
}
|
||||
@@ -86,6 +147,16 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
<div class="mx-4">
|
||||
<Form />
|
||||
<Collapse v-model:active-key="activeKey" class="mt-4">
|
||||
<CollapsePanel key="advanced" header="更多设置">
|
||||
<template #extra>
|
||||
<span class="text-gray-500">📷</span>
|
||||
</template>
|
||||
<AdvancedForm />
|
||||
</CollapsePanel>
|
||||
</Collapse>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user