From ee118c0bf4984fe49365c3e81f91e29f392c389b Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:11:42 +0800 Subject: [PATCH] feat: add post pinning --- open-isle-cli/src/views/HomePageView.vue | 12 +- open-isle-cli/src/views/PostPageView.vue | 44 ++++++- .../controller/AdminPostController.java | 12 ++ .../openisle/controller/PostController.java | 2 + src/main/java/com/openisle/model/Post.java | 3 + .../com/openisle/service/PostService.java | 122 +++++++++--------- 6 files changed, 133 insertions(+), 62 deletions(-) diff --git a/open-isle-cli/src/views/HomePageView.vue b/open-isle-cli/src/views/HomePageView.vue index 5503d2c6b..4d4ebf2f2 100644 --- a/open-isle-cli/src/views/HomePageView.vue +++ b/open-isle-cli/src/views/HomePageView.vue @@ -58,6 +58,7 @@
+ {{ article.title }}
{{ sanitizeDescription(article.description) }}
@@ -236,7 +237,8 @@ export default { members: (p.participants || []).map(m => ({ id: m.id, avatar: m.avatar })), comments: (p.comments || []).length, views: p.views, - time: TimeManager.format(p.createdAt) + time: TimeManager.format(p.createdAt), + pinned: !!p.pinnedAt })) ) if (data.length < pageSize) { @@ -272,7 +274,8 @@ export default { members: (p.participants || []).map(m => ({ id: m.id, avatar: m.avatar })), comments: (p.comments || []).length, views: p.views, - time: TimeManager.format(p.createdAt) + time: TimeManager.format(p.createdAt), + pinned: !!p.pinnedAt })) ) if (data.length < pageSize) { @@ -502,6 +505,11 @@ export default { text-decoration: underline; } +.pinned-icon { + margin-right: 4px; + color: var(--primary-color); +} + .article-item-description { margin-top: 10px; font-size: 14px; diff --git a/open-isle-cli/src/views/PostPageView.vue b/open-isle-cli/src/views/PostPageView.vue index fca654538..856ef6872 100644 --- a/open-isle-cli/src/views/PostPageView.vue +++ b/open-isle-cli/src/views/PostPageView.vue @@ -138,6 +138,7 @@ export default { const postReactions = ref([]) const comments = ref([]) const status = ref('PUBLISHED') + const pinnedAt = ref(null) const isWaitingFetchingPost = ref(false); const isWaitingPostingComment = ref(false); const postTime = ref('') @@ -157,6 +158,13 @@ export default { if (isAuthor.value || isAdmin.value) { items.push({ text: '删除文章', color: 'red', onClick: () => deletePost() }) } + if (isAdmin.value) { + if (pinnedAt.value) { + items.push({ text: '取消置顶', onClick: () => unpinPost() }) + } else { + items.push({ text: '置顶', onClick: () => pinPost() }) + } + } if (isAdmin.value && status.value === 'PENDING') { items.push({ text: '通过审核', onClick: () => approvePost() }) items.push({ text: '驳回', color: 'red', onClick: () => rejectPost() }) @@ -276,6 +284,7 @@ export default { comments.value = (data.comments || []).map(mapComment) subscribed.value = !!data.subscribed status.value = data.status + pinnedAt.value = data.pinnedAt postTime.value = TimeManager.format(data.createdAt) await nextTick() gatherPostItems() @@ -394,6 +403,36 @@ export default { } } + const pinPost = async () => { + const token = getToken() + if (!token) return + const res = await fetch(`${API_BASE_URL}/api/admin/posts/${postId}/pin`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` } + }) + if (res.ok) { + pinnedAt.value = new Date().toISOString() + toast.success('已置顶') + } else { + toast.error('操作失败') + } + } + + const unpinPost = async () => { + const token = getToken() + if (!token) return + const res = await fetch(`${API_BASE_URL}/api/admin/posts/${postId}/unpin`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` } + }) + if (res.ok) { + pinnedAt.value = null + toast.success('已取消置顶') + } else { + toast.error('操作失败') + } + } + const deletePost = async () => { const token = getToken() if (!token) { @@ -508,12 +547,15 @@ export default { approvePost, onCommentDeleted, deletePost, + pinPost, + unpinPost, rejectPost, lightboxVisible, lightboxIndex, lightboxImgs, handleContentClick, - isMobile + isMobile, + pinnedAt } } } diff --git a/src/main/java/com/openisle/controller/AdminPostController.java b/src/main/java/com/openisle/controller/AdminPostController.java index 0347d2cc3..7bb6fffd7 100644 --- a/src/main/java/com/openisle/controller/AdminPostController.java +++ b/src/main/java/com/openisle/controller/AdminPostController.java @@ -36,6 +36,16 @@ public class AdminPostController { return toDto(postService.rejectPost(id)); } + @PostMapping("/{id}/pin") + public PostDto pin(@PathVariable Long id) { + return toDto(postService.pinPost(id)); + } + + @PostMapping("/{id}/unpin") + public PostDto unpin(@PathVariable Long id) { + return toDto(postService.unpinPost(id)); + } + private PostDto toDto(Post post) { PostDto dto = new PostDto(); dto.setId(post.getId()); @@ -46,6 +56,7 @@ public class AdminPostController { dto.setCategory(toCategoryDto(post.getCategory())); dto.setViews(post.getViews()); dto.setStatus(post.getStatus()); + dto.setPinnedAt(post.getPinnedAt()); return dto; } @@ -69,6 +80,7 @@ public class AdminPostController { private CategoryDto category; private long views; private com.openisle.model.PostStatus status; + private LocalDateTime pinnedAt; } @Data diff --git a/src/main/java/com/openisle/controller/PostController.java b/src/main/java/com/openisle/controller/PostController.java index 61802835b..32a247c93 100644 --- a/src/main/java/com/openisle/controller/PostController.java +++ b/src/main/java/com/openisle/controller/PostController.java @@ -135,6 +135,7 @@ public class PostController { dto.setTags(post.getTags().stream().map(this::toTagDto).collect(Collectors.toList())); dto.setViews(post.getViews()); dto.setStatus(post.getStatus()); + dto.setPinnedAt(post.getPinnedAt()); List reactions = reactionService.getReactionsForPost(post.getId()) .stream() @@ -251,6 +252,7 @@ public class PostController { private java.util.List tags; private long views; private com.openisle.model.PostStatus status; + private LocalDateTime pinnedAt; private List comments; private List reactions; private java.util.List participants; diff --git a/src/main/java/com/openisle/model/Post.java b/src/main/java/com/openisle/model/Post.java index 9c3996207..0f7a752e1 100644 --- a/src/main/java/com/openisle/model/Post.java +++ b/src/main/java/com/openisle/model/Post.java @@ -59,4 +59,7 @@ public class Post { @Column(nullable = false) private PostStatus status = PostStatus.PUBLISHED; + @Column + private LocalDateTime pinnedAt; + } diff --git a/src/main/java/com/openisle/service/PostService.java b/src/main/java/com/openisle/service/PostService.java index 5eacdb3e8..b000316e0 100644 --- a/src/main/java/com/openisle/service/PostService.java +++ b/src/main/java/com/openisle/service/PostService.java @@ -173,22 +173,14 @@ public class PostService { java.util.List tagIds, Integer page, Integer pageSize) { - Pageable pageable = null; - if (page != null && pageSize != null) { - pageable = PageRequest.of(page, pageSize, Sort.Direction.DESC, "createdAt"); - } - boolean hasCategories = categoryIds != null && !categoryIds.isEmpty(); boolean hasTags = tagIds != null && !tagIds.isEmpty(); - if (!hasCategories && !hasTags) { - if (pageable != null) { - return postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED, pageable); - } - return postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED); - } + java.util.List posts; - if (hasCategories) { + if (!hasCategories && !hasTags) { + posts = postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED); + } else if (hasCategories) { java.util.List categories = categoryRepository.findAllById(categoryIds); if (categories.isEmpty()) { return java.util.List.of(); @@ -198,49 +190,33 @@ public class PostService { if (tags.isEmpty()) { return java.util.List.of(); } - if (pageable != null) { - return postRepository.findByCategoriesAndAllTagsOrderByViewsDesc( - categories, tags, PostStatus.PUBLISHED, tags.size(), pageable); - } - return postRepository.findByCategoriesAndAllTagsOrderByViewsDesc( + posts = postRepository.findByCategoriesAndAllTagsOrderByViewsDesc( categories, tags, PostStatus.PUBLISHED, tags.size()); + } else { + posts = postRepository.findByCategoryInAndStatusOrderByViewsDesc(categories, PostStatus.PUBLISHED); } - if (pageable != null) { - return postRepository.findByCategoryInAndStatusOrderByViewsDesc(categories, PostStatus.PUBLISHED, pageable); + } else { + java.util.List tags = tagRepository.findAllById(tagIds); + if (tags.isEmpty()) { + return java.util.List.of(); } - return postRepository.findByCategoryInAndStatusOrderByViewsDesc(categories, PostStatus.PUBLISHED); + posts = postRepository.findByAllTagsOrderByViewsDesc(tags, PostStatus.PUBLISHED, tags.size()); } - java.util.List tags = tagRepository.findAllById(tagIds); - if (tags.isEmpty()) { - return java.util.List.of(); - } - if (pageable != null) { - return postRepository.findByAllTagsOrderByViewsDesc(tags, PostStatus.PUBLISHED, tags.size(), pageable); - } - return postRepository.findByAllTagsOrderByViewsDesc(tags, PostStatus.PUBLISHED, tags.size()); + return paginate(sortByPinnedAndViews(posts), page, pageSize); } public List listPostsByCategories(java.util.List categoryIds, Integer page, Integer pageSize) { - Pageable pageable = null; - if (page != null && pageSize != null) { - pageable = PageRequest.of(page, pageSize, Sort.Direction.DESC, "createdAt"); - } - if (categoryIds == null || categoryIds.isEmpty()) { - if (pageable != null) { - return postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable); - } - return postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED); + java.util.List posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED); + return paginate(sortByPinnedAndCreated(posts), page, pageSize); } java.util.List categories = categoryRepository.findAllById(categoryIds); - if (pageable != null) { - return postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED, pageable); - } - return postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED); + java.util.List posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED); + return paginate(sortByPinnedAndCreated(posts), page, pageSize); } public List getRecentPostsByUser(String username, int limit) { @@ -266,20 +242,13 @@ public class PostService { return java.util.List.of(); } - Pageable pageable = null; - if (page != null && pageSize != null) { - pageable = PageRequest.of(page, pageSize, Sort.Direction.DESC, "createdAt"); - } - java.util.List tags = tagRepository.findAllById(tagIds); if (tags.isEmpty()) { return java.util.List.of(); } - if (pageable != null) { - return postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size(), pageable); - } - return postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size()); + java.util.List posts = postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size()); + return paginate(sortByPinnedAndCreated(posts), page, pageSize); } public List listPostsByCategoriesAndTags(java.util.List categoryIds, @@ -290,21 +259,14 @@ public class PostService { return java.util.List.of(); } - Pageable pageable = null; - if (page != null && pageSize != null) { - pageable = PageRequest.of(page, pageSize); - } - java.util.List categories = categoryRepository.findAllById(categoryIds); java.util.List tags = tagRepository.findAllById(tagIds); if (categories.isEmpty() || tags.isEmpty()) { return java.util.List.of(); } - if (pageable != null) { - return postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc(categories, tags, PostStatus.PUBLISHED, tags.size(), pageable); - } - return postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc(categories, tags, PostStatus.PUBLISHED, tags.size()); + java.util.List posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc(categories, tags, PostStatus.PUBLISHED, tags.size()); + return paginate(sortByPinnedAndCreated(posts), page, pageSize); } public List listPendingPosts() { @@ -347,6 +309,20 @@ public class PostService { return post; } + public Post pinPost(Long id) { + Post post = postRepository.findById(id) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); + post.setPinnedAt(java.time.LocalDateTime.now()); + return postRepository.save(post); + } + + public Post unpinPost(Long id) { + Post post = postRepository.findById(id) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); + post.setPinnedAt(null); + return postRepository.save(post); + } + @org.springframework.transaction.annotation.Transactional public void deletePost(Long id, String username) { Post post = postRepository.findById(id) @@ -377,4 +353,32 @@ public class PostService { public long countPostsByTag(Long tagId) { return postRepository.countDistinctByTags_Id(tagId); } + + private java.util.List sortByPinnedAndCreated(java.util.List posts) { + return posts.stream() + .sorted(java.util.Comparator + .comparing(Post::getPinnedAt, java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder())) + .thenComparing(Post::getCreatedAt, java.util.Comparator.reverseOrder())) + .toList(); + } + + private java.util.List sortByPinnedAndViews(java.util.List posts) { + return posts.stream() + .sorted(java.util.Comparator + .comparing(Post::getPinnedAt, java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder())) + .thenComparing(Post::getViews, java.util.Comparator.reverseOrder())) + .toList(); + } + + private java.util.List paginate(java.util.List posts, Integer page, Integer pageSize) { + if (page == null || pageSize == null) { + return posts; + } + int from = page * pageSize; + if (from >= posts.size()) { + return java.util.List.of(); + } + int to = Math.min(from + pageSize, posts.size()); + return posts.subList(from, to); + } }