feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
This commit is contained in:
@@ -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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user