Files
jiapu/public/js/generation-pages.js
T
fizzleaf fb1743aa2a feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询
- 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作
- 集成通知详情获取方法和通知ID安全验证机制
- 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约
- 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作
- 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示
- 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
2026-07-29 16:57:27 +08:00

694 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function (root, factory) {
// 字辈管理模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.GenerationPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.GenerationPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var poemsById = Object.create(null);
var lastPreviewSignature = '';
var managementAccess = false;
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-generation-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 buildGenerationPoemBody(values) {
var source = values || {};
var body = {
generationNo: toIntegerOrUndefined(source.generationNo),
generationText: trimOrUndefined(source.generationText)
};
var description = trimOrUndefined(source.description);
var sortOrder = trimOrUndefined(source.sortOrder);
var status = trimOrUndefined(source.status);
if (description !== undefined) body.description = description;
if (sortOrder !== undefined) body.sortOrder = Number(sortOrder);
if (status !== undefined) body.status = status;
return body;
}
function buildBatchBody(values) {
var source = values || {};
return {
poemText: String(source.poemText || '').trim(),
disableMissing: Boolean(source.disableMissing)
};
}
function validateGenerationPoemBody(body) {
if (!body || !Number.isInteger(body.generationNo) || body.generationNo < 1 || body.generationNo > 2147483647) {
return '世代序号必须是 1 到 2147483647 之间的整数';
}
if (!body.generationText) return '请填写字辈文字';
if (body.generationText.length > 50) return '字辈文字不能超过 50 个字符';
if (body.description && body.description.length > 500) return '说明不能超过 500 个字符';
if (body.sortOrder !== undefined && (!Number.isInteger(body.sortOrder) || !Number.isSafeInteger(body.sortOrder))) {
return '排序值必须是整数';
}
if (body.sortOrder !== undefined && (body.sortOrder < -2147483648 || body.sortOrder > 2147483647)) {
return '排序值必须在 -2147483648 到 2147483647 之间';
}
if (body.status !== undefined && body.status !== '0' && body.status !== '1') return '字辈状态只能是 0 或 1';
return '';
}
function validateBatchBody(body) {
var words;
if (!body || !body.poemText) return '请填写批量字辈内容';
if (body.poemText.length > 26000) return '批量字辈内容不能超过 26000 个字符';
words = splitPoemText(body.poemText);
if (words.length > 500) return '一次最多导入 500 个世代';
if (words.some(function (word) { return word.length > 50; })) return '单个字辈不能超过 50 个字符';
return '';
}
function splitPoemText(value) {
var text = String(value || '').trim();
if (!text) return [];
if (/[\s,;;、/|]/.test(text)) {
return text.split(/[\s,;;、/|]+/).filter(Boolean);
}
return Array.from(text);
}
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 getPoemId(item) {
return normalizeId(item && item.poemId);
}
function normalizeGenerationPoem(item) {
var poemId = getPoemId(item);
var genealogyId = normalizeId(item && item.genealogyId);
var generationNo = toIntegerOrUndefined(item && item.generationNo);
var generationText = trimOrUndefined(item && item.generationText);
var status = trimOrUndefined(item && item.status);
var poem;
if (!poemId || generationNo === undefined || !generationText || (status !== '0' && status !== '1')) return null;
poem = {
poemId: poemId,
generationNo: generationNo,
generationText: generationText,
status: status
};
if (genealogyId) poem.genealogyId = genealogyId;
if (item.genealogyNo !== undefined && item.genealogyNo !== null) poem.genealogyNo = String(item.genealogyNo);
if (item.genealogyName !== undefined && item.genealogyName !== null) poem.genealogyName = String(item.genealogyName);
if (item.description !== undefined && item.description !== null) poem.description = String(item.description);
if (toIntegerOrUndefined(item.sortOrder) !== undefined) poem.sortOrder = toIntegerOrUndefined(item.sortOrder);
if (item.remark !== undefined && item.remark !== null) poem.remark = String(item.remark);
return poem;
}
function normalizeGenerationAccess(genealogy) {
var canEditContent = Boolean(genealogy && genealogy.canEditContent === true);
return {
canEditContent: canEditContent,
listMethod: canEditContent ? 'generationPoemsManagement' : 'generationPoems'
};
}
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderGenerationPoem(poem) {
var details = [];
var stateText = poem.status === '1' ? '已停用' : '正常';
var actions = '';
if (poem.description) details.push('说明:' + poem.description);
if (poem.sortOrder !== undefined) details.push('排序:' + poem.sortOrder);
details.push('状态:' + stateText);
if (managementAccess) {
actions = '<div class="row-actions">' +
'<button class="pill" type="button" data-generation-edit="' + escapeHtml(poem.poemId) + '">编辑</button>' +
'<button class="pill" type="button" data-generation-status="' + escapeHtml(poem.poemId) + '">' + (poem.status === '1' ? '恢复' : '停用') + '</button>' +
'</div>';
}
return '<div class="module-row" data-generation-poem-id="' + escapeHtml(poem.poemId) + '">' +
'<div><h3>第 ' + escapeHtml(poem.generationNo) + ' 代:' + escapeHtml(poem.generationText) + '</h3><p>' + escapeHtml(details.join(' · ')) + '</p></div>' +
actions + '</div>';
}
function renderGenerationPoems(data) {
var container = query('[data-generation-list]');
var normalized = normalizeList(data).map(normalizeGenerationPoem);
var invalidCount = normalized.filter(function (item) { return !item; }).length;
var poems = normalized.filter(Boolean);
poemsById = Object.create(null);
poems.forEach(function (poem) {
poemsById[poem.poemId] = poem;
});
if (!container) return;
if (invalidCount) {
container.innerHTML = '<div class="api-empty">字辈响应缺少 poemId、generationNo、generationText 或 status,请联系后端补充 DTO。</div>';
return;
}
if (!poems.length) {
container.innerHTML = '<div class="api-empty">暂无字辈记录</div>';
return;
}
container.innerHTML = poems.map(renderGenerationPoem).join('');
}
function normalizeCount(value) {
var count = toIntegerOrUndefined(value);
return count !== undefined && count >= 0 ? count : undefined;
}
function normalizePreviewItem(item) {
var action = item && item.action;
var generationNo = toIntegerOrUndefined(item && item.generationNo);
var poemId = getPoemId(item);
var oldText = trimOrUndefined(item && item.oldGenerationText);
var newText = trimOrUndefined(item && item.newGenerationText);
var oldStatus = trimOrUndefined(item && item.oldStatus);
var newStatus = trimOrUndefined(item && item.newStatus);
var normalized;
if (!generationNo || ['create', 'update', 'keep', 'disable'].indexOf(action) === -1) return null;
if (newStatus !== '0' && newStatus !== '1') return null;
if (action === 'create' && !newText) return null;
if (action !== 'create' && (!poemId || !oldText || (oldStatus !== '0' && oldStatus !== '1'))) return null;
if ((action === 'update' || action === 'keep') && !newText) return null;
normalized = {
generationNo: generationNo
};
if (poemId) normalized.poemId = poemId;
if (oldText) normalized.oldGenerationText = oldText;
if (newText) normalized.newGenerationText = newText;
if (oldStatus !== undefined) normalized.oldStatus = oldStatus;
normalized.newStatus = newStatus;
normalized.action = action;
if (item.warning !== undefined && item.warning !== null) normalized.warning = String(item.warning);
return normalized;
}
function normalizePreview(data) {
var counts;
var items;
var actualCounts = { create: 0, update: 0, keep: 0, disable: 0 };
if (!data || !Array.isArray(data.items) || !data.items.length) return null;
counts = {
createCount: normalizeCount(data.createCount),
updateCount: normalizeCount(data.updateCount),
keepCount: normalizeCount(data.keepCount),
disableCount: normalizeCount(data.disableCount)
};
if (Object.keys(counts).some(function (key) { return counts[key] === undefined; })) return null;
items = data.items.map(normalizePreviewItem);
if (items.some(function (item) { return !item; })) return null;
items.forEach(function (item) { actualCounts[item.action] += 1; });
if (counts.createCount !== actualCounts.create ||
counts.updateCount !== actualCounts.update ||
counts.keepCount !== actualCounts.keep ||
counts.disableCount !== actualCounts.disable) return null;
counts.items = items;
return counts;
}
function buildBatchConfirmMessage(disableMissing) {
if (disableMissing) {
return '确认按当前预览保存字辈,并停用未出现在文本中的后续世代吗?历史记录不会删除。';
}
return '确认按当前预览保存字辈吗?';
}
function renderBatchPreview(data) {
var container = query('[data-generation-batch-preview]');
var preview = normalizePreview(data);
var summary;
if (!container) return false;
if (!preview) {
container.innerHTML = '<div class="api-empty">批量预览响应缺少 items,请联系后端补充 DTO。</div>';
return false;
}
summary = '新增 ' + (preview.createCount === undefined ? '-' : preview.createCount) +
' 条 · 更新 ' + (preview.updateCount === undefined ? '-' : preview.updateCount) +
' 条 · 保留 ' + (preview.keepCount === undefined ? '-' : preview.keepCount) +
' 条 · 停用 ' + (preview.disableCount === undefined ? '-' : preview.disableCount) + ' 条';
container.innerHTML = '<div class="module-row"><div><h3>变更预览</h3><p>' + escapeHtml(summary) + '</p></div></div>' +
preview.items.map(function (item) {
var generationNo = item && item.generationNo;
var action = item && item.action;
var oldText = item && item.oldGenerationText;
var newText = item && item.newGenerationText;
var warning = item && item.warning;
var oldStatus = item && item.oldStatus;
var newStatus = item && item.newStatus;
var text = '第 ' + (generationNo === undefined || generationNo === null ? '-' : generationNo) + ' 代:' +
(oldText === undefined || oldText === null ? '(新增)' : oldText) + ' → ' +
(newText === undefined || newText === null ? '(无)' : newText);
if (action) text += ' · ' + action;
if (oldStatus !== undefined || newStatus !== undefined) {
text += ' · 状态:' + formatStatus(oldStatus) + ' → ' + formatStatus(newStatus);
}
if (warning) text += ' · 警告:' + warning;
return '<div class="module-row"><p>' + escapeHtml(text) + '</p></div>';
}).join('');
return true;
}
function formatStatus(value) {
if (String(value) === '0') return '正常';
if (String(value) === '1') return '停用';
return value === undefined || value === null ? '未提供' : String(value);
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.type === 'checkbox' ? field.checked : field.value;
});
return values;
}
function setBatchStatus(message) {
var target = query('[data-generation-batch-status]');
if (target) target.textContent = message || '';
}
function refreshManagementControls() {
var disabled = !managementAccess || writePending;
queryAll('[data-generation-management]').forEach(function (element) {
element.hidden = !managementAccess;
});
queryAll('[data-generation-add], [data-generation-batch-action], [data-generation-edit], [data-generation-status]').forEach(function (control) {
control.disabled = disabled;
});
queryAll('[data-generation-management] textarea, [data-generation-management] input').forEach(function (control) {
control.disabled = disabled;
});
}
function setManagementEnabled(enabled) {
managementAccess = Boolean(enabled);
refreshManagementControls();
}
function setWritePending(pending) {
writePending = Boolean(pending);
refreshManagementControls();
}
function renderForbiddenState() {
var container = query('[data-generation-list]');
if (container) container.innerHTML = '<div class="api-empty">当前账号没有字辈维护权限</div>';
lastPreviewSignature = '';
setManagementEnabled(false);
setBatchStatus('当前账号没有字辈维护权限');
}
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 requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
var container;
if (!genealogyId) {
container = query('[data-generation-list]');
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入字辈管理</div>';
showMessage('请从具体家谱进入字辈管理');
}
return genealogyId;
}
function promptForPoem(existing) {
var generationNo;
var generationText;
var description;
var sortOrder;
if (!root.prompt) return null;
generationNo = root.prompt('世代序号(从 1 开始)', existing ? String(existing.generationNo) : '');
if (generationNo === null) return null;
generationText = root.prompt('字辈文字(最多 50 个字符)', existing ? existing.generationText : '');
if (generationText === null) return null;
description = root.prompt('说明(最多 500 个字符,可留空)', existing && existing.description ? existing.description : '');
if (description === null) return null;
sortOrder = root.prompt('排序值(可留空,越小越靠前)', existing && existing.sortOrder !== undefined ? String(existing.sortOrder) : '');
if (sortOrder === null) return null;
return buildGenerationPoemBody({
generationNo: generationNo,
generationText: generationText,
description: description,
sortOrder: sortOrder,
status: existing && existing.status
});
}
async function loadGenerationPoems() {
var api = getApi();
var genealogyId;
var access;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
try {
access = normalizeGenerationAccess(await api.genealogyDetail(genealogyId));
setManagementEnabled(access.canEditContent);
renderGenerationPoems(await api[access.listMethod](genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
showMessage(error.message || '字辈列表加载失败');
}
}
async function savePoem(poemId) {
var api = getApi();
var genealogyId;
var existing = poemId ? poemsById[poemId] : null;
var body;
var validation;
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || (poemId && !existing)) return;
body = promptForPoem(existing);
if (!body) return;
validation = validateGenerationPoemBody(body);
if (validation) {
showMessage(validation);
return;
}
setWritePending(true);
try {
if (poemId) {
await api.updateGenerationPoem(genealogyId, poemId, body);
} else {
await api.createGenerationPoem(genealogyId, body);
}
showMessage('字辈已保存');
await loadGenerationPoems();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
showMessage(error.message || '字辈保存失败');
} finally {
setWritePending(false);
}
}
async function togglePoemStatus(poemId) {
var api = getApi();
var genealogyId;
var existing = poemsById[poemId];
var body;
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !existing) return;
body = buildGenerationPoemBody({
generationNo: existing.generationNo,
generationText: existing.generationText,
description: existing.description,
sortOrder: existing.sortOrder,
status: existing.status === '1' ? '0' : '1'
});
setWritePending(true);
try {
await api.updateGenerationPoem(genealogyId, poemId, body);
showMessage(existing.status === '1' ? '字辈已恢复' : '字辈已停用');
await loadGenerationPoems();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
showMessage(error.message || '字辈状态更新失败');
} finally {
setWritePending(false);
}
}
async function previewBatch() {
var api = getApi();
var genealogyId;
var form = query('[data-generation-batch-form]');
var body;
var validation;
if (writePending || !managementAccess || redirectUnauthorized(api) || !form) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
body = buildBatchBody(getFormValues(form));
validation = validateBatchBody(body);
if (validation) {
setBatchStatus(validation);
return;
}
setBatchStatus('正在预览…');
setWritePending(true);
try {
if (renderBatchPreview(await api.previewGenerationPoems(genealogyId, body))) {
lastPreviewSignature = JSON.stringify(body);
setBatchStatus('预览完成,请确认后保存');
} else {
lastPreviewSignature = '';
setBatchStatus('预览响应不完整');
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
lastPreviewSignature = '';
setBatchStatus(error.message || '批量预览失败');
} finally {
setWritePending(false);
}
}
async function saveBatch() {
var api = getApi();
var genealogyId;
var form = query('[data-generation-batch-form]');
var body;
var validation;
if (writePending || !managementAccess || redirectUnauthorized(api) || !form) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
body = buildBatchBody(getFormValues(form));
validation = validateBatchBody(body);
if (validation) {
setBatchStatus(validation);
return;
}
if (lastPreviewSignature !== JSON.stringify(body)) {
setBatchStatus('请先预览当前内容,再保存');
return;
}
if (root.confirm && !root.confirm(buildBatchConfirmMessage(body.disableMissing))) return;
setBatchStatus('正在保存…');
setWritePending(true);
try {
await api.saveGenerationPoems(genealogyId, body);
lastPreviewSignature = '';
setBatchStatus('批量字辈已保存');
await loadGenerationPoems();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
renderForbiddenState();
return;
}
setBatchStatus(error.message || '批量保存失败');
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var target = event.target;
var action = target.closest('[data-generation-batch-action]');
var poemButton = target.closest('[data-generation-edit], [data-generation-status]');
if (action) {
event.preventDefault();
if (action.getAttribute('data-generation-batch-action') === 'preview') previewBatch();
if (action.getAttribute('data-generation-batch-action') === 'save') saveBatch();
return;
}
if (target.closest('[data-generation-add]')) {
event.preventDefault();
savePoem('');
return;
}
if (!poemButton) return;
event.preventDefault();
if (poemButton.hasAttribute('data-generation-edit')) savePoem(poemButton.getAttribute('data-generation-edit'));
if (poemButton.hasAttribute('data-generation-status')) togglePoemStatus(poemButton.getAttribute('data-generation-status'));
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-generation-page]')) loadGenerationPoems();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
buildGenerationPoemBody: buildGenerationPoemBody,
buildBatchBody: buildBatchBody,
validateGenerationPoemBody: validateGenerationPoemBody,
validateBatchBody: validateBatchBody,
splitPoemText: splitPoemText,
normalizeGenerationPoem: normalizeGenerationPoem,
normalizeGenerationAccess: normalizeGenerationAccess,
normalizePreview: normalizePreview,
buildBatchConfirmMessage: buildBatchConfirmMessage,
normalizeList: normalizeList,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});