+
{{ 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);
+ }
}