diff --git a/src/api/assets/asset/index.ts b/src/api/assets/asset/index.ts index b8a84b0..88bbd65 100644 --- a/src/api/assets/asset/index.ts +++ b/src/api/assets/asset/index.ts @@ -137,8 +137,8 @@ export function getAssetSku(id: string) { }); } -// 修改 SKU -export function updateAssetSku(data: CreateSkuParams & { id: string }) { +// 修改 SKU(支持部分更新,只传递修改过的字段) +export function updateAssetSku(data: Partial & { id: string }) { return newService({ url: '/assets/asset/sku/updateAssetSku', method: 'put', diff --git a/src/utils/diffUtils.ts b/src/utils/diffUtils.ts new file mode 100644 index 0000000..51e4dfa --- /dev/null +++ b/src/utils/diffUtils.ts @@ -0,0 +1,152 @@ +/** + * 差异比较工具函数 + * 用于编辑时最小化传参,只传递修改过的字段 + */ + +/** + * 深度比较两个值是否相等 + * @param val1 值1 + * @param val2 值2 + * @returns 是否相等 + */ +export function isEqual(val1: any, val2: any): boolean { + // 处理 null 和 undefined + if (val1 === val2) return true; + if (val1 == null || val2 == null) return val1 == val2; + + // 处理基本类型 + if (typeof val1 !== 'object' || typeof val2 !== 'object') { + return val1 === val2; + } + + // 处理数组 + if (Array.isArray(val1) && Array.isArray(val2)) { + if (val1.length !== val2.length) return false; + return val1.every((item, index) => isEqual(item, val2[index])); + } + + // 处理对象 + if (Array.isArray(val1) !== Array.isArray(val2)) return false; + + const keys1 = Object.keys(val1); + const keys2 = Object.keys(val2); + if (keys1.length !== keys2.length) return false; + + return keys1.every((key) => isEqual(val1[key], val2[key])); +} + +/** + * 比较两个对象,返回差异部分 + * @param original 原始数据 + * @param current 当前数据 + * @param options 配置选项 + * @returns 差异数据对象 + */ +export function getChangedFields>( + original: T, + current: T, + options?: { + /** 需要包含的字段(即使没有变化也会包含) */ + alwaysInclude?: string[]; + /** 需要排除的字段(即使有变化也不会包含) */ + exclude?: string[]; + /** 字段值转换器,用于提交前转换值 */ + transformers?: Record any>; + } +): Partial { + const { alwaysInclude = [], exclude = [], transformers = {} } = options || {}; + const changed: Partial = {}; + + // 遍历当前数据的所有字段 + const allKeys = new Set([...Object.keys(original), ...Object.keys(current)]); + + allKeys.forEach((key) => { + // 排除指定字段 + if (exclude.includes(key)) return; + + const originalValue = original[key]; + const currentValue = current[key]; + + // 检查是否需要始终包含 + if (alwaysInclude.includes(key)) { + const value = transformers[key] ? transformers[key](currentValue) : currentValue; + (changed as any)[key] = value; + return; + } + + // 比较值是否变化 + if (!isEqual(originalValue, currentValue)) { + const value = transformers[key] ? transformers[key](currentValue) : currentValue; + (changed as any)[key] = value; + } + }); + + return changed; +} + +/** + * 创建一个用于保存和比较表单数据的工具 + * @returns 工具对象 + */ +export function createFormDiff>() { + let originalData: T | null = null; + + return { + /** + * 保存原始数据 + * @param data 原始数据 + */ + saveOriginal(data: T) { + // 深拷贝保存原始数据 + originalData = JSON.parse(JSON.stringify(data)); + }, + + /** + * 获取原始数据 + * @returns 原始数据 + */ + getOriginal(): T | null { + return originalData; + }, + + /** + * 获取变化的字段 + * @param current 当前数据 + * @param options 配置选项 + * @returns 差异数据对象 + */ + getChanges( + current: T, + options?: { + alwaysInclude?: string[]; + exclude?: string[]; + transformers?: Record any>; + } + ): Partial { + if (!originalData) { + // 如果没有原始数据,返回所有当前数据 + return { ...current }; + } + return getChangedFields(originalData, current, options); + }, + + /** + * 检查是否有变化 + * @param current 当前数据 + * @param exclude 排除的字段 + * @returns 是否有变化 + */ + hasChanges(current: T, exclude?: string[]): boolean { + if (!originalData) return true; + const changes = getChangedFields(originalData, current, { exclude }); + return Object.keys(changes).length > 0; + }, + + /** + * 重置原始数据 + */ + reset() { + originalData = null; + }, + }; +} diff --git a/src/utils/request.ts b/src/utils/request.ts index 7443fd5..97e20dc 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from import { ElMessage, ElMessageBox } from 'element-plus'; import { Session } from '/@/utils/storage'; import qs from 'qs'; +import { getChangedFields } from '/@/utils/diffUtils'; // 标记是否正在处理 token 过期,避免重复弹窗 let isHandlingTokenExpired = false; @@ -92,6 +93,33 @@ const requestInterceptor = (config: InternalAxiosRequestConfig) => { // 可以在这里添加 token 有效性检查(如果需要) config.headers!['Authorization'] = `Bearer ${token}`; } + + // PUT 请求最小化传参处理 + // 如果请求数据中包含 _originalData,则自动计算差异,只传递修改过的字段 + if (config.method?.toLowerCase() === 'put' && config.data && typeof config.data === 'object') { + const { _originalData, ...currentData } = config.data; + + if (_originalData && typeof _originalData === 'object') { + // 获取 id 字段(必须保留) + const idField = currentData.id || currentData.Id || currentData.ID; + + // 计算差异 + const changedFields = getChangedFields(_originalData, currentData, { + exclude: ['_originalData', 'id', 'Id', 'ID'], + }); + + // 如果有变化,只传递 id + 变化的字段 + if (Object.keys(changedFields).length > 0) { + config.data = { id: idField, ...changedFields }; + } else { + // 没有变化,只传递 id + config.data = { id: idField }; + } + + console.log('[最小化传参] 原始字段数:', Object.keys(currentData).length, '-> 传递字段数:', Object.keys(config.data).length); + } + } + return config; }; diff --git a/src/views/assets/asset/component/editAsset.vue b/src/views/assets/asset/component/editAsset.vue index 21bd4c6..43d548e 100644 --- a/src/views/assets/asset/component/editAsset.vue +++ b/src/views/assets/asset/component/editAsset.vue @@ -445,6 +445,7 @@ import type { FormInstance, FormRules } from 'element-plus'; import { Plus, Delete } from '@element-plus/icons-vue'; import { getAsset, createAsset, updateAsset, uploadAssetImage } from '/@/api/assets/asset'; import { getCategoryTree, getCategory } from '/@/api/assets/category'; +import { createFormDiff } from '/@/utils/diffUtils'; import Editor from '/@/components/editor/index.vue'; import type { UploadFile, UploadUserFile, UploadRequestOptions } from 'element-plus'; @@ -555,6 +556,8 @@ const dialogVisible = ref(false); const dialogImageUrl = ref(''); // 图片拼接 const fileAddressPrefix = ref(''); +// 使用通用工具函数保存原始数据,用于最小化传参 +const assetFormDiff = createFormDiff>(); const formatImageUrl = (url?: string) => { if (!url) return ''; @@ -1021,6 +1024,9 @@ const openDialog = (row?: any, edit?: boolean) => { categoryAttrs.value = []; }); } + + // 保存原始数据用于最小化传参 + assetFormDiff.saveOriginal(JSON.parse(JSON.stringify(ruleForm))); }) .finally(() => { formLoading.value = false; @@ -1187,7 +1193,21 @@ const onSubmit = async () => { if (valid) { submitLoading.value = true; try { - const requestBody = await buildRequestBody(); + const fullRequestBody = await buildRequestBody(); + + let requestBody: any; + if (isEdit.value) { + // 编辑模式:通过 _originalData 让拦截器自动处理最小化传参 + const originalData = assetFormDiff.getOriginal(); + requestBody = { + ...fullRequestBody, + _originalData: originalData, + }; + } else { + // 新增模式:传递所有字段 + requestBody = fullRequestBody; + } + const request = isEdit.value ? updateAsset(requestBody) : createAsset(requestBody); await request; diff --git a/src/views/assets/asset/component/skuDialog.vue b/src/views/assets/asset/component/skuDialog.vue index cb12137..90929a1 100644 --- a/src/views/assets/asset/component/skuDialog.vue +++ b/src/views/assets/asset/component/skuDialog.vue @@ -174,6 +174,7 @@ import { ref, reactive } from 'vue'; import { ElMessage, type FormInstance, type FormRules } from 'element-plus'; import { ElMessageBox } from 'element-plus'; import { listAssetSkus, createAssetSku, updateAssetSku, deleteAssetSku, getAssetSku, getAsset, uploadAssetImage, getSpecsUnitOptions } from '/@/api/assets/asset'; +import { createFormDiff } from '/@/utils/diffUtils'; import type { UploadRequestOptions, UploadUserFile } from 'element-plus'; interface SpecValueItem { @@ -228,6 +229,9 @@ const skuForm = reactive({ const specValuesList = ref([{ key: '', value: '' }]); const specValuesMap = reactive>({}); +// 使用通用工具函数保存原始数据,用于最小化传参 +const skuFormDiff = createFormDiff>(); +const specValuesMapDiff = createFormDiff>(); const skuRules: FormRules = { skuName: [{ required: true, message: '请输入SKU名称', trigger: 'blur' }], @@ -402,6 +406,19 @@ const onEditSku = async (row: any) => { skuForm.specsUnit = data.specsUnit || ''; } skuForm.specsCount = data.specsCount || 1; + // 使用工具函数保存原始数据用于最小化传参 + skuFormDiff.saveOriginal({ + skuName: skuForm.skuName, + price: skuForm.price, + stock: skuForm.stock, + unlimitedStock: skuForm.unlimitedStock, + status: skuForm.status, + sort: skuForm.sort, + imageUrl: skuForm.imageUrl, + description: skuForm.description, + specsUnit: skuForm.specsUnit, + specsCount: skuForm.specsCount, + }); // 图片预览回显 if (data.imageUrl) { skuImagePreview.value = formatImageUrl(data.imageUrl); @@ -437,6 +454,8 @@ const onEditSku = async (row: any) => { } else { specValuesList.value = [{ key: '', value: '' }]; } + // 保存原始规格属性数据 + specValuesMapDiff.saveOriginal({ ...specValuesMap }); } catch (error) { skuFormVisible.value = false; } finally { @@ -599,7 +618,7 @@ const onSubmitSku = async () => { }); // 构建 specsUnit 对象格式 - let specsUnitObj = undefined; + let specsUnitObj: { key: string; value: string } | undefined = undefined; if (skuForm.specsUnit) { const unitOption = specsUnitOptions.value.find((opt) => opt.key === skuForm.specsUnit); specsUnitObj = { @@ -608,26 +627,57 @@ const onSubmitSku = async () => { }; } - const data = { - assetId: assetId.value, - assetName: assetName.value, - skuName: skuForm.skuName, - imageUrl: skuForm.imageUrl || undefined, - specValues: specValues.length > 0 ? specValues : undefined, - price: Math.round(skuForm.price * 100), - unlimitedStock: skuForm.unlimitedStock, - stock: skuForm.stock, - sort: skuForm.sort, - status: skuForm.status, - description: skuForm.description, - specsUnit: specsUnitObj, - specsCount: skuForm.specsCount || undefined, - }; - try { if (isEditSku.value) { - await updateAssetSku({ ...data, id: editSkuId.value }); + // 编辑模式:使用工具函数获取修改过的字段 + const currentFormData = { + skuName: skuForm.skuName, + price: skuForm.price, + stock: skuForm.stock, + unlimitedStock: skuForm.unlimitedStock, + status: skuForm.status, + sort: skuForm.sort, + imageUrl: skuForm.imageUrl, + description: skuForm.description, + specsUnit: skuForm.specsUnit, + specsCount: skuForm.specsCount, + }; + + const changedFields = skuFormDiff.getChanges(currentFormData, { + alwaysInclude: ['id'], + transformers: { + price: (val) => Math.round(val * 100), + specsUnit: () => specsUnitObj, + imageUrl: (val) => val || undefined, + }, + }); + + // 添加 id + const changedData: Record = { id: editSkuId.value, ...changedFields }; + + // 比较规格属性 + if (specValuesMapDiff.hasChanges(specValuesMap) && specValues.length > 0) { + changedData.specValues = specValues; + } + + await updateAssetSku(changedData as any); } else { + // 新增模式:传递所有字段 + const data = { + assetId: assetId.value, + assetName: assetName.value, + skuName: skuForm.skuName, + imageUrl: skuForm.imageUrl || undefined, + specValues: specValues.length > 0 ? specValues : undefined, + price: Math.round(skuForm.price * 100), + unlimitedStock: skuForm.unlimitedStock, + stock: skuForm.stock, + sort: skuForm.sort, + status: skuForm.status, + description: skuForm.description, + specsUnit: specsUnitObj, + specsCount: skuForm.specsCount || undefined, + }; await createAssetSku(data); } ElMessage.success(isEditSku.value ? '编辑成功' : '添加成功'); diff --git a/src/views/assets/category/component/editCategory.vue b/src/views/assets/category/component/editCategory.vue index 8c09c00..7fb5ea5 100644 --- a/src/views/assets/category/component/editCategory.vue +++ b/src/views/assets/category/component/editCategory.vue @@ -126,6 +126,7 @@ import { ElMessage } from 'element-plus'; import { Plus, Delete } from '@element-plus/icons-vue'; import { getCategoryTree, getCategory, addCategory, updateCategory, getCategoryAttrTypeOptions } from '/@/api/assets/category'; import { getDicts } from '/@/api/system/dict/data'; +import { createFormDiff } from '/@/utils/diffUtils'; interface CategoryRow { id: string; @@ -181,6 +182,8 @@ const attrTypeOptions = ref<{ key: string; value: string }[]>([]); const dictTypeOptions = ref([]); const dictValueOptions = ref([]); const dictLoading = ref(false); +// 使用通用工具函数保存原始数据,用于最小化传参 +const categoryFormDiff = createFormDiff>(); const ruleForm = reactive({ id: '', @@ -362,7 +365,12 @@ const openDialog = (row?: CategoryRow | string, edit?: boolean) => { } } }); + // 保存原始数据用于最小化传参 + categoryFormDiff.saveOriginal(JSON.parse(JSON.stringify(ruleForm))); }); + } else { + // 保存原始数据用于最小化传参 + categoryFormDiff.saveOriginal(JSON.parse(JSON.stringify(ruleForm))); } }); } else if (row && typeof row === 'string') { @@ -432,10 +440,16 @@ const onSubmit = () => { options: [], }; }); - const submitData = { ...ruleForm, attrs: processedAttrs }; if (isEdit.value) { - // 修改 + // 编辑模式:通过 _originalData 让拦截器自动处理最小化传参 + const originalData = categoryFormDiff.getOriginal(); + const submitData = { + ...ruleForm, + attrs: processedAttrs, + _originalData: originalData, + }; + updateCategory(submitData) .then(() => { ElMessage.success('修改成功'); @@ -446,7 +460,8 @@ const onSubmit = () => { submitLoading.value = false; }); } else { - // 新增 + // 新增模式:传递所有字段 + const submitData = { ...ruleForm, attrs: processedAttrs }; addCategory(submitData) .then(() => { ElMessage.success('添加成功');