mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-04-21 03:17:28 +08:00
Add latest reply tab and backend support
This commit is contained in:
@@ -151,8 +151,14 @@ export default {
|
|||||||
const tagOptions = ref([])
|
const tagOptions = ref([])
|
||||||
const categoryOptions = ref([])
|
const categoryOptions = ref([])
|
||||||
const isLoadingPosts = ref(false)
|
const isLoadingPosts = ref(false)
|
||||||
const topics = ref(['最新', '排行榜' /*, '热门', '类别'*/])
|
const topics = ref(['最新', '最新回复', '排行榜' /*, '热门', '类别'*/])
|
||||||
const selectedTopic = ref(route.query.view === 'ranking' ? '排行榜' : '最新')
|
const selectedTopic = ref(
|
||||||
|
route.query.view === 'ranking'
|
||||||
|
? '排行榜'
|
||||||
|
: route.query.view === 'latest-reply'
|
||||||
|
? '最新回复'
|
||||||
|
: '最新'
|
||||||
|
)
|
||||||
|
|
||||||
const articles = ref([])
|
const articles = ref([])
|
||||||
const page = ref(0)
|
const page = ref(0)
|
||||||
@@ -209,6 +215,19 @@ export default {
|
|||||||
return url
|
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) => {
|
const fetchPosts = async (reset = false) => {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
page.value = 0
|
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 handleScroll = (e) => {
|
||||||
const el = e.target
|
const el = e.target
|
||||||
if (el.scrollHeight - el.scrollTop <= el.clientHeight + 50) {
|
if (el.scrollHeight - el.scrollTop <= el.clientHeight + 50) {
|
||||||
if (selectedTopic.value === '排行榜') {
|
if (selectedTopic.value === '排行榜') {
|
||||||
fetchRanking()
|
fetchRanking()
|
||||||
|
} else if (selectedTopic.value === '最新回复') {
|
||||||
|
fetchLatestReply()
|
||||||
} else {
|
} else {
|
||||||
fetchPosts()
|
fetchPosts()
|
||||||
}
|
}
|
||||||
@@ -303,6 +361,8 @@ export default {
|
|||||||
await loadOptions()
|
await loadOptions()
|
||||||
if (selectedTopic.value === '排行榜') {
|
if (selectedTopic.value === '排行榜') {
|
||||||
fetchRanking()
|
fetchRanking()
|
||||||
|
} else if (selectedTopic.value === '最新回复') {
|
||||||
|
fetchLatestReply()
|
||||||
} else {
|
} else {
|
||||||
fetchPosts()
|
fetchPosts()
|
||||||
}
|
}
|
||||||
@@ -313,12 +373,16 @@ export default {
|
|||||||
fetchPosts(true)
|
fetchPosts(true)
|
||||||
} else if (selectedTopic.value === '排行榜') {
|
} else if (selectedTopic.value === '排行榜') {
|
||||||
fetchRanking(true)
|
fetchRanking(true)
|
||||||
|
} else if (selectedTopic.value === '最新回复') {
|
||||||
|
fetchLatestReply(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(selectedTopic, () => {
|
watch(selectedTopic, () => {
|
||||||
if (selectedTopic.value === '排行榜') {
|
if (selectedTopic.value === '排行榜') {
|
||||||
fetchRanking(true)
|
fetchRanking(true)
|
||||||
|
} else if (selectedTopic.value === '最新回复') {
|
||||||
|
fetchLatestReply(true)
|
||||||
} else {
|
} else {
|
||||||
fetchPosts(true)
|
fetchPosts(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,31 @@ public class PostController {
|
|||||||
.stream().map(this::toDto).collect(Collectors.toList());
|
.stream().map(this::toDto).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/latest-reply")
|
||||||
|
public List<PostDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||||
|
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||||
|
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||||
|
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
Authentication auth) {
|
||||||
|
List<Long> ids = categoryIds;
|
||||||
|
if (categoryId != null) {
|
||||||
|
ids = java.util.List.of(categoryId);
|
||||||
|
}
|
||||||
|
List<Long> 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) {
|
private PostDto toDto(Post post) {
|
||||||
PostDto dto = new PostDto();
|
PostDto dto = new PostDto();
|
||||||
dto.setId(post.getId());
|
dto.setId(post.getId());
|
||||||
@@ -160,6 +185,9 @@ public class PostController {
|
|||||||
java.util.List<com.openisle.model.User> participants = commentService.getParticipants(post.getId(), 5);
|
java.util.List<com.openisle.model.User> participants = commentService.getParticipants(post.getId(), 5);
|
||||||
dto.setParticipants(participants.stream().map(this::toAuthorDto).collect(Collectors.toList()));
|
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;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +289,7 @@ public class PostController {
|
|||||||
private long views;
|
private long views;
|
||||||
private com.openisle.model.PostStatus status;
|
private com.openisle.model.PostStatus status;
|
||||||
private LocalDateTime pinnedAt;
|
private LocalDateTime pinnedAt;
|
||||||
|
private LocalDateTime lastReplyAt;
|
||||||
private List<CommentDto> comments;
|
private List<CommentDto> comments;
|
||||||
private List<ReactionDto> reactions;
|
private List<ReactionDto> reactions;
|
||||||
private java.util.List<AuthorDto> participants;
|
private java.util.List<AuthorDto> participants;
|
||||||
|
|||||||
@@ -16,4 +16,7 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
|
|||||||
|
|
||||||
@org.springframework.data.jpa.repository.Query("SELECT DISTINCT c.author FROM Comment c WHERE c.post = :post")
|
@org.springframework.data.jpa.repository.Query("SELECT DISTINCT c.author FROM Comment c WHERE c.post = :post")
|
||||||
java.util.List<User> findDistinctAuthorsByPost(@org.springframework.data.repository.query.Param("post") Post post);
|
java.util.List<User> 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,12 @@ public class CommentService {
|
|||||||
return commentRepository.findAllById(ids);
|
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
|
@org.springframework.transaction.annotation.Transactional
|
||||||
public void deleteComment(String username, Long id) {
|
public void deleteComment(String username, Long id) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
|
|||||||
@@ -206,6 +206,47 @@ public class PostService {
|
|||||||
return paginate(sortByPinnedAndViews(posts), page, pageSize);
|
return paginate(sortByPinnedAndViews(posts), page, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Post> listPostsByLatestReply(Integer page, Integer pageSize) {
|
||||||
|
return listPostsByLatestReply(null, null, page, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Post> listPostsByLatestReply(java.util.List<Long> categoryIds,
|
||||||
|
java.util.List<Long> tagIds,
|
||||||
|
Integer page,
|
||||||
|
Integer pageSize) {
|
||||||
|
boolean hasCategories = categoryIds != null && !categoryIds.isEmpty();
|
||||||
|
boolean hasTags = tagIds != null && !tagIds.isEmpty();
|
||||||
|
|
||||||
|
java.util.List<Post> posts;
|
||||||
|
|
||||||
|
if (!hasCategories && !hasTags) {
|
||||||
|
posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED);
|
||||||
|
} else if (hasCategories) {
|
||||||
|
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
||||||
|
if (categories.isEmpty()) {
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
if (hasTags) {
|
||||||
|
java.util.List<com.openisle.model.Tag> 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<com.openisle.model.Tag> 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<Post> listPostsByCategories(java.util.List<Long> categoryIds,
|
public List<Post> listPostsByCategories(java.util.List<Long> categoryIds,
|
||||||
Integer page,
|
Integer page,
|
||||||
Integer pageSize) {
|
Integer pageSize) {
|
||||||
@@ -403,6 +444,17 @@ public class PostService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private java.util.List<Post> sortByPinnedAndLastReply(java.util.List<Post> 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<Post> paginate(java.util.List<Post> posts, Integer page, Integer pageSize) {
|
private java.util.List<Post> paginate(java.util.List<Post> posts, Integer page, Integer pageSize) {
|
||||||
if (page == null || pageSize == null) {
|
if (page == null || pageSize == null) {
|
||||||
return posts;
|
return posts;
|
||||||
|
|||||||
Reference in New Issue
Block a user