460 lines
16 KiB
JavaScript
460 lines
16 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(root);
|
|
return;
|
|
}
|
|
|
|
root.JoinPages = factory(root);
|
|
if (root.document) {
|
|
root.document.addEventListener('DOMContentLoaded', function () {
|
|
root.JoinPages.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 APPLY_STATUS = {
|
|
'0': '待审核',
|
|
'1': '已通过',
|
|
'2': '已拒绝',
|
|
'3': '已撤销'
|
|
};
|
|
|
|
function escapeHtml(value) {
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function renderApiState(type, message) {
|
|
if (root.ProfileUI && root.ProfileUI.renderApiState) {
|
|
return root.ProfileUI.renderApiState(type, message);
|
|
}
|
|
return '<div class="api-state api-state--' + type + '" data-api-state="' + type + '">' +
|
|
escapeHtml(message) + '</div>';
|
|
}
|
|
|
|
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 optionalText(value) {
|
|
var text;
|
|
|
|
if (value === undefined || value === null) return '';
|
|
text = String(value).trim();
|
|
return text;
|
|
}
|
|
|
|
function buildJoinApplyBody(values) {
|
|
var source = values || {};
|
|
var body = {};
|
|
|
|
['applicantName', 'phone', 'relationDesc', 'applyReason'].forEach(function (name) {
|
|
var value = optionalText(source[name]);
|
|
|
|
if (value) body[name] = value;
|
|
});
|
|
return body;
|
|
}
|
|
|
|
function validateJoinApplyBody(body) {
|
|
var source = body || {};
|
|
var limits = {
|
|
applicantName: [50, '申请人姓名不能超过 50 个字符'],
|
|
phone: [30, '联系电话不能超过 30 个字符'],
|
|
relationDesc: [100, '关系说明不能超过 100 个字符'],
|
|
applyReason: [500, '申请理由不能超过 500 个字符']
|
|
};
|
|
var names = Object.keys(limits);
|
|
var index;
|
|
|
|
for (index = 0; index < names.length; index += 1) {
|
|
if (String(source[names[index]] || '').length > limits[names[index]][0]) {
|
|
return limits[names[index]][1];
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function normalizeJoinGenealogy(item) {
|
|
var source = item || {};
|
|
var genealogyId = normalizeId(source.genealogyId);
|
|
var genealogyName = optionalText(source.genealogyName);
|
|
var surname = optionalText(source.surname);
|
|
var status = source.status === undefined || source.status === null ? '' : String(source.status);
|
|
|
|
if (!genealogyId || !genealogyName || !surname || (status && status !== '0')) return null;
|
|
return {
|
|
genealogyId: genealogyId,
|
|
genealogyName: genealogyName,
|
|
surname: surname,
|
|
regionFullName: optionalText(source.regionFullName),
|
|
memberCount: source.memberCount,
|
|
joinMode: source.joinMode === undefined || source.joinMode === null ? '' : String(source.joinMode)
|
|
};
|
|
}
|
|
|
|
function normalizeJoinApply(item) {
|
|
var source = item || {};
|
|
var applyId = normalizeId(source.applyId);
|
|
var genealogyId = normalizeId(source.genealogyId);
|
|
var genealogyName = optionalText(source.genealogyName);
|
|
var status = source.status === undefined || source.status === null ? '' : String(source.status);
|
|
|
|
if (!applyId || !genealogyId || !genealogyName || !Object.prototype.hasOwnProperty.call(APPLY_STATUS, status)) {
|
|
return null;
|
|
}
|
|
return {
|
|
applyId: applyId,
|
|
genealogyId: genealogyId,
|
|
genealogyName: genealogyName,
|
|
surname: optionalText(source.surname),
|
|
relationDesc: optionalText(source.relationDesc),
|
|
applyReason: optionalText(source.applyReason),
|
|
auditRemark: optionalText(source.auditRemark),
|
|
auditTime: optionalText(source.auditTime),
|
|
status: status
|
|
};
|
|
}
|
|
|
|
function normalizeJoinApplies(data) {
|
|
var items;
|
|
|
|
if (!Array.isArray(data)) return [];
|
|
items = data.map(normalizeJoinApply);
|
|
return items.some(function (item) { return !item; }) ? [] : items;
|
|
}
|
|
|
|
function renderJoinGenealogyOptions(data) {
|
|
var items;
|
|
|
|
if (!Array.isArray(data)) return renderApiState('error', '家谱列表数据无效,请刷新重试');
|
|
items = data.map(normalizeJoinGenealogy).filter(Boolean);
|
|
if (!items.length) return renderApiState('empty', '没有找到可申请加入的家谱');
|
|
return items.map(function (item) {
|
|
var meta = [
|
|
item.surname + '氏',
|
|
item.regionFullName,
|
|
item.memberCount === undefined || item.memberCount === null ? '' : item.memberCount + ' 位成员'
|
|
].filter(Boolean).join(' · ');
|
|
|
|
return '<button class="module-row" type="button" data-join-genealogy-id="' +
|
|
escapeHtml(item.genealogyId) + '" data-join-genealogy-name="' +
|
|
escapeHtml(item.genealogyName) + '"><span><strong>' +
|
|
escapeHtml(item.genealogyName) + '</strong><small>' +
|
|
escapeHtml(meta) + '</small></span><span class="pill">选择</span></button>';
|
|
}).join('');
|
|
}
|
|
|
|
function renderMyJoinApplies(data) {
|
|
var items = normalizeJoinApplies(data);
|
|
|
|
if (!items.length) return renderApiState('empty', '当前没有加入申请');
|
|
return items.map(function (item) {
|
|
var details = [
|
|
item.surname ? item.surname + '氏' : '',
|
|
item.relationDesc,
|
|
item.applyReason,
|
|
item.auditRemark ? '审核说明:' + item.auditRemark : '',
|
|
item.auditTime ? '审核时间:' + item.auditTime : ''
|
|
].filter(Boolean).join(' · ');
|
|
var action = item.status === '0'
|
|
? '<button class="btn ghost" type="button" data-join-cancel-id="' +
|
|
escapeHtml(item.applyId) + '">撤销申请</button>'
|
|
: '';
|
|
|
|
return '<article class="module-row"><div><h3>' + escapeHtml(item.genealogyName) +
|
|
'</h3><p>' + escapeHtml(details || '等待申请状态更新') +
|
|
'</p></div><div><span class="pill">' + APPLY_STATUS[item.status] +
|
|
'</span>' + action + '</div></article>';
|
|
}).join('');
|
|
}
|
|
|
|
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 getApiStateType(error) {
|
|
return Number(error && (error.status || error.code)) === 403 ? 'forbidden' : 'error';
|
|
}
|
|
|
|
function setText(selector, message, type) {
|
|
var element = root.document && root.document.querySelector(selector);
|
|
|
|
if (!element) return;
|
|
if (!message) {
|
|
element.innerHTML = '';
|
|
return;
|
|
}
|
|
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
|
root.ProfileUI.setApiState(element, type || 'error', message);
|
|
return;
|
|
}
|
|
element.textContent = message;
|
|
}
|
|
|
|
function setOptionsState(options, type, message) {
|
|
if (!options) return;
|
|
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
|
root.ProfileUI.setApiState(options, type, message);
|
|
return;
|
|
}
|
|
options.innerHTML = renderApiState(type, message);
|
|
}
|
|
|
|
function setSubmitPending(button, pending, pendingLabel) {
|
|
if (!button) return;
|
|
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
|
button.disabled = Boolean(pending);
|
|
button.textContent = pending ? pendingLabel : button.dataset.defaultLabel;
|
|
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
|
}
|
|
|
|
function getFormValues(form) {
|
|
var values = {};
|
|
|
|
Array.prototype.forEach.call(form.querySelectorAll('[name]'), function (field) {
|
|
values[field.name] = field.value;
|
|
});
|
|
return values;
|
|
}
|
|
|
|
function canJoinGenealogy(quota) {
|
|
var remaining = Number(quota && quota.joinRemaining);
|
|
|
|
if (quota && typeof quota.canJoin === 'boolean') return quota.canJoin;
|
|
return Number.isInteger(remaining) && (remaining === -1 || remaining > 0);
|
|
}
|
|
|
|
async function initJoinApplyPage() {
|
|
var page = root.document && root.document.querySelector('[data-join-apply-page]');
|
|
var searchForm = root.document && root.document.querySelector('[data-join-genealogy-search]');
|
|
var options = root.document && root.document.querySelector('[data-join-genealogy-options]');
|
|
var applyForm = root.document && root.document.querySelector('[data-join-apply-form]');
|
|
var selected = root.document && root.document.querySelector('[data-join-selected-genealogy]');
|
|
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
var selectedGenealogyId = '';
|
|
var writePending = false;
|
|
|
|
if (!page) return;
|
|
if (shouldRedirectToLogin(api)) {
|
|
redirectToLogin(api);
|
|
return;
|
|
}
|
|
|
|
function clearSelectedGenealogy() {
|
|
selectedGenealogyId = '';
|
|
if (selected) selected.textContent = '尚未选择家谱';
|
|
}
|
|
|
|
async function loadOptions(keyword) {
|
|
var data;
|
|
|
|
clearSelectedGenealogy();
|
|
setText('[data-join-options-status]', '正在读取可申请家谱…', 'loading');
|
|
setOptionsState(options, 'loading', '正在读取可申请家谱…');
|
|
try {
|
|
data = await api.genealogyOptions(keyword ? { keyword: keyword } : {});
|
|
options.innerHTML = renderJoinGenealogyOptions(data);
|
|
setText('[data-join-options-status]', '');
|
|
} catch (error) {
|
|
if (shouldRedirectToLogin(api, error)) {
|
|
redirectToLogin(api);
|
|
return;
|
|
}
|
|
setOptionsState(options, getApiStateType(error), '读取家谱列表失败');
|
|
setText('[data-join-options-status]', error.message || '读取家谱列表失败,请稍后重试', getApiStateType(error));
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (!canJoinGenealogy(await api.genealogyQuota())) {
|
|
setText('[data-join-apply-status]', '当前没有可用的家谱加入额度', 'empty');
|
|
applyForm.querySelector('[data-join-apply-submit]').disabled = true;
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
if (shouldRedirectToLogin(api, error)) {
|
|
redirectToLogin(api);
|
|
return;
|
|
}
|
|
setText('[data-join-apply-status]', error.message || '无法读取家谱加入额度', getApiStateType(error));
|
|
applyForm.querySelector('[data-join-apply-submit]').disabled = true;
|
|
return;
|
|
}
|
|
|
|
searchForm.addEventListener('submit', function (event) {
|
|
event.preventDefault();
|
|
loadOptions(optionalText(searchForm.querySelector('[name="keyword"]').value));
|
|
});
|
|
options.addEventListener('click', function (event) {
|
|
var button = event.target.closest('[data-join-genealogy-id]');
|
|
|
|
if (!button || !options.contains(button)) return;
|
|
selectedGenealogyId = normalizeId(button.getAttribute('data-join-genealogy-id'));
|
|
if (!selectedGenealogyId) return;
|
|
selected.textContent = '已选择:' + button.getAttribute('data-join-genealogy-name');
|
|
Array.prototype.forEach.call(options.querySelectorAll('[data-join-genealogy-id]'), function (item) {
|
|
item.classList.toggle('is-selected', item === button);
|
|
});
|
|
});
|
|
applyForm.addEventListener('submit', async function (event) {
|
|
var body;
|
|
var validation;
|
|
var created;
|
|
var refreshed;
|
|
|
|
event.preventDefault();
|
|
if (writePending) return;
|
|
if (!selectedGenealogyId) {
|
|
setText('[data-join-apply-status]', '请先从家谱列表中选择要加入的家谱', 'error');
|
|
return;
|
|
}
|
|
body = buildJoinApplyBody(getFormValues(applyForm));
|
|
validation = validateJoinApplyBody(body);
|
|
if (validation) {
|
|
setText('[data-join-apply-status]', validation, 'error');
|
|
return;
|
|
}
|
|
writePending = true;
|
|
setSubmitPending(applyForm.querySelector('[data-join-apply-submit]'), true, '正在提交…');
|
|
setText('[data-join-apply-status]', '正在提交申请…', 'loading');
|
|
try {
|
|
created = normalizeJoinApply(await api.applyToGenealogy(selectedGenealogyId, body));
|
|
if (!created) throw new Error('申请响应缺少有效申请信息');
|
|
refreshed = normalizeJoinApplies(await api.myGenealogyJoinApplies());
|
|
if (!refreshed.some(function (item) { return item.applyId === created.applyId; })) {
|
|
throw new Error('提交后无法读取同一申请');
|
|
}
|
|
if (root.location) root.location.href = 'profile-join-family.html';
|
|
} catch (error) {
|
|
if (shouldRedirectToLogin(api, error)) {
|
|
redirectToLogin(api);
|
|
return;
|
|
}
|
|
setText('[data-join-apply-status]', error.message || '提交加入申请失败,请稍后重试', getApiStateType(error));
|
|
} finally {
|
|
writePending = false;
|
|
setSubmitPending(applyForm.querySelector('[data-join-apply-submit]'), false, '正在提交…');
|
|
}
|
|
});
|
|
await loadOptions('');
|
|
}
|
|
|
|
async function initMyJoinAppliesPage() {
|
|
var page = root.document && root.document.querySelector('[data-my-join-applies-page]');
|
|
var list = root.document && root.document.querySelector('[data-join-apply-list="mine"]');
|
|
var refresh = root.document && root.document.querySelector('[data-join-apply-refresh]');
|
|
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
var writePending = false;
|
|
|
|
if (!page) return;
|
|
if (shouldRedirectToLogin(api)) {
|
|
redirectToLogin(api);
|
|
return;
|
|
}
|
|
|
|
async function loadApplies() {
|
|
var data;
|
|
|
|
setText('[data-my-join-applies-status]', '正在读取申请记录…', 'loading');
|
|
try {
|
|
data = await api.myGenealogyJoinApplies();
|
|
list.innerHTML = renderMyJoinApplies(data);
|
|
setText('[data-my-join-applies-status]', '');
|
|
return normalizeJoinApplies(data);
|
|
} catch (error) {
|
|
if (shouldRedirectToLogin(api, error)) {
|
|
redirectToLogin(api);
|
|
return [];
|
|
}
|
|
list.innerHTML = renderApiState(getApiStateType(error), '读取加入申请失败');
|
|
setText('[data-my-join-applies-status]', error.message || '读取加入申请失败,请稍后重试', getApiStateType(error));
|
|
return [];
|
|
}
|
|
}
|
|
|
|
refresh.addEventListener('click', function () {
|
|
loadApplies();
|
|
});
|
|
list.addEventListener('click', async function (event) {
|
|
var button = event.target.closest('[data-join-cancel-id]');
|
|
var applyId;
|
|
var refreshed;
|
|
|
|
if (!button || !list.contains(button) || writePending) return;
|
|
applyId = normalizeId(button.getAttribute('data-join-cancel-id'));
|
|
if (!applyId) return;
|
|
if (!await confirmAction('确认撤销这条加入申请吗?')) return;
|
|
writePending = true;
|
|
button.disabled = true;
|
|
setText('[data-my-join-applies-status]', '正在撤销申请…', 'loading');
|
|
try {
|
|
await api.cancelGenealogyJoinApply(applyId);
|
|
refreshed = await loadApplies();
|
|
if (refreshed.some(function (item) { return item.applyId === applyId && item.status === '0'; })) {
|
|
throw new Error('撤销后申请仍处于待审核状态');
|
|
}
|
|
} catch (error) {
|
|
if (shouldRedirectToLogin(api, error)) {
|
|
redirectToLogin(api);
|
|
return;
|
|
}
|
|
setText('[data-my-join-applies-status]', error.message || '撤销加入申请失败,请稍后重试', getApiStateType(error));
|
|
} finally {
|
|
writePending = false;
|
|
}
|
|
});
|
|
await loadApplies();
|
|
}
|
|
|
|
async function init() {
|
|
if (root.document && root.document.querySelector('[data-join-apply-page]')) {
|
|
await initJoinApplyPage();
|
|
return;
|
|
}
|
|
if (root.document && root.document.querySelector('[data-my-join-applies-page]')) {
|
|
await initMyJoinAppliesPage();
|
|
}
|
|
}
|
|
|
|
return {
|
|
buildJoinApplyBody: buildJoinApplyBody,
|
|
validateJoinApplyBody: validateJoinApplyBody,
|
|
normalizeJoinGenealogy: normalizeJoinGenealogy,
|
|
normalizeJoinApply: normalizeJoinApply,
|
|
normalizeJoinApplies: normalizeJoinApplies,
|
|
renderJoinGenealogyOptions: renderJoinGenealogyOptions,
|
|
renderMyJoinApplies: renderMyJoinApplies,
|
|
canJoinGenealogy: canJoinGenealogy,
|
|
initJoinApplyPage: initJoinApplyPage,
|
|
initMyJoinAppliesPage: initMyJoinAppliesPage,
|
|
init: init
|
|
};
|
|
});
|