feat(api): 添加帮助中心反馈系统和VIP服务功能

- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、
  siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法
- 添加帮助文章和站点资讯的参数验证逻辑
- 更新测试文件添加新的API方法测试用例
- 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用
- 更新加入家谱页面为完整的申请流程界面
- 修改资讯详情页面为站点资讯展示页面
- 更新AxiosRequestUtil中认证处理逻辑
- 添加世系树渲染的HTML生成函数用于页面复用
- 更新文档中的API契约说明和页面规划
This commit is contained in:
fizzleaf
2026-07-30 16:04:48 +08:00
parent fb1743aa2a
commit 309598bfa6
53 changed files with 6740 additions and 282 deletions
+150
View File
@@ -0,0 +1,150 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.AppPromotionPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.AppPromotionPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
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 normalizeId(value) {
var normalized;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
normalized = String(value).trim();
return /^[1-9][0-9]*$/.test(normalized) ? normalized : '';
}
function text(value) {
return value === undefined || value === null ? '' : String(value).trim();
}
function normalizeTargetUrl(value) {
var normalized = text(value);
var parsed;
if (!normalized) return '';
try {
parsed = new URL(normalized);
} catch (error) {
return '';
}
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : '';
}
function normalizePromotion(item) {
var source = item || {};
var promotionId = normalizeId(source.promotionId);
var promotionTitle = text(source.promotionTitle);
if (!promotionId || !promotionTitle || String(source.status) !== '0') return null;
return {
promotionId: promotionId,
promotionTitle: promotionTitle,
promotionDesc: text(source.promotionDesc),
targetUrl: normalizeTargetUrl(source.targetUrl)
};
}
function normalizePromotionList(data) {
var items;
if (!Array.isArray(data)) return [];
items = data.map(normalizePromotion);
return items.some(function (item) { return !item; }) ? [] : items;
}
function renderPromotionCard(item) {
var content = '<div><h3>' + escapeHtml(item.promotionTitle) + '</h3>' +
'<p>' + escapeHtml(item.promotionDesc) + '</p></div>';
if (!item.targetUrl) {
return '<article class="promotion-card" data-promotion-id="' +
escapeHtml(item.promotionId) + '">' + content + '</article>';
}
return '<a class="promotion-card" data-promotion-id="' + escapeHtml(item.promotionId) +
'" href="' + escapeHtml(item.targetUrl) +
'" target="_blank" rel="noopener noreferrer">' + content + '</a>';
}
function renderPromotionList(data) {
var items = normalizePromotionList(data);
if (!items.length) return '<div class="api-empty">当前暂无应用推广</div>';
return items.map(renderPromotionCard).join('');
}
function renderPromotionLoginRequired() {
return '<div class="api-empty">登录后可查看应用推广,<a href="login.html">立即登录</a></div>';
}
async function loadPromotions(api) {
var data;
var items;
if (!api || typeof api.promotions !== 'function') throw new Error('应用推广接口不可用');
data = await api.promotions({ platform: 'pc' });
items = normalizePromotionList(data);
if (!Array.isArray(data) || items.length !== data.length) {
throw new Error('推广列表响应无效');
}
return items;
}
function isUnauthorized(error) {
return Number(error && (error.status || error.code)) === 401;
}
async function init() {
var page = root.document && root.document.querySelector('[data-promotion-page]');
var list = root.document && root.document.querySelector('[data-promotion-list]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
if (!page || !list) return;
if (!api || !api.getToken || !api.getToken()) {
list.innerHTML = renderPromotionLoginRequired();
return;
}
try {
list.classList.add('is-loading');
list.innerHTML = renderPromotionList(await loadPromotions(api));
} catch (error) {
if (isUnauthorized(error)) {
if (api.clearToken) api.clearToken();
list.innerHTML = renderPromotionLoginRequired();
return;
}
list.innerHTML = '<div class="api-empty">应用推广读取失败,请稍后重试</div>';
} finally {
list.classList.remove('is-loading');
}
}
return {
normalizePromotion: normalizePromotion,
normalizePromotionList: normalizePromotionList,
normalizeTargetUrl: normalizeTargetUrl,
renderPromotionList: renderPromotionList,
renderPromotionLoginRequired: renderPromotionLoginRequired,
loadPromotions: loadPromotions,
isUnauthorized: isUnauthorized,
init: init
};
});
+202
View File
@@ -0,0 +1,202 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.FamilyHomePages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.FamilyHomePages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
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 normalizeId(value) {
var result;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
result = String(value).trim();
return /^[1-9][0-9]*$/.test(result) ? result : '';
}
function text(value) {
if (value === undefined || value === null) return '';
return String(value).trim();
}
function normalizeCount(value) {
var result = Number(value);
return Number.isSafeInteger(result) && result >= 0 ? result : null;
}
function normalizeFamilyOverview(item, expectedGenealogyId) {
var source = item || {};
var genealogyId = normalizeId(source.genealogyId);
var expectedId = normalizeId(expectedGenealogyId);
var memberCount = normalizeCount(source.memberCount);
var personCount = normalizeCount(source.personCount);
if (!genealogyId || genealogyId !== expectedId ||
!text(source.genealogyName) || String(source.status) !== '0' ||
memberCount === null || personCount === null) {
return null;
}
return {
genealogyId: genealogyId,
genealogyNo: text(source.genealogyNo),
genealogyName: text(source.genealogyName),
surname: text(source.surname),
ancestralHall: text(source.ancestralHall),
originPlace: text(source.originPlace),
regionFullName: text(source.regionFullName),
memberCount: memberCount,
personCount: personCount,
canManage: source.canManage === true,
canEditContent: source.canEditContent === true
};
}
function renderFamilyOverview(overview) {
var source = overview || {};
var details = [
source.genealogyNo ? '家谱编号 ' + source.genealogyNo : '',
source.surname ? '姓氏 ' + source.surname : '',
source.ancestralHall ? '堂号 ' + source.ancestralHall : '',
source.originPlace ? '祖籍 ' + source.originPlace : '',
source.regionFullName ? '地区 ' + source.regionFullName : ''
].filter(Boolean).join(' · ');
return '<h3>' + escapeHtml(source.genealogyName) + '</h3><p>' +
escapeHtml(details) + '</p><p>成员 ' + escapeHtml(source.memberCount) +
' 人 · 世系人物 ' + escapeHtml(source.personCount) + ' 人</p>';
}
function shouldShowFamilyManagement(overview) {
return !!overview && overview.canManage === true;
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
async function loadFamilyHome(api, genealogyId) {
var normalizedId = normalizeId(genealogyId);
var results;
var overview;
if (!normalizedId || !api || !api.genealogyOverview || !api.lineageTree) {
throw new Error('家谱主页接口初始化失败');
}
results = await Promise.all([
api.genealogyOverview(normalizedId),
api.lineageTree(normalizedId)
]);
overview = normalizeFamilyOverview(results[0], normalizedId);
if (!overview) throw new Error('家谱概览响应无效');
return {
overview: overview,
tree: results[1]
};
}
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 query(selector) {
return root.document && root.document.querySelector(selector);
}
function queryAll(selector) {
return root.document ? Array.prototype.slice.call(root.document.querySelectorAll(selector)) : [];
}
function setText(selector, value) {
var element = query(selector);
if (element) element.textContent = value;
}
async function initFamilyHomePage() {
var page = query('[data-family-home-page]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var genealogyId;
var result;
if (!page) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId();
if (!genealogyId) {
if (root.location && typeof root.location.replace === 'function') {
root.location.replace('profile-families.html?next=profile-family-home.html');
}
return;
}
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
}
if (!root.LineagePages || !root.LineagePages.renderLineageTreeHtml) {
setText('[data-family-home-status]', '世系预览初始化失败,请刷新后重试');
return;
}
try {
result = await loadFamilyHome(api, genealogyId);
setText('[data-family-home-title]', result.overview.genealogyName);
query('[data-family-home-overview]').innerHTML = renderFamilyOverview(result.overview);
setText('[data-family-home-member-count]', String(result.overview.memberCount));
setText('[data-family-home-person-count]', String(result.overview.personCount));
queryAll('[data-family-home-management]').forEach(function (element) {
element.hidden = !shouldShowFamilyManagement(result.overview);
});
query('[data-lineage-home-tree]').innerHTML =
root.LineagePages.renderLineageTreeHtml(result.tree);
setText('[data-family-home-status]', '家谱概览已更新');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
query('[data-family-home-overview]').innerHTML =
'<div class="api-empty">家谱概览读取失败,请稍后重试</div>';
query('[data-lineage-home-tree]').innerHTML =
'<div class="api-empty">世系树读取失败,请稍后重试</div>';
setText('[data-family-home-status]', error.message || '家谱主页读取失败,请稍后重试');
}
}
function init() {
return initFamilyHomePage();
}
return {
normalizeFamilyOverview: normalizeFamilyOverview,
renderFamilyOverview: renderFamilyOverview,
shouldShowFamilyManagement: shouldShowFamilyManagement,
shouldRedirectToLogin: shouldRedirectToLogin,
loadFamilyHome: loadFamilyHome,
initFamilyHomePage: initFamilyHomePage,
init: init
};
});
+385
View File
@@ -0,0 +1,385 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.FeedbackPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.FeedbackPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var FEEDBACK_TYPES = {
advice: '意见建议',
bug: '问题反馈',
complaint: '投诉反馈',
other: '其他反馈'
};
var HANDLE_STATUS = {
'0': '待处理',
'1': '处理中',
'2': '已处理',
'3': '已关闭'
};
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 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) {
if (value === undefined || value === null) return '';
return String(value).trim();
}
function getFeedbackId(search) {
var params = new URLSearchParams(String(search === undefined && root.location ? root.location.search : search || '').replace(/^\?/, ''));
return normalizeId(params.get('feedbackId'));
}
function buildFeedbackBody(values) {
var source = values || {};
var body = {};
var feedbackType = optionalText(source.feedbackType);
var feedbackContent = optionalText(source.feedbackContent);
var contactInfo = optionalText(source.contactInfo);
if (feedbackType) body.feedbackType = feedbackType;
if (feedbackContent) body.feedbackContent = feedbackContent;
if (contactInfo) body.contactInfo = contactInfo;
return body;
}
function validateFeedbackBody(body) {
var source = body || {};
if (!optionalText(source.feedbackContent)) return '请填写反馈内容';
if (source.feedbackType && !Object.prototype.hasOwnProperty.call(FEEDBACK_TYPES, source.feedbackType)) {
return '反馈类型无效';
}
return '';
}
function normalizeFeedback(item) {
var source = item || {};
var feedbackId = normalizeId(source.feedbackId);
var feedbackType = optionalText(source.feedbackType);
var feedbackContent = optionalText(source.feedbackContent);
var handleStatus = String(source.handleStatus === undefined || source.handleStatus === null ? '' : source.handleStatus);
var status = String(source.status === undefined || source.status === null ? '' : source.status);
if (!feedbackId || !feedbackContent ||
!Object.prototype.hasOwnProperty.call(FEEDBACK_TYPES, feedbackType) ||
!Object.prototype.hasOwnProperty.call(HANDLE_STATUS, handleStatus) ||
['0', '1'].indexOf(status) < 0) {
return null;
}
return {
feedbackId: feedbackId,
feedbackType: feedbackType,
feedbackContent: feedbackContent,
contactInfo: optionalText(source.contactInfo),
handleStatus: handleStatus,
handleResult: optionalText(source.handleResult),
handleTime: optionalText(source.handleTime),
status: status,
remark: optionalText(source.remark)
};
}
function normalizeFeedbackList(data) {
var items;
if (!Array.isArray(data)) return [];
items = data.map(normalizeFeedback);
return items.some(function (item) { return !item; }) ? [] : items;
}
function findFeedbackById(data, feedbackId) {
var targetId = normalizeId(feedbackId);
var items = normalizeFeedbackList(data);
var match;
if (!targetId) return null;
match = items.find(function (item) { return item.feedbackId === targetId; });
return match || null;
}
function buildFeedbackDetailUrl(feedbackId, detailPage) {
var id = normalizeId(feedbackId);
var params;
if (!id) return '';
params = new URLSearchParams();
params.set('feedbackId', id);
return (detailPage || 'ticket-detail.html') + '?' + params.toString();
}
function renderFeedbackList(data, options) {
var settings = options || {};
var items = normalizeFeedbackList(data);
var detailPage = settings.detailPage || 'ticket-detail.html';
if (!items.length) return '<div class="api-empty">当前没有反馈记录</div>';
return items.map(function (item) {
return '<a class="module-row" href="' +
escapeHtml(buildFeedbackDetailUrl(item.feedbackId, detailPage)) +
'"><div><h3>' + escapeHtml(FEEDBACK_TYPES[item.feedbackType]) +
'</h3><p>' + escapeHtml(item.feedbackContent) +
'</p></div><span class="pill">' + HANDLE_STATUS[item.handleStatus] +
'</span></a>';
}).join('');
}
function renderFeedbackDetail(item) {
var feedback = normalizeFeedback(item);
var details;
if (!feedback) return '<div class="api-empty">反馈记录不存在或无权查看</div>';
details = [
feedback.contactInfo ? '<dt>提交联系方式</dt><dd>' + escapeHtml(feedback.contactInfo) + '</dd>' : '',
'<dt>处理状态</dt><dd>' + HANDLE_STATUS[feedback.handleStatus] + '</dd>',
feedback.handleResult ? '<dt>处理结果</dt><dd>' + escapeHtml(feedback.handleResult) + '</dd>' : '',
feedback.handleTime ? '<dt>处理时间</dt><dd>' + escapeHtml(feedback.handleTime) + '</dd>' : '',
feedback.remark ? '<dt>备注</dt><dd>' + escapeHtml(feedback.remark) + '</dd>' : ''
].filter(Boolean).join('');
return '<article class="module-panel"><div class="module-kicker">' +
escapeHtml(FEEDBACK_TYPES[feedback.feedbackType]) +
'</div><h2>问题说明</h2><p>' + escapeHtml(feedback.feedbackContent) +
'</p><dl class="detail-list">' + details + '</dl></article>';
}
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 setStatus(message) {
var status = root.document && root.document.querySelector('[data-feedback-status]');
if (status) status.textContent = message || '';
}
async function readMyFeedback(api) {
var data = await api.myFeedback();
var items = normalizeFeedbackList(data);
if (!Array.isArray(data) || items.length !== data.length) {
throw new Error('反馈列表响应无效');
}
return items;
}
async function initFeedbackFormPage() {
var form = root.document && root.document.querySelector('[data-feedback-form]');
var list = root.document && root.document.querySelector('[data-feedback-list]');
var refresh = root.document && root.document.querySelector('[data-feedback-refresh]');
var page = root.document && root.document.querySelector('[data-feedback-page]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var pageMode = page && page.getAttribute('data-feedback-page');
var writePending = false;
if (!form) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
async function loadList() {
var items;
if (!list) return [];
setStatus('正在读取反馈记录…');
try {
items = await readMyFeedback(api);
list.innerHTML = renderFeedbackList(items, { detailPage: 'ticket-detail.html' });
setStatus('');
return items;
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return [];
}
list.innerHTML = '<div class="api-empty">读取反馈记录失败</div>';
setStatus(error.message || '读取反馈记录失败,请稍后重试');
return [];
}
}
if (refresh) refresh.addEventListener('click', loadList);
form.addEventListener('submit', async function (event) {
var body;
var validation;
var created;
var refreshed;
var button = form.querySelector('[data-feedback-submit]');
event.preventDefault();
if (writePending) return;
body = buildFeedbackBody(getFormValues(form));
validation = validateFeedbackBody(body);
if (validation) {
setStatus(validation);
return;
}
writePending = true;
if (button) button.disabled = true;
setStatus('正在提交反馈…');
try {
created = normalizeFeedback(await api.submitFeedback(body));
if (!created) throw new Error('提交响应缺少有效反馈信息');
refreshed = await readMyFeedback(api);
if (!refreshed.some(function (item) { return item.feedbackId === created.feedbackId; })) {
throw new Error('提交后无法读取同一反馈记录');
}
if (pageMode === 'ticket') {
if (root.location) root.location.href = buildFeedbackDetailUrl(created.feedbackId);
return;
}
if (list) list.innerHTML = renderFeedbackList(refreshed, { detailPage: 'ticket-detail.html' });
form.reset();
setStatus('反馈已提交');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setStatus(error.message || '提交反馈失败,请稍后重试');
} finally {
writePending = false;
if (button) button.disabled = false;
}
});
await loadList();
}
async function initFeedbackListPage() {
var page = root.document && root.document.querySelector('[data-feedback-list-page]');
var list = root.document && root.document.querySelector('[data-feedback-list]');
var refresh = root.document && root.document.querySelector('[data-feedback-refresh]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
if (!page || !list) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
async function loadList() {
try {
setStatus('正在读取反馈记录…');
list.innerHTML = renderFeedbackList(await readMyFeedback(api), {
detailPage: 'ticket-detail.html'
});
setStatus('');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
list.innerHTML = '<div class="api-empty">读取反馈记录失败</div>';
setStatus(error.message || '读取反馈记录失败,请稍后重试');
}
}
if (refresh) refresh.addEventListener('click', loadList);
await loadList();
}
async function initFeedbackDetailPage() {
var page = root.document && root.document.querySelector('[data-feedback-detail-page]');
var detail = root.document && root.document.querySelector('[data-feedback-detail]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var feedbackId = getFeedbackId();
var item;
if (!page || !detail) return;
if (!feedbackId) {
detail.innerHTML = '<div class="api-empty">反馈记录不存在或无权查看</div>';
return;
}
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
try {
setStatus('正在读取反馈详情…');
item = findFeedbackById(await api.myFeedback(), feedbackId);
detail.innerHTML = renderFeedbackDetail(item);
setStatus(item ? '' : '反馈记录不存在或无权查看');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
detail.innerHTML = '<div class="api-empty">反馈详情读取失败</div>';
setStatus(error.message || '反馈详情读取失败,请稍后重试');
}
}
async function init() {
if (root.document && root.document.querySelector('[data-feedback-form]')) {
await initFeedbackFormPage();
return;
}
if (root.document && root.document.querySelector('[data-feedback-list-page]')) {
await initFeedbackListPage();
return;
}
if (root.document && root.document.querySelector('[data-feedback-detail-page]')) {
await initFeedbackDetailPage();
}
}
return {
getFeedbackId: getFeedbackId,
buildFeedbackBody: buildFeedbackBody,
validateFeedbackBody: validateFeedbackBody,
normalizeFeedback: normalizeFeedback,
normalizeFeedbackList: normalizeFeedbackList,
findFeedbackById: findFeedbackById,
buildFeedbackDetailUrl: buildFeedbackDetailUrl,
renderFeedbackList: renderFeedbackList,
renderFeedbackDetail: renderFeedbackDetail,
initFeedbackFormPage: initFeedbackFormPage,
initFeedbackListPage: initFeedbackListPage,
initFeedbackDetailPage: initFeedbackDetailPage,
init: init
};
});
+206
View File
@@ -0,0 +1,206 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.HelpPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.HelpPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
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 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 normalizeCount(value) {
var text;
if (value === undefined || value === null || value === '') return '0';
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value < 0)) return '';
text = String(value).trim();
return /^(?:0|[1-9][0-9]*)$/.test(text) ? text : '';
}
function text(value) {
return value === undefined || value === null ? '' : String(value).trim();
}
function normalizeHelpArticle(item) {
var source = item || {};
var helpId = normalizeId(source.helpId);
var helpTitle = text(source.helpTitle);
var helpContent = text(source.helpContent);
var viewCount = normalizeCount(source.viewCount);
if (!helpId || !helpTitle || !helpContent || !viewCount || String(source.status) !== '0') {
return null;
}
return {
helpId: helpId,
helpCategory: text(source.helpCategory),
helpTitle: helpTitle,
helpContent: helpContent,
viewCount: viewCount
};
}
function normalizeHelpArticleList(data) {
var items;
if (!Array.isArray(data)) return [];
items = data.map(normalizeHelpArticle);
return items.some(function (item) { return !item; }) ? [] : items;
}
function renderHelpArticleList(data) {
var items = normalizeHelpArticleList(data);
if (!items.length) return '<div class="api-empty">当前暂无帮助文章</div>';
return items.map(function (item) {
var meta = [item.helpCategory, '浏览 ' + item.viewCount + ' 次'].filter(Boolean).join(' · ');
return '<details class="tilt-card" data-help-article-id="' + escapeHtml(item.helpId) + '">' +
'<summary>' + escapeHtml(item.helpTitle) + '</summary>' +
'<p>' + escapeHtml(meta) + '</p>' +
'<div class="help-detail" data-help-detail-panel="' + escapeHtml(item.helpId) + '">' +
'<button class="btn ghost" type="button" data-help-detail-id="' +
escapeHtml(item.helpId) + '">查看完整解答</button></div></details>';
}).join('');
}
function renderHelpArticleDetail(item) {
var article = normalizeHelpArticle(item);
var meta;
if (!article) return '<div class="api-empty">帮助文章不存在或已停用</div>';
meta = [article.helpCategory, '浏览 ' + article.viewCount + ' 次'].filter(Boolean).join(' · ');
return '<div class="help-detail-content"><p class="module-meta">' + escapeHtml(meta) +
'</p><p>' + escapeHtml(article.helpContent).replace(/\r?\n/g, '<br />') + '</p></div>';
}
async function loadHelpArticleDetail(api, helpId) {
var targetId = normalizeId(helpId);
var article;
if (!api || typeof api.helpArticleDetail !== 'function') throw new Error('帮助文章接口不可用');
if (!targetId) throw new Error('帮助文章编号无效');
article = normalizeHelpArticle(await api.helpArticleDetail(targetId));
if (!article) throw new Error('帮助文章详情响应无效');
if (article.helpId !== targetId) throw new Error('详情响应与请求编号不一致');
return article;
}
async function loadHelpArticles(api) {
var data;
var items;
if (!api || typeof api.helpArticles !== 'function') throw new Error('帮助文章接口不可用');
data = await api.helpArticles();
items = normalizeHelpArticleList(data);
if (!Array.isArray(data) || items.length !== data.length) throw new Error('帮助文章列表响应无效');
return items;
}
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';
}
async function init() {
var list = root.document && root.document.querySelector('[data-help-list]');
var status = root.document && root.document.querySelector('[data-help-status]');
var refresh = root.document && root.document.querySelector('[data-help-refresh]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
if (!list) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
function setStatus(message) {
if (status) status.textContent = message || '';
}
async function refreshList() {
try {
list.classList.add('is-loading');
setStatus('正在读取帮助文章…');
list.innerHTML = renderHelpArticleList(await loadHelpArticles(api));
setStatus('');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
list.innerHTML = '<div class="api-empty">帮助文章读取失败,请稍后重试</div>';
setStatus(error.message || '帮助文章读取失败');
} finally {
list.classList.remove('is-loading');
}
}
list.addEventListener('click', async function (event) {
var button = event.target.closest('[data-help-detail-id]');
var helpId;
var panel;
if (!button || button.disabled) return;
helpId = button.getAttribute('data-help-detail-id');
panel = list.querySelector('[data-help-detail-panel="' + helpId + '"]');
if (!panel) return;
button.disabled = true;
panel.textContent = '正在读取完整解答…';
try {
panel.innerHTML = renderHelpArticleDetail(await loadHelpArticleDetail(api, helpId));
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
panel.textContent = error.message || '帮助文章详情读取失败';
}
});
if (refresh) refresh.addEventListener('click', refreshList);
await refreshList();
}
return {
normalizeHelpArticle: normalizeHelpArticle,
normalizeHelpArticleList: normalizeHelpArticleList,
renderHelpArticleList: renderHelpArticleList,
renderHelpArticleDetail: renderHelpArticleDetail,
loadHelpArticleDetail: loadHelpArticleDetail,
loadHelpArticles: loadHelpArticles,
shouldRedirectToLogin: shouldRedirectToLogin,
init: init
};
});
+407
View File
@@ -0,0 +1,407 @@
(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';
var APPLY_STATUS = {
'0': '待审核',
'1': '已通过',
'2': '已拒绝',
'3': '已撤销'
};
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 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 '<div class="api-empty">家谱列表数据无效,请刷新重试</div>';
items = data.map(normalizeJoinGenealogy).filter(Boolean);
if (!items.length) return '<div class="api-empty">没有找到可申请加入的家谱</div>';
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 '<div class="api-empty">当前没有加入申请</div>';
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 setText(selector, message) {
var element = root.document && root.document.querySelector(selector);
if (element) element.textContent = message || '';
}
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;
}
async function loadOptions(keyword) {
var data;
setText('[data-join-apply-status]', '正在读取可申请家谱…');
try {
data = await api.genealogyOptions(keyword ? { keyword: keyword } : {});
options.innerHTML = renderJoinGenealogyOptions(data);
setText('[data-join-apply-status]', '');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
options.innerHTML = '<div class="api-empty">读取家谱列表失败</div>';
setText('[data-join-apply-status]', error.message || '读取家谱列表失败,请稍后重试');
}
}
try {
if (!canJoinGenealogy(await api.genealogyQuota())) {
setText('[data-join-apply-status]', '当前没有可用的家谱加入额度');
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 || '无法读取家谱加入额度');
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]', '请先从家谱列表中选择要加入的家谱');
return;
}
body = buildJoinApplyBody(getFormValues(applyForm));
validation = validateJoinApplyBody(body);
if (validation) {
setText('[data-join-apply-status]', validation);
return;
}
writePending = true;
applyForm.querySelector('[data-join-apply-submit]').disabled = true;
setText('[data-join-apply-status]', '正在提交申请…');
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 || '提交加入申请失败,请稍后重试');
} finally {
writePending = false;
applyForm.querySelector('[data-join-apply-submit]').disabled = 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]', '正在读取申请记录…');
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 = '<div class="api-empty">读取加入申请失败</div>';
setText('[data-my-join-applies-status]', error.message || '读取加入申请失败,请稍后重试');
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 (root.confirm && !root.confirm('确认撤销这条加入申请吗?')) return;
writePending = true;
button.disabled = true;
setText('[data-my-join-applies-status]', '正在撤销申请…');
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 || '撤销加入申请失败,请稍后重试');
} 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
};
});
+250
View File
@@ -0,0 +1,250 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.JoinReviewPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.JoinReviewPages.initJoinReviewPage();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
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 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) {
if (value === undefined || value === null) return '';
return String(value).trim();
}
function getCurrentGenealogyId(search) {
var contextId;
var params;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
contextId = root.ProfileUI.getGenealogyId();
return normalizeId(contextId);
}
params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return normalizeId(params.get('genealogyId'));
}
function buildJoinAuditBody(values) {
var source = values || {};
var body = { status: String(source.status === undefined || source.status === null ? '' : source.status).trim() };
var auditRemark = optionalText(source.auditRemark);
if (auditRemark) body.auditRemark = auditRemark;
return body;
}
function validateJoinAuditBody(body) {
if (!body || ['1', '2'].indexOf(body.status) < 0) return '审核状态无效';
if (String(body.auditRemark || '').length > 500) return '审核说明不能超过 500 个字符';
return '';
}
function normalizePendingApply(item) {
var source = item || {};
var applyId = normalizeId(source.applyId);
var genealogyId = normalizeId(source.genealogyId);
if (!applyId || !genealogyId || String(source.status) !== '0') return null;
return {
applyId: applyId,
genealogyId: genealogyId,
genealogyName: optionalText(source.genealogyName),
applicantName: optionalText(source.applicantName) || optionalText(source.appUserNickName) || '未填写姓名',
phone: optionalText(source.phone),
relationDesc: optionalText(source.relationDesc),
applyReason: optionalText(source.applyReason)
};
}
function normalizePendingApplies(data) {
var items;
if (!Array.isArray(data)) return [];
items = data.map(normalizePendingApply);
return items.some(function (item) { return !item; }) ? [] : items;
}
function renderPendingJoinApplies(data, canManage) {
var items = normalizePendingApplies(data);
if (!items.length) return '<div class="api-empty">当前没有待审核申请</div>';
return items.map(function (item) {
var details = [
item.phone ? '申请联系电话:' + item.phone : '',
item.relationDesc ? '关系:' + item.relationDesc : '',
item.applyReason ? '理由:' + item.applyReason : ''
].filter(Boolean).join(' · ');
var actions = canManage
? '<div class="bottom-actions"><button class="btn primary" type="button" data-join-audit-id="' +
escapeHtml(item.applyId) + '" data-join-audit-status="1">通过</button>' +
'<button class="btn ghost" type="button" data-join-audit-id="' +
escapeHtml(item.applyId) + '" data-join-audit-status="2">拒绝</button></div>'
: '';
return '<article class="module-row"><div><h3>' + escapeHtml(item.applicantName) +
'申请加入' + escapeHtml(item.genealogyName || '当前家谱') + '</h3><p>' +
escapeHtml(details || '申请人未补充说明') + '</p></div>' + actions + '</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 setStatus(message) {
var element = root.document && root.document.querySelector('[data-join-review-status]');
if (element) element.textContent = message || '';
}
async function initJoinReviewPage() {
var page = root.document && root.document.querySelector('[data-join-review-page]');
var list = root.document && root.document.querySelector('[data-join-apply-list="pending"]');
var refresh = root.document && root.document.querySelector('[data-join-review-refresh]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var genealogyId = getCurrentGenealogyId();
var canManage = false;
var writePending = false;
if (!page) return;
if (!genealogyId) {
setStatus('请先从“我的家谱”选择要管理的家谱');
list.innerHTML = '<a class="module-row" href="profile-families.html?next=profile-join-review.html">选择家谱后继续</a>';
return;
}
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
}
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
async function loadPending() {
var data;
setStatus('正在读取待审核申请…');
try {
data = await api.pendingGenealogyJoinApplies(genealogyId);
list.innerHTML = renderPendingJoinApplies(data, canManage);
setStatus(canManage ? '' : '当前账号没有审核该家谱申请的权限');
return normalizePendingApplies(data);
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return [];
}
list.innerHTML = '<div class="api-empty">读取待审核申请失败</div>';
setStatus(error.message || '读取待审核申请失败,请稍后重试');
return [];
}
}
try {
var detail = await api.genealogyDetail(genealogyId);
canManage = detail && detail.canManage === true;
if (!canManage) {
list.innerHTML = '<div class="api-empty">当前账号没有审核该家谱申请的权限</div>';
setStatus('当前账号没有审核该家谱申请的权限');
return;
}
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setStatus(error.message || '无法核验当前家谱管理权限');
return;
}
refresh.addEventListener('click', function () {
loadPending();
});
list.addEventListener('click', async function (event) {
var button = event.target.closest('[data-join-audit-id]');
var applyId;
var status;
var auditRemark = '';
var body;
var validation;
var refreshed;
if (!button || !list.contains(button) || writePending || !canManage) return;
applyId = normalizeId(button.getAttribute('data-join-audit-id'));
status = button.getAttribute('data-join-audit-status');
if (!applyId || ['1', '2'].indexOf(status) < 0) return;
if (status === '1' && root.confirm && !root.confirm('确认通过这条加入申请吗?')) return;
if (status === '2' && root.prompt) {
auditRemark = root.prompt('可填写拒绝原因(最多 500 个字符)', '') || '';
}
body = buildJoinAuditBody({ status: status, auditRemark: auditRemark });
validation = validateJoinAuditBody(body);
if (validation) {
setStatus(validation);
return;
}
writePending = true;
button.disabled = true;
setStatus('正在提交审核结果…');
try {
await api.auditGenealogyJoinApply(genealogyId, applyId, body);
refreshed = await loadPending();
if (refreshed.some(function (item) { return item.applyId === applyId; })) {
throw new Error('审核后申请仍在待审核列表');
}
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setStatus(error.message || '提交审核结果失败,请稍后重试');
} finally {
writePending = false;
}
});
await loadPending();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
buildJoinAuditBody: buildJoinAuditBody,
validateJoinAuditBody: validateJoinAuditBody,
normalizePendingApply: normalizePendingApply,
renderPendingJoinApplies: renderPendingJoinApplies,
initJoinReviewPage: initJoinReviewPage
};
});
+10 -6
View File
@@ -341,16 +341,19 @@
'<strong>' + escapeHtml(person.name) + '</strong><span>' + escapeHtml(getPersonSummary(person)) + '</span></button>' + branches + '</li>';
}
function renderTree(data) {
var container = query('[data-lineage-tree]');
function renderLineageTreeHtml(data) {
var nodes = normalizeList(data).map(function (item) { return renderTreeNode(item, {}); }).filter(Boolean);
if (!container) return;
if (!nodes.length) {
container.innerHTML = '<div class="api-empty">暂无世系树</div>';
return;
return '<div class="api-empty">暂无世系树</div>';
}
container.innerHTML = '<ul class="lineage-tree">' + nodes.join('') + '</ul>';
return '<ul class="lineage-tree">' + nodes.join('') + '</ul>';
}
function renderTree(data) {
var container = query('[data-lineage-tree]');
if (container) container.innerHTML = renderLineageTreeHtml(data);
}
function renderOptions(data) {
@@ -962,6 +965,7 @@
normalizeLineageBinding: normalizeLineageBinding,
buildLineagePageQuery: buildLineagePageQuery,
normalizeList: normalizeList,
renderLineageTreeHtml: renderLineageTreeHtml,
relationLabel: relationLabel,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
+252
View File
@@ -0,0 +1,252 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.SiteNewsPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.SiteNewsPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var ARTICLE_TYPE_LABELS = {
news: '新闻',
notice: '公告',
download: '下载'
};
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 text(value) {
return value === undefined || value === null ? '' : String(value).trim();
}
function normalizeId(value) {
var normalized;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
normalized = String(value).trim();
return /^[1-9][0-9]*$/.test(normalized) ? normalized : '';
}
function normalizeArticleType(value) {
var normalized = text(value);
return Object.prototype.hasOwnProperty.call(ARTICLE_TYPE_LABELS, normalized)
? normalized
: '';
}
function normalizeExternalUrl(value) {
var normalized = text(value);
var parsed;
if (!normalized) return '';
try {
parsed = new URL(normalized);
} catch (error) {
return '';
}
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : '';
}
function normalizeArticle(item) {
var source = item || {};
var articleId = normalizeId(source.articleId);
var articleType = normalizeArticleType(source.articleType);
var articleTitle = text(source.articleTitle);
if (!articleId || !articleType || !articleTitle || String(source.status) !== '0') {
return null;
}
return {
articleId: articleId,
articleType: articleType,
articleTitle: articleTitle,
articleSummary: text(source.articleSummary),
articleContent: text(source.articleContent),
externalUrl: normalizeExternalUrl(source.externalUrl),
publishTime: text(source.publishTime)
};
}
function normalizeArticleList(data) {
var items;
if (!Array.isArray(data)) return [];
items = data.map(normalizeArticle);
return items.some(function (item) { return !item; }) ? [] : items;
}
function renderNormalizedArticleList(items) {
if (!items.length) return '<div class="api-empty">当前暂无资讯</div>';
return items.map(function (item) {
return '<a class="site-info-item" href="article-detail.html?articleId=' +
encodeURIComponent(item.articleId) + '">' +
'<small>' + escapeHtml(ARTICLE_TYPE_LABELS[item.articleType]) + '</small>' +
'<h2>' + escapeHtml(item.articleTitle) + '</h2>' +
'<p>' + escapeHtml(item.articleSummary) + '</p>' +
'<span>' + escapeHtml(item.publishTime) + '</span></a>';
}).join('');
}
function renderArticleList(data) {
return renderNormalizedArticleList(normalizeArticleList(data));
}
function renderNormalizedArticleDetail(item) {
var externalLink = item.externalUrl
? '<a class="btn ghost" data-external-link href="' + escapeHtml(item.externalUrl) +
'" target="_blank" rel="noopener noreferrer">查看外部内容</a>'
: '';
var content = escapeHtml(item.articleContent).replace(/\r?\n/g, '<br />');
return '<section class="inner-hero article-hero"><div class="container">' +
'<div class="hero-copy"><div class="eyebrow">' +
escapeHtml(ARTICLE_TYPE_LABELS[item.articleType]) + '</div><h1>' +
escapeHtml(item.articleTitle) + '</h1><p class="lead">' +
escapeHtml(item.articleSummary) + '</p><div class="article-meta"><span>' +
escapeHtml(item.publishTime) + '</span></div></div>' +
'<div class="hero-card article-cover-detail tilt-card"><span>闻</span><p>' +
escapeHtml(item.articleSummary) + '</p></div></div></section>' +
'<section class="section alt"><div class="container article-layout">' +
'<article class="article-content tilt-card"><p>' + content + '</p>' +
externalLink + '</article><aside class="article-side tilt-card"><h3>相关入口</h3>' +
'<a href="news.html">返回新闻资讯</a><a href="genealogy.html">了解数字家谱</a>' +
'<a href="create-genealogy.html">创建一本家谱</a></aside></div></section>';
}
function renderArticleDetail(item) {
var article = normalizeArticle(item);
return article
? renderNormalizedArticleDetail(article)
: '<div class="api-empty">资讯不存在或已下线</div>';
}
function readQueryValue(search, key) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return text(params.get(key));
}
function readArticleId(search) {
return normalizeId(readQueryValue(search, 'articleId'));
}
async function loadArticles(api, articleType) {
var requestedType = text(articleType);
var normalizedType = normalizeArticleType(requestedType);
var query = { limit: 100 };
var data;
var items;
if (!api || typeof api.siteArticles !== 'function') throw new Error('资讯接口不可用');
if (requestedType && !normalizedType) throw new Error('资讯分类无效');
if (normalizedType) query.articleType = normalizedType;
data = await api.siteArticles(query);
items = normalizeArticleList(data);
if (!Array.isArray(data) || items.length !== data.length) {
throw new Error('资讯列表响应无效');
}
return items;
}
async function loadArticleDetail(api, targetArticleId) {
var normalizedId = normalizeId(targetArticleId);
var items;
var match;
if (!normalizedId) throw new Error('资讯编号无效');
items = await loadArticles(api, '');
match = items.find(function (item) {
return item.articleId === normalizedId;
});
if (!match) throw new Error('资讯不存在或已下线');
return match;
}
async function init() {
var document = root.document;
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var search = root.location && root.location.search || '';
var listPage = document && document.querySelector('[data-site-news-page]');
var detailPage = document && document.querySelector('[data-site-article-page]');
var list;
var status;
var requestedType;
var detail;
var articleId;
if (listPage) {
list = document.querySelector('[data-site-news-list]');
status = document.querySelector('[data-site-news-status]');
requestedType = readQueryValue(search, 'articleType');
if (!list) return;
if (requestedType && !normalizeArticleType(requestedType)) {
list.innerHTML = '<div class="api-empty">资讯分类无效</div>';
if (status) status.textContent = '资讯分类无效';
return;
}
try {
list.classList.add('is-loading');
list.innerHTML = renderNormalizedArticleList(await loadArticles(api, requestedType));
if (status) status.textContent = '';
} catch (error) {
list.innerHTML = '<div class="api-empty">资讯读取失败,请稍后重试</div>';
if (status) status.textContent = error.message || '资讯读取失败';
} finally {
list.classList.remove('is-loading');
}
return;
}
if (detailPage) {
detail = document.querySelector('[data-site-article-detail]');
status = document.querySelector('[data-site-article-status]');
articleId = readArticleId(search);
if (!detail) return;
if (!articleId) {
detail.innerHTML = '<div class="api-empty">资讯编号无效</div>';
if (status) status.textContent = '资讯编号无效';
return;
}
try {
detail.classList.add('is-loading');
detail.innerHTML = renderNormalizedArticleDetail(await loadArticleDetail(api, articleId));
if (status) status.textContent = '';
} catch (error) {
detail.innerHTML = '<div class="api-empty">' +
escapeHtml(error.message || '资讯读取失败,请稍后重试') + '</div>';
if (status) status.textContent = error.message || '资讯读取失败';
} finally {
detail.classList.remove('is-loading');
}
}
}
return {
normalizeArticle: normalizeArticle,
normalizeArticleList: normalizeArticleList,
normalizeArticleType: normalizeArticleType,
normalizeExternalUrl: normalizeExternalUrl,
readArticleId: readArticleId,
renderArticleList: renderArticleList,
renderArticleDetail: renderArticleDetail,
loadArticles: loadArticles,
loadArticleDetail: loadArticleDetail,
init: init
};
});
+414
View File
@@ -0,0 +1,414 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.VipPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.VipPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var PACKAGE_TYPES = {
vip: '会员套餐',
storage: '存储扩容'
};
var DURATION_UNITS = {
permanent: '永久',
day: '天',
month: '个月',
year: '年'
};
var PAY_STATUS = {
'0': '待支付',
'1': '已支付',
'2': '已关闭',
'3': '已退款'
};
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 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) {
if (value === undefined || value === null) return '';
return String(value).trim();
}
function moneyText(value, optional) {
var text;
if ((value === undefined || value === null || value === '') && optional) return '';
if (typeof value === 'number' && (!Number.isFinite(value) || value < 0)) return null;
text = String(value === undefined || value === null ? '' : value).trim();
return /^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(text) ? text : null;
}
function validLimit(value) {
return value === undefined || value === null ||
(Number.isSafeInteger(Number(value)) && Number(value) >= 0);
}
function normalizeVipPackage(item) {
var source = item || {};
var packageId = normalizeId(source.packageId);
var packageName = optionalText(source.packageName);
var packageType = optionalText(source.packageType);
var durationUnit = optionalText(source.durationUnit);
var price = moneyText(source.price, false);
var originalPrice = moneyText(source.originalPrice, true);
var status = String(source.status === undefined || source.status === null ? '' : source.status);
if (!packageId || !packageName ||
!Object.prototype.hasOwnProperty.call(PACKAGE_TYPES, packageType) ||
!Object.prototype.hasOwnProperty.call(DURATION_UNITS, durationUnit) ||
price === null || originalPrice === null || status !== '0' ||
!validLimit(source.durationValue) || !validLimit(source.genealogyLimit) ||
!validLimit(source.memberLimit) || !validLimit(source.storageLimitMb)) {
return null;
}
return {
packageId: packageId,
packageName: packageName,
packageType: packageType,
packageDesc: optionalText(source.packageDesc),
price: price,
originalPrice: originalPrice,
durationValue: source.durationValue === undefined || source.durationValue === null ? null : Number(source.durationValue),
durationUnit: durationUnit,
genealogyLimit: source.genealogyLimit === undefined || source.genealogyLimit === null ? null : Number(source.genealogyLimit),
memberLimit: source.memberLimit === undefined || source.memberLimit === null ? null : Number(source.memberLimit),
storageLimitMb: source.storageLimitMb === undefined || source.storageLimitMb === null ? null : Number(source.storageLimitMb),
remark: optionalText(source.remark)
};
}
function normalizeVipPackages(data) {
if (!Array.isArray(data)) return [];
return data.map(normalizeVipPackage).filter(Boolean);
}
function normalizeGenealogyOption(item) {
var source = item || {};
var genealogyId = normalizeId(source.genealogyId);
var genealogyName = optionalText(source.genealogyName);
if (!genealogyId || !genealogyName) return null;
return {
genealogyId: genealogyId,
genealogyName: genealogyName,
surname: optionalText(source.surname)
};
}
function buildVipOrderBody(values) {
var source = values || {};
var body = {};
var packageId = normalizeId(source.packageId);
var genealogyId = normalizeId(source.genealogyId);
var payType = optionalText(source.payType);
if (packageId) body.packageId = packageId;
else if (source.packageId !== undefined) body.invalidPackageId = true;
if (genealogyId) body.genealogyId = genealogyId;
else if (source.genealogyId) body.invalidGenealogyId = true;
if (payType) body.payType = payType;
return body;
}
function validateVipOrderBody(body) {
if (!body || !normalizeId(body.packageId) || body.invalidPackageId) return '请选择有效会员套餐';
if (body.invalidGenealogyId ||
(body.genealogyId !== undefined && body.genealogyId !== null &&
String(body.genealogyId).trim() !== '' && !normalizeId(body.genealogyId))) {
return '家谱选项无效';
}
if (body.payType && body.payType !== 'wechat') return '支付方式无效';
return '';
}
function normalizeVipOrder(item) {
var source = item || {};
var orderId = normalizeId(source.orderId);
var packageId = normalizeId(source.packageId);
var genealogyId = source.genealogyId === undefined || source.genealogyId === null
? ''
: normalizeId(source.genealogyId);
var orderAmount = moneyText(source.orderAmount, false);
var payAmount = moneyText(source.payAmount, false);
var payStatus = String(source.payStatus === undefined || source.payStatus === null ? '' : source.payStatus);
var status = String(source.status === undefined || source.status === null ? '' : source.status);
if (!orderId || !packageId || !optionalText(source.orderNo) ||
!optionalText(source.packageName) ||
(source.genealogyId !== undefined && source.genealogyId !== null && !genealogyId) ||
orderAmount === null || payAmount === null ||
!Object.prototype.hasOwnProperty.call(PAY_STATUS, payStatus) ||
['0', '1'].indexOf(status) < 0) {
return null;
}
return {
orderId: orderId,
orderNo: optionalText(source.orderNo),
packageId: packageId,
packageName: optionalText(source.packageName),
genealogyId: genealogyId,
genealogyNo: optionalText(source.genealogyNo),
genealogyName: optionalText(source.genealogyName),
orderAmount: orderAmount,
payAmount: payAmount,
payType: optionalText(source.payType),
payStatus: payStatus,
payTime: optionalText(source.payTime),
expireTime: optionalText(source.expireTime),
status: status,
remark: optionalText(source.remark)
};
}
function normalizeVipOrders(data) {
if (!Array.isArray(data)) return [];
return data.map(normalizeVipOrder).filter(Boolean);
}
function renderVipPackages(data, selectedPackageId) {
var items = Array.isArray(data) ? data : [];
var selectedId = normalizeId(selectedPackageId);
if (!items.length) return '<div class="api-empty">当前没有可用会员套餐</div>';
return items.map(function (item) {
var duration = item.durationUnit === 'permanent'
? DURATION_UNITS[item.durationUnit]
: (item.durationValue === null ? '' : item.durationValue) + DURATION_UNITS[item.durationUnit];
var limits = [
item.genealogyLimit === null ? '' : '家谱 ' + item.genealogyLimit + ' 部',
item.memberLimit === null ? '' : '成员 ' + item.memberLimit + ' 人',
item.storageLimitMb === null ? '' : '存储 ' + item.storageLimitMb + ' MB'
].filter(Boolean).join(' · ');
return '<button class="module-card' + (item.packageId === selectedId ? ' is-selected' : '') +
'" type="button" data-vip-package-id="' + escapeHtml(item.packageId) +
'" data-vip-package-name="' + escapeHtml(item.packageName) + '"><span class="icon">会</span><h3>' +
escapeHtml(item.packageName) + '</h3><p>' +
escapeHtml([PACKAGE_TYPES[item.packageType], item.packageDesc, duration, limits].filter(Boolean).join(' · ')) +
'</p><strong>¥' + escapeHtml(item.price) + '</strong></button>';
}).join('');
}
function renderGenealogyOptions(data) {
var items = Array.isArray(data) ? data.map(normalizeGenealogyOption).filter(Boolean) : [];
return '<option value="">不关联具体家谱</option>' + items.map(function (item) {
return '<option value="' + escapeHtml(item.genealogyId) + '">' +
escapeHtml(item.genealogyName + (item.surname ? ' · ' + item.surname + '氏' : '')) +
'</option>';
}).join('');
}
function renderVipOrders(data) {
var items = normalizeVipOrders(data);
if (!items.length) return '<div class="api-empty">当前没有会员订单</div>';
return items.map(function (item) {
var details = [
item.orderNo,
item.genealogyName,
'订单金额 ¥' + item.orderAmount,
'应付 ¥' + item.payAmount,
item.payType ? '支付方式 ' + item.payType : '',
item.payTime ? '支付时间 ' + item.payTime : '',
item.expireTime ? '有效期至 ' + item.expireTime : '',
item.remark
].filter(Boolean).join(' · ');
return '<article class="module-row"><div><h3>' + escapeHtml(item.packageName) +
'</h3><p>' + escapeHtml(details) + '</p></div><span class="pill">' +
PAY_STATUS[item.payStatus] + '</span></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';
}
async function init() {
var page = root.document && root.document.querySelector('[data-vip-page]');
var packageList = root.document && root.document.querySelector('[data-vip-package-list]');
var selectedText = root.document && root.document.querySelector('[data-vip-selected-package]');
var genealogySelect = root.document && root.document.querySelector('[data-vip-genealogy-options]');
var form = root.document && root.document.querySelector('[data-vip-order-form]');
var submit = root.document && root.document.querySelector('[data-vip-order-submit]');
var status = root.document && root.document.querySelector('[data-vip-order-status]');
var refresh = root.document && root.document.querySelector('[data-vip-order-refresh]');
var orderList = root.document && root.document.querySelector('[data-vip-order-list]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var packages = [];
var selectedPackageId = '';
var writePending = false;
if (!page) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
if (!packageList || !selectedText || !genealogySelect || !form || !orderList ||
!api.vipPackages || !api.genealogiesMine || !api.vipOrders || !api.createVipOrder) {
if (status) status.textContent = '会员服务接口初始化失败,请刷新后重试';
return;
}
function setStatus(message) {
if (status) status.textContent = message;
}
function renderPackages() {
packageList.innerHTML = renderVipPackages(packages, selectedPackageId);
}
function renderOrders(orders) {
orderList.innerHTML = renderVipOrders(orders);
}
async function loadOrders() {
var orders = normalizeVipOrders(await api.vipOrders());
renderOrders(orders);
return orders;
}
try {
var initial = await Promise.all([api.vipPackages(), api.genealogiesMine(), api.vipOrders()]);
packages = normalizeVipPackages(initial[0]);
renderPackages();
genealogySelect.innerHTML = renderGenealogyOptions(initial[1]);
renderOrders(initial[2]);
setStatus(packages.length ? '请选择会员套餐' : '当前没有可用会员套餐');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
packageList.innerHTML = '<div class="api-empty">会员套餐读取失败,请稍后重试</div>';
orderList.innerHTML = '<div class="api-empty">会员订单读取失败,请稍后重试</div>';
setStatus(error.message || '会员服务读取失败,请稍后重试');
return;
}
packageList.addEventListener('click', function (event) {
var button = event.target.closest('[data-vip-package-id]');
var packageId;
var selected;
if (!button || !packageList.contains(button)) return;
packageId = normalizeId(button.getAttribute('data-vip-package-id'));
selected = packages.find(function (item) { return item.packageId === packageId; });
if (!selected) return;
selectedPackageId = selected.packageId;
selectedText.textContent = '已选择:' + selected.packageName + '(¥' + selected.price + '';
renderPackages();
setStatus('可以创建会员订单');
});
if (refresh) {
refresh.addEventListener('click', async function () {
if (writePending) return;
refresh.disabled = true;
setStatus('正在刷新订单…');
try {
await loadOrders();
setStatus('会员订单已刷新');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setStatus(error.message || '会员订单刷新失败,请稍后重试');
} finally {
refresh.disabled = false;
}
});
}
form.addEventListener('submit', async function (event) {
var body;
var validation;
var created;
var refreshed;
var verified;
event.preventDefault();
if (writePending) return;
body = buildVipOrderBody({
packageId: selectedPackageId,
genealogyId: genealogySelect.value
});
validation = validateVipOrderBody(body);
if (validation) {
setStatus(validation);
return;
}
writePending = true;
if (submit) submit.disabled = true;
setStatus('正在创建会员订单…');
try {
created = normalizeVipOrder(await api.createVipOrder(body));
if (!created) throw new Error('创建响应缺少有效会员订单信息');
refreshed = normalizeVipOrders(await api.vipOrders());
verified = refreshed.find(function (item) { return item.orderId === created.orderId; });
if (!verified) throw new Error('订单已提交,但重新读取未找到对应记录');
renderOrders(refreshed);
setStatus('订单已创建;当前 PC 暂未开放在线支付');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setStatus(error.message || '会员订单创建失败,请稍后重试');
} finally {
writePending = false;
if (submit) submit.disabled = false;
}
});
}
return {
normalizeVipPackage: normalizeVipPackage,
normalizeVipPackages: normalizeVipPackages,
normalizeGenealogyOption: normalizeGenealogyOption,
buildVipOrderBody: buildVipOrderBody,
validateVipOrderBody: validateVipOrderBody,
normalizeVipOrder: normalizeVipOrder,
normalizeVipOrders: normalizeVipOrders,
renderVipPackages: renderVipPackages,
renderGenealogyOptions: renderGenealogyOptions,
renderVipOrders: renderVipOrders,
init: init
};
});