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

317 lines
12 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) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.GenealogyEntryPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.GenealogyEntryPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
function getTargetPage(search) {
var params = new URLSearchParams(String(search === undefined && root.location ? root.location.search : search || '').replace(/^\?/, ''));
var target = params.get('next') || '';
return /^profile-[a-z-]+\.html$/.test(target) ? target : 'profile-family-home.html';
}
function buildStatus(quota) {
var source = quota || {};
var details = [];
if (Number(source.createRemaining) === -1) details.push('创建不限');
else if (Number(source.createRemaining) >= 0) details.push('还可创建 ' + source.createRemaining + ' 部');
if (Number(source.joinRemaining) === -1) details.push('加入不限');
else if (Number(source.joinRemaining) >= 0) details.push('还可加入 ' + source.joinRemaining + ' 部');
return details.length ? '家谱额度:' + details.join('') + '。' : '';
}
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 trimOrUndefined(value) {
var text;
if (value === undefined || value === null) return undefined;
text = String(value).trim();
return text || undefined;
}
function normalizeId(value) {
var text;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
text = String(value).trim();
return /^[1-9][0-9]*$/.test(text) ? text : '';
}
function buildGenealogyCreateBody(values) {
var source = values || {};
var body = {
genealogyName: String(source.genealogyName || '').trim(),
surname: String(source.surname || '').trim(),
regionCode: String(source.regionCode || '').trim()
};
var optionalNames = ['ancestralHall', 'originPlace', 'addressDetail', 'intro'];
var coverOssId = normalizeId(source.coverOssId);
var visibility = trimOrUndefined(source.visibility);
var joinMode = trimOrUndefined(source.joinMode);
optionalNames.forEach(function (name) {
var value = trimOrUndefined(source[name]);
if (value !== undefined) body[name] = value;
});
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
if (coverOssId) body.coverOssId = coverOssId;
else body.invalidCoverOssId = true;
}
if (visibility !== undefined) body.visibility = visibility;
if (joinMode !== undefined) body.joinMode = joinMode;
return body;
}
function validateGenealogyCreateBody(body) {
if (!body || !body.genealogyName) return '请填写谱名';
if (!body.surname) return '请填写姓氏';
if (!body.regionCode) return '请选择家谱所在地区';
if (body.visibility !== undefined && ['0', '1', '2'].indexOf(body.visibility) < 0) return '可见范围选项无效';
if (body.joinMode !== undefined && ['0', '1', '2'].indexOf(body.joinMode) < 0) return '加入方式选项无效';
if (body.invalidCoverOssId) return '封面文件无效,请重新选择';
return '';
}
function canCreateGenealogy(quota) {
var remaining = Number(quota && quota.createRemaining);
if (quota && typeof quota.canCreate === 'boolean') return quota.canCreate;
return Number.isInteger(remaining) && (remaining === -1 || remaining > 0);
}
function normalizeCreatedGenealogy(item) {
var genealogyId = normalizeId(item && item.genealogyId);
var genealogyName = String(item && item.genealogyName || '').trim();
var surname = String(item && item.surname || '').trim();
if (!genealogyId || !genealogyName || !surname) return null;
return {
genealogyId: genealogyId,
genealogyName: genealogyName,
surname: surname,
regionFullName: String(item && item.regionFullName || '')
};
}
function buildCreatedGenealogyUrl(item) {
var genealogy = normalizeCreatedGenealogy(item);
return genealogy ? buildEntryUrl(genealogy.genealogyId, 'profile-family-home.html') : '';
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function redirectToLogin(api) {
if (api && api.clearToken) api.clearToken();
if (!root.location) return;
if (typeof root.location.replace === 'function') root.location.replace('login.html');
else root.location.href = 'login.html';
}
function getFormValues(form) {
var values = {};
Array.prototype.forEach.call(form.querySelectorAll('[name]'), function (field) {
values[field.name] = field.value;
});
return values;
}
function setCreateStatus(message) {
var status = root.document && root.document.querySelector('[data-genealogy-create-status]');
if (status) status.textContent = message || '';
}
function setCreatePending(pending) {
var button = root.document && root.document.querySelector('[data-genealogy-create-submit]');
if (button) button.disabled = Boolean(pending);
}
async function initGenealogyCreatePage() {
var form = root.document && root.document.querySelector('[data-genealogy-create-form]');
var quotaElement = root.document && root.document.querySelector('[data-genealogy-create-quota]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var quota;
var writePending = false;
if (!form) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
try {
quota = await api.genealogyQuota();
if (quotaElement) quotaElement.textContent = buildStatus(quota) || '当前额度信息不可用';
if (!canCreateGenealogy(quota)) {
setCreateStatus('当前没有可用的家谱创建额度');
setCreatePending(true);
return;
}
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
if (quotaElement) quotaElement.textContent = '家谱额度读取失败';
setCreateStatus(error.message || '无法读取家谱创建额度');
setCreatePending(true);
return;
}
if (form.__genealogyCreateBound) return;
form.__genealogyCreateBound = true;
form.addEventListener('submit', async function (event) {
var body;
var validation;
var created;
var verified;
event.preventDefault();
if (writePending) return;
body = buildGenealogyCreateBody(getFormValues(form));
validation = validateGenealogyCreateBody(body);
if (validation) {
setCreateStatus(validation);
return;
}
delete body.invalidCoverOssId;
writePending = true;
setCreatePending(true);
setCreateStatus('正在创建家谱…');
try {
created = normalizeCreatedGenealogy(await api.createGenealogy(body));
if (!created) throw new Error('创建响应缺少有效家谱信息');
verified = normalizeCreatedGenealogy(await api.genealogyDetail(created.genealogyId));
if (!verified || verified.genealogyId !== created.genealogyId) throw new Error('创建后无法读取同一家谱');
if (root.location) root.location.href = buildCreatedGenealogyUrl(verified);
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setCreateStatus(error.message || '创建家谱失败,请稍后重试');
} finally {
writePending = false;
setCreatePending(false);
}
});
}
function normalizeGenealogy(item) {
var source = item || {};
var id = source.genealogyId === undefined || source.genealogyId === null
? ''
: String(source.genealogyId);
if (!/^[1-9][0-9]*$/.test(id) || !source.genealogyName) return null;
return {
genealogyId: id,
genealogyName: String(source.genealogyName),
surname: source.surname === undefined || source.surname === null ? '' : String(source.surname),
regionFullName: source.regionFullName === undefined || source.regionFullName === null ? '' : String(source.regionFullName),
memberCount: source.memberCount,
personCount: source.personCount,
roleType: source.roleType === undefined || source.roleType === null ? '' : String(source.roleType),
canManage: source.canManage === true
};
}
function buildEntryUrl(genealogyId, target) {
var params = new URLSearchParams();
params.set('genealogyId', String(genealogyId));
return (target || 'profile-family-home.html') + '?' + params.toString();
}
function renderGenealogies(container, data, target) {
var items = Array.isArray(data) ? data.map(normalizeGenealogy).filter(Boolean) : [];
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">当前账号还没有可进入的家谱</div>';
return;
}
container.innerHTML = items.map(function (item) {
var meta = [
item.surname ? item.surname + '氏' : '',
item.regionFullName,
item.memberCount === undefined || item.memberCount === null ? '' : item.memberCount + ' 位成员',
item.personCount === undefined || item.personCount === null ? '' : item.personCount + ' 位世系人物'
].filter(Boolean).join(' · ');
return '<a class="module-row" data-genealogy-id="' + escapeHtml(item.genealogyId) +
'" href="' + escapeHtml(buildEntryUrl(item.genealogyId, target)) + '">' +
'<div><h3>' + escapeHtml(item.genealogyName) + '</h3><p>' + escapeHtml(meta) +
'</p></div><span class="pill">' + (item.canManage ? '管理并进入' : '进入家谱') + '</span></a>';
}).join('');
}
async function init() {
if (root.document && root.document.querySelector('[data-genealogy-create-page]')) {
await initGenealogyCreatePage();
return;
}
var status = root.document && root.document.querySelector('[data-genealogy-entry-status]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var target = getTargetPage();
var container = root.document && root.document.querySelector('[data-genealogy-list="mine"]');
var result;
if (!status) return;
if (!api || !api.genealogyQuota || !api.genealogiesMine) {
status.textContent = '家谱入口接口初始化失败,请刷新后重试。';
return;
}
try {
result = await Promise.all([api.genealogiesMine(), api.genealogyQuota()]);
renderGenealogies(container, result[0], target);
status.textContent = buildStatus(result[1]);
} catch (error) {
status.textContent = '无法读取我的家谱,请稍后重试。';
}
}
return {
getTargetPage: getTargetPage,
buildStatus: buildStatus,
buildGenealogyCreateBody: buildGenealogyCreateBody,
validateGenealogyCreateBody: validateGenealogyCreateBody,
canCreateGenealogy: canCreateGenealogy,
normalizeCreatedGenealogy: normalizeCreatedGenealogy,
buildCreatedGenealogyUrl: buildCreatedGenealogyUrl,
shouldRedirectToLogin: shouldRedirectToLogin,
initGenealogyCreatePage: initGenealogyCreatePage,
normalizeGenealogy: normalizeGenealogy,
buildEntryUrl: buildEntryUrl,
renderGenealogies: renderGenealogies,
init: init
};
});