Skip to content

Commit ed41c74

Browse files
committed
v 1.0.9.2 更新评论结构
1 parent 37cefae commit ed41c74

5 files changed

Lines changed: 126 additions & 38 deletions

File tree

frontend/src/assets/blog.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,13 @@ body {
373373
border-left: 2px solid rgba(170, 59, 255, 0.18);
374374
}
375375

376+
.comment-replies-title {
377+
font-size: 12px;
378+
font-weight: 750;
379+
color: var(--muted);
380+
margin: 6px 0 6px;
381+
}
382+
376383
.comment-reply-box {
377384
margin-top: 10px;
378385
}

frontend/src/components/CommentItem.vue

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,15 @@
2828
</button>
2929
</div>
3030
</div>
31-
32-
<div v-if="comment.replies?.length" class="comment-replies">
33-
<CommentItem
34-
v-for="c in comment.replies"
35-
:key="c.id"
36-
:comment="c"
37-
:depth="depth + 1"
38-
:me="me"
39-
@reply="$emit('reply', $event)"
40-
@delete="$emit('delete', $event)"
41-
@submit-reply="$emit('submit-reply', $event)"
42-
/>
43-
</div>
4431
</div>
4532
</div>
4633
</template>
4734

4835
<script setup>
4936
import { computed, ref, watch } from 'vue'
50-
import CommentItem from './CommentItem.vue'
5137
5238
const props = defineProps({
5339
comment: { type: Object, required: true },
54-
depth: { type: Number, default: 1 },
5540
me: { type: Object, default: null }
5641
})
5742

frontend/src/components/CommentSection.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@
4242
@delete="onDelete"
4343
@submit-reply="onSubmitReply"
4444
/>
45+
46+
<div v-for="p in items" :key="'replies-' + p.id">
47+
<div v-if="p.replies?.length" class="comment-replies">
48+
<div class="comment-replies-title">回复</div>
49+
<CommentItem
50+
v-for="r in p.replies"
51+
:key="r.id"
52+
:comment="r"
53+
:me="me"
54+
@reply="onReplyClick"
55+
@delete="onDelete"
56+
@submit-reply="onSubmitReply"
57+
/>
58+
</div>
59+
</div>
4560
</div>
4661
</div>
4762
</div>

frontend/src/views/HomeView.vue

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,59 @@
1414
<div class="card-body" style="padding: 22px">加载中...</div>
1515
</div>
1616

17-
<div v-else class="card">
18-
<div class="article-content">
19-
<div v-if="!article?.id" style="padding: 20px 0">暂无文章</div>
20-
21-
<div v-else>
22-
<div style="margin-bottom: 14px">
23-
<img
24-
v-if="article.coverMediaKey"
25-
:src="coverUrl(article.coverMediaKey)"
26-
alt="cover"
27-
style="width: 100%; max-height: 340px; object-fit: cover; border-radius: 12px"
28-
/>
17+
<div v-else>
18+
<div class="card">
19+
<div class="article-content">
20+
<div v-if="!article?.id" style="padding: 20px 0">暂无文章</div>
21+
22+
<div v-else>
23+
<div style="margin-bottom: 14px">
24+
<img
25+
v-if="article.coverMediaKey"
26+
:src="coverUrl(article.coverMediaKey)"
27+
alt="cover"
28+
style="width: 100%; max-height: 340px; object-fit: cover; border-radius: 12px"
29+
/>
30+
</div>
31+
32+
<h1 class="article-hero-title">{{ article.title }}</h1>
33+
<div class="article-hero-meta">
34+
发布于 {{ formatDate(article.publishedAt) }} · 作者 {{ article.authorNickname || '' }} · 阅读
35+
{{ article.viewCount ?? 0 }}
36+
</div>
37+
38+
<div class="markdown-body article-markdown" v-html="html" />
2939
</div>
40+
</div>
41+
</div>
3042

31-
<h1 class="article-hero-title">{{ article.title }}</h1>
32-
<div class="article-hero-meta">
33-
发布于 {{ formatDate(article.publishedAt) }} · 作者 {{ article.authorNickname || '' }} · 阅读
34-
{{ article.viewCount ?? 0 }}
43+
<div v-if="article?.id" class="card" style="margin-top: 18px">
44+
<div class="card-body">
45+
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px">
46+
<div style="font-weight: 900">最新评论</div>
47+
<button type="button" class="btn" @click="go(article.id)">进入文章</button>
3548
</div>
3649

37-
<div class="markdown-body article-markdown" v-html="html" />
50+
<div v-if="commentsLoading" style="color: var(--muted); margin-top: 10px">加载中…</div>
51+
<div v-else-if="commentPreviews.length === 0" style="color: var(--muted); margin-top: 10px">
52+
暂无评论
53+
</div>
54+
<div v-else class="mini-list" style="margin-top: 12px">
55+
<div v-for="c in commentPreviews" :key="c.id" class="mini-item" style="grid-template-columns: 44px 1fr">
56+
<div class="mini-cover" style="width: 44px; height: 44px; border-radius: 999px">
57+
<img :src="c.user?.avatarUrl || 'https://www.gravatar.com/avatar/?d=mp&s=96'" alt="avatar" />
58+
</div>
59+
<div>
60+
<div style="display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap">
61+
<div style="font-weight: 850; font-size: 13px">{{ c.user?.nickname || '匿名' }}</div>
62+
<div style="color: var(--muted); font-size: 12px">{{ formatRelative(c.createdAt) }}</div>
63+
</div>
64+
<div style="margin-top: 4px; color: var(--text); font-size: 13px; line-height: 1.6">
65+
{{ c.content }}
66+
</div>
67+
</div>
68+
</div>
69+
</div>
3870
</div>
3971
</div>
4072
</div>
@@ -48,18 +80,21 @@ import { computed, onMounted, ref } from 'vue'
4880
import DOMPurify from 'dompurify'
4981
import { marked } from 'marked'
5082
51-
import { fetchArticles, fetchArticleDetail } from '../api/article'
83+
import { fetchArticles, fetchArticleDetail, coverUrl } from '../api/article'
5284
import ProfileCard from '../components/ProfileCard.vue'
5385
import BlogInfoCard from '../components/BlogInfoCard.vue'
5486
import AdminEntryCard from '../components/AdminEntryCard.vue'
5587
import { useRouter } from 'vue-router'
5688
import { fetchPublicProfile } from '../api/profile'
89+
import { listComments } from '../api/comment'
5790
5891
const router = useRouter()
5992
6093
const loading = ref(true)
6194
const article = ref({})
6295
const profile = ref(null)
96+
const commentsLoading = ref(false)
97+
const commentPreviews = ref([])
6398
6499
onMounted(async () => {
65100
loading.value = true
@@ -69,6 +104,18 @@ onMounted(async () => {
69104
article.value = first?.id ? await fetchArticleDetail(first.id) : {}
70105
71106
profile.value = await fetchPublicProfile()
107+
108+
if (article.value?.id) {
109+
commentsLoading.value = true
110+
try {
111+
const resp = await listComments(article.value.id, 0, 3)
112+
const top = resp?.items || []
113+
// 首页预览只展示顶层评论
114+
commentPreviews.value = top.slice(0, 3)
115+
} finally {
116+
commentsLoading.value = false
117+
}
118+
}
72119
} finally {
73120
loading.value = false
74121
}
@@ -92,5 +139,21 @@ function formatDate(v) {
92139
return ''
93140
}
94141
}
142+
143+
function formatRelative(v) {
144+
if (!v) return ''
145+
const t = new Date(v).getTime()
146+
if (Number.isNaN(t)) return ''
147+
let diff = Date.now() - t
148+
if (diff < 0) diff = 0
149+
if (diff < 60_000) return '刚刚'
150+
const m = Math.floor(diff / 60_000)
151+
if (m < 60) return `${m} 分钟前`
152+
const h = Math.floor(m / 60)
153+
if (h < 24) return `${h} 小时前`
154+
const d = Math.floor(h / 24)
155+
if (d < 7) return `${d} 天前`
156+
return new Date(t).toISOString().slice(0, 10)
157+
}
95158
</script>
96159

src/main/java/com/yqz/openblog/comment/service/CommentService.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,21 @@ public PageResult<CommentThreadResponse> listComments(Long articleId, int page,
100100
}
101101

102102
// 挂载 replies
103+
// 两层结构:所有后代评论都折叠挂到“顶层评论”的 replies 下
104+
Map<Long, Long> parentById = new HashMap<>();
105+
for (Comment c : all) {
106+
parentById.put(c.getId(), c.getParentId());
107+
}
108+
Set<Long> topIds = new HashSet<>(top.stream().map(Comment::getId).toList());
103109
for (Comment c : descendants) {
104-
Long pid = c.getParentId();
105-
if (pid == null) continue;
106-
CommentThreadResponse parent = nodeById.get(pid);
110+
Long topId = findTopAncestorId(c.getId(), parentById, topIds);
111+
if (topId == null) continue;
112+
CommentThreadResponse topNode = nodeById.get(topId);
107113
CommentThreadResponse child = nodeById.get(c.getId());
108-
if (parent == null || child == null) continue;
109-
parent.getReplies().add(child);
114+
if (topNode == null || child == null) continue;
115+
// 强制让前端按两层渲染
116+
child.setReplies(List.of());
117+
topNode.getReplies().add(child);
110118
}
111119

112120
// 每个节点的 replies 按时间排序(稳定)
@@ -123,6 +131,16 @@ public PageResult<CommentThreadResponse> listComments(Long articleId, int page,
123131
return new PageResult<>(items, page, size, p.getTotal());
124132
}
125133

134+
private Long findTopAncestorId(Long id, Map<Long, Long> parentById, Set<Long> topIds) {
135+
Long cur = id;
136+
int guard = 0;
137+
while (cur != null && guard++ < 16) {
138+
if (topIds.contains(cur)) return cur;
139+
cur = parentById.get(cur);
140+
}
141+
return null;
142+
}
143+
126144
private CommentThreadResponse toNode(Comment c) {
127145
CommentThreadResponse resp = new CommentThreadResponse();
128146
resp.setId(c.getId());

0 commit comments

Comments
 (0)