1054 lines
40 KiB
JavaScript
1054 lines
40 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';
|
|
|
|
function confirmAction(message) {
|
|
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
|
|
return root.ProfileUI.confirmAction(message);
|
|
}
|
|
return Promise.resolve(!root['confirm'] || root['confirm'](message));
|
|
}
|
|
|
|
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
|
|
|
|
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 = false;
|
|
var writePending = false;
|
|
var currentUserId = '';
|
|
|
|
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 normalizeId(value) {
|
|
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 getPersonId(item) {
|
|
return normalizeId(item && item.personId);
|
|
}
|
|
|
|
function formatLineageDate(value) {
|
|
var text = trimOrUndefined(value);
|
|
|
|
if (!text) return undefined;
|
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(text)) return text.replace('T', ' ') + ':00';
|
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(text)) return text.replace('T', ' ');
|
|
return text;
|
|
}
|
|
|
|
function buildLineagePersonBody(values) {
|
|
var source = values || {};
|
|
var bindingMode = trimOrUndefined(source.bindingMode);
|
|
var body = {
|
|
bindingMode: bindingMode,
|
|
name: trimOrUndefined(source.name)
|
|
};
|
|
var optionalFields = [
|
|
'personNo', 'aliasName', 'sex', 'generationName', 'birthLunar', 'birthPlace',
|
|
'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography',
|
|
'remark', 'relationName'
|
|
];
|
|
var idFields = ['avatarOssId', 'fatherId', 'motherId'];
|
|
var generation = trimOrUndefined(source.generation);
|
|
var sortOrder = trimOrUndefined(source.sortOrder);
|
|
var birthDate = formatLineageDate(source.birthDate);
|
|
var deathDate = formatLineageDate(source.deathDate);
|
|
|
|
optionalFields.forEach(function (field) {
|
|
var value = trimOrUndefined(source[field]);
|
|
|
|
if (value !== undefined) body[field] = value;
|
|
});
|
|
idFields.forEach(function (field) {
|
|
var value = normalizeId(source[field]);
|
|
|
|
if (value) body[field] = value;
|
|
});
|
|
if (bindingMode === 'SPECIFIED') {
|
|
body.appUserId = normalizeId(source.appUserId) || trimOrUndefined(source.appUserId);
|
|
}
|
|
if (birthDate !== undefined) body.birthDate = birthDate;
|
|
if (deathDate !== undefined) body.deathDate = deathDate;
|
|
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.bindingMode) return '请选择账号绑定方式';
|
|
if (['NONE', 'SELF', 'SPECIFIED'].indexOf(body.bindingMode) === -1) return '账号绑定方式无效';
|
|
if (body.bindingMode === 'SPECIFIED' && !normalizeId(body.appUserId)) return '指定账号绑定缺少用户选项';
|
|
if (body.bindingMode !== 'SPECIFIED' && body.appUserId !== undefined) return '当前绑定方式不能提交指定用户';
|
|
if (body.sex !== undefined && ['0', '1', '2'].indexOf(body.sex) === -1) return '性别选项无效';
|
|
if (body.personStatus !== undefined && ['0', '1', '2'].indexOf(body.personStatus) === -1) return '人物状态选项无效';
|
|
if (body.birthLunar !== undefined && ['0', '1'].indexOf(body.birthLunar) === -1) return '出生历法选项无效';
|
|
if (body.deathLunar !== undefined && ['0', '1'].indexOf(body.deathLunar) === -1) return '逝世历法选项无效';
|
|
if (['avatarOssId', 'fatherId', 'motherId'].some(function (field) {
|
|
return body[field] !== undefined && !normalizeId(body[field]);
|
|
})) 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;
|
|
var invalidId = false;
|
|
|
|
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'].forEach(function (field) {
|
|
if (item && item[field] !== undefined && item[field] !== null) {
|
|
var value = normalizeId(item[field]);
|
|
|
|
if (!value) {
|
|
invalidId = true;
|
|
return;
|
|
}
|
|
person[field] = value;
|
|
}
|
|
});
|
|
if (invalidId) return null;
|
|
if (item && item.avatarFile !== undefined && item.avatarFile !== null) {
|
|
person.avatarFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item.avatarFile);
|
|
if (!person.avatarFile) return null;
|
|
}
|
|
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 normalizeLineageAccess(genealogy) {
|
|
return {
|
|
canEditContent: Boolean(genealogy && genealogy.canEditContent === true)
|
|
};
|
|
}
|
|
|
|
function normalizeGenerationOption(item) {
|
|
var generation = toIntegerOrUndefined(item && item.generationNo);
|
|
var generationName = trimOrUndefined(item && item.generationText);
|
|
|
|
if (!generation || !generationName) return null;
|
|
return {
|
|
generation: generation,
|
|
generationName: generationName
|
|
};
|
|
}
|
|
|
|
function normalizeGenealogyMemberOption(item) {
|
|
var memberId = normalizeId(item && item.memberId);
|
|
var genealogyId = normalizeId(item && item.genealogyId);
|
|
var appUserId = normalizeId(item && item.appUserId);
|
|
var status = trimOrUndefined(item && item.status);
|
|
|
|
if (!memberId || !genealogyId || !appUserId || status !== '0') return null;
|
|
return {
|
|
memberId: memberId,
|
|
genealogyId: genealogyId,
|
|
appUserId: appUserId,
|
|
appUserNickName: String(item.appUserNickName === undefined || item.appUserNickName === null ? '' : item.appUserNickName),
|
|
memberName: String(item.memberName === undefined || item.memberName === null ? '' : item.memberName),
|
|
roleType: String(item.roleType === undefined || item.roleType === null ? '' : item.roleType),
|
|
status: status
|
|
};
|
|
}
|
|
|
|
function normalizeGenealogyMemberOptions(data) {
|
|
if (!Array.isArray(data)) return [];
|
|
return data.map(normalizeGenealogyMemberOption).filter(Boolean);
|
|
}
|
|
|
|
function normalizeLineageBinding(person, userId) {
|
|
var appUserId = normalizeId(person && person.appUserId);
|
|
var normalizedUserId = normalizeId(userId);
|
|
|
|
if (!appUserId) return { bindingMode: 'NONE', appUserId: '' };
|
|
if (normalizedUserId && appUserId === normalizedUserId) return { bindingMode: 'SELF', appUserId: '' };
|
|
return { bindingMode: 'SPECIFIED', appUserId: appUserId };
|
|
}
|
|
|
|
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, '"')
|
|
.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) {
|
|
setLineageState('[data-lineage-list]', 'error', '人物响应缺少可安全使用的 personId 或 name,请联系后端补充 DTO。');
|
|
return;
|
|
}
|
|
if (!people.length) {
|
|
setLineageState('[data-lineage-list]', 'empty', '暂无世系人物');
|
|
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) + '">' +
|
|
(MediaDisplay ? MediaDisplay.renderAvatar(person.avatarFile, { name: person.name, className: 'lineage-avatar-media' }) : '') +
|
|
'<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) + '">' +
|
|
(MediaDisplay ? MediaDisplay.renderAvatar(person.avatarFile, { name: person.name, className: 'lineage-node-avatar' }) : '') +
|
|
'<strong>' + escapeHtml(person.name) + '</strong><span>' + escapeHtml(getPersonSummary(person)) + '</span></button>' + branches + '</li>';
|
|
}
|
|
|
|
function renderLineageTreeHtml(data) {
|
|
var nodes = normalizeList(data).map(function (item) { return renderTreeNode(item, {}); }).filter(Boolean);
|
|
|
|
if (!nodes.length) return '';
|
|
return '<ul class="lineage-tree">' + nodes.join('') + '</ul>';
|
|
}
|
|
|
|
function renderTree(data) {
|
|
var container = query('[data-lineage-tree]');
|
|
var html = renderLineageTreeHtml(data);
|
|
|
|
if (!container) return;
|
|
if (!html) {
|
|
setLineageState('[data-lineage-tree]', 'empty', '暂无世系树');
|
|
return;
|
|
}
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderOptions(data) {
|
|
var select = query('[data-lineage-person-options]');
|
|
var people = normalizeList(data).map(normalizeLineagePerson).filter(Boolean);
|
|
var options = people.map(function (person) {
|
|
return '<option value="' + escapeHtml(person.personId) + '">' + escapeHtml(person.name + (person.generationName ? ' · ' + person.generationName : '')) + '</option>';
|
|
}).join('');
|
|
|
|
if (!select) return;
|
|
select.innerHTML = '<option value="">请选择当前成员</option>' + options;
|
|
select.value = selectedPersonId;
|
|
queryAll('[data-lineage-parent-option]').forEach(function (parentSelect) {
|
|
var emptyLabel = parentSelect.getAttribute('data-empty-label') || '请选择人物';
|
|
|
|
parentSelect.innerHTML = '<option value="">' + escapeHtml(emptyLabel) + '</option>' + options;
|
|
});
|
|
}
|
|
|
|
function renderGenerationOptions(data) {
|
|
var select = query('[data-lineage-generation]');
|
|
var filter = query('[data-lineage-generation-filter]');
|
|
var selectedGeneration = select ? select.value : '';
|
|
var selectedFilter = filter ? filter.value : '';
|
|
var options = normalizeList(data).map(normalizeGenerationOption).filter(Boolean);
|
|
var optionHtml = options.map(function (option) {
|
|
return '<option value="' + escapeHtml(option.generation) + '" data-generation-name="' + escapeHtml(option.generationName) + '">' +
|
|
escapeHtml('第 ' + option.generation + ' 世 · ' + option.generationName) + '</option>';
|
|
}).join('');
|
|
|
|
if (select) {
|
|
select.innerHTML = '<option value="">请选择世代</option>' + optionHtml;
|
|
select.value = selectedGeneration;
|
|
}
|
|
if (filter) {
|
|
filter.innerHTML = '<option value="">全部世代</option>' + optionHtml;
|
|
filter.value = selectedFilter;
|
|
}
|
|
}
|
|
|
|
function updateBindingFields() {
|
|
var bindingMode = query('[name="bindingMode"]');
|
|
var memberSelect = query('[data-lineage-member-options]');
|
|
var specifiedMember = query('[data-lineage-specified-member]');
|
|
var specified = Boolean(bindingMode && bindingMode.value === 'SPECIFIED');
|
|
|
|
if (!memberSelect) return;
|
|
if (specifiedMember) specifiedMember.hidden = !specified;
|
|
memberSelect.hidden = !specified;
|
|
memberSelect.required = specified;
|
|
if (!specified) memberSelect.value = '';
|
|
}
|
|
|
|
function renderGenealogyMemberOptions(data) {
|
|
var select = query('[data-lineage-member-options]');
|
|
var selectedValue = select ? select.value : '';
|
|
var options = normalizeGenealogyMemberOptions(data).filter(function (option) {
|
|
return option.appUserId !== currentUserId;
|
|
});
|
|
|
|
if (!select) return options;
|
|
select.innerHTML = '<option value="">请选择已绑定账号的家谱成员</option>' + options.map(function (option) {
|
|
var label = option.memberName || option.appUserNickName || '家谱成员';
|
|
|
|
return '<option value="' + escapeHtml(option.appUserId) + '">' + escapeHtml(label) + '</option>';
|
|
}).join('');
|
|
select.value = selectedValue;
|
|
updateBindingFields();
|
|
return options;
|
|
}
|
|
|
|
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 = 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 canUseLineagePagination(pending) {
|
|
return !Boolean(pending);
|
|
}
|
|
|
|
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;
|
|
var actions = '';
|
|
|
|
if (!container) return;
|
|
if (!person) {
|
|
setLineageState('[data-lineage-detail]', 'error', '人物详情缺少可安全使用的 personId 或 name,请联系后端补充 DTO。');
|
|
return;
|
|
}
|
|
selectedPersonId = person.personId;
|
|
peopleById[person.personId] = person;
|
|
values = [
|
|
person.sex !== undefined ? '性别:' + ({ '0': '男', '1': '女', '2': '未知' }[person.sex] || person.sex) : '',
|
|
person.generation !== undefined ? '世代:' + person.generation : '',
|
|
person.generationName ? '字辈:' + person.generationName : '',
|
|
person.personNo ? '人物编号:' + person.personNo : '',
|
|
person.aliasName ? '别名:' + person.aliasName : '',
|
|
person.fatherName ? '父亲:' + person.fatherName : '',
|
|
person.motherName ? '母亲:' + person.motherName : '',
|
|
person.spouseNames ? '配偶:' + person.spouseNames : '',
|
|
person.appUserNickName ? '绑定账号:' + person.appUserNickName : '',
|
|
person.birthDate ? '出生:' + person.birthDate : '',
|
|
person.birthPlace ? '出生地:' + person.birthPlace : '',
|
|
person.deathDate ? '逝世:' + person.deathDate : '',
|
|
person.deathPlace ? '逝世地:' + person.deathPlace : '',
|
|
person.burialPlace ? '安葬地:' + person.burialPlace : '',
|
|
person.personStatus !== undefined ? '人物状态:' + ({ '0': '健在', '1': '已故', '2': '未知' }[person.personStatus] || person.personStatus) : '',
|
|
person.biography ? '简介:' + person.biography : '',
|
|
person.remark ? '备注:' + person.remark : ''
|
|
].filter(Boolean);
|
|
if (managementAccess) {
|
|
actions = '<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>';
|
|
}
|
|
container.innerHTML = '<div class="module-row"><div><h3>' + escapeHtml(person.name) + '</h3><p>' + escapeHtml(values.join(' · ') || '暂无更多资料') + '</p></div>' +
|
|
actions + '</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 setLineageState(selector, type, message) {
|
|
var target = query(selector);
|
|
|
|
if (!target) return;
|
|
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
|
root.ProfileUI.setApiState(target, type, message);
|
|
return;
|
|
}
|
|
target.innerHTML = '<div class="api-state api-state--' + escapeHtml(type) + '" data-api-state="' + escapeHtml(type) + '">' +
|
|
escapeHtml(message) + '</div>';
|
|
}
|
|
|
|
function setFormStatus(message, type) {
|
|
var target = query('[data-lineage-form-status]');
|
|
|
|
if (!target) return;
|
|
if (!message) {
|
|
target.innerHTML = '';
|
|
return;
|
|
}
|
|
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
|
root.ProfileUI.setApiState(target, type, message);
|
|
return;
|
|
}
|
|
target.textContent = message;
|
|
}
|
|
|
|
function setActionStatus(message, type) {
|
|
var target = query('[data-lineage-action-status]');
|
|
|
|
if (!target) return;
|
|
if (!message) {
|
|
target.innerHTML = '';
|
|
return;
|
|
}
|
|
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
|
root.ProfileUI.setApiState(target, type, message);
|
|
return;
|
|
}
|
|
target.textContent = message;
|
|
}
|
|
|
|
function setLineageSubmitPending(pending) {
|
|
queryAll('[data-lineage-submit]').forEach(function (button) {
|
|
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
|
button.textContent = pending ? '正在保存…' : button.dataset.defaultLabel;
|
|
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
|
});
|
|
}
|
|
|
|
function setLineageSubmitLabel(label) {
|
|
queryAll('[data-lineage-submit]').forEach(function (button) {
|
|
button.dataset.defaultLabel = label;
|
|
if (!writePending) button.textContent = label;
|
|
});
|
|
}
|
|
|
|
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) {
|
|
setLineageState(selector, 'error', '请从具体家谱进入世系管理');
|
|
});
|
|
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
|
|
currentPageTotal = null;
|
|
setLineageEnabled(false);
|
|
setActionStatus('请从具体家谱进入世系管理。', 'error');
|
|
updatePaginationControls();
|
|
}
|
|
|
|
function requireGenealogyId() {
|
|
var genealogyId = getCurrentGenealogyId();
|
|
|
|
if (!genealogyId) {
|
|
renderNoContext();
|
|
showMessage('请从具体家谱进入世系管理');
|
|
}
|
|
return genealogyId;
|
|
}
|
|
|
|
function refreshControls() {
|
|
var writeDisabled = !managementAccess || writePending;
|
|
|
|
queryAll('[data-lineage-management]').forEach(function (element) {
|
|
element.hidden = !managementAccess;
|
|
});
|
|
queryAll('[data-lineage-relation], [data-lineage-form] button, [data-lineage-form] input, [data-lineage-form] select, [data-lineage-form] textarea, [data-lineage-edit], [data-lineage-disable]').forEach(function (control) {
|
|
control.disabled = writeDisabled;
|
|
});
|
|
queryAll('[data-lineage-relation]').forEach(function (control) {
|
|
control.disabled = writeDisabled || !selectedPersonId;
|
|
});
|
|
queryAll('[data-lineage-search], [data-lineage-person-options]').forEach(function (control) {
|
|
control.disabled = writePending;
|
|
});
|
|
updatePaginationControls();
|
|
}
|
|
|
|
function setLineageEnabled(enabled) {
|
|
managementAccess = Boolean(enabled);
|
|
refreshControls();
|
|
}
|
|
|
|
function setWritePending(pending) {
|
|
writePending = Boolean(pending);
|
|
refreshControls();
|
|
setLineageSubmitPending(writePending);
|
|
}
|
|
|
|
function renderForbiddenState() {
|
|
['[data-lineage-tree]', '[data-lineage-list]', '[data-lineage-detail]'].forEach(function (selector) {
|
|
setLineageState(selector, 'forbidden', '当前账号没有世系人物管理权限');
|
|
});
|
|
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
|
|
currentPageTotal = null;
|
|
setLineageEnabled(false);
|
|
setFormStatus('当前账号没有世系人物管理权限', 'forbidden');
|
|
setActionStatus('当前账号没有世系人物管理权限', 'forbidden');
|
|
}
|
|
|
|
function getCurrentKeyword() {
|
|
var input = query('[data-lineage-keyword]');
|
|
|
|
return input ? input.value : '';
|
|
}
|
|
|
|
function buildLineagePageQuery(values, pageNum, pageSize) {
|
|
var source = values || {};
|
|
var queryValues = {
|
|
pageNum: Number(pageNum),
|
|
pageSize: Number(pageSize)
|
|
};
|
|
var keyword = trimOrUndefined(source.keyword);
|
|
var generation = toIntegerOrUndefined(source.generation);
|
|
var personStatus = trimOrUndefined(source.personStatus);
|
|
|
|
if (keyword !== undefined) queryValues.keyword = keyword;
|
|
if (generation !== undefined && generation > 0) queryValues.generation = generation;
|
|
if (['0', '1', '2'].indexOf(personStatus) !== -1) queryValues.personStatus = personStatus;
|
|
return queryValues;
|
|
}
|
|
|
|
function getCurrentFilters(keyword) {
|
|
var generation = query('[data-lineage-generation-filter]');
|
|
var status = query('[data-lineage-status-filter]');
|
|
|
|
return {
|
|
keyword: keyword,
|
|
generation: generation ? generation.value : '',
|
|
personStatus: status ? status.value : ''
|
|
};
|
|
}
|
|
|
|
async function loadLineage(keyword, pageNum) {
|
|
var api = getApi();
|
|
var genealogyId;
|
|
var results;
|
|
var access;
|
|
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);
|
|
setLineageState('[data-lineage-tree]', 'loading', '正在加载世系树…');
|
|
setLineageState('[data-lineage-list]', 'loading', '正在加载世系人物…');
|
|
setActionStatus('');
|
|
try {
|
|
results = await Promise.all([
|
|
api.genealogyDetail(genealogyId),
|
|
api.currentProfile(),
|
|
api.lineageTree(genealogyId),
|
|
api.lineagePersons(genealogyId),
|
|
api.lineagePersonsPage(genealogyId, buildLineagePageQuery(
|
|
getCurrentFilters(searchKeyword),
|
|
currentPageNum,
|
|
currentPageSize
|
|
)),
|
|
api.lineagePersonOptions(genealogyId),
|
|
api.generationPoems(genealogyId)
|
|
]);
|
|
access = normalizeLineageAccess(results[0]);
|
|
currentUserId = normalizeId(results[1] && results[1].userId);
|
|
setLineageEnabled(access.canEditContent);
|
|
renderTree(results[2]);
|
|
renderListSummary(results[3]);
|
|
renderLineageList(results[4]);
|
|
renderPagination(results[4]);
|
|
renderOptions(results[5]);
|
|
renderGenerationOptions(results[6]);
|
|
if (access.canEditContent) {
|
|
renderGenealogyMemberOptions(await api.genealogyMemberOptions(genealogyId));
|
|
}
|
|
} catch (error) {
|
|
if (redirectUnauthorized(api, error)) return;
|
|
if (isForbidden(error)) {
|
|
renderForbiddenState();
|
|
return;
|
|
}
|
|
['[data-lineage-tree]', '[data-lineage-list]'].forEach(function (selector) {
|
|
setLineageState(selector, 'error', '世系人物加载失败,请稍后重试');
|
|
});
|
|
showMessage(error.message || '世系人物加载失败');
|
|
}
|
|
}
|
|
|
|
async function loadPersonDetail(personId) {
|
|
var api = getApi();
|
|
var genealogyId;
|
|
|
|
if (redirectUnauthorized(api)) return;
|
|
genealogyId = requireGenealogyId();
|
|
if (!genealogyId || !personId) return;
|
|
setLineageState('[data-lineage-detail]', 'loading', '正在加载成员详情…');
|
|
try {
|
|
renderDetail(await api.lineagePersonDetail(genealogyId, personId));
|
|
} catch (error) {
|
|
if (redirectUnauthorized(api, error)) return;
|
|
if (isForbidden(error)) {
|
|
renderForbiddenState();
|
|
return;
|
|
}
|
|
setLineageState('[data-lineage-detail]', 'error', '人物详情加载失败,请稍后重试。');
|
|
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 = '';
|
|
setRelationFieldVisibility(mode === 'spouses');
|
|
if (submitButton) setLineageSubmitLabel('新增' + relationLabel(mode));
|
|
setFormStatus('当前将为“' + person.name + '”新增' + relationLabel(mode));
|
|
}
|
|
|
|
function toLocalDateTime(value) {
|
|
var text = trimOrUndefined(value);
|
|
|
|
return text ? text.slice(0, 16).replace(' ', 'T') : '';
|
|
}
|
|
|
|
function setRelationFieldVisibility(visible) {
|
|
var field = query('[data-lineage-spouse-field]');
|
|
|
|
if (field) field.hidden = !visible;
|
|
}
|
|
|
|
function fillForm(person) {
|
|
var form = query('[data-lineage-form]');
|
|
var values;
|
|
var submitButton;
|
|
var binding;
|
|
|
|
if (!form || !person) return;
|
|
binding = normalizeLineageBinding(person, currentUserId);
|
|
values = {
|
|
bindingMode: binding.bindingMode,
|
|
appUserId: binding.appUserId,
|
|
avatarOssId: person.avatarOssId,
|
|
name: person.name,
|
|
sex: person.sex,
|
|
generation: person.generation,
|
|
generationName: person.generationName,
|
|
fatherId: person.fatherId,
|
|
motherId: person.motherId,
|
|
personNo: person.personNo,
|
|
aliasName: person.aliasName,
|
|
birthDate: toLocalDateTime(person.birthDate),
|
|
birthLunar: person.birthLunar,
|
|
birthPlace: person.birthPlace,
|
|
deathDate: toLocalDateTime(person.deathDate),
|
|
deathLunar: person.deathLunar,
|
|
deathPlace: person.deathPlace,
|
|
burialPlace: person.burialPlace,
|
|
personStatus: person.personStatus,
|
|
sortOrder: person.sortOrder,
|
|
relationName: person.relationName,
|
|
biography: person.biography,
|
|
remark: person.remark
|
|
};
|
|
queryAll('[name]', form).forEach(function (field) {
|
|
field.value = values[field.name] === undefined || values[field.name] === null ? '' : values[field.name];
|
|
});
|
|
updateBindingFields();
|
|
relationMode = '';
|
|
editingPersonId = person.personId;
|
|
setRelationFieldVisibility(false);
|
|
submitButton = query('[data-lineage-form] button[type="submit"]');
|
|
if (submitButton) setLineageSubmitLabel('保存修改');
|
|
setFormStatus('正在编辑“' + person.name + '”');
|
|
}
|
|
|
|
function resetEditor() {
|
|
var submitButton = query('[data-lineage-form] button[type="submit"]');
|
|
|
|
relationMode = '';
|
|
editingPersonId = '';
|
|
setRelationFieldVisibility(false);
|
|
if (submitButton) setLineageSubmitLabel('保存成员');
|
|
setFormStatus('');
|
|
if (root.setTimeout) root.setTimeout(updateBindingFields, 0);
|
|
}
|
|
|
|
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, 'error');
|
|
return;
|
|
}
|
|
setWritePending(true);
|
|
setFormStatus('正在保存成员…', 'loading');
|
|
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 || '成员保存失败', 'error');
|
|
} 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 (!await confirmAction('确认停用“' + (person ? person.name : '该成员') + '”吗?有正常子女时后端会拒绝停用。')) return;
|
|
setWritePending(true);
|
|
setActionStatus('正在停用成员…', 'loading');
|
|
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;
|
|
}
|
|
setActionStatus(error.message || '成员停用失败', 'error');
|
|
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]');
|
|
var generationSelect = event.target.closest('[data-lineage-generation]');
|
|
var bindingSelect = event.target.closest('[name="bindingMode"]');
|
|
var selectedOption;
|
|
var generationName;
|
|
var appUserId;
|
|
|
|
if (select && select.value) loadPersonDetail(select.value);
|
|
if (generationSelect) {
|
|
selectedOption = generationSelect.options[generationSelect.selectedIndex];
|
|
generationName = query('[name="generationName"]', generationSelect.form);
|
|
if (generationName) generationName.value = selectedOption ? selectedOption.getAttribute('data-generation-name') || '' : '';
|
|
}
|
|
if (bindingSelect) {
|
|
appUserId = query('[name="appUserId"]', bindingSelect.form);
|
|
if (bindingSelect.value !== 'SPECIFIED' && appUserId) appUserId.value = '';
|
|
updateBindingFields();
|
|
}
|
|
});
|
|
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 (!canUseLineagePagination(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,
|
|
normalizeLineageAccess: normalizeLineageAccess,
|
|
canUseLineagePagination: canUseLineagePagination,
|
|
normalizeGenerationOption: normalizeGenerationOption,
|
|
normalizeGenealogyMemberOption: normalizeGenealogyMemberOption,
|
|
normalizeGenealogyMemberOptions: normalizeGenealogyMemberOptions,
|
|
normalizeLineageBinding: normalizeLineageBinding,
|
|
buildLineagePageQuery: buildLineagePageQuery,
|
|
normalizeList: normalizeList,
|
|
renderLineageTreeHtml: renderLineageTreeHtml,
|
|
relationLabel: relationLabel,
|
|
shouldRedirectToLogin: shouldRedirectToLogin,
|
|
isForbidden: isForbidden,
|
|
init: init
|
|
};
|
|
});
|