-
![avatar]()
+
+
+
+
+
+
+
暂时没有帖子 :( 点击发帖发送第一篇相关帖子吧!
-
- {{ article.comments }}
-
-
- {{ article.views }}
-
-
- {{ article.time }}
+
+
+
+
+ {{ article.title }}
+
+
{{ sanitizeDescription(article.description) }}
+
+
+
+
+
+
![avatar]()
+
+
+
+ {{ article.comments }}
+
+
+ {{ article.views }}
+
+
+ {{ article.time }}
+
+
+
+ 排行榜功能开发中,敬请期待。
+
+
+ 热门帖子功能开发中,敬请期待。
+
+
+ 分类浏览功能开发中,敬请期待。
@@ -325,4 +342,13 @@ export default {
height: 100%;
object-fit: cover;
}
+
+.placeholder-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 200px;
+ font-size: 16px;
+ opacity: 0.7;
+}
From 4627d34dbe29c127fdea37b955cb8a2d93432d91 Mon Sep 17 00:00:00 2001
From: Tim <135014430+nagisa77@users.noreply.github.com>
Date: Wed, 9 Jul 2025 18:17:35 +0800
Subject: [PATCH 2/4] Implement reaction panel with backend support
---
open-isle-cli/src/components/CommentItem.vue | 26 +--
.../src/components/ReactionsGroup.vue | 172 ++++++++++++++++++
open-isle-cli/src/views/PostPageView.vue | 36 ++--
.../controller/ReactionController.java | 6 +
.../java/com/openisle/model/Reaction.java | 4 +-
.../repository/ReactionRepository.java | 4 +-
.../com/openisle/service/ReactionService.java | 16 +-
7 files changed, 213 insertions(+), 51 deletions(-)
create mode 100644 open-isle-cli/src/components/ReactionsGroup.vue
diff --git a/open-isle-cli/src/components/CommentItem.vue b/open-isle-cli/src/components/CommentItem.vue
index e4dfcd4a0..0dad7f7e9 100644
--- a/open-isle-cli/src/components/CommentItem.vue
+++ b/open-isle-cli/src/components/CommentItem.vue
@@ -18,27 +18,14 @@
-
({
id: r.id,
userName: r.author.username,
time: new Date(r.createdAt).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }),
avatar: r.avatar,
text: r.content,
+ reactions: r.reactions || [],
reply: [],
openReplies: false
})),
@@ -168,7 +158,7 @@ const CommentItem = {
return { showReplies, toggleReplies, showEditor, toggleEditor, submitReply, copyCommentLink, renderMarkdown, isWaitingForReply }
}
}
-CommentItem.components = { CommentItem, CommentEditor, BaseTimeline }
+CommentItem.components = { CommentItem, CommentEditor, BaseTimeline, ReactionsGroup }
export default CommentItem
diff --git a/open-isle-cli/src/components/ReactionsGroup.vue b/open-isle-cli/src/components/ReactionsGroup.vue
new file mode 100644
index 000000000..ac9be94cd
--- /dev/null
+++ b/open-isle-cli/src/components/ReactionsGroup.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+ {{ iconMap[r.type] }}
+
+
点击以表态
+
+
{{ totalCount }}
+
+
+
+
+ {{ likeCount }}
+
+
+
+
+
+ {{ iconMap[t] }}{{ counts[t] }}
+
+
+
+
+
+
+
+
diff --git a/open-isle-cli/src/views/PostPageView.vue b/open-isle-cli/src/views/PostPageView.vue
index 51063c9a8..6fc447ee9 100644
--- a/open-isle-cli/src/views/PostPageView.vue
+++ b/open-isle-cli/src/views/PostPageView.vue
@@ -27,31 +27,11 @@
-
-
-
-
- 🤣
-
-
- ❤️
-
-
- 👏
-
-
-
1882
+
+
+
-
-
-
+
@@ -98,6 +78,7 @@ import CommentItem from '../components/CommentItem.vue'
import CommentEditor from '../components/CommentEditor.vue'
import BaseTimeline from '../components/BaseTimeline.vue'
import ArticleTags from '../components/ArticleTags.vue'
+import ReactionsGroup from '../components/ReactionsGroup.vue'
import { renderMarkdown } from '../utils/markdown'
import { API_BASE_URL, toast } from '../main'
import { getToken } from '../utils/auth'
@@ -107,7 +88,7 @@ hatch.register()
export default {
name: 'PostPageView',
- components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags },
+ components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags, ReactionsGroup },
setup() {
const route = useRoute()
const postId = route.params.id
@@ -118,6 +99,7 @@ export default {
const postContent = ref('')
const category = ref('')
const tags = ref([])
+ const postReactions = ref([])
const comments = ref([])
const isWaitingFetchingPost = ref(false);
const isWaitingPostingComment = ref(false);
@@ -150,6 +132,7 @@ export default {
time: new Date(c.createdAt).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }),
avatar: c.author.avatar,
text: c.content,
+ reactions: c.reactions || [],
reply: (c.replies || []).map(mapComment),
openReplies: false,
src: c.author.avatar,
@@ -195,6 +178,7 @@ export default {
title.value = data.title
category.value = data.category
tags.value = data.tags || []
+ postReactions.value = data.reactions || []
comments.value = (data.comments || []).map(mapComment)
postTime.value = new Date(data.createdAt).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
await nextTick()
@@ -322,6 +306,8 @@ export default {
mainContainer,
currentIndex,
totalPosts,
+ postReactions,
+ postId,
postComment,
onSliderInput,
onScroll: updateCurrentIndex,
diff --git a/src/main/java/com/openisle/controller/ReactionController.java b/src/main/java/com/openisle/controller/ReactionController.java
index bef570999..ef8ddd1fa 100644
--- a/src/main/java/com/openisle/controller/ReactionController.java
+++ b/src/main/java/com/openisle/controller/ReactionController.java
@@ -28,6 +28,9 @@ public class ReactionController {
@RequestBody ReactionRequest req,
Authentication auth) {
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
+ if (reaction == null) {
+ return ResponseEntity.noContent().build();
+ }
return ResponseEntity.ok(toDto(reaction));
}
@@ -36,6 +39,9 @@ public class ReactionController {
@RequestBody ReactionRequest req,
Authentication auth) {
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
+ if (reaction == null) {
+ return ResponseEntity.noContent().build();
+ }
return ResponseEntity.ok(toDto(reaction));
}
diff --git a/src/main/java/com/openisle/model/Reaction.java b/src/main/java/com/openisle/model/Reaction.java
index 380d2ceb9..9fb71d8c1 100644
--- a/src/main/java/com/openisle/model/Reaction.java
+++ b/src/main/java/com/openisle/model/Reaction.java
@@ -14,8 +14,8 @@ import lombok.Setter;
@NoArgsConstructor
@Table(name = "reactions",
uniqueConstraints = {
- @UniqueConstraint(columnNames = {"user_id", "post_id"}),
- @UniqueConstraint(columnNames = {"user_id", "comment_id"})
+ @UniqueConstraint(columnNames = {"user_id", "post_id", "type"}),
+ @UniqueConstraint(columnNames = {"user_id", "comment_id", "type"})
})
public class Reaction {
@Id
diff --git a/src/main/java/com/openisle/repository/ReactionRepository.java b/src/main/java/com/openisle/repository/ReactionRepository.java
index a9ffa9c43..c5ba190ac 100644
--- a/src/main/java/com/openisle/repository/ReactionRepository.java
+++ b/src/main/java/com/openisle/repository/ReactionRepository.java
@@ -13,8 +13,8 @@ import java.util.List;
import java.util.Optional;
public interface ReactionRepository extends JpaRepository
{
- Optional findByUserAndPost(User user, Post post);
- Optional findByUserAndComment(User user, Comment comment);
+ Optional findByUserAndPostAndType(User user, Post post, com.openisle.model.ReactionType type);
+ Optional findByUserAndCommentAndType(User user, Comment comment, com.openisle.model.ReactionType type);
List findByPost(Post post);
List findByComment(Comment comment);
diff --git a/src/main/java/com/openisle/service/ReactionService.java b/src/main/java/com/openisle/service/ReactionService.java
index 57c39a28e..cfbea0c33 100644
--- a/src/main/java/com/openisle/service/ReactionService.java
+++ b/src/main/java/com/openisle/service/ReactionService.java
@@ -28,8 +28,12 @@ public class ReactionService {
.orElseThrow(() -> new IllegalArgumentException("User not found"));
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("Post not found"));
- Reaction reaction = reactionRepository.findByUserAndPost(user, post)
- .orElseGet(Reaction::new);
+ java.util.Optional existing = reactionRepository.findByUserAndPostAndType(user, post, type);
+ if (existing.isPresent()) {
+ reactionRepository.delete(existing.get());
+ return null;
+ }
+ Reaction reaction = new Reaction();
reaction.setUser(user);
reaction.setPost(post);
reaction.setType(type);
@@ -45,8 +49,12 @@ public class ReactionService {
.orElseThrow(() -> new IllegalArgumentException("User not found"));
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new IllegalArgumentException("Comment not found"));
- Reaction reaction = reactionRepository.findByUserAndComment(user, comment)
- .orElseGet(Reaction::new);
+ java.util.Optional existing = reactionRepository.findByUserAndCommentAndType(user, comment, type);
+ if (existing.isPresent()) {
+ reactionRepository.delete(existing.get());
+ return null;
+ }
+ Reaction reaction = new Reaction();
reaction.setUser(user);
reaction.setComment(comment);
reaction.setPost(null);
From ccd0c817f675bba1b494cdc6083a135a132631c3 Mon Sep 17 00:00:00 2001
From: tim
Date: Wed, 9 Jul 2025 18:22:31 +0800
Subject: [PATCH 3/4] feat: add reaction token
---
open-isle-cli/src/components/BaseTimeline.vue | 2 +-
open-isle-cli/src/components/ReactionsGroup.vue | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/open-isle-cli/src/components/BaseTimeline.vue b/open-isle-cli/src/components/BaseTimeline.vue
index e3ebadc12..0c14729c6 100644
--- a/open-isle-cli/src/components/BaseTimeline.vue
+++ b/open-isle-cli/src/components/BaseTimeline.vue
@@ -44,7 +44,7 @@ export default {
width: 32px;
height: 32px;
border-radius: 50%;
- color: white;
+ color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
diff --git a/open-isle-cli/src/components/ReactionsGroup.vue b/open-isle-cli/src/components/ReactionsGroup.vue
index ac9be94cd..fa563e939 100644
--- a/open-isle-cli/src/components/ReactionsGroup.vue
+++ b/open-isle-cli/src/components/ReactionsGroup.vue
@@ -33,7 +33,10 @@ let cachedTypes = null
const fetchTypes = async () => {
if (cachedTypes) return cachedTypes
try {
- const res = await fetch(`${API_BASE_URL}/api/reaction-types`)
+ const token = getToken()
+ const res = await fetch(`${API_BASE_URL}/api/reaction-types`, {
+ headers: { Authorization: `Bearer ${token}` }
+ })
if (res.ok) {
cachedTypes = await res.json()
} else {
From 206718640abdcdd7cfb42435bd0ebe33cdea4be3 Mon Sep 17 00:00:00 2001
From: Tim <135014430+nagisa77@users.noreply.github.com>
Date: Wed, 9 Jul 2025 18:23:24 +0800
Subject: [PATCH 4/4] feat: indicate read messages
---
open-isle-cli/src/utils/notification.js | 18 +++++++++
open-isle-cli/src/views/MessagePageView.vue | 43 +++++++++++++++++----
2 files changed, 54 insertions(+), 7 deletions(-)
diff --git a/open-isle-cli/src/utils/notification.js b/open-isle-cli/src/utils/notification.js
index 8e912d8f9..6cce96cab 100644
--- a/open-isle-cli/src/utils/notification.js
+++ b/open-isle-cli/src/utils/notification.js
@@ -15,3 +15,21 @@ export async function fetchUnreadCount() {
return 0
}
}
+
+export async function markNotificationsRead(ids) {
+ try {
+ const token = getToken()
+ if (!token || !ids || ids.length === 0) return false
+ const res = await fetch(`${API_BASE_URL}/api/notifications/read`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ ids })
+ })
+ return res.ok
+ } catch (e) {
+ return false
+ }
+}
diff --git a/open-isle-cli/src/views/MessagePageView.vue b/open-isle-cli/src/views/MessagePageView.vue
index 7dde55195..8b841a4ec 100644
--- a/open-isle-cli/src/views/MessagePageView.vue
+++ b/open-isle-cli/src/views/MessagePageView.vue
@@ -13,18 +13,19 @@
-
+
+
{{ item.comment.author.username }} 对我的评论
-
{{ sanitizeDescription(item.parentComment.content) }}
回复了
-
+
{{ sanitizeDescription(item.comment.content) }}
@@ -34,11 +35,11 @@
{{ item.comment.author.username }} 对我的文章
-
+
{{ sanitizeDescription(item.post.title) }}
回复了
-
+
{{ sanitizeDescription(item.comment.content) }}
@@ -63,6 +64,7 @@ import { useRouter } from 'vue-router'
import { API_BASE_URL } from '../main'
import BaseTimeline from '../components/BaseTimeline.vue'
import { getToken } from '../utils/auth'
+import { markNotificationsRead } from '../utils/notification'
import { toast } from '../main'
import { stripMarkdown } from '../utils/markdown'
import { hatch } from 'ldrs'
@@ -76,6 +78,15 @@ export default {
const notifications = ref([])
const isLoadingMessage = ref(false)
+ const markRead = async id => {
+ if (!id) return
+ const ok = await markNotificationsRead([id])
+ if (ok) {
+ const n = notifications.value.find(n => n.id === id)
+ if (n) n.read = true
+ }
+ }
+
const iconMap = {
POST_VIEWED: 'fas fa-eye',
COMMENT_REPLY: 'fas fa-reply',
@@ -114,7 +125,10 @@ export default {
notifications.value.push({
...n,
src: n.comment.author.avatar,
- iconClick: () => router.push(`/users/${n.comment.author.id}`)
+ iconClick: () => {
+ markRead(n.id)
+ router.push(`/users/${n.comment.author.id}`)
+ }
})
} else {
notifications.value.push({
@@ -149,7 +163,7 @@ export default {
onMounted(fetchNotifications)
- return { notifications, formatType, sanitizeDescription, isLoadingMessage }
+ return { notifications, formatType, sanitizeDescription, isLoadingMessage, markRead }
}
}
@@ -187,6 +201,21 @@ export default {
display: flex;
flex-direction: column;
margin-bottom: 30px;
+ position: relative;
+}
+
+.notif-content.read {
+ opacity: 0.7;
+}
+
+.unread-dot {
+ position: absolute;
+ left: -10px;
+ top: 4px;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: #ff4d4f;
}
.notif-type {