561 lines
18 KiB
JavaScript
561 lines
18 KiB
JavaScript
/**
|
||
* 字段处理模块 - 字段路径转换、树结构构建
|
||
* @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
|
||
};
|
||
|
||
})();
|