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:
@@ -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 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="uh-backdrop-blur-xs fixed inset-0 z-110 flex items-end justify-center bg-black/20"
|
||||
@click.stop="emits('close')"
|
||||
class="box-border w-screen flex flex-col rounded-t-3xl bg-white"
|
||||
@click.stop
|
||||
>
|
||||
<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 class="w-full center py-2" @click.stop="handleClose">
|
||||
<view class="h-1 w-10 rounded-full bg-gray-300" />
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<wd-icon name="close-circle" size="24px" color="#1A1A1A" @click="emits('close')" />
|
||||
|
||||
<!-- 头部 -->
|
||||
<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"
|
||||
class="h-[60vh] overflow-hidden"
|
||||
scroll-y
|
||||
@scrolltolower="handleLoadMoreComments"
|
||||
>
|
||||
<view class="box-border w-full">
|
||||
这里是评论记录
|
||||
<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>
|
||||
</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 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 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 }} 条记录
|
||||
</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 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>
|
||||
|
||||
@@ -97,8 +97,8 @@ font-size: 14px;
|
||||
strong: `font-weight: 700;color: ${primaryColor};`,
|
||||
video: 'width: 100%',
|
||||
},
|
||||
// containStyle: 'font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 14px;color: #606266;word-spacing: 0.8px;letter-spacing: 0.8px; background-color:transparent;',
|
||||
containStyle: 'word-spacing: 0.8px;letter-spacing: 0.8px; background-color:transparent;',
|
||||
// containStyle: 'font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 14px;color: #606266;word-spacing: 0.8px;letter-spacing: 0.8px; background-color:transparent;',
|
||||
containStyle: 'padding:0 12px;word-spacing: 0.8px;letter-spacing: 0.8px; background-color:transparent;',
|
||||
|
||||
loadingGif: '',
|
||||
emptyGif: '',
|
||||
|
||||
@@ -127,7 +127,7 @@ onPageScroll(() => { })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-page3 min-h-screen">
|
||||
<view class="min-h-screen bg-page3">
|
||||
<!-- 顶部banner -->
|
||||
<view v-if="false" class="relative h-72 w-full">
|
||||
<image class="h-72 w-full object-cover" :src="exampleImageUrls.banner" />
|
||||
@@ -237,7 +237,7 @@ onPageScroll(() => { })
|
||||
<wd-empty :icon-size="60" icon="no-result" tip="暂无数据" />
|
||||
</view>
|
||||
<template v-else>
|
||||
<post-card v-for="post in postList" :key="post.metadata.name" :post="post" :type="postCardStyle" />
|
||||
<post-card v-for="post in postList" :key="post.metadata.name" :post="post" :type="postCardStyle" class="w-full" />
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
}
|
||||
|
||||
.uh-backdrop-blur {
|
||||
backdrop-filter: blur(4rpx);
|
||||
backdrop-filter: blur(2rpx);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur-xs {
|
||||
backdrop-filter: blur(8rpx);
|
||||
backdrop-filter: blur(4rpx);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur-sm {
|
||||
@@ -23,5 +23,5 @@
|
||||
}
|
||||
|
||||
.uh-backdrop-blur-md {
|
||||
backdrop-filter: blur(24rpx);
|
||||
backdrop-filter: blur(16rpx);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { formatNumberUnit } from '@/utils/base/digital'
|
||||
import type { NavigationPostVo, PostV1alpha1PublicApiQueryPostByNameRequest, PostVo } from '@halo-dev/api-client'
|
||||
import type { VoteRequest } from '@/api/halo-base/trackers'
|
||||
import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue'
|
||||
import UhPostCommentPanel from '@/components/post-detail/uh-post-comment-panel.vue'
|
||||
|
||||
interface IPostDetailLoadOptions {
|
||||
metadataName: string
|
||||
@@ -154,22 +155,11 @@ onShareAppMessage(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const commentPanel = reactive<{
|
||||
show: boolean
|
||||
post: PostVo | undefined
|
||||
}>({
|
||||
show: false,
|
||||
post: undefined,
|
||||
})
|
||||
// --- 评论面板 ---
|
||||
const showCommentPanel = ref(false)
|
||||
|
||||
function handleOpenCommentPanel(post: PostVo) {
|
||||
commentPanel.show = true
|
||||
commentPanel.post = post
|
||||
}
|
||||
|
||||
function handleCloseCommentPanel() {
|
||||
commentPanel.show = false
|
||||
commentPanel.post = undefined
|
||||
function handleOpenCommentPanel() {
|
||||
showCommentPanel.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -177,6 +167,7 @@ function handleCloseCommentPanel() {
|
||||
<view class="min-h-screen w-full flex flex-col bg-page3">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view
|
||||
v-if="false"
|
||||
class="fixed left-0 z-100 box-border w-full flex items-center justify-between"
|
||||
:style="{ paddingTop: 'calc(var(--status-bar-height, 44px) ' }"
|
||||
>
|
||||
@@ -203,12 +194,12 @@ function handleCloseCommentPanel() {
|
||||
<!-- 文章信息卡片(覆盖封面底部) -->
|
||||
<view class="relative z-20 box-border w-full flex flex-col gap-y-4 pt-52 -mt-8">
|
||||
<!-- 主信息卡片 -->
|
||||
<view v-if="htmlContent" class="uh-shadow-xs box-border w-full flex flex-col gap-y-3 overflow-hidden rounded-3xl bg-white p-4">
|
||||
<view v-if="htmlContent" class="uh-shadow-xs box-border w-full flex flex-col gap-y-3 overflow-hidden rounded-3xl bg-white">
|
||||
<!-- 标题 -->
|
||||
<text class="text-lg text-gray-900 font-bold leading-6">{{ post?.spec?.title ?? '加载中...' }}</text>
|
||||
<text class="box-border p-4 pb-0 text-lg text-gray-900 font-bold leading-6">{{ post?.spec?.title ?? '加载中...' }}</text>
|
||||
|
||||
<!-- 分类 & 标签 -->
|
||||
<view v-if="categories.length > 0 || tags.length > 0" class="flex flex-wrap items-center gap-2">
|
||||
<view v-if="categories.length > 0 || tags.length > 0" class="box-border flex flex-wrap items-center gap-2 px-4">
|
||||
<view
|
||||
v-for="cat in categories" :key="cat.metadata.name"
|
||||
class="rounded-full bg-gray-900 px-2.5 py-1 text-xs text-white font-medium"
|
||||
@@ -224,7 +215,7 @@ function handleCloseCommentPanel() {
|
||||
</view>
|
||||
|
||||
<!-- 元信息 -->
|
||||
<view class="flex flex-1 items-center gap-x-1">
|
||||
<view class="box-border flex flex-1 items-center gap-x-1 px-4">
|
||||
<text class="text-[11px] text-gray-500">{{ publishTime }}</text>
|
||||
<text class="text-[11px] text-gray-400">·</text>
|
||||
<text class="text-[11px] text-gray-500">浏览 {{ stats.visit }}</text>
|
||||
@@ -237,10 +228,10 @@ function handleCloseCommentPanel() {
|
||||
</view>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<view class="h-1px w-full bg-gray-50" />
|
||||
<view class="box-border h-1px w-full bg-gray-50 px-4" />
|
||||
|
||||
<!-- 作者信息 -->
|
||||
<view v-if="post?.owner" class="flex items-center gap-x-2 hidden!">
|
||||
<view v-if="post?.owner" class="box-border flex items-center gap-x-2 hidden!">
|
||||
<image
|
||||
v-if="post.owner.avatar" class="h-8 w-8 border-2 border-white rounded-full border-solid"
|
||||
:src="completeUrl(post.owner.avatar)" mode="aspectFill"
|
||||
@@ -252,11 +243,13 @@ function handleCloseCommentPanel() {
|
||||
</view>
|
||||
|
||||
<!-- 文章正文 -->
|
||||
<view class="box-border px-1">
|
||||
<mp-html
|
||||
:content="htmlContent" :tag-style="markdownConfig.tagStyle"
|
||||
:container-style="markdownConfig.containStyle" :domain="markdownConfig.domain" lazy-load selectable
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中占位 -->
|
||||
<view v-else-if="loading" class="uh-shadow-xs box-border w-full center rounded-2xl bg-white py-16">
|
||||
@@ -311,8 +304,24 @@ function handleCloseCommentPanel() {
|
||||
<view class="fixed bottom-6 left-4 right-4 z-100 flex items-center gap-x-4" @touchmove.stop.prevent>
|
||||
<!-- 左侧操作栏 -->
|
||||
<view
|
||||
class="uh-shadow-sm uh-backdrop-blur box-border h-46px flex flex-1 items-center justify-around border border-white rounded-full border-solid bg-white/95 p-1"
|
||||
class="uh-shadow-md uh-backdrop-blur box-border h-46px flex flex-1 items-center justify-around border border-white rounded-full border-solid bg-white/95 p-1"
|
||||
>
|
||||
<!-- 返回 -->
|
||||
<view class="flex flex-1 items-center justify-center gap-x-0.5" @click="handleBack">
|
||||
<view
|
||||
class="center rounded-full transition-colors"
|
||||
>
|
||||
<wd-icon
|
||||
name="arrow-left"
|
||||
color="#6B7280"
|
||||
:size="18"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-xs text-gray-900 font-medium">
|
||||
返回
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 点赞 -->
|
||||
<view class="flex flex-1 items-center justify-center gap-x-1" @click="handleUpvote">
|
||||
<view
|
||||
@@ -325,17 +334,17 @@ function handleCloseCommentPanel() {
|
||||
:size="18"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-sm font-medium" :class="isUpvoted ? 'text-red-500' : 'text-gray-600'">
|
||||
<text class="text-xs font-medium" :class="isUpvoted ? 'text-red-500' : 'text-gray-900'">
|
||||
{{ stats.upvote }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 评论 -->
|
||||
<view class="flex flex-1 items-center justify-center gap-x-1" @click="handleOpenCommentPanel(post)">
|
||||
<view class="flex flex-1 items-center justify-center gap-x-1" @click="handleOpenCommentPanel">
|
||||
<view class="center rounded-full bg-transparent">
|
||||
<wd-icon name="message" color="#6B7280" :size="18" />
|
||||
</view>
|
||||
<text class="text-sm text-gray-600 font-medium">{{ stats.comment }}</text>
|
||||
<text class="text-xs text-gray-900 font-medium">{{ stats.comment }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 分享 -->
|
||||
@@ -343,13 +352,13 @@ function handleCloseCommentPanel() {
|
||||
<view class="center rounded-full bg-transparent">
|
||||
<wd-icon name="share-alt" color="#6B7280" :size="18" />
|
||||
</view>
|
||||
<text class="text-sm text-gray-600 font-medium">分享</text>
|
||||
<text class="text-xs text-gray-900 font-medium">分享</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧返回顶部 -->
|
||||
<view
|
||||
class="uh-shadow-sm uh-backdrop-blur box-border h-46px w-46px center flex shrink-0 border border-white rounded-full border-solid bg-white/95"
|
||||
class="uh-shadow-md uh-backdrop-blur box-border h-46px w-46px center flex shrink-0 border border-white rounded-full border-solid bg-white/95"
|
||||
@click="handleScrollToTop"
|
||||
>
|
||||
<wd-icon name="arrow-up" color="#1A1A1A" :size="18" />
|
||||
@@ -358,24 +367,12 @@ function handleCloseCommentPanel() {
|
||||
</view>
|
||||
|
||||
<!-- 评论面板 -->
|
||||
<uh-post-comment-panel v-if="commentPanel.show" :post="commentPanel.post" @close="handleCloseCommentPanel" />
|
||||
<uh-post-comment-panel
|
||||
v-if="showCommentPanel"
|
||||
:post-name="postName"
|
||||
:comment-subject-version="post?.spec?.releaseSnapshot ?? ''"
|
||||
:comment-count="post?.stats?.comment ?? 0"
|
||||
@close="showCommentPanel = false"
|
||||
@commented="loadData"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-shadow-xs {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
|
||||
.uh-shadow-card {
|
||||
box-shadow: 0 16rpx 60rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.uh-shadow-sm {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur {
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { completeUrl } from '@/utils/url'
|
||||
import { getRandomUrl } from '@/utils/randomResources'
|
||||
import { getCrAvatar, getRandomUrl } from '@/utils/randomResources'
|
||||
|
||||
/**
|
||||
* 图片封面处理
|
||||
@@ -7,10 +7,30 @@ import { getRandomUrl } from '@/utils/randomResources'
|
||||
* @returns 处理后的图片封面
|
||||
*/
|
||||
export function cover(cover: string) {
|
||||
const defaultCover = getRandomUrl()
|
||||
if (!cover) {
|
||||
return defaultCover
|
||||
}
|
||||
if (cover.trim()) {
|
||||
return completeUrl(cover)
|
||||
}
|
||||
return getRandomUrl()
|
||||
return defaultCover
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像处理
|
||||
* @param avatar 头像
|
||||
* @returns 处理后的头像, 默认返回复古头像
|
||||
*/
|
||||
export function avatar(avatar: string) {
|
||||
const defaultAvatar = getCrAvatar('retro')
|
||||
if (!avatar) {
|
||||
return defaultAvatar
|
||||
}
|
||||
if (avatar.trim()) {
|
||||
return completeUrl(avatar)
|
||||
}
|
||||
return defaultAvatar
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sleep } from '@/utils/common'
|
||||
|
||||
export const randomImageUrl = {
|
||||
ycy: 'https://t.alcy.cc/ycy',
|
||||
pc: 'https://t.alcy.cc/pc',
|
||||
@@ -7,6 +5,7 @@ export const randomImageUrl = {
|
||||
picsum: 'https://picsum.photos/800/600',
|
||||
bing: 'https://bing.img.run/rand_uhd.php',
|
||||
xsot: 'https://api.xsot.cn/bing?jump=true',
|
||||
boxmoe: 'https://api.boxmoe.com/random.php',
|
||||
}
|
||||
|
||||
type randomUrlKeys = keyof typeof randomImageUrl
|
||||
@@ -15,6 +14,12 @@ export const randomVideosUrl = {
|
||||
acg: 'https://t.alcy.cc/acg',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Picsum 随机图片URL
|
||||
* @param width 图片宽度 默认800
|
||||
* @param height 图片高度 默认600
|
||||
* @returns 随机图片URL
|
||||
*/
|
||||
export function getPicSumImages(width: number = 800, height?: number) {
|
||||
const t = `${Date.now()}+${Math.random()}`
|
||||
if (!height) {
|
||||
@@ -23,6 +28,26 @@ export function getPicSumImages(width: number = 800, height?: number) {
|
||||
return `https://picsum.photos/${width}/${height}?t=${t}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 DiceURL
|
||||
* @param avatar 头像名称 可选值:lorelei=简约卡通人脸, pixel-art=像素小人, adventurer=可爱大头, avataaars=极简线条
|
||||
* @returns 头像URL
|
||||
*/
|
||||
export function getDiceBearAvatar(avatar: 'lorelei' | 'pixel-art' | 'adventurer' | 'avataaars', size?: number) {
|
||||
const t = `${Date.now()}+${Math.random()}`
|
||||
return `https://api.dicebear.com/9.x/${avatar}/svg?size=${size || 128}&random=${t}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 CravatarURL
|
||||
* @param avatar 头像名称 可选值:monsterid=怪物头像, retro=复古头像, identicon=图标头像
|
||||
* @returns 头像URL
|
||||
*/
|
||||
export function getCrAvatar(avatar: 'monsterid' | 'retro' | 'identicon', size?: number) {
|
||||
const id = `${Date.now()}+${Math.random()}`
|
||||
return `https://cravatar.cn/avatar/${id}?d=${avatar}&s=${size || 128}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机图片URL
|
||||
* @param type 图片类型 默认pc
|
||||
|
||||
Reference in New Issue
Block a user