mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-26 16:10:49 +08:00
Compare commits
6 Commits
bugfix/113
...
680e44e743
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
680e44e743 | ||
|
|
657b76bb1e | ||
|
|
f695db62c6 | ||
|
|
e5b386cdc2 | ||
|
|
179699dd66 | ||
|
|
ef39b5fedf |
@@ -57,6 +57,14 @@ cd OpenIsle
|
||||
--profile dev up -d --force-recreate
|
||||
```
|
||||
|
||||
仅重启后端容器(不重建镜像、不影响前端):
|
||||
```shell
|
||||
docker compose \
|
||||
-f docker/docker-compose.yaml \
|
||||
--env-file .env \
|
||||
--profile dev restart springboot websocket-service
|
||||
```
|
||||
|
||||
数据初始化sql会创建几个帐户供大家测试使用
|
||||
> username:admin/user1/user2 password:123456
|
||||
|
||||
|
||||
@@ -112,7 +112,9 @@ public class CommentController {
|
||||
)
|
||||
public List<TimelineItemDto<?>> listComments(
|
||||
@PathVariable Long postId,
|
||||
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort
|
||||
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort,
|
||||
@RequestParam(value = "page", required = false, defaultValue = "0") int page,
|
||||
@RequestParam(value = "pageSize", required = false, defaultValue = "20") int pageSize
|
||||
) {
|
||||
log.debug("listComments called for post {} with sort {}", postId, sort);
|
||||
List<CommentDto> commentDtoList = commentService
|
||||
@@ -183,8 +185,23 @@ public class CommentController {
|
||||
createdAtComparator = createdAtComparator.reversed();
|
||||
}
|
||||
itemDtoList.sort(comparator.thenComparing(createdAtComparator));
|
||||
log.debug("listComments returning {} comments", itemDtoList.size());
|
||||
return itemDtoList;
|
||||
|
||||
int safePage = Math.max(0, page);
|
||||
int safePageSize = Math.max(1, pageSize);
|
||||
int fromIndex = safePage * safePageSize;
|
||||
int toIndex = Math.min(fromIndex + safePageSize, itemDtoList.size());
|
||||
List<TimelineItemDto<?>> pagedItems =
|
||||
fromIndex >= itemDtoList.size() ? List.of() : itemDtoList.subList(fromIndex, toIndex);
|
||||
|
||||
log.debug(
|
||||
"listComments returning {} items for post {} page {} size {} (total {})",
|
||||
pagedItems.size(),
|
||||
postId,
|
||||
safePage,
|
||||
safePageSize,
|
||||
itemDtoList.size()
|
||||
);
|
||||
return pagedItems;
|
||||
}
|
||||
|
||||
@GetMapping("/comments/{commentId}/context")
|
||||
|
||||
@@ -217,11 +217,7 @@ public class PostController {
|
||||
// userVisitService.recordVisit(auth.getName());
|
||||
// }
|
||||
|
||||
return postService
|
||||
.defaultListPosts(ids, tids, page, pageSize)
|
||||
.stream()
|
||||
.map(postMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
return postMapper.toListDtos(postService.defaultListPosts(ids, tids, page, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/recent")
|
||||
@@ -269,11 +265,7 @@ public class PostController {
|
||||
// userVisitService.recordVisit(auth.getName());
|
||||
// }
|
||||
|
||||
return postService
|
||||
.listPostsByViews(ids, tids, page, pageSize)
|
||||
.stream()
|
||||
.map(postMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
return postMapper.toListDtos(postService.listPostsByViews(ids, tids, page, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/latest-reply")
|
||||
@@ -305,8 +297,7 @@ public class PostController {
|
||||
// userVisitService.recordVisit(auth.getName());
|
||||
// }
|
||||
|
||||
List<Post> posts = postService.listPostsByLatestReply(ids, tids, page, pageSize);
|
||||
return posts.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||
return postMapper.toListDtos(postService.listPostsByLatestReply(ids, tids, page, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/featured")
|
||||
@@ -333,10 +324,6 @@ public class PostController {
|
||||
// if (auth != null) {
|
||||
// userVisitService.recordVisit(auth.getName());
|
||||
// }
|
||||
return postService
|
||||
.listFeaturedPosts(ids, tids, page, pageSize)
|
||||
.stream()
|
||||
.map(postMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
return postMapper.toListDtos(postService.listFeaturedPosts(ids, tids, page, pageSize));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,38 @@ public class PostMapper {
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<PostSummaryDto> toListDtos(List<Post> posts) {
|
||||
if (posts == null || posts.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, List<User>> participantsMap = commentService.getParticipantsForPosts(posts, 5);
|
||||
return posts
|
||||
.stream()
|
||||
.map(post -> {
|
||||
PostSummaryDto dto = new PostSummaryDto();
|
||||
applyListFields(post, dto);
|
||||
List<User> participants = participantsMap.get(post.getId());
|
||||
if (participants != null) {
|
||||
dto.setParticipants(
|
||||
participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList())
|
||||
);
|
||||
} else {
|
||||
dto.setParticipants(List.of());
|
||||
}
|
||||
dto.setReactions(List.of());
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public PostSummaryDto toListDto(Post post) {
|
||||
PostSummaryDto dto = new PostSummaryDto();
|
||||
applyListFields(post, dto);
|
||||
dto.setParticipants(List.of());
|
||||
dto.setReactions(List.of());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public PostDetailDto toDetailDto(Post post, String viewer) {
|
||||
PostDetailDto dto = new PostDetailDto();
|
||||
applyCommon(post, dto);
|
||||
@@ -61,6 +93,25 @@ public class PostMapper {
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void applyListFields(Post post, PostSummaryDto dto) {
|
||||
dto.setId(post.getId());
|
||||
dto.setTitle(post.getTitle());
|
||||
dto.setContent(post.getContent());
|
||||
dto.setCreatedAt(post.getCreatedAt());
|
||||
dto.setAuthor(userMapper.toAuthorDto(post.getAuthor()));
|
||||
dto.setCategory(categoryMapper.toDto(post.getCategory()));
|
||||
dto.setTags(post.getTags().stream().map(tagMapper::toDto).collect(Collectors.toList()));
|
||||
dto.setViews(post.getViews());
|
||||
dto.setCommentCount(post.getCommentCount());
|
||||
dto.setStatus(post.getStatus());
|
||||
dto.setPinnedAt(post.getPinnedAt());
|
||||
dto.setLastReplyAt(post.getLastReplyAt());
|
||||
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
|
||||
dto.setClosed(post.isClosed());
|
||||
dto.setVisibleScope(post.getVisibleScope());
|
||||
dto.setType(post.getType());
|
||||
}
|
||||
|
||||
private void applyCommon(Post post, PostSummaryDto dto) {
|
||||
dto.setId(post.getId());
|
||||
dto.setTitle(post.getTitle());
|
||||
|
||||
@@ -25,6 +25,13 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
|
||||
@org.springframework.data.repository.query.Param("post") Post post
|
||||
);
|
||||
|
||||
@org.springframework.data.jpa.repository.Query(
|
||||
"SELECT DISTINCT c.post.id, c.author FROM Comment c WHERE c.post.id IN :postIds"
|
||||
)
|
||||
java.util.List<Object[]> findDistinctAuthorsByPostIds(
|
||||
@org.springframework.data.repository.query.Param("postIds") java.util.List<Long> postIds
|
||||
);
|
||||
|
||||
@org.springframework.data.jpa.repository.Query(
|
||||
"SELECT MAX(c.createdAt) FROM Comment c WHERE c.post = :post"
|
||||
)
|
||||
|
||||
@@ -19,6 +19,8 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
List<Post> findByStatusOrderByCreatedAtDesc(PostStatus status, Pageable pageable);
|
||||
List<Post> findByStatusOrderByViewsDesc(PostStatus status);
|
||||
List<Post> findByStatusOrderByViewsDesc(PostStatus status, Pageable pageable);
|
||||
List<Post> findByStatusOrderByPinnedAtDescViewsDesc(PostStatus status, Pageable pageable);
|
||||
List<Post> findByStatusOrderByPinnedAtDescLastReplyAtDesc(PostStatus status, Pageable pageable);
|
||||
List<Post> findByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc(
|
||||
PostStatus status,
|
||||
LocalDateTime createdAt
|
||||
@@ -43,6 +45,16 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
PostStatus status,
|
||||
Pageable pageable
|
||||
);
|
||||
List<Post> findByCategoryInAndStatusOrderByPinnedAtDescViewsDesc(
|
||||
List<Category> categories,
|
||||
PostStatus status,
|
||||
Pageable pageable
|
||||
);
|
||||
List<Post> findByCategoryInAndStatusOrderByPinnedAtDescLastReplyAtDesc(
|
||||
List<Category> categories,
|
||||
PostStatus status,
|
||||
Pageable pageable
|
||||
);
|
||||
List<Post> findDistinctByTagsInAndStatus(List<Tag> tags, PostStatus status);
|
||||
List<Post> findDistinctByTagsInAndStatus(List<Tag> tags, PostStatus status, Pageable pageable);
|
||||
List<Post> findDistinctByTagsInAndStatusOrderByCreatedAtDesc(List<Tag> tags, PostStatus status);
|
||||
@@ -132,6 +144,26 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query(
|
||||
"SELECT p FROM Post p JOIN p.tags t WHERE t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.views DESC"
|
||||
)
|
||||
List<Post> findByAllTagsOrderByPinnedAtDescViewsDesc(
|
||||
@Param("tags") List<Tag> tags,
|
||||
@Param("status") PostStatus status,
|
||||
@Param("tagCount") long tagCount,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query(
|
||||
"SELECT p FROM Post p JOIN p.tags t WHERE t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.lastReplyAt DESC"
|
||||
)
|
||||
List<Post> findByAllTagsOrderByPinnedAtDescLastReplyAtDesc(
|
||||
@Param("tags") List<Tag> tags,
|
||||
@Param("status") PostStatus status,
|
||||
@Param("tagCount") long tagCount,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query(
|
||||
"SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount"
|
||||
)
|
||||
@@ -174,6 +206,28 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query(
|
||||
"SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.views DESC"
|
||||
)
|
||||
List<Post> findByCategoriesAndAllTagsOrderByPinnedAtDescViewsDesc(
|
||||
@Param("categories") List<Category> categories,
|
||||
@Param("tags") List<Tag> tags,
|
||||
@Param("status") PostStatus status,
|
||||
@Param("tagCount") long tagCount,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query(
|
||||
"SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.lastReplyAt DESC"
|
||||
)
|
||||
List<Post> findByCategoriesAndAllTagsOrderByPinnedAtDescLastReplyAtDesc(
|
||||
@Param("categories") List<Category> categories,
|
||||
@Param("tags") List<Tag> tags,
|
||||
@Param("status") PostStatus status,
|
||||
@Param("tagCount") long tagCount,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query(
|
||||
"SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.createdAt DESC"
|
||||
)
|
||||
|
||||
@@ -21,8 +21,12 @@ import com.openisle.service.NotificationService;
|
||||
import com.openisle.service.PointService;
|
||||
import com.openisle.service.SubscriptionService;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -316,6 +320,37 @@ public class CommentService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<Long, List<User>> getParticipantsForPosts(List<Post> posts, int limit) {
|
||||
if (posts == null || posts.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<Long, LinkedHashSet<User>> map = new HashMap<>();
|
||||
List<Long> postIds = new ArrayList<>(posts.size());
|
||||
for (Post post : posts) {
|
||||
postIds.add(post.getId());
|
||||
LinkedHashSet<User> set = new LinkedHashSet<>();
|
||||
set.add(post.getAuthor());
|
||||
map.put(post.getId(), set);
|
||||
}
|
||||
for (Object[] row : commentRepository.findDistinctAuthorsByPostIds(postIds)) {
|
||||
Long postId = (Long) row[0];
|
||||
User author = (User) row[1];
|
||||
LinkedHashSet<User> set = map.get(postId);
|
||||
if (set != null) {
|
||||
set.add(author);
|
||||
}
|
||||
}
|
||||
Map<Long, List<User>> result = new HashMap<>(map.size());
|
||||
for (Map.Entry<Long, LinkedHashSet<User>> entry : map.entrySet()) {
|
||||
List<User> list = new ArrayList<>(entry.getValue());
|
||||
if (list.size() > limit) {
|
||||
list = list.subList(0, limit);
|
||||
}
|
||||
result.put(entry.getKey(), list);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public java.util.List<Comment> getCommentsByIds(java.util.List<Long> ids) {
|
||||
log.debug("getCommentsByIds called for ids {}", ids);
|
||||
java.util.List<Comment> comments = commentRepository.findAllById(ids);
|
||||
|
||||
@@ -339,6 +339,7 @@ public class PostService {
|
||||
post.setCategory(category);
|
||||
post.setTags(new HashSet<>(tags));
|
||||
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
|
||||
post.setLastReplyAt(LocalDateTime.now());
|
||||
|
||||
// 什么都没设置的情况下,默认为ALL
|
||||
if (Objects.isNull(postVisibleScopeType)) {
|
||||
@@ -809,9 +810,10 @@ public class PostService {
|
||||
boolean hasTags = tagIds != null && !tagIds.isEmpty();
|
||||
|
||||
java.util.List<Post> posts;
|
||||
Pageable pageable = buildPageable(page, pageSize);
|
||||
|
||||
if (!hasCategories && !hasTags) {
|
||||
posts = postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED);
|
||||
posts = postRepository.findByStatusOrderByPinnedAtDescViewsDesc(PostStatus.PUBLISHED, pageable);
|
||||
} else if (hasCategories) {
|
||||
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
||||
if (categories.isEmpty()) {
|
||||
@@ -822,16 +824,18 @@ public class PostService {
|
||||
if (tags.isEmpty()) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
posts = postRepository.findByCategoriesAndAllTagsOrderByViewsDesc(
|
||||
posts = postRepository.findByCategoriesAndAllTagsOrderByPinnedAtDescViewsDesc(
|
||||
categories,
|
||||
tags,
|
||||
PostStatus.PUBLISHED,
|
||||
tags.size()
|
||||
tags.size(),
|
||||
pageable
|
||||
);
|
||||
} else {
|
||||
posts = postRepository.findByCategoryInAndStatusOrderByViewsDesc(
|
||||
posts = postRepository.findByCategoryInAndStatusOrderByPinnedAtDescViewsDesc(
|
||||
categories,
|
||||
PostStatus.PUBLISHED
|
||||
PostStatus.PUBLISHED,
|
||||
pageable
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -839,10 +843,15 @@ public class PostService {
|
||||
if (tags.isEmpty()) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
posts = postRepository.findByAllTagsOrderByViewsDesc(tags, PostStatus.PUBLISHED, tags.size());
|
||||
posts = postRepository.findByAllTagsOrderByPinnedAtDescViewsDesc(
|
||||
tags,
|
||||
PostStatus.PUBLISHED,
|
||||
tags.size(),
|
||||
pageable
|
||||
);
|
||||
}
|
||||
|
||||
return paginate(sortByPinnedAndViews(posts), page, pageSize);
|
||||
return posts;
|
||||
}
|
||||
|
||||
public List<Post> listPostsByLatestReply(Integer page, Integer pageSize) {
|
||||
@@ -859,9 +868,13 @@ public class PostService {
|
||||
boolean hasTags = tagIds != null && !tagIds.isEmpty();
|
||||
|
||||
java.util.List<Post> posts;
|
||||
Pageable pageable = buildPageable(page, pageSize);
|
||||
|
||||
if (!hasCategories && !hasTags) {
|
||||
posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED);
|
||||
posts = postRepository.findByStatusOrderByPinnedAtDescLastReplyAtDesc(
|
||||
PostStatus.PUBLISHED,
|
||||
pageable
|
||||
);
|
||||
} else if (hasCategories) {
|
||||
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
||||
if (categories.isEmpty()) {
|
||||
@@ -872,16 +885,18 @@ public class PostService {
|
||||
if (tags.isEmpty()) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc(
|
||||
posts = postRepository.findByCategoriesAndAllTagsOrderByPinnedAtDescLastReplyAtDesc(
|
||||
categories,
|
||||
tags,
|
||||
PostStatus.PUBLISHED,
|
||||
tags.size()
|
||||
tags.size(),
|
||||
pageable
|
||||
);
|
||||
} else {
|
||||
posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(
|
||||
posts = postRepository.findByCategoryInAndStatusOrderByPinnedAtDescLastReplyAtDesc(
|
||||
categories,
|
||||
PostStatus.PUBLISHED
|
||||
PostStatus.PUBLISHED,
|
||||
pageable
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -889,14 +904,15 @@ public class PostService {
|
||||
if (tags.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
posts = postRepository.findByAllTagsOrderByCreatedAtDesc(
|
||||
posts = postRepository.findByAllTagsOrderByPinnedAtDescLastReplyAtDesc(
|
||||
tags,
|
||||
PostStatus.PUBLISHED,
|
||||
tags.size()
|
||||
tags.size(),
|
||||
pageable
|
||||
);
|
||||
}
|
||||
|
||||
return paginate(sortByPinnedAndLastReply(posts), page, pageSize);
|
||||
return posts;
|
||||
}
|
||||
|
||||
public List<Post> listPostsByCategories(
|
||||
@@ -1394,6 +1410,13 @@ public class PostService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Pageable buildPageable(Integer page, Integer pageSize) {
|
||||
if (page == null || pageSize == null) {
|
||||
return Pageable.unpaged();
|
||||
}
|
||||
return PageRequest.of(page, pageSize);
|
||||
}
|
||||
|
||||
private List<Post> paginate(List<Post> posts, Integer page, Integer pageSize) {
|
||||
if (page == null || pageSize == null) {
|
||||
return posts;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Backfill last_reply_at for posts without comments to preserve latest-reply ordering
|
||||
UPDATE posts
|
||||
SET last_reply_at = created_at
|
||||
WHERE last_reply_at IS NULL;
|
||||
@@ -159,6 +159,12 @@ const selectedTags = ref([])
|
||||
const route = useRoute()
|
||||
const tagOptions = ref([])
|
||||
const categoryOptions = ref([])
|
||||
const clearFilters = () => {
|
||||
selectedCategory.value = ''
|
||||
selectedTags.value = []
|
||||
selectedCategoryGlobal.value = ''
|
||||
selectedTagsGlobal.value = []
|
||||
}
|
||||
|
||||
const topics = ref(['最新回复', '最新', '精选', '排行榜' /*, '热门', '类别'*/])
|
||||
const selectedTopicCookie = useCookie('homeTab')
|
||||
@@ -218,8 +224,18 @@ watch(
|
||||
(query) => {
|
||||
const category = query.category
|
||||
const tags = query.tags
|
||||
category && selectedCategorySet(category)
|
||||
tags && selectedTagsSet(tags)
|
||||
if (category) {
|
||||
selectedCategorySet(category)
|
||||
} else {
|
||||
selectedCategory.value = ''
|
||||
selectedCategoryGlobal.value = ''
|
||||
}
|
||||
if (tags) {
|
||||
selectedTagsSet(tags)
|
||||
} else {
|
||||
selectedTags.value = []
|
||||
selectedTagsGlobal.value = []
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -367,12 +383,18 @@ watch(selectedTopic, (val) => {
|
||||
if (import.meta.server) {
|
||||
await loadOptions()
|
||||
}
|
||||
|
||||
const handleRefreshHome = () => {
|
||||
clearFilters()
|
||||
refreshFirst()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (categoryOptions.value.length === 0 && tagOptions.value.length === 0) loadOptions()
|
||||
window.addEventListener('refresh-home', refreshFirst)
|
||||
window.addEventListener('refresh-home', handleRefreshHome)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('refresh-home', refreshFirst)
|
||||
window.removeEventListener('refresh-home', handleRefreshHome)
|
||||
})
|
||||
|
||||
/** 供 InfiniteLoadMore 重建用的 key:筛选/Tab 改变即重建内部状态 */
|
||||
|
||||
@@ -181,6 +181,12 @@
|
||||
<PostChangeLogItem v-else :log="item" :title="title" />
|
||||
</template>
|
||||
</BaseTimeline>
|
||||
<InfiniteLoadMore
|
||||
v-if="timelineItems.length > 0"
|
||||
:key="commentSort"
|
||||
:on-load="loadMoreTimeline"
|
||||
:pause="isLoadingMoreComments || isFetchingComments"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -211,6 +217,7 @@ import CommentEditor from '~/components/CommentEditor.vue'
|
||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||
import PostChangeLogItem from '~/components/PostChangeLogItem.vue'
|
||||
import InfiniteLoadMore from '~/components/InfiniteLoadMore.vue'
|
||||
import ArticleTags from '~/components/ArticleTags.vue'
|
||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||
@@ -276,6 +283,10 @@ const currentIndex = ref(1)
|
||||
const subscribed = ref(false)
|
||||
const commentSort = ref('NEWEST')
|
||||
const isFetchingComments = ref(false)
|
||||
const commentPage = ref(0)
|
||||
const commentPageSize = 10
|
||||
const hasMoreComments = ref(true)
|
||||
const isLoadingMoreComments = ref(false)
|
||||
const isMobile = useIsMobile()
|
||||
const timelineItems = ref([])
|
||||
|
||||
@@ -869,17 +880,33 @@ const fetchCommentSorts = () => {
|
||||
])
|
||||
}
|
||||
|
||||
const fetchCommentsAndChangeLog = async () => {
|
||||
isFetchingComments.value = true
|
||||
console.info('Fetching comments and chang log', { postId, sort: commentSort.value })
|
||||
const fetchCommentsAndChangeLog = async ({ pageNo = 0, append = false } = {}) => {
|
||||
if (isLoadingMoreComments.value) return false
|
||||
if (!append) {
|
||||
hasMoreComments.value = true
|
||||
commentPage.value = 0
|
||||
}
|
||||
if (pageNo === 0) {
|
||||
isFetchingComments.value = true
|
||||
} else {
|
||||
isLoadingMoreComments.value = true
|
||||
}
|
||||
console.info('Fetching comments and chang log', {
|
||||
postId,
|
||||
sort: commentSort.value,
|
||||
page: pageNo,
|
||||
pageSize: commentPageSize,
|
||||
})
|
||||
let done = false
|
||||
try {
|
||||
const token = getToken()
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/posts/${postId}/comments?sort=${commentSort.value}`,
|
||||
{
|
||||
headers: { Authorization: token ? `Bearer ${token}` : '' },
|
||||
},
|
||||
)
|
||||
const url = new URL(`${API_BASE_URL}/api/posts/${postId}/comments`)
|
||||
url.searchParams.set('sort', commentSort.value)
|
||||
url.searchParams.set('page', String(pageNo))
|
||||
url.searchParams.set('pageSize', String(commentPageSize))
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { Authorization: token ? `Bearer ${token}` : '' },
|
||||
})
|
||||
console.info('Fetch comments response status', res.status)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
@@ -902,23 +929,48 @@ const fetchCommentsAndChangeLog = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
comments.value = commentList
|
||||
changeLogs.value = changeLogList
|
||||
timelineItems.value = newTimelineItemList
|
||||
if (append) {
|
||||
comments.value.push(...commentList)
|
||||
changeLogs.value.push(...changeLogList)
|
||||
timelineItems.value.push(...newTimelineItemList)
|
||||
commentPage.value = pageNo
|
||||
} else {
|
||||
comments.value = commentList
|
||||
changeLogs.value = changeLogList
|
||||
timelineItems.value = newTimelineItemList
|
||||
commentPage.value = 0
|
||||
}
|
||||
|
||||
isFetchingComments.value = false
|
||||
done = data.length < commentPageSize
|
||||
hasMoreComments.value = !done
|
||||
await nextTick()
|
||||
gatherPostItems()
|
||||
return done
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Fetch comments error', e)
|
||||
hasMoreComments.value = false
|
||||
return true
|
||||
} finally {
|
||||
isFetchingComments.value = false
|
||||
isLoadingMoreComments.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTimeline = async () => {
|
||||
await fetchCommentsAndChangeLog()
|
||||
hasMoreComments.value = true
|
||||
commentPage.value = 0
|
||||
comments.value = []
|
||||
changeLogs.value = []
|
||||
timelineItems.value = []
|
||||
await fetchCommentsAndChangeLog({ pageNo: 0, append: false })
|
||||
}
|
||||
|
||||
const loadMoreTimeline = async () => {
|
||||
if (!hasMoreComments.value || isLoadingMoreComments.value) return true
|
||||
const nextPage = commentPage.value + 1
|
||||
const done = await fetchCommentsAndChangeLog({ pageNo: nextPage, append: true })
|
||||
return done || !hasMoreComments.value
|
||||
}
|
||||
|
||||
watch(commentSort, async () => {
|
||||
@@ -929,8 +981,17 @@ const jumpToHashComment = async () => {
|
||||
const hash = location.hash
|
||||
if (hash.startsWith('#comment-')) {
|
||||
const id = hash.substring('#comment-'.length)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
const el = document.getElementById('comment-' + id)
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
let el = document.getElementById('comment-' + id)
|
||||
|
||||
// 若未加载到目标评论,尝试继续分页加载直到找到或无更多
|
||||
while (!el && hasMoreComments.value) {
|
||||
const done = await loadMoreTimeline()
|
||||
await nextTick()
|
||||
el = document.getElementById('comment-' + id)
|
||||
if (done) break
|
||||
}
|
||||
|
||||
if (el) {
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - headerHeight - 20 // 20 for beauty
|
||||
window.scrollTo({ top, behavior: 'smooth' })
|
||||
|
||||
Reference in New Issue
Block a user