mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-13 18:40:58 +08:00
Compare commits
1 Commits
codex/add-
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d7ca3d29a |
@@ -1,29 +0,0 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.dto.CommentDto;
|
||||
import com.openisle.mapper.CommentMapper;
|
||||
import com.openisle.service.CommentService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Endpoints for administrators to manage comments.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/comments")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminCommentController {
|
||||
private final CommentService commentService;
|
||||
private final CommentMapper commentMapper;
|
||||
|
||||
@PostMapping("/{id}/pin")
|
||||
public CommentDto pin(@PathVariable Long id, Authentication auth) {
|
||||
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/unpin")
|
||||
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
|
||||
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
||||
}
|
||||
}
|
||||
@@ -85,16 +85,4 @@ public class CommentController {
|
||||
commentService.deleteComment(auth.getName(), id);
|
||||
log.debug("deleteComment completed for comment {}", id);
|
||||
}
|
||||
|
||||
@PostMapping("/comments/{id}/pin")
|
||||
public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
|
||||
log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
|
||||
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
|
||||
}
|
||||
|
||||
@PostMapping("/comments/{id}/unpin")
|
||||
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
|
||||
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
|
||||
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ public class CommentDto {
|
||||
private Long id;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime pinnedAt;
|
||||
private AuthorDto author;
|
||||
private List<CommentDto> replies;
|
||||
private List<ReactionDto> reactions;
|
||||
|
||||
@@ -24,7 +24,6 @@ public class CommentMapper {
|
||||
dto.setId(comment.getId());
|
||||
dto.setContent(comment.getContent());
|
||||
dto.setCreatedAt(comment.getCreatedAt());
|
||||
dto.setPinnedAt(comment.getPinnedAt());
|
||||
dto.setAuthor(userMapper.toAuthorDto(comment.getAuthor()));
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
|
||||
@@ -38,7 +38,4 @@ public class Comment {
|
||||
@JoinColumn(name = "parent_id")
|
||||
private Comment parent;
|
||||
|
||||
@Column
|
||||
private LocalDateTime pinnedAt;
|
||||
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ public enum NotificationType {
|
||||
REGISTER_REQUEST,
|
||||
/** A user redeemed an activity reward */
|
||||
ACTIVITY_REDEEM,
|
||||
/** You won a lottery post */
|
||||
LOTTERY_WIN,
|
||||
/** You were mentioned in a post or comment */
|
||||
MENTION
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.List;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -130,26 +129,13 @@ public class CommentService {
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
List<Comment> list = commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post);
|
||||
java.util.List<Comment> pinned = new java.util.ArrayList<>();
|
||||
java.util.List<Comment> others = new java.util.ArrayList<>();
|
||||
for (Comment c : list) {
|
||||
if (c.getPinnedAt() != null) {
|
||||
pinned.add(c);
|
||||
} else {
|
||||
others.add(c);
|
||||
}
|
||||
}
|
||||
pinned.sort(java.util.Comparator.comparing(Comment::getPinnedAt).reversed());
|
||||
if (sort == CommentSort.NEWEST) {
|
||||
others.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed());
|
||||
list.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed());
|
||||
} else if (sort == CommentSort.MOST_INTERACTIONS) {
|
||||
others.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a)));
|
||||
list.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a)));
|
||||
}
|
||||
java.util.List<Comment> result = new java.util.ArrayList<>();
|
||||
result.addAll(pinned);
|
||||
result.addAll(others);
|
||||
log.debug("getCommentsForPost returning {} comments", result.size());
|
||||
return result;
|
||||
log.debug("getCommentsForPost returning {} comments", list.size());
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Comment> getReplies(Long parentId) {
|
||||
@@ -237,32 +223,6 @@ public class CommentService {
|
||||
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Comment pinComment(String username, Long id) {
|
||||
Comment c = commentRepository.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(LocalDateTime.now());
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Comment unpinComment(String username, Long id) {
|
||||
Comment c = commentRepository.findById(id)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
c.setPinnedAt(null);
|
||||
return commentRepository.save(c);
|
||||
}
|
||||
|
||||
private int interactionCount(Comment comment) {
|
||||
int reactions = reactionRepository.findByComment(comment).size();
|
||||
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
|
||||
|
||||
@@ -69,8 +69,6 @@ public class PostService {
|
||||
private final EmailSender emailSender;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
private String websiteUrl;
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
public PostService(PostRepository postRepository,
|
||||
@@ -251,8 +249,6 @@ public class PostService {
|
||||
if (w.getEmail() != null) {
|
||||
emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖");
|
||||
}
|
||||
notificationService.createNotification(w, NotificationType.LOTTERY_WIN, lp, null, null, lp.getAuthor(), null, null);
|
||||
notificationService.sendCustomPush(w, "你中奖了", String.format("%s/posts/%d", websiteUrl, lp.getId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE comments ADD COLUMN pinned_at DATETIME(6) NULL;
|
||||
5
frontend_nuxt/.env
Normal file
5
frontend_nuxt/.env
Normal file
@@ -0,0 +1,5 @@
|
||||
NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-xxx.apps.googleusercontent.com
|
||||
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
|
||||
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
|
||||
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
|
||||
@@ -22,7 +22,6 @@
|
||||
:to="`/users/${comment.userId}?tab=achievements`"
|
||||
>{{ getMedalTitle(comment.medal) }}</router-link
|
||||
>
|
||||
<i v-if="comment.pinned" class="fas fa-thumbtack pin-icon"></i>
|
||||
<span v-if="level >= 2">
|
||||
<i class="fas fa-reply reply-icon"></i>
|
||||
<span class="user-name reply-user-name">{{ comment.parentUserName }}</span>
|
||||
@@ -75,7 +74,6 @@
|
||||
:comment="item"
|
||||
:level="level + 1"
|
||||
:default-show-replies="item.openReplies"
|
||||
:post-author-id="postAuthorId"
|
||||
/>
|
||||
</template>
|
||||
</BaseTimeline>
|
||||
@@ -121,10 +119,6 @@ const CommentItem = {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
postAuthorId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter()
|
||||
@@ -177,22 +171,12 @@ const CommentItem = {
|
||||
})
|
||||
|
||||
const isAuthor = computed(() => authState.username === props.comment.userName)
|
||||
const isPostAuthor = computed(() => Number(authState.userId) === Number(props.postAuthorId))
|
||||
const isAdmin = computed(() => authState.role === 'ADMIN')
|
||||
const commentMenuItems = computed(() => {
|
||||
const items = []
|
||||
if (isAuthor.value || isAdmin.value) {
|
||||
items.push({ text: '删除评论', color: 'red', onClick: () => deleteComment() })
|
||||
}
|
||||
if (isAdmin.value || isPostAuthor.value) {
|
||||
if (props.comment.pinned) {
|
||||
items.push({ text: '取消置顶', onClick: () => unpinComment() })
|
||||
} else {
|
||||
items.push({ text: '置顶', onClick: () => pinComment() })
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
const commentMenuItems = computed(() =>
|
||||
isAuthor.value || isAdmin.value
|
||||
? [{ text: '删除评论', color: 'red', onClick: () => deleteComment() }]
|
||||
: [],
|
||||
)
|
||||
const deleteComment = async () => {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
@@ -212,46 +196,6 @@ const CommentItem = {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
}
|
||||
const pinComment = async () => {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
toast.error('请先登录')
|
||||
return
|
||||
}
|
||||
const url = isAdmin.value
|
||||
? `${API_BASE_URL}/api/admin/comments/${props.comment.id}/pin`
|
||||
: `${API_BASE_URL}/api/comments/${props.comment.id}/pin`
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (res.ok) {
|
||||
props.comment.pinned = true
|
||||
toast.success('已置顶')
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
}
|
||||
const unpinComment = async () => {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
toast.error('请先登录')
|
||||
return
|
||||
}
|
||||
const url = isAdmin.value
|
||||
? `${API_BASE_URL}/api/admin/comments/${props.comment.id}/unpin`
|
||||
: `${API_BASE_URL}/api/comments/${props.comment.id}/unpin`
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (res.ok) {
|
||||
props.comment.pinned = false
|
||||
toast.success('已取消置顶')
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
}
|
||||
const submitReply = async (parentUserName, text, clear) => {
|
||||
if (!text.trim()) return
|
||||
isWaitingForReply.value = true
|
||||
@@ -340,9 +284,6 @@ const CommentItem = {
|
||||
isWaitingForReply,
|
||||
commentMenuItems,
|
||||
deleteComment,
|
||||
pinComment,
|
||||
unpinComment,
|
||||
isPostAuthor,
|
||||
lightboxVisible,
|
||||
lightboxIndex,
|
||||
lightboxImgs,
|
||||
@@ -429,12 +370,6 @@ export default CommentItem
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
from {
|
||||
background-color: yellow;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
export const API_BASE_URL = 'https://www.open-isle.com'
|
||||
// export const API_BASE_URL = 'http://127.0.0.1:8081'
|
||||
// export const API_BASE_URL = 'http://30.211.97.238:8081'
|
||||
export const GOOGLE_CLIENT_ID =
|
||||
'777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com'
|
||||
export const GITHUB_CLIENT_ID = 'Ov23liVkO1NPAX5JyWxJ'
|
||||
export const DISCORD_CLIENT_ID = '1394985417044000779'
|
||||
export const TWITTER_CLIENT_ID = 'ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ'
|
||||
import { useRuntimeConfig } from '#app'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
export const API_BASE_URL = config.public.apiBaseUrl
|
||||
export const GOOGLE_CLIENT_ID = config.public.googleClientId
|
||||
export const GITHUB_CLIENT_ID = config.public.githubClientId
|
||||
export const DISCORD_CLIENT_ID = config.public.discordClientId
|
||||
export const TWITTER_CLIENT_ID = config.public.twitterClientId
|
||||
|
||||
// 重新导出 toast 功能,使用 composable 方式
|
||||
export { toast } from './composables/useToast'
|
||||
|
||||
@@ -2,6 +2,15 @@ import { defineNuxtConfig } from 'nuxt/config'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
ssr: true,
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || '',
|
||||
googleClientId: process.env.NUXT_PUBLIC_GOOGLE_CLIENT_ID || '',
|
||||
githubClientId: process.env.NUXT_PUBLIC_GITHUB_CLIENT_ID || '',
|
||||
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
|
||||
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
||||
},
|
||||
},
|
||||
// Ensure Vditor styles load before our overrides in global.css
|
||||
css: ['vditor/dist/index.css', '~/assets/global.css'],
|
||||
app: {
|
||||
|
||||
@@ -185,19 +185,6 @@
|
||||
</router-link>
|
||||
</NotificationContainer>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'LOTTERY_WIN'">
|
||||
<NotificationContainer :item="item" :markRead="markRead">
|
||||
恭喜你在抽奖贴
|
||||
<router-link
|
||||
class="notif-content-text"
|
||||
@click="markRead(item.id)"
|
||||
:to="`/posts/${item.post.id}`"
|
||||
>
|
||||
{{ stripMarkdownLength(item.post.title, 100) }}
|
||||
</router-link>
|
||||
中获奖
|
||||
</NotificationContainer>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'POST_UPDATED'">
|
||||
<NotificationContainer :item="item" :markRead="markRead">
|
||||
您关注的帖子
|
||||
@@ -578,7 +565,6 @@ export default {
|
||||
POST_UNSUBSCRIBED: 'fas fa-bookmark',
|
||||
REGISTER_REQUEST: 'fas fa-user-clock',
|
||||
ACTIVITY_REDEEM: 'fas fa-coffee',
|
||||
LOTTERY_WIN: 'fas fa-trophy',
|
||||
MENTION: 'fas fa-at',
|
||||
}
|
||||
|
||||
@@ -636,17 +622,6 @@ export default {
|
||||
}
|
||||
},
|
||||
})
|
||||
} else if (n.type === 'LOTTERY_WIN') {
|
||||
notifications.value.push({
|
||||
...n,
|
||||
icon: iconMap[n.type],
|
||||
iconClick: () => {
|
||||
if (n.post) {
|
||||
markRead(n.id)
|
||||
router.push(`/posts/${n.post.id}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
} else if (n.type === 'POST_UPDATED') {
|
||||
notifications.value.push({
|
||||
...n,
|
||||
@@ -816,8 +791,6 @@ export default {
|
||||
return '有人申请注册'
|
||||
case 'ACTIVITY_REDEEM':
|
||||
return '有人申请兑换奶茶'
|
||||
case 'LOTTERY_WIN':
|
||||
return '抽奖中奖了'
|
||||
default:
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -195,7 +195,6 @@
|
||||
:comment="item"
|
||||
:level="0"
|
||||
:default-show-replies="item.openReplies"
|
||||
:post-author-id="author.id"
|
||||
@deleted="onCommentDeleted"
|
||||
/>
|
||||
</template>
|
||||
@@ -406,7 +405,6 @@ export default {
|
||||
avatar: c.author.avatar,
|
||||
text: c.content,
|
||||
reactions: c.reactions || [],
|
||||
pinned: !!c.pinnedAt,
|
||||
reply: (c.replies || []).map((r) => mapComment(r, c.author.username, level + 1)),
|
||||
openReplies: level === 0,
|
||||
src: c.author.avatar,
|
||||
|
||||
Reference in New Issue
Block a user