diff --git a/package.json b/package.json index 1a12256e..8ef3b4ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yudao-ui-admin-vue3", - "version": "2025.12-snapshot", + "version": "2026.01-snapshot", "description": "基于vue3、vite4、element-plus、typesScript", "author": "xingyu", "private": false, diff --git a/src/api/iot/device/device/index.ts b/src/api/iot/device/device/index.ts index 1804628c..cf9f6d62 100644 --- a/src/api/iot/device/device/index.ts +++ b/src/api/iot/device/device/index.ts @@ -21,7 +21,6 @@ export interface DeviceVO { mqttClientId: string // MQTT 客户端 ID mqttUsername: string // MQTT 用户名 mqttPassword: string // MQTT 密码 - authType: string // 认证类型 latitude?: number // 设备位置的纬度 longitude?: number // 设备位置的经度 areaId: number // 地区编码 @@ -158,5 +157,28 @@ export const DeviceApi = { // 发送设备消息 sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => { return await request.post({ url: `/iot/device/message/send`, data: params }) + }, + + // 绑定子设备到网关 + bindDeviceGateway: async (data: { subIds: number[]; gatewayId: number }) => { + return await request.put({ url: `/iot/device/bind-gateway`, data }) + }, + + // 解绑子设备与网关 + unbindDeviceGateway: async (data: { subIds: number[]; gatewayId: number }) => { + return await request.put({ url: `/iot/device/unbind-gateway`, data }) + }, + + // 获取网关的子设备列表 + getSubDeviceList: async (gatewayId: number) => { + return await request.get({ + url: `/iot/device/sub-device-list`, + params: { gatewayId } + }) + }, + + // 获取未绑定网关的子设备分页 + getUnboundSubDevicePage: async (params: any) => { + return await request.get({ url: `/iot/device/unbound-sub-device-page`, params }) } } diff --git a/src/api/iot/product/product/index.ts b/src/api/iot/product/product/index.ts index a34efaeb..ba465efd 100644 --- a/src/api/iot/product/product/index.ts +++ b/src/api/iot/product/product/index.ts @@ -5,6 +5,8 @@ export interface ProductVO { id: number // 产品编号 name: string // 产品名称 productKey: string // 产品标识 + productSecret?: string // 产品密钥 + registerEnabled?: boolean // 动态注册 protocolId: number // 协议编号 categoryId: number // 产品所属品类标识符 categoryName?: string // 产品所属品类名称 @@ -68,8 +70,8 @@ export const ProductApi = { }, // 查询产品(精简)列表 - getSimpleProductList() { - return request.get({ url: '/iot/product/simple-list' }) + getSimpleProductList(deviceType?: number) { + return request.get({ url: '/iot/product/simple-list', params: { deviceType } }) }, // 根据 ProductKey 获取产品信息 diff --git a/src/components/FormCreate/src/components/AreaSelect.vue b/src/components/FormCreate/src/components/AreaSelect.vue new file mode 100644 index 00000000..1633d7a9 --- /dev/null +++ b/src/components/FormCreate/src/components/AreaSelect.vue @@ -0,0 +1,138 @@ + + + + diff --git a/src/components/FormCreate/src/components/DeptSelect.vue b/src/components/FormCreate/src/components/DeptSelect.vue new file mode 100644 index 00000000..2e18f4ad --- /dev/null +++ b/src/components/FormCreate/src/components/DeptSelect.vue @@ -0,0 +1,196 @@ + + + + diff --git a/src/components/FormCreate/src/components/IframeComponent.vue b/src/components/FormCreate/src/components/IframeComponent.vue new file mode 100644 index 00000000..57b4358a --- /dev/null +++ b/src/components/FormCreate/src/components/IframeComponent.vue @@ -0,0 +1,102 @@ + + + + + + diff --git a/src/components/FormCreate/src/components/useApiSelect.tsx b/src/components/FormCreate/src/components/useApiSelect.tsx index 89a7e8db..847eb077 100644 --- a/src/components/FormCreate/src/components/useApiSelect.tsx +++ b/src/components/FormCreate/src/components/useApiSelect.tsx @@ -2,6 +2,7 @@ import request from '@/config/axios' import { isEmpty } from '@/utils/is' import { ApiSelectProps } from '@/components/FormCreate/src/type' import { jsonParse } from '@/utils' +import { useUserStoreWithOut } from '@/store/modules/user' export const useApiSelect = (option: ApiSelectProps) => { return defineComponent({ @@ -61,13 +62,53 @@ export const useApiSelect = (option: ApiSelectProps) => { returnType: { type: String, default: 'id' + }, + // 是否默认选中当前用户(仅 UserSelect 使用) + defaultCurrentUser: { + type: Boolean, + default: false } }, - setup(props) { + setup(props, { emit }) { const attrs = useAttrs() const options = ref([]) // 下拉数据 const loading = ref(false) // 是否正在从远程获取数据 const queryParam = ref() // 当前输入的值 + + // 检查是否有有效的预设值 + const hasValidPresetValue = (): boolean => { + const value = attrs.modelValue + if (value === undefined || value === null || value === '') { + return false + } + if (Array.isArray(value)) { + return value.length > 0 + } + return true + } + + // 设置默认当前用户(仅当 defaultCurrentUser 为 true 且无预设值时) + const setDefaultCurrentUser = () => { + // 仅当组件名为 UserSelect 且 defaultCurrentUser 为 true 时处理 + if (option.name !== 'UserSelect' || !props.defaultCurrentUser) { + return + } + // 检查是否已有预设值(预设值优先级高于默认当前用户) + if (hasValidPresetValue()) { + return + } + + // 获取当前用户 ID + const userStore = useUserStoreWithOut() + const user = userStore.getUser + const currentUserId = user?.id + if (currentUserId) { + // 根据多选/单选模式设置默认值 + const defaultValue = props.multiple ? [currentUserId] : currentUserId + emit('update:modelValue', defaultValue) + } + } + const getOptions = async () => { options.value = [] // 接口选择器 @@ -188,6 +229,8 @@ export const useApiSelect = (option: ApiSelectProps) => { onMounted(async () => { await getOptions() + // 设置默认当前用户(在数据加载完成后) + setDefaultCurrentUser() }) const buildSelect = () => { diff --git a/src/components/FormCreate/src/config/index.ts b/src/components/FormCreate/src/config/index.ts index b1e2ddea..d4d384c3 100644 --- a/src/components/FormCreate/src/config/index.ts +++ b/src/components/FormCreate/src/config/index.ts @@ -4,6 +4,8 @@ import { useUploadImgsRule } from './useUploadImgsRule' import { useDictSelectRule } from './useDictSelectRule' import { useEditorRule } from './useEditorRule' import { useSelectRule } from './useSelectRule' +import { useIframeRule } from './useIframeRule' +import { useAreaSelectRule } from './useAreaSelectRule' export { useUploadFileRule, @@ -11,5 +13,7 @@ export { useUploadImgsRule, useDictSelectRule, useEditorRule, - useSelectRule + useSelectRule, + useIframeRule, + useAreaSelectRule } diff --git a/src/components/FormCreate/src/config/useAreaSelectRule.ts b/src/components/FormCreate/src/config/useAreaSelectRule.ts new file mode 100644 index 00000000..6f2bd35c --- /dev/null +++ b/src/components/FormCreate/src/config/useAreaSelectRule.ts @@ -0,0 +1,73 @@ +import { generateUUID } from '@/utils' +import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' + +/** + * 省市区选择器规则 + */ +export const useAreaSelectRule = () => { + const label = '省市区选择器' + const name = 'AreaSelect' + return { + icon: 'icon-location', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + title: label, + info: '', + $required: false + } + }, + props(_, { t }) { + return localeProps(t, name + '.props', [ + makeRequiredRule(), + { + type: 'select', + field: 'level', + title: '选择层级', + value: 3, + options: [ + { label: '省', value: 1 }, + { label: '省/市', value: 2 }, + { label: '省/市/区', value: 3 } + ], + info: '限制可选择的地区层级' + }, + { + type: 'input', + field: 'placeholder', + title: '占位符', + value: '请选择省市区' + }, + { + type: 'switch', + field: 'clearable', + title: '是否可清空', + value: true + }, + { + type: 'switch', + field: 'showAllLevels', + title: '显示完整路径', + value: true, + info: '输入框中是否显示选中值的完整路径' + }, + { + type: 'input', + field: 'separator', + title: '分隔符', + value: '/', + info: '选项分隔符' + }, + { + type: 'switch', + field: 'disabled', + title: '是否禁用', + value: false + } + ]) + } + } +} diff --git a/src/components/FormCreate/src/config/useIframeRule.ts b/src/components/FormCreate/src/config/useIframeRule.ts new file mode 100644 index 00000000..da75cd78 --- /dev/null +++ b/src/components/FormCreate/src/config/useIframeRule.ts @@ -0,0 +1,73 @@ +import { generateUUID } from '@/utils' +import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' + +/** + * iframe 组件规则 + */ +export const useIframeRule = () => { + const label = '网页 iframe' + const name = 'IframeComponent' + return { + icon: 'icon-link', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + title: label, + info: '', + $required: false + } + }, + props(_, { t }) { + return localeProps(t, name + '.props', [ + makeRequiredRule(), + { + type: 'input', + field: 'url', + title: 'URL 地址', + value: '', + info: '请输入完整的 HTTP 或 HTTPS 地址' + }, + { + type: 'input', + field: 'height', + title: 'iframe 高度', + value: '500px', + info: '支持 px、%、vh 等单位' + }, + { + type: 'input', + field: 'width', + title: 'iframe 宽度', + value: '100%', + info: '支持 px、%、vw 等单位' + }, + { + type: 'select', + field: 'loading', + title: '加载方式', + value: 'lazy', + options: [ + { label: '懒加载', value: 'lazy' }, + { label: '立即加载', value: 'eager' } + ] + }, + { + type: 'switch', + field: 'allowfullscreen', + title: '允许全屏', + value: true + }, + { + type: 'input', + field: 'sandbox', + title: 'sandbox 属性', + value: '', + info: '安全沙箱限制,如:allow-scripts allow-same-origin' + } + ]) + } + } +} diff --git a/src/components/FormCreate/src/config/useSelectRule.ts b/src/components/FormCreate/src/config/useSelectRule.ts index e1d77fbc..c95e047d 100644 --- a/src/components/FormCreate/src/config/useSelectRule.ts +++ b/src/components/FormCreate/src/config/useSelectRule.ts @@ -19,13 +19,24 @@ export const useSelectRule = (option: SelectRuleOption) => { name, event: option.event, rule() { - return { + // 构建基础规则 + const baseRule: any = { type: name, field: generateUUID(), title: label, info: '', $required: false } + // 将自定义 props 的默认值添加到 rule 的 props 中 + if (option.props && option.props.length > 0) { + baseRule.props = {} + option.props.forEach((prop: any) => { + if (prop.field && prop.value !== undefined) { + baseRule.props[prop.field] = prop.value + } + }) + } + return baseRule }, props(_, { t }) { if (!option.props) { diff --git a/src/components/FormCreate/src/useFormCreateDesigner.ts b/src/components/FormCreate/src/useFormCreateDesigner.ts index 4e87e432..60d30e32 100644 --- a/src/components/FormCreate/src/useFormCreateDesigner.ts +++ b/src/components/FormCreate/src/useFormCreateDesigner.ts @@ -4,7 +4,9 @@ import { useSelectRule, useUploadFileRule, useUploadImgRule, - useUploadImgsRule + useUploadImgsRule, + useIframeRule, + useAreaSelectRule } from './config' import { Ref } from 'vue' import { Menu } from '@/components/FormCreate/src/type' @@ -36,7 +38,9 @@ export const useFormCreateDesigner = async (designer: Ref) => { designer.value?.removeMenuItem('upload') // 移除自带的富文本组件规则,使用 editorRule 替代 designer.value?.removeMenuItem('fcEditor') - const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule] + const iframeRule = useIframeRule() + const areaSelectRule = useAreaSelectRule() + const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule, iframeRule, areaSelectRule] components.forEach((component) => { // 插入组件规则 designer.value?.addComponent(component) @@ -52,7 +56,15 @@ export const useFormCreateDesigner = async (designer: Ref) => { const userSelectRule = useSelectRule({ name: 'UserSelect', label: '用户选择器', - icon: 'icon-user-o' + icon: 'icon-user-o', + props: [ + { + type: 'switch', + field: 'defaultCurrentUser', + title: '默认选中当前用户', + value: false + } + ] }) const deptSelectRule = useSelectRule({ name: 'DeptSelect', @@ -68,6 +80,12 @@ export const useFormCreateDesigner = async (designer: Ref) => { { label: '部门编号', value: 'id' }, { label: '部门名称', value: 'name' } ] + }, + { + type: 'switch', + field: 'defaultCurrentDept', + title: '默认选中当前部门', + value: false } ] }) diff --git a/src/config/axios/service.ts b/src/config/axios/service.ts index 9214bf8e..dfaea75d 100644 --- a/src/config/axios/service.ts +++ b/src/config/axios/service.ts @@ -85,7 +85,7 @@ service.interceptors.request.use( } } // 是否 API 加密 - if ((config!.headers || {}).isEncrypt) { + if ((config!.headers || {}).isEncrypt && !(config!.headers || {}).isEncrypted) { try { // 加密请求数据 if (config.data) { @@ -169,6 +169,9 @@ service.interceptors.response.use( cb() }) requestList = [] + if ((config!.headers || {}).isEncrypt){ + (config!.headers || {}).isEncrypted = true + } return service(config) } catch (e) { // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。 diff --git a/src/plugins/formCreate/index.ts b/src/plugins/formCreate/index.ts index 01a57beb..42593050 100644 --- a/src/plugins/formCreate/index.ts +++ b/src/plugins/formCreate/index.ts @@ -68,6 +68,9 @@ import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile' import { useApiSelect } from '@/components/FormCreate' import { Editor } from '@/components/Editor' import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue' +import DeptSelect from '@/components/FormCreate/src/components/DeptSelect.vue' +import IframeComponent from '@/components/FormCreate/src/components/IframeComponent.vue' +import AreaSelect from '@/components/FormCreate/src/components/AreaSelect.vue' const UserSelect = useApiSelect({ name: 'UserSelect', @@ -75,12 +78,6 @@ const UserSelect = useApiSelect({ valueField: 'id', url: '/system/user/simple-list' }) -const DeptSelect = useApiSelect({ - name: 'DeptSelect', - labelField: 'name', - valueField: 'id', - url: '/system/dept/simple-list' -}) const ApiSelect = useApiSelect({ name: 'ApiSelect' }) @@ -119,6 +116,8 @@ const components = [ DeptSelect, ApiSelect, Editor, + IframeComponent, + AreaSelect, ElCollapse, ElCollapseItem, ElCard diff --git a/src/views/Login/components/ForgetPasswordForm.vue b/src/views/Login/components/ForgetPasswordForm.vue index f47b2299..f5f41dc8 100644 --- a/src/views/Login/components/ForgetPasswordForm.vue +++ b/src/views/Login/components/ForgetPasswordForm.vue @@ -39,6 +39,7 @@ 转化为客户 - 已转化客户 + 已转化客户 diff --git a/src/views/iot/device/device/DeviceForm.vue b/src/views/iot/device/device/DeviceForm.vue index 0ddbf2af..fe75eed5 100644 --- a/src/views/iot/device/device/DeviceForm.vue +++ b/src/views/iot/device/device/DeviceForm.vue @@ -30,20 +30,6 @@ :disabled="formType === 'update'" /> - - - - - @@ -114,7 +100,6 @@ const formData = ref({ deviceName: undefined, nickname: undefined, picUrl: undefined, - gatewayId: undefined, deviceType: undefined as number | undefined, serialNumber: undefined, longitude: undefined as number | string | undefined, @@ -222,7 +207,6 @@ const formRules = reactive({ }) const formRef = ref() // 表单 Ref const products = ref([]) // 产品列表 -const gatewayDevices = ref([]) // 网关设备列表 const deviceGroups = ref([]) /** 打开弹窗 */ @@ -242,8 +226,6 @@ const open = async (type: string, id?: number) => { } } - // 加载网关设备列表 - gatewayDevices.value = await DeviceApi.getSimpleDeviceList(DeviceTypeEnum.GATEWAY) // 加载产品列表 products.value = await ProductApi.getSimpleProductList() // 加载设备分组列表 @@ -283,7 +265,6 @@ const resetForm = () => { deviceName: undefined, nickname: undefined, picUrl: undefined, - gatewayId: undefined, deviceType: undefined, serialNumber: undefined, longitude: undefined, diff --git a/src/views/iot/device/device/detail/DeviceDetailsSubDevice.vue b/src/views/iot/device/device/detail/DeviceDetailsSubDevice.vue new file mode 100644 index 00000000..290cd5cc --- /dev/null +++ b/src/views/iot/device/device/detail/DeviceDetailsSubDevice.vue @@ -0,0 +1,264 @@ + + + + diff --git a/src/views/iot/device/device/detail/index.vue b/src/views/iot/device/device/detail/index.vue index 3ec756b5..f8949ecd 100644 --- a/src/views/iot/device/device/detail/index.vue +++ b/src/views/iot/device/device/detail/index.vue @@ -17,7 +17,13 @@ :thing-model-list="thingModelList" /> - + + + @@ -50,6 +56,7 @@ import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue' import DeviceDetailsMessage from './DeviceDetailsMessage.vue' import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue' import DeviceDetailConfig from './DeviceDetailConfig.vue' +import DeviceDetailsSubDevice from './DeviceDetailsSubDevice.vue' defineOptions({ name: 'IoTDeviceDetail' }) diff --git a/src/views/iot/product/product/ProductForm.vue b/src/views/iot/product/product/ProductForm.vue index 32a7b222..9cb7c509 100644 --- a/src/views/iot/product/product/ProductForm.vue +++ b/src/views/iot/product/product/ProductForm.vue @@ -75,6 +75,20 @@ + + + + @@ -120,7 +134,8 @@ const formData = ref({ description: undefined, deviceType: undefined, netType: undefined, - codecType: CodecTypeEnum.ALINK + codecType: CodecTypeEnum.ALINK, + registerEnabled: false }) const formRules = reactive({ productKey: [{ required: true, message: 'ProductKey 不能为空', trigger: 'blur' }], @@ -194,7 +209,8 @@ const resetForm = () => { description: undefined, deviceType: undefined, netType: undefined, - codecType: CodecTypeEnum.ALINK + codecType: CodecTypeEnum.ALINK, + registerEnabled: false } formRef.value?.resetFields() } diff --git a/src/views/iot/product/product/components/ProductSelect.vue b/src/views/iot/product/product/components/ProductSelect.vue new file mode 100644 index 00000000..fa288fa1 --- /dev/null +++ b/src/views/iot/product/product/components/ProductSelect.vue @@ -0,0 +1,65 @@ + + + + diff --git a/src/views/iot/product/product/detail/ProductDetailsInfo.vue b/src/views/iot/product/product/detail/ProductDetailsInfo.vue index feb7eb5d..38c523b6 100644 --- a/src/views/iot/product/product/detail/ProductDetailsInfo.vue +++ b/src/views/iot/product/product/detail/ProductDetailsInfo.vue @@ -21,6 +21,28 @@ > + + + {{ product.registerEnabled ? '已开启' : '已关闭' }} + + + +
+ {{ secretVisible ? product.productSecret : '******' }} + + + + + + +
+
{{ product.description }} @@ -29,6 +51,19 @@ import { DICT_TYPE } from '@/utils/dict' import { DeviceTypeEnum, ProductVO } from '@/api/iot/product/product' import { formatDate } from '@/utils/formatTime' +import { useClipboard } from '@vueuse/core' const { product } = defineProps<{ product: ProductVO }>() + +const message = useMessage() +const secretVisible = ref(false) +const { copy } = useClipboard() + +/** 复制产品密钥 */ +const copySecret = async () => { + if (product.productSecret) { + await copy(product.productSecret) + message.success('复制成功') + } +} diff --git a/src/views/iot/utils/constants.ts b/src/views/iot/utils/constants.ts index f25bb7de..8b9c6722 100644 --- a/src/views/iot/utils/constants.ts +++ b/src/views/iot/utils/constants.ts @@ -24,7 +24,41 @@ export const IotDeviceMessageMethodEnum = { // ========== 设备状态 ========== STATE_UPDATE: { method: 'thing.state.update', - name: '设备状态变更', + name: '设备状态更新', + upstream: true + }, + + // ========== 拓扑管理 ========== + TOPO_ADD: { + method: 'thing.topo.add', + name: '添加拓扑关系', + upstream: true + }, + TOPO_DELETE: { + method: 'thing.topo.delete', + name: '删除拓扑关系', + upstream: true + }, + TOPO_GET: { + method: 'thing.topo.get', + name: '获取拓扑关系', + upstream: true + }, + TOPO_CHANGE: { + method: 'thing.topo.change', + name: '拓扑关系变更通知', + upstream: false + }, + + // ========== 设备注册 ========== + DEVICE_REGISTER: { + method: 'thing.auth.register', + name: '设备动态注册', + upstream: true + }, + SUB_DEVICE_REGISTER: { + method: 'thing.auth.register.sub', + name: '子设备动态注册', upstream: true }, @@ -39,6 +73,11 @@ export const IotDeviceMessageMethodEnum = { name: '属性设置', upstream: false }, + PROPERTY_PACK_POST: { + method: 'thing.event.property.pack.post', + name: '批量上报(属性 + 事件 + 子设备)', + upstream: true + }, // ========== 设备事件 ========== EVENT_POST: { @@ -59,6 +98,18 @@ export const IotDeviceMessageMethodEnum = { method: 'thing.config.push', name: '配置推送', upstream: false + }, + + // ========== OTA 固件 ========== + OTA_UPGRADE: { + method: 'thing.ota.upgrade', + name: 'OTA 固件信息推送', + upstream: false + }, + OTA_PROGRESS: { + method: 'thing.ota.progress', + name: 'OTA 升级进度上报', + upstream: true } } diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue index c41da4b2..10d82319 100644 --- a/src/views/mall/product/spu/components/SkuList.vue +++ b/src/views/mall/product/spu/components/SkuList.vue @@ -318,6 +318,7 @@ const props = defineProps({ const formData: Ref = ref() // 表单数据 const skuList = ref([ { + name: '', // SKU 名称 price: 0, // 商品价格 marketPrice: 0, // 市场价 costPrice: 0, // 成本价 @@ -449,6 +450,7 @@ const generateTableData = (propertyList: any[]) => { } for (const item of buildSkuList) { const row = { + name: '', // SKU 名称,提交时会自动使用 SPU 名称 properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象 price: 0, marketPrice: 0, @@ -525,6 +527,7 @@ watch( if (props.isBatch) { skuList.value = [ { + name: '', // SKU 名称 price: 0, marketPrice: 0, costPrice: 0, diff --git a/src/views/mall/product/spu/form/SkuForm.vue b/src/views/mall/product/spu/form/SkuForm.vue index 18cd0296..782d8ed4 100644 --- a/src/views/mall/product/spu/form/SkuForm.vue +++ b/src/views/mall/product/spu/form/SkuForm.vue @@ -173,6 +173,7 @@ const onChangeSpec = () => { // 重置sku列表 formData.skus = [ { + name: '', // SKU 名称,提交时会自动使用 SPU 名称 price: 0, marketPrice: 0, costPrice: 0, diff --git a/src/views/mall/product/spu/form/index.vue b/src/views/mall/product/spu/form/index.vue index c4e4b7b2..bf4aa934 100644 --- a/src/views/mall/product/spu/form/index.vue +++ b/src/views/mall/product/spu/form/index.vue @@ -62,6 +62,7 @@ import OtherForm from './OtherForm.vue' import SkuForm from './SkuForm.vue' import DeliveryForm from './DeliveryForm.vue' import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils' +import { isEmpty } from '@/utils/is' defineOptions({ name: 'ProductSpuAdd' }) @@ -94,6 +95,7 @@ const formData = ref({ subCommissionType: false, // 分销类型 skus: [ { + name: '', // SKU 名称,提交时会自动使用 SPU 名称 price: 0, // 商品价格 marketPrice: 0, // 市场价 costPrice: 0, // 成本价 @@ -158,8 +160,13 @@ const submitForm = async () => { await unref(otherRef)?.validate() // 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据 const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu + // 校验商品名称不能为空(用于 SKU name) + if (isEmpty(deepCopyFormData.name)) { + message.error('商品名称不能为空') + return + } deepCopyFormData.skus!.forEach((item) => { - // 给sku name赋值 + // 给sku name赋值(使用商品名称作为 SKU 名称) item.name = deepCopyFormData.name // sku相关价格元转分 item.price = convertToInteger(item.price)