Files
workspace/code/g3hd/frontend/src/utils/dataDiffUtils.js
T
2026-08-08 17:29:51 +08:00

693 lines
15 KiB
JavaScript

/* 比对并分类数据变更
* @param {Array} currentData 变更后的数据数组
* @param {Array} originalData 原始数据数组
* @param {String} primaryKey 主键字段名
* @param {Array} ignoreFields 过滤字段(可传可不传)
* @returns {Object} 包含分类结果的对象 { insertdata: [], updatedata: [], deletedata: [], hasChanges: Boolean }
*/
export function compareAndClassifyData(currentData, originalData, primaryKey) {
const versionFields = ["mx_version", "b_version"];
const getVersionFields = (row) => {
const fields = {};
versionFields.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(row, key)) {
fields[key] = row[key] ?? "";
}
});
return fields;
};
const result = {
inserts: [],
updates: [],
deletes: [],
hasChanges: false, // 默认无变动
};
// 1. 找出新增的数据
result.inserts = currentData.filter(
(row) =>
!row[primaryKey] ||
!originalData.some((originalRow) => originalRow[primaryKey] === row[primaryKey]),
);
// 2. 找出修改的数据
result.updates = currentData.reduce((acc, row) => {
const rowId = row[primaryKey];
if (!rowId) return acc;
const originalRow = originalData.find((r) => r[primaryKey] === rowId);
if (!originalRow) return acc;
const changedFields = {};
Object.keys(row).forEach((key) => {
if (key !== primaryKey && !key.startsWith("v_") && !versionFields.includes(key)) {
const oldVal = originalRow[key];
const newVal = row[key];
const normalize = (v) => (v === null || v === undefined ? "" : v);
if (normalize(newVal) !== normalize(oldVal)) {
// ✅ 避免 undefined 被丢弃
changedFields[key] = newVal ?? "";
}
}
});
// Object.keys(row).forEach((key) => {
// if (key !== primaryKey && !isEqual(row[key], originalRow[key])) {
// if (!key.startsWith('v_')) {
// changedFields[key] = row[key]
// }
// }
// })
if (Object.keys(changedFields).length > 0) {
acc.push({
[primaryKey]: rowId,
...getVersionFields(row),
...changedFields,
});
}
return acc;
}, []);
// 3. 找出删除的数据
result.deletes = originalData.filter(
(originalRow) => !currentData.some((row) => row[primaryKey] === originalRow[primaryKey]),
);
// 4. 是否有任何变动
result.hasChanges =
result.inserts.length > 0 || result.updates.length > 0 || result.deletes.length > 0;
return result;
}
/**
* 在数组对象的每个元素中覆盖式填入 JSON 对象的值
* @param {Array} arr - 目标数组(将被修改)
* @param {object} json - 包含要添加/覆盖的键值对的 JSON 对象
* @returns {Array} 修改后的数组
* @throws {Error} 当参数类型不正确时抛出错误
*/
// export function fillDefaultArray(arr, json) {
// // 基本数据验证
// if (!Array.isArray(arr))
// throw new Error('第一个参数必须是数组')
// if (typeof json !== 'object' || json === null || Array.isArray(json)) {
// throw new Error('第二个参数必须是普通对象(JSON对象)')
// }
// // 覆盖式填充每个元素
// return arr.map((item) => {
// // 只处理对象元素
// if (typeof item === 'object' && item !== null) {
// return { ...item, ...json } // 覆盖已有属性
// }
// return item // 非对象元素保持不变
// })
// }
export function fillDefaultArray(arr, json) {
// 基本数据验证
if (!Array.isArray(arr)) throw new Error("第一个参数必须是数组");
if (typeof json !== "object" || json === null || Array.isArray(json)) {
throw new Error("第二个参数必须是普通对象(JSON对象)");
}
// 原地修改数组中的每个对象
for (const item of arr) {
if (typeof item === "object" && item !== null) {
Object.assign(item, json);
}
}
// 可选:方便链式调用
return arr;
}
// export function fillSerialNoAndDefaults(arr, defaults = {}) {
// if (!Array.isArray(arr)) return;
// arr.forEach((item, index) => {
// if (typeof item === "object" && item !== null) {
// item.mx_xh = index + 1;
// Object.assign(item, defaults);
// }
// });
// }
export function fillSerialNoAndDefaults(arr, defaults = {}) {
if (!Array.isArray(arr)) return;
arr.forEach((item, index) => {
if (typeof item === "object" && item !== null) {
item.mx_xh = index + 1;
Object.entries(defaults).forEach(([key, value]) => {
item[key] = typeof value === "function" ? value(item, index) : value;
});
}
});
}
export function compareAndClassifyData1(currentData, originalData, primaryKey, ignoreFields = []) {
const ignoreSet = new Set(ignoreFields);
const result = {
insertdata: [],
updatedata: [],
deletedata: [],
hasChanges: false,
};
// 工具函数:剔除不需要的字段
const omitFields = (row) => {
const res = {};
Object.keys(row).forEach((key) => {
if (key !== primaryKey && ignoreSet.has(key)) return;
res[key] = row[key];
});
return res;
};
// 1️⃣ 新增
result.insertdata = currentData
.filter(
(row) => !row[primaryKey] || !originalData.some((o) => o[primaryKey] === row[primaryKey]),
)
.map((row) => omitFields(row)); // 在插入时过滤字段
// 2️⃣ 修改
result.updatedata = currentData.reduce((acc, row) => {
const rowId = row[primaryKey];
if (!rowId) return acc;
const originalRow = originalData.find((r) => r[primaryKey] === rowId);
if (!originalRow) return acc;
const changedFields = {};
Object.keys(row).forEach((key) => {
if (key === primaryKey || ignoreSet.has(key)) return; // 忽略指定字段
if (row[key] !== originalRow[key]) changedFields[key] = row[key];
});
if (Object.keys(changedFields).length > 0) {
acc.push({
[primaryKey]: rowId,
...omitFields(changedFields), // 在更新时也过滤字段
});
}
return acc;
}, []);
// 3️⃣ 删除,只保留 primaryKey
result.deletedata = originalData
.filter((o) => !currentData.some((c) => c[primaryKey] === o[primaryKey]))
.map((row) => ({ [primaryKey]: row[primaryKey] }));
// 4️⃣ 是否有变动
result.hasChanges =
result.insertdata.length > 0 || result.updatedata.length > 0 || result.deletedata.length > 0;
return result;
}
/**
* 深度比较两个值是否相等
* @param {*} a 第一个值
* @param {*} b 第二个值
* @returns {boolean} 是否相等
*/
export function isEqual(a, b) {
// 处理基本类型和null/undefined
if (a === b) return true;
if (typeof a !== typeof b) return false;
if (typeof a !== "object" || a === null || b === null) {
return a === b;
}
// 处理数组
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item, i) => isEqual(item, b[i]));
}
// 处理对象
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => isEqual(a[key], b[key]));
}
/**
* 检测并提取变更的字段(跳过v_开头的字段)
* @param {object} currentData 当前数据
* @param {object} originalData 原始数据
* @param {string} primaryKey 主键字段名
* @param {string} primaryKeyValue 主键值
* @returns {object} 包含变更数据的对象,结构为 { hasChanges: boolean, updateData: Object }
*/
export function detectChanges(currentData, originalData, primaryKey, primaryKeyValue) {
const updateValue = {};
// 遍历所有字段检测变更
Object.keys(currentData).forEach((key) => {
// 跳过以v_开头的字段
if (key.startsWith("v_")) return;
if (key === "b_type") {
const currentStr = normalizeOwnerCustomer(currentData[key]);
const originalStr = normalizeOwnerCustomer(originalData[key]);
if (currentStr !== originalStr) {
updateValue[key] = currentStr; // 保存时格式化为逗号分隔字符串
}
} else {
// 比较前,先将 undefined 转换为空字符串
const currentValue = currentData[key] === undefined ? "" : currentData[key];
const originalValue = originalData[key] === undefined ? "" : originalData[key];
// 使用JSON.stringify进行深度比较
if (JSON.stringify(currentValue) !== JSON.stringify(originalValue)) {
updateValue[key] = currentValue;
}
}
});
// 如果没有有效变更
if (Object.keys(updateValue).length === 0) {
return { hasChanges: true, updateData: [] };
}
// 返回变更数据和主键
return {
hasChanges: true,
updateData: [
{
[primaryKey]: primaryKeyValue,
...updateValue,
},
],
};
}
/**
* 检测并提取变更的字段(跳过v_开头的字段和指定排除字段)
* @param {object} currentData 当前数据
* @param {object} originalData 原始数据
* @param {Array|string} primaryKeys 主键字段名或数组
* @param {Array} excludeKeys 可选,排除的字段数组
* @returns {object} 包含变更数据的对象,结构为 { hasChanges: boolean, updateData: Array }
*/
export function detectChangesFields(currentData, originalData, primaryKeys, excludeKeys = []) {
const updateValue = {};
Object.keys(currentData).forEach((key) => {
// 跳过以v_开头的字段
if (key.startsWith("v_")) return;
// 跳过排除字段
if (excludeKeys.includes(key)) return;
const currentValue = currentData[key] === undefined ? "" : currentData[key];
const originalValue = originalData[key] === undefined ? "" : originalData[key];
if (JSON.stringify(currentValue) !== JSON.stringify(originalValue)) {
updateValue[key] = currentValue;
}
});
if (Object.keys(updateValue).length === 0) {
return { hasChanges: false, updateData: [] };
}
// 构建主键字段对象
const primaryKeyObj = {};
if (Array.isArray(primaryKeys)) {
primaryKeys.forEach((key) => {
primaryKeyObj[key] = originalData[key];
});
} else {
primaryKeyObj[primaryKeys] = originalData[primaryKeys];
}
return {
hasChanges: true,
updateData: [
{
...primaryKeyObj,
...updateValue,
},
],
};
}
function normalizeOwnerCustomer(val) {
if (typeof val === "string") {
return val.split(",").filter(Boolean).join(","); // '001,002,003'
}
if (Array.isArray(val)) {
return val.map((item) => (Array.isArray(item) ? item[0] : item)).join(",");
}
return "";
}
/**
* 统一数据差异比对(主表 / 子表通用)
*
* @param {Array | object} current 当前数据
* @param {Array | object} original 原始数据
* @param {string | Array} primaryKey 主键
* @param {object} options
* @param {Array} options.ignoreFields 忽略字段
* @param {boolean} options.isSingle 是否单条主表
*/
export function diffData(
current,
original,
primaryKey,
{ ignoreFields = [], isSingle = false } = {},
) {
const ignoreSet = new Set(ignoreFields);
const versionFields = new Set(["b_version", "mx_version"]);
/**
* 判断是否为数字类型
*
* 支持:
* 40
* "40"
* "40.00"
*/
const isNumberValue = (value) => {
if (typeof value === "number") {
return !Number.isNaN(value);
}
if (typeof value === "string") {
if (value.trim() === "") {
return false;
}
return !Number.isNaN(Number(value));
}
return false;
};
/**
* 数据值比较
*
* 数字:
* 40 === "40.00"
*
* 非数字:
* 保持原 JSON 比较逻辑
*/
const isEqualValue = (a, b) => {
// null undefined 空字符串统一
if (a == null || a === "") {
a = "";
}
if (b == null || b === "") {
b = "";
}
// 两边都是数字,按数值比较
if (isNumberValue(a) && isNumberValue(b)) {
return Math.abs(Number(a) - Number(b)) < 0.000001;
}
// 其他类型保持原来的比较方式
return JSON.stringify(a) === JSON.stringify(b);
};
const result = {
hasChanges: false,
insertdata: [],
updatedata: [],
deletedata: [],
};
const filterFields = (row) => {
return Object.fromEntries(
Object.entries(row).filter(([key]) => {
return (
!key.startsWith("_") &&
!key.startsWith("v_") &&
!ignoreSet.has(key)
);
}),
);
};
// 获取版本字段
const getVersionData = (row) => {
const data = {};
Object.keys(row).forEach((key) => {
if (versionFields.has(key)) {
data[key] = row[key];
}
});
return data;
};
// ===========================
// 单表
// ===========================
if (isSingle) {
const isNewRecord =
!original ||
Object.keys(original).length === 0 ||
!original[primaryKey];
if (isNewRecord) {
result.hasChanges = true;
result.insertdata.push(filterFields(current));
return result;
}
const changes = {};
Object.keys(current).forEach((key) => {
// 版本字段不参与比较
if (versionFields.has(key)) {
return;
}
if (
key.startsWith("_") ||
key.startsWith("v_") ||
ignoreSet.has(key)
) {
return;
}
const cur = current[key] ?? "";
const org = original[key] ?? "";
if (!isEqualValue(cur, org)) {
changes[key] = cur;
}
});
if (Object.keys(changes).length > 0) {
result.hasChanges = true;
result.updatedata.push({
[primaryKey]: current[primaryKey],
// 修改字段
...changes,
// 版本
...getVersionData(current),
});
}
return result;
}
// ===========================
// 子表
// ===========================
const pk = Array.isArray(primaryKey)
? primaryKey[0]
: primaryKey;
const originalMap = new Map(
original.map((item) => [item[pk], item]),
);
// 新增
result.insertdata = current
.filter(
(row) =>
!row[pk] ||
!originalMap.has(row[pk]),
)
.map(filterFields);
// 修改
current.forEach((row) => {
const id = row[pk];
if (!id) {
return;
}
const oldRow = originalMap.get(id);
if (!oldRow) {
return;
}
const changes = {};
Object.keys(row).forEach((key) => {
// 版本字段不比较
if (versionFields.has(key)) {
return;
}
if (
key === pk ||
key.startsWith("_") ||
key.startsWith("v_") ||
ignoreSet.has(key)
) {
return;
}
const cur = row[key] ?? "";
const org = oldRow[key] ?? "";
if (!isEqualValue(cur, org)) {
changes[key] = cur;
}
});
if (Object.keys(changes).length > 0) {
result.updatedata.push({
[pk]: id,
...changes,
// 版本
...getVersionData(row),
});
}
});
// 删除
result.deletedata = original
.filter(
(o) =>
!current.some(
(c) => c[pk] === o[pk],
),
)
.map((o) => ({
[pk]: o[pk],
// 删除带版本
...getVersionData(o),
}));
result.hasChanges =
result.insertdata.length > 0 ||
result.updatedata.length > 0 ||
result.deletedata.length > 0;
return result;
}
// 默认导出所有方法
export default {
isEqual,
compareAndClassifyData,
detectChanges,
fillDefaultArray,
diffData,
fillSerialNoAndDefaults,
};