(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, '>') .replace(/"/g, '"') .replace(/'/g, '''); } 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 = '
人物响应缺少可安全使用的 personId 或 name,请联系后端补充 DTO。
'; return; } if (!people.length) { container.innerHTML = '
暂无世系人物
'; return; } container.innerHTML = people.map(function (person) { var selected = person.personId === selectedPersonId ? ' is-selected' : ''; return ''; }).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 '
  • ' + escapeHtml(person.name) + ' 已在上级关系中展示
  • '; } 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 += ''; if (children) branches += ''; return '
  • ' + branches + '
  • '; } 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 = '
    暂无世系树
    '; return; } container.innerHTML = ''; } function renderOptions(data) { var select = query('[data-lineage-person-options]'); var people = normalizeList(data).map(normalizeLineagePerson).filter(Boolean); if (!select) return; select.innerHTML = '' + people.map(function (person) { return ''; }).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 = '
    人物详情缺少可安全使用的 personId 或 name,请联系后端补充 DTO。
    '; 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 = '

    ' + escapeHtml(person.name) + '

    ' + escapeHtml(values.join(' · ') || '暂无更多资料') + '

    ' + '
    ' + '
    '; 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 = '
    请从具体家谱进入世系管理
    '; }); 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 = '
    当前账号没有世系人物管理权限
    '; }); 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 }; });