feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
This commit is contained in:
@@ -183,10 +183,13 @@
|
||||
}
|
||||
|
||||
.promotion-card {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #fffaf0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 18px 42px rgba(57, 48, 36, .08);
|
||||
transition: transform .24s ease, border-color .24s ease, box-shadow .24s ease;
|
||||
}
|
||||
|
||||
@@ -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, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user