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

- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、
  siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法
- 添加帮助文章和站点资讯的参数验证逻辑
- 更新测试文件添加新的API方法测试用例
- 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用
- 更新加入家谱页面为完整的申请流程界面
- 修改资讯详情页面为站点资讯展示页面
- 更新AxiosRequestUtil中认证处理逻辑
- 添加世系树渲染的HTML生成函数用于页面复用
- 更新文档中的API契约说明和页面规划
This commit is contained in:
fizzleaf
2026-07-30 16:04:48 +08:00
parent fb1743aa2a
commit 309598bfa6
53 changed files with 6740 additions and 282 deletions
+252
View File
@@ -0,0 +1,252 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.SiteNewsPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.SiteNewsPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var ARTICLE_TYPE_LABELS = {
news: '新闻',
notice: '公告',
download: '下载'
};
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function text(value) {
return value === undefined || value === null ? '' : String(value).trim();
}
function normalizeId(value) {
var normalized;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
normalized = String(value).trim();
return /^[1-9][0-9]*$/.test(normalized) ? normalized : '';
}
function normalizeArticleType(value) {
var normalized = text(value);
return Object.prototype.hasOwnProperty.call(ARTICLE_TYPE_LABELS, normalized)
? normalized
: '';
}
function normalizeExternalUrl(value) {
var normalized = text(value);
var parsed;
if (!normalized) return '';
try {
parsed = new URL(normalized);
} catch (error) {
return '';
}
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : '';
}
function normalizeArticle(item) {
var source = item || {};
var articleId = normalizeId(source.articleId);
var articleType = normalizeArticleType(source.articleType);
var articleTitle = text(source.articleTitle);
if (!articleId || !articleType || !articleTitle || String(source.status) !== '0') {
return null;
}
return {
articleId: articleId,
articleType: articleType,
articleTitle: articleTitle,
articleSummary: text(source.articleSummary),
articleContent: text(source.articleContent),
externalUrl: normalizeExternalUrl(source.externalUrl),
publishTime: text(source.publishTime)
};
}
function normalizeArticleList(data) {
var items;
if (!Array.isArray(data)) return [];
items = data.map(normalizeArticle);
return items.some(function (item) { return !item; }) ? [] : items;
}
function renderNormalizedArticleList(items) {
if (!items.length) return '<div class="api-empty">当前暂无资讯</div>';
return items.map(function (item) {
return '<a class="site-info-item" href="article-detail.html?articleId=' +
encodeURIComponent(item.articleId) + '">' +
'<small>' + escapeHtml(ARTICLE_TYPE_LABELS[item.articleType]) + '</small>' +
'<h2>' + escapeHtml(item.articleTitle) + '</h2>' +
'<p>' + escapeHtml(item.articleSummary) + '</p>' +
'<span>' + escapeHtml(item.publishTime) + '</span></a>';
}).join('');
}
function renderArticleList(data) {
return renderNormalizedArticleList(normalizeArticleList(data));
}
function renderNormalizedArticleDetail(item) {
var externalLink = item.externalUrl
? '<a class="btn ghost" data-external-link href="' + escapeHtml(item.externalUrl) +
'" target="_blank" rel="noopener noreferrer">查看外部内容</a>'
: '';
var content = escapeHtml(item.articleContent).replace(/\r?\n/g, '<br />');
return '<section class="inner-hero article-hero"><div class="container">' +
'<div class="hero-copy"><div class="eyebrow">' +
escapeHtml(ARTICLE_TYPE_LABELS[item.articleType]) + '</div><h1>' +
escapeHtml(item.articleTitle) + '</h1><p class="lead">' +
escapeHtml(item.articleSummary) + '</p><div class="article-meta"><span>' +
escapeHtml(item.publishTime) + '</span></div></div>' +
'<div class="hero-card article-cover-detail tilt-card"><span>闻</span><p>' +
escapeHtml(item.articleSummary) + '</p></div></div></section>' +
'<section class="section alt"><div class="container article-layout">' +
'<article class="article-content tilt-card"><p>' + content + '</p>' +
externalLink + '</article><aside class="article-side tilt-card"><h3>相关入口</h3>' +
'<a href="news.html">返回新闻资讯</a><a href="genealogy.html">了解数字家谱</a>' +
'<a href="create-genealogy.html">创建一本家谱</a></aside></div></section>';
}
function renderArticleDetail(item) {
var article = normalizeArticle(item);
return article
? renderNormalizedArticleDetail(article)
: '<div class="api-empty">资讯不存在或已下线</div>';
}
function readQueryValue(search, key) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return text(params.get(key));
}
function readArticleId(search) {
return normalizeId(readQueryValue(search, 'articleId'));
}
async function loadArticles(api, articleType) {
var requestedType = text(articleType);
var normalizedType = normalizeArticleType(requestedType);
var query = { limit: 100 };
var data;
var items;
if (!api || typeof api.siteArticles !== 'function') throw new Error('资讯接口不可用');
if (requestedType && !normalizedType) throw new Error('资讯分类无效');
if (normalizedType) query.articleType = normalizedType;
data = await api.siteArticles(query);
items = normalizeArticleList(data);
if (!Array.isArray(data) || items.length !== data.length) {
throw new Error('资讯列表响应无效');
}
return items;
}
async function loadArticleDetail(api, targetArticleId) {
var normalizedId = normalizeId(targetArticleId);
var items;
var match;
if (!normalizedId) throw new Error('资讯编号无效');
items = await loadArticles(api, '');
match = items.find(function (item) {
return item.articleId === normalizedId;
});
if (!match) throw new Error('资讯不存在或已下线');
return match;
}
async function init() {
var document = root.document;
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var search = root.location && root.location.search || '';
var listPage = document && document.querySelector('[data-site-news-page]');
var detailPage = document && document.querySelector('[data-site-article-page]');
var list;
var status;
var requestedType;
var detail;
var articleId;
if (listPage) {
list = document.querySelector('[data-site-news-list]');
status = document.querySelector('[data-site-news-status]');
requestedType = readQueryValue(search, 'articleType');
if (!list) return;
if (requestedType && !normalizeArticleType(requestedType)) {
list.innerHTML = '<div class="api-empty">资讯分类无效</div>';
if (status) status.textContent = '资讯分类无效';
return;
}
try {
list.classList.add('is-loading');
list.innerHTML = renderNormalizedArticleList(await loadArticles(api, requestedType));
if (status) status.textContent = '';
} catch (error) {
list.innerHTML = '<div class="api-empty">资讯读取失败,请稍后重试</div>';
if (status) status.textContent = error.message || '资讯读取失败';
} finally {
list.classList.remove('is-loading');
}
return;
}
if (detailPage) {
detail = document.querySelector('[data-site-article-detail]');
status = document.querySelector('[data-site-article-status]');
articleId = readArticleId(search);
if (!detail) return;
if (!articleId) {
detail.innerHTML = '<div class="api-empty">资讯编号无效</div>';
if (status) status.textContent = '资讯编号无效';
return;
}
try {
detail.classList.add('is-loading');
detail.innerHTML = renderNormalizedArticleDetail(await loadArticleDetail(api, articleId));
if (status) status.textContent = '';
} catch (error) {
detail.innerHTML = '<div class="api-empty">' +
escapeHtml(error.message || '资讯读取失败,请稍后重试') + '</div>';
if (status) status.textContent = error.message || '资讯读取失败';
} finally {
detail.classList.remove('is-loading');
}
}
}
return {
normalizeArticle: normalizeArticle,
normalizeArticleList: normalizeArticleList,
normalizeArticleType: normalizeArticleType,
normalizeExternalUrl: normalizeExternalUrl,
readArticleId: readArticleId,
renderArticleList: renderArticleList,
renderArticleDetail: renderArticleDetail,
loadArticles: loadArticles,
loadArticleDetail: loadArticleDetail,
init: init
};
});