diff --git a/open-isle-cli/src/views/HomePageView.vue b/open-isle-cli/src/views/HomePageView.vue index 4d4ebf2f2..a8a20014a 100644 --- a/open-isle-cli/src/views/HomePageView.vue +++ b/open-isle-cli/src/views/HomePageView.vue @@ -151,8 +151,14 @@ export default { const tagOptions = ref([]) const categoryOptions = ref([]) const isLoadingPosts = ref(false) - const topics = ref(['最新', '排行榜' /*, '热门', '类别'*/]) - const selectedTopic = ref(route.query.view === 'ranking' ? '排行榜' : '最新') + const topics = ref(['最新', '最新回复', '排行榜' /*, '热门', '类别'*/]) + const selectedTopic = ref( + route.query.view === 'ranking' + ? '排行榜' + : route.query.view === 'latest-reply' + ? '最新回复' + : '最新' + ) const articles = ref([]) const page = ref(0) @@ -209,6 +215,19 @@ export default { return url } + const buildReplyUrl = () => { + let url = `${API_BASE_URL}/api/posts/latest-reply?page=${page.value}&pageSize=${pageSize}` + if (selectedCategory.value) { + url += `&categoryId=${selectedCategory.value}` + } + if (selectedTags.value.length) { + selectedTags.value.forEach(t => { + url += `&tagIds=${t}` + }) + } + return url + } + const fetchPosts = async (reset = false) => { if (reset) { page.value = 0 @@ -288,11 +307,50 @@ export default { } } + const fetchLatestReply = async (reset = false) => { + if (reset) { + page.value = 0 + allLoaded.value = false + articles.value = [] + } + if (isLoadingPosts.value || allLoaded.value) return + try { + isLoadingPosts.value = true + const res = await fetch(buildReplyUrl()) + isLoadingPosts.value = false + if (!res.ok) return + const data = await res.json() + articles.value.push( + ...data.map(p => ({ + id: p.id, + title: p.title, + description: p.content, + category: p.category, + tags: p.tags || [], + members: (p.participants || []).map(m => ({ id: m.id, avatar: m.avatar })), + comments: (p.comments || []).length, + views: p.views, + time: TimeManager.format(p.lastReplyAt || p.createdAt), + pinned: !!p.pinnedAt + })) + ) + if (data.length < pageSize) { + allLoaded.value = true + } else { + page.value += 1 + } + } catch (e) { + console.error(e) + } + } + const handleScroll = (e) => { const el = e.target if (el.scrollHeight - el.scrollTop <= el.clientHeight + 50) { if (selectedTopic.value === '排行榜') { fetchRanking() + } else if (selectedTopic.value === '最新回复') { + fetchLatestReply() } else { fetchPosts() } @@ -303,6 +361,8 @@ export default { await loadOptions() if (selectedTopic.value === '排行榜') { fetchRanking() + } else if (selectedTopic.value === '最新回复') { + fetchLatestReply() } else { fetchPosts() } @@ -313,12 +373,16 @@ export default { fetchPosts(true) } else if (selectedTopic.value === '排行榜') { fetchRanking(true) + } else if (selectedTopic.value === '最新回复') { + fetchLatestReply(true) } }) watch(selectedTopic, () => { if (selectedTopic.value === '排行榜') { fetchRanking(true) + } else if (selectedTopic.value === '最新回复') { + fetchLatestReply(true) } else { fetchPosts(true) } diff --git a/src/main/java/com/openisle/controller/PostController.java b/src/main/java/com/openisle/controller/PostController.java index 06cfe7c3f..20d5ef5b7 100644 --- a/src/main/java/com/openisle/controller/PostController.java +++ b/src/main/java/com/openisle/controller/PostController.java @@ -132,6 +132,31 @@ public class PostController { .stream().map(this::toDto).collect(Collectors.toList()); } + @GetMapping("/latest-reply") + public List latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, + @RequestParam(value = "categoryIds", required = false) List categoryIds, + @RequestParam(value = "tagId", required = false) Long tagId, + @RequestParam(value = "tagIds", required = false) List tagIds, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "pageSize", required = false) Integer pageSize, + Authentication auth) { + List ids = categoryIds; + if (categoryId != null) { + ids = java.util.List.of(categoryId); + } + List tids = tagIds; + if (tagId != null) { + tids = java.util.List.of(tagId); + } + + if (auth != null) { + userVisitService.recordVisit(auth.getName()); + } + + return postService.listPostsByLatestReply(ids, tids, page, pageSize) + .stream().map(this::toDto).collect(Collectors.toList()); + } + private PostDto toDto(Post post) { PostDto dto = new PostDto(); dto.setId(post.getId()); @@ -160,6 +185,9 @@ public class PostController { java.util.List participants = commentService.getParticipants(post.getId(), 5); dto.setParticipants(participants.stream().map(this::toAuthorDto).collect(Collectors.toList())); + java.time.LocalDateTime last = commentService.getLastCommentTime(post.getId()); + dto.setLastReplyAt(last != null ? last : post.getCreatedAt()); + return dto; } @@ -261,6 +289,7 @@ public class PostController { private long views; private com.openisle.model.PostStatus status; private LocalDateTime pinnedAt; + private LocalDateTime lastReplyAt; private List comments; private List reactions; private java.util.List participants; diff --git a/src/main/java/com/openisle/repository/CommentRepository.java b/src/main/java/com/openisle/repository/CommentRepository.java index f76a7e099..07ab209e8 100644 --- a/src/main/java/com/openisle/repository/CommentRepository.java +++ b/src/main/java/com/openisle/repository/CommentRepository.java @@ -16,4 +16,7 @@ public interface CommentRepository extends JpaRepository { @org.springframework.data.jpa.repository.Query("SELECT DISTINCT c.author FROM Comment c WHERE c.post = :post") java.util.List findDistinctAuthorsByPost(@org.springframework.data.repository.query.Param("post") Post post); + + @org.springframework.data.jpa.repository.Query("SELECT MAX(c.createdAt) FROM Comment c WHERE c.post = :post") + java.time.LocalDateTime findLastCommentTime(@org.springframework.data.repository.query.Param("post") Post post); } diff --git a/src/main/java/com/openisle/service/CommentService.java b/src/main/java/com/openisle/service/CommentService.java index 0fc74d272..e30408c2b 100644 --- a/src/main/java/com/openisle/service/CommentService.java +++ b/src/main/java/com/openisle/service/CommentService.java @@ -123,6 +123,12 @@ public class CommentService { return commentRepository.findAllById(ids); } + public java.time.LocalDateTime getLastCommentTime(Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); + return commentRepository.findLastCommentTime(post); + } + @org.springframework.transaction.annotation.Transactional public void deleteComment(String username, Long id) { User user = userRepository.findByUsername(username) diff --git a/src/main/java/com/openisle/service/PostService.java b/src/main/java/com/openisle/service/PostService.java index 8de2bc3ad..789fd74d3 100644 --- a/src/main/java/com/openisle/service/PostService.java +++ b/src/main/java/com/openisle/service/PostService.java @@ -206,6 +206,47 @@ public class PostService { return paginate(sortByPinnedAndViews(posts), page, pageSize); } + public List listPostsByLatestReply(Integer page, Integer pageSize) { + return listPostsByLatestReply(null, null, page, pageSize); + } + + public List listPostsByLatestReply(java.util.List categoryIds, + java.util.List tagIds, + Integer page, + Integer pageSize) { + boolean hasCategories = categoryIds != null && !categoryIds.isEmpty(); + boolean hasTags = tagIds != null && !tagIds.isEmpty(); + + java.util.List posts; + + if (!hasCategories && !hasTags) { + posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED); + } else if (hasCategories) { + java.util.List categories = categoryRepository.findAllById(categoryIds); + if (categories.isEmpty()) { + return java.util.List.of(); + } + if (hasTags) { + java.util.List tags = tagRepository.findAllById(tagIds); + if (tags.isEmpty()) { + return java.util.List.of(); + } + posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc( + categories, tags, PostStatus.PUBLISHED, tags.size()); + } else { + posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED); + } + } else { + java.util.List tags = tagRepository.findAllById(tagIds); + if (tags.isEmpty()) { + return java.util.List.of(); + } + posts = postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size()); + } + + return paginate(sortByPinnedAndLastReply(posts), page, pageSize); + } + public List listPostsByCategories(java.util.List categoryIds, Integer page, Integer pageSize) { @@ -403,6 +444,17 @@ public class PostService { .toList(); } + private java.util.List sortByPinnedAndLastReply(java.util.List posts) { + return posts.stream() + .sorted(java.util.Comparator + .comparing(Post::getPinnedAt, java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder())) + .thenComparing(p -> { + java.time.LocalDateTime t = commentRepository.findLastCommentTime(p); + return t != null ? t : p.getCreatedAt(); + }, java.util.Comparator.nullsLast(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;