优化PUT请求传参机制,实现最小化传参以减少网络传输和提高性能,在请求拦截器中自动计算差异只传递修改过的字段,同时新增通用的表单差异比较工具函数,
This commit is contained in:
@@ -137,8 +137,8 @@ export function getAssetSku(id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// 修改 SKU
|
||||
export function updateAssetSku(data: CreateSkuParams & { id: string }) {
|
||||
// 修改 SKU(支持部分更新,只传递修改过的字段)
|
||||
export function updateAssetSku(data: Partial<CreateSkuParams> & { id: string }) {
|
||||
return newService({
|
||||
url: '/assets/asset/sku/updateAssetSku',
|
||||
method: 'put',
|
||||
|
||||
152
src/utils/diffUtils.ts
Normal file
152
src/utils/diffUtils.ts
Normal file
@@ -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<T extends Record<string, any>>(
|
||||
original: T,
|
||||
current: T,
|
||||
options?: {
|
||||
/** 需要包含的字段(即使没有变化也会包含) */
|
||||
alwaysInclude?: string[];
|
||||
/** 需要排除的字段(即使有变化也不会包含) */
|
||||
exclude?: string[];
|
||||
/** 字段值转换器,用于提交前转换值 */
|
||||
transformers?: Record<string, (value: any) => any>;
|
||||
}
|
||||
): Partial<T> {
|
||||
const { alwaysInclude = [], exclude = [], transformers = {} } = options || {};
|
||||
const changed: Partial<T> = {};
|
||||
|
||||
// 遍历当前数据的所有字段
|
||||
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<T extends Record<string, any>>() {
|
||||
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<string, (value: any) => any>;
|
||||
}
|
||||
): Partial<T> {
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Record<string, any>>();
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<SpecValueItem[]>([{ key: '', value: '' }]);
|
||||
const specValuesMap = reactive<Record<string, string>>({});
|
||||
// 使用通用工具函数保存原始数据,用于最小化传参
|
||||
const skuFormDiff = createFormDiff<Record<string, any>>();
|
||||
const specValuesMapDiff = createFormDiff<Record<string, string>>();
|
||||
|
||||
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<string, any> = { 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 ? '编辑成功' : '添加成功');
|
||||
|
||||
@@ -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<DictInfo[]>([]);
|
||||
const dictValueOptions = ref<any[]>([]);
|
||||
const dictLoading = ref(false);
|
||||
// 使用通用工具函数保存原始数据,用于最小化传参
|
||||
const categoryFormDiff = createFormDiff<Record<string, any>>();
|
||||
|
||||
const ruleForm = reactive<RuleForm>({
|
||||
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('添加成功');
|
||||
|
||||
Reference in New Issue
Block a user