feat(api): 完善家谱系统API客户端契约

- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询
- 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作
- 集成通知详情获取方法和通知ID安全验证机制
- 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约
- 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作
- 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示
- 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
fizzleaf
2026-07-29 16:57:27 +08:00
parent 59a72fb22b
commit fb1743aa2a
74 changed files with 13166 additions and 1320 deletions
+462 -97
View File
@@ -15,6 +15,16 @@
'use strict';
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;
@@ -62,9 +72,14 @@
}
function getCurrentGenealogyId(search) {
var page = query('[data-feed-page], [data-feed-edit-page]');
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')) || '';
}
@@ -86,20 +101,86 @@
if (!text) return undefined;
number = Number(text);
return Number.isFinite(number) ? number : undefined;
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(),
mediaOssIds: trimOrUndefined(source.mediaOssIds),
status: trimOrUndefined(source.status)
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;
}
@@ -117,21 +198,11 @@
}
function getFeedId(item) {
var value = item && item.feedId;
var text;
if (value === undefined || value === null || value === '') return '';
text = String(value);
return /^\d+$/.test(text) ? text : '';
return toId(item && item.feedId);
}
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 : '';
return toId(item && item.commentId);
}
function normalizeFeed(item) {
@@ -140,13 +211,29 @@
var feed;
if (!feedId || feedContent === undefined || feedContent === null) return null;
feed = {
feed = setDefined({
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);
}, item, [
'genealogyNo',
'genealogyName',
'publisherNickName',
'publisherStatus',
'feedType',
'mediaOssIds',
'likedByMe',
'likeCount',
'commentCount',
'pinned',
'pinnedTime',
'sortOrder',
'status',
'remark',
'createTime',
'updateTime'
]);
if (toId(item.genealogyId)) feed.genealogyId = toId(item.genealogyId);
if (toId(item.publisherUserId)) feed.publisherUserId = toId(item.publisherUserId);
return feed;
}
@@ -157,13 +244,24 @@
if (!commentId || commentContent === undefined) return null;
userDeleted = String(item && item.userDeleted || '') === '1';
return {
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) {
@@ -183,24 +281,38 @@
}
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();
return buildFeedUrl(feedId ? 'edit' : 'list', genealogyId, feedId);
}
function renderFeedRow(feed, genealogyId) {
var canDelete = viewerContext.userId && viewerContext.userId === feed.publisherUserId;
var mediaCount = feed.mediaOssIds ? String(feed.mediaOssIds).split(',').filter(Boolean).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" type="button" data-feed-delete="' + escapeHtml(feed.feedId) + '">删除</button>';
}
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>';
'<div><h3>' + escapeHtml(title) + '</h3><p>' + escapeHtml(feed.feedContent) + '</p>' +
'<p class="module-meta">' + escapeHtml(meta.join(' · ')) + '</p></div>' +
'<div class="row-actions">' + actions + '</div></article>';
}
function renderFeeds(data, genealogyId) {
@@ -224,6 +336,24 @@
}).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;
@@ -233,16 +363,20 @@
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>';
actions += '<button class="pill" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button>';
if (canDelete) {
actions += '<button class="pill" 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>' + actions + '</div>';
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) {
@@ -255,25 +389,33 @@
}
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 previous = row && row.nextElementSibling;
var content;
if (!row) return;
if (!row && !detailContainer) return;
if (previous && previous.getAttribute('data-feed-comment-list') === feedId) previous.remove();
if (result.invalidCount) {
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>');
content = '<div class="api-empty">评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>';
if (detailContainer) detailContainer.innerHTML = content;
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
return;
}
if (!list.length) {
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">暂无评论</div>');
content = '<div class="api-empty">暂无评论</div>';
if (detailContainer) detailContainer.innerHTML = content;
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
return;
}
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-comment-list="' + escapeHtml(feedId) + '">' + list.map(function (comment) {
content = '<div class="module-list">' + list.map(function (comment) {
return renderCommentRow(feedId, comment);
}).join('') + '</div>');
}).join('') + '</div>' + renderPageControls('comments', feedId, '', currentCommentPages[feedId] || 1, getPageTotal(data));
if (detailContainer) detailContainer.innerHTML = content;
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
}
function renderCommentReplies(feedId, parentCommentId, data) {
@@ -294,13 +436,27 @@
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + result.comments.map(function (comment) {
return renderCommentRow(feedId, comment);
}).join('') + '</div>');
}).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;
@@ -315,9 +471,70 @@
});
}
function normalizeViewerContext(genealogy, profile) {
return {
userId: toId(profile && profile.userId),
canEditContent: genealogy && genealogy.canEditContent === true,
canManage: 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);
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" 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>' +
'<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 = getListUrl(genealogyId);
link.href = link.hasAttribute('data-feed-create-link')
? buildFeedUrl('edit', genealogyId)
: buildFeedUrl('list', genealogyId);
});
}
@@ -333,18 +550,24 @@
return genealogyId;
}
async function loadFeeds() {
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;
try {
renderFeeds(await api.feedsPage(genealogyId, { pageNum: 1, pageSize: 20 }), genealogyId);
await loadViewerContext(api, genealogyId);
data = await api.feedsPage(genealogyId, buildPageQuery(currentFeedPage));
renderFeeds(data, genealogyId);
updateListPager(data);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-list]')) query('[data-feed-list]').innerHTML = '<div class="api-empty">家族动态加载失败,请稍后重试。</div>';
showMessage(error.message || '家族动态加载失败');
}
}
@@ -358,8 +581,15 @@
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
if (!feedId) return;
try {
await loadViewerContext(api, genealogyId);
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) return;
fillFeedForm(await api.feedDetail(genealogyId, feedId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
@@ -367,6 +597,32 @@
}
}
async function loadFeedDetail() {
var api = getApi();
var genealogyId;
var feedId = getCurrentFeedId();
var feed;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) {
if (query('[data-feed-detail]')) query('[data-feed-detail]').innerHTML = '<div class="api-empty">动态链接缺少有效编号</div>';
return;
}
syncGenealogyLinks(genealogyId);
try {
await loadViewerContext(api, genealogyId);
feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
if (!feed) throw new Error('动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
renderFeedDetail(feed, genealogyId);
await showComments(feedId, currentCommentPages[feedId] || 1);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-detail]')) query('[data-feed-detail]').innerHTML = '<div class="api-empty">动态详情加载失败,请返回列表重试。</div>';
showMessage(error.message || '动态详情加载失败');
}
}
async function submitFeed(form) {
var api = getApi();
var genealogyId;
@@ -378,23 +634,42 @@
if (!genealogyId) return;
// 富文本编辑器在提交前同步回 textarea,保证发送的是用户当前输入。
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
body = buildFeedBody(getFormValues(form));
try {
body = buildFeedBody(getFormValues(form));
} catch (error) {
showMessage(error.message || '动态表单校验失败');
return;
}
if (!body.feedContent) {
showMessage('请填写动态内容');
return;
}
try {
if (feedId) {
await api.updateFeed(genealogyId, feedId, body);
} else {
await api.createFeed(genealogyId, body);
return withActionLock('save:' + (feedId || 'new'), async function () {
var saved;
var savedFeedId;
var buttons = queryAll('[type="submit"]', form);
buttons.forEach(function (button) { button.disabled = true; });
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;
showMessage(error.message || '动态保存失败');
} finally {
buttons.forEach(function (button) { button.disabled = false; });
}
showMessage('动态已保存');
root.location.href = getListUrl(genealogyId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '动态保存失败');
}
});
}
async function likeFeed(feedId, shouldLike) {
@@ -404,17 +679,20 @@
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
try {
if (shouldLike) {
await api.likeFeed(genealogyId, feedId);
} else {
await api.unlikeFeed(genealogyId, feedId);
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 ? '点赞失败' : '取消点赞失败'));
}
await loadFeeds();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || (shouldLike ? '点赞失败' : '取消点赞失败'));
}
});
}
async function deleteFeed(feedId) {
@@ -425,41 +703,67 @@
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId) return;
if (root.confirm && !root.confirm('确认删除这条动态吗?')) return;
try {
await api.deleteFeed(genealogyId, feedId);
await loadFeeds();
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除动态失败');
}
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) {
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;
try {
renderComments(feedId, await api.feedComments(genealogyId, feedId));
renderComments(
feedId,
await api.feedCommentsPage(genealogyId, feedId, buildPageQuery(currentCommentPages[feedId]))
);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-detail-comments]')) {
query('[data-feed-detail-comments]').innerHTML = '<div class="api-empty">评论加载失败,请稍后重试。</div>';
}
showMessage(error.message || '评论加载失败');
}
}
async function showCommentReplies(feedId, commentId) {
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;
try {
renderCommentReplies(feedId, commentId, await api.feedCommentReplies(genealogyId, feedId, commentId));
renderCommentReplies(
feedId,
commentId,
await api.feedCommentRepliesPage(
genealogyId,
feedId,
commentId,
buildPageQuery(currentReplyPages[commentId])
)
);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (query('[data-feed-comment-node="' + commentId + '"]')) {
query('[data-feed-comment-node="' + commentId + '"]').insertAdjacentHTML(
'afterend',
'<div class="api-empty">回复加载失败,请稍后重试。</div>'
);
}
showMessage(error.message || '回复加载失败');
}
}
@@ -476,13 +780,26 @@
content = root.prompt ? root.prompt(parentCommentId ? '请输入回复内容' : '请输入评论内容', '') : '';
body = buildCommentBody({ commentContent: content, parentCommentId: parentCommentId });
if (!body.commentContent) return;
try {
await api.createFeedComment(genealogyId, feedId, body);
await showComments(feedId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '评论发布失败');
if (body.commentContent.length > 1000) {
showMessage('评论不能超过 1000 字');
return;
}
return withActionLock('comment:' + feedId + ':' + (parentCommentId || 'root'), async function () {
try {
await api.createFeedComment(genealogyId, feedId, body);
await showComments(feedId, currentCommentPages[feedId] || 1);
if (parentCommentId) await showCommentReplies(feedId, parentCommentId, currentReplyPages[parentCommentId] || 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 || '评论发布失败');
}
});
}
async function deleteComment(feedId, commentId) {
@@ -493,13 +810,21 @@
genealogyId = requireGenealogyId();
if (!genealogyId || !feedId || !commentId) return;
if (root.confirm && !root.confirm('确认删除这条评论吗?')) return;
try {
await api.deleteFeedComment(genealogyId, feedId, commentId);
await showComments(feedId);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(error.message || '删除评论失败');
}
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() {
@@ -515,6 +840,7 @@
var target = event.target;
var feedId;
var commentId;
var pageNum;
if (target.closest('[data-feed-like]')) {
event.preventDefault();
@@ -527,7 +853,7 @@
if (target.closest('[data-feed-comments]')) {
event.preventDefault();
feedId = target.closest('[data-feed-comments]').getAttribute('data-feed-comments');
showComments(feedId);
showComments(feedId, 1);
}
if (target.closest('[data-feed-comment]')) {
event.preventDefault();
@@ -544,7 +870,7 @@
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);
showCommentReplies(feedId, commentId, 1);
}
if (target.closest('[data-feed-delete]')) {
event.preventDefault();
@@ -556,6 +882,39 @@
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('请从浏览器地址栏复制分享链接');
}
}
});
}
@@ -564,6 +923,7 @@
bindActions();
if (query('[data-feed-page]')) loadFeeds();
if (query('[data-feed-edit-page]')) loadFeedForEdit();
if (query('[data-feed-detail-page]')) loadFeedDetail();
}
return {
@@ -571,8 +931,13 @@
getCurrentFeedId: getCurrentFeedId,
buildFeedBody: buildFeedBody,
buildCommentBody: buildCommentBody,
buildPageQuery: buildPageQuery,
buildFeedUrl: buildFeedUrl,
buildPostSaveUrl: buildPostSaveUrl,
withActionLock: withActionLock,
normalizeFeed: normalizeFeed,
normalizeComment: normalizeComment,
normalizeViewerContext: normalizeViewerContext,
normalizeComments: normalizeComments,
normalizeList: normalizeList,
shouldRedirectToLogin: shouldRedirectToLogin,