mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 22:21:09 +08:00
Merge pull request #246 from nagisa77/codex/add-article-pinning-feature
Add post pinning feature
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
<div class="article-item" v-for="article in articles" :key="article.id">
|
||||
<div class="article-main-container">
|
||||
<router-link class="article-item-title main-item" :to="`/posts/${article.id}`">
|
||||
<i v-if="article.pinned" class="fas fa-thumbtack pinned-icon"></i>
|
||||
{{ article.title }}
|
||||
</router-link>
|
||||
<div class="article-item-description main-item">{{ sanitizeDescription(article.description) }}</div>
|
||||
@@ -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;
|
||||
|
||||
@@ -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('')
|
||||
@@ -158,6 +159,13 @@ export default {
|
||||
items.push({ text: '编辑文章', onClick: () => editPost() })
|
||||
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() })
|
||||
@@ -277,6 +285,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()
|
||||
@@ -395,6 +404,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 editPost = () => {
|
||||
router.push(`/posts/${postId}/edit`)
|
||||
}
|
||||
@@ -514,12 +553,15 @@ export default {
|
||||
editPost,
|
||||
onCommentDeleted,
|
||||
deletePost,
|
||||
pinPost,
|
||||
unpinPost,
|
||||
rejectPost,
|
||||
lightboxVisible,
|
||||
lightboxIndex,
|
||||
lightboxImgs,
|
||||
handleContentClick,
|
||||
isMobile
|
||||
isMobile,
|
||||
pinnedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -143,6 +143,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<ReactionDto> reactions = reactionService.getReactionsForPost(post.getId())
|
||||
.stream()
|
||||
@@ -259,6 +260,7 @@ public class PostController {
|
||||
private java.util.List<TagDto> tags;
|
||||
private long views;
|
||||
private com.openisle.model.PostStatus status;
|
||||
private LocalDateTime pinnedAt;
|
||||
private List<CommentDto> comments;
|
||||
private List<ReactionDto> reactions;
|
||||
private java.util.List<AuthorDto> participants;
|
||||
|
||||
@@ -59,4 +59,7 @@ public class Post {
|
||||
@Column(nullable = false)
|
||||
private PostStatus status = PostStatus.PUBLISHED;
|
||||
|
||||
@Column
|
||||
private LocalDateTime pinnedAt;
|
||||
|
||||
}
|
||||
|
||||
@@ -173,22 +173,14 @@ public class PostService {
|
||||
java.util.List<Long> 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<Post> posts;
|
||||
|
||||
if (hasCategories) {
|
||||
if (!hasCategories && !hasTags) {
|
||||
posts = postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED);
|
||||
} else if (hasCategories) {
|
||||
java.util.List<Category> 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<com.openisle.model.Tag> 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<com.openisle.model.Tag> 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<Post> listPostsByCategories(java.util.List<Long> 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<Post> posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED);
|
||||
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
||||
}
|
||||
|
||||
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
||||
if (pageable != null) {
|
||||
return postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED, pageable);
|
||||
}
|
||||
return postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED);
|
||||
java.util.List<Post> posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED);
|
||||
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
||||
}
|
||||
|
||||
public List<Post> 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<com.openisle.model.Tag> 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<Post> posts = postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size());
|
||||
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
||||
}
|
||||
|
||||
public List<Post> listPostsByCategoriesAndTags(java.util.List<Long> 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<Category> categories = categoryRepository.findAllById(categoryIds);
|
||||
java.util.List<com.openisle.model.Tag> 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<Post> posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc(categories, tags, PostStatus.PUBLISHED, tags.size());
|
||||
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
||||
}
|
||||
|
||||
public List<Post> 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 Post updatePost(Long id,
|
||||
String username,
|
||||
@@ -410,4 +386,32 @@ public class PostService {
|
||||
public long countPostsByTag(Long tagId) {
|
||||
return postRepository.countDistinctByTags_Id(tagId);
|
||||
}
|
||||
|
||||
private java.util.List<Post> sortByPinnedAndCreated(java.util.List<Post> 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<Post> sortByPinnedAndViews(java.util.List<Post> 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<Post> paginate(java.util.List<Post> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user