fb1743aa2a
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
317 lines
12 KiB
JavaScript
317 lines
12 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(root);
|
|
return;
|
|
}
|
|
|
|
root.CeremonyPages = factory(root);
|
|
if (root.document) {
|
|
root.document.addEventListener('DOMContentLoaded', function () {
|
|
root.CeremonyPages.init();
|
|
});
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
|
'use strict';
|
|
|
|
var documentRef = root.document;
|
|
var invitationsById = Object.create(null);
|
|
var selectedInvitationId = '';
|
|
var writePending = false;
|
|
var invitationStatuses = ['PENDING', 'ACCEPTED', 'DECLINED', 'CANCELED'];
|
|
var responseStatuses = ['ACCEPTED', 'DECLINED'];
|
|
var statusLabels = {
|
|
PENDING: '待响应',
|
|
ACCEPTED: '已接受',
|
|
DECLINED: '已拒绝',
|
|
CANCELED: '已取消'
|
|
};
|
|
|
|
function getApi() {
|
|
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
}
|
|
|
|
function query(selector, rootNode) {
|
|
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
|
|
}
|
|
|
|
function shouldRedirectToLogin(api, error) {
|
|
var status = error && (error.status || error.code);
|
|
|
|
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
|
}
|
|
|
|
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 normalizeId(value) {
|
|
if (value === undefined || value === null || value === '') return '';
|
|
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
|
return /^[1-9][0-9]*$/.test(String(value)) ? String(value) : '';
|
|
}
|
|
|
|
function stringValue(value) {
|
|
return value === undefined || value === null ? '' : String(value);
|
|
}
|
|
|
|
function optionalInteger(value) {
|
|
var number;
|
|
|
|
if (value === undefined || value === null || value === '') return undefined;
|
|
number = Number(value);
|
|
return Number.isSafeInteger(number) ? number : undefined;
|
|
}
|
|
|
|
function optionalNumber(value) {
|
|
var number;
|
|
|
|
if (value === undefined || value === null || value === '') return undefined;
|
|
number = Number(value);
|
|
return Number.isFinite(number) ? number : undefined;
|
|
}
|
|
|
|
function normalizeInvitation(item) {
|
|
var invitationId = normalizeId(item && item.invitationId);
|
|
var genealogyId = normalizeId(item && item.genealogyId);
|
|
var ceremonyId = normalizeId(item && item.ceremonyId);
|
|
var inviteeUserId = normalizeId(item && item.inviteeUserId);
|
|
var inviteStatus = stringValue(item && item.inviteStatus);
|
|
var invitation;
|
|
var inviteVersion;
|
|
var longitude;
|
|
var latitude;
|
|
|
|
if (!invitationId || !genealogyId || !ceremonyId || invitationStatuses.indexOf(inviteStatus) < 0) {
|
|
return null;
|
|
}
|
|
invitation = {
|
|
invitationId: invitationId,
|
|
genealogyId: genealogyId,
|
|
ceremonyId: ceremonyId,
|
|
inviteStatus: inviteStatus,
|
|
deliveredTime: stringValue(item.deliveredTime),
|
|
readTime: stringValue(item.readTime),
|
|
responseTime: stringValue(item.responseTime),
|
|
ceremonyTitle: stringValue(item.ceremonyTitle),
|
|
ceremonyTime: stringValue(item.ceremonyTime),
|
|
location: stringValue(item.location),
|
|
locationAddress: stringValue(item.locationAddress)
|
|
};
|
|
if (inviteeUserId) invitation.inviteeUserId = inviteeUserId;
|
|
inviteVersion = optionalInteger(item.inviteVersion);
|
|
longitude = optionalNumber(item.longitude);
|
|
latitude = optionalNumber(item.latitude);
|
|
if (inviteVersion !== undefined) invitation.inviteVersion = inviteVersion;
|
|
if (longitude !== undefined) invitation.longitude = longitude;
|
|
if (latitude !== undefined) invitation.latitude = latitude;
|
|
return invitation;
|
|
}
|
|
|
|
function normalizeList(data) {
|
|
return Array.isArray(data) ? data : [];
|
|
}
|
|
|
|
function buildInvitationResponseBody(values) {
|
|
return {
|
|
inviteStatus: stringValue(values && values.inviteStatus)
|
|
};
|
|
}
|
|
|
|
function validateInvitationResponseBody(body) {
|
|
return !body || responseStatuses.indexOf(body.inviteStatus) < 0 ? '邀请只能选择接受或拒绝' : '';
|
|
}
|
|
|
|
function canRespondToInvitation(invitation) {
|
|
return Boolean(invitation && invitation.inviteStatus === 'PENDING');
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return stringValue(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function invitationTitle(invitation) {
|
|
return invitation.ceremonyTitle || '未命名贺礼活动';
|
|
}
|
|
|
|
function renderInvitationRow(invitation) {
|
|
var actions = '<button class="pill" type="button" data-invitation-detail="' +
|
|
escapeHtml(invitation.invitationId) + '">查看详情</button>';
|
|
|
|
if (canRespondToInvitation(invitation)) {
|
|
actions += '<button class="pill" type="button" data-invitation-response="ACCEPTED" data-invitation-id="' +
|
|
escapeHtml(invitation.invitationId) + '">接受邀请</button>' +
|
|
'<button class="pill is-danger" type="button" data-invitation-response="DECLINED" data-invitation-id="' +
|
|
escapeHtml(invitation.invitationId) + '">拒绝邀请</button>';
|
|
}
|
|
return '<article class="module-row ceremony-invitation-row" data-invitation-row="' +
|
|
escapeHtml(invitation.invitationId) + '"><div><h3>' + escapeHtml(invitationTitle(invitation)) +
|
|
'</h3><p>' + escapeHtml(statusLabels[invitation.inviteStatus]) +
|
|
(invitation.ceremonyTime ? ' · ' + escapeHtml(invitation.ceremonyTime) : '') +
|
|
(invitation.location ? ' · ' + escapeHtml(invitation.location) : '') +
|
|
'</p></div><div class="row-actions">' + actions + '</div></article>';
|
|
}
|
|
|
|
function renderDetailValue(label, value) {
|
|
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
|
|
}
|
|
|
|
function renderInvitationDetail(invitation) {
|
|
var mapLink = '';
|
|
|
|
if (!invitation) return '<div class="api-empty">请选择一条邀请查看详情</div>';
|
|
if (invitation.longitude !== undefined && invitation.latitude !== undefined) {
|
|
mapLink = ' <a class="pill" target="_blank" rel="noopener noreferrer" href="https://uri.amap.com/marker?position=' +
|
|
encodeURIComponent(invitation.longitude + ',' + invitation.latitude) + '&name=' +
|
|
encodeURIComponent(invitation.location || invitation.locationAddress || invitationTitle(invitation)) +
|
|
'">打开地图</a>';
|
|
}
|
|
return '<div class="form-like invitation-detail">' +
|
|
renderDetailValue('活动标题', invitationTitle(invitation)) +
|
|
renderDetailValue('邀请状态', statusLabels[invitation.inviteStatus]) +
|
|
renderDetailValue('活动时间', invitation.ceremonyTime) +
|
|
renderDetailValue('地点', invitation.location) +
|
|
'<p><span>详细地址</span><b>' + escapeHtml(invitation.locationAddress || '未提供') + mapLink + '</b></p>' +
|
|
renderDetailValue('投递时间', invitation.deliveredTime) +
|
|
renderDetailValue('阅读时间', invitation.readTime) +
|
|
renderDetailValue('响应时间', invitation.responseTime) +
|
|
'</div>';
|
|
}
|
|
|
|
function renderDetail(invitation) {
|
|
var container = query('[data-my-invitation-detail]');
|
|
|
|
selectedInvitationId = invitation ? invitation.invitationId : '';
|
|
if (container) container.innerHTML = renderInvitationDetail(invitation);
|
|
}
|
|
|
|
function renderInvitations(data) {
|
|
var container = query('[data-my-invitation-list]');
|
|
var normalized = normalizeList(data).map(normalizeInvitation);
|
|
var invalidCount = normalized.filter(function (item) { return !item; }).length;
|
|
var invitations = normalized.filter(Boolean);
|
|
|
|
invitationsById = Object.create(null);
|
|
invitations.forEach(function (invitation) {
|
|
invitationsById[invitation.invitationId] = invitation;
|
|
});
|
|
if (!container) return invitations;
|
|
if (invalidCount) {
|
|
container.innerHTML = '<div class="api-empty">邀请响应缺少稳定 ID 或合法状态,请联系后端核对 DTO。</div>';
|
|
renderDetail(null);
|
|
return [];
|
|
}
|
|
if (!invitations.length) {
|
|
container.innerHTML = '<div class="api-empty">暂无贺礼邀请</div>';
|
|
renderDetail(null);
|
|
return [];
|
|
}
|
|
container.innerHTML = invitations.map(renderInvitationRow).join('');
|
|
renderDetail(invitationsById[selectedInvitationId] || invitations[0]);
|
|
return invitations;
|
|
}
|
|
|
|
function setWritePending(pending) {
|
|
var buttons;
|
|
|
|
writePending = pending;
|
|
if (!documentRef) return;
|
|
buttons = documentRef.querySelectorAll('[data-invitation-response]');
|
|
Array.prototype.forEach.call(buttons, function (button) {
|
|
button.disabled = pending;
|
|
});
|
|
}
|
|
|
|
function setListState(message) {
|
|
var container = query('[data-my-invitation-list]');
|
|
|
|
if (container) container.innerHTML = '<div class="api-empty">' + escapeHtml(message) + '</div>';
|
|
}
|
|
|
|
async function loadMyInvitations() {
|
|
var api = getApi();
|
|
|
|
if (redirectUnauthorized(api)) return;
|
|
setListState('正在加载我的邀请…');
|
|
try {
|
|
renderInvitations(await api.myCeremonyInvitations());
|
|
} catch (error) {
|
|
if (redirectUnauthorized(api, error)) return;
|
|
setListState(Number(error && (error.status || error.code)) === 403 ? '当前账号无权查看邀请' : (error.message || '邀请加载失败'));
|
|
renderDetail(null);
|
|
}
|
|
}
|
|
|
|
async function respondToInvitation(invitationId, inviteStatus) {
|
|
var api = getApi();
|
|
var invitation = invitationsById[normalizeId(invitationId)];
|
|
var body = buildInvitationResponseBody({ inviteStatus: inviteStatus });
|
|
var validation = validateInvitationResponseBody(body);
|
|
|
|
if (writePending || redirectUnauthorized(api) || !invitation || !canRespondToInvitation(invitation)) return;
|
|
if (validation) return;
|
|
setWritePending(true);
|
|
try {
|
|
await api.respondCeremonyInvitation(invitation.genealogyId, invitation.ceremonyId, body);
|
|
selectedInvitationId = invitation.invitationId;
|
|
await loadMyInvitations();
|
|
} catch (error) {
|
|
if (redirectUnauthorized(api, error)) return;
|
|
setListState(Number(error && (error.status || error.code)) === 403 ? '当前账号无权响应该邀请' : (error.message || '邀请响应失败'));
|
|
} finally {
|
|
setWritePending(false);
|
|
}
|
|
}
|
|
|
|
function bindActions() {
|
|
if (!documentRef) return;
|
|
documentRef.addEventListener('click', function (event) {
|
|
var detailButton = event.target.closest('[data-invitation-detail]');
|
|
var responseButton = event.target.closest('[data-invitation-response]');
|
|
|
|
if (detailButton) {
|
|
event.preventDefault();
|
|
renderDetail(invitationsById[detailButton.getAttribute('data-invitation-detail')] || null);
|
|
return;
|
|
}
|
|
if (!responseButton) return;
|
|
event.preventDefault();
|
|
respondToInvitation(
|
|
responseButton.getAttribute('data-invitation-id'),
|
|
responseButton.getAttribute('data-invitation-response')
|
|
);
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
if (!documentRef || !query('[data-ceremony-page]')) return;
|
|
bindActions();
|
|
loadMyInvitations();
|
|
}
|
|
|
|
return {
|
|
normalizeInvitation: normalizeInvitation,
|
|
normalizeList: normalizeList,
|
|
buildInvitationResponseBody: buildInvitationResponseBody,
|
|
validateInvitationResponseBody: validateInvitationResponseBody,
|
|
canRespondToInvitation: canRespondToInvitation,
|
|
renderInvitationRow: renderInvitationRow,
|
|
renderInvitationDetail: renderInvitationDetail,
|
|
shouldRedirectToLogin: shouldRedirectToLogin,
|
|
loadMyInvitations: loadMyInvitations,
|
|
init: init
|
|
};
|
|
});
|