200 lines
8.3 KiB
JavaScript
200 lines
8.3 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(root);
|
|
return;
|
|
}
|
|
|
|
root.FamilyInvitePages = factory(root);
|
|
if (root.document) root.document.addEventListener('DOMContentLoaded', root.FamilyInvitePages.init);
|
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
|
'use strict';
|
|
|
|
var STATUS_TEXT = { ACTIVE: '待使用', REDEEMED: '已使用', REVOKED: '已撤销', EXPIRED: '已过期' };
|
|
|
|
function escapeHtml(value) {
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
function normalizeId(value) {
|
|
var text = String(value === undefined || value === null ? '' : value).trim();
|
|
return /^[1-9][0-9]*$/.test(text) ? text : '';
|
|
}
|
|
|
|
function normalizeInvitation(item) {
|
|
var source = item || {};
|
|
var inviteId = normalizeId(source.inviteId);
|
|
var genealogyId = normalizeId(source.genealogyId);
|
|
|
|
if (!inviteId || !genealogyId || !String(source.genealogyName || '').trim()) return null;
|
|
return {
|
|
inviteId: inviteId,
|
|
genealogyId: genealogyId,
|
|
genealogyName: String(source.genealogyName).trim(),
|
|
lineagePersonName: String(source.lineagePersonName || '').trim(),
|
|
status: String(source.status || '').toUpperCase(),
|
|
token: String(source.token || '').trim(),
|
|
expiresAt: String(source.expiresAt || '').trim(),
|
|
createTime: String(source.createTime || '').trim()
|
|
};
|
|
}
|
|
|
|
function invitationLink(token) {
|
|
var base;
|
|
|
|
if (!token) return '';
|
|
if (!root.location) return 'profile-invite.html?token=' + encodeURIComponent(token);
|
|
base = String(root.location.href || '').split('?')[0].split('#')[0];
|
|
return base + '?token=' + encodeURIComponent(token);
|
|
}
|
|
|
|
function renderInvitations(items) {
|
|
var list = Array.isArray(items) ? items.map(normalizeInvitation).filter(Boolean) : [];
|
|
|
|
if (!list.length) return '<div class="api-empty">还没有发出的邀请</div>';
|
|
return list.map(function (item) {
|
|
var canRevoke = item.status === 'ACTIVE';
|
|
return '<article class="module-row"><div><h3>' + escapeHtml(item.genealogyName) +
|
|
(item.lineagePersonName ? ' · ' + escapeHtml(item.lineagePersonName) : '') + '</h3><p>' +
|
|
escapeHtml([item.createTime, item.expiresAt ? '有效期至 ' + item.expiresAt : ''].filter(Boolean).join(' · ')) +
|
|
'</p></div><div class="row-actions"><span class="pill">' +
|
|
escapeHtml(STATUS_TEXT[item.status] || item.status || '未知') + '</span>' +
|
|
(canRevoke ? '<button class="btn outline" type="button" data-invite-revoke="' +
|
|
escapeHtml(item.inviteId) + '">撤销</button>' : '') + '</div></article>';
|
|
}).join('');
|
|
}
|
|
|
|
function redirectToLogin(api) {
|
|
if (api && api.clearToken) api.clearToken();
|
|
if (root.location) root.location.href = 'login.html';
|
|
}
|
|
|
|
async function init() {
|
|
var page = root.document && root.document.querySelector('[data-family-invite-page]');
|
|
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId ? root.ProfileUI.getGenealogyId() : '';
|
|
var form = page && page.querySelector('[data-invite-form]');
|
|
var personSelect = page && page.querySelector('[data-invite-person]');
|
|
var status = page && page.querySelector('[data-invite-status]');
|
|
var result = page && page.querySelector('[data-invite-result]');
|
|
var list = page && page.querySelector('[data-invite-list]');
|
|
var redeem = page && page.querySelector('[data-invite-redeem]');
|
|
var token = root.location ? new URLSearchParams(root.location.search).get('token') : '';
|
|
var pending = false;
|
|
|
|
if (!page) return;
|
|
if (!api || !api.getToken || !api.getToken()) return redirectToLogin(api);
|
|
if (token && redeem) {
|
|
form.hidden = true;
|
|
list.hidden = true;
|
|
redeem.hidden = false;
|
|
redeem.querySelector('[data-invite-token]').textContent = token;
|
|
try {
|
|
var preview = normalizeInvitation(await api.previewGenealogyInvitation(token));
|
|
if (!preview) throw new Error('邀请预览响应无效');
|
|
redeem.querySelector('h3').textContent = '接受“' + preview.genealogyName + '”邀请';
|
|
setStatus('', preview.expiresAt ? '邀请有效期至 ' + preview.expiresAt : '请确认是否加入该家谱');
|
|
} catch (error) {
|
|
setStatus('error', error.message || '邀请预览失败');
|
|
redeem.querySelector('[data-invite-redeem-submit]').disabled = true;
|
|
return;
|
|
}
|
|
redeem.querySelector('[data-invite-redeem-submit]').addEventListener('click', async function () {
|
|
if (pending) return;
|
|
pending = true;
|
|
setStatus('loading', '正在接受邀请…');
|
|
try {
|
|
await api.redeemGenealogyInvitation({ token: token });
|
|
setStatus('', '邀请已接受,你已加入该家谱。');
|
|
} catch (error) {
|
|
setStatus('error', error.message || '接受邀请失败');
|
|
} finally { pending = false; }
|
|
});
|
|
return;
|
|
}
|
|
if (!genealogyId) {
|
|
root.ProfileUI.setApiState(status, 'error', '缺少家谱上下文,请从“我的家谱”重新进入。');
|
|
return;
|
|
}
|
|
|
|
function setStatus(type, message) {
|
|
if (type) root.ProfileUI.setApiState(status, type, message);
|
|
else status.textContent = message || '';
|
|
}
|
|
|
|
async function loadList() {
|
|
var invitations = await api.myGenealogyInvitations();
|
|
list.innerHTML = renderInvitations(invitations);
|
|
}
|
|
|
|
try {
|
|
var initial = await Promise.all([api.lineagePersonOptions(genealogyId), api.myGenealogyInvitations()]);
|
|
personSelect.innerHTML = '<option value="">不指定人物,加入后再关联</option>' +
|
|
(Array.isArray(initial[0]) ? initial[0] : []).map(function (item) {
|
|
var id = normalizeId(item.personId || item.lineagePersonId);
|
|
var name = String(item.displayName || item.personName || item.name || '').trim();
|
|
return id && name ? '<option value="' + escapeHtml(id) + '">' + escapeHtml(name) + '</option>' : '';
|
|
}).join('');
|
|
list.innerHTML = renderInvitations(initial[1]);
|
|
setStatus('', '可生成一次性邀请链接');
|
|
} catch (error) {
|
|
setStatus('error', error.message || '邀请服务读取失败');
|
|
}
|
|
|
|
form.addEventListener('submit', async function (event) {
|
|
var body = {};
|
|
var created;
|
|
var link;
|
|
|
|
event.preventDefault();
|
|
if (pending) return;
|
|
if (personSelect.value) body.lineagePersonId = personSelect.value;
|
|
pending = true;
|
|
setStatus('loading', '正在生成邀请…');
|
|
try {
|
|
created = normalizeInvitation(await api.issueGenealogyInvitation(genealogyId, body));
|
|
if (!created || !created.token) throw new Error('邀请已创建,但响应缺少一次性令牌');
|
|
link = invitationLink(created.token);
|
|
result.hidden = false;
|
|
result.querySelector('[data-invite-link]').value = link;
|
|
setStatus('', '邀请已生成,请及时复制给家人。');
|
|
await loadList();
|
|
} catch (error) {
|
|
setStatus('error', error.message || '邀请生成失败');
|
|
} finally { pending = false; }
|
|
});
|
|
|
|
result.querySelector('[data-invite-copy]').addEventListener('click', async function () {
|
|
var value = result.querySelector('[data-invite-link]').value;
|
|
try {
|
|
await root.navigator.clipboard.writeText(value);
|
|
setStatus('', '邀请链接已复制');
|
|
} catch (error) {
|
|
result.querySelector('[data-invite-link]').select();
|
|
setStatus('', '请按 Ctrl+C 复制邀请链接');
|
|
}
|
|
});
|
|
|
|
list.addEventListener('click', async function (event) {
|
|
var button = event.target.closest('[data-invite-revoke]');
|
|
if (!button || pending) return;
|
|
pending = true;
|
|
setStatus('loading', '正在撤销邀请…');
|
|
try {
|
|
await api.revokeGenealogyInvitation(button.getAttribute('data-invite-revoke'));
|
|
await loadList();
|
|
setStatus('', '邀请已撤销');
|
|
} catch (error) { setStatus('error', error.message || '撤销邀请失败'); }
|
|
finally { pending = false; }
|
|
});
|
|
}
|
|
|
|
return {
|
|
normalizeInvitation: normalizeInvitation,
|
|
invitationLink: invitationLink,
|
|
renderInvitations: renderInvitations,
|
|
init: init
|
|
};
|
|
});
|