1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

refactor: 抽离点赞hook并统一点赞逻辑,优化评论列表组件

1. 新增useUpvote点赞hook,统一文章、瞬间的点赞逻辑与本地缓存
2. 重构文章详情、瞬间详情页的点赞代码,移除重复实现
3. 为评论列表组件添加kind参数支持动态类型,新增refresh暴露方法与全局刷新监听
4. 优化瞬间列表页的点赞、评论交互与样式
This commit is contained in:
小莫唐尼
2026-09-09 18:40:14 +08:00
parent d528d214b5
commit b793920097
5 changed files with 192 additions and 69 deletions
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import { getPostCommentList } from '@/api/halo' import { getPostCommentList } from '@/api/halo'
import { checkAvatarUrl } from '@/utils/url' import { checkAvatarUrl } from '@/utils/url'
import type { IComment, ICommentListRes } from '@/api/types/halo' import type { IComment, ICommentListRes } from '@/api/types/halo'
@@ -8,8 +8,11 @@
disallowComment ?: boolean disallowComment ?: boolean
postName : string postName : string
post : { metadata : { name : string } } post : { metadata : { name : string } }
/** 评论目标 kind(文章 Post / 瞬间 Moment) */
kind ?: string
}>(), { }>(), {
disallowComment: false, disallowComment: false,
kind: 'Post',
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@@ -21,7 +24,7 @@
const loading = ref<'loading' | 'success' | 'error'>('loading') const loading = ref<'loading' | 'success' | 'error'>('loading')
const queryParams = ref({ const queryParams = ref({
group: 'content.halo.run', group: 'content.halo.run',
kind: 'Post', kind: props.kind,
version: 'v1alpha1', version: 'v1alpha1',
name: props.postName, name: props.postName,
page: 1, page: 1,
@@ -91,6 +94,22 @@
}) })
} }
/** 外部刷新(评论成功后由宿主调用) */
function refresh() {
handleGetData()
}
/** 兼容旧广播链路(article-detail 评论成功后 uni.$emit('comment_list_refresh')) */
onMounted(() => {
uni.$on('comment_list_refresh', handleGetData)
})
onUnmounted(() => {
uni.$off('comment_list_refresh', handleGetData)
})
defineExpose({ refresh })
handleGetData() handleGetData()
</script> </script>
@@ -135,7 +154,7 @@
<view v-if="dataList.length === 0" class="empty py-12"> <view v-if="dataList.length === 0" class="empty py-12">
<view class="empty-box flex flex-col items-center"> <view class="empty-box flex flex-col items-center">
<wd-icon class-prefix="uhemoji-icon" name="-confused" size="100rpx" class="text-primary" /> <wd-icon class-prefix="uhemoji-icon" name="-confused" size="100rpx" class="text-primary" />
<text class="mt-2 text-sm text-gray-500">{{disallowComment ? '暂无评论' : '暂无评论'}}</text> <text class="mt-2 text-sm text-gray-500">暂无评论</text>
<view v-if="disallowComment" class="mt-2 text-xs text-red-400"> <view v-if="disallowComment" class="mt-2 text-xs text-red-400">
文章已开启禁止评论 文章已开启禁止评论
</view> </view>
+60
View File
@@ -0,0 +1,60 @@
/**
* 点赞交互(文章详情 / 瞬间详情 / 瞬间列表共用)
* 已点赞名单本地持久化(刷新/重进后仍防重复);支持单目标(详情页)与多目标(列表页)。
*/
import { ref } from 'vue'
import { submitUpvote } from '@/api/halo'
import { getCache, setCache } from '@/utils/storage'
const UPVOTE_CACHE_KEY = 'uh_upvoted_names'
function readUpvotedNames(): string[] {
const list = getCache<string[]>(UPVOTE_CACHE_KEY)
return Array.isArray(list) ? list : []
}
export function useUpvote(plural: 'posts' | 'moments', getName: () => string | undefined) {
const upvotedNames = ref<string[]>(readUpvotedNames())
/** 判断是否已点赞(可指定 name;缺省用单目标 getName) */
function hasUpvoted(name?: string): boolean {
const n = name ?? getName()
return !!n && upvotedNames.value.includes(n)
}
/** 按 name 点赞(列表多目标场景;成功后持久化 + 回调) */
async function likeByName(name: string, onSuccess?: (name: string) => void) {
if (!name)
return
if (upvotedNames.value.includes(name)) {
uni.showToast({ icon: 'none', title: '已经点过赞啦!' })
return
}
try {
await submitUpvote({ group: 'content.halo.run', plural, name })
uni.showToast({ icon: 'none', title: '点赞成功!' })
upvotedNames.value.push(name)
setCache(UPVOTE_CACHE_KEY, upvotedNames.value)
onSuccess?.(name)
}
catch (err) {
console.error('点赞失败', err)
uni.showToast({ icon: 'none', title: '点赞失败' })
}
}
/** 点赞当前目标(详情页单目标场景) */
async function handleDoLikes(onSuccess?: (name: string) => void) {
const name = getName()
if (!name)
return
await likeByName(name, onSuccess)
}
return {
upvotedNames,
hasUpvoted,
handleDoLikes,
likeByName,
}
}
@@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getPostByName, getPostCommentReplyList, postTrackersCounter, submitUpvote } from '@/api/halo' import { getPostByName, getPostCommentReplyList, postTrackersCounter } from '@/api/halo'
import { useUpvote } from '@/hooks/useUpvote'
import { createVerificationCode, requestRestrictReadCheck } from '@/api/uni-halo' import { createVerificationCode, requestRestrictReadCheck } from '@/api/uni-halo'
import { formatTime } from '@/utils/formatTime' import { formatTime } from '@/utils/formatTime'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
@@ -188,36 +189,14 @@
} }
/* ---------------- 点赞 ---------------- */ /* ---------------- 点赞 ---------------- */
const upvotedNames = ref<string[]>([]) const { hasUpvoted, handleDoLikes } = useUpvote('posts', () => result.value?.metadata.name)
function hasUpvoted() : boolean { function handleDoLikesClick() {
return upvotedNames.value.includes(result.value?.metadata.name || '') handleDoLikes((name) => {
} if (result.value?.stats) {
async function handleDoLikes() {
if (!result.value) {
return
}
if (hasUpvoted()) {
uni.showToast({ icon: 'none', title: '已经点过赞啦!' })
return
}
try {
await submitUpvote({
group: 'content.halo.run',
plural: 'posts',
name: result.value.metadata.name,
})
uni.showToast({ icon: 'none', title: '点赞成功!' })
upvotedNames.value.push(result.value.metadata.name)
if (result.value.stats) {
result.value.stats.upvote = (result.value.stats.upvote || 0) + 1 result.value.stats.upvote = (result.value.stats.upvote || 0) + 1
} }
} })
catch (err) {
console.error('点赞失败', err)
uni.showToast({ icon: 'none', title: '点赞失败' })
}
} }
/* ---------------- 收藏 ---------------- */ /* ---------------- 收藏 ---------------- */
@@ -358,6 +337,13 @@
commentModal.value.show = false commentModal.value.show = false
} }
/** 评论列表加载完成:同步评论计数(统计区展示) */
function handleCommentLoaded(list : IComment[]) {
if (result.value?.stats) {
result.value.stats.comment = list.length
}
}
function handleOnShowCommentDetail(data : { postName : string, comment : IComment }) { function handleOnShowCommentDetail(data : { postName : string, comment : IComment }) {
commentDetail.value = { commentDetail.value = {
show: true, show: true,
@@ -595,8 +581,8 @@
<!-- 评论区域 --> <!-- 评论区域 -->
<uh-comment-list v-if="calcIsShowComment && result" :disallow-comment="!result.spec.allowComment" <uh-comment-list v-if="calcIsShowComment && result" :disallow-comment="!result.spec.allowComment"
:post-name="result.metadata.name" :post="result" @on-comment="handleOnComment" :post-name="result.metadata.name" :post="result" @on-comment="handleOnComment"
@on-comment-detail="handleOnShowCommentDetail" /> @on-comment-detail="handleOnShowCommentDetail" @on-loaded="handleCommentLoaded" />
</view> </view>
</view> </view>
@@ -607,7 +593,7 @@
class="uh-global-card-glass box-border flex items-center justify-center gap-2 border rounded-full p-1 text-primary"> class="uh-global-card-glass box-border flex items-center justify-center gap-2 border rounded-full p-1 text-primary">
<view <view
class="uh-global-card-glass box-border h-[72rpx] flex flex-1 items-center justify-center gap-x-1 border rounded-full px-4 shadow-none" class="uh-global-card-glass box-border h-[72rpx] flex flex-1 items-center justify-center gap-x-1 border rounded-full px-4 shadow-none"
:class="{ active: hasUpvoted() }" @click="handleDoLikes"> :class="{ active: hasUpvoted() }" @click="handleDoLikesClick">
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="36rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="36rpx" />
<text class="shrink-0 text-sm text-gray-900 font-semibold">点赞</text> <text class="shrink-0 text-sm text-gray-900 font-semibold">点赞</text>
</view> </view>
+31 -29
View File
@@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getMomentByName, submitUpvote } from '@/api/halo' import { getMomentByName } from '@/api/halo'
import { useUpvote } from '@/hooks/useUpvote'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { useFavoritesStore } from '@/store/favorites' import { useFavoritesStore } from '@/store/favorites'
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url' import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
@@ -138,35 +139,15 @@
} }
/* ---------------- 点赞 ---------------- */ /* ---------------- 点赞 ---------------- */
const upvotedNames = ref<string[]>([]) const { hasUpvoted, handleDoLikes } = useUpvote('moments', () => moment.value?.metadata.name)
function hasUpvoted() : boolean { function handleDoLikesClick() {
return upvotedNames.value.includes(moment.value?.metadata.name || '') handleDoLikes((name) => {
} const current = moment.value
if (current?.stats) {
async function handleDoLikes() {
const current = moment.value
if (!current) { return }
if (hasUpvoted()) {
uni.showToast({ icon: 'none', title: '已经点过赞啦!' })
return
}
try {
await submitUpvote({
group: 'content.halo.run',
plural: 'moments',
name: current.metadata.name,
})
uni.showToast({ icon: 'none', title: '点赞成功!' })
upvotedNames.value.push(current.metadata.name)
if (current.stats) {
current.stats.upvote = (current.stats.upvote || 0) + 1 current.stats.upvote = (current.stats.upvote || 0) + 1
} }
} })
catch (err) {
console.error('点赞失败', err)
uni.showToast({ icon: 'none', title: '点赞失败' })
}
} }
/* ---------------- 评论 ---------------- */ /* ---------------- 评论 ---------------- */
@@ -176,6 +157,8 @@
postName: '', postName: '',
title: '', title: '',
}) })
/** 评论列表组件实例(评论成功后刷新) */
const commentListRef = ref<{ refresh : () => void } | null>(null)
function handleToComment() { function handleToComment() {
const current = moment.value const current = moment.value
@@ -193,8 +176,23 @@
} }
} }
/** 评论列表触发(回复某条评论/新增) */
function handleOnComment(data : { isComment : boolean, postName : string, title : string }) {
commentModal.value = {
show: true,
isComment: data.isComment,
postName: data.postName,
title: data.title,
}
}
function handleOnCommentModalClose(data : { refresh : boolean, isSubmit : boolean }) { function handleOnCommentModalClose(data : { refresh : boolean, isSubmit : boolean }) {
commentModal.value.show = false commentModal.value.show = false
if (data.isSubmit) {
// 评论成功后刷新评论列表与计数
commentListRef.value?.refresh()
loadMoment()
}
} }
/* ---------------- 视频互斥 ---------------- */ /* ---------------- 视频互斥 ---------------- */
@@ -358,6 +356,10 @@
</view> </view>
</view> </view>
</view> </view>
<!-- 评论列表(瞬间评论,kind=Moment) -->
<uh-comment-list v-if="moment" ref="commentListRef" :post-name="moment.metadata.name" :post="moment"
kind="Moment" :disallow-comment="!moment.spec.allowComment" @on-comment="handleOnComment" />
</view> </view>
<!-- 悬浮操作(与文章详情一致:点赞/评论/收藏) --> <!-- 悬浮操作(与文章详情一致:点赞/评论/收藏) -->
<view class="fixed bottom-8 left-1/2 z-10 flex items-center justify-center pb-safe -translate-x-1/2"> <view class="fixed bottom-8 left-1/2 z-10 flex items-center justify-center pb-safe -translate-x-1/2">
@@ -366,12 +368,12 @@
<!-- 点赞 --> <!-- 点赞 -->
<view <view
class="uh-global-card-glass box-border h-[72rpx] flex flex-1 items-center justify-center gap-x-1 border rounded-full px-4 shadow-none" class="uh-global-card-glass box-border h-[72rpx] flex flex-1 items-center justify-center gap-x-1 border rounded-full px-4 shadow-none"
:class="{ active: hasUpvoted() }" @click="handleDoLikes"> :class="{ active: hasUpvoted() }" @click="handleDoLikesClick">
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="36rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="36rpx" />
<text class="shrink-0 text-sm text-gray-900 font-semibold">点赞</text> <text class="shrink-0 text-sm text-gray-900 font-semibold">点赞</text>
</view> </view>
<!-- 评论 --> <!-- 评论 -->
<view <view v-if="moment.spec.allowComment"
class="uh-global-card-glass box-border h-[72rpx] flex flex-1 items-center justify-center gap-x-1 border rounded-full px-4 shadow-none" class="uh-global-card-glass box-border h-[72rpx] flex flex-1 items-center justify-center gap-x-1 border rounded-full px-4 shadow-none"
@click="handleToComment()"> @click="handleToComment()">
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="36rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-thinking" size="36rpx" />
+62 -6
View File
@@ -13,6 +13,7 @@
import { formatTime } from '@/utils/formatTime' import { formatTime } from '@/utils/formatTime'
import { t } from '@/locale' import { t } from '@/locale'
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
import { useUpvote } from '@/hooks/useUpvote'
import { markdownConfig } from '@/config/markdown' import { markdownConfig } from '@/config/markdown'
import type { IMoment } from '@/api/types/halo' import type { IMoment } from '@/api/types/halo'
@@ -204,6 +205,52 @@
uni.showToast({ icon: 'none', title: favorited ? '收藏成功' : '已取消收藏' }) uni.showToast({ icon: 'none', title: favorited ? '收藏成功' : '已取消收藏' })
} }
/* ---------------- 点赞(useUpvote 持久化防重复) ---------------- */
const { hasUpvoted, likeByName } = useUpvote('moments', () => '')
function handleMomentLike(moment : MomentCard) {
if (!moment) { return }
likeByName(moment.metadata.name, (name) => {
if (moment.stats) {
moment.stats.upvote = (moment.stats.upvote || 0) + 1
}
})
}
/* ---------------- 评论(列表内直接弹评论窗) ---------------- */
const commentModal = ref({
show: false,
isComment: true,
postName: '',
title: '',
})
function handleMomentComment(moment : MomentCard) {
if (!moment) { return }
if (!moment.spec.allowComment) {
uni.showToast({ icon: 'none', title: '瞬间已开启禁止评论!' })
return
}
commentModal.value = {
show: true,
isComment: true,
postName: moment.metadata.name,
title: '新增评论',
}
}
function handleOnCommentModalClose(data : { refresh : boolean, isSubmit : boolean }) {
const postName = commentModal.value.postName
commentModal.value.show = false
if (data.isSubmit) {
// 评论成功后列表计数 +1
const target = dataList.value.find(item => item.metadata.name === postName)
if (target?.stats) {
target.stats.totalComment = (target.stats.totalComment || 0) + 1
}
}
}
function handleToTopPage(duration = 500) { function handleToTopPage(duration = 500) {
uni.pageScrollTo({ uni.pageScrollTo({
scrollTop: 0, scrollTop: 0,
@@ -311,7 +358,7 @@
<!-- 正文 --> <!-- 正文 -->
<view class="box-border px-4 pt-3"> <view class="box-border px-4 pt-3">
<view class="relative box-border rounded-lg bg-page p-3"> <view class="relative box-border rounded-lg bg-page p-3 text-gray-900 text-sm">
<mp-html lazy-load :domain="markdownConfig.domain ?? ''" <mp-html lazy-load :domain="markdownConfig.domain ?? ''"
:loading-img="markdownConfig.loadingGif" scroll-table selectable :loading-img="markdownConfig.loadingGif" scroll-table selectable
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle" :tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
@@ -342,18 +389,22 @@
<!-- (点赞/评论) --> <!-- (点赞/评论) -->
<view <view
class="mb-1 mt-2 box-border w-full flex items-center justify-between border-t border-black/5 px-4 py-3 text-xs text-gray-400"> class="mb-1 mt-2 box-border w-full flex items-center justify-between border-t border-black/5 px-4 py-3 text-xs text-gray-400">
<view class="flex items-center gap-x-1"> <view class="flex items-center gap-x-1" @click.stop="handleMomentLike(moment)">
<wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-kiss-" size="32rpx" />
<text class="text-sm text-gray-600">点赞 {{ moment.stats.upvote || 0 }}</text> <text class="text-sm"
:class="hasUpvoted(moment.metadata.name) ? 'text-primary' : 'text-gray-600'">
点赞 {{ moment.stats.upvote || 0 }}
</text>
</view> </view>
<view class="flex items-center gap-x-1"> <view v-if="moment.spec.allowComment" class="flex items-center gap-x-1"
@click.stop="handleMomentComment(moment)">
<wd-icon class-prefix="uhemoji-icon" name="-thinking" size="32rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-thinking" size="32rpx" />
<text class="text-sm text-gray-600">评论 {{ moment.stats.totalComment || 0 }}</text> <text class="text-sm text-gray-600">评论 {{ moment.stats.totalComment || 0 }}</text>
</view> </view>
<view class="flex items-center gap-x-1" @click.stop="handleToggleMomentFavorite(moment)"> <view class="flex items-center gap-x-1" @click.stop="handleToggleMomentFavorite(moment)">
<wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" /> <wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" />
<text class="text-sm text-gray-600" <text class="text-sm"
:style="isMomentFavorite(moment) ? { color: '#ffb300' } : ''"> :class="isMomentFavorite(moment) ? 'text-primary' : 'text-gray-600'">
{{ isMomentFavorite(moment) ? '已收藏' : '收藏' }} {{ isMomentFavorite(moment) ? '已收藏' : '收藏' }}
</text> </text>
</view> </view>
@@ -366,4 +417,9 @@
</view> </view>
</template> </template>
</view> </view>
<!-- 评论弹窗(瞬间评论,subjectKind=Moment) -->
<uh-comment-modal v-if="commentModal.show" :show="commentModal.show" :is-comment="commentModal.isComment"
:title="commentModal.title" :post-name="commentModal.postName" subject-kind="Moment"
@on-close="handleOnCommentModalClose" />
</template> </template>