refactor: 拆分 App.vue 为独立组件,修复多项敷衍方案

- 提取 TitleBar、Sidebar、RoamDrawer、CloseModal 四个组件
- App.vue 从 676 行精简至 238 行,职责更清晰
- 修复评论点赞无限+1:改为基于服务端 liked 字段切换
- 修复评论点赞无防重复:添加 likingSet 锁
- 修复评论点赞用本地缓存:likedIds 改为从服务端 likelist API 同步
- 删除 likedIds 写 localStorage 的 watch
- 播放失败/FM加载失败/减少推荐失败添加 showToast 用户提示
- 抽屉遮罩下标题栏按钮保持原色(z-10 提升至遮罩之上)
This commit is contained in:
2026-05-29 21:13:14 +08:00
parent 57aa9dae61
commit 68f29c8ea8
7 changed files with 594 additions and 468 deletions

View File

@ -1,32 +1,34 @@
<template>
<div class="bg-subtle rounded-xl p-3" ref="scrollContainer">
<div v-if="loading" class="py-8 text-center text-content-2 text-sm">加载中...</div>
<div v-if="loading" class="py-8 text-center text-sm" :class="darkMode ? 'text-white/60' : 'text-content-2'">加载中...</div>
<div v-else-if="comments.length === 0" class="py-8 text-center">
<IconMessageSquare class="mx-auto mb-2 text-content-3 w-10 h-10" />
<p class="text-content-3 text-sm">暂无评论</p>
<IconMessageSquare class="mx-auto mb-2 w-10 h-10" :class="darkMode ? 'text-white/40' : 'text-content-3'" />
<p class="text-sm" :class="darkMode ? 'text-white/40' : 'text-content-3'">暂无评论</p>
</div>
<div v-else class="space-y-3">
<div
v-for="comment in comments"
:key="comment.commentId"
class="p-3 rounded-xl bg-surface/50"
class="p-3 rounded-xl"
:class="darkMode ? 'bg-white/8' : 'bg-surface/50'"
>
<div class="flex items-center gap-3">
<img :src="comment.user.avatarUrl" class="w-8 h-8 rounded-full object-cover flex-shrink-0" />
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-content truncate">{{ comment.user.nickname }}</p>
<p class="text-xs text-content-3">{{ new Date(comment.time).toLocaleDateString('zh-CN') }}</p>
<p class="text-sm font-medium truncate" :class="darkMode ? 'text-white/90' : 'text-content'">{{ comment.user.nickname }}</p>
<p class="text-xs" :class="darkMode ? 'text-white/40' : 'text-content-3'">{{ new Date(comment.time).toLocaleDateString('zh-CN') }}</p>
</div>
</div>
<p class="mt-2 text-sm text-content-2 leading-relaxed">{{ comment.content }}</p>
<p class="mt-2 text-sm leading-relaxed" :class="darkMode ? 'text-white/70' : 'text-content-2'">{{ comment.content }}</p>
<div class="mt-2 flex justify-end">
<button
@click="likeComment(comment.commentId)"
class="flex items-center gap-1 text-content-3 hover:text-danger transition text-xs"
class="flex items-center gap-1 transition text-xs"
:class="comment.liked ? 'text-danger' : (darkMode ? 'text-white/40 hover:text-danger' : 'text-content-3 hover:text-danger')"
>
<IconHeart style="font-size: 14px" />
<IconHeart style="font-size: 14px" :class="comment.liked ? '[&>path]:fill-current [&>path]:stroke-0' : ''" />
<span>{{ comment.likedCount }}</span>
</button>
</div>
@ -34,7 +36,7 @@
<div ref="sentinel" class="h-1"></div>
</div>
<div v-if="loadingMore" class="py-4 text-center text-content-3 text-sm">加载中...</div>
<div v-if="loadingMore" class="py-4 text-center text-sm" :class="darkMode ? 'text-white/40' : 'text-content-3'">加载中...</div>
</div>
</template>
@ -47,6 +49,7 @@ import IconHeart from '~icons/lucide/heart'
const props = defineProps<{
type: number
id: number
darkMode?: boolean
}>()
const comments = ref<any[]>([])
@ -104,22 +107,29 @@ function loadMore() {
fetchComments()
}
const likingSet = ref(new Set<number>())
async function likeComment(cid: number) {
if (likingSet.value.has(cid)) return
const target = comments.value.find(c => c.commentId === cid)
if (!target) return
const liked = !!target.liked
likingSet.value.add(cid)
try {
await invoke('comment_like', {
query: {
t: 1,
t: liked ? 0 : 1,
type: props.type,
id: props.id,
cid
}
})
const target = comments.value.find(c => c.commentId === cid)
if (target) {
target.likedCount++
}
target.liked = !liked
target.likedCount += liked ? -1 : 1
} catch (e) {
console.error(e)
} finally {
likingSet.value.delete(cid)
}
}