修改功,现在补齐页面

This commit is contained in:
2026-07-11 21:36:03 +08:00
parent db397a7917
commit a6038be376
143 changed files with 7218 additions and 11912 deletions
+20
View File
@@ -101,6 +101,26 @@
gap: 20px;
}
.feature-status-banner {
padding: 18px 20px;
border: 1px solid rgba(196, 146, 69, .38);
border-radius: 16px;
color: #6b542d;
background: #fff8e8;
box-shadow: 0 12px 26px rgba(89, 66, 26, .07);
}
.feature-status-banner strong {
color: var(--red);
font-size: 16px;
}
.feature-status-banner p {
margin: 6px 0 0;
color: inherit;
line-height: 1.75;
}
.module-panel {
padding: 28px;
}
+176
View File
@@ -0,0 +1,176 @@
.site-info-hero {
padding: 78px 0 64px;
background:
radial-gradient(circle at 82% 18%, rgba(196, 146, 69, .2), transparent 24%),
linear-gradient(135deg, #fffaf0, #f3eadc 62%, #edf4ef);
}
.site-info-hero .container {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(220px, .65fr);
align-items: center;
gap: 44px;
}
.site-info-kicker {
margin-bottom: 14px;
color: var(--red);
font-size: 13px;
font-weight: 900;
letter-spacing: .12em;
text-transform: uppercase;
}
.site-info-hero h1 {
margin: 0;
font-size: clamp(36px, 5vw, 58px);
line-height: 1.15;
}
.site-info-hero p {
max-width: 680px;
margin: 20px 0 0;
color: var(--muted);
font-size: 17px;
line-height: 1.9;
}
.site-info-mark {
min-height: 180px;
display: grid;
place-items: center;
border: 1px solid rgba(196, 146, 69, .36);
border-radius: 28px;
color: var(--red);
background: rgba(255, 253, 248, .76);
box-shadow: var(--shadow);
font-size: 62px;
font-weight: 900;
}
.site-info-section {
padding: 72px 0;
}
.site-info-section.alt {
background: rgba(247, 239, 226, .64);
}
.site-info-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(240px, .36fr);
gap: 28px;
}
.site-info-list,
.site-info-panel,
.site-info-status {
padding: 30px;
border: 1px solid var(--line);
border-radius: 22px;
background: var(--white);
box-shadow: 0 16px 46px rgba(69, 53, 35, .08);
}
.site-info-list {
display: grid;
gap: 16px;
}
.site-info-item {
display: block;
padding: 20px;
border: 1px solid rgba(234, 223, 206, .9);
border-radius: 16px;
background: #fffaf2;
transition: transform .2s ease, border-color .2s ease;
}
.site-info-item:hover {
border-color: rgba(200, 59, 50, .4);
transform: translateY(-2px);
}
.site-info-item small,
.site-info-panel small {
color: var(--red);
font-weight: 800;
}
.site-info-item h2,
.site-info-panel h2,
.site-info-status h2 {
margin: 10px 0;
font-size: 22px;
}
.site-info-item p,
.site-info-panel p,
.site-info-status p {
margin: 0;
color: var(--muted);
line-height: 1.8;
}
.site-info-links {
display: grid;
gap: 12px;
margin-top: 18px;
}
.site-info-links a {
color: #8f4b31;
font-weight: 800;
}
.site-info-links a:hover {
color: var(--red);
}
.site-info-notice {
margin-bottom: 22px;
padding: 18px 20px;
border: 1px solid rgba(196, 146, 69, .42);
border-radius: 14px;
color: #775227;
background: #fff6e2;
line-height: 1.75;
}
.site-info-status {
max-width: 760px;
margin: 0 auto;
text-align: center;
}
.site-info-status .site-info-mark {
width: 96px;
min-height: 96px;
margin: 0 auto 22px;
border-radius: 50%;
font-size: 34px;
}
.site-info-actions {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
margin-top: 26px;
}
@media (max-width: 760px) {
.site-info-hero .container,
.site-info-layout {
grid-template-columns: 1fr;
}
.site-info-hero,
.site-info-section {
padding: 52px 0;
}
.site-info-mark {
min-height: 120px;
}
}
-312
View File
@@ -1,312 +0,0 @@
(function (root, factory) {
// 相册模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.AlbumPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.AlbumPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var selectedAlbumId = '';
function normalizeList(data) {
// 相册接口列表响应兼容数组、rows、records、list、data。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 相册名称和照片说明进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-album-page], [data-promo-album-page]');
return getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
getQueryParam(search, 'id') ||
'';
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toNumber(value, fallback) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : fallback;
}
function buildAlbumBody(values) {
var source = values || {};
return {
albumName: String(source.albumName || '').trim(),
albumDesc: trimOrUndefined(source.albumDesc),
coverOssId: toNumber(source.coverOssId),
sortOrder: toNumber(source.sortOrder, 1),
status: String(source.status || '').trim() || '0'
};
}
function buildPhotoBody(values) {
var source = values || {};
return {
ossId: toNumber(source.ossId),
photoTitle: trimOrUndefined(source.photoTitle),
photoDesc: trimOrUndefined(source.photoDesc),
photographer: trimOrUndefined(source.photographer),
shootTime: trimOrUndefined(source.shootTime),
sortOrder: toNumber(source.sortOrder, 1),
status: String(source.status || '').trim() || '0'
};
}
function getAlbumId(item) {
return pick(item, ['albumId', 'id'], '');
}
function buildAlbumRow(item) {
var albumId = getAlbumId(item);
var name = pick(item, ['albumName', 'name'], '未命名相册');
var photoCount = pick(item, ['photoCount', 'photos'], '0');
var desc = pick(item, ['albumDesc', 'description'], '暂无说明');
return '<button class="module-row album-row" type="button" data-album-id="' + escapeHtml(albumId) + '">' +
'<div><h3>' + escapeHtml(name) + '</h3><p>' + escapeHtml(photoCount) + ' 张照片 · ' + escapeHtml(desc) + '</p></div>' +
'<span class="pill">管理</span></button>';
}
function buildPhotoRow(item) {
var title = pick(item, ['photoTitle', 'title'], '未命名照片');
var desc = pick(item, ['photoDesc', 'description'], '暂无说明');
var photographer = pick(item, ['photographer'], '未记录拍摄人');
return '<div class="module-row album-photo-row"><div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(desc) + ' · ' + escapeHtml(photographer) + '</p></div><span class="pill">照片</span></div>';
}
function renderAlbums(items) {
var container = query('[data-album-list], [data-promo-album-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无相册</div>';
return;
}
container.innerHTML = list.map(buildAlbumRow).join('');
}
function renderPhotos(items) {
var container = query('[data-album-photo-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无照片</div>';
return;
}
container.innerHTML = list.map(buildPhotoRow).join('');
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
async function loadAlbums() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
if (!api || !genealogyId) return;
try {
renderAlbums(await api.albums(genealogyId));
} catch (error) {
showMessage(error.message || '相册加载失败');
}
}
async function loadPhotos(albumId) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
if (!api || !genealogyId || !albumId) return;
selectedAlbumId = albumId;
try {
renderPhotos(await api.albumPhotos(genealogyId, albumId));
} catch (error) {
showMessage(error.message || '照片加载失败');
}
}
async function submitAlbum(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var body = buildAlbumBody(getFormValues(form));
if (!api || !genealogyId) {
showMessage('请从具体家谱进入相册管理');
return;
}
if (!body.albumName) {
showMessage('请填写相册名称');
return;
}
try {
if (selectedAlbumId) {
await api.updateAlbum(genealogyId, selectedAlbumId, body);
} else {
await api.createAlbum(genealogyId, body);
}
form.reset();
selectedAlbumId = '';
showMessage('相册已保存');
loadAlbums();
} catch (error) {
showMessage(error.message || '保存相册失败');
}
}
async function submitPhoto(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var body = buildPhotoBody(getFormValues(form));
if (!api || !genealogyId || !selectedAlbumId) {
showMessage('请先选择相册');
return;
}
if (!body.ossId) {
showMessage('请填写照片 OSS ID');
return;
}
try {
await api.createAlbumPhoto(genealogyId, selectedAlbumId, body);
form.reset();
showMessage('照片已添加');
loadPhotos(selectedAlbumId);
} catch (error) {
showMessage(error.message || '添加照片失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var albumButton = event.target.closest('[data-album-id]');
if (!albumButton) return;
event.preventDefault();
loadPhotos(albumButton.getAttribute('data-album-id'));
});
documentRef.addEventListener('submit', function (event) {
var albumForm = event.target.closest('[data-album-form]');
var photoForm = event.target.closest('[data-album-photo-form]');
if (albumForm) {
event.preventDefault();
submitAlbum(albumForm);
}
if (photoForm) {
event.preventDefault();
submitPhoto(photoForm);
}
});
}
function init() {
if (!documentRef) return;
bindActions();
loadAlbums();
}
return {
normalizeList: normalizeList,
buildAlbumBody: buildAlbumBody,
buildPhotoBody: buildPhotoBody,
buildAlbumRow: buildAlbumRow,
init: init
};
});
-296
View File
@@ -1,296 +0,0 @@
(function (root, factory) {
// 谱文模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.ArticlePages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.ArticlePages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 通用列表响应兼容数组、rows、records、list、data。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 标题和摘要进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-article-page], [data-article-edit-page], [data-article-detail-page], [data-content-page]');
return getQueryParam(search, 'genealogyId') ||
getQueryParam(search, 'familyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
getQueryParam(search, 'id') ||
'';
}
function getCurrentArticleId() {
var search = root.location && root.location.search;
return getQueryParam(search, 'articleId') || getQueryParam(search, 'id') || '';
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toNumber(value, fallback) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : fallback;
}
function formatStatus(status) {
var value = String(status === undefined || status === null ? '' : status);
if (value === '0' || value === 'published') return '已发布';
if (value === '1' || value === 'draft') return '草稿';
return value || '未知';
}
function buildArticleBody(values) {
var source = values || {};
return {
categoryId: toNumber(source.categoryId),
articleTitle: String(source.articleTitle || '').trim(),
articleSummary: trimOrUndefined(source.articleSummary),
coverOssId: toNumber(source.coverOssId),
articleContent: String(source.articleContent || '').trim(),
authorName: trimOrUndefined(source.authorName),
sortOrder: toNumber(source.sortOrder, 1),
status: String(source.status || '').trim() || '0'
};
}
function getArticleId(item) {
return pick(item, ['articleId', 'id'], '');
}
function buildArticleRow(item, genealogyId) {
var articleId = getArticleId(item);
var title = pick(item, ['articleTitle', 'title'], '未命名谱文');
var category = pick(item, ['categoryName', 'categoryTitle', 'typeName'], '谱文');
var status = formatStatus(pick(item, ['status'], '0'));
var updateTime = pick(item, ['updateTime', 'publishTime', 'createTime'], '未记录时间');
return '<a class="module-row article-row" href="article-detail.html?id=' + encodeURIComponent(articleId) + '&genealogyId=' + encodeURIComponent(genealogyId || '') + '">' +
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(category) + ' · ' + escapeHtml(status) + ' · ' + escapeHtml(updateTime) + '</p></div>' +
'<span class="pill">查看</span></a>';
}
function buildCategoryOption(item) {
var categoryId = pick(item, ['categoryId', 'id'], '');
var name = pick(item, ['categoryName', 'name', 'title'], '未命名分类');
return '<option value="' + escapeHtml(categoryId) + '">' + escapeHtml(name) + '</option>';
}
function renderArticles(items, genealogyId) {
var container = query('[data-article-list], [data-recent-content]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无谱文内容</div>';
return;
}
container.innerHTML = list.map(function (item) {
return buildArticleRow(item, genealogyId);
}).join('');
}
function renderCategories(items) {
var select = query('[data-article-category]');
var list = normalizeList(items);
if (!select) return;
select.innerHTML = '<option value="">请选择分类</option>' + list.map(buildCategoryOption).join('');
}
function renderArticleDetail(article) {
var source = article || {};
var title = query('[data-article-title]');
var summary = query('[data-article-summary]');
var meta = query('[data-article-meta]');
var content = query('[data-article-content]');
if (title) title.textContent = pick(source, ['articleTitle', 'title'], '未命名谱文');
if (summary) summary.textContent = pick(source, ['articleSummary', 'summary'], '暂无摘要');
if (meta) {
meta.innerHTML = '<span>' + escapeHtml(pick(source, ['categoryName'], '谱文')) + '</span><span>' + escapeHtml(pick(source, ['authorName'], '佚名')) + '</span><span>' + escapeHtml(pick(source, ['updateTime', 'publishTime', 'createTime'], '未记录时间')) + '</span>';
}
if (content) content.innerHTML = pick(source, ['articleContent', 'content'], '<p>暂无正文</p>');
}
function fillArticleForm(article) {
var source = article || {};
queryAll('[name]').forEach(function (field) {
var value = pick(source, [field.name], '');
if (value !== '') field.value = value;
});
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
async function loadArticles() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var articleId = getCurrentArticleId();
if (!api || !genealogyId) return;
try {
if (query('[data-article-list], [data-recent-content]')) {
renderArticles(await api.articles(genealogyId), genealogyId);
}
if (query('[data-article-category]')) {
renderCategories(await api.articleCategories(genealogyId));
}
if (articleId && query('[data-article-detail-page], [data-article-edit-page]')) {
if (query('[data-article-detail-page]')) {
renderArticleDetail(await api.articleDetail(genealogyId, articleId));
} else {
fillArticleForm(await api.articleDetail(genealogyId, articleId));
}
}
} catch (error) {
showMessage(error.message || '谱文数据加载失败');
}
}
async function submitArticle(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var articleId = getCurrentArticleId();
var body = buildArticleBody(getFormValues(form));
if (!api || !genealogyId) {
showMessage('请从具体家谱进入谱文编辑');
return;
}
if (!body.articleTitle || !body.articleContent) {
showMessage('请填写谱文标题和正文');
return;
}
try {
if (articleId) {
await api.updateArticle(genealogyId, articleId, body);
} else {
await api.createArticle(genealogyId, body);
}
showMessage('谱文已保存');
root.location.href = 'profile-article.html?genealogyId=' + encodeURIComponent(genealogyId);
} catch (error) {
showMessage(error.message || '保存谱文失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-article-form]');
if (!form) return;
event.preventDefault();
submitArticle(form);
});
}
function init() {
if (!documentRef) return;
bindActions();
loadArticles();
}
return {
normalizeList: normalizeList,
buildArticleBody: buildArticleBody,
buildArticleRow: buildArticleRow,
formatStatus: formatStatus,
init: init
};
});
-382
View File
@@ -1,382 +0,0 @@
(function (root, factory) {
// 祭祀/贺礼/功德模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.CeremonyPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.CeremonyPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var selectedCeremonyId = '';
function normalizeList(data) {
// 通用列表响应兼容数组、rows、records、list、data。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 标题、姓名和说明进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-ceremony-page], [data-ceremony-edit-page], [data-merit-page], [data-merit-edit-page]');
return getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
getQueryParam(search, 'id') ||
'';
}
function getCurrentCeremonyId() {
var search = root.location && root.location.search;
return getQueryParam(search, 'ceremonyId') || '';
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toNumber(value, fallback) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : fallback;
}
function buildCeremonyBody(values) {
var source = values || {};
return {
ceremonyType: String(source.ceremonyType || '').trim(),
ceremonyTitle: String(source.ceremonyTitle || '').trim(),
ceremonyDesc: trimOrUndefined(source.ceremonyDesc),
ceremonyTime: trimOrUndefined(source.ceremonyTime),
location: trimOrUndefined(source.location),
coverOssId: toNumber(source.coverOssId),
sortOrder: toNumber(source.sortOrder, 1),
status: String(source.status || '').trim() || '0'
};
}
function buildGiftBody(values) {
var source = values || {};
return {
giverName: trimOrUndefined(source.giverName),
giftAmount: toNumber(source.giftAmount, 0),
giftMessage: trimOrUndefined(source.giftMessage)
};
}
function buildMeritBody(values) {
var source = values || {};
return {
donorName: String(source.donorName || '').trim(),
meritType: String(source.meritType || '').trim() || 'donation',
meritTitle: String(source.meritTitle || '').trim(),
meritContent: trimOrUndefined(source.meritContent),
amount: toNumber(source.amount, 0),
meritTime: trimOrUndefined(source.meritTime),
sortOrder: toNumber(source.sortOrder, 1),
status: String(source.status || '').trim() || '0'
};
}
function getCeremonyId(item) {
return pick(item, ['ceremonyId', 'id'], '');
}
function buildCeremonyRow(item) {
var ceremonyId = getCeremonyId(item);
var title = pick(item, ['ceremonyTitle', 'title'], '未命名活动');
var type = pick(item, ['ceremonyType', 'type'], 'ceremony');
var time = pick(item, ['ceremonyTime', 'time', 'createTime'], '未设置时间');
var giftCount = pick(item, ['giftCount', 'gifts'], '0');
return '<button class="module-row ceremony-row" type="button" data-ceremony-id="' + escapeHtml(ceremonyId) + '">' +
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(type) + ' · ' + escapeHtml(time) + ' · ' + escapeHtml(giftCount) + ' 条献礼</p></div>' +
'<span class="pill">管理</span></button>';
}
function buildGiftRow(item) {
var name = pick(item, ['giverName', 'memberName', 'name'], '匿名');
var amount = pick(item, ['giftAmount', 'amount'], '0');
var message = pick(item, ['giftMessage', 'message'], '暂无留言');
return '<div class="module-row ceremony-gift-row"><div><h3>' + escapeHtml(name) + '</h3><p>' + escapeHtml(amount) + ' 元 · ' + escapeHtml(message) + '</p></div><span class="pill">献礼</span></div>';
}
function buildMeritRow(item) {
var name = pick(item, ['donorName', 'name'], '未命名功德人');
var title = pick(item, ['meritTitle', 'title'], '功德记录');
var time = pick(item, ['meritTime', 'createTime'], '未记录时间');
return '<div class="module-row merit-row"><div><h3>' + escapeHtml(name) + '</h3><p>' + escapeHtml(title) + ' · ' + escapeHtml(time) + '</p></div><span class="pill">查看</span></div>';
}
function renderCeremonies(items) {
var container = query('[data-ceremony-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无贺礼邀请</div>';
return;
}
container.innerHTML = list.map(buildCeremonyRow).join('');
}
function renderGifts(items) {
var container = query('[data-ceremony-gift-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无献礼记录</div>';
return;
}
container.innerHTML = list.map(buildGiftRow).join('');
}
function renderMerits(items) {
var container = query('[data-merit-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无功德记录</div>';
return;
}
container.innerHTML = list.map(buildMeritRow).join('');
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
async function loadCeremonies() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var ceremonyId = getCurrentCeremonyId();
if (!api || !genealogyId) return;
try {
if (query('[data-ceremony-list]')) {
renderCeremonies(await api.ceremonies(genealogyId));
}
if (ceremonyId && query('[data-ceremony-gift-list]')) {
selectedCeremonyId = ceremonyId;
renderGifts(await api.ceremonyGifts(genealogyId, ceremonyId));
}
} catch (error) {
showMessage(error.message || '贺礼数据加载失败');
}
}
async function loadMerits() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
if (!api || !genealogyId || !query('[data-merit-list]')) return;
try {
renderMerits(await api.meritRecords(genealogyId));
} catch (error) {
showMessage(error.message || '功德记录加载失败');
}
}
async function submitCeremony(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var ceremonyId = getCurrentCeremonyId();
var body = buildCeremonyBody(getFormValues(form));
if (!api || !genealogyId) return;
if (!body.ceremonyType || !body.ceremonyTitle) {
showMessage('请填写贺礼类型和标题');
return;
}
try {
if (ceremonyId) {
await api.updateCeremony(genealogyId, ceremonyId, body);
} else {
await api.createCeremony(genealogyId, body);
}
showMessage('贺礼已保存');
root.location.href = 'profile-gift.html?genealogyId=' + encodeURIComponent(genealogyId);
} catch (error) {
showMessage(error.message || '保存贺礼失败');
}
}
async function submitGift(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var ceremonyId = selectedCeremonyId || getCurrentCeremonyId();
var body = buildGiftBody(getFormValues(form));
if (!api || !genealogyId || !ceremonyId) {
showMessage('请先选择贺礼邀请');
return;
}
try {
await api.createCeremonyGift(genealogyId, ceremonyId, body);
form.reset();
showMessage('献礼已保存');
renderGifts(await api.ceremonyGifts(genealogyId, ceremonyId));
} catch (error) {
showMessage(error.message || '保存献礼失败');
}
}
async function submitMerit(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var body = buildMeritBody(getFormValues(form));
if (!api || !genealogyId) return;
if (!body.donorName || !body.meritTitle) {
showMessage('请填写功德人和功德标题');
return;
}
try {
await api.createMeritRecord(genealogyId, body);
showMessage('功德记录已保存');
root.location.href = 'profile-merit.html?genealogyId=' + encodeURIComponent(genealogyId);
} catch (error) {
showMessage(error.message || '保存功德记录失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var ceremonyButton = event.target.closest('[data-ceremony-id]');
if (!ceremonyButton) return;
selectedCeremonyId = ceremonyButton.getAttribute('data-ceremony-id');
if (getApi() && getCurrentGenealogyId()) {
getApi().ceremonyGifts(getCurrentGenealogyId(), selectedCeremonyId).then(renderGifts).catch(function (error) {
showMessage(error.message || '献礼加载失败');
});
}
});
documentRef.addEventListener('submit', function (event) {
var ceremonyForm = event.target.closest('[data-ceremony-form]');
var giftForm = event.target.closest('[data-ceremony-gift-form]');
var meritForm = event.target.closest('[data-merit-form]');
if (ceremonyForm) {
event.preventDefault();
submitCeremony(ceremonyForm);
}
if (giftForm) {
event.preventDefault();
submitGift(giftForm);
}
if (meritForm) {
event.preventDefault();
submitMerit(meritForm);
}
});
}
function init() {
if (!documentRef) return;
bindActions();
loadCeremonies();
loadMerits();
}
return {
normalizeList: normalizeList,
buildCeremonyBody: buildCeremonyBody,
buildMeritBody: buildMeritBody,
buildCeremonyRow: buildCeremonyRow,
init: init
};
});
+285 -148
View File
@@ -16,50 +16,16 @@
var documentRef = root.document;
function normalizeList(data) {
// 动态分页响应兼容 rows、records、list、data。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 动态内容进入 innerHTML 前转义,详情富文本不在这里处理。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
}
function showMessage(message) {
@@ -68,28 +34,26 @@
return;
}
root.alert(message);
if (root.alert) root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
function getCurrentGenealogyId(search) {
var page = query('[data-feed-page], [data-feed-edit-page]');
var source = search === undefined && root.location ? root.location.search : search;
return getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
getQueryParam(search, 'id') ||
'';
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
}
function getCurrentFeedId() {
var search = root.location && root.location.search;
function getCurrentFeedId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return getQueryParam(search, 'feedId') || '';
return getQueryParam(source, 'feedId');
}
function trimOrUndefined(value) {
@@ -98,187 +62,338 @@
return text || undefined;
}
function toNumber(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
function toNumberOrUndefined(value) {
var text = trimOrUndefined(value);
var number;
return text && Number.isFinite(number) ? number : undefined;
if (!text) return undefined;
number = Number(text);
return Number.isFinite(number) ? number : undefined;
}
function buildFeedBody(values) {
// FamilyFeedBody 只提交最新接口文档定义的字段。
var source = values || {};
return {
content: String(source.content || '').trim(),
var body = {
feedType: trimOrUndefined(source.feedType) || 'text',
feedContent: String(source.feedContent || '').trim(),
mediaOssIds: trimOrUndefined(source.mediaOssIds),
visibility: String(source.visibility || '').trim() || '2',
status: String(source.status || '').trim() || '0'
status: trimOrUndefined(source.status)
};
var sortOrder = toNumberOrUndefined(source.sortOrder);
if (sortOrder !== undefined) body.sortOrder = sortOrder;
return body;
}
function buildCommentBody(values) {
// FamilyFeedCommentBody 要求 content 必填
// FamilyFeedCommentBody content 是唯一必填字段,其余字段按需携带
var source = values || {};
return {
parentCommentId: toNumber(source.parentCommentId),
replyUserId: toNumber(source.replyUserId),
var body = {
content: String(source.content || '').trim()
};
var parentCommentId = toNumberOrUndefined(source.parentCommentId);
var replyUserId = toNumberOrUndefined(source.replyUserId);
if (parentCommentId !== undefined) body.parentCommentId = parentCommentId;
if (replyUserId !== undefined) body.replyUserId = replyUserId;
return body;
}
function getFeedId(item) {
return pick(item, ['feedId', 'id'], '');
var value = item && item.feedId;
var text;
if (value === undefined || value === null || value === '') return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
}
function buildFeedRow(item) {
function getCommentId(item) {
var value = item && item.commentId;
var text;
if (value === undefined || value === null || value === '') return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
}
function normalizeFeed(item) {
var feedId = getFeedId(item);
var author = pick(item, ['authorName', 'nickName', 'memberName'], '家族成员');
var content = pick(item, ['content', 'feedContent'], '暂无内容');
var likeCount = pick(item, ['likeCount', 'likes'], '0');
var commentCount = pick(item, ['commentCount', 'comments'], '0');
var createTime = pick(item, ['createTime', 'publishTime', 'updateTime'], '未记录时间');
var feedContent = item && item.feedContent;
var feed;
return '<div class="module-row feed-row" data-feed-id="' + escapeHtml(feedId) + '">' +
'<div><h3>' + escapeHtml(author) + '</h3><p>' + escapeHtml(content) + '</p><small>' + escapeHtml(createTime) + ' · ' + escapeHtml(likeCount) + ' 赞 · ' + escapeHtml(commentCount) + ' 评论</small></div>' +
'<div class="row-actions"><button class="pill" type="button" data-feed-like="' + escapeHtml(feedId) + '">点赞</button><button class="pill" type="button" data-feed-comment="' + escapeHtml(feedId) + '">评论</button></div>' +
'</div>';
if (!feedId || feedContent === undefined || feedContent === null) return null;
feed = {
feedId: feedId,
feedContent: String(feedContent)
};
if (trimOrUndefined(item.mediaOssIds)) feed.mediaOssIds = trimOrUndefined(item.mediaOssIds);
if (trimOrUndefined(item.feedType)) feed.feedType = trimOrUndefined(item.feedType);
if (trimOrUndefined(item.status)) feed.status = trimOrUndefined(item.status);
return feed;
}
function renderFeeds(items) {
function normalizeComment(item) {
var commentId = getCommentId(item);
var content = item && item.content;
if (!commentId || content === undefined || content === null) return null;
return {
commentId: commentId,
content: String(content)
};
}
function normalizeList(data) {
// AxiosRequestUtil 已解包 ListResult 与 PageResult,这里只接收对应的 data 或 rows。
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.rows)) return data.rows;
return [];
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getListUrl(genealogyId, feedId) {
var params = new URLSearchParams();
params.set('genealogyId', genealogyId);
if (feedId) params.set('feedId', feedId);
return 'profile-feed' + (feedId ? '-edit' : '') + '.html?' + params.toString();
}
function renderFeedRow(feed, genealogyId) {
return '<article class="module-row feed-row" data-feed-id="' + escapeHtml(feed.feedId) + '">' +
'<div><h3>家族动态</h3><p>' + escapeHtml(feed.feedContent) + '</p></div>' +
'<div class="row-actions">' +
'<button class="pill" type="button" data-feed-like="' + escapeHtml(feed.feedId) + '">点赞</button>' +
'<button class="pill" type="button" data-feed-unlike="' + escapeHtml(feed.feedId) + '">取消点赞</button>' +
'<button class="pill" type="button" data-feed-comments="' + escapeHtml(feed.feedId) + '">查看评论</button>' +
'<button class="pill" type="button" data-feed-comment="' + escapeHtml(feed.feedId) + '">发表评论</button>' +
'<a class="pill" href="' + escapeHtml(getListUrl(genealogyId, feed.feedId)) + '">编辑</a>' +
'<button class="pill" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>' +
'</div></article>';
}
function renderFeeds(data, genealogyId) {
var container = query('[data-feed-list]');
var list = normalizeList(items);
var items = normalizeList(data).map(normalizeFeed);
var invalidCount = items.filter(function (item) { return !item; }).length;
var feeds = items.filter(Boolean);
if (!container) return;
if (!list.length) {
if (invalidCount) {
container.innerHTML = '<div class="api-empty">动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。</div>';
return;
}
if (!feeds.length) {
container.innerHTML = '<div class="api-empty">暂无家族动态</div>';
return;
}
container.innerHTML = list.map(buildFeedRow).join('');
container.innerHTML = feeds.map(function (feed) {
return renderFeedRow(feed, genealogyId);
}).join('');
}
function renderComments(feedId, data) {
var row = query('[data-feed-id="' + feedId + '"]');
var comments = normalizeList(data).map(normalizeComment);
var invalidCount = comments.filter(function (item) { return !item; }).length;
var list = comments.filter(Boolean);
var previous = row && row.nextElementSibling;
if (!row) return;
if (previous && previous.getAttribute('data-feed-comment-list') === feedId) previous.remove();
if (invalidCount) {
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">评论响应缺少 commentId 或 content,请联系后端补充 DTO。</div>');
return;
}
if (!list.length) {
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">暂无评论</div>');
return;
}
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-comment-list="' + escapeHtml(feedId) + '">' + list.map(function (comment) {
return '<div class="module-row"><p>' + escapeHtml(comment.content) + '</p><button class="pill" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button></div>';
}).join('') + '</div>');
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
if (field.type === 'checkbox') {
values[field.name] = field.checked ? '1' : '';
return;
}
values[field.name] = field.value;
});
return values;
}
function fillFeedForm(feed) {
var source = feed || {};
function fillFeedForm(data) {
var feed = normalizeFeed(data);
queryAll('[name]').forEach(function (field) {
var value = pick(source, [field.name], '');
if (field.type === 'checkbox') {
field.checked = value === '1' || value === true;
return;
}
if (value !== '') field.value = value;
if (!feed) throw new Error('动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
queryAll('[data-feed-form] [name]').forEach(function (field) {
if (feed[field.name] !== undefined && feed[field.name] !== null) field.value = feed[field.name];
});
}
function syncGenealogyLinks(genealogyId) {
queryAll('[data-feed-create-link], [data-feed-list-link]').forEach(function (link) {
link.href = getListUrl(genealogyId);
});
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
var container;
if (!genealogyId) {
container = query('[data-feed-list]');
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入家族圈动态</div>';
showMessage('请从具体家谱进入家族圈动态');
}
return genealogyId;
}
async function loadFeeds() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var feedId = getCurrentFeedId();
var genealogyId = requireGenealogyId();
if (!api || !genealogyId) return;
syncGenealogyLinks(genealogyId);
try {
if (query('[data-feed-list]')) {
renderFeeds(await api.feedsPage(genealogyId, {
pageNum: 1,
pageSize: 10
}));
}
if (feedId && query('[data-feed-edit-page]')) {
fillFeedForm(await api.feedDetail(genealogyId, feedId));
}
renderFeeds(await api.feedsPage(genealogyId, { pageNum: 1, pageSize: 20 }), genealogyId);
} catch (error) {
showMessage(error.message || '家族动态加载失败');
}
}
async function loadFeedForEdit() {
var api = getApi();
var genealogyId = requireGenealogyId();
var feedId = getCurrentFeedId();
if (!api || !genealogyId) return;
syncGenealogyLinks(genealogyId);
if (!feedId) return;
try {
fillFeedForm(await api.feedDetail(genealogyId, feedId));
} catch (error) {
showMessage(error.message || '动态详情加载失败');
}
}
async function submitFeed(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var genealogyId = requireGenealogyId();
var feedId = getCurrentFeedId();
var body = buildFeedBody(getFormValues(form));
var body;
if (!api || !genealogyId) {
showMessage('请从具体家谱进入动态发布');
return;
}
if (!body.content) {
if (!api || !genealogyId) return;
// 富文本编辑器在提交前同步回 textarea,保证发送的是用户当前输入。
if (root.KindEditor && root.KindEditor.sync) root.KindEditor.sync('#feedContent');
body = buildFeedBody(getFormValues(form));
if (!body.feedContent) {
showMessage('请填写动态内容');
return;
}
try {
if (feedId) {
await api.updateFeed(genealogyId, feedId, body);
} else {
await api.createFeed(genealogyId, body);
}
showMessage('动态已保存');
root.location.href = 'profile-feed.html?genealogyId=' + encodeURIComponent(genealogyId);
root.location.href = getListUrl(genealogyId);
} catch (error) {
showMessage(error.message || '保存动态失败');
showMessage(error.message || '动态保存失败');
}
}
async function likeFeed(feedId) {
async function likeFeed(feedId, shouldLike) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var genealogyId = requireGenealogyId();
if (!api || !genealogyId || !feedId) return;
try {
await api.likeFeed(genealogyId, feedId);
showMessage('已点赞');
loadFeeds();
if (shouldLike) {
await api.likeFeed(genealogyId, feedId);
} else {
await api.unlikeFeed(genealogyId, feedId);
}
await loadFeeds();
} catch (error) {
showMessage(error.message || '点赞失败');
showMessage(error.message || (shouldLike ? '点赞失败' : '取消点赞失败'));
}
}
async function commentFeed(feedId) {
async function deleteFeed(feedId) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var genealogyId = requireGenealogyId();
if (!api || !genealogyId || !feedId) return;
if (root.confirm && !root.confirm('确认删除这条动态吗?')) return;
try {
await api.deleteFeed(genealogyId, feedId);
await loadFeeds();
} catch (error) {
showMessage(error.message || '删除动态失败');
}
}
async function showComments(feedId) {
var api = getApi();
var genealogyId = requireGenealogyId();
if (!api || !genealogyId || !feedId) return;
try {
renderComments(feedId, await api.feedComments(genealogyId, feedId));
} catch (error) {
showMessage(error.message || '评论加载失败');
}
}
async function createComment(feedId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var content;
var body;
if (!api || !genealogyId || !feedId) return;
content = root.prompt('请输入评论内容', '');
if (!content) return;
content = root.prompt ? root.prompt('请输入评论内容', '') : '';
body = buildCommentBody({ content: content });
if (!body.content) return;
try {
await api.createFeedComment(genealogyId, feedId, buildCommentBody({
content: content
}));
showMessage('评论已发布');
loadFeeds();
await api.createFeedComment(genealogyId, feedId, body);
await showComments(feedId);
} catch (error) {
showMessage(error.message || '评论失败');
showMessage(error.message || '评论发布失败');
}
}
async function deleteComment(feedId, commentId) {
var api = getApi();
var genealogyId = requireGenealogyId();
if (!api || !genealogyId || !feedId || !commentId) return;
if (root.confirm && !root.confirm('确认删除这条评论吗?')) return;
try {
await api.deleteFeedComment(genealogyId, feedId, commentId);
await showComments(feedId);
} catch (error) {
showMessage(error.message || '删除评论失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-feed-form]');
@@ -286,35 +401,57 @@
event.preventDefault();
submitFeed(form);
});
documentRef.addEventListener('click', function (event) {
var likeButton = event.target.closest('[data-feed-like]');
var commentButton = event.target.closest('[data-feed-comment]');
var target = event.target;
var feedId;
var commentId;
if (likeButton) {
if (target.closest('[data-feed-like]')) {
event.preventDefault();
likeFeed(likeButton.getAttribute('data-feed-like'));
likeFeed(target.closest('[data-feed-like]').getAttribute('data-feed-like'), true);
}
if (commentButton) {
if (target.closest('[data-feed-unlike]')) {
event.preventDefault();
commentFeed(commentButton.getAttribute('data-feed-comment'));
likeFeed(target.closest('[data-feed-unlike]').getAttribute('data-feed-unlike'), false);
}
if (target.closest('[data-feed-comments]')) {
event.preventDefault();
feedId = target.closest('[data-feed-comments]').getAttribute('data-feed-comments');
showComments(feedId);
}
if (target.closest('[data-feed-comment]')) {
event.preventDefault();
feedId = target.closest('[data-feed-comment]').getAttribute('data-feed-comment');
createComment(feedId);
}
if (target.closest('[data-feed-delete]')) {
event.preventDefault();
deleteFeed(target.closest('[data-feed-delete]').getAttribute('data-feed-delete'));
}
if (target.closest('[data-feed-comment-delete]')) {
event.preventDefault();
commentId = target.closest('[data-feed-comment-delete]').getAttribute('data-feed-comment-delete');
feedId = target.closest('[data-feed-comment-delete]').getAttribute('data-feed-id');
deleteComment(feedId, commentId);
}
});
}
function init() {
if (!documentRef) return;
bindActions();
loadFeeds();
if (query('[data-feed-page]')) loadFeeds();
if (query('[data-feed-edit-page]')) loadFeedForEdit();
}
return {
normalizeList: normalizeList,
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentFeedId: getCurrentFeedId,
buildFeedBody: buildFeedBody,
buildCommentBody: buildCommentBody,
buildFeedRow: buildFeedRow,
normalizeFeed: normalizeFeed,
normalizeComment: normalizeComment,
normalizeList: normalizeList,
init: init
};
});
-177
View File
@@ -1,177 +0,0 @@
(function (root, factory) {
// 反馈模块同时支持浏览器页面和 Node 测试。
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 documentRef = root.document;
function normalizeList(data) {
// 兼容分页 rows、records、list 和直接数组。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function buildFeedbackBody(values) {
// 接口只接收类型、内容、联系方式;页面上的标题合并到内容前面。
var title = values.feedbackTitle ? '【' + values.feedbackTitle + '】' : '';
return {
feedbackType: values.feedbackType || 'suggestion',
feedbackContent: title + (values.feedbackContent || ''),
contactInfo: values.contactInfo || ''
};
}
function buildFeedbackRow(item) {
var type = pick(item, ['feedbackType', 'typeName'], '反馈');
var content = pick(item, ['feedbackContent', 'content'], '暂无描述');
var status = pick(item, ['status', 'statusName'], '已提交');
var time = pick(item, ['createTime', 'createDate', 'createdAt'], '时间待确认');
return '<div class="module-row"><div><h3>' + type + '</h3><p>' +
status + ' · ' + time + ' · ' + content +
'</p></div><span class="pill">查看</span></div>';
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function readForm(form) {
// name 属性和反馈接口字段保持一致,减少页面文案变化带来的影响。
var values = {};
queryAll('input[name], textarea[name], select[name]', form).forEach(function (field) {
values[field.name] = String(field.value || '').trim();
});
return values;
}
function setBusy(button, busy) {
// 加载态只切换 class,具体样式在 CSS 中维护。
if (!button) return;
button.classList.toggle('is-loading', busy);
if ('disabled' in button) button.disabled = busy;
button.setAttribute('aria-busy', busy ? 'true' : 'false');
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function renderFeedbackList(items) {
var container = query('[data-feedback-list]');
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">暂无历史反馈</div>';
return;
}
container.innerHTML = items.map(buildFeedbackRow).join('');
}
async function loadFeedbackList() {
var api = getApi();
var container = query('[data-feedback-list]');
if (!api || !container) return;
container.classList.add('is-loading');
try {
renderFeedbackList(normalizeList(await api.feedbackList()));
} catch (error) {
showMessage(error.message || '历史反馈加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function submitFeedback(form, button) {
var api = getApi();
var values = readForm(form);
if (!api) return;
if (!values.feedbackContent) {
showMessage('请填写反馈内容');
return;
}
setBusy(button, true);
try {
await api.submitFeedback(buildFeedbackBody(values));
form.reset();
showMessage('反馈已提交');
loadFeedbackList();
} catch (error) {
showMessage(error.message || '反馈提交失败');
} finally {
setBusy(button, false);
}
}
function bindFeedbackForms() {
queryAll('[data-feedback-form]').forEach(function (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
submitFeedback(form, query('[type="submit"]', form));
});
});
}
function init() {
if (!documentRef) return;
bindFeedbackForms();
loadFeedbackList();
}
return {
normalizeList: normalizeList,
buildFeedbackBody: buildFeedbackBody,
buildFeedbackRow: buildFeedbackRow,
init: init
};
});
-407
View File
@@ -1,407 +0,0 @@
(function (root, factory) {
// 家谱模块脚本同时支持浏览器运行和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.GenealogyPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.GenealogyPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 列表接口可能返回数组、rows、records 或 list,这里统一成数组供页面渲染。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
// 后端字段名存在少量差异时,按优先级取第一个可用字段。
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function buildGenealogyBody(values) {
// 创建家谱接口必填 genealogyName、surname、regionCode,地区编码由地区选择器回填。
var source = values || {};
return {
genealogyName: source.genealogyName || '',
surname: source.surname || '',
regionCode: source.regionCode || '',
addressDetail: source.addressDetail || '',
intro: source.intro || '',
visibility: source.visibility || '1',
joinMode: source.joinMode || '1'
};
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function readForm(form) {
// 页面表单 name 与接口字段对齐,后续加字段只需要补 HTML。
var values = {};
queryAll('input[name], textarea[name], select[name]', form).forEach(function (field) {
values[field.name] = String(field.value || '').trim();
});
return values;
}
function getQueryParam(search, name) {
// 详情页和加入页用 query 参数传递 genealogyId,测试环境也可直接传 search 字符串。
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
return getQueryParam(search, 'id') || getQueryParam(search, 'genealogyId');
}
function escapeHtml(value) {
// 接口内容进入 innerHTML 前统一转义,避免公开页面渲染异常内容。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function setBusy(button, busy) {
// 加载状态只加 class,样式统一写在 CSS 中。
if (!button) return;
button.classList.toggle('is-loading', busy);
if ('disabled' in button) button.disabled = busy;
button.setAttribute('aria-busy', busy ? 'true' : 'false');
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function requireValue(value, message) {
if (value) return true;
showMessage(message);
return false;
}
function buildFamilyHeroTitle(detail) {
return pick(detail, ['genealogyName', 'name', 'title'], '未命名家谱');
}
function formatJoinMode(value) {
var map = {
0: '关闭加入',
1: '审核加入',
2: '邀请码加入'
};
return map[value] || value || '多人共修';
}
function buildFamilySummary(detail) {
// 后端详情字段可能来自统计字段或普通展示字段,这里集中做字段映射。
var memberCount = pick(detail, ['memberCount', 'membersCount'], '0');
var generationCount = pick(detail, ['generationCount', 'generationTotal'], '0');
var articleCount = pick(detail, ['articleCount', 'articlesCount'], '0');
var albumCount = pick(detail, ['albumCount', 'albumsCount'], '0');
return [
{ label: '成员', value: memberCount },
{ label: '世代', value: generationCount },
{ label: '谱文', value: articleCount },
{ label: '相册', value: albumCount }
];
}
function buildJoinApplyBody(values) {
// 接口文档要求 applicantName、phone、relationDesc、applyReason,可选 inviterUserId。
var source = values || {};
var inviterUserId = source.inviterUserId ? Number(source.inviterUserId) : undefined;
return {
applicantName: String(source.applicantName || '').trim(),
phone: String(source.phone || '').trim(),
relationDesc: String(source.relationDesc || '').trim(),
applyReason: String(source.applyReason || '').trim() || '申请加入家谱',
inviterUserId: inviterUserId
};
}
function renderPublicGenealogies(items) {
// 广场页复用现有卡片结构,只替换接口返回的数据。
var container = query('[data-genealogy-list="public"]');
var html;
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">暂无公开家谱</div>';
return;
}
html = items.slice(0, 6).map(function (item, index) {
// 第一条沿用推荐卡片样式,其余走普通家谱卡片样式。
var id = pick(item, ['genealogyId', 'id'], '');
var name = pick(item, ['genealogyName', 'name', 'title'], '未命名家谱');
var surname = pick(item, ['surname'], '家谱');
var region = pick(item, ['addressDetail', 'regionName', 'originPlace'], '地区待完善');
var intro = pick(item, ['intro', 'description', 'summary'], '家谱资料正在完善中。');
var className = index === 0 ? 'family-card featured tilt-card' : 'card family-lite tilt-card';
return '<a class="' + className + '" href="family-detail.html?id=' + encodeURIComponent(id) + '">' +
'<h3>' + name + '</h3>' +
'<p>' + intro + '</p>' +
'<div class="tag-row"><span class="tag">' + region + '</span><span class="tag">' + surname + '</span></div>' +
'</a>';
}).join('');
container.innerHTML = html;
}
function buildMyGenealogyRow(item) {
// 我的家谱列表保持 profile-module.css 的 module-row 结构。
var id = pick(item, ['genealogyId', 'id'], '');
var name = pick(item, ['genealogyName', 'name'], '未命名家谱');
var role = pick(item, ['roleName', 'memberRole'], '成员');
var count = pick(item, ['memberCount', 'membersCount'], '0');
return '<a class="module-row" href="profile-family-home.html?id=' + encodeURIComponent(id) + '">' +
'<div><h3>' + name + '</h3><p>' + role + ' · 共修入 ' + count + ' 人</p></div>' +
'<span class="pill">进入主页</span></a>';
}
function renderMyGenealogies(items) {
// 没有数据时显示公共空状态,避免保留旧静态示例误导用户。
var container = query('[data-genealogy-list="mine"]');
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">你还没有创建或加入家谱</div>';
return;
}
container.innerHTML = items.map(buildMyGenealogyRow).join('');
}
function renderFamilyDetail(detail) {
var page = query('[data-family-detail]');
var summaryGrid = query('[data-family-summary]');
var joinLink = query('[data-family-join-link]');
var id;
if (!page) return;
id = pick(detail, ['genealogyId', 'id'], getCurrentGenealogyId());
queryAll('[data-family-text]').forEach(function (node) {
var field = node.getAttribute('data-family-text');
var fallback = node.getAttribute('data-fallback') || '';
var value = field === 'title'
? buildFamilyHeroTitle(detail)
: pick(detail, field.split('|'), fallback);
node.textContent = field.indexOf('joinMode') !== -1 ? formatJoinMode(value) : value;
});
if (summaryGrid) {
summaryGrid.innerHTML = buildFamilySummary(detail).map(function (item) {
return '<span><b>' + escapeHtml(item.value) + '</b>' + escapeHtml(item.label) + '</span>';
}).join('');
}
if (joinLink && id) {
joinLink.href = 'join-genealogy.html?id=' + encodeURIComponent(id);
}
}
async function loadFamilyDetail() {
// 只有详情页存在 data-family-detail,且 URL 带 id/genealogyId 时才请求接口。
var api = getApi();
var page = query('[data-family-detail]');
var genealogyId = getCurrentGenealogyId();
if (!api || !page) return;
if (!genealogyId) {
showMessage('缺少家谱ID');
return;
}
page.classList.add('is-loading');
try {
renderFamilyDetail(await api.genealogyDetail(genealogyId));
} catch (error) {
showMessage(error.message || '家谱详情加载失败');
} finally {
page.classList.remove('is-loading');
}
}
async function submitJoinApply(form, button) {
var api = getApi();
var values = readForm(form);
var genealogyId = values.genealogyId || getCurrentGenealogyId();
var body = buildJoinApplyBody(values);
if (!api) return;
if (!requireValue(genealogyId, '请填写家谱ID或邀请码')) return;
if (!requireValue(body.applicantName, '请填写你的姓名')) return;
if (!requireValue(body.phone, '请填写手机号')) return;
if (!requireValue(body.relationDesc, '请填写关系说明')) return;
setBusy(button, true);
try {
await api.applyJoinGenealogy(genealogyId, body);
showMessage('申请已提交,请等待管理员审核');
root.location.href = form.getAttribute('data-success-url') || 'profile-join-family.html';
} catch (error) {
showMessage(error.message || '申请提交失败');
} finally {
setBusy(button, false);
}
}
async function loadPublicGenealogies() {
// 只有带 data-genealogy-list="public" 的页面才会真正发起请求。
var api = getApi();
var container = query('[data-genealogy-list="public"]');
if (!api || !container) return;
container.classList.add('is-loading');
try {
renderPublicGenealogies(normalizeList(await api.publicGenealogies()));
} catch (error) {
showMessage(error.message || '公开家谱加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function loadMyGenealogies() {
// 只有个人中心“我的家谱”页面会命中这个容器。
var api = getApi();
var container = query('[data-genealogy-list="mine"]');
if (!api || !container) return;
container.classList.add('is-loading');
try {
renderMyGenealogies(normalizeList(await api.myGenealogies()));
} catch (error) {
showMessage(error.message || '我的家谱加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function submitGenealogy(form, button) {
// 创建成功后跳到我的家谱列表,后续继续维护成员和内容。
var api = getApi();
var body = buildGenealogyBody(readForm(form));
if (!api) return;
if (!requireValue(body.genealogyName, '请填写家谱名称')) return;
if (!requireValue(body.surname, '请填写主要姓氏')) return;
if (!requireValue(body.regionCode, '请选择家族所在地')) return;
setBusy(button, true);
try {
await api.createGenealogy(body);
showMessage('家谱创建成功');
root.location.href = form.getAttribute('data-success-url') || 'profile-families.html';
} catch (error) {
showMessage(error.message || '家谱创建失败');
} finally {
setBusy(button, false);
}
}
function bindCreateForm() {
// 创建表单用 submit 事件,兼容按钮点击和键盘回车提交。
var form = query('[data-genealogy-form="create"]');
if (!form) return;
form.addEventListener('submit', function (event) {
event.preventDefault();
submitGenealogy(form, query('[type="submit"]', form));
});
}
function bindJoinForm() {
// 加入页用同一个 submit 入口,兼容详情页带 id 跳转和用户手动输入家谱ID。
var form = query('[data-genealogy-form="join"]');
var idField;
var currentId;
if (!form) return;
idField = query('[name="genealogyId"]', form);
currentId = getCurrentGenealogyId();
if (idField && currentId && !idField.value) idField.value = currentId;
form.addEventListener('submit', function (event) {
event.preventDefault();
submitJoinApply(form, query('[type="submit"]', form));
});
}
function init() {
// 一个模块脚本服务多个家谱页面,通过 data 属性决定当前页面要做什么。
if (!documentRef) return;
bindCreateForm();
bindJoinForm();
loadPublicGenealogies();
loadMyGenealogies();
loadFamilyDetail();
}
return {
normalizeList: normalizeList,
pick: pick,
buildGenealogyBody: buildGenealogyBody,
getQueryParam: getQueryParam,
buildFamilyHeroTitle: buildFamilyHeroTitle,
buildFamilySummary: buildFamilySummary,
formatJoinMode: formatJoinMode,
buildJoinApplyBody: buildJoinApplyBody,
buildMyGenealogyRow: buildMyGenealogyRow,
init: init
};
});
-349
View File
@@ -1,349 +0,0 @@
(function (root, factory) {
// 字辈谱模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.GenerationPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.GenerationPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 通用列表响应可能是数组,也可能包在 rows、records、list 或 data 中。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 字辈文本进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
if (!documentRef) return null;
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
if (!documentRef) return [];
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) {
root.alert(message);
}
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-generation-page]');
return getQueryParam(search, 'id') ||
getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
'';
}
function toNumber(value, fallback) {
var number = Number(String(value || '').trim());
return Number.isFinite(number) && number > 0 ? number : fallback;
}
function toBoolean(value) {
return value === true || value === 'true' || value === '1' || value === 'on';
}
function buildGenerationBody(values) {
// GenerationPoemBody 必填 generationNo、generationText,其余字段给接口默认值。
var source = values || {};
var generationNo = toNumber(source.generationNo, 1);
return {
generationNo: generationNo,
generationText: String(source.generationText || '').trim(),
description: String(source.description || '').trim(),
sortOrder: toNumber(source.sortOrder, generationNo),
status: String(source.status || '').trim() || '0'
};
}
function buildGenerationBatchBody(values) {
// GenerationPoemBatchBody 只包含批量文本和是否遇到断代即停止。
var source = values || {};
return {
content: String(source.content || '').trim(),
stopMissingOldGeneration: toBoolean(source.stopMissingOldGeneration)
};
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
if (field.type === 'checkbox') {
values[field.name] = field.checked ? 'on' : '';
return;
}
values[field.name] = field.value;
});
return values;
}
function setBatchStatus(message) {
var statusNode = query('[data-generation-batch-status]');
if (statusNode) {
statusNode.textContent = message || '';
}
}
function buildGenerationRow(item, genealogyId) {
var poemId = pick(item, ['poemId', 'id'], '');
var generationNo = pick(item, ['generationNo', 'generation', 'sortOrder'], '1');
var text = pick(item, ['generationText', 'content', 'text'], '未设置');
var memberCount = pick(item, ['memberCount', 'personCount', 'count'], '0');
var description = pick(item, ['description', 'remark'], '暂无备注');
return '<div class="module-row generation-row" data-generation-id="' + escapeHtml(poemId) + '">' +
'<div><h3>第 ' + escapeHtml(generationNo) + ' 代:' + escapeHtml(text) + '</h3><p>' + escapeHtml(memberCount) + ' 人 · ' + escapeHtml(description) + '</p></div>' +
'<button class="pill" type="button" data-generation-edit="' + escapeHtml(poemId) + '" data-generation-no="' + escapeHtml(generationNo) + '" data-generation-text="' + escapeHtml(text) + '" data-genealogy-id="' + escapeHtml(genealogyId) + '">编辑</button>' +
'</div>';
}
function renderGenerations(items, genealogyId) {
var container = query('[data-generation-list]');
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">暂无字辈记录</div>';
return;
}
container.innerHTML = items.map(function (item) {
return buildGenerationRow(item, genealogyId);
}).join('');
}
function renderBatchPreview(data) {
var container = query('[data-generation-batch-preview]');
var items = normalizeList(data);
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">预览完成,接口未返回明细</div>';
return;
}
container.innerHTML = items.map(function (item) {
return buildGenerationRow(item, getCurrentGenealogyId());
}).join('');
}
async function loadGenerations() {
var api = getApi();
var container = query('[data-generation-list]');
var genealogyId = getCurrentGenealogyId();
if (!api || !container) return;
if (!genealogyId) {
container.innerHTML = '<div class="api-empty">请从具体家谱进入字辈谱</div>';
return;
}
container.classList.add('is-loading');
try {
renderGenerations(normalizeList(await api.generationPoems(genealogyId)), genealogyId);
} catch (error) {
showMessage(error.message || '字辈谱加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function createGeneration() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var generationNo;
var generationText;
var body;
if (!api || !genealogyId) {
showMessage('请从具体家谱进入字辈谱');
return;
}
generationNo = root.prompt('请输入世代序号', '');
if (!generationNo) return;
generationText = root.prompt('请输入字辈内容', '');
if (!generationText) return;
body = buildGenerationBody({
generationNo: generationNo,
generationText: generationText
});
try {
await api.createGenerationPoem(genealogyId, body);
showMessage('字辈已新增');
loadGenerations();
} catch (error) {
showMessage(error.message || '新增字辈失败');
}
}
async function editGeneration(button) {
var api = getApi();
var genealogyId = button.getAttribute('data-genealogy-id') || getCurrentGenealogyId();
var poemId = button.getAttribute('data-generation-edit');
var currentText = button.getAttribute('data-generation-text') || '';
var generationNo = button.getAttribute('data-generation-no') || '1';
var nextText;
if (!api || !genealogyId || !poemId) return;
nextText = root.prompt('请输入新的字辈', currentText);
if (!nextText) return;
button.classList.add('is-loading');
try {
await api.updateGenerationPoem(genealogyId, poemId, buildGenerationBody({
generationNo: generationNo,
generationText: nextText
}));
showMessage('字辈已更新');
loadGenerations();
} catch (error) {
showMessage(error.message || '更新字辈失败');
} finally {
button.classList.remove('is-loading');
}
}
async function submitGenerationBatch(action) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var form = query('[data-generation-batch-form]');
var body;
var result;
if (!api || !genealogyId || !form) {
showMessage('请从具体家谱进入字辈谱');
return;
}
body = buildGenerationBatchBody(getFormValues(form));
if (!body.content) {
showMessage('请填写批量字辈内容');
return;
}
try {
setBatchStatus(action === 'save' ? '批量保存中...' : '批量预览中...');
if (action === 'save') {
await api.saveGenerationPoemsBatch(genealogyId, body);
setBatchStatus('批量保存完成');
showMessage('批量字辈已保存');
loadGenerations();
return;
}
result = await api.previewGenerationPoems(genealogyId, body);
renderBatchPreview(result);
setBatchStatus('批量预览完成');
} catch (error) {
setBatchStatus('批量处理失败');
showMessage(error.message || '批量字辈处理失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var addButton = event.target.closest('[data-generation-add]');
var editButton = event.target.closest('[data-generation-edit]');
var batchButton = event.target.closest('[data-generation-batch-action]');
if (addButton) {
event.preventDefault();
createGeneration();
}
if (editButton) {
event.preventDefault();
editGeneration(editButton);
}
if (batchButton) {
event.preventDefault();
submitGenerationBatch(batchButton.getAttribute('data-generation-batch-action'));
}
});
}
function init() {
if (!documentRef) return;
bindActions();
loadGenerations();
}
return {
normalizeList: normalizeList,
buildGenerationBody: buildGenerationBody,
buildGenerationBatchBody: buildGenerationBatchBody,
buildGenerationRow: buildGenerationRow,
init: init
};
});
-244
View File
@@ -1,244 +0,0 @@
(function (root, factory) {
// 成长记录模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.GrowthPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.GrowthPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 成长记录列表兼容数组、rows、records、list、data。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 成长记录标题和内容进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-growth-page], [data-growth-edit-page]');
return getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
getQueryParam(search, 'id') ||
'';
}
function getCurrentRecordId() {
var search = root.location && root.location.search;
return getQueryParam(search, 'recordId') || '';
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toNumber(value, fallback) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : fallback;
}
function buildGrowthBody(values) {
var source = values || {};
return {
lineagePersonId: toNumber(source.lineagePersonId),
recordType: String(source.recordType || '').trim() || 'growth',
recordTitle: String(source.recordTitle || '').trim(),
recordContent: trimOrUndefined(source.recordContent),
recordDate: trimOrUndefined(source.recordDate),
remindTime: trimOrUndefined(source.remindTime),
mediaOssIds: trimOrUndefined(source.mediaOssIds),
sortOrder: toNumber(source.sortOrder, 1),
status: String(source.status || '').trim() || '0'
};
}
function getRecordId(item) {
return pick(item, ['recordId', 'id'], '');
}
function buildGrowthRow(item, genealogyId) {
var recordId = getRecordId(item);
var title = pick(item, ['recordTitle', 'title'], '未命名成长记录');
var type = pick(item, ['recordType', 'type'], 'growth');
var date = pick(item, ['recordDate', 'createTime'], '未记录日期');
return '<a class="module-row growth-row" href="profile-growth-edit.html?recordId=' + encodeURIComponent(recordId) + '&genealogyId=' + encodeURIComponent(genealogyId || '') + '">' +
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(type) + ' · ' + escapeHtml(date) + '</p></div>' +
'<span class="pill">编辑</span></a>';
}
function renderGrowthRecords(items, genealogyId) {
var container = query('[data-growth-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无成长记录</div>';
return;
}
container.innerHTML = list.map(function (item) {
return buildGrowthRow(item, genealogyId);
}).join('');
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
function fillGrowthForm(record) {
var source = record || {};
queryAll('[name]').forEach(function (field) {
var value = pick(source, [field.name], '');
if (value !== '') field.value = value;
});
}
async function loadGrowthRecords() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var recordId = getCurrentRecordId();
if (!api || !genealogyId) return;
try {
if (query('[data-growth-list]')) {
renderGrowthRecords(await api.growthRecords(genealogyId), genealogyId);
}
if (recordId && query('[data-growth-edit-page]')) {
fillGrowthForm(await api.growthRecordDetail(genealogyId, recordId));
}
} catch (error) {
showMessage(error.message || '成长记录加载失败');
}
}
async function submitGrowth(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var recordId = getCurrentRecordId();
var body = buildGrowthBody(getFormValues(form));
if (!api || !genealogyId) return;
if (!body.recordTitle) {
showMessage('请填写成长记录标题');
return;
}
try {
if (recordId) {
await api.updateGrowthRecord(genealogyId, recordId, body);
} else {
await api.createGrowthRecord(genealogyId, body);
}
showMessage('成长记录已保存');
root.location.href = 'profile-growth.html?genealogyId=' + encodeURIComponent(genealogyId);
} catch (error) {
showMessage(error.message || '保存成长记录失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-growth-form]');
if (!form) return;
event.preventDefault();
submitGrowth(form);
});
}
function init() {
if (!documentRef) return;
bindActions();
loadGrowthRecords();
}
return {
normalizeList: normalizeList,
buildGrowthBody: buildGrowthBody,
buildGrowthRow: buildGrowthRow,
init: init
};
});
-156
View File
@@ -1,156 +0,0 @@
(function (root, factory) {
// 帮助中心模块同时支持浏览器页面和 Node 单元测试。
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';
var documentRef = root.document;
function normalizeList(data) {
// 通用列表响应可能是数组,也可能包在 rows、records、list 或 data 中。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 帮助文章来自接口,进入 innerHTML 前先转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function buildHelpItem(item, index) {
// 列表接口字段未在文档中细化,这里兼容常见标题和摘要字段。
var id = pick(item, ['helpId', 'id', 'articleId'], '');
var title = pick(item, ['title', 'articleTitle', 'name'], '帮助文章');
var content = pick(item, ['summary', 'description', 'content', 'articleContent'], '暂无详细说明');
var hasFullContent = Boolean(pick(item, ['content', 'articleContent'], ''));
var open = index === 0 ? ' open' : '';
return '<details' + open + ' class="tilt-card" data-help-id="' + escapeHtml(id) + '" data-help-loaded="' + (hasFullContent ? 'true' : 'false') + '">' +
'<summary>' + escapeHtml(title) + '</summary>' +
'<p>' + escapeHtml(content) + '</p>' +
'</details>';
}
function renderHelpArticles(items) {
var container = query('[data-help-list]');
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">暂无帮助文章</div>';
return;
}
container.innerHTML = items.map(buildHelpItem).join('');
}
async function loadHelpArticles() {
var api = getApi();
var container = query('[data-help-list]');
if (!api || !container) return;
container.classList.add('is-loading');
try {
renderHelpArticles(normalizeList(await api.helpArticles()));
} catch (error) {
showMessage(error.message || '帮助文章加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function loadHelpDetail(details) {
var api = getApi();
var helpId = details && details.getAttribute('data-help-id');
var paragraph = details && query('p', details);
var content;
if (!api || !details || !helpId || details.getAttribute('data-help-loaded') === 'true') return;
details.classList.add('is-loading');
try {
content = await api.helpArticleDetail(helpId);
paragraph.textContent = pick(content, ['content', 'articleContent', 'summary', 'description'], paragraph.textContent);
details.setAttribute('data-help-loaded', 'true');
} catch (error) {
showMessage(error.message || '帮助详情加载失败');
} finally {
details.classList.remove('is-loading');
}
}
function bindDetailLoad() {
var container = query('[data-help-list]');
if (!container) return;
container.addEventListener('toggle', function (event) {
if (event.target && event.target.matches('details') && event.target.open) {
loadHelpDetail(event.target);
}
}, true);
}
function init() {
if (!documentRef) return;
bindDetailLoad();
loadHelpArticles();
}
return {
normalizeList: normalizeList,
buildHelpItem: buildHelpItem,
renderHelpArticles: renderHelpArticles,
init: init
};
});
-271
View File
@@ -1,271 +0,0 @@
(function (root, factory) {
// 加入申请模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.JoinApplyPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.JoinApplyPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 列表响应可能直接是数组,也可能使用 rows、records、list 或 data 包裹。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 申请人信息来自接口,渲染前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var fromQuery = getQueryParam(search, 'id') || getQueryParam(search, 'genealogyId');
var fromPage = query('[data-genealogy-id]');
return fromQuery || (fromPage && fromPage.getAttribute('data-genealogy-id')) || '';
}
function formatApplyStatus(status) {
// 审核接口要求 status,按常见字典值做页面中文展示。
var map = {
0: '待审核',
1: '已通过',
2: '已拒绝'
};
return map[status] || status || '待审核';
}
function buildAuditBody(action, remark) {
var isReject = action === 'reject';
return {
status: isReject ? '2' : '1',
auditRemark: remark || (isReject ? '信息核验未通过' : '信息核验通过')
};
}
function buildPendingRow(item, genealogyId) {
var applyId = pick(item, ['applyId', 'id'], '');
var name = pick(item, ['applicantName', 'name', 'nickName'], '未命名申请人');
var relation = pick(item, ['relationDesc', 'applyReason', 'remark'], '关系说明待确认');
var time = pick(item, ['createTime', 'createdAt', 'applyTime'], '提交时间待确认');
return '<div class="module-row join-apply-row" data-join-apply-id="' + escapeHtml(applyId) + '">' +
'<div><h3>' + escapeHtml(name) + '申请加入家谱</h3><p>' + escapeHtml(time) + ' · ' + escapeHtml(relation) + '</p></div>' +
'<div class="bottom-actions">' +
'<button class="btn primary" type="button" data-join-audit="approve" data-genealogy-id="' + escapeHtml(genealogyId) + '" data-apply-id="' + escapeHtml(applyId) + '">同意</button>' +
'<button class="btn ghost" type="button" data-join-audit="reject" data-genealogy-id="' + escapeHtml(genealogyId) + '" data-apply-id="' + escapeHtml(applyId) + '">拒绝</button>' +
'</div></div>';
}
function buildMineRow(item) {
var applyId = pick(item, ['applyId', 'id'], '');
var genealogyName = pick(item, ['genealogyName', 'familyName', 'name'], '未命名家谱');
var status = formatApplyStatus(pick(item, ['status', 'auditStatus'], '0'));
var relation = pick(item, ['relationDesc', 'applyReason', 'auditRemark'], '关系说明待确认');
var action = status === '待审核'
? '<button class="btn ghost" type="button" data-join-cancel="' + escapeHtml(applyId) + '">撤销申请</button>'
: '<span class="pill">' + escapeHtml(status) + '</span>';
return '<div class="module-row join-apply-row" data-join-apply-id="' + escapeHtml(applyId) + '">' +
'<div><h3>' + escapeHtml(genealogyName) + '</h3><p>' + escapeHtml(status) + ' · ' + escapeHtml(relation) + '</p></div>' +
action +
'</div>';
}
function renderPending(items, genealogyId) {
var container = query('[data-join-apply-list="pending"]');
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">暂无待审核申请</div>';
return;
}
container.innerHTML = items.map(function (item) {
return buildPendingRow(item, genealogyId);
}).join('');
}
function renderMine(items) {
var container = query('[data-join-apply-list="mine"]');
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">暂无加入申请记录</div>';
return;
}
container.innerHTML = items.map(buildMineRow).join('');
}
async function loadPending() {
var api = getApi();
var container = query('[data-join-apply-list="pending"]');
var genealogyId = getCurrentGenealogyId();
if (!api || !container) return;
if (!genealogyId) {
container.innerHTML = '<div class="api-empty">请从具体家谱进入审核页</div>';
return;
}
container.classList.add('is-loading');
try {
renderPending(normalizeList(await api.pendingJoinApplies(genealogyId)), genealogyId);
} catch (error) {
showMessage(error.message || '待审核申请加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function loadMine() {
var api = getApi();
var container = query('[data-join-apply-list="mine"]');
if (!api || !container) return;
container.classList.add('is-loading');
try {
renderMine(normalizeList(await api.myJoinApplies()));
} catch (error) {
showMessage(error.message || '加入申请记录加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function audit(button) {
var api = getApi();
var action = button.getAttribute('data-join-audit');
var genealogyId = button.getAttribute('data-genealogy-id');
var applyId = button.getAttribute('data-apply-id');
var remark = action === 'reject' ? root.prompt('请输入拒绝原因', '') : '';
if (!api || !genealogyId || !applyId) return;
if (action === 'reject' && remark === null) return;
button.classList.add('is-loading');
try {
await api.auditJoinApply(genealogyId, applyId, buildAuditBody(action, remark));
showMessage(action === 'reject' ? '已拒绝申请' : '已同意申请');
loadPending();
} catch (error) {
showMessage(error.message || '审核申请失败');
} finally {
button.classList.remove('is-loading');
}
}
async function cancel(button) {
var api = getApi();
var applyId = button.getAttribute('data-join-cancel');
if (!api || !applyId) return;
if (root.confirm && !root.confirm('确认撤销当前加入申请?')) return;
button.classList.add('is-loading');
try {
await api.cancelJoinApply(applyId);
showMessage('已撤销加入申请');
loadMine();
} catch (error) {
showMessage(error.message || '撤销申请失败');
} finally {
button.classList.remove('is-loading');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var auditButton = event.target.closest('[data-join-audit]');
var cancelButton = event.target.closest('[data-join-cancel]');
if (auditButton) {
event.preventDefault();
audit(auditButton);
}
if (cancelButton) {
event.preventDefault();
cancel(cancelButton);
}
});
}
function init() {
if (!documentRef) return;
bindActions();
loadPending();
loadMine();
}
return {
normalizeList: normalizeList,
formatApplyStatus: formatApplyStatus,
buildAuditBody: buildAuditBody,
buildPendingRow: buildPendingRow,
buildMineRow: buildMineRow,
init: init
};
});
-399
View File
@@ -1,399 +0,0 @@
(function (root, factory) {
// 世系模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.LineagePages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.LineagePages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
var selectedPersonId = '';
function normalizeList(data) {
// 通用列表响应可能是数组,也可能包在 rows、records、list、data 中。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 世系人物信息进入 innerHTML 前统一转义,避免姓名和简介污染页面结构。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-lineage-page], [data-family-home-page]');
return getQueryParam(search, 'id') ||
getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
'';
}
function toNumber(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : undefined;
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function formatSex(value) {
var text = String(value === undefined || value === null ? '' : value);
if (text === '0' || text === '男') return '男';
if (text === '1' || text === '女') return '女';
return '未知';
}
function getPersonId(item) {
return pick(item, ['personId', 'lineagePersonId', 'id'], '');
}
function buildLineagePersonBody(values) {
var source = values || {};
return {
appUserId: toNumber(source.appUserId),
personNo: trimOrUndefined(source.personNo),
personName: String(source.personName || '').trim(),
aliasName: trimOrUndefined(source.aliasName),
sex: String(source.sex || '').trim() || '0',
generationNo: toNumber(source.generationNo),
generationName: trimOrUndefined(source.generationName),
avatarOssId: toNumber(source.avatarOssId),
birthDate: trimOrUndefined(source.birthDate),
deathDate: trimOrUndefined(source.deathDate),
introduction: trimOrUndefined(source.introduction)
};
}
function buildPersonRow(item) {
var personId = getPersonId(item);
var name = pick(item, ['personName', 'name', 'realName'], '未命名成员');
var generationNo = pick(item, ['generationNo', 'generation', 'generationIndex'], '-');
var generationName = pick(item, ['generationName', 'generationText'], '未设置');
var sex = formatSex(pick(item, ['sex', 'gender'], ''));
var bound = pick(item, ['boundAccount', 'bindAccount', 'hasAccount', 'appUserId'], '') ? '已绑定账号' : '未绑定账号';
return '<button class="module-row lineage-row" type="button" data-lineage-person="' + escapeHtml(personId) + '">' +
'<div><h3>' + escapeHtml(name) + '</h3><p>第 ' + escapeHtml(generationNo) + ' 世 · ' + escapeHtml(generationName) + '字辈 · ' + escapeHtml(sex) + ' · ' + escapeHtml(bound) + '</p></div>' +
'<span class="pill">查看资料</span>' +
'</button>';
}
function getChildren(item) {
var source = item || {};
return normalizeList(source.children || source.childList || source.descendants);
}
function buildTreeNode(item) {
var personId = getPersonId(item);
var name = pick(item, ['personName', 'name', 'realName'], '未命名成员');
var generationNo = pick(item, ['generationNo', 'generation', 'generationIndex'], '-');
var children = getChildren(item);
var html = '<li><button type="button" class="lineage-node" data-lineage-person="' + escapeHtml(personId) + '">' +
'<strong>' + escapeHtml(name) + '</strong><span>第 ' + escapeHtml(generationNo) + ' 世</span></button>';
if (children.length) {
html += '<ul>' + children.map(buildTreeNode).join('') + '</ul>';
}
return html + '</li>';
}
function buildHomeSummary(detail, rootCount) {
// 家谱主页摘要优先展示后端家谱信息,世系根节点数量来自树接口。
var source = detail || {};
var genealogyNo = pick(source, ['genealogyNo', 'genealogyCode', 'code'], '未设置编号');
var hallName = pick(source, ['hallName', 'ancestralHall', 'tanghao'], '未设置');
var memberCount = pick(source, ['memberCount', 'personCount', 'members'], '0');
return genealogyNo + ' · 堂号 ' + hallName + ' · 共 ' + memberCount + ' 人 · ' + rootCount + ' 个世系根节点';
}
function renderTree(items) {
var container = query('[data-lineage-tree], [data-lineage-home-tree]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无世系树数据</div>';
return;
}
container.innerHTML = '<ul class="lineage-tree">' + list.map(buildTreeNode).join('') + '</ul>';
}
function renderPersons(items) {
var container = query('[data-lineage-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无世系人物</div>';
return;
}
container.innerHTML = list.map(buildPersonRow).join('');
}
function renderHomeDetail(detail, rootCount) {
var title = query('[data-family-home-title]');
var summary = query('[data-family-home-summary]');
var source = detail || {};
if (title) title.textContent = pick(source, ['genealogyName', 'name'], title.textContent || '家谱主页');
if (summary) summary.textContent = buildHomeSummary(source, rootCount);
}
function renderDetail(item) {
var container = query('[data-lineage-detail]');
var source = item || {};
var name = pick(source, ['personName', 'name', 'realName'], '未命名成员');
var intro = pick(source, ['introduction', 'intro', 'remark'], '暂无简介');
if (!container) return;
container.innerHTML = '<div class="form-like">' +
'<p><span>姓名</span><b>' + escapeHtml(name) + '</b><em>' + escapeHtml(formatSex(pick(source, ['sex', 'gender'], ''))) + '</em></p>' +
'<p><span>字辈</span><b>' + escapeHtml(pick(source, ['generationName', 'generationText'], '未设置')) + '</b><em>第 ' + escapeHtml(pick(source, ['generationNo', 'generation'], '-')) + ' 世</em></p>' +
'<p><span>生卒</span><b>' + escapeHtml(pick(source, ['birthDate'], '未填写')) + '</b><em>' + escapeHtml(pick(source, ['deathDate'], '健在或未填')) + '</em></p>' +
'<p><span>简介</span><b>' + escapeHtml(intro) + '</b><em>世系资料</em></p>' +
'</div>';
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
async function loadLineage() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var keyword = query('[data-lineage-keyword]');
var homeSummary = query('[data-family-home-summary]');
var treeData;
var treeList;
var queryData = {
pageNum: 1,
pageSize: 50
};
if (!api) return;
if (!genealogyId) {
renderTree([]);
renderPersons([]);
showMessage('请从具体家谱进入世系图');
return;
}
if (keyword && keyword.value.trim()) queryData.keyword = keyword.value.trim();
try {
treeData = await api.lineageTree(genealogyId);
treeList = normalizeList(treeData);
renderTree(treeData);
if (homeSummary && api.genealogyDetail) {
renderHomeDetail(await api.genealogyDetail(genealogyId), treeList.length);
}
if (query('[data-lineage-list]')) {
renderPersons(await api.lineagePersonsPage(genealogyId, queryData));
}
} catch (error) {
showMessage(error.message || '世系数据加载失败');
}
}
async function loadPersonDetail(personId) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
if (!api || !genealogyId || !personId) return;
selectedPersonId = personId;
queryAll('[data-lineage-person]').forEach(function (item) {
item.classList.toggle('is-selected', item.getAttribute('data-lineage-person') === String(personId));
});
try {
renderDetail(await api.lineagePersonDetail(genealogyId, personId));
} catch (error) {
showMessage(error.message || '人物详情加载失败');
}
}
async function submitPerson(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var body = buildLineagePersonBody(getFormValues(form));
if (!api || !genealogyId) {
showMessage('请从具体家谱进入世系图');
return;
}
if (!body.personName) {
showMessage('请填写成员姓名');
return;
}
try {
await api.createLineagePerson(genealogyId, body);
form.reset();
showMessage('世系人物已新增');
loadLineage();
} catch (error) {
showMessage(error.message || '新增世系人物失败');
}
}
async function createRelation(relationType) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var relationNames = {
parents: '父母',
spouses: '配偶',
children: '子女',
siblings: '兄弟姐妹'
};
var personName;
if (!api || !genealogyId || !selectedPersonId) {
showMessage('请先在成员列表中选择一个成员');
return;
}
personName = root.prompt('请输入要添加的' + (relationNames[relationType] || '亲属') + '姓名', '');
if (!personName) return;
try {
await api.addLineageRelation(genealogyId, selectedPersonId, relationType, buildLineagePersonBody({
personName: personName
}));
showMessage('亲属关系已新增');
loadLineage();
} catch (error) {
showMessage(error.message || '新增亲属关系失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-lineage-form]');
if (!form) return;
event.preventDefault();
submitPerson(form);
});
documentRef.addEventListener('click', function (event) {
var personButton = event.target.closest('[data-lineage-person]');
var searchButton = event.target.closest('[data-lineage-search]');
var relationButton = event.target.closest('[data-lineage-relation]');
if (personButton) {
event.preventDefault();
loadPersonDetail(personButton.getAttribute('data-lineage-person'));
}
if (searchButton) {
event.preventDefault();
loadLineage();
}
if (relationButton) {
event.preventDefault();
createRelation(relationButton.getAttribute('data-lineage-relation'));
}
});
}
function init() {
if (!documentRef) return;
bindActions();
loadLineage();
}
return {
normalizeList: normalizeList,
buildLineagePersonBody: buildLineagePersonBody,
formatSex: formatSex,
buildPersonRow: buildPersonRow,
buildTreeNode: buildTreeNode,
buildHomeSummary: buildHomeSummary,
init: init
};
});
-364
View File
@@ -1,364 +0,0 @@
(function (root, factory) {
// 成员与家族管理模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.MemberAdminPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.MemberAdminPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 成员接口返回可能是数组,也可能包在 rows、records、list、data 中。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 成员姓名、关系名进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-member-admin-page], [data-admin-permission-page], [data-family-invite-page], [data-family-share-page]');
return getQueryParam(search, 'id') ||
getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
'';
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toNumber(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : undefined;
}
function formatRole(roleType) {
var role = String(roleType || '').trim();
var roleMap = {
owner: '创建者',
creator: '创建者',
admin: '管理员',
manager: '管理员',
member: '普通成员'
};
return roleMap[role] || role || '普通成员';
}
function buildMemberUpdateBody(values) {
var source = values || {};
return {
memberName: trimOrUndefined(source.memberName),
relationName: trimOrUndefined(source.relationName),
roleType: String(source.roleType || '').trim() || 'member',
lineagePersonId: toNumber(source.lineagePersonId)
};
}
function buildOverviewSummary(detail) {
var source = detail || {};
var name = pick(source, ['genealogyName', 'name'], '当前家谱');
var memberCount = pick(source, ['memberCount', 'members', 'personCount'], '0');
var articleCount = pick(source, ['articleCount', 'articles'], '0');
var albumCount = pick(source, ['albumCount', 'albums'], '0');
return name + ' · ' + memberCount + ' 位成员 · ' + articleCount + ' 篇谱文 · ' + albumCount + ' 个相册';
}
function getMemberId(item) {
return pick(item, ['memberId', 'id', 'genealogyMemberId'], '');
}
function buildMemberRow(item) {
var memberId = getMemberId(item);
var name = pick(item, ['memberName', 'nickName', 'name'], '未命名成员');
var relation = pick(item, ['relationName', 'relation'], '族亲');
var roleType = pick(item, ['roleType', 'role'], 'member');
var bindName = pick(item, ['lineagePersonName', 'personName'], '未绑定');
return '<div class="module-row member-admin-row" data-member-id="' + escapeHtml(memberId) + '">' +
'<div><h3>' + escapeHtml(name) + '</h3><p>' + escapeHtml(relation) + ' · ' + escapeHtml(formatRole(roleType)) + ' · 绑定:' + escapeHtml(bindName) + '</p></div>' +
'<div class="row-actions"><button class="pill" type="button" data-member-edit="' + escapeHtml(memberId) + '" data-member-name="' + escapeHtml(name) + '" data-member-relation="' + escapeHtml(relation) + '" data-member-role="' + escapeHtml(roleType) + '">权限</button><button class="pill is-danger" type="button" data-member-remove="' + escapeHtml(memberId) + '">移除</button></div>' +
'</div>';
}
function buildOption(item) {
var memberId = getMemberId(item);
var name = pick(item, ['memberName', 'label', 'name'], '未命名成员');
return '<option value="' + escapeHtml(memberId) + '">' + escapeHtml(name) + '</option>';
}
function buildInviteLink(currentUrl, genealogyId) {
var url = new URL(currentUrl || 'http://localhost/profile-invite.html');
url.pathname = url.pathname.replace(/[^/]*$/, 'join-genealogy.html');
url.search = '?id=' + encodeURIComponent(genealogyId || '');
url.hash = '';
return url.toString();
}
function renderOverview(detail) {
var summary = query('[data-member-overview]');
var title = query('[data-member-title]');
if (title) title.textContent = pick(detail, ['genealogyName', 'name'], title.textContent || '家族管理');
if (summary) summary.textContent = buildOverviewSummary(detail);
}
function renderMembers(items) {
var container = query('[data-member-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无家谱成员</div>';
return;
}
container.innerHTML = list.map(buildMemberRow).join('');
}
function renderMemberOptions(items) {
var select = query('[data-member-select]');
var list = normalizeList(items);
if (!select) return;
select.innerHTML = '<option value="">请选择成员</option>' + list.map(buildOption).join('');
}
function renderInvite(detail, genealogyId) {
var code = query('[data-invite-code]');
var link = query('[data-invite-link]');
var currentUrl = root.location && root.location.href;
var inviteCode = pick(detail, ['inviteCode', 'genealogyNo', 'genealogyCode'], genealogyId || '未获取');
if (code) code.textContent = inviteCode;
if (link) link.textContent = buildInviteLink(currentUrl, genealogyId);
}
async function loadMemberAdmin() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var detail;
if (!api) return;
if (!genealogyId) {
showMessage('请从具体家谱进入管理页面');
return;
}
try {
detail = await api.genealogyOverview(genealogyId);
renderOverview(detail);
renderInvite(detail, genealogyId);
if (query('[data-member-list]')) {
renderMembers(await api.genealogyMembers(genealogyId));
}
if (query('[data-member-select]')) {
renderMemberOptions(await api.genealogyMemberOptions(genealogyId));
}
} catch (error) {
showMessage(error.message || '家族管理数据加载失败');
}
}
async function updateMemberFromPrompt(button) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var memberId = button.getAttribute('data-member-edit');
var roleType = root.prompt('请输入角色类型:owner/admin/member', button.getAttribute('data-member-role') || 'member');
if (!api || !genealogyId || !memberId || !roleType) return;
try {
await api.updateGenealogyMember(genealogyId, memberId, buildMemberUpdateBody({
memberName: button.getAttribute('data-member-name'),
relationName: button.getAttribute('data-member-relation'),
roleType: roleType
}));
showMessage('成员权限已更新');
loadMemberAdmin();
} catch (error) {
showMessage(error.message || '更新成员失败');
}
}
async function removeMember(memberId) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
if (!api || !genealogyId || !memberId) return;
if (!root.confirm('确定移除该成员?')) return;
try {
await api.removeGenealogyMember(genealogyId, memberId);
showMessage('成员已移除');
loadMemberAdmin();
} catch (error) {
showMessage(error.message || '移除成员失败');
}
}
async function submitPermission(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var memberId = query('[data-member-select]', form).value;
var roleType = query('[name="roleType"]', form).value;
if (!api || !genealogyId || !memberId) {
showMessage('请选择要设置的成员');
return;
}
try {
await api.updateGenealogyMember(genealogyId, memberId, buildMemberUpdateBody({
roleType: roleType
}));
showMessage('管理员权限已保存');
} catch (error) {
showMessage(error.message || '保存权限失败');
}
}
async function transferOwner() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var select = query('[data-member-select]');
if (!api || !genealogyId || !select || !select.value) {
showMessage('请选择要转让的成员');
return;
}
if (!root.confirm('确定转让家谱所有者?')) return;
try {
await api.transferGenealogyOwner(genealogyId, {
targetMemberId: Number(select.value)
});
showMessage('家谱所有者已转让');
} catch (error) {
showMessage(error.message || '转让失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var editButton = event.target.closest('[data-member-edit]');
var removeButton = event.target.closest('[data-member-remove]');
var transferButton = event.target.closest('[data-owner-transfer]');
if (editButton) {
event.preventDefault();
updateMemberFromPrompt(editButton);
}
if (removeButton) {
event.preventDefault();
removeMember(removeButton.getAttribute('data-member-remove'));
}
if (transferButton) {
event.preventDefault();
transferOwner();
}
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-permission-form]');
if (!form) return;
event.preventDefault();
submitPermission(form);
});
}
function init() {
if (!documentRef) return;
bindActions();
loadMemberAdmin();
}
return {
normalizeList: normalizeList,
buildMemberUpdateBody: buildMemberUpdateBody,
buildOverviewSummary: buildOverviewSummary,
buildMemberRow: buildMemberRow,
buildInviteLink: buildInviteLink,
init: init
};
});
-246
View File
@@ -1,246 +0,0 @@
(function (root, factory) {
// 备忘录模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.MemoPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.MemoPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 备忘录列表兼容数组、rows、records、list、data。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 备忘标题和内容进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId() {
var search = root.location && root.location.search;
var page = query('[data-memo-page], [data-memo-edit-page]');
return getQueryParam(search, 'genealogyId') ||
(page && page.getAttribute('data-genealogy-id')) ||
getQueryParam(search, 'id') ||
'';
}
function getCurrentMemoId() {
var search = root.location && root.location.search;
return getQueryParam(search, 'memoId') || '';
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toNumber(value, fallback) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : fallback;
}
function formatCompleted(value) {
return String(value || '0') === '1' ? '已完成' : '未完成';
}
function buildMemoBody(values) {
var source = values || {};
return {
memoTitle: String(source.memoTitle || '').trim(),
memoContent: trimOrUndefined(source.memoContent),
remindTime: trimOrUndefined(source.remindTime),
completed: String(source.completed || '').trim() || '0',
mediaOssIds: trimOrUndefined(source.mediaOssIds),
sortOrder: toNumber(source.sortOrder, 1),
status: String(source.status || '').trim() || '0'
};
}
function getMemoId(item) {
return pick(item, ['memoId', 'id'], '');
}
function buildMemoRow(item, genealogyId) {
var memoId = getMemoId(item);
var title = pick(item, ['memoTitle', 'title'], '未命名备忘');
var completed = formatCompleted(pick(item, ['completed'], '0'));
var remindTime = pick(item, ['remindTime', 'createTime'], '未设置提醒');
return '<a class="module-row memo-row" href="profile-memo-edit.html?memoId=' + encodeURIComponent(memoId) + '&genealogyId=' + encodeURIComponent(genealogyId || '') + '">' +
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(completed) + ' · ' + escapeHtml(remindTime) + '</p></div>' +
'<span class="pill">编辑</span></a>';
}
function renderMemos(items, genealogyId) {
var container = query('[data-memo-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无备忘录</div>';
return;
}
container.innerHTML = list.map(function (item) {
return buildMemoRow(item, genealogyId);
}).join('');
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
function fillMemoForm(memo) {
var source = memo || {};
queryAll('[name]').forEach(function (field) {
var value = pick(source, [field.name], '');
if (value !== '') field.value = value;
});
}
async function loadMemos() {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var memoId = getCurrentMemoId();
if (!api || !genealogyId) return;
try {
if (query('[data-memo-list]')) {
renderMemos(await api.memos(genealogyId), genealogyId);
}
if (memoId && query('[data-memo-edit-page]')) {
fillMemoForm(await api.memoDetail(genealogyId, memoId));
}
} catch (error) {
showMessage(error.message || '备忘录加载失败');
}
}
async function submitMemo(form) {
var api = getApi();
var genealogyId = getCurrentGenealogyId();
var memoId = getCurrentMemoId();
var body = buildMemoBody(getFormValues(form));
if (!api || !genealogyId) return;
if (!body.memoTitle) {
showMessage('请填写备忘标题');
return;
}
try {
if (memoId) {
await api.updateMemo(genealogyId, memoId, body);
} else {
await api.createMemo(genealogyId, body);
}
showMessage('备忘录已保存');
root.location.href = 'profile-memo.html?genealogyId=' + encodeURIComponent(genealogyId);
} catch (error) {
showMessage(error.message || '保存备忘录失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-memo-form]');
if (!form) return;
event.preventDefault();
submitMemo(form);
});
}
function init() {
if (!documentRef) return;
bindActions();
loadMemos();
}
return {
normalizeList: normalizeList,
buildMemoBody: buildMemoBody,
buildMemoRow: buildMemoRow,
init: init
};
});
-198
View File
@@ -1,198 +0,0 @@
(function (root, factory) {
// 消息模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.NotificationPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.NotificationPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 列表响应在不同接口中可能使用 rows、records、list 或 data 包裹。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 通知标题和正文来自接口,进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function isRead(item) {
var readStatus = pick(item, ['readStatus', 'status'], '');
var readFlag = pick(item, ['readFlag', 'isRead', 'read'], '');
if (readFlag === true || readFlag === 'true' || readFlag === 1 || readFlag === '1') return true;
return readStatus === '1' || readStatus === 1 || readStatus === '已读';
}
function formatNotificationStatus(item) {
return isRead(item) ? '已读' : '未读';
}
function buildNotificationRow(item) {
// 通知字段名按接口常见命名做兼容,页面只关心标题、内容、时间和已读状态。
var id = pick(item, ['notificationId', 'id'], '');
var title = pick(item, ['title', 'noticeTitle', 'messageTitle'], '系统通知');
var content = pick(item, ['content', 'message', 'noticeContent', 'summary'], '暂无通知内容');
var time = pick(item, ['createTime', 'createdAt', 'time'], '');
var status = formatNotificationStatus(item);
var className = isRead(item)
? 'module-row notification-row'
: 'module-row notification-row is-unread';
var action = isRead(item)
? '<span class="pill">已读</span>'
: '<button class="btn ghost" type="button" data-notification-read="' + escapeHtml(id) + '">标记已读</button>';
return '<div class="' + className + '" data-notification-id="' + escapeHtml(id) + '">' +
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(time) + ' · ' + escapeHtml(content) + '</p></div>' +
action +
'</div>';
}
function renderNotifications(items) {
var container = query('[data-notification-list]');
if (!container) return;
if (!items.length) {
container.innerHTML = '<div class="api-empty">暂无消息通知</div>';
return;
}
container.innerHTML = items.map(buildNotificationRow).join('');
}
async function loadNotifications() {
var api = getApi();
var container = query('[data-notification-list]');
if (!api || !container) return;
container.classList.add('is-loading');
try {
renderNotifications(normalizeList(await api.notifications()));
} catch (error) {
showMessage(error.message || '消息通知加载失败');
} finally {
container.classList.remove('is-loading');
}
}
async function markRead(notificationId) {
var api = getApi();
if (!api || !notificationId) return;
try {
await api.markNotificationRead(notificationId);
showMessage('已标记为已读');
loadNotifications();
} catch (error) {
showMessage(error.message || '标记已读失败');
}
}
async function markAllRead() {
var api = getApi();
if (!api) return;
try {
await api.markAllNotificationsRead();
showMessage('已全部标记为已读');
loadNotifications();
} catch (error) {
showMessage(error.message || '全部标记已读失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var readButton = event.target.closest('[data-notification-read]');
var readAllButton = event.target.closest('[data-notification-read-all]');
var refreshButton = event.target.closest('[data-notification-refresh]');
if (readButton) {
event.preventDefault();
markRead(readButton.getAttribute('data-notification-read'));
}
if (readAllButton) {
event.preventDefault();
markAllRead();
}
if (refreshButton) {
event.preventDefault();
loadNotifications();
}
});
}
function init() {
if (!documentRef) return;
bindActions();
loadNotifications();
}
return {
normalizeList: normalizeList,
formatNotificationStatus: formatNotificationStatus,
buildNotificationRow: buildNotificationRow,
renderNotifications: renderNotifications,
init: init
};
});
+112
View File
@@ -0,0 +1,112 @@
(function (root, factory) {
// 待开发页面状态模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.PageAvailability = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.PageAvailability.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildBanner(message) {
return '<section class="feature-status-banner" role="status">' +
'<strong>功能开发中</strong>' +
'<p>' + escapeHtml(message || '该功能正在开发中,当前页面保留设计预览。') + '</p>' +
'</section>';
}
function isPendingPage(body) {
return Boolean(body && body.getAttribute('data-feature-status') === 'pending');
}
function getMessage(body) {
return body && body.getAttribute('data-feature-message') || '该功能正在开发中,当前页面保留设计预览。';
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
if (root.alert) root.alert(message);
}
function containsTarget(container, target) {
return Boolean(container && target && container.contains(target));
}
function shouldBlockLink(link) {
var href;
if (!link || link.getAttribute('data-feature-link') === 'available') return false;
href = link.getAttribute('href') || '';
return Boolean(href) && href.charAt(0) !== '#' && !/^(?:https?:|mailto:|tel:)/i.test(href);
}
function init() {
var body;
var main;
var target;
var message;
if (!documentRef) return;
body = documentRef.body;
if (!isPendingPage(body)) return;
main = documentRef.querySelector('main');
target = main && (main.querySelector('.module-main') || main);
message = getMessage(body);
if (target && !target.querySelector('.feature-status-banner')) {
target.insertAdjacentHTML('afterbegin', buildBanner(message));
}
documentRef.addEventListener('submit', function (event) {
if (!containsTarget(main, event.target)) return;
event.preventDefault();
showMessage(message);
});
documentRef.addEventListener('click', function (event) {
var source = event.target;
var button = source && source.closest && source.closest('button');
var link = source && source.closest && source.closest('a');
if (button && containsTarget(main, button) && button.type !== 'reset') {
event.preventDefault();
showMessage(message);
return;
}
// 侧栏用于浏览已设计页面,主内容区的业务入口必须显式声明可用后才能跳转。
if (!link || !containsTarget(main, link) || link.closest('.module-nav') || !shouldBlockLink(link)) return;
event.preventDefault();
showMessage(message);
});
}
return {
buildBanner: buildBanner,
isPendingPage: isPendingPage,
shouldBlockLink: shouldBlockLink,
init: init
};
});
-220
View File
@@ -1,220 +0,0 @@
(function (root, factory) {
// 推广内容模块同时服务应用介绍、下载页和公告详情页。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.PromotionPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.PromotionPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var documentRef = root.document;
function normalizeList(data) {
// 推广接口使用通用列表响应,兼容后端常见数组包裹字段。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 标题、摘要、链接等普通文本进入 HTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getPromotionId(item) {
return String(pick(item, ['promotionId', 'id', 'articleId'], ''));
}
function getTitle(item) {
return pick(item, ['promotionTitle', 'title', 'name'], '推广内容');
}
function getSummary(item) {
return pick(item, ['summary', 'description', 'promotionDesc', 'remark'], '了解平台最新内容与应用服务。');
}
function getContent(item) {
return pick(item, ['promotionContent', 'content', 'articleContent'], '<p>暂无详细内容</p>');
}
function getTime(item) {
return pick(item, ['publishTime', 'createTime', 'updateTime'], '');
}
function getLink(item, fallback) {
return pick(item, ['linkUrl', 'url', 'targetUrl'], fallback || 'notice-detail.html?id=' + encodeURIComponent(getPromotionId(item)));
}
function getCover(item) {
return pick(item, ['coverUrl', 'coverImage', 'imageUrl', 'bannerUrl'], 'public/images/app-home.png');
}
function buildPromotionCard(item) {
var title = getTitle(item);
var summary = getSummary(item);
var time = getTime(item);
var link = getLink(item);
return '<a class="promotion-card" href="' + escapeHtml(link) + '" data-promotion-id="' + escapeHtml(getPromotionId(item)) + '">' +
'<img src="' + escapeHtml(getCover(item)) + '" alt="' + escapeHtml(title) + '" />' +
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(summary) + '</p><span>' + escapeHtml(time) + '</span></div></a>';
}
function buildDownloadCard(item) {
var title = getTitle(item);
var summary = getSummary(item);
var link = getLink(item, 'download.html');
return '<a class="card soft download-card tilt-card" href="' + escapeHtml(link) + '" data-promotion-id="' + escapeHtml(getPromotionId(item)) + '">' +
'<h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(summary) + '</p></a>';
}
function pickPromotion(list, promotionId) {
var items = normalizeList(list);
var id = String(promotionId || '');
var index;
if (!items.length) return null;
if (!id) return items[0];
for (index = 0; index < items.length; index += 1) {
if (getPromotionId(items[index]) === id) return items[index];
}
return items[0];
}
function buildNoticeDetail(item) {
return {
title: getTitle(item),
summary: getSummary(item),
content: getContent(item),
time: getTime(item)
};
}
function renderPromotionCards(items) {
var container = query('[data-promotion-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无推广内容</div>';
return;
}
container.innerHTML = list.map(buildPromotionCard).join('');
}
function renderDownloadCards(items) {
var container = query('[data-promotion-download-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无下载推广内容</div>';
return;
}
container.innerHTML = list.map(buildDownloadCard).join('');
}
function renderNoticeDetail(items) {
var search = root.location && root.location.search;
var selected = pickPromotion(items, getQueryParam(search, 'id'));
var detail = selected ? buildNoticeDetail(selected) : null;
var title = query('[data-promotion-title]');
var summary = query('[data-promotion-summary]');
var content = query('[data-promotion-content]');
var time = query('[data-promotion-time]');
if (!detail) return;
if (title) title.textContent = detail.title;
if (summary) summary.textContent = detail.summary;
if (content) content.innerHTML = detail.content;
if (time) time.textContent = detail.time || '发布时间待定';
}
async function loadPromotions() {
var api = getApi();
var data;
if (!api) return;
try {
data = await api.promotions();
renderPromotionCards(data);
renderDownloadCards(data);
renderNoticeDetail(data);
} catch (error) {
showMessage(error.message || '推广内容加载失败');
}
}
function init() {
if (!documentRef || !query('[data-promotion-page]')) return;
loadPromotions();
}
return {
normalizeList: normalizeList,
buildPromotionCard: buildPromotionCard,
buildDownloadCard: buildDownloadCard,
pickPromotion: pickPromotion,
buildNoticeDetail: buildNoticeDetail,
init: init
};
});
+71 -2
View File
@@ -15,6 +15,14 @@
'use strict';
var documentRef = root.document;
var CAPTCHA_SCENES = {
password: 'WEB_H5_CHANGE_PASSWORD',
phone: 'WEB_H5_CHANGE_PHONE'
};
function getCaptchaScene(formType) {
return CAPTCHA_SCENES[formType] || '';
}
function hashPassword(value) {
// 接口文档要求密码提交 32 位 MD5,测试环境可传入 hashFn 替代。
@@ -35,7 +43,8 @@
return {
oldPassword: hash(String(source.oldPassword || '').trim()),
newPassword: hash(String(source.newPassword || '').trim())
newPassword: hash(String(source.newPassword || '').trim()),
validToken: trimOrUndefined(source.validToken)
};
}
@@ -67,6 +76,23 @@
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
async function ensureCaptcha(form, formType, subject) {
// 改密和换绑必须先取得与当前业务场景绑定的验证码票据。
var captcha = root.CaptchaPages;
var sceneCode = getCaptchaScene(formType);
if (!sceneCode) return true;
if (!captcha || !captcha.ensureToken) {
showMessage('验证码组件未加载,请刷新后重试');
return false;
}
return Boolean(await captcha.ensureToken(form, {
sceneCode: sceneCode,
subject: subject || ''
}));
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
@@ -114,6 +140,9 @@
return;
}
if (!await ensureCaptcha(form, 'password')) return;
values = getFormValues(form);
try {
await api.changePassword(buildPasswordChangeBody(values));
setStatus(form, '密码已修改');
@@ -134,6 +163,9 @@
return;
}
if (!await ensureCaptcha(form, 'phone', body.newPhone)) return;
body = buildPhoneChangeBody(getFormValues(form));
try {
await api.changePhone(body);
setStatus(form, '手机号已换绑');
@@ -144,6 +176,36 @@
}
}
async function sendPhoneCode(button) {
// 换绑手机号使用独立短信场景,避免复用登录或找回密码票据。
var api = getApi();
var form = query('[data-security-form="phone"]');
var values;
if (!api || !form) return;
values = getFormValues(form);
if (!/^1[3-9]\d{9}$/.test(String(values.newPhone || '').trim())) {
showMessage('请输入正确的新手机号');
return;
}
if (!await ensureCaptcha(form, 'phone', values.newPhone)) return;
values = getFormValues(form);
button.disabled = true;
try {
await api.sendSmsCode({
phone: values.newPhone,
sceneCode: getCaptchaScene('phone'),
validToken: values.validToken || ''
});
showMessage('验证码已发送');
} catch (error) {
showMessage(error.message || '验证码发送失败');
} finally {
button.disabled = false;
}
}
async function submitDeactivate(form) {
var api = getApi();
var values = getFormValues(form);
@@ -176,7 +238,7 @@
try {
if (api) await api.logout();
} catch (error) {
// ApiClient.logout 会在请求失败时清理本地 token,页面仍应离开受保护区域。
// 退出请求失败时客户端仍会清理本地令牌,页面必须离开受保护区域。
} finally {
root.location.href = 'index.html';
}
@@ -199,8 +261,14 @@
});
documentRef.addEventListener('click', function (event) {
var codeButton = event.target.closest('[data-security-send-code]');
var button = event.target.closest('[data-security-logout]');
if (codeButton) {
event.preventDefault();
sendPhoneCode(codeButton);
return;
}
if (!button) return;
event.preventDefault();
logout();
@@ -217,6 +285,7 @@
buildPasswordChangeBody: buildPasswordChangeBody,
buildPhoneChangeBody: buildPhoneChangeBody,
buildDeactivateBody: buildDeactivateBody,
getCaptchaScene: getCaptchaScene,
isDangerConfirmed: isDangerConfirmed,
init: init
};
+29 -173
View File
@@ -1,5 +1,4 @@
(function (root, factory) {
// 公共上传模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
@@ -15,7 +14,25 @@
'use strict';
var documentRef = root.document;
var DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024;
function normalizeUploadResult(data) {
var source = data || {};
return {
ossId: String(source.ossId || ''),
fileName: source.fileName || source.originalName || '',
url: source.url || ''
};
}
function buildUploadStatus(fileName, ossId) {
return (fileName || '文件') + ' 上传完成,OSS ID' + ossId;
}
function getUploadMode() {
// 本期只保留头像单文件上传,不再根据文件大小切换分片流程。
return 'single';
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
@@ -31,187 +48,30 @@
return;
}
root.alert(message);
if (root.alert) root.alert(message);
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function normalizeUploadResult(data) {
// FileUploadResult 字段可能因后端实现不同而略有差异,这里只收敛页面需要的值。
var source = data || {};
var ossId = pick(source, ['ossId', 'id', 'fileId', 'oss_id'], '');
var fileName = pick(source, ['fileName', 'originalName', 'name'], '');
var url = pick(source, ['url', 'fileUrl', 'previewUrl'], '');
return {
ossId: String(ossId || ''),
fileName: String(fileName || ''),
url: String(url || '')
};
}
function mergeOssIds(currentValue, nextValue, multiple) {
var next = String(nextValue || '').trim();
var current = String(currentValue || '').trim();
var values;
if (!next) return current;
if (!multiple) return next;
values = current ? current.split(',').map(function (item) {
return item.trim();
}).filter(Boolean) : [];
if (values.indexOf(next) === -1) values.push(next);
return values.join(',');
}
function buildUploadStatus(fileName, ossId) {
return (fileName || '文件') + ' 上传完成,OSS ID' + ossId;
}
function getUploadMode(fileSize, chunkSize) {
var size = Number(fileSize) || 0;
var limit = Number(chunkSize) || DEFAULT_CHUNK_SIZE;
return size > limit ? 'resumable' : 'single';
}
function buildResumableInitBody(file, fileMd5, chunkSize, options) {
var source = file || {};
var settings = options || {};
var size = Number(source.size) || 0;
var sizePerChunk = Number(chunkSize) || DEFAULT_CHUNK_SIZE;
var body = {
fileName: String(source.name || ''),
fileSize: size,
fileMd5: String(fileMd5 || ''),
contentType: String(source.type || 'application/octet-stream'),
chunkSize: sizePerChunk,
totalChunks: Math.ceil(size / sizePerChunk)
};
if (settings.bizType) body.bizType = settings.bizType;
if (settings.usageScene) body.usageScene = settings.usageScene;
return body;
}
function buildResumableCompleteBody(uploadId, fileMd5, fileSize) {
return {
uploadId: String(uploadId || ''),
fileMd5: String(fileMd5 || ''),
fileSize: Number(fileSize) || 0
};
}
function toBinaryString(buffer) {
var bytes = new Uint8Array(buffer);
var chunkSize = 0x8000;
var chunks = [];
var index;
for (index = 0; index < bytes.length; index += chunkSize) {
chunks.push(String.fromCharCode.apply(null, bytes.subarray(index, index + chunkSize)));
}
return chunks.join('');
}
async function hashBlob(blob) {
if (!root.hex_md5 || !blob || !blob.arrayBuffer) {
throw new Error('缺少文件 MD5 工具');
}
return root.hex_md5(toBinaryString(await blob.arrayBuffer()));
}
async function uploadResumable(api, file, options) {
var settings = options || {};
var chunkSize = Number(settings.chunkSize) || DEFAULT_CHUNK_SIZE;
var hash = settings.hashBlob || hashBlob;
var fileMd5;
var init;
var uploadId;
var totalChunks;
var index;
var start;
var end;
var chunk;
if (!api || !file || !file.slice) throw new Error('请选择可分片上传的文件');
fileMd5 = await hash(file);
init = await api.initResumableUpload(buildResumableInitBody(file, fileMd5, chunkSize, settings));
uploadId = pick(init, ['uploadId', 'id'], '');
if (!uploadId) throw new Error('分片上传初始化缺少 uploadId');
totalChunks = Math.ceil(file.size / chunkSize);
for (index = 0; index < totalChunks; index += 1) {
start = index * chunkSize;
end = Math.min(start + chunkSize, file.size);
chunk = file.slice(start, end);
await api.uploadChunk({
uploadId: uploadId,
chunkIndex: index,
chunkMd5: await hash(chunk),
file: chunk
});
if (settings.onProgress) settings.onProgress(index + 1, totalChunks);
}
return api.completeResumableUpload(buildResumableCompleteBody(uploadId, fileMd5, file.size));
}
async function uploadFileForPage(api, file, options) {
var settings = options || {};
var mode = settings.mode === 'single' || settings.mode === 'resumable'
? settings.mode
: getUploadMode(file && file.size, settings.chunkSize);
if (mode === 'single') return api.uploadFile(file);
return uploadResumable(api, file, settings);
function uploadFileForPage(api, file) {
// 上传结果仅用于回填资料表单中的 avatarOssId。
if (!api || !file) return Promise.reject(new Error('请选择文件'));
return api.uploadFile(file);
}
async function uploadFromInput(input) {
var api = getApi();
var file = input.files && input.files[0];
var targetSelector = input.getAttribute('data-upload-target');
var statusSelector = input.getAttribute('data-upload-status');
var target = targetSelector && query(targetSelector);
var status = statusSelector && query(statusSelector);
var multiple = input.getAttribute('data-upload-multiple') === 'true';
var chunkSize = Number(input.getAttribute('data-upload-chunk-size')) || DEFAULT_CHUNK_SIZE;
var mode = input.getAttribute('data-upload-mode') || '';
var target = query(input.getAttribute('data-upload-target'));
var status = query(input.getAttribute('data-upload-status'));
var result;
if (!api || !file || !target) return;
if (status) status.textContent = '上传中...';
input.disabled = true;
try {
result = normalizeUploadResult(await uploadFileForPage(api, file, {
mode: mode || getUploadMode(file.size, chunkSize),
chunkSize: chunkSize,
bizType: input.getAttribute('data-upload-biz-type') || undefined,
usageScene: input.getAttribute('data-upload-usage-scene') || undefined,
onProgress: function (current, total) {
if (status) status.textContent = '分片上传 ' + current + '/' + total;
}
}));
if (!result.ossId) throw new Error('上传结果缺少 OSS ID');
target.value = mergeOssIds(target.value, result.ossId, multiple);
result = normalizeUploadResult(await uploadFileForPage(api, file));
if (!result.ossId) throw new Error('上传响应缺少 ossId');
target.value = result.ossId;
if (status) status.textContent = buildUploadStatus(result.fileName || file.name, result.ossId);
showMessage('文件上传完成');
} catch (error) {
if (status) status.textContent = error.message || '上传失败';
showMessage(error.message || '上传失败');
@@ -238,12 +98,8 @@
return {
normalizeUploadResult: normalizeUploadResult,
mergeOssIds: mergeOssIds,
buildUploadStatus: buildUploadStatus,
getUploadMode: getUploadMode,
buildResumableInitBody: buildResumableInitBody,
buildResumableCompleteBody: buildResumableCompleteBody,
uploadResumable: uploadResumable,
uploadFileForPage: uploadFileForPage,
init: init
};
-269
View File
@@ -1,269 +0,0 @@
(function (root, factory) {
// 会员服务模块同时支持浏览器页面和 Node 单元测试。
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 documentRef = root.document;
function normalizeList(data) {
// VIP 列表接口使用通用 ListResult,兼容 rows、records、list、data 等常见包裹。
if (Array.isArray(data)) return data;
if (!data || typeof data !== 'object') return [];
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.data)) return data.data;
return [];
}
function pick(item, keys, fallback) {
var source = item || {};
var index;
var value;
for (index = 0; index < keys.length; index += 1) {
value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') return value;
}
return fallback || '';
}
function escapeHtml(value) {
// 套餐名、说明和订单号进入 innerHTML 前统一转义。
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, rootNode) {
return (rootNode || documentRef).querySelector(selector);
}
function queryAll(selector, rootNode) {
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
}
function showMessage(message) {
if (root.layui && root.layui.layer) {
root.layui.layer.msg(message);
return;
}
root.alert(message);
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toNumberOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
var number = Number(text);
return text && Number.isFinite(number) ? number : undefined;
}
function formatPrice(value) {
var number = Number(value);
if (!Number.isFinite(number)) return '价格待定';
return '¥' + number;
}
function formatDuration(value) {
var duration = String(value === undefined || value === null ? '' : value).trim();
return duration ? duration + '天' : '有效期见套餐说明';
}
function formatPayType(value) {
var map = {
wechat: '微信支付',
alipay: '支付宝',
balance: '余额支付'
};
var key = String(value || '').trim();
return map[key] || key || '支付方式待确认';
}
function buildVipOrderBody(values) {
var source = values || {};
return {
packageId: toNumberOrUndefined(source.packageId),
genealogyId: toNumberOrUndefined(source.genealogyId),
payType: trimOrUndefined(source.payType)
};
}
function getPackageId(item) {
return pick(item, ['packageId', 'vipPackageId', 'id'], '');
}
function buildPackageCard(item) {
var packageId = getPackageId(item);
var name = pick(item, ['packageName', 'name', 'title'], '会员套餐');
var price = pick(item, ['price', 'salePrice', 'amount'], '');
var duration = pick(item, ['durationDays', 'validDays', 'days'], '');
var description = pick(item, ['description', 'packageDesc', 'remark'], '开通后享受会员服务权益');
return '<button class="vip-package-card" type="button" data-vip-package-id="' + escapeHtml(packageId) + '">' +
'<span class="vip-package-name">' + escapeHtml(name) + '</span>' +
'<strong>' + escapeHtml(formatPrice(price)) + '</strong>' +
'<small>' + escapeHtml(formatDuration(duration)) + '</small>' +
'<p>' + escapeHtml(description) + '</p></button>';
}
function buildOrderRow(item) {
var orderNo = pick(item, ['orderNo', 'orderSn', 'id'], '订单号待返回');
var packageName = pick(item, ['packageName', 'vipPackageName', 'name'], 'VIP 订单');
var payType = formatPayType(pick(item, ['payType', 'paymentType'], ''));
var status = pick(item, ['orderStatus', 'status', 'payStatus'], '状态待确认');
return '<div class="module-row vip-order-row"><div><h3>' + escapeHtml(packageName) + '</h3><p>' +
escapeHtml(orderNo) + ' · ' + escapeHtml(payType) + ' · ' + escapeHtml(status) +
'</p></div><span class="pill">订单</span></div>';
}
function renderPackages(items) {
var container = query('[data-vip-package-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无会员套餐</div>';
return;
}
container.innerHTML = list.map(buildPackageCard).join('');
}
function renderOrders(items) {
var container = query('[data-vip-order-list]');
var list = normalizeList(items);
if (!container) return;
if (!list.length) {
container.innerHTML = '<div class="api-empty">暂无会员订单</div>';
return;
}
container.innerHTML = list.map(buildOrderRow).join('');
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
});
return values;
}
function setOrderStatus(message) {
var status = query('[data-vip-order-status]');
if (status) status.textContent = message;
}
async function loadVipData() {
var api = getApi();
if (!api) return;
try {
renderPackages(await api.vipPackages());
renderOrders(await api.vipOrders());
} catch (error) {
showMessage(error.message || '会员服务加载失败');
}
}
function selectPackage(button) {
var packageId = button.getAttribute('data-vip-package-id') || '';
var field = query('[name="packageId"]');
queryAll('[data-vip-package-id]').forEach(function (item) {
item.classList.remove('is-selected');
});
button.classList.add('is-selected');
if (field) field.value = packageId;
}
async function submitOrder(form) {
var api = getApi();
var body = buildVipOrderBody(getFormValues(form));
if (!api) return;
if (!body.packageId) {
showMessage('请选择会员套餐');
return;
}
try {
await api.createVipOrder(body);
setOrderStatus('订单已创建,请继续完成支付');
showMessage('会员订单已创建');
renderOrders(await api.vipOrders());
} catch (error) {
showMessage(error.message || '会员订单创建失败');
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var packageButton = event.target.closest('[data-vip-package-id]');
if (!packageButton) return;
selectPackage(packageButton);
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-vip-order-form]');
if (!form) return;
event.preventDefault();
submitOrder(form);
});
}
function init() {
if (!documentRef || !query('[data-vip-page]')) return;
bindActions();
loadVipData();
}
return {
normalizeList: normalizeList,
buildVipOrderBody: buildVipOrderBody,
buildPackageCard: buildPackageCard,
buildOrderRow: buildOrderRow,
init: init
};
});