|
@@ -233,7 +233,7 @@
|
|
|
Y_STEP: 160, // 行高 (代与代之间的距离)
|
|
Y_STEP: 160, // 行高 (代与代之间的距离)
|
|
|
NODE_WIDTH: 24, // 名字竖排的估计宽度
|
|
NODE_WIDTH: 24, // 名字竖排的估计宽度
|
|
|
NODE_HEIGHT: 80, // 名字竖排的估计高度
|
|
NODE_HEIGHT: 80, // 名字竖排的估计高度
|
|
|
- MARGIN_LEFT: 60, // 左侧世代标记留白
|
|
|
|
|
|
|
+ MARGIN_LEFT: 120, // 左侧世系世代标记留白
|
|
|
MARGIN_TOP: 20, // 顶部留白
|
|
MARGIN_TOP: 20, // 顶部留白
|
|
|
MARGIN_BOTTOM: 40, // 底部留白
|
|
MARGIN_BOTTOM: 40, // 底部留白
|
|
|
LINE_MID_OFFSET: 40 // 横线距离父节点底部的距离
|
|
LINE_MID_OFFSET: 40 // 横线距离父节点底部的距离
|
|
@@ -411,6 +411,76 @@
|
|
|
return match ? parseInt(match[0]) : null;
|
|
return match ? parseInt(match[0]) : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ===== 世系世代:中文数字转换 =====
|
|
|
|
|
+ const CN_DIGITS = ['零','一','二','三','四','五','六','七','八','九'];
|
|
|
|
|
+ const CN_UNITS = ['','十','百','千','万'];
|
|
|
|
|
+
|
|
|
|
|
+ function numToChinese(n) {
|
|
|
|
|
+ if (n <= 0) return '零';
|
|
|
|
|
+ if (n <= 10) return n === 10 ? '十' : CN_DIGITS[n];
|
|
|
|
|
+ if (n < 20) return '十' + (n % 10 === 0 ? '' : CN_DIGITS[n % 10]);
|
|
|
|
|
+ let result = '';
|
|
|
|
|
+ const str = String(n);
|
|
|
|
|
+ const len = str.length;
|
|
|
|
|
+ for (let i = 0; i < len; i++) {
|
|
|
|
|
+ const d = parseInt(str[i]);
|
|
|
|
|
+ const unitIdx = len - 1 - i;
|
|
|
|
|
+ if (d === 0) {
|
|
|
|
|
+ if (result && !result.endsWith('零') && i < len - 1) result += '零';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result += CN_DIGITS[d] + CN_UNITS[unitIdx];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result.replace(/零+$/, '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function chineseToNum(s) {
|
|
|
|
|
+ if (!s) return NaN;
|
|
|
|
|
+ s = s.trim();
|
|
|
|
|
+ const digitMap = {'零':0,'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9};
|
|
|
|
|
+ const unitMap = {'十':10,'百':100,'千':1000,'万':10000};
|
|
|
|
|
+ let result = 0, temp = 0;
|
|
|
|
|
+ for (let i = 0; i < s.length; i++) {
|
|
|
|
|
+ const ch = s[i];
|
|
|
|
|
+ if (digitMap[ch] !== undefined) {
|
|
|
|
|
+ temp = digitMap[ch];
|
|
|
|
|
+ } else if (unitMap[ch] !== undefined) {
|
|
|
|
|
+ if (temp === 0 && unitMap[ch] === 10) temp = 1;
|
|
|
|
|
+ result += temp * unitMap[ch];
|
|
|
|
|
+ temp = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ result += temp;
|
|
|
|
|
+ return result || NaN;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseLineageGenerations(str) {
|
|
|
|
|
+ if (!str) return [];
|
|
|
|
|
+ return str.split(';').filter(s => s.trim()).map(item => {
|
|
|
|
|
+ item = item.trim();
|
|
|
|
|
+ const m = item.match(/^(.+?)第(.+?)代$/);
|
|
|
|
|
+ if (m) {
|
|
|
|
|
+ return { place: m[1], num: chineseToNum(m[2]), raw: item };
|
|
|
|
|
+ }
|
|
|
|
|
+ return { place: '', num: NaN, raw: item };
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function formatLineageGeneration(place, num) {
|
|
|
|
|
+ if (isNaN(num) || num <= 0) return '';
|
|
|
|
|
+ return place + '第' + numToChinese(num) + '代';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function computeLineageForGen(refLineage, refGen, targetGen) {
|
|
|
|
|
+ if (!refLineage || refLineage.length === 0) return [];
|
|
|
|
|
+ const diff = targetGen - refGen;
|
|
|
|
|
+ return refLineage.map(lg => ({
|
|
|
|
|
+ place: lg.place,
|
|
|
|
|
+ num: lg.num + diff,
|
|
|
|
|
+ formatted: formatLineageGeneration(lg.place, lg.num + diff)
|
|
|
|
|
+ })).filter(lg => lg.num > 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function buildAndRenderPages(members, relations, selectedRootIds) {
|
|
function buildAndRenderPages(members, relations, selectedRootIds) {
|
|
|
// 1. 构建树结构
|
|
// 1. 构建树结构
|
|
|
const memberMap = {};
|
|
const memberMap = {};
|
|
@@ -458,6 +528,22 @@
|
|
|
// 按照世代对roots进行排序,优先显示最老的祖先
|
|
// 按照世代对roots进行排序,优先显示最老的祖先
|
|
|
roots.sort((a, b) => a.gen - b.gen);
|
|
roots.sort((a, b) => a.gen - b.gen);
|
|
|
|
|
|
|
|
|
|
+ // 查找世系世代参考点:找到任意一个有 name_word_generation 的人
|
|
|
|
|
+ let lineageRef = null;
|
|
|
|
|
+ function findLineageRef(node) {
|
|
|
|
|
+ if (lineageRef) return;
|
|
|
|
|
+ if (node.name_word_generation) {
|
|
|
|
|
+ const parsed = parseLineageGenerations(node.name_word_generation);
|
|
|
|
|
+ if (parsed.length > 0 && !isNaN(parsed[0].num)) {
|
|
|
|
|
+ lineageRef = { gen: node.gen, lineage: parsed };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!lineageRef && node.children) {
|
|
|
|
|
+ node.children.forEach(c => findLineageRef(c));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ roots.forEach(r => findLineageRef(r));
|
|
|
|
|
+
|
|
|
document.getElementById('loading').style.display = 'none';
|
|
document.getElementById('loading').style.display = 'none';
|
|
|
const container = document.getElementById('exportArea');
|
|
const container = document.getElementById('exportArea');
|
|
|
|
|
|
|
@@ -494,7 +580,8 @@
|
|
|
nodes: blockNodes,
|
|
nodes: blockNodes,
|
|
|
startGen: currentJob.startGen,
|
|
startGen: currentJob.startGen,
|
|
|
endGen: maxGenForThisPage,
|
|
endGen: maxGenForThisPage,
|
|
|
- leftTitle: currentJob.leftTitle
|
|
|
|
|
|
|
+ leftTitle: currentJob.leftTitle,
|
|
|
|
|
+ lineageRef: lineageRef
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -654,20 +741,29 @@
|
|
|
return html;
|
|
return html;
|
|
|
}).join('');
|
|
}).join('');
|
|
|
|
|
|
|
|
- // 4. 构建左侧世代标记
|
|
|
|
|
|
|
+ // 4. 构建左侧世系世代标记
|
|
|
let genLabels = '';
|
|
let genLabels = '';
|
|
|
|
|
+ const hasLineageRef = !!page.lineageRef;
|
|
|
|
|
+ const lineageColCount = hasLineageRef ? page.lineageRef.lineage.length : 0;
|
|
|
|
|
+
|
|
|
for(let i=0; i<=maxDepth; i++) {
|
|
for(let i=0; i<=maxDepth; i++) {
|
|
|
const actualGen = page.startGen + i;
|
|
const actualGen = page.startGen + i;
|
|
|
const y = i * CONFIG.Y_STEP + CONFIG.MARGIN_TOP + 15;
|
|
const y = i * CONFIG.Y_STEP + CONFIG.MARGIN_TOP + 15;
|
|
|
- const x = 30; // 左侧留白
|
|
|
|
|
|
|
|
|
|
- // 将代数竖排
|
|
|
|
|
- let genStr = actualGen + '世';
|
|
|
|
|
- // 支持如"30世"
|
|
|
|
|
- genLabels += `<text class="gen-text" x="${x}" y="${y}" text-anchor="middle">${actualGen}</text>`;
|
|
|
|
|
- genLabels += `<text class="gen-text" x="${x}" y="${y + 18}" text-anchor="middle">世</text>`;
|
|
|
|
|
|
|
+ if (hasLineageRef) {
|
|
|
|
|
+ const lineageItems = computeLineageForGen(page.lineageRef.lineage, page.lineageRef.gen, actualGen);
|
|
|
|
|
+ lineageItems.forEach((item, colIdx) => {
|
|
|
|
|
+ const x = 20 + colIdx * 22;
|
|
|
|
|
+ const chars = Array.from(item.formatted);
|
|
|
|
|
+ chars.forEach((ch, ci) => {
|
|
|
|
|
+ genLabels += `<text class="gen-text" x="${x}" y="${y + ci * 15}" text-anchor="middle" style="font-size:13px;">${ch}</text>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ genLabels += `<text class="gen-text" x="30" y="${y}" text-anchor="middle">${actualGen}</text>`;
|
|
|
|
|
+ genLabels += `<text class="gen-text" x="30" y="${y + 18}" text-anchor="middle">世</text>`;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 画一条灰色的分隔虚线(非必须,但好看)
|
|
|
|
|
genLabels += `<line x1="10" y1="${y-20}" x2="${svgWidth}" y2="${y-20}" stroke="#eee" stroke-dasharray="4" />`;
|
|
genLabels += `<line x1="10" y1="${y-20}" x2="${svgWidth}" y2="${y-20}" stroke="#eee" stroke-dasharray="4" />`;
|
|
|
}
|
|
}
|
|
|
|
|
|