400 lines
13 KiB
JavaScript
400 lines
13 KiB
JavaScript
(function (root, factory) {
|
|
// 世系模块同时支持浏览器页面和 Node 单元测试。
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(root);
|
|
return;
|
|
}
|
|
|
|
root.LineagePages = factory(root);
|
|
if (root.document) {
|
|
root.document.addEventListener('DOMContentLoaded', function () {
|
|
root.LineagePages.init();
|
|
});
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
|
'use strict';
|
|
|
|
var documentRef = root.document;
|
|
var selectedPersonId = '';
|
|
|
|
function normalizeList(data) {
|
|
// 通用列表响应可能是数组,也可能包在 rows、records、list、data 中。
|
|
if (Array.isArray(data)) return data;
|
|
if (!data || typeof data !== 'object') return [];
|
|
if (Array.isArray(data.rows)) return data.rows;
|
|
if (Array.isArray(data.records)) return data.records;
|
|
if (Array.isArray(data.list)) return data.list;
|
|
if (Array.isArray(data.data)) return data.data;
|
|
return [];
|
|
}
|
|
|
|
function pick(item, keys, fallback) {
|
|
var source = item || {};
|
|
var index;
|
|
var value;
|
|
|
|
for (index = 0; index < keys.length; index += 1) {
|
|
value = source[keys[index]];
|
|
if (value !== undefined && value !== null && value !== '') return value;
|
|
}
|
|
|
|
return fallback || '';
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
// 世系人物信息进入 innerHTML 前统一转义,避免姓名和简介污染页面结构。
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function getApi() {
|
|
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
}
|
|
|
|
function query(selector, rootNode) {
|
|
return (rootNode || documentRef).querySelector(selector);
|
|
}
|
|
|
|
function queryAll(selector, rootNode) {
|
|
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
|
|
}
|
|
|
|
function showMessage(message) {
|
|
if (root.layui && root.layui.layer) {
|
|
root.layui.layer.msg(message);
|
|
return;
|
|
}
|
|
|
|
root.alert(message);
|
|
}
|
|
|
|
function getQueryParam(search, name) {
|
|
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
|
|
return params.get(name) || '';
|
|
}
|
|
|
|
function getCurrentGenealogyId() {
|
|
var search = root.location && root.location.search;
|
|
var page = query('[data-lineage-page], [data-family-home-page]');
|
|
|
|
return getQueryParam(search, 'id') ||
|
|
getQueryParam(search, 'genealogyId') ||
|
|
(page && page.getAttribute('data-genealogy-id')) ||
|
|
'';
|
|
}
|
|
|
|
function toNumber(value) {
|
|
var text = String(value === undefined || value === null ? '' : value).trim();
|
|
var number = Number(text);
|
|
|
|
return text && Number.isFinite(number) ? number : undefined;
|
|
}
|
|
|
|
function trimOrUndefined(value) {
|
|
var text = String(value === undefined || value === null ? '' : value).trim();
|
|
|
|
return text || undefined;
|
|
}
|
|
|
|
function formatSex(value) {
|
|
var text = String(value === undefined || value === null ? '' : value);
|
|
|
|
if (text === '0' || text === '男') return '男';
|
|
if (text === '1' || text === '女') return '女';
|
|
return '未知';
|
|
}
|
|
|
|
function getPersonId(item) {
|
|
return pick(item, ['personId', 'lineagePersonId', 'id'], '');
|
|
}
|
|
|
|
function buildLineagePersonBody(values) {
|
|
var source = values || {};
|
|
|
|
return {
|
|
appUserId: toNumber(source.appUserId),
|
|
personNo: trimOrUndefined(source.personNo),
|
|
personName: String(source.personName || '').trim(),
|
|
aliasName: trimOrUndefined(source.aliasName),
|
|
sex: String(source.sex || '').trim() || '0',
|
|
generationNo: toNumber(source.generationNo),
|
|
generationName: trimOrUndefined(source.generationName),
|
|
avatarOssId: toNumber(source.avatarOssId),
|
|
birthDate: trimOrUndefined(source.birthDate),
|
|
deathDate: trimOrUndefined(source.deathDate),
|
|
introduction: trimOrUndefined(source.introduction)
|
|
};
|
|
}
|
|
|
|
function buildPersonRow(item) {
|
|
var personId = getPersonId(item);
|
|
var name = pick(item, ['personName', 'name', 'realName'], '未命名成员');
|
|
var generationNo = pick(item, ['generationNo', 'generation', 'generationIndex'], '-');
|
|
var generationName = pick(item, ['generationName', 'generationText'], '未设置');
|
|
var sex = formatSex(pick(item, ['sex', 'gender'], ''));
|
|
var bound = pick(item, ['boundAccount', 'bindAccount', 'hasAccount', 'appUserId'], '') ? '已绑定账号' : '未绑定账号';
|
|
|
|
return '<button class="module-row lineage-row" type="button" data-lineage-person="' + escapeHtml(personId) + '">' +
|
|
'<div><h3>' + escapeHtml(name) + '</h3><p>第 ' + escapeHtml(generationNo) + ' 世 · ' + escapeHtml(generationName) + '字辈 · ' + escapeHtml(sex) + ' · ' + escapeHtml(bound) + '</p></div>' +
|
|
'<span class="pill">查看资料</span>' +
|
|
'</button>';
|
|
}
|
|
|
|
function getChildren(item) {
|
|
var source = item || {};
|
|
|
|
return normalizeList(source.children || source.childList || source.descendants);
|
|
}
|
|
|
|
function buildTreeNode(item) {
|
|
var personId = getPersonId(item);
|
|
var name = pick(item, ['personName', 'name', 'realName'], '未命名成员');
|
|
var generationNo = pick(item, ['generationNo', 'generation', 'generationIndex'], '-');
|
|
var children = getChildren(item);
|
|
var html = '<li><button type="button" class="lineage-node" data-lineage-person="' + escapeHtml(personId) + '">' +
|
|
'<strong>' + escapeHtml(name) + '</strong><span>第 ' + escapeHtml(generationNo) + ' 世</span></button>';
|
|
|
|
if (children.length) {
|
|
html += '<ul>' + children.map(buildTreeNode).join('') + '</ul>';
|
|
}
|
|
|
|
return html + '</li>';
|
|
}
|
|
|
|
function buildHomeSummary(detail, rootCount) {
|
|
// 家谱主页摘要优先展示后端家谱信息,世系根节点数量来自树接口。
|
|
var source = detail || {};
|
|
var genealogyNo = pick(source, ['genealogyNo', 'genealogyCode', 'code'], '未设置编号');
|
|
var hallName = pick(source, ['hallName', 'ancestralHall', 'tanghao'], '未设置');
|
|
var memberCount = pick(source, ['memberCount', 'personCount', 'members'], '0');
|
|
|
|
return genealogyNo + ' · 堂号 ' + hallName + ' · 共 ' + memberCount + ' 人 · ' + rootCount + ' 个世系根节点';
|
|
}
|
|
|
|
function renderTree(items) {
|
|
var container = query('[data-lineage-tree], [data-lineage-home-tree]');
|
|
var list = normalizeList(items);
|
|
|
|
if (!container) return;
|
|
if (!list.length) {
|
|
container.innerHTML = '<div class="api-empty">暂无世系树数据</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '<ul class="lineage-tree">' + list.map(buildTreeNode).join('') + '</ul>';
|
|
}
|
|
|
|
function renderPersons(items) {
|
|
var container = query('[data-lineage-list]');
|
|
var list = normalizeList(items);
|
|
|
|
if (!container) return;
|
|
if (!list.length) {
|
|
container.innerHTML = '<div class="api-empty">暂无世系人物</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = list.map(buildPersonRow).join('');
|
|
}
|
|
|
|
function renderHomeDetail(detail, rootCount) {
|
|
var title = query('[data-family-home-title]');
|
|
var summary = query('[data-family-home-summary]');
|
|
var source = detail || {};
|
|
|
|
if (title) title.textContent = pick(source, ['genealogyName', 'name'], title.textContent || '家谱主页');
|
|
if (summary) summary.textContent = buildHomeSummary(source, rootCount);
|
|
}
|
|
|
|
function renderDetail(item) {
|
|
var container = query('[data-lineage-detail]');
|
|
var source = item || {};
|
|
var name = pick(source, ['personName', 'name', 'realName'], '未命名成员');
|
|
var intro = pick(source, ['introduction', 'intro', 'remark'], '暂无简介');
|
|
|
|
if (!container) return;
|
|
container.innerHTML = '<div class="form-like">' +
|
|
'<p><span>姓名</span><b>' + escapeHtml(name) + '</b><em>' + escapeHtml(formatSex(pick(source, ['sex', 'gender'], ''))) + '</em></p>' +
|
|
'<p><span>字辈</span><b>' + escapeHtml(pick(source, ['generationName', 'generationText'], '未设置')) + '</b><em>第 ' + escapeHtml(pick(source, ['generationNo', 'generation'], '-')) + ' 世</em></p>' +
|
|
'<p><span>生卒</span><b>' + escapeHtml(pick(source, ['birthDate'], '未填写')) + '</b><em>' + escapeHtml(pick(source, ['deathDate'], '健在或未填')) + '</em></p>' +
|
|
'<p><span>简介</span><b>' + escapeHtml(intro) + '</b><em>世系资料</em></p>' +
|
|
'</div>';
|
|
}
|
|
|
|
function getFormValues(form) {
|
|
var values = {};
|
|
|
|
queryAll('[name]', form).forEach(function (field) {
|
|
values[field.name] = field.value;
|
|
});
|
|
|
|
return values;
|
|
}
|
|
|
|
async function loadLineage() {
|
|
var api = getApi();
|
|
var genealogyId = getCurrentGenealogyId();
|
|
var keyword = query('[data-lineage-keyword]');
|
|
var homeSummary = query('[data-family-home-summary]');
|
|
var treeData;
|
|
var treeList;
|
|
var queryData = {
|
|
pageNum: 1,
|
|
pageSize: 50
|
|
};
|
|
|
|
if (!api) return;
|
|
if (!genealogyId) {
|
|
renderTree([]);
|
|
renderPersons([]);
|
|
showMessage('请从具体家谱进入世系图');
|
|
return;
|
|
}
|
|
|
|
if (keyword && keyword.value.trim()) queryData.keyword = keyword.value.trim();
|
|
|
|
try {
|
|
treeData = await api.lineageTree(genealogyId);
|
|
treeList = normalizeList(treeData);
|
|
renderTree(treeData);
|
|
|
|
if (homeSummary && api.genealogyDetail) {
|
|
renderHomeDetail(await api.genealogyDetail(genealogyId), treeList.length);
|
|
}
|
|
|
|
if (query('[data-lineage-list]')) {
|
|
renderPersons(await api.lineagePersonsPage(genealogyId, queryData));
|
|
}
|
|
} catch (error) {
|
|
showMessage(error.message || '世系数据加载失败');
|
|
}
|
|
}
|
|
|
|
async function loadPersonDetail(personId) {
|
|
var api = getApi();
|
|
var genealogyId = getCurrentGenealogyId();
|
|
|
|
if (!api || !genealogyId || !personId) return;
|
|
|
|
selectedPersonId = personId;
|
|
queryAll('[data-lineage-person]').forEach(function (item) {
|
|
item.classList.toggle('is-selected', item.getAttribute('data-lineage-person') === String(personId));
|
|
});
|
|
|
|
try {
|
|
renderDetail(await api.lineagePersonDetail(genealogyId, personId));
|
|
} catch (error) {
|
|
showMessage(error.message || '人物详情加载失败');
|
|
}
|
|
}
|
|
|
|
async function submitPerson(form) {
|
|
var api = getApi();
|
|
var genealogyId = getCurrentGenealogyId();
|
|
var body = buildLineagePersonBody(getFormValues(form));
|
|
|
|
if (!api || !genealogyId) {
|
|
showMessage('请从具体家谱进入世系图');
|
|
return;
|
|
}
|
|
|
|
if (!body.personName) {
|
|
showMessage('请填写成员姓名');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api.createLineagePerson(genealogyId, body);
|
|
form.reset();
|
|
showMessage('世系人物已新增');
|
|
loadLineage();
|
|
} catch (error) {
|
|
showMessage(error.message || '新增世系人物失败');
|
|
}
|
|
}
|
|
|
|
async function createRelation(relationType) {
|
|
var api = getApi();
|
|
var genealogyId = getCurrentGenealogyId();
|
|
var relationNames = {
|
|
parents: '父母',
|
|
spouses: '配偶',
|
|
children: '子女',
|
|
siblings: '兄弟姐妹'
|
|
};
|
|
var personName;
|
|
|
|
if (!api || !genealogyId || !selectedPersonId) {
|
|
showMessage('请先在成员列表中选择一个成员');
|
|
return;
|
|
}
|
|
|
|
personName = root.prompt('请输入要添加的' + (relationNames[relationType] || '亲属') + '姓名', '');
|
|
if (!personName) return;
|
|
|
|
try {
|
|
await api.addLineageRelation(genealogyId, selectedPersonId, relationType, buildLineagePersonBody({
|
|
personName: personName
|
|
}));
|
|
showMessage('亲属关系已新增');
|
|
loadLineage();
|
|
} catch (error) {
|
|
showMessage(error.message || '新增亲属关系失败');
|
|
}
|
|
}
|
|
|
|
function bindActions() {
|
|
if (!documentRef) return;
|
|
|
|
documentRef.addEventListener('submit', function (event) {
|
|
var form = event.target.closest('[data-lineage-form]');
|
|
|
|
if (!form) return;
|
|
event.preventDefault();
|
|
submitPerson(form);
|
|
});
|
|
|
|
documentRef.addEventListener('click', function (event) {
|
|
var personButton = event.target.closest('[data-lineage-person]');
|
|
var searchButton = event.target.closest('[data-lineage-search]');
|
|
var relationButton = event.target.closest('[data-lineage-relation]');
|
|
|
|
if (personButton) {
|
|
event.preventDefault();
|
|
loadPersonDetail(personButton.getAttribute('data-lineage-person'));
|
|
}
|
|
|
|
if (searchButton) {
|
|
event.preventDefault();
|
|
loadLineage();
|
|
}
|
|
|
|
if (relationButton) {
|
|
event.preventDefault();
|
|
createRelation(relationButton.getAttribute('data-lineage-relation'));
|
|
}
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
if (!documentRef) return;
|
|
|
|
bindActions();
|
|
loadLineage();
|
|
}
|
|
|
|
return {
|
|
normalizeList: normalizeList,
|
|
buildLineagePersonBody: buildLineagePersonBody,
|
|
formatSex: formatSex,
|
|
buildPersonRow: buildPersonRow,
|
|
buildTreeNode: buildTreeNode,
|
|
buildHomeSummary: buildHomeSummary,
|
|
init: init
|
|
};
|
|
});
|