From 4261ca0a50dfd479ce5c2c8c1b7edc49a4314149 Mon Sep 17 00:00:00 2001 From: huppygo Date: Mon, 13 Oct 2025 22:20:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Diot=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=9A=201.=E5=88=A0=E9=99=A4=EF=BC=8C?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=EF=BC=8C=E8=8E=B7=E5=8F=96=E5=88=86=E9=A1=B5?= =?UTF-8?q?=20=E5=BC=95=E5=85=A5=E5=9C=B0=E5=9D=80=E4=B8=8D=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98=202.=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2=20=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E7=A1=AE=E5=AE=9A=E6=8C=89=E9=92=AE=E6=B2=A1=E6=9C=89=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=8E=A5=E5=8F=A3=203.=E6=96=B0=E5=A2=9E=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E9=A1=B5=E9=9D=A2productKey=E4=B8=8D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=B2=A1=E6=9C=89=E7=94=9F=E6=88=90=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98=204.=E6=96=B0=E5=A2=9E=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=8A=98=E5=8F=A0=E6=A1=86=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E4=B8=BA=E6=9B=B4=E5=A4=9A=E8=AE=BE=E7=BD=AE=20=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=20=E5=9B=BE=E7=89=87=20=E4=BA=A7=E5=93=81=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E6=94=BE=E5=88=B0=20=E6=9B=B4=E5=A4=9A=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=8A=98=E5=8F=A0=E5=8C=BA=E5=9F=9F=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=8A=98=E5=8F=A0=E7=82=B9=E5=87=BB=E5=B1=95=E5=BC=80=205.?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=88=97=E8=A1=A8=E4=B8=AD=E7=9A=84=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=9B=BE=E6=A0=87=E6=A0=B9=E6=8D=AE=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E7=9A=84=E5=9B=BE=E6=A0=87=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Administrator <425053404@qq.com> --- .../src/views/iot/product/product/data.ts | 247 +++++++++++++++++- .../src/views/iot/product/product/index.vue | 2 +- .../product/modules/ProductCardView.vue | 2 +- .../product/product/modules/ProductForm.vue | 97 ++++++- 4 files changed, 320 insertions(+), 28 deletions(-) diff --git a/apps/web-antd/src/views/iot/product/product/data.ts b/apps/web-antd/src/views/iot/product/product/data.ts index 68fd51b46..8811e2c64 100644 --- a/apps/web-antd/src/views/iot/product/product/data.ts +++ b/apps/web-antd/src/views/iot/product/product/data.ts @@ -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,26 +118,37 @@ export function useFormSchema(): VbenFormSchema[] { rules: 'required', }, { - fieldName: 'protocolType', - label: '接入协议', - component: 'Select', + fieldName: 'codecType', + label: '数据格式', + component: 'RadioGroup', componentProps: { - options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'number'), - placeholder: '请选择接入协议', + options: getDictOptions(DICT_TYPE.IOT_CODEC_TYPE, 'string'), + buttonStyle: 'solid', + optionType: 'button', }, rules: 'required', }, { - fieldName: 'dataFormat', - label: '数据格式', + fieldName: 'status', + label: '产品状态', component: 'RadioGroup', componentProps: { - options: getDictOptions(DICT_TYPE.IOT_DATA_FORMAT, 'number'), + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), buttonStyle: 'solid', optionType: 'button', }, rules: 'required', }, + { + fieldName: 'icon', + label: '产品图标', + component: 'ImageUpload', + }, + { + fieldName: 'picUrl', + label: '产品图片', + component: 'ImageUpload', + }, { fieldName: 'description', label: '产品描述', @@ -95,13 +157,121 @@ export function useFormSchema(): VbenFormSchema[] { 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: 'validateType', - label: '认证方式', + fieldName: 'codecType', + 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 +288,47 @@ export function useFormSchema(): VbenFormSchema[] { }, 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; +} diff --git a/apps/web-antd/src/views/iot/product/product/index.vue b/apps/web-antd/src/views/iot/product/product/index.vue index ad056a811..190da4a18 100644 --- a/apps/web-antd/src/views/iot/product/product/index.vue +++ b/apps/web-antd/src/views/iot/product/product/index.vue @@ -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'; diff --git a/apps/web-antd/src/views/iot/product/product/modules/ProductCardView.vue b/apps/web-antd/src/views/iot/product/product/modules/ProductCardView.vue index 124428fb9..e28090822 100644 --- a/apps/web-antd/src/views/iot/product/product/modules/ProductCardView.vue +++ b/apps/web-antd/src/views/iot/product/product/modules/ProductCardView.vue @@ -117,7 +117,7 @@ defineExpose({
diff --git a/apps/web-antd/src/views/iot/product/product/modules/ProductForm.vue b/apps/web-antd/src/views/iot/product/product/modules/ProductForm.vue index e21d6f85c..7fad6cd19 100644 --- a/apps/web-antd/src/views/iot/product/product/modules/ProductForm.vue +++ b/apps/web-antd/src/views/iot/product/product/modules/ProductForm.vue @@ -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(); const getTitle = computed(() => { return formData.value?.id ? '编辑产品' : '新增产品'; }); +// 折叠面板的激活key,默认不展开 +const activeKey = ref([]); + 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({ -- Gitee