Skip to content

Commit 9dcd98c

Browse files
committed
✨ feat: 列表添加评论
1 parent da7ac59 commit 9dcd98c

9 files changed

Lines changed: 361 additions & 110 deletions

File tree

components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ declare module 'vue' {
3232
HomePageSectionManager: typeof import('./src/components/Modal/HomePageSectionManager.vue')['default']
3333
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
3434
KeyboardSetting: typeof import('./src/components/Setting/KeyboardSetting.vue')['default']
35+
ListComment: typeof import('./src/components/List/ListComment.vue')['default']
3536
ListDetail: typeof import('./src/components/List/ListDetail.vue')['default']
3637
LocalSetting: typeof import('./src/components/Setting/LocalSetting.vue')['default']
3738
Login: typeof import('./src/components/Modal/Login/Login.vue')['default']
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<!-- 列表评论 -->
2+
<template>
3+
<div ref="commentListRef" class="list-comment">
4+
<div
5+
:style="{ height: height === 'auto' ? 'auto' : `${height || commentListHeight}px` }"
6+
class="comment-container"
7+
>
8+
<n-scrollbar class="comment-scroll">
9+
<div class="comment-content">
10+
<template v-if="commentHotData">
11+
<div class="placeholder">
12+
<div class="title">
13+
<SvgIcon name="Fire" />
14+
<span>热门评论</span>
15+
</div>
16+
</div>
17+
<CommentList
18+
:data="commentHotData"
19+
:loading="commentHotData?.length === 0"
20+
:type="type"
21+
/>
22+
</template>
23+
<div class="placeholder">
24+
<div class="title">
25+
<SvgIcon name="Message" />
26+
<span>全部评论</span>
27+
</div>
28+
</div>
29+
<CommentList
30+
:data="commentData"
31+
:loading="commentLoading"
32+
:type="type"
33+
:load-more="commentHasMore"
34+
@load-more="handleLoadMore"
35+
/>
36+
<div class="placeholder" />
37+
</div>
38+
</n-scrollbar>
39+
</div>
40+
</div>
41+
</template>
42+
43+
<script setup lang="ts">
44+
import type { CommentType } from "@/types/main";
45+
import CommentList from "@/components/List/CommentList.vue";
46+
import { useElementSize } from "@vueuse/core";
47+
import { getComment, getHotComment } from "@/api/comment";
48+
import { formatCommentList } from "@/utils/format";
49+
import { isEmpty } from "lodash-es";
50+
51+
const props = withDefaults(
52+
defineProps<{
53+
// 资源 ID
54+
id: number;
55+
// 评论类型 0: 歌曲, 1: mv, 2: 歌单, 3: 专辑, 4: 电台节目, 5: 视频, 6: 动态, 7: 电台
56+
type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
57+
// 高度
58+
height?: number | "auto"; // px
59+
}>(),
60+
{},
61+
);
62+
63+
const commentListRef = ref<HTMLElement | null>(null);
64+
65+
// 列表高度
66+
const { height: commentListHeight, stop: stopCalcHeight } = useElementSize(commentListRef);
67+
68+
// 评论数据
69+
const commentLoading = ref<boolean>(false);
70+
const commentData = ref<CommentType[]>([]);
71+
const commentHotData = ref<CommentType[] | null>(null);
72+
const commentPage = ref<number>(1);
73+
const commentHasMore = ref<boolean>(true);
74+
75+
// 获取热门评论
76+
const getHotCommentData = async () => {
77+
if (!props.id) return;
78+
try {
79+
const result = await getHotComment(props.id, props.type);
80+
const formatData = formatCommentList(result.hotComments);
81+
commentHotData.value = formatData?.length > 0 ? formatData : null;
82+
} catch (error) {
83+
console.error("Error getting hot comment data:", error);
84+
commentHotData.value = null;
85+
}
86+
};
87+
88+
// 获取评论数据
89+
const getCommentData = async (clean: boolean = true) => {
90+
if (!props.id) return;
91+
try {
92+
commentLoading.value = true;
93+
if (clean) {
94+
commentData.value = [];
95+
commentPage.value = 1;
96+
commentHasMore.value = true;
97+
}
98+
// 获取热门评论
99+
await getHotCommentData();
100+
// 分页参数
101+
const cursor =
102+
commentPage.value > 1 && commentData.value?.length > 0
103+
? commentData.value[commentData.value.length - 1]?.time
104+
: undefined;
105+
// 获取评论
106+
const result = await getComment(props.id, props.type, commentPage.value, 20, 3, cursor);
107+
if (isEmpty(result.data?.comments)) {
108+
commentHasMore.value = false;
109+
commentLoading.value = false;
110+
return;
111+
}
112+
// 处理数据
113+
const formatData = formatCommentList(result.data.comments);
114+
commentData.value = commentData.value.concat(formatData);
115+
// 是否还有
116+
commentHasMore.value = result.data.hasMore;
117+
commentLoading.value = false;
118+
} catch (error) {
119+
console.error("Error getting comment data:", error);
120+
window.$message.error("获取评论数据失败");
121+
commentLoading.value = false;
122+
}
123+
};
124+
125+
// 加载更多评论
126+
const handleLoadMore = () => {
127+
if (!commentHasMore.value || commentLoading.value) return;
128+
commentPage.value += 1;
129+
getCommentData(false);
130+
};
131+
132+
// 监听 id 变化,重置评论数据
133+
watch(
134+
() => props.id,
135+
(newId) => {
136+
if (newId) {
137+
commentData.value = [];
138+
commentHotData.value = null;
139+
commentPage.value = 1;
140+
commentHasMore.value = true;
141+
getCommentData();
142+
}
143+
},
144+
{ immediate: true },
145+
);
146+
147+
// 如果高度是 auto,停止计算高度
148+
watch(
149+
() => props.height,
150+
(newHeight) => {
151+
if (newHeight === "auto") stopCalcHeight();
152+
},
153+
{ immediate: true },
154+
);
155+
</script>
156+
157+
<style lang="scss" scoped>
158+
.list-comment {
159+
height: 100%;
160+
.title {
161+
display: flex;
162+
align-items: center;
163+
font-size: 18px;
164+
font-weight: bold;
165+
margin-bottom: 12px;
166+
.n-icon {
167+
margin-right: 8px;
168+
font-size: 20px;
169+
}
170+
}
171+
}
172+
</style>

src/components/List/ListDetail.vue

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@
169169
<SvgIcon name="Search" />
170170
</template>
171171
</n-input>
172+
<!-- 查看评论 -->
173+
<n-tabs
174+
v-if="showCommentTab"
175+
v-model:value="currentTab"
176+
class="tabs"
177+
type="segment"
178+
@update:value="handleTabChange"
179+
>
180+
<n-tab name="songs"> 歌曲 </n-tab>
181+
<n-tab name="comments"> 评论 </n-tab>
182+
</n-tabs>
172183
</n-flex>
173184
</n-flex>
174185
</div>
@@ -211,6 +222,7 @@ interface Props {
211222
listScrolling: boolean;
212223
searchValue: string;
213224
showSearch?: boolean;
225+
showCommentTab?: boolean;
214226
config: ListDetailConfig;
215227
titleText?: string;
216228
playButtonText?: string;
@@ -219,6 +231,7 @@ interface Props {
219231
220232
const props = withDefaults(defineProps<Props>(), {
221233
showSearch: true,
234+
showCommentTab: false,
222235
titleText: "",
223236
playButtonText: "播放",
224237
moreOptions: () => [],
@@ -227,10 +240,14 @@ const props = withDefaults(defineProps<Props>(), {
227240
const emit = defineEmits<{
228241
"update:searchValue": [value: string];
229242
"play-all": [];
243+
"tab-change": [value: "songs" | "comments"];
230244
}>();
231245
232246
const router = useRouter();
233247
248+
// 当前 tab
249+
const currentTab = ref<"songs" | "comments">("songs");
250+
234251
// 标题文本
235252
const titleText = computed(() => {
236253
if (props.titleText) return props.titleText;
@@ -244,6 +261,7 @@ const handlePlayAll = () => {
244261
245262
// 处理搜索
246263
const handleSearch = (val: string) => {
264+
if ((!val || !val.trim()) && !props.searchValue) return;
247265
emit("update:searchValue", val);
248266
};
249267
@@ -268,6 +286,12 @@ const handleDescriptionClick = () => {
268286
openDescModal(props.detailData.description, title);
269287
}
270288
};
289+
290+
// 处理 tab 切换
291+
const handleTabChange = (value: "songs" | "comments") => {
292+
currentTab.value = value;
293+
emit("tab-change", value);
294+
};
271295
</script>
272296

273297
<style lang="scss" scoped>
@@ -456,6 +480,13 @@ const handleDescriptionClick = () => {
456480
width: 200px;
457481
}
458482
}
483+
.tabs {
484+
width: 200px;
485+
--n-tab-border-radius: 25px !important;
486+
:deep(.n-tabs-rail) {
487+
outline: 1px solid var(--n-tab-color-segment);
488+
}
489+
}
459490
}
460491
}
461492
}
@@ -475,11 +506,14 @@ const handleDescriptionClick = () => {
475506
}
476507
.menu {
477508
:deep(.n-button),
478-
.search {
509+
.search,
510+
.tabs {
479511
height: 32px;
480512
--n-font-size: 13px;
481513
--n-padding: 0 14px;
482514
--n-icon-size: 16px;
515+
--n-tab-font-size: 13px;
516+
--n-tab-padding: 2px 0;
483517
}
484518
}
485519
}

src/style/main.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ audio {
4747
.n-tabs-nav {
4848
position: relative;
4949
}
50+
.n-tabs-tab {
51+
transition: all 0.3s var(--n-bezier);
52+
}
5053
}
5154

5255
.n-switch {
@@ -274,6 +277,7 @@ audio {
274277
display: flex;
275278
flex-direction: column;
276279
.song-list,
280+
.list-comment,
277281
.loading,
278282
.n-empty {
279283
padding-top: 240px;
@@ -283,6 +287,7 @@ audio {
283287
}
284288
.list-detail.small {
285289
& ~ .song-list,
290+
& ~ .list-comment,
286291
& ~ .loading,
287292
& ~ .n-empty {
288293
padding-top: 120px;

0 commit comments

Comments
 (0)