MarketingSystemDataExportTool/web/modules/fields.js

561 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 字段处理模块 - 字段路径转换、树结构构建
* @module fields
*/
;(function() {
'use strict';
// 依赖AppConfig
/**
* 字段管理器类
* 管理字段元数据、树结构构建和路径转换
*/
class FieldsManager {
constructor() {
// 字段映射:{ tableName: [{ value, label }] }
this.fieldsMap = {};
// 表标签映射:{ tableName: label }
this.tableLabels = {};
// 推荐字段列表
this.recommendedFields = [];
}
/**
* 更新字段元数据
* @param {Array} tables - 表定义数组
* @param {Array} recommended - 推荐字段数组
*/
updateMetadata(tables, recommended = []) {
const map = {};
const labels = {};
tables.forEach(table => {
const fields = Array.isArray(table.fields) ? table.fields : [];
const visibleFields = fields.filter(f => !f.hidden);
map[table.table] = visibleFields.map(f => ({
value: f.field,
label: f.label
}));
if (table.label) {
labels[table.table] = table.label;
}
});
this.fieldsMap = map;
this.tableLabels = labels;
this.recommendedFields = recommended;
}
/**
* 获取表标签
* @param {string} table - 表名
* @returns {string} 表标签
*/
getTableLabel(table) {
return this.tableLabels[table]
|| window.AppConfig?.DEFAULT_TABLE_LABELS[table]
|| table;
}
/**
* 获取指定表的字段列表
* @param {string} table - 表名
* @returns {Array<{value: string, label: string}>} 字段列表
*/
getTableFields(table) {
return this.fieldsMap[table] || [];
}
/**
* 检查表中是否存在某字段
* @param {string} table - 表名
* @param {string} field - 字段名
* @returns {boolean} 是否存在
*/
hasField(table, field) {
const fields = this.fieldsMap[table] || [];
return fields.some(f => f.value === field);
}
/**
* 获取所有表名
* @returns {string[]} 表名列表
*/
getAllTables() {
return Object.keys(this.fieldsMap);
}
/**
* 构建树节点
* @param {string} table - 表名
* @param {Array} children - 子节点
* @param {string} parentPrefix - 父级前缀用于构建唯一value
* @returns {Object} 树节点
*/
buildTreeNode(table, children = [], parentPrefix = '') {
// 为子表的字段节点添加表名前缀,确保 value 唯一
const childrenWithPrefix = children.map(child => {
if (child.children) {
// 是子表节点,递归处理
return child;
} else {
// 是字段节点,添加表名前缀
return {
value: `${table}.${child.value}`,
label: child.label
};
}
});
return {
value: parentPrefix ? `${parentPrefix}.${table}` : table,
label: this.getTableLabel(table),
children: childrenWithPrefix
};
}
/**
* 构建易码通订单子节点
* @param {number} orderType - 订单类型
* @returns {Array} 子节点数组
*/
buildYmtOrderChildren(orderType) {
const orderFields = this.getTableFields('order');
// 使用 table.field 格式作为唯一 value
const children = orderFields.map(f => ({ value: `order.${f.value}`, label: f.label }));
// 添加公共子表(所有订单类型都有)
children.push(this.buildTreeNode('merchant', this.getTableFields('merchant')));
children.push(this.buildTreeNode('activity', this.getTableFields('activity')));
// 根据订单类型添加特定子表
if (orderType === 2) {
// 直充卡密
children.push(this.buildTreeNode('order_digit', this.getTableFields('order_digit')));
} else if (orderType === 3) {
// 立减金
children.push(this.buildTreeNode('order_voucher', this.getTableFields('order_voucher')));
children.push(this.buildTreeNode('goods_voucher_batch', this.getTableFields('goods_voucher_batch')));
children.push(this.buildTreeNode('goods_voucher_subject_config', this.getTableFields('goods_voucher_subject_config')));
} else if (orderType === 1) {
// 红包
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
} else {
// 未指定类型或全部类型orderType = 0 或其他)
children.push(this.buildTreeNode('order_voucher', this.getTableFields('order_voucher')));
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
children.push(this.buildTreeNode('order_digit', this.getTableFields('order_digit')));
children.push(this.buildTreeNode('goods_voucher_batch', this.getTableFields('goods_voucher_batch')));
children.push(this.buildTreeNode('goods_voucher_subject_config', this.getTableFields('goods_voucher_subject_config')));
}
// 调试:检查是否有重复的 value
const valueMap = new Map();
const checkDuplicates = (nodes, prefix = '') => {
nodes.forEach(node => {
const fullPath = prefix ? `${prefix}/${node.value}` : node.value;
if (valueMap.has(node.value)) {
console.warn(`[树节点重复] 检测到重复的节点value: "${node.value}", 路径: ${fullPath}, 之前出现在: ${valueMap.get(node.value)}`);
} else {
valueMap.set(node.value, fullPath);
}
if (node.children && node.children.length > 0) {
checkDuplicates(node.children, fullPath);
}
});
};
checkDuplicates(children, 'order');
return children;
}
/**
* 构建营销系统订单子节点
* @param {number} orderType - 订单类型
* @returns {Array} 子节点数组
*/
buildMarketingOrderChildren(orderType) {
const orderFields = this.getTableFields('order');
// 使用 table.field 格式作为唯一 value
const children = orderFields.map(f => ({ value: `order.${f.value}`, label: f.label }));
// 订单详情
children.push(this.buildTreeNode('order_detail', this.getTableFields('order_detail')));
// 构建计划节点(包含 key_batch -> code_batch 嵌套)
const planChildren = [
...this.getTableFields('plan').map(f => ({ value: `plan.${f.value}`, label: f.label })),
this.buildTreeNode('key_batch', [
...this.getTableFields('key_batch').map(f => ({ value: `key_batch.${f.value}`, label: f.label })),
this.buildTreeNode('code_batch', this.getTableFields('code_batch'))
])
];
// 构建立减金节点(包含 voucher -> voucher_batch 嵌套)
const voucherChildren = [
...this.getTableFields('order_voucher').map(f => ({ value: `order_voucher.${f.value}`, label: f.label })),
this.buildTreeNode('voucher', [
...this.getTableFields('voucher').map(f => ({ value: `voucher.${f.value}`, label: f.label })),
this.buildTreeNode('voucher_batch', this.getTableFields('voucher_batch'))
])
];
// 根据订单类型添加特定子表
if (orderType === 1) {
// 直充卡密
children.push(this.buildTreeNode('plan', planChildren));
children.push(this.buildTreeNode('merchant_key_send', this.getTableFields('merchant_key_send')));
} else if (orderType === 2) {
// 立减金
children.push(this.buildTreeNode('order_voucher', voucherChildren));
children.push(this.buildTreeNode('plan', planChildren));
} else if (orderType === 3) {
// 红包
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
children.push(this.buildTreeNode('plan', planChildren));
} else {
// 全部类型
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
children.push(this.buildTreeNode('order_voucher', voucherChildren));
children.push(this.buildTreeNode('plan', planChildren));
children.push(this.buildTreeNode('merchant_key_send', this.getTableFields('merchant_key_send')));
}
return children;
}
/**
* 构建字段选择树数据
* @param {string} datasource - 数据源
* @param {number} orderType - 订单类型
* @param {string} [mainTable] - 主表名(可选,用于编辑模式)
* @returns {Array} 树数据
*/
buildFieldTree(datasource, orderType, mainTable) {
const type = Number(orderType || 0);
if (datasource === 'ymt') {
const actualMainTable = (mainTable === 'order_info') ? 'order' : (mainTable || 'order');
const orderNode = this.buildTreeNode(actualMainTable, this.buildYmtOrderChildren(type));
return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }];
} else {
const tableForTree = mainTable || 'order';
const orderNode = this.buildTreeNode(tableForTree, this.buildMarketingOrderChildren(type));
return type ? [orderNode] : [{ value: 'scene_order', label: '订单数据', children: [orderNode] }];
}
}
/**
* 将 table.field 格式转换为路径数组格式
* @param {string[]} fields - 字段数组,格式为 ['table.field', ...]
* @param {string} datasource - 数据源
* @param {string} mainTable - 主表名
* @returns {Array<string[]>} 路径数组,格式为 [['table', 'field'], ...]
*/
convertFieldsToPaths(fields, datasource, mainTable) {
const actualMainTable = (datasource === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable;
const relations = window.AppConfig?.getTableRelations(datasource) || {};
const toPath = (tableField) => {
const parts = String(tableField || '').split('.');
if (parts.length !== 2) return null;
let table = parts[0];
const field = parts[1];
// ymt 数据源 order_info 映射为 order
if (datasource === 'ymt' && table === 'order_info') {
table = 'order';
}
// 查找表的路径关系
const relation = relations[table];
if (!relation) {
// 未知表,尝试直接返回
return [actualMainTable, table, field];
}
const pathToTable = relation.path || [];
if (pathToTable.length === 0) {
// 主表字段
return [actualMainTable, field];
} else {
// 子表字段
return [actualMainTable, ...pathToTable, field];
}
};
const paths = fields
.map(toPath)
.filter(path => Array.isArray(path) && path.length >= 2);
// 添加调试日志:验证字段存在性
const validPaths = paths.filter(path => {
const table = path[path.length - 2];
const field = path[path.length - 1];
const exists = this.hasField(table, field);
if (!exists) {
console.warn(`[字段回显] 字段不存在于元数据中: ${table}.${field}, 路径: ${JSON.stringify(path)}`);
}
return exists;
});
console.log(`[字段回显] 共 ${fields.length} 个字段, 转换为 ${paths.length} 个路径, 验证通过 ${validPaths.length}`);
return validPaths;
}
/**
* 将路径数组格式转换为 table.field 格式
* @param {Array<string[]>} paths - 路径数组
* @param {string} datasource - 数据源
* @param {string} mainTable - 主表名
* @returns {string[]} 字段数组,格式为 ['table.field', ...]
*/
convertPathsToFields(paths, datasource, mainTable) {
const actualMainTable = (datasource === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable;
const allTables = this.getAllTables();
// 检查是否只选中了主表节点(表示选择整表)
const hasMainTableOnly = paths.some(
p => Array.isArray(p) && p.length === 1 && p[0] === actualMainTable
);
if (hasMainTableOnly) {
// 返回主表所有字段
return this.getTableFields(actualMainTable).map(f => `${mainTable}.${f.value}`);
}
return paths.flatMap(path => {
if (!Array.isArray(path)) return [];
// 检查是否是组节点(表节点而非字段节点)
const lastPart = path[path.length - 1];
if (allTables.includes(lastPart)) {
// 是表节点,跳过
return [];
}
if (path.length >= 2) {
const table = path[path.length - 2];
const field = path[path.length - 1];
// ymt 数据源需要将 order 转回 order_info
const finalTable = (datasource === 'ymt' && table === 'order') ? 'order_info' : table;
return [`${finalTable}.${field}`];
}
return [];
});
}
/**
* 路径数组去重
* @param {Array<string[]>} paths - 路径数组
* @returns {Array<string[]>} 去重后的路径数组
*/
deduplicatePaths(paths = []) {
const seen = new Set();
const result = [];
for (const path of paths) {
if (!Array.isArray(path)) continue;
const key = path.join('|');
if (seen.has(key)) continue;
seen.add(key);
result.push(path);
}
return result;
}
/**
* 获取主表所有叶子字段的路径
* @param {string} datasource - 数据源
* @returns {Array<string[]>} 路径数组
*/
getMainTableLeafPaths(datasource) {
const mainTable = datasource === 'ymt' ? 'order' : 'order';
return this.getTableFields(mainTable).map(f => [mainTable, f.value]);
}
}
/**
* 树节点工具函数
*/
const TreeUtils = {
/**
* 在树中查找节点
* @param {Array} nodes - 树节点数组
* @param {string} targetKey - 目标节点 key
* @returns {Object|null} 找到的节点
*/
findNode(nodes, targetKey) {
for (const node of nodes) {
if (node.value === targetKey) {
return node;
}
if (node.children) {
const found = TreeUtils.findNode(node.children, targetKey);
if (found) return found;
}
}
return null;
},
/**
* 查找节点的完整路径
* @param {Array} nodes - 树节点数组
* @param {string} targetKey - 目标节点 key
* @param {string[]} currentPath - 当前路径
* @returns {string[]|null} 节点路径
*/
findNodePath(nodes, targetKey, currentPath = []) {
for (const node of nodes) {
const newPath = [...currentPath, node.value];
if (node.value === targetKey) {
return newPath;
}
if (node.children && node.children.length > 0) {
const found = TreeUtils.findNodePath(node.children, targetKey, newPath);
if (found) return found;
}
}
return null;
},
/**
* 根据路径数组查找节点 value
* @param {Array} nodes - 树节点数组
* @param {string[]} path - 路径数组,如 ['order', 'status'] 或 ['order', 'merchant', 'name']
* @param {number} index - 当前索引
* @returns {string|null} 节点 value
*/
findNodeByPath(nodes, path, index = 0) {
if (!nodes || index >= path.length) return null;
const target = path[index];
for (const node of nodes) {
// 节点value可能是 'order', 'order.status', 'merchant' 等格式
const nodeTableOrField = node.value.split('.').pop(); // 取最后一部分
const nodeValue = node.value;
// 检查是否匹配当前路径段
const isMatch = nodeValue === target || nodeTableOrField === target || node.value.endsWith(`.${target}`);
if (isMatch) {
if (index === path.length - 1) {
// 到达路径末尾返回节点value
return node.value;
} else if (node.children && node.children.length > 0) {
// 继续在子节点中查找
const found = TreeUtils.findNodeByPath(node.children, path, index + 1);
if (found) return found;
}
}
}
// 特殊处理:如果路径是 ['order', 'status'],直接尝试查找 'order.status'
if (index === 0 && path.length === 2) {
const combinedValue = `${path[0]}.${path[1]}`;
for (const node of nodes) {
if (node.value === combinedValue) {
return node.value;
}
if (node.children) {
const found = TreeUtils.findNode(node.children, combinedValue);
if (found) return found.value;
}
}
}
return null;
},
/**
* 判断节点是否为叶子节点
* @param {Object} node - 节点对象
* @returns {boolean} 是否为叶子节点
*/
isLeafNode(node) {
return !node.children || node.children.length === 0;
},
/**
* 将选中的 keys 转换为路径数组
* @param {string[]} checkedKeys - 选中的节点 keys格式如 ['order.status', 'merchant.name']
* @param {Array} treeData - 树数据
* @returns {Array<string[]>} 路径数组(仅叶子节点),格式如 [['order', 'status'], ['order', 'merchant', 'name']]
*/
checkedKeysToLeafPaths(checkedKeys, treeData) {
return checkedKeys
.filter(key => {
const node = TreeUtils.findNode(treeData, key);
return node && TreeUtils.isLeafNode(node);
})
.map(key => {
// key 格式可能是 'order.status' 或 'merchant.name'
// 需要根据树结构找到完整路径
const path = TreeUtils.findNodePath(treeData, key);
if (path && path.length >= 2) {
// path 是节点value的数组需要转换为实际的表/字段路径
// 例如:['order', 'order.status'] -> ['order', 'status']
// ['order', 'merchant', 'merchant.name'] -> ['order', 'merchant', 'name']
return path.map((segment, index) => {
if (segment.includes('.')) {
// 是字段节点,取字段名部分
return segment.split('.').pop();
}
return segment;
});
}
// 回退方案:直接使用 key
return key.includes('.') ? key.split('.') : [key];
})
.filter(path => path.length >= 2);
},
/**
* 将路径数组转换为节点 keys
* @param {Array<string[]>} paths - 路径数组
* @param {Array} treeData - 树数据
* @returns {string[]} 节点 keys
*/
pathsToNodeKeys(paths, treeData) {
const keys = paths.map(path => {
if (Array.isArray(path)) {
const nodeValue = TreeUtils.findNodeByPath(treeData, path);
if (nodeValue) {
return nodeValue;
} else {
// 找不到时使用路径最后一部分
const fallbackKey = path[path.length - 1];
console.warn(`[树节点转换] 路径未找到对应节点: ${JSON.stringify(path)}, 使用回退key: ${fallbackKey}`);
return fallbackKey;
}
} else if (typeof path === 'string') {
return path;
}
return null;
}).filter(Boolean);
console.log(`[树节点转换] 共转换 ${paths.length} 个路径为 ${keys.length} 个节点key`);
console.log(`[树节点转换] 节点keys:`, keys);
return keys;
}
};
// 创建全局实例
const fieldsManager = new FieldsManager();
// 导出模块
window.FieldsModule = {
FieldsManager,
TreeUtils,
fieldsManager
};
})();