Files
jiapu/public/js/lineage-pages.js
T
2026-07-24 18:11:32 +08:00

698 lines
25 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 = '';
var editingPersonId = '';
var relationMode = '';
var peopleById = Object.create(null);
var currentPageNum = 1;
var currentPageSize = 20;
var currentPageTotal = null;
var managementAccess = true;
var writePending = false;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function isForbidden(error) {
return Number(error && (error.status || error.code)) === 403;
}
function redirectUnauthorized(api, error) {
if (!shouldRedirectToLogin(api, error)) return false;
if (api && api.clearToken) api.clearToken();
if (!root.location) return true;
if (typeof root.location.replace === 'function') {
root.location.replace('login.html');
return true;
}
root.location.href = 'login.html';
return true;
}
function query(selector, rootNode) {
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
}
function queryAll(selector, rootNode) {
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId(search) {
var page = query('[data-lineage-page]');
var source = search === undefined && root.location ? root.location.search : search;
var profileGenealogyId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
profileGenealogyId = root.ProfileUI.getGenealogyId();
if (profileGenealogyId) return profileGenealogyId;
}
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toIntegerOrUndefined(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
}
function getPersonId(item) {
var value = item && item.personId;
var text;
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
}
function buildLineagePersonBody(values) {
var source = values || {};
var body = { name: trimOrUndefined(source.name) };
var optionalFields = [
'personNo', 'aliasName', 'sex', 'generationName', 'birthDate', 'birthLunar', 'birthPlace',
'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography',
'remark', 'relationName'
];
var generation = trimOrUndefined(source.generation);
var sortOrder = trimOrUndefined(source.sortOrder);
optionalFields.forEach(function (field) {
var value = trimOrUndefined(source[field]);
if (value !== undefined) body[field] = value;
});
if (generation !== undefined) body.generation = Number(generation);
if (sortOrder !== undefined) body.sortOrder = Number(sortOrder);
return body;
}
function validateLineagePersonBody(body) {
if (!body || !body.name) return '请填写成员姓名';
if (body.generation !== undefined && (!Number.isInteger(body.generation) || !Number.isSafeInteger(body.generation))) {
return '世代序号必须是整数';
}
if (body.sortOrder !== undefined && (!Number.isInteger(body.sortOrder) || !Number.isSafeInteger(body.sortOrder))) {
return '排序值必须是整数';
}
return '';
}
function normalizeLineagePerson(item) {
var personId = getPersonId(item);
var name = trimOrUndefined(item && item.name);
var person;
if (!personId || !name) return null;
person = { personId: personId, name: name };
[
'genealogyName', 'genealogyNo', 'appUserNickName', 'personNo', 'aliasName', 'sex', 'generationName',
'fatherName', 'motherName', 'spouseNames', 'birthDate', 'birthLunar', 'birthPlace', 'deathDate',
'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography', 'status', 'remark',
'relationType', 'relationName'
].forEach(function (field) {
if (item && item[field] !== undefined && item[field] !== null) person[field] = String(item[field]);
});
['appUserId', 'genealogyId', 'fatherId', 'motherId', 'avatarOssId'].forEach(function (field) {
if (item && item[field] !== undefined && item[field] !== null) {
if (typeof item[field] === 'number' && !Number.isSafeInteger(item[field])) return;
person[field] = String(item[field]);
}
});
if (toIntegerOrUndefined(item && item.generation) !== undefined) person.generation = toIntegerOrUndefined(item.generation);
if (toIntegerOrUndefined(item && item.sortOrder) !== undefined) person.sortOrder = toIntegerOrUndefined(item.sortOrder);
return person;
}
function normalizeList(data) {
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.rows)) return data.rows;
return [];
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getPersonSummary(person) {
var values = [];
if (person.generation !== undefined) values.push('第 ' + person.generation + ' 世');
if (person.generationName) values.push('字辈:' + person.generationName);
if (person.aliasName) values.push('别名:' + person.aliasName);
if (person.relationName) values.push('关系称谓:' + person.relationName);
if (person.personStatus) values.push('人物状态:' + person.personStatus);
return values.join(' · ') || '未填写世代信息';
}
function renderLineageList(data) {
var container = query('[data-lineage-list]');
var normalized = normalizeList(data).map(normalizeLineagePerson);
var invalidCount = normalized.filter(function (item) { return !item; }).length;
var people = normalized.filter(Boolean);
peopleById = Object.create(null);
people.forEach(function (person) {
peopleById[person.personId] = person;
});
if (!container) return;
if (invalidCount) {
container.innerHTML = '<div class="api-empty">人物响应缺少可安全使用的 personId 或 name,请联系后端补充 DTO。</div>';
return;
}
if (!people.length) {
container.innerHTML = '<div class="api-empty">暂无世系人物</div>';
return;
}
container.innerHTML = people.map(function (person) {
var selected = person.personId === selectedPersonId ? ' is-selected' : '';
return '<button class="module-row lineage-row' + selected + '" type="button" data-lineage-person="' + escapeHtml(person.personId) + '">' +
'<div><h3>' + escapeHtml(person.name) + '</h3><p>' + escapeHtml(getPersonSummary(person)) + '</p></div>' +
'<span class="pill">查看</span></button>';
}).join('');
}
function renderTreeNode(item, ancestry) {
var person = normalizeLineagePerson(item);
var nextAncestry;
var spouses;
var children;
var branches = '';
if (!person) return '';
if (ancestry[person.personId]) {
return '<li><span class="api-empty">' + escapeHtml(person.name) + ' 已在上级关系中展示</span></li>';
}
nextAncestry = Object.assign({}, ancestry);
nextAncestry[person.personId] = true;
spouses = normalizeList(item && item.spouses).map(function (node) { return renderTreeNode(node, nextAncestry); }).filter(Boolean).join('');
children = normalizeList(item && item.children).map(function (node) { return renderTreeNode(node, nextAncestry); }).filter(Boolean).join('');
if (spouses) branches += '<ul><li><span>配偶</span><ul>' + spouses + '</ul></li></ul>';
if (children) branches += '<ul><li><span>子女</span><ul>' + children + '</ul></li></ul>';
return '<li><button class="lineage-node" type="button" data-lineage-person="' + escapeHtml(person.personId) + '">' +
'<strong>' + escapeHtml(person.name) + '</strong><span>' + escapeHtml(getPersonSummary(person)) + '</span></button>' + branches + '</li>';
}
function renderTree(data) {
var container = query('[data-lineage-tree]');
var nodes = normalizeList(data).map(function (item) { return renderTreeNode(item, {}); }).filter(Boolean);
if (!container) return;
if (!nodes.length) {
container.innerHTML = '<div class="api-empty">暂无世系树</div>';
return;
}
container.innerHTML = '<ul class="lineage-tree">' + nodes.join('') + '</ul>';
}
function renderOptions(data) {
var select = query('[data-lineage-person-options]');
var people = normalizeList(data).map(normalizeLineagePerson).filter(Boolean);
if (!select) return;
select.innerHTML = '<option value="">请选择当前成员</option>' + people.map(function (person) {
return '<option value="' + escapeHtml(person.personId) + '">' + escapeHtml(person.name + (person.generationName ? ' · ' + person.generationName : '')) + '</option>';
}).join('');
select.value = selectedPersonId;
}
function renderListSummary(data) {
var container = query('[data-lineage-list-summary]');
var count;
if (!container) return;
count = normalizeList(data).map(normalizeLineagePerson).filter(Boolean).length;
container.textContent = count ? '成员总览已返回 ' + count + ' 人;下方结果按页展示。' : '成员总览暂无可展示人物。';
}
function updatePaginationControls() {
var container = query('[data-lineage-pagination]');
var previous = query('[data-lineage-page-action="previous"]');
var next = query('[data-lineage-page-action="next"]');
var status = query('[data-lineage-page-status]');
var disabled = !managementAccess || writePending;
if (!container) return;
if (currentPageTotal === null) {
container.hidden = true;
return;
}
container.hidden = false;
if (previous) previous.disabled = disabled || currentPageNum <= 1;
if (next) next.disabled = disabled || currentPageNum * currentPageSize >= currentPageTotal;
if (status) status.textContent = '第 ' + currentPageNum + ' 页,共 ' + currentPageTotal + ' 人';
}
function renderPagination(data) {
var total = data && Number(data.total);
currentPageTotal = Number.isFinite(total) && total >= 0 ? total : null;
updatePaginationControls();
}
function renderDetail(data) {
var container = query('[data-lineage-detail]');
var person = normalizeLineagePerson(data);
var values;
if (!container) return;
if (!person) {
container.innerHTML = '<div class="api-empty">人物详情缺少可安全使用的 personId 或 name,请联系后端补充 DTO。</div>';
return;
}
selectedPersonId = person.personId;
peopleById[person.personId] = person;
values = [
person.sex ? '性别:' + person.sex : '',
person.generation !== undefined ? '世代:' + person.generation : '',
person.generationName ? '字辈:' + person.generationName : '',
person.personNo ? '人物编号:' + person.personNo : '',
person.birthDate ? '出生:' + person.birthDate : '',
person.deathDate ? '逝世:' + person.deathDate : '',
person.biography ? '简介:' + person.biography : ''
].filter(Boolean);
container.innerHTML = '<div class="module-row"><div><h3>' + escapeHtml(person.name) + '</h3><p>' + escapeHtml(values.join(' · ') || '暂无更多资料') + '</p></div>' +
'<div class="row-actions"><button class="pill" type="button" data-lineage-edit="' + escapeHtml(person.personId) + '">编辑</button>' +
'<button class="pill is-danger" type="button" data-lineage-disable="' + escapeHtml(person.personId) + '">停用</button></div></div>';
renderLineageList(Object.keys(peopleById).map(function (id) { return peopleById[id]; }));
if (query('[data-lineage-person-options]')) query('[data-lineage-person-options]').value = person.personId;
refreshControls();
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
function setFormStatus(message) {
var target = query('[data-lineage-form-status]');
if (target) target.textContent = message || '';
}
function relationLabel(mode) {
return {
parents: '父母',
spouses: '配偶',
children: '子女',
siblings: '兄弟姐妹'
}[mode] || '';
}
function syncGenealogyLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
return;
}
queryAll('[data-genealogy-context-link]').forEach(function (link) {
var href = link.getAttribute('href');
var base;
if (!href || href === '#') return;
base = href.split('?')[0];
link.href = base + '?genealogyId=' + encodeURIComponent(genealogyId);
});
}
function renderNoContext() {
['[data-lineage-tree]', '[data-lineage-list]', '[data-lineage-detail]'].forEach(function (selector) {
var container = query(selector);
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入世系管理</div>';
});
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
currentPageTotal = null;
updatePaginationControls();
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) {
renderNoContext();
showMessage('请从具体家谱进入世系管理');
}
return genealogyId;
}
function refreshControls() {
var disabled = !managementAccess || writePending;
queryAll('[data-lineage-relation], [data-lineage-search], [data-lineage-form] button, [data-lineage-person-options], [data-lineage-edit], [data-lineage-disable]').forEach(function (control) {
control.disabled = disabled;
});
updatePaginationControls();
}
function setLineageEnabled(enabled) {
managementAccess = Boolean(enabled);
refreshControls();
}
function setWritePending(pending) {
writePending = Boolean(pending);
refreshControls();
}
function renderForbiddenState() {
['[data-lineage-tree]', '[data-lineage-list]', '[data-lineage-detail]'].forEach(function (selector) {
var container = query(selector);
if (container) container.innerHTML = '<div class="api-empty">当前账号没有世系人物管理权限</div>';
});
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
currentPageTotal = null;
setLineageEnabled(false);
setFormStatus('当前账号没有世系人物管理权限');
}
function getCurrentKeyword() {
var input = query('[data-lineage-keyword]');
return input ? input.value : '';
}
async function loadLineage(keyword, pageNum) {
var api = getApi();
var genealogyId;
var results;
var searchKeyword = trimOrUndefined(keyword);
var requestedPage = Number(pageNum);
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
currentPageNum = Number.isInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
syncGenealogyLinks(genealogyId);
try {
results = await Promise.all([
api.lineageTree(genealogyId),
api.lineagePersons(genealogyId),
api.lineagePersonsPage(genealogyId, {
pageNum: currentPageNum,
pageSize: currentPageSize,
keyword: searchKeyword
}),
api.lineagePersonOptions(genealogyId)
]);
renderTree(results[0]);
renderListSummary(results[1]);
renderLineageList(results[2]);
renderPagination(results[2]);
renderOptions(results[3]);
setLineageEnabled(true);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
showMessage(error.message || '世系人物加载失败');
}
}
async function loadPersonDetail(personId) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !personId) return;
try {
renderDetail(await api.lineagePersonDetail(genealogyId, personId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
showMessage(error.message || '人物详情加载失败');
}
}
function setRelationMode(mode) {
var person = peopleById[selectedPersonId];
var submitButton = query('[data-lineage-form] button[type="submit"]');
if (!managementAccess || writePending) return;
if (!person) {
showMessage('请先从成员列表、世系树或下拉框选择当前成员');
return;
}
relationMode = mode;
editingPersonId = '';
if (submitButton) submitButton.textContent = '新增' + relationLabel(mode);
setFormStatus('当前将为“' + person.name + '”新增' + relationLabel(mode));
}
function fillForm(person) {
var form = query('[data-lineage-form]');
var values;
var submitButton;
if (!form || !person) return;
values = {
name: person.name,
sex: person.sex,
generation: person.generation,
generationName: person.generationName,
personNo: person.personNo,
aliasName: person.aliasName,
birthDate: person.birthDate ? person.birthDate.slice(0, 10) : '',
deathDate: person.deathDate ? person.deathDate.slice(0, 10) : '',
sortOrder: person.sortOrder,
relationName: person.relationName,
biography: person.biography
};
queryAll('[name]', form).forEach(function (field) {
field.value = values[field.name] === undefined || values[field.name] === null ? '' : values[field.name];
});
relationMode = '';
editingPersonId = person.personId;
submitButton = query('[data-lineage-form] button[type="submit"]');
if (submitButton) submitButton.textContent = '保存修改';
setFormStatus('正在编辑“' + person.name + '”');
}
function resetEditor() {
var submitButton = query('[data-lineage-form] button[type="submit"]');
relationMode = '';
editingPersonId = '';
if (submitButton) submitButton.textContent = '保存成员';
setFormStatus('');
}
async function submitPerson(form) {
var api = getApi();
var genealogyId;
var body = buildLineagePersonBody(getFormValues(form));
var validation = validateLineagePersonBody(body);
var result;
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
if (validation) {
setFormStatus(validation);
return;
}
setWritePending(true);
setFormStatus('正在保存成员...');
try {
if (editingPersonId) {
result = await api.updateLineagePerson(genealogyId, editingPersonId, body);
} else if (relationMode === 'parents') {
result = await api.createLineageParent(genealogyId, selectedPersonId, body);
} else if (relationMode === 'spouses') {
result = await api.createLineageSpouse(genealogyId, selectedPersonId, body);
} else if (relationMode === 'children') {
result = await api.createLineageChild(genealogyId, selectedPersonId, body);
} else if (relationMode === 'siblings') {
result = await api.createLineageSibling(genealogyId, selectedPersonId, body);
} else {
result = await api.createLineagePerson(genealogyId, body);
}
form.reset();
resetEditor();
setFormStatus('成员已保存');
await loadLineage(getCurrentKeyword(), currentPageNum);
if (getPersonId(result)) await loadPersonDetail(getPersonId(result));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
setFormStatus(error.message || '成员保存失败');
} finally {
setWritePending(false);
}
}
async function disablePerson(personId) {
var api = getApi();
var genealogyId;
var person = peopleById[personId];
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !personId) return;
if (root.confirm && !root.confirm('确认停用“' + (person ? person.name : '该成员') + '”吗?有正常子女时后端会拒绝停用。')) return;
setWritePending(true);
try {
await api.disableLineagePerson(genealogyId, personId);
selectedPersonId = '';
resetEditor();
await loadLineage(getCurrentKeyword(), currentPageNum);
showMessage('成员已停用');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
showMessage(error.message || '成员停用失败');
} finally {
setWritePending(false);
}
}
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('reset', function (event) {
if (event.target.closest('[data-lineage-form]')) resetEditor();
});
documentRef.addEventListener('change', function (event) {
var select = event.target.closest('[data-lineage-person-options]');
if (select && select.value) loadPersonDetail(select.value);
});
documentRef.addEventListener('click', function (event) {
var target = event.target;
var personButton = target.closest('[data-lineage-person]');
var relationButton = target.closest('[data-lineage-relation]');
var editButton = target.closest('[data-lineage-edit]');
var disableButton = target.closest('[data-lineage-disable]');
var pageButton = target.closest('[data-lineage-page-action]');
if (target.closest('[data-lineage-search]')) {
event.preventDefault();
loadLineage(getCurrentKeyword(), 1);
return;
}
if (pageButton) {
event.preventDefault();
if (!managementAccess || writePending) return;
if (pageButton.getAttribute('data-lineage-page-action') === 'previous' && currentPageNum > 1) {
loadLineage(getCurrentKeyword(), currentPageNum - 1);
}
if (pageButton.getAttribute('data-lineage-page-action') === 'next' && currentPageTotal !== null && currentPageNum * currentPageSize < currentPageTotal) {
loadLineage(getCurrentKeyword(), currentPageNum + 1);
}
return;
}
if (personButton) {
event.preventDefault();
loadPersonDetail(personButton.getAttribute('data-lineage-person'));
return;
}
if (relationButton) {
event.preventDefault();
setRelationMode(relationButton.getAttribute('data-lineage-relation'));
return;
}
if (editButton) {
event.preventDefault();
fillForm(peopleById[editButton.getAttribute('data-lineage-edit')]);
return;
}
if (disableButton) {
event.preventDefault();
disablePerson(disableButton.getAttribute('data-lineage-disable'));
}
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-lineage-page]')) loadLineage(getCurrentKeyword(), 1);
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getPersonId: getPersonId,
buildLineagePersonBody: buildLineagePersonBody,
validateLineagePersonBody: validateLineagePersonBody,
normalizeLineagePerson: normalizeLineagePerson,
normalizeList: normalizeList,
relationLabel: relationLabel,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});