修改功,现在补齐页面
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user