455 lines
14 KiB
JavaScript
455 lines
14 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 - 子节点
|
||
* @returns {Object} 树节点
|
||
*/
|
||
buildTreeNode(table, children = []) {
|
||
return {
|
||
value: table,
|
||
label: this.getTableLabel(table),
|
||
children
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 构建易码通订单子节点
|
||
* @param {number} orderType - 订单类型
|
||
* @returns {Array} 子节点数组
|
||
*/
|
||
buildYmtOrderChildren(orderType) {
|
||
const orderFields = this.getTableFields('order');
|
||
const children = orderFields.map(f => ({ value: f.value, label: f.label }));
|
||
|
||
// 添加公共子表(这些表与订单类型无关,应始终展示)
|
||
children.push(this.buildTreeNode('merchant', this.getTableFields('merchant')));
|
||
children.push(this.buildTreeNode('activity', this.getTableFields('activity')));
|
||
|
||
// 为避免“有时显示有时不显示”的问题,这里不再按订单类型裁剪子表,
|
||
// 而是始终展示所有与订单相关的子表,由业务在导出层面控制是否使用。
|
||
children.push(this.buildTreeNode('order_digit', this.getTableFields('order_digit')));
|
||
children.push(this.buildTreeNode('order_voucher', this.getTableFields('order_voucher')));
|
||
children.push(this.buildTreeNode('order_cash', this.getTableFields('order_cash')));
|
||
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')));
|
||
|
||
return children;
|
||
}
|
||
|
||
/**
|
||
* 构建营销系统订单子节点
|
||
* @param {number} orderType - 订单类型
|
||
* @returns {Array} 子节点数组
|
||
*/
|
||
buildMarketingOrderChildren(orderType) {
|
||
const orderFields = this.getTableFields('order');
|
||
const children = orderFields.map(f => ({ value: 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: f.value, label: f.label })),
|
||
this.buildTreeNode('key_batch', [
|
||
...this.getTableFields('key_batch').map(f => ({ value: 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: f.value, label: f.label })),
|
||
this.buildTreeNode('voucher', [
|
||
...this.getTableFields('voucher').map(f => ({ value: 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];
|
||
}
|
||
};
|
||
|
||
return fields
|
||
.map(toPath)
|
||
.filter(path => Array.isArray(path) && path.length >= 2)
|
||
.filter(path => {
|
||
// 验证字段存在
|
||
const table = path[path.length - 2];
|
||
const field = path[path.length - 1];
|
||
return this.hasField(table, field);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 将路径数组格式转换为 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 - 路径数组
|
||
* @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) {
|
||
if (node.value === target) {
|
||
if (index === path.length - 1) {
|
||
return node.value;
|
||
} else if (node.children && node.children.length > 0) {
|
||
const found = TreeUtils.findNodeByPath(node.children, path, index + 1);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
|
||
/**
|
||
* 判断节点是否为叶子节点
|
||
* @param {Object} node - 节点对象
|
||
* @returns {boolean} 是否为叶子节点
|
||
*/
|
||
isLeafNode(node) {
|
||
return !node.children || node.children.length === 0;
|
||
},
|
||
|
||
/**
|
||
* 将选中的 keys 转换为路径数组
|
||
* @param {string[]} checkedKeys - 选中的节点 keys
|
||
* @param {Array} treeData - 树数据
|
||
* @returns {Array<string[]>} 路径数组(仅叶子节点)
|
||
*/
|
||
checkedKeysToLeafPaths(checkedKeys, treeData) {
|
||
return checkedKeys
|
||
.filter(key => {
|
||
const node = TreeUtils.findNode(treeData, key);
|
||
return node && TreeUtils.isLeafNode(node);
|
||
})
|
||
.map(key => {
|
||
const path = TreeUtils.findNodePath(treeData, key);
|
||
return path || [key];
|
||
})
|
||
.filter(path => path.length >= 2);
|
||
},
|
||
|
||
/**
|
||
* 将路径数组转换为节点 keys
|
||
* @param {Array<string[]>} paths - 路径数组
|
||
* @param {Array} treeData - 树数据
|
||
* @returns {string[]} 节点 keys
|
||
*/
|
||
pathsToNodeKeys(paths, treeData) {
|
||
return paths.map(path => {
|
||
if (Array.isArray(path)) {
|
||
const nodeValue = TreeUtils.findNodeByPath(treeData, path);
|
||
if (nodeValue) return nodeValue;
|
||
// 找不到时使用路径最后一部分
|
||
return path[path.length - 1];
|
||
} else if (typeof path === 'string') {
|
||
return path;
|
||
}
|
||
return null;
|
||
}).filter(Boolean);
|
||
}
|
||
};
|
||
|
||
// 创建全局实例
|
||
const fieldsManager = new FieldsManager();
|
||
|
||
// 导出模块
|
||
window.FieldsModule = {
|
||
FieldsManager,
|
||
TreeUtils,
|
||
fieldsManager
|
||
};
|
||
|
||
})();
|