feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
+462
-97
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user