1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-07-27 04:20:43 +08:00

style: 调整背景模糊值与布局样式,优化评论与图片功能

1.  调整全局背景模糊参数,统一并优化页面毛玻璃效果
2.  调整home页布局类名顺序,为post-card添加全屏宽度
3.  新增头像处理工具函数,补充随机图片源
4.  优化markdown内容容器的内边距样式
5.  重构文章详情页评论面板,实现完整评论回复功能
6.  优化文章详情页布局与样式细节
This commit is contained in:
小莫唐尼
2026-06-15 21:29:14 +08:00
parent a86baa9641
commit e191eb8358
7 changed files with 480 additions and 122 deletions
@@ -1,84 +1,400 @@
<script setup lang="ts">
import { listComments } from '@/api/halo-base/comment'
import { createComment, createReply, listCommentReplies, listComments } from '@/api/halo-base/comment'
import { completeUrl } from '@/utils/url'
import { useHaloScroll } from '@/hooks/useHaloScroll'
import type { CommentV1alpha1PublicApiListComments1Request, CommentWithReplyVo, PostVo } from '@halo-dev/api-client'
import { avatar } from '@/utils/imageHelper'
import { timeFrom } from '@/utils/base/timeFrom'
import type { CommentV1alpha1PublicApiListCommentRepliesRequest, CommentV1alpha1PublicApiListComments1Request, CommentWithReplyVo, ReplyVo } from '@halo-dev/api-client'
import type { IHaloListResponseBase } from '@/api/types/halo'
interface IProps {
post: PostVo | undefined
postName: string
commentSubjectVersion: string
commentCount?: number
}
const props = defineProps<IProps>()
const props = withDefaults(defineProps<IProps>(), {
commentCount: 0,
})
const emits = defineEmits<{
(e: 'close'): void
(e: 'commented'): void
}>()
const {
response, // 响应式的原始数据响应
list, // 响应式的数据列表
loading, // 是否加载中
finished, // 是否已全部加载
error, // 是否加载失败
refresh, // 刷新数据的函数
loadMore, // 加载更多数据的函数
} = useHaloScroll<CommentWithReplyVo, CommentV1alpha1PublicApiListComments1Request>({
fetchData: listComments,
params: {
version: 'v1alpha1',
kind: 'Comment',
page: 1,
size: 12,
name: props.post?.metadata?.name ?? undefined,
},
// --- 评论列表 ---
const commentList = ref<CommentWithReplyVo[]>([])
const commentPage = ref(1)
const commentHasNext = ref(false)
const commentLoading = ref(false)
// --- 回复目标 ---
const replyTarget = ref<{
commentName: string
replyName?: string
displayName: string
} | null>(null)
// --- 输入内容 ---
const inputText = ref('')
const submitLoading = ref(false)
// --- 展开回复的评论 ---
const expandedReplies = ref<Set<string>>(new Set())
const replyLoadingMap = ref<Record<string, boolean>>({})
const replyHasNextMap = ref<Record<string, boolean>>({})
const replyPageMap = ref<Record<string, number>>({})
// --- 获取评论列表 ---
async function fetchComments(page = 1, append = false) {
commentLoading.value = true
try {
const res = await listComments({
group: 'content.halo.run',
name: props.postName,
kind: 'Post',
version: props.commentSubjectVersion,
page,
size: 20,
sort: ['metadata.creationTimestamp,desc'],
} as CommentV1alpha1PublicApiListComments1Request)
const data = res as unknown as IHaloListResponseBase<CommentWithReplyVo>
if (append) {
commentList.value = [...commentList.value, ...(data.items ?? [])]
}
else {
commentList.value = data.items ?? []
}
commentHasNext.value = data.hasNext
commentPage.value = page
}
catch (err) {
console.error('获取评论失败:', err)
}
finally {
commentLoading.value = false
}
}
// --- 加载更多评论 ---
function handleLoadMoreComments() {
if (commentLoading.value || !commentHasNext.value)
return
fetchComments(commentPage.value + 1, true)
}
// --- 获取回复列表 ---
async function fetchReplies(commentName: string, page = 1, append = false) {
replyLoadingMap.value[commentName] = true
try {
const res = await listCommentReplies({
name: commentName,
page,
size: 10,
} as CommentV1alpha1PublicApiListCommentRepliesRequest)
const data = res as unknown as IHaloListResponseBase<ReplyVo>
const comment = commentList.value.find(c => c.metadata.name === commentName)
if (comment) {
if (append) {
comment.replies = {
...comment.replies,
items: [...(comment.replies?.items ?? []), ...(data.items ?? [])],
hasNext: data.hasNext,
} as any
}
else {
comment.replies = {
...data,
items: data.items ?? [],
} as any
}
}
replyHasNextMap.value[commentName] = data.hasNext
replyPageMap.value[commentName] = page
}
catch (err) {
console.error('获取回复失败:', err)
uni.showToast({ title: '获取回复失败', icon: 'none' })
}
finally {
replyLoadingMap.value[commentName] = false
}
}
// --- 切换展开回复 ---
function handleToggleReplies(comment: CommentWithReplyVo) {
const name = comment.metadata.name
if (expandedReplies.value.has(name)) {
expandedReplies.value.delete(name)
}
else {
expandedReplies.value.add(name)
if (!comment.replies?.items?.length) {
fetchReplies(name)
}
}
}
// --- 加载更多回复 ---
function handleLoadMoreReplies(commentName: string) {
if (replyLoadingMap.value[commentName] || !replyHasNextMap.value[commentName])
return
fetchReplies(commentName, (replyPageMap.value[commentName] ?? 1) + 1, true)
}
// --- 设置回复目标 ---
function handleReply(commentName: string, replyName: string | undefined, displayName: string) {
replyTarget.value = { commentName, replyName, displayName }
}
// --- 取消回复 ---
function handleCancelReply() {
replyTarget.value = null
}
// --- 提交评论/回复 ---
async function handleSubmit() {
const text = inputText.value.trim()
if (!text || submitLoading.value)
return
submitLoading.value = true
try {
if (replyTarget.value) {
await createReply({
name: replyTarget.value.commentName,
replyRequest: {
content: text,
raw: text,
quoteReply: replyTarget.value.replyName || undefined,
allowNotification: true,
},
} as any)
}
else {
await createComment({
commentRequest: {
content: text,
raw: text,
subjectRef: {
group: 'content.halo.run',
kind: 'Post',
name: props.postName,
version: props.commentSubjectVersion,
},
allowNotification: true,
},
} as any)
}
inputText.value = ''
replyTarget.value = null
uni.showToast({ title: '评论成功', icon: 'none' })
await fetchComments(1)
emits('commented')
}
catch (err) {
console.error('评论失败:', err)
uni.showToast({ title: '评论失败', icon: 'none' })
}
finally {
submitLoading.value = false
}
}
// --- 关闭面板 ---
function handleClose() {
emits('close')
}
defineExpose({
refresh: () => fetchComments(),
})
onMounted(() => {
fetchComments()
})
</script>
<template>
<!-- 评论面板根据分组显示 -->
<view
class="uh-backdrop-blur-xs fixed inset-0 z-110 flex items-end justify-center bg-black/20"
@click.stop="emits('close')"
>
<wd-transition :show="true" :lazy-render="true" :duration="150" name="fade-up">
<view class="box-border w-screen rounded-2xl bg-white p-4" @click.stop>
<view class="box-border w-full flex shrink-0 items-center justify-between gap-x-4">
<view class="text-gray-900 font-bold">
记录点什么
</view>
<view class="shrink-0">
<wd-icon name="close-circle" size="24px" color="#1A1A1A" @click="emits('close')" />
<view class="uh-backdrop-blur fixed inset-0 z-110 flex items-end bg-black/20" @click="handleClose">
<wd-transition :show="true" :duration="300" name="slide-up" :lazy-render="false">
<!-- 面板主体 -->
<view
class="box-border w-screen flex flex-col rounded-t-3xl bg-white"
@click.stop
>
<!-- 拖拽指示条 -->
<view class="w-full center py-2" @click.stop="handleClose">
<view class="h-1 w-10 rounded-full bg-gray-300" />
</view>
<!-- 头部 -->
<view class="box-border flex items-center justify-between border-b border-gray-100 px-4 pb-3">
<text class="text-base text-gray-900 font-bold">
说点什么 <text v-if="commentCount > 0" class="text-sm text-gray-600 font-normal">{{ commentCount }}</text>
</text>
<view class="h-8 w-8 center rounded-full bg-gray-50" @click="handleClose">
<wd-icon name="close" color="#6B7280" :size="18" />
</view>
</view>
<view class="mt-2 w-full">
<scroll-view
v-if="list.length > 0" class="h-[60vh] w-full" scroll-y :refresher-enabled="true"
:refresher-triggered="loading" @scrolltolower="loadMore" @refresherrefresh="refresh"
>
<view class="box-border w-full">
这里是评论记录
<!-- 评论列表 -->
<scroll-view
class="h-[60vh] overflow-hidden"
scroll-y
@scrolltolower="handleLoadMoreComments"
>
<view class="box-border h-full px-4 pt-3">
<!-- 加载中 -->
<view v-if="commentLoading && commentList.length === 0" class="box-border h-full center">
<wd-loading />
</view>
<!-- 空状态 -->
<view v-else-if="!commentLoading && commentList.length === 0" class="box-border h-full center flex-col gap-y-2">
<wd-icon name="chat" color="#D1D5DB" :size="40" />
<text class="text-sm text-gray-400">快来抢沙发吧~</text>
</view>
<!-- 评论列表 -->
<view v-else class="box-border h-full w-full flex flex-col">
<view
v-for="comment in commentList"
:key="comment.metadata.name"
class="border-b border-gray-50 py-3"
>
<view class="flex gap-x-3">
<!-- 头像 -->
<image
class="uh-shadow-md box-border h-9 w-9 shrink-0 border-2 border-white rounded-xl border-solid bg-gray-100"
:src="avatar(comment.owner.avatar)"
mode="aspectFill"
/>
<!-- 内容区 -->
<view class="flex flex-1 flex-col gap-y-2 overflow-hidden">
<view class="flex items-center justify-between">
<text class="text-xs text-gray-500 font-medium">{{ comment.owner.displayName || '匿名用户' }}</text>
<text class="text-[11px] text-gray-400">{{ timeFrom(comment.spec.creationTime || comment.metadata.creationTimestamp) }}</text>
</view>
<text class="text-sm text-gray-900 leading-5">
<rich-text :nodes="comment.spec.raw" />
</text>
<!-- 操作栏 -->
<view class="flex items-center gap-x-4">
<view
class="flex items-center gap-x-1"
@click="handleReply(comment.metadata.name, undefined, comment.owner.displayName || '匿名用户')"
>
<wd-icon name="message" color="#9CA3AF" :size="14" />
<text class="text-[11px] text-gray-400">回复</text>
</view>
<view
v-if="(comment.status?.replyCount ?? 0) > 0"
class="flex items-center gap-x-1"
@click="handleToggleReplies(comment)"
>
<wd-icon
:name="expandedReplies.has(comment.metadata.name) ? 'up' : 'down'"
color="#9CA3AF"
:size="14"
/>
<text class="text-[11px] text-gray-400">
{{ expandedReplies.has(comment.metadata.name) ? '收起' : `${comment.status?.replyCount}条回复` }}
</text>
</view>
</view>
<!-- 回复列表 -->
<view v-if="expandedReplies.has(comment.metadata.name)" class="mt-2 flex flex-col gap-y-2 rounded-xl bg-gray-50 p-3">
<view
v-for="reply in (comment.replies?.items ?? [])"
:key="reply.metadata.name"
class="flex gap-x-2.5"
>
<image
class="h-7 w-7 shrink-0 border-2 border-white rounded-lg border-solid bg-gray-200"
:src="avatar(reply.owner.avatar)"
mode="aspectFill"
/>
<view class="flex flex-1 flex-col gap-y-1 overflow-hidden">
<view class="flex items-center justify-between">
<text class="text-[11px] text-gray-500 font-medium">{{ reply.owner.displayName || '匿名用户' }}</text>
<text class="text-[10px] text-gray-400">{{ timeFrom(reply.spec.creationTime || reply.metadata.creationTimestamp) }}</text>
</view>
<view v-if="reply.spec.quoteReply" class="rounded-lg bg-gray-100 px-2.5 py-1.5">
<text class="text-[11px] text-gray-400">回复 {{ reply.spec.quoteReply }}</text>
</view>
<text class="text-xs text-gray-700 leading-4.5">{{ reply.spec.raw }}</text>
<view
class="flex items-center gap-x-1"
@click="handleReply(comment.metadata.name, reply.metadata.name, reply.owner.displayName || '匿名用户')"
>
<wd-icon name="chat" color="#9CA3AF" :size="12" />
<text class="text-[10px] text-gray-400">回复</text>
</view>
</view>
</view>
<!-- 加载更多回复 -->
<view
v-if="replyHasNextMap[comment.metadata.name]"
class="center py-1"
@click="handleLoadMoreReplies(comment.metadata.name)"
>
<text class="text-xs text-gray-400">
{{ replyLoadingMap[comment.metadata.name] ? '加载中...' : '查看更多回复' }}
</text>
</view>
<view v-if="replyLoadingMap[comment.metadata.name] && !(comment.replies?.items?.length)" class="center py-2">
<wd-loading :size="20" />
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="commentHasNext" class="center py-3" @click="handleLoadMoreComments">
<text class="text-xs text-gray-400">{{ commentLoading ? '加载中...' : '加载更多评论' }}</text>
</view>
<view class="h-4 w-full" />
</view>
</scroll-view>
<view v-else class="h-[60vh] flex items-center justify-center">
<wd-empty tip="暂无数据">
<template #image>
<wd-icon name="empty" color="#1A1A1A" :size="52" />
</template>
</wd-empty>
</view>
<view class="mt-4 w-full flex flex-col items-center justify-center gap-y-2 text-xs text-gray-900">
<view class="text-xs text-gray-900">
{{ response?.total || 0 }} 条记录 已加载 {{ list.length }} 条记录
</scroll-view>
<!-- 底部输入区 -->
<view class="box-border border-t border-gray-100 px-4 pt-2">
<!-- 回复提示 -->
<view v-if="replyTarget" class="mb-2 flex items-center justify-between rounded-lg bg-gray-50 px-3 py-1.5">
<text class="text-xs text-gray-500">回复 {{ replyTarget.displayName }}</text>
<view @click="handleCancelReply">
<wd-icon name="close" color="#9CA3AF" :size="14" />
</view>
<view v-if="loading || error || finished">
<wd-loading v-if="loading" text="加载中..." direction="horizontal" color="#1A1A1A" :size="16" />
<view v-else-if="error" class="w-full rounded-lg bg-gray-900 px-2 py-1.5 text-center text-xs text-white">
<wd-icon name="refresh" :size="14" /> 加载失败点击重试
</view>
<view v-else-if="finished" class="text-xs text-gray-900">
数据已全部加载
</view>
</view>
<view class="flex items-center gap-x-3 pb-2">
<input
v-model="inputText"
class="flex-1 rounded-xl bg-gray-50 px-4 py-2.5 text-sm active:bg-gray-100 focus:bg-gray-100"
:placeholder="replyTarget ? `回复 ${replyTarget.displayName}...` : '写评论...'"
confirm-type="send"
:adjust-position="true"
@confirm="handleSubmit"
>
<view
class="h-9 w-9 center shrink-0 rounded-xl transition-colors"
:class="inputText.trim() ? 'bg-gray-900' : 'bg-gray-200'"
@click="handleSubmit"
>
<wd-icon name="arrow-up" :color="inputText.trim() ? '#FFFFFF' : '#9CA3AF'" :size="18" />
</view>
</view>
</view>