From 6ea9b4a33c05d1700a19386d738dc3ee395d7c07 Mon Sep 17 00:00:00 2001 From: wangshun <932054296@qq.com> Date: Mon, 15 Sep 2025 11:23:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=97=AE=E9=A2=98#927,#860?= =?UTF-8?q?=201.=E4=BC=98=E5=8C=96=E8=AF=84=E8=AE=BA=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=EF=BC=8C=E5=B0=86=E4=B8=A4=E4=B8=AA=E8=AF=B7=E6=B1=82=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E4=B8=BA=E4=B8=80=E4=B8=AA=202.=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=B8=BB=E9=A1=B5=E6=8C=89=E9=92=AE=E7=9A=84?= =?UTF-8?q?=E4=B8=BB=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CommentController.java | 53 ++++++++++++--- .../com/openisle/dto/TimelineItemDto.java | 20 ++++++ frontend_nuxt/assets/global.css | 2 + frontend_nuxt/pages/posts/[id]/index.vue | 66 +++++++++---------- frontend_nuxt/pages/users/[id].vue | 22 ++++++- 5 files changed, 117 insertions(+), 46 deletions(-) create mode 100644 backend/src/main/java/com/openisle/dto/TimelineItemDto.java diff --git a/backend/src/main/java/com/openisle/controller/CommentController.java b/backend/src/main/java/com/openisle/controller/CommentController.java index d611e9622..fb53f35d2 100644 --- a/backend/src/main/java/com/openisle/controller/CommentController.java +++ b/backend/src/main/java/com/openisle/controller/CommentController.java @@ -1,13 +1,14 @@ package com.openisle.controller; +import com.openisle.dto.PostChangeLogDto; +import com.openisle.dto.TimelineItemDto; +import com.openisle.mapper.PostChangeLogMapper; import com.openisle.model.Comment; import com.openisle.dto.CommentDto; import com.openisle.dto.CommentRequest; import com.openisle.mapper.CommentMapper; -import com.openisle.service.CaptchaService; -import com.openisle.service.CommentService; -import com.openisle.service.LevelService; -import com.openisle.service.PointService; +import com.openisle.model.CommentSort; +import com.openisle.service.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -21,6 +22,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -34,6 +37,8 @@ public class CommentController { private final CaptchaService captchaService; private final CommentMapper commentMapper; private final PointService pointService; + private final PostChangeLogService changeLogService; + private final PostChangeLogMapper postChangeLogMapper; @Value("${app.captcha.enabled:false}") private boolean captchaEnabled; @@ -85,15 +90,43 @@ public class CommentController { @GetMapping("/posts/{postId}/comments") @Operation(summary = "List comments", description = "List comments for a post") @ApiResponse(responseCode = "200", description = "Comments", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentDto.class)))) - public List listComments(@PathVariable Long postId, - @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) { + content = @Content(array = @ArraySchema(schema = @Schema(implementation = TimelineItemDto.class)))) + public List> listComments(@PathVariable Long postId, + @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort) { log.debug("listComments called for post {} with sort {}", postId, sort); - List list = commentService.getCommentsForPost(postId, sort).stream() + List commentDtoList = commentService.getCommentsForPost(postId, sort).stream() .map(commentMapper::toDtoWithReplies) .collect(Collectors.toList()); - log.debug("listComments returning {} comments", list.size()); - return list; + List postChangeLogDtoList = changeLogService.listLogs(postId).stream() + .map(postChangeLogMapper::toDto) + .collect(Collectors.toList()); + List> itemDtoList = new ArrayList<>(); + + itemDtoList.addAll(commentDtoList.stream() + .map(c -> new TimelineItemDto<>( + c.getId(), + "comment", + c.getCreatedAt(), + c // payload 是 CommentDto + )) + .toList()); + + itemDtoList.addAll(postChangeLogDtoList.stream() + .map(l -> new TimelineItemDto<>( + l.getId(), + "log", + l.getTime(), // 注意字段名不一样 + l // payload 是 PostChangeLogDto + )) + .toList()); + // 排序 + Comparator> comparator = Comparator.comparing(TimelineItemDto::getCreatedAt); + if (CommentSort.NEWEST.equals(sort)) { + comparator = comparator.reversed(); + } + itemDtoList.sort(comparator); + log.debug("listComments returning {} comments", itemDtoList.size()); + return itemDtoList; } @DeleteMapping("/comments/{id}") diff --git a/backend/src/main/java/com/openisle/dto/TimelineItemDto.java b/backend/src/main/java/com/openisle/dto/TimelineItemDto.java new file mode 100644 index 000000000..7e53f8a9c --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/TimelineItemDto.java @@ -0,0 +1,20 @@ +package com.openisle.dto; + +import lombok.*; + +import java.time.LocalDateTime; + +/** + * comment and change_log Dto + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class TimelineItemDto { + + private Long id; + private String kind; // "comment" | "log" + private LocalDateTime createdAt; + private T payload; // 泛型,具体类型由外部决定 +} diff --git a/frontend_nuxt/assets/global.css b/frontend_nuxt/assets/global.css index 6105d1ab9..3d610cc33 100644 --- a/frontend_nuxt/assets/global.css +++ b/frontend_nuxt/assets/global.css @@ -2,6 +2,8 @@ --primary-color-hover: rgb(9, 95, 105); --primary-color: rgb(10, 110, 120); --primary-color-disabled: rgba(93, 152, 156, 0.5); + --secondary-color:rgb(255, 255, 255); + --secondary-color-hover:rgba(165, 255, 255, 0.447); --new-post-icon-color: rgba(10, 111, 120, 0.598); --header-height: 60px; --header-background-color: white; diff --git a/frontend_nuxt/pages/posts/[id]/index.vue b/frontend_nuxt/pages/posts/[id]/index.vue index 5aa2bf57e..ad31ff183 100644 --- a/frontend_nuxt/pages/posts/[id]/index.vue +++ b/frontend_nuxt/pages/posts/[id]/index.vue @@ -320,6 +320,7 @@ const mapComment = ( level = 0, ) => ({ id: c.id, + kind: 'comment', userName: c.author.username, medal: c.author.displayMedal, userId: c.author.id, @@ -374,6 +375,7 @@ const changeLogIcon = (l) => { const mapChangeLog = (l) => ({ id: l.id, + kind: 'log', username: l.username, userAvatar: l.userAvatar, type: l.type, @@ -788,9 +790,9 @@ const fetchCommentSorts = () => { ]) } -const fetchComments = async () => { +const fetchCommentsAndChangeLog = async () => { isFetchingComments.value = true - console.debug('Fetching comments', { postId, sort: commentSort.value }) + console.info('Fetching comments and chang log', { postId, sort: commentSort.value }) try { const token = getToken() const res = await fetch( @@ -799,11 +801,34 @@ const fetchComments = async () => { headers: { Authorization: token ? `Bearer ${token}` : '' }, }, ) - console.debug('Fetch comments response status', res.status) + console.info('Fetch comments response status', res.status) if (res.ok) { const data = await res.json() - console.debug('Fetched comments count', data.length) - comments.value = data.map(mapComment) + console.info('Fetched comments data', data) + + const commentList = [] + const changeLogList = [] + // 时间线列表,包含评论和日志 + const newTimelineItemList = [] + + for (const item of data) { + const mappedPayload = + item.kind === 'comment' + ? mapComment(item.payload) + : mapChangeLog(item.payload) + newTimelineItemList.push(mappedPayload) + + if (item.kind === 'comment') { + commentList.push(mappedPayload) + } else { + changeLogList.push(mappedPayload) + } + } + + comments.value = commentList + changeLogs.value = changeLogList + timelineItems.value = newTimelineItemList + isFetchingComments.value = false await nextTick() gatherPostItems() @@ -815,37 +840,8 @@ const fetchComments = async () => { } } -const fetchChangeLogs = async () => { - try { - const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/change-logs`) - if (res.ok) { - const data = await res.json() - changeLogs.value = data.map(mapChangeLog) - await nextTick() - gatherPostItems() - } - } catch (e) { - console.debug('Fetch change logs error', e) - } -} - -// -// todo(tim): fetchComments, fetchChangeLogs 整合到一个请求,并且取消前端排序 -// const fetchTimeline = async () => { - await Promise.all([fetchComments(), fetchChangeLogs()]) - const cs = comments.value.map((c) => ({ ...c, kind: 'comment' })) - const ls = changeLogs.value.map((l) => ({ ...l, kind: 'log' })) - - if (commentSort.value === 'NEWEST') { - timelineItems.value = [...cs, ...ls].sort( - (a, b) => new Date(b.createdAt) - new Date(a.createdAt), - ) - } else { - timelineItems.value = [...cs, ...ls].sort( - (a, b) => new Date(a.createdAt) - new Date(b.createdAt), - ) - } + await fetchCommentsAndChangeLog() } watch(commentSort, async () => { diff --git a/frontend_nuxt/pages/users/[id].vue b/frontend_nuxt/pages/users/[id].vue index 4e810e2b7..0e7107964 100644 --- a/frontend_nuxt/pages/users/[id].vue +++ b/frontend_nuxt/pages/users/[id].vue @@ -29,7 +29,7 @@ 取消关注 -
+
发私信
@@ -703,6 +703,26 @@ watch(selectedTab, async (val) => { cursor: pointer; } +.profile-page-header-send-mail-button { + display: flex; + flex-direction: row; + gap: 5px; + align-items: center; + font-size: 14px; + border-radius: 8px; + padding: 5px 10px; + color: var(--primary-color); + background-color: var(--secondary-color); + border: 1px solid var(--primary-color); + margin-top: 15px; + width: fit-content; + cursor: pointer; +} + +.profile-page-header-send-mail-button:hover { + background-color: var(--secondary-color-hover); +} + .profile-level-container { display: flex; flex-direction: column;