Files
jiapu/public/js/feed-pages.js
T
2026-08-29 19:06:07 +08:00

1216 lines
45 KiB
JavaScript

(function (root, factory) {
// 家族圈动态模块同时支持浏览器页面和 Node 单元测试。
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.FeedPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.FeedPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
function confirmAction(message) {
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
return root.ProfileUI.confirmAction(message);
}
return Promise.resolve(!root['confirm'] || root['confirm'](message));
}
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
var documentRef = root.document;
var PAGE_SIZE = 20;
var currentFeedPage = 1;
var currentCommentPages = {};
var currentReplyPages = {};
var pendingActions = {};
var viewerContext = {
userId: '',
canEditContent: false,
canManage: false
};
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function redirectUnauthorized(api, error) {
if (!shouldRedirectToLogin(api, error)) return false;
if (api && api.clearToken) api.clearToken();
if (!root.location) return true;
if (typeof root.location.replace === 'function') {
root.location.replace('login.html');
return true;
}
root.location.href = 'login.html';
return true;
}
function query(selector, rootNode) {
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
}
function queryAll(selector, rootNode) {
return documentRef ? 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 getApiStateType(error) {
return Number(error && (error.status || error.code)) === 403 ? 'forbidden' : 'error';
}
function setFeedFormStatus(message, type) {
var target = query('[data-feed-form-status]');
if (!target) return;
if (!message) {
target.innerHTML = '';
return;
}
if (root.ProfileUI && root.ProfileUI.setApiState) {
root.ProfileUI.setApiState(target, type || 'error', message);
return;
}
target.textContent = message;
}
function setFeedContainerState(selector, type, message) {
var target = query(selector);
if (!target) return;
if (root.ProfileUI && root.ProfileUI.setApiState) {
root.ProfileUI.setApiState(target, type, message);
return;
}
target.innerHTML = '<div class="api-state api-state--' + type + '" data-api-state="' + type + '">' +
escapeHtml(message) + '</div>';
}
function setFeedFormEnabled(enabled) {
var form = query('[data-feed-form]');
if (form) {
queryAll('input, textarea, select, button', form).forEach(function (field) {
field.disabled = !enabled;
});
}
queryAll('[data-feed-editor-action]').forEach(function (action) {
action.hidden = !enabled;
action.disabled = !enabled;
});
}
function setFeedSubmitPending(pending) {
var selectors = '[data-feed-form] [type="submit"], [type="submit"][form="feed-edit-form"]';
queryAll(selectors).forEach(function (button) {
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
button.disabled = Boolean(pending);
button.textContent = pending ? '正在保存…' : button.dataset.defaultLabel;
button.setAttribute('aria-busy', pending ? 'true' : 'false');
});
}
function setFeedCommentFormStatus(message, type) {
var target = query('[data-feed-comment-form-status]');
if (!target) return;
if (!message) {
target.innerHTML = '';
return;
}
if (root.ProfileUI && root.ProfileUI.setApiState) {
root.ProfileUI.setApiState(target, type || 'error', message);
return;
}
target.textContent = message;
}
function setFeedCommentFormEnabled(enabled) {
var form = query('[data-feed-comment-form]');
if (!form) return;
queryAll('input, textarea, button', form).forEach(function (field) {
field.disabled = !enabled;
});
}
function setFeedCommentSubmitPending(pending) {
queryAll('[data-feed-comment-form] [type="submit"]').forEach(function (button) {
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
button.textContent = pending ? '正在发布…' : button.dataset.defaultLabel;
button.setAttribute('aria-busy', pending ? 'true' : 'false');
});
}
function setFeedCommentTarget(parentCommentId) {
var form = query('[data-feed-comment-form]');
var target = query('[data-feed-comment-form-target]');
var parent = parentCommentId ? toId(parentCommentId) : '';
if (!form) return;
if (query('[name="parentCommentId"]', form)) query('[name="parentCommentId"]', form).value = parent;
if (target) target.textContent = parent ? '正在回复这条评论' : '发表公开评论';
}
function openFeedCommentForm(feedId, parentCommentId) {
var genealogyId = getCurrentGenealogyId();
var form;
var submitButton;
if (!feedId || !genealogyId) return;
if (!query('[data-feed-detail-page]')) {
if (root.location) root.location.href = buildFeedUrl('detail', genealogyId, feedId);
return;
}
if (getCurrentFeedId() !== toId(feedId)) return;
form = query('[data-feed-comment-form]');
submitButton = form && query('[type="submit"]', form);
if (!form || !submitButton || submitButton.disabled) return;
setFeedCommentTarget(parentCommentId);
setFeedCommentFormStatus('');
if (query('[name="commentContent"]', form) && query('[name="commentContent"]', form).focus) {
query('[name="commentContent"]', form).focus();
}
}
function getQueryParam(search, name) {
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
return params.get(name) || '';
}
function getCurrentGenealogyId(search) {
var page = query('[data-feed-page], [data-feed-edit-page], [data-feed-detail-page]');
var source = search === undefined && root.location ? root.location.search : search;
var profileGenealogyId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
profileGenealogyId = root.ProfileUI.getGenealogyId();
if (profileGenealogyId) return profileGenealogyId;
}
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
}
function getCurrentFeedId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return getQueryParam(source, 'feedId');
}
function trimOrUndefined(value) {
var text = String(value === undefined || value === null ? '' : value).trim();
return text || undefined;
}
function toIntegerOrUndefined(value) {
var text = trimOrUndefined(value);
var number;
if (!text) return undefined;
number = Number(text);
return Number.isSafeInteger(number) && number >= 0 ? number : undefined;
}
function toId(value) {
var text;
if (value === undefined || value === null || value === '') return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
}
function setDefined(target, source, fields) {
fields.forEach(function (field) {
if (source[field] !== undefined && source[field] !== null) target[field] = source[field];
});
return target;
}
function buildPageQuery(pageNum) {
var number = Number(pageNum);
if (!Number.isSafeInteger(number) || number < 1) number = 1;
return {
pageNum: number,
pageSize: PAGE_SIZE
};
}
function buildFeedUrl(kind, genealogyId, feedId) {
var params = new URLSearchParams();
var page = 'profile-feed.html';
params.set('genealogyId', toId(genealogyId));
if (kind === 'detail' || kind === 'edit') {
params.set('feedId', toId(feedId));
page = kind === 'detail' ? 'profile-feed-detail.html' : 'profile-feed-edit.html';
}
return page + '?' + params.toString();
}
function buildPostSaveUrl(genealogyId, feedId, status) {
return String(status || '0') === '1'
? buildFeedUrl('list', genealogyId)
: buildFeedUrl('detail', genealogyId, feedId);
}
function withActionLock(key, task) {
var promise;
if (pendingActions[key]) return pendingActions[key];
try {
promise = Promise.resolve(task());
} catch (error) {
promise = Promise.reject(error);
}
pendingActions[key] = promise.finally(function () {
delete pendingActions[key];
});
return pendingActions[key];
}
function buildFeedBody(values) {
// FamilyFeedBody 只提交最新接口文档定义的字段。
var source = values || {};
var mediaOssIds = trimOrUndefined(source.mediaOssIds);
var status = trimOrUndefined(source.status);
var body = {
feedType: trimOrUndefined(source.feedType) || 'text',
feedContent: String(source.feedContent || '').trim()
};
var sortOrder = toIntegerOrUndefined(source.sortOrder);
if (status !== undefined && status !== '0' && status !== '1') {
throw new Error('动态状态只能是 0 或 1');
}
if (trimOrUndefined(source.sortOrder) !== undefined && sortOrder === undefined) {
throw new Error('排序值必须是非负整数');
}
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (status !== undefined) body.status = status;
if (sortOrder !== undefined) body.sortOrder = sortOrder;
return body;
}
function buildCommentBody(values) {
// FamilyFeedCommentBody 仅接受 commentContent 和可选的 parentCommentId。
var source = values || {};
var body = {
commentContent: String(source.commentContent || '').trim()
};
var parentCommentId = trimOrUndefined(source.parentCommentId);
if (parentCommentId !== undefined) body.parentCommentId = parentCommentId;
return body;
}
function getFeedId(item) {
return toId(item && item.feedId);
}
function getCommentId(item) {
return toId(item && item.commentId);
}
function normalizeFeed(item) {
var feedId = getFeedId(item);
var feedContent = item && item.feedContent;
var feed;
if (!feedId || feedContent === undefined || feedContent === null) return null;
feed = setDefined({
feedId: feedId,
feedContent: String(feedContent)
}, item, [
'genealogyNo',
'genealogyName',
'publisherNickName',
'publisherStatus',
'feedType',
'likedByMe',
'likeCount',
'commentCount',
'pinned',
'pinnedTime',
'sortOrder',
'status',
'remark',
'createTime',
'updateTime'
]);
var mediaFiles = MediaDisplay ? MediaDisplay.normalizeFileList(item && item.mediaFiles) : [];
if (mediaFiles.length) feed.mediaFiles = mediaFiles;
if (toId(item.genealogyId)) feed.genealogyId = toId(item.genealogyId);
if (toId(item.publisherUserId)) feed.publisherUserId = toId(item.publisherUserId);
return feed;
}
function normalizeComment(item) {
var commentId = getCommentId(item);
var commentContent = item && item.commentContent;
var userDeleted;
if (!commentId || commentContent === undefined) return null;
userDeleted = String(item && item.userDeleted || '') === '1';
var comment = {
commentId: commentId,
commentContent: commentContent === null ? '该评论已删除' : String(commentContent),
appUserNickName: trimOrUndefined(item && item.appUserNickName),
replyCount: item && item.replyCount,
userDeleted: userDeleted || commentContent === null
};
setDefined(comment, item, [
'appUserAvatar',
'parentAppUserNickName',
'commentLevel',
'status',
'createTime'
]);
['genealogyId', 'feedId', 'parentCommentId', 'appUserId', 'parentAppUserId'].forEach(function (field) {
if (toId(item && item[field])) comment[field] = toId(item[field]);
});
return comment;
}
function normalizeList(data) {
// AxiosRequestUtil 已解包 ListResult 与 PageResult,这里只接收对应的 data 或 rows。
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.rows)) return data.rows;
return [];
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderFeedState(type, message) {
if (root.ProfileUI && root.ProfileUI.renderApiState) {
return root.ProfileUI.renderApiState(type, message);
}
return '<div class="api-state api-state--' + escapeHtml(type) + '" data-api-state="' +
escapeHtml(type) + '">' + escapeHtml(message) + '</div>';
}
function replaceInlineCommentList(feedId, content) {
var row = query('[data-feed-id="' + feedId + '"]');
var previous = row && row.nextElementSibling;
if (!row) return;
if (previous && previous.getAttribute('data-feed-comment-list') === feedId) previous.remove();
row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
}
function replaceReplyList(parentCommentId, content) {
var row = query('[data-feed-comment-node="' + parentCommentId + '"]');
var previous = row && row.nextElementSibling;
if (!row) return;
if (previous && previous.getAttribute('data-feed-reply-list') === parentCommentId) previous.remove();
row.insertAdjacentHTML('afterend', content);
}
function setReplyState(parentCommentId, type, message) {
replaceReplyList(
parentCommentId,
'<div data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + renderFeedState(type, message) + '</div>'
);
}
function getListUrl(genealogyId, feedId) {
return buildFeedUrl(feedId ? 'edit' : 'list', genealogyId, feedId);
}
function renderFeedRow(feed, genealogyId) {
var canDelete = viewerContext.userId && viewerContext.userId === feed.publisherUserId;
var mediaCount = Array.isArray(feed.mediaFiles) ? feed.mediaFiles.length : 0;
var title = feed.publisherNickName || feed.genealogyName || '家族动态';
var meta = [];
var actions = '';
if (feed.pinned === true || String(feed.pinned) === '1') meta.push('置顶');
if (feed.createTime) meta.push(feed.createTime);
if (mediaCount) meta.push('附件 ' + mediaCount);
meta.push('点赞 ' + Number(feed.likeCount || 0));
meta.push('评论 ' + Number(feed.commentCount || 0));
actions += feed.likedByMe
? '<button class="pill" type="button" data-feed-unlike="' + escapeHtml(feed.feedId) + '">取消点赞</button>'
: '<button class="pill" type="button" data-feed-like="' + escapeHtml(feed.feedId) + '">点赞</button>';
actions += '<a class="pill" href="' + escapeHtml(buildFeedUrl('detail', genealogyId, feed.feedId)) + '">查看详情</a>';
actions += '<button class="pill" type="button" data-feed-comments="' + escapeHtml(feed.feedId) + '">查看评论</button>';
actions += '<button class="pill" type="button" data-feed-comment="' + escapeHtml(feed.feedId) + '">发表评论</button>';
if (viewerContext.canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildFeedUrl('edit', genealogyId, feed.feedId)) + '">编辑</a>';
}
if (canDelete) {
actions += '<button class="pill is-danger" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>';
}
return '<article class="module-row feed-row" data-feed-id="' + escapeHtml(feed.feedId) + '">' +
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(feed.feedContent) + '</p>' +
(MediaDisplay ? MediaDisplay.renderGallery(feed.mediaFiles, { label: '动态附件', className: 'feed-media-gallery' }) : '') +
'<p class="module-meta">' + escapeHtml(meta.join(' · ')) + '</p></div>' +
'<div class="row-actions">' + actions + '</div></article>';
}
function renderFeeds(data, genealogyId) {
var container = query('[data-feed-list]');
var items = normalizeList(data).map(normalizeFeed);
var invalidCount = items.filter(function (item) { return !item; }).length;
var feeds = items.filter(Boolean);
if (!container) return;
if (invalidCount) {
setFeedContainerState('[data-feed-list]', 'error', '动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
return;
}
if (!feeds.length) {
setFeedContainerState('[data-feed-list]', 'empty', '暂无家族动态');
return;
}
container.innerHTML = feeds.map(function (feed) {
return renderFeedRow(feed, genealogyId);
}).join('');
}
function getPageTotal(data) {
var total = Number(data && data.total);
return Number.isFinite(total) && total >= 0 ? total : 0;
}
function updateListPager(data) {
var total = getPageTotal(data);
var totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
var previous = query('[data-feed-page-prev]');
var next = query('[data-feed-page-next]');
var label = query('[data-feed-page-label]');
if (previous) previous.disabled = currentFeedPage <= 1;
if (next) next.disabled = currentFeedPage >= totalPages;
if (label) label.textContent = '第 ' + currentFeedPage + ' / ' + totalPages + ' 页,共 ' + total + ' 条';
}
function hasReplies(comment) {
var count = comment && comment.replyCount;
return count !== undefined && count !== null && String(count) !== '' && String(count) !== '0';
}
function renderCommentRow(feedId, comment) {
var author = comment.appUserNickName ? '<strong>' + escapeHtml(comment.appUserNickName) + '</strong> ' : '';
var actions = '';
var canDelete = viewerContext.userId && viewerContext.userId === comment.appUserId;
if (!comment.userDeleted) {
actions += '<button class="pill" type="button" data-feed-comment-reply="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">回复</button>';
if (canDelete) {
actions += '<button class="pill is-danger" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button>';
}
}
if (hasReplies(comment)) {
actions += '<button class="pill" type="button" data-feed-comment-replies="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">查看回复</button>';
}
return '<div class="module-row" data-feed-comment-node="' + escapeHtml(comment.commentId) + '"><p>' + author + escapeHtml(comment.commentContent) + '</p>' +
(comment.createTime ? '<p class="module-meta">' + escapeHtml(comment.createTime) + '</p>' : '') + actions + '</div>';
}
function normalizeComments(data) {
var comments = normalizeList(data).map(normalizeComment);
return {
invalidCount: comments.filter(function (item) { return !item; }).length,
comments: comments.filter(Boolean)
};
}
function renderComments(feedId, data) {
var detailContainer = query('[data-feed-detail-comments]');
var row = query('[data-feed-id="' + feedId + '"]');
var result = normalizeComments(data);
var list = result.comments;
var content;
if (!row && !detailContainer) return;
if (result.invalidCount) {
content = renderFeedState('error', '评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。');
if (detailContainer) setFeedContainerState('[data-feed-detail-comments]', 'error', '评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。');
else replaceInlineCommentList(feedId, content);
return;
}
if (!list.length) {
content = renderFeedState('empty', '暂无评论');
if (detailContainer) setFeedContainerState('[data-feed-detail-comments]', 'empty', '暂无评论');
else replaceInlineCommentList(feedId, content);
return;
}
content = '<div class="module-list">' + list.map(function (comment) {
return renderCommentRow(feedId, comment);
}).join('') + '</div>' + renderPageControls('comments', feedId, '', currentCommentPages[feedId] || 1, getPageTotal(data));
if (detailContainer) detailContainer.innerHTML = content;
else replaceInlineCommentList(feedId, content);
}
function renderCommentReplies(feedId, parentCommentId, data) {
var row = query('[data-feed-comment-node="' + parentCommentId + '"]');
var result = normalizeComments(data);
if (!row) return;
if (result.invalidCount) {
setReplyState(parentCommentId, 'error', '回复响应缺少 commentId 或 commentContent,请联系后端补充 DTO。');
return;
}
if (!result.comments.length) {
setReplyState(parentCommentId, 'empty', '暂无直接回复');
return;
}
replaceReplyList(parentCommentId, '<div class="module-list" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + result.comments.map(function (comment) {
return renderCommentRow(feedId, comment);
}).join('') + renderPageControls('replies', feedId, parentCommentId, currentReplyPages[parentCommentId] || 1, getPageTotal(data)) + '</div>');
}
function renderPageControls(kind, feedId, commentId, pageNum, total) {
var totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
var prefix = kind === 'comments' ? 'data-feed-comments-' : 'data-feed-replies-';
var ids = ' data-feed-id="' + escapeHtml(feedId) + '"' +
(commentId ? ' data-comment-id="' + escapeHtml(commentId) + '"' : '');
return '<div class="row-actions">' +
'<button class="pill" type="button" ' + prefix + 'prev' + ids + (pageNum <= 1 ? ' disabled' : '') + '>上一页</button>' +
'<span>第 ' + pageNum + ' / ' + totalPages + ' 页</span>' +
'<button class="pill" type="button" ' + prefix + 'next' + ids + (pageNum >= totalPages ? ' disabled' : '') + '>下一页</button>' +
'</div>';
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
if (field.disabled) return;
values[field.name] = field.value;
});
return values;
}
function fillFeedForm(data) {
var feed = normalizeFeed(data);
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 canEditFeeds(genealogy) {
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
}
function setFeedContentEditingEnabled(enabled) {
queryAll('[data-feed-create-link]').forEach(function (link) {
link.hidden = !enabled;
});
}
function normalizeViewerContext(genealogy, profile) {
return {
userId: toId(profile && profile.userId),
canEditContent: canEditFeeds(genealogy),
canManage: Boolean(genealogy && genealogy.canManage === true)
};
}
async function loadViewerContext(api, genealogyId) {
var genealogy;
var profile;
try {
genealogy = await api.genealogyDetail(genealogyId);
} catch (error) {
genealogy = null;
}
try {
profile = await api.currentProfile();
} catch (error) {
profile = null;
}
viewerContext = normalizeViewerContext(genealogy, profile);
setFeedContentEditingEnabled(viewerContext.canEditContent);
return viewerContext;
}
function renderFeedDetail(feed, genealogyId) {
var container = query('[data-feed-detail]');
var canDelete = viewerContext.userId && viewerContext.userId === feed.publisherUserId;
var actions = '';
if (!container) return;
actions += feed.likedByMe
? '<button class="pill" type="button" data-feed-unlike="' + escapeHtml(feed.feedId) + '">取消点赞</button>'
: '<button class="pill" type="button" data-feed-like="' + escapeHtml(feed.feedId) + '">点赞</button>';
actions += '<button class="pill" type="button" data-feed-comment="' + escapeHtml(feed.feedId) + '">发表评论</button>';
actions += '<button class="pill" type="button" data-feed-share>复制分享链接</button>';
if (viewerContext.canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildFeedUrl('edit', genealogyId, feed.feedId)) + '">编辑</a>';
}
if (canDelete) {
actions += '<button class="pill is-danger" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>';
}
container.setAttribute('data-feed-id', feed.feedId);
container.innerHTML = '<h2>' + escapeHtml(feed.publisherNickName || feed.genealogyName || '家族动态') + '</h2>' +
'<p>' + escapeHtml(feed.feedContent) + '</p>' +
(MediaDisplay ? MediaDisplay.renderGallery(feed.mediaFiles, { label: '动态附件', className: 'feed-media-gallery' }) : '') +
'<p class="module-meta">' +
escapeHtml([
feed.createTime || '',
'点赞 ' + Number(feed.likeCount || 0),
'评论 ' + Number(feed.commentCount || 0),
String(feed.status) === '1' ? '已停用' : '正常'
].filter(Boolean).join(' · ')) +
'</p><div class="row-actions">' + actions + '</div>';
}
function syncGenealogyLinks(genealogyId) {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
root.ProfileUI.syncGenealogyContextLinks();
}
queryAll('[data-feed-create-link], [data-feed-list-link]').forEach(function (link) {
link.href = link.hasAttribute('data-feed-create-link')
? buildFeedUrl('edit', genealogyId)
: buildFeedUrl('list', genealogyId);
});
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
var container;
if (!genealogyId) {
container = query('[data-feed-list]');
if (container) setFeedContainerState('[data-feed-list]', 'forbidden', '请从具体家谱进入家族圈动态。');
setFeedFormEnabled(false);
setFeedFormStatus('请从具体家谱进入动态发布。', 'forbidden');
setFeedCommentFormEnabled(false);
setFeedCommentFormStatus('请从具体家谱进入动态详情后发表评论。', 'forbidden');
showMessage('请从具体家谱进入家族圈动态');
}
return genealogyId;
}
async function loadFeeds(pageNum) {
var api = getApi();
var genealogyId;
var data;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
currentFeedPage = buildPageQuery(pageNum || getQueryParam(root.location && root.location.search, 'pageNum')).pageNum;
setFeedContainerState('[data-feed-list]', 'loading', '正在加载家族动态…');
try {
await loadViewerContext(api, genealogyId);
data = await api.feedsPage(genealogyId, buildPageQuery(currentFeedPage));
renderFeeds(data, genealogyId);
updateListPager(data);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFeedContainerState(
'[data-feed-list]',
getApiStateType(error),
getApiStateType(error) === 'forbidden' ? '当前账号无权查看该家谱的动态。' : '家族动态加载失败,请稍后重试。'
);
showMessage(error.message || '家族动态加载失败');
}
}
async function loadFeedForEdit() {
var api = getApi();
var genealogyId;
var feedId = getCurrentFeedId();
var isEditing = Boolean(feedId);
var editorTitle = isEditing ? '编辑动态' : '发布动态';
if (root.document) root.document.title = editorTitle + ' - 个人中心 - 代代相传';
if (query('[data-feed-editor-kicker]')) query('[data-feed-editor-kicker]').textContent = editorTitle;
if (query('[data-feed-editor-title]')) query('[data-feed-editor-title]').textContent = editorTitle;
queryAll('[data-feed-editor-action]').forEach(function (button) {
button.textContent = isEditing ? '保存修改' : '发布动态';
});
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
setFeedFormEnabled(false);
setFeedFormStatus('正在读取家谱权限…', 'loading');
try {
await loadViewerContext(api, genealogyId);
if (!viewerContext.canEditContent) {
setFeedFormStatus('当前账号无权发布或编辑该家谱的动态。', 'forbidden');
return;
}
setFeedFormEnabled(true);
if (query('[data-feed-advanced]')) {
query('[data-feed-advanced]').hidden = !viewerContext.canManage;
queryAll('[name]', query('[data-feed-advanced]')).forEach(function (field) {
field.disabled = !viewerContext.canManage;
});
}
if (!feedId) {
setFeedFormStatus('');
return;
}
fillFeedForm(await api.feedDetail(genealogyId, feedId));
setFeedFormStatus('');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFeedFormStatus(
getApiStateType(error) === 'forbidden' ? '当前账号无权维护该家谱的动态。' : (error.message || '动态详情加载失败'),
getApiStateType(error)
);
}
}
async function loadFeedDetail() {
var api = getApi();
var genealogyId;
var feedId = getCurrentFeedId();
var feed;
if (redirectUnauthorized(api)) return;
setFeedCommentFormEnabled(false);
setFeedCommentFormStatus('正在读取动态评论权限…', 'loading');
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) {
setFeedContainerState('[data-feed-detail]', 'error', '动态链接缺少有效编号。');
setFeedCommentFormStatus('动态链接缺少有效编号。', 'error');
return;
}
syncGenealogyLinks(genealogyId);
setFeedContainerState('[data-feed-detail]', 'loading', '正在加载动态详情…');
try {
await loadViewerContext(api, genealogyId);
feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
if (!feed) throw new Error('动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
renderFeedDetail(feed, genealogyId);
setFeedCommentFormEnabled(true);
setFeedCommentFormStatus('');
await showComments(feedId, currentCommentPages[feedId] || 1);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFeedContainerState(
'[data-feed-detail]',
getApiStateType(error),
getApiStateType(error) === 'forbidden' ? '当前账号无权查看该动态。' : '动态详情加载失败,请返回列表重试。'
);
setFeedCommentFormStatus(
getApiStateType(error) === 'forbidden' ? '当前账号无权评论该动态。' : '动态详情加载失败,暂不能发表评论。',
getApiStateType(error)
);
showMessage(error.message || '动态详情加载失败');
}
}
async function submitFeed(form) {
var api = getApi();
var genealogyId;
var feedId = getCurrentFeedId();
var body;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
if (!viewerContext.canEditContent) {
setFeedFormStatus('当前账号无权发布或编辑该家谱的动态。', 'forbidden');
return;
}
// 富文本编辑器在提交前同步回 textarea,保证发送的是用户当前输入。
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
try {
body = buildFeedBody(getFormValues(form));
} catch (error) {
setFeedFormStatus(error.message || '动态表单校验失败', 'error');
return;
}
if (!body.feedContent) {
setFeedFormStatus('请填写动态内容', 'error');
return;
}
return withActionLock('save:' + (feedId || 'new'), async function () {
var saved;
var savedFeedId;
setFeedSubmitPending(true);
setFeedFormStatus('正在保存动态…', 'loading');
try {
if (feedId) {
saved = await api.updateFeed(genealogyId, feedId, body);
} else {
saved = await api.createFeed(genealogyId, body);
}
savedFeedId = getFeedId(saved) || feedId;
if (!savedFeedId) throw new Error('动态保存响应缺少 feedId');
if (String(body.status || '0') === '0') {
await api.feedDetail(genealogyId, savedFeedId);
}
showMessage('动态已保存');
root.location.href = buildPostSaveUrl(genealogyId, savedFeedId, body.status);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFeedFormStatus(
getApiStateType(error) === 'forbidden' ? '当前账号无权保存该家谱的动态。' : (error.message || '动态保存失败'),
getApiStateType(error)
);
} finally {
setFeedSubmitPending(false);
}
});
}
async function likeFeed(feedId, shouldLike) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
return withActionLock((shouldLike ? 'like:' : 'unlike:') + feedId, async function () {
try {
if (shouldLike) {
await api.likeFeed(genealogyId, feedId);
} else {
await api.unlikeFeed(genealogyId, feedId);
}
if (query('[data-feed-detail-page]')) await loadFeedDetail();
else await loadFeeds(currentFeedPage);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || (shouldLike ? '点赞失败' : '取消点赞失败'));
}
});
}
async function deleteFeed(feedId) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
if (!await confirmAction('确认删除这条动态吗?')) return;
return withActionLock('delete:' + feedId, async function () {
try {
await api.deleteFeed(genealogyId, feedId);
if (query('[data-feed-detail-page]')) root.location.href = buildFeedUrl('list', genealogyId);
else await loadFeeds(currentFeedPage);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除动态失败');
}
});
}
async function showComments(feedId, pageNum) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
currentCommentPages[feedId] = buildPageQuery(pageNum).pageNum;
if (query('[data-feed-detail-comments]')) {
setFeedContainerState('[data-feed-detail-comments]', 'loading', '正在加载评论…');
} else {
replaceInlineCommentList(feedId, renderFeedState('loading', '正在加载评论…'));
}
try {
renderComments(
feedId,
await api.feedCommentsPage(genealogyId, feedId, buildPageQuery(currentCommentPages[feedId]))
);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-detail-comments]')) {
setFeedContainerState(
'[data-feed-detail-comments]',
getApiStateType(error),
getApiStateType(error) === 'forbidden' ? '当前账号无权查看评论。' : '评论加载失败,请稍后重试。'
);
} else {
replaceInlineCommentList(
feedId,
renderFeedState(
getApiStateType(error),
getApiStateType(error) === 'forbidden' ? '当前账号无权查看评论。' : '评论加载失败,请稍后重试。'
)
);
}
showMessage(error.message || '评论加载失败');
}
}
async function showCommentReplies(feedId, commentId, pageNum) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId || !commentId) return;
currentReplyPages[commentId] = buildPageQuery(pageNum).pageNum;
setReplyState(commentId, 'loading', '正在加载回复…');
try {
renderCommentReplies(
feedId,
commentId,
await api.feedCommentRepliesPage(
genealogyId,
feedId,
commentId,
buildPageQuery(currentReplyPages[commentId])
)
);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setReplyState(
commentId,
getApiStateType(error),
getApiStateType(error) === 'forbidden' ? '当前账号无权查看回复。' : '回复加载失败,请稍后重试。'
);
showMessage(error.message || '回复加载失败');
}
}
async function submitComment(form) {
var api = getApi();
var genealogyId;
var feedId = getCurrentFeedId();
var body;
var values;
var forbidden = false;
if (redirectUnauthorized(api) || !form) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
values = getFormValues(form);
body = buildCommentBody(values);
if (!body.commentContent) {
setFeedCommentFormStatus('请填写评论内容。', 'error');
return;
}
if (body.commentContent.length > 1000) {
setFeedCommentFormStatus('评论不能超过 1000 字。', 'error');
return;
}
return withActionLock('comment:' + feedId + ':' + (body.parentCommentId || 'root'), async function () {
setFeedCommentFormEnabled(false);
setFeedCommentSubmitPending(true);
setFeedCommentFormStatus('正在发布评论…', 'loading');
try {
await api.createFeedComment(genealogyId, feedId, body);
await showComments(feedId, currentCommentPages[feedId] || 1);
if (body.parentCommentId) await showCommentReplies(feedId, body.parentCommentId, currentReplyPages[body.parentCommentId] || 1);
form.reset();
setFeedCommentTarget('');
setFeedCommentFormStatus('');
var feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
if (feed) renderFeedDetail(feed, genealogyId);
showMessage('评论已发布');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
forbidden = getApiStateType(error) === 'forbidden';
setFeedCommentFormStatus(
forbidden ? '当前账号无权评论该动态。' : (error.message || '评论发布失败,请稍后重试。'),
getApiStateType(error)
);
} finally {
setFeedCommentSubmitPending(false);
if (!forbidden) setFeedCommentFormEnabled(true);
}
});
}
async function deleteComment(feedId, commentId) {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId || !commentId) return;
if (!await confirmAction('确认删除这条评论吗?')) return;
return withActionLock('delete-comment:' + commentId, async function () {
try {
await api.deleteFeedComment(genealogyId, feedId, commentId);
await showComments(feedId, currentCommentPages[feedId] || 1);
if (query('[data-feed-detail-page]')) {
var feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
if (feed) renderFeedDetail(feed, genealogyId);
} else {
await loadFeeds(currentFeedPage);
}
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除评论失败');
}
});
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-feed-form]');
if (form) {
event.preventDefault();
submitFeed(form);
return;
}
form = event.target.closest('[data-feed-comment-form]');
if (form) {
event.preventDefault();
submitComment(form);
}
});
documentRef.addEventListener('click', function (event) {
var target = event.target;
var feedId;
var commentId;
var pageNum;
if (target.closest('[data-feed-like]')) {
event.preventDefault();
likeFeed(target.closest('[data-feed-like]').getAttribute('data-feed-like'), true);
}
if (target.closest('[data-feed-unlike]')) {
event.preventDefault();
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, 1);
}
if (target.closest('[data-feed-comment]')) {
event.preventDefault();
feedId = target.closest('[data-feed-comment]').getAttribute('data-feed-comment');
openFeedCommentForm(feedId);
}
if (target.closest('[data-feed-comment-reply]')) {
event.preventDefault();
commentId = target.closest('[data-feed-comment-reply]').getAttribute('data-feed-comment-reply');
feedId = target.closest('[data-feed-comment-reply]').getAttribute('data-feed-id');
openFeedCommentForm(feedId, commentId);
}
if (target.closest('[data-feed-comment-cancel]')) {
event.preventDefault();
setFeedCommentTarget('');
setFeedCommentFormStatus('');
}
if (target.closest('[data-feed-comment-replies]')) {
event.preventDefault();
commentId = target.closest('[data-feed-comment-replies]').getAttribute('data-feed-comment-replies');
feedId = target.closest('[data-feed-comment-replies]').getAttribute('data-feed-id');
showCommentReplies(feedId, commentId, 1);
}
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);
}
if (target.closest('[data-feed-page-prev]')) {
event.preventDefault();
loadFeeds(Math.max(1, currentFeedPage - 1));
}
if (target.closest('[data-feed-page-next]')) {
event.preventDefault();
loadFeeds(currentFeedPage + 1);
}
if (target.closest('[data-feed-comments-prev], [data-feed-comments-next]')) {
event.preventDefault();
feedId = target.closest('[data-feed-id]').getAttribute('data-feed-id');
pageNum = currentCommentPages[feedId] || 1;
showComments(feedId, target.closest('[data-feed-comments-prev]') ? Math.max(1, pageNum - 1) : pageNum + 1);
}
if (target.closest('[data-feed-replies-prev], [data-feed-replies-next]')) {
event.preventDefault();
feedId = target.closest('[data-feed-id]').getAttribute('data-feed-id');
commentId = target.closest('[data-comment-id]').getAttribute('data-comment-id');
pageNum = currentReplyPages[commentId] || 1;
showCommentReplies(feedId, commentId, target.closest('[data-feed-replies-prev]') ? Math.max(1, pageNum - 1) : pageNum + 1);
}
if (target.closest('[data-feed-share]')) {
event.preventDefault();
if (root.navigator && root.navigator.clipboard && root.location) {
root.navigator.clipboard.writeText(root.location.href).then(function () {
showMessage('分享链接已复制');
}).catch(function () {
showMessage('复制失败,请从浏览器地址栏复制');
});
} else {
showMessage('请从浏览器地址栏复制分享链接');
}
}
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-feed-page]')) loadFeeds();
if (query('[data-feed-edit-page]')) loadFeedForEdit();
if (query('[data-feed-detail-page]')) loadFeedDetail();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentFeedId: getCurrentFeedId,
buildFeedBody: buildFeedBody,
buildCommentBody: buildCommentBody,
buildPageQuery: buildPageQuery,
buildFeedUrl: buildFeedUrl,
buildPostSaveUrl: buildPostSaveUrl,
withActionLock: withActionLock,
normalizeFeed: normalizeFeed,
normalizeComment: normalizeComment,
canEditFeeds: canEditFeeds,
normalizeViewerContext: normalizeViewerContext,
normalizeComments: normalizeComments,
normalizeList: normalizeList,
shouldRedirectToLogin: shouldRedirectToLogin,
init: init
};
});