Files
sbnews/uniapp/src/packages_community/pages/post_detail.vue
T
2026-06-11 16:22:12 +08:00

1241 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="detail-page">
<view class="detail-topbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="detail-topbar__nav">
<view class="detail-topbar__back" @tap="goBack">
<u-icon name="arrow-left" size="38" color="#111827" />
</view>
<view class="detail-topbar__title-wrap">
<text class="detail-topbar__title">{{ pageTitle }}</text>
</view>
<view class="detail-topbar__placeholder" />
</view>
</view>
<scroll-view scroll-y class="detail-scroll">
<view v-if="loading" class="detail-loading">
<u-loading mode="circle" />
</view>
<view v-if="!loading && post" class="detail-content">
<view class="detail-header-area">
<view class="detail-header-area__top">
<text class="detail-header-area__label">{{ headerCategoryText }}</text>
<text class="detail-header-area__hint">{{ headerHintText }}</text>
</view>
<view class="detail-header detail-header--panel">
<image class="detail-header__avatar" :src="post.user?.avatar || defaultAvatar" mode="aspectFill"
@tap="goUserProfile(post.user_id)" />
<view class="detail-header__info">
<text class="detail-header__name">{{ post.user?.nickname || '匿名用户' }}</text>
<text class="detail-header__time">{{ formatTime(post.create_time) }}</text>
</view>
<view v-if="!isSelf" class="detail-header__actions">
<view class="detail-header__follow"
:class="{ 'detail-header__follow--active': post.is_friend || post.is_followed }"
@tap.stop="handleFollow">
<text>{{ post.is_friend || post.is_followed ? '已好友' : '加好友' }}</text>
</view>
<view v-if="post.is_friend || post.is_followed" class="detail-header__chat"
@tap.stop="handlePrivateChat">
<text>私聊</text>
</view>
</view>
</view>
</view>
<view class="detail-body">
<view v-for="(line, index) in contentLines" :key="`content-line-${index}`"
class="detail-body__line">
<text v-if="line.original" class="detail-body__text">{{ line.original }}</text>
<text v-if="line.translated" class="detail-body__translate">{{ line.translated }}</text>
</view>
</view>
<view v-if="post.images && post.images.length > 0" class="detail-images">
<view v-if="showExternalCover" class="detail-images__external" @tap="openSourceUrl">
<view class="detail-images__external-icon">
<text>T</text>
</view>
<view class="detail-images__external-body">
<text class="detail-images__external-title">Truth Social 图片暂不可直连</text>
<text class="detail-images__external-desc">点击跳转查看原帖图片</text>
</view>
<view class="detail-images__external-btn">
<text>查看原帖</text>
</view>
</view>
<view class="detail-images__grid">
<image v-for="(img, idx) in visibleImages" :key="idx" class="detail-images__item" :src="img"
mode="aspectFill" @tap="!post.is_locked && previewImage(idx)" />
</view>
</view>
<view v-if="post.is_locked" class="detail-unlock-card" @tap="handlePurchase">
<view class="detail-unlock-card__text">
<text class="detail-unlock-card__price">{{ post.price_points }}积分解锁全文</text>
<text class="detail-unlock-card__count">已有{{ post.paid_count || 0 }}人解锁</text>
</view>
<view class="detail-unlock-card__btn" :class="{ 'detail-unlock-card__btn--disabled': purchasing }">
<text>{{ purchasing ? '解锁中' : '立即解锁' }}</text>
</view>
</view>
<view v-if="post.tags && post.tags.length > 0" class="detail-tags">
<text v-for="(t, i) in post.tags" :key="i" class="detail-tags__item">#{{ t }}</text>
</view>
<view class="detail-stats">
<view class="detail-stats__item" @tap="handleLike">
<u-icon name="thumb-up" size="36" :color="post.is_liked ? '#185dff' : '#666'" />
<text :class="{ 'detail-stats__active': post.is_liked }">{{ post.like_count || 0 }}</text>
</view>
<view class="detail-stats__item">
<u-icon name="chat" size="36" color="#666" />
<text>{{ post.comment_count || 0 }}</text>
</view>
<view class="detail-stats__item">
<u-icon name="eye" size="36" color="#666" />
<text>{{ post.view_count || 0 }}</text>
</view>
<view class="detail-stats__item" @tap="handleShare">
<u-icon name="share" size="36" color="#666" />
<text>{{ post.share_count || '' }}</text>
</view>
<view v-if="isLiuhePost" class="detail-stats__item" @tap="handleAiAnalysisBtn">
<text class="detail-stats__ai">AI分析</text>
</view>
<view v-if="isSelf" class="detail-stats__item detail-stats__item--delete" @tap="handleDeletePost">
<text>删除</text>
</view>
</view>
<view class="detail-divider" />
<view class="detail-comments">
<view class="detail-comments__title">
<text>评论 ({{ commentTotal }})</text>
</view>
<view v-if="comments.length === 0 && !commentLoading" class="detail-comments__empty">
<text>暂无评论快来抢沙发</text>
</view>
<view v-for="c in comments" :key="c.id" class="comment-item">
<image class="comment-item__avatar" :src="c.user?.avatar || '/static/images/avatar.png'"
mode="aspectFill" />
<view class="comment-item__body">
<text class="comment-item__name">{{ c.user?.nickname || '匿名' }}</text>
<text class="comment-item__text">{{ c.content }}</text>
<view class="comment-item__meta">
<text class="comment-item__time">{{ formatTime(c.create_time) }}</text>
<view class="comment-item__like" @tap.stop="handleCommentLike(c)">
<u-icon name="thumb-up" size="24" :color="c.is_liked ? '#185dff' : '#999'" />
<text>{{ c.like_count || '' }}</text>
</view>
<text class="comment-item__reply-btn" @tap.stop="openReply(c)">回复</text>
<text v-if="isOwnComment(c)" class="comment-item__delete-btn"
@tap.stop="handleDeleteComment(c)">删除</text>
</view>
<view v-if="c.replies && c.replies.length > 0" class="comment-replies">
<view v-for="r in c.replies" :key="r.id" class="comment-reply">
<text class="comment-reply__name">{{ r.user?.nickname || '匿名' }}</text>
<text v-if="r.reply_user" class="comment-reply__arrow"> 回复 </text>
<text v-if="r.reply_user" class="comment-reply__name">{{ r.reply_user.nickname
}}</text>
<text class="comment-reply__text">{{ r.content }}</text>
<text v-if="isOwnComment(r)" class="comment-reply__delete-btn"
@tap.stop="handleDeleteComment(r)"> 删除</text>
</view>
<text v-if="c.reply_count > 3" class="comment-replies__more">查看全部{{ c.reply_count
}}条回复</text>
</view>
</view>
</view>
<view v-if="commentLoading" class="detail-comments__loading">
<u-loading mode="circle" size="28" />
</view>
</view>
</view>
</scroll-view>
<view class="detail-input-bar">
<input v-model="inputContent" class="detail-input-bar__input" :placeholder="inputPlaceholder"
:adjust-position="true" confirm-type="send" @confirm="submitComment" />
<view class="detail-input-bar__btn" @tap="submitComment">
<text>发送</text>
</view>
</view>
<share-popup v-if="showSharePopup" v-model:show="showSharePopup" page-type="post" :page-id="postId"
:path="`/packages_community/pages/post_detail?id=${postId}`"
:title="post?.content?.substring(0, 50) || '来看看这篇帖子'" />
</view>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import SharePopup from '@/components/share-popup/share-popup.vue'
import {
getCommunityPostDetail,
getCommentLists,
addComment,
deleteComment,
deletePost,
toggleLike,
toggleFollow,
purchasePost,
translatePost
} from '@/api/community'
import { getToken } from '@/utils/auth'
import { useAppStore } from '@/stores/app'
import { useUserStore } from '@/stores/user'
import { openChat } from '@/api/chat'
const appStore = useAppStore()
const userStore = useUserStore()
const defaultAvatar = computed(() => {
const logo = appStore.config?.website?.shop_logo
if (!logo) return '/static/images/avatar.png'
if (logo.startsWith('http')) return logo
return (appStore.config?.domain || '') + logo
})
const checkLogin = () => {
if (!getToken()) {
uni.showModal({
title: '提示',
content: '该操作需要登录,是否前往登录?',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/login' })
}
}
})
return false
}
return true
}
const statusBarHeight = ref(0)
const post = ref<any>(null)
const loading = ref(true)
const postId = ref(0)
const comments = ref<any[]>([])
const commentTotal = ref(0)
const commentLoading = ref(false)
const commentPage = ref(1)
const translatedText = ref('')
const autoTranslateStarted = ref(false)
const inputContent = ref('')
const replyTarget = ref<any>(null)
const purchasing = ref(false)
const isSelf = computed(() => !!post.value?.is_self)
const LIUHE_TAGS = ['旧澳六合', '新澳六合']
const hasTag = (tags: any, target: string) => Array.isArray(tags) && tags.includes(target)
const hasAnyTag = (tags: any, targets: string[]) => Array.isArray(tags) && targets.some((tag) => tags.includes(tag))
const isTrumpPost = computed(
() => hasTag(post.value?.tags, '特朗普')
)
const isLiuhePost = computed(
() => hasAnyTag(post.value?.tags, LIUHE_TAGS)
)
const trumpSourceUrl = computed(() => {
if (post.value?.source_url) return post.value.source_url
const originId = post.value?.origin_id || ''
if (!originId.startsWith('truthsocial:')) return ''
const sourcePostId = originId.split(':')[1]
if (!sourcePostId) return ''
return `https://truthsocial.com/@realDonaldTrump/${sourcePostId}`
})
const showExternalCover = computed(() => isTrumpPost.value && !!trumpSourceUrl.value)
const visibleImages = computed(() => {
if (showExternalCover.value) return []
return Array.isArray(post.value?.images) ? post.value.images : []
})
const pageTitle = computed(() => {
const firstLine = contentLines.value.find((line) => line.original)?.original || ''
if (!firstLine) return '帖子详情'
return firstLine.length > 18 ? `${firstLine.slice(0, 18)}...` : firstLine
})
const headerCategoryText = computed(() => {
if (post.value?.is_paid) return '付费帖子'
if (showExternalCover.value) return '外链帖子'
if (visibleImages.value.length > 0) return '图文帖子'
return '社区帖子'
})
const headerHintText = computed(() => {
if (visibleImages.value.length > 0) {
return `${visibleImages.value.length}张图片`
}
if (post.value?.is_paid) {
return `${post.value.price_points || 0}积分解锁`
}
if (translatedText.value) {
return '双语展示'
}
return '正文详情'
})
const splitContentLines = (text: string) => {
const normalized = String(text || '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.trim()
if (!normalized) return []
const manualLines = normalized
.split('\n')
.map((line) => line.replace(/\s+/g, ' ').trim())
.filter(Boolean)
if (manualLines.length > 1) return manualLines
const sentenceLines = normalized
.replace(/\s+/g, ' ')
.match(/[^。!?.!?]+[。!?.!?]?/g)
?.map((line) => line.trim())
.filter(Boolean)
return sentenceLines?.length ? sentenceLines : [normalized]
}
const contentLines = computed(() => {
const originals = splitContentLines(post.value?.content || '')
const translations = splitContentLines(translatedText.value)
if (!translations.length) {
return originals.map((original) => ({ original, translated: '' }))
}
if (originals.length === translations.length) {
return originals.map((original, index) => ({
original,
translated: translations[index] || ''
}))
}
if (originals.length <= 1 || translations.length <= 1) {
return [{
original: originals.join(' '),
translated: translations.join(' ')
}]
}
return originals.map((original, index) => ({
original,
translated: translations[index] || ''
}))
})
const inputPlaceholder = computed(() => {
if (replyTarget.value) {
return `回复 ${replyTarget.value.user?.nickname || ''}`
}
return '写评论...'
})
const isOwnComment = (comment: any) => {
const currentUserId = Number(userStore.userInfo?.id || 0)
return !!comment?.is_self || (currentUserId > 0 && Number(comment?.user_id || 0) === currentUserId)
}
onMounted(() => {
const sysInfo = uni.getSystemInfoSync()
// #ifdef APP-PLUS || MP
statusBarHeight.value = sysInfo.statusBarHeight || 44
// #endif
})
onLoad((options) => {
const id = Number(options?.id || 0)
if (id > 0) {
postId.value = id
if (options?.show_ai === '1') {
const title = encodeURIComponent(String(options?.title || '帖子AI分析'))
uni.redirectTo({ url: `/pages/ai_analysis/ai_analysis?type=post&id=${id}&title=${title}` })
return
}
fetchDetail(id)
fetchComments()
} else {
uni.showToast({ title: '参数错误', icon: 'none' })
setTimeout(() => uni.navigateBack(), 1500)
}
})
const fetchDetail = async (id: number) => {
loading.value = true
try {
const res = await getCommunityPostDetail({ id })
post.value = res
autoTranslateStarted.value = false
translatedText.value = res?.translated_content || ''
const tags = Array.isArray(res?.tags) ? res.tags : []
const isTrump = hasTag(tags, '特朗普')
const isLiuhe = hasAnyTag(tags, LIUHE_TAGS)
if (isTrump && res?.content && !translatedText.value) {
autoTranslateStarted.value = true
await requestTranslate()
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
const fetchComments = async () => {
commentLoading.value = true
try {
const res = await getCommentLists({
post_id: postId.value,
page_no: commentPage.value,
page_size: 20
})
comments.value = res?.lists || []
commentTotal.value = res?.count || 0
// eslint-disable-next-line no-empty
} catch (e) {
} finally {
commentLoading.value = false
}
}
const submitComment = async () => {
if (!checkLogin()) return
const content = inputContent.value.trim()
if (!content) return
try {
const data: any = {
post_id: postId.value,
content
}
if (replyTarget.value) {
data.parent_id = replyTarget.value.id
data.reply_user_id = replyTarget.value.user_id
}
await addComment(data)
inputContent.value = ''
replyTarget.value = null
uni.showToast({ title: '评论成功', icon: 'success' })
fetchComments()
if (post.value) {
post.value.comment_count = (post.value.comment_count || 0) + 1
}
} catch (e) {
uni.showToast({ title: '评论失败', icon: 'none' })
}
}
const handleLike = async () => {
if (!checkLogin()) return
if (!post.value) return
try {
const res = await toggleLike({ target_id: postId.value, target_type: 1 })
post.value.is_liked = res?.is_liked
post.value.like_count += res?.is_liked ? 1 : -1
} catch (e) { }
}
const handleCommentLike = async (c: any) => {
if (!checkLogin()) return
try {
const res = await toggleLike({ target_id: c.id, target_type: 2 })
c.is_liked = res?.is_liked
c.like_count += res?.is_liked ? 1 : -1
} catch (e) { }
}
const handleFollow = async () => {
if (!checkLogin()) return
if (!post.value) return
try {
const res = await toggleFollow({ follow_user_id: post.value.user_id })
const followed = !!res?.is_followed
post.value.is_followed = followed
post.value.is_friend = followed
const relation = post.value.relationship || {}
relation.is_followed_by_me = followed
relation.is_mutual = followed && !!relation.is_following_me
relation.relationship_text = relation.is_mutual ? '互相关注' : '对方还未添加你为好友'
post.value.relationship = relation
uni.showToast({ title: followed ? '已加好友' : '已取消好友', icon: 'none' })
} catch (e) { }
}
const handlePrivateChat = async () => {
if (!checkLogin()) return
if (!post.value?.user_id) return
try {
const session = await openChat({ target_user_id: Number(post.value.user_id) })
if (session?.id) {
uni.navigateTo({ url: `/pages/private_chat/room?session_id=${session.id}` })
}
} catch (e) {
// 请求层会提示后端返回信息
}
}
const goUserProfile = (userId: number) => {
if (!userId) return
uni.navigateTo({ url: `/packages_community/pages/user_profile?user_id=${userId}` })
}
const goBack = () => {
uni.navigateBack()
}
const handlePurchase = () => {
if (!checkLogin()) return
if (purchasing.value) return
const price = post.value?.price_points || 0
uni.showModal({
title: '确认解锁',
content: `确定使用 ${price} 积分解锁该内容吗?`,
confirmText: '确认解锁',
confirmColor: '#185dff',
success: async (res) => {
if (!res.confirm) return
purchasing.value = true
try {
await purchasePost({ post_id: postId.value })
uni.showToast({ title: '解锁成功', icon: 'success' })
fetchDetail(postId.value)
} catch (e: any) {
// 框架已自动toast后端返回的msg(如"积分不足"),此处无需重复提示
} finally {
purchasing.value = false
}
}
})
}
const openReply = (c: any) => {
replyTarget.value = c
}
const handleDeleteComment = (comment: any) => {
if (!checkLogin()) return
uni.showModal({
title: '删除评论',
content: '确认删除这条评论?',
confirmText: '删除',
confirmColor: '#ee3835',
success: async (res) => {
if (!res.confirm) return
try {
const result = await deleteComment({ comment_id: comment.id })
const deletedIds: number[] = result?.deleted_ids || [comment.id]
comments.value = comments.value.filter((item) => {
if (deletedIds.includes(item.id)) return false
if (Array.isArray(item.replies)) {
item.replies = item.replies.filter((reply: any) => !deletedIds.includes(reply.id))
}
return true
})
commentTotal.value = Math.max(0, Number(commentTotal.value || 0) - deletedIds.length)
if (post.value) {
post.value.comment_count = Math.max(0, Number(post.value.comment_count || 0) - deletedIds.length)
}
uni.showToast({ title: '删除成功', icon: 'success' })
} catch (_e) {
// 请求层已自动提示错误信息
}
}
})
}
const handleDeletePost = () => {
if (!checkLogin()) return
uni.showModal({
title: '删除帖子',
content: '确认删除这条帖子?删除后无法恢复。',
confirmText: '删除',
confirmColor: '#ee3835',
success: async (res) => {
if (!res.confirm) return
try {
await deletePost({ post_id: postId.value })
uni.showToast({ title: '删除成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1000)
} catch (_e) {
// 请求层已自动提示错误信息
}
}
})
}
const showSharePopup = ref(false)
const handleShare = () => {
if (!post.value) return
showSharePopup.value = true
}
const previewImage = (idx: number) => {
if (!visibleImages.value.length) return
uni.previewImage({
current: idx,
urls: visibleImages.value
})
}
const openSourceUrl = () => {
if (!trumpSourceUrl.value) return
uni.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(trumpSourceUrl.value)}`
})
}
const requestTranslate = async () => {
if (!post.value?.id) return
try {
const res = await translatePost({ post_id: post.value.id })
translatedText.value = res?.translated_content || ''
} catch (e) {
translatedText.value = ''
}
}
const handleAiAnalysisBtn = () => {
if (!checkLogin()) return
if (!isLiuhePost.value) return
const title = encodeURIComponent(pageTitle.value || '帖子AI分析')
uni.navigateTo({ url: `/pages/ai_analysis/ai_analysis?type=post&id=${postId.value}&title=${title}` })
}
const formatTime = (ts: any) => {
if (!ts) return ''
let timestamp: number
if (typeof ts === 'string') {
timestamp = new Date(ts.replace(/-/g, '/')).getTime() / 1000
} else {
timestamp = ts
}
if (isNaN(timestamp)) return ''
const now = Date.now() / 1000
const diff = now - timestamp
if (diff < 60) return '刚刚'
if (diff < 3600) return Math.floor(diff / 60) + '分钟前'
if (diff < 86400) return Math.floor(diff / 3600) + '小时前'
if (diff < 2592000) return Math.floor(diff / 86400) + '天前'
const d = new Date(timestamp * 1000)
return `${d.getMonth() + 1}-${d.getDate()}`
}
</script>
<style lang="scss" scoped>
.detail-page {
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
}
.detail-topbar {
background: #fff;
border-bottom: 1rpx solid #eef1f6;
position: relative;
z-index: 10;
flex-shrink: 0;
&__nav {
height: 88rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
&__back,
&__placeholder {
width: 64rpx;
height: 64rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
&__back {
border-radius: 18rpx;
background: #f4f6fb;
}
&__title-wrap {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
}
&__title {
max-width: 100%;
font-size: 30rpx;
font-weight: 600;
color: #111827;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
}
.detail-scroll {
flex: 1;
min-height: 0;
background: #fff;
}
.detail-loading {
display: flex;
justify-content: center;
padding: 120rpx 0;
}
.detail-content {
background: #fff;
}
.detail-header-area {
margin: 0 24rpx;
padding: 22rpx 28rpx 20rpx;
border-radius: 20rpx;
background: #fff;
border: 1rpx solid #eef1f6;
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.04);
&__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 18rpx;
}
&__label {
font-size: 22rpx;
color: #185dff;
font-weight: 600;
}
&__hint {
font-size: 22rpx;
color: #98a2b3;
}
}
.detail-header {
display: flex;
align-items: center;
padding: 28rpx 32rpx 0;
&--hero {
padding: 0;
}
&--panel {
padding: 0;
}
&__avatar {
width: 84rpx;
height: 84rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
&__info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
min-width: 0;
}
&__name {
font-size: 28rpx;
font-weight: 600;
color: #000;
}
&__time {
font-size: 24rpx;
color: #666;
margin-top: 4rpx;
}
&__actions {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 12rpx;
}
&__follow,
&__chat {
padding: 8rpx 24rpx;
border-radius: 12rpx;
font-size: 24rpx;
white-space: nowrap;
text {
color: #fff;
}
}
&__follow {
background: #185dff;
&--active {
background: #e5edff;
text {
color: #185dff;
}
}
}
&__chat {
background: #111827;
}
}
.detail-body {
padding: 24rpx 32rpx;
&__line {
margin-bottom: 18rpx;
&:last-child {
margin-bottom: 0;
}
}
&__text {
display: block;
font-size: 28rpx;
color: #000;
line-height: 1.8;
word-break: break-word;
}
&__translate {
display: block;
padding-left: 4rpx;
font-size: 26rpx;
color: #3f4a5a;
line-height: 1.8;
word-break: break-word;
white-space: pre-wrap;
}
}
.detail-images {
padding: 0 32rpx 24rpx;
&__external {
display: flex;
align-items: center;
gap: 20rpx;
padding: 28rpx 24rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
margin-bottom: 16rpx;
}
&__external-icon {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.12);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text {
font-size: 44rpx;
color: #fff;
font-weight: 700;
}
}
&__external-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
&__external-title {
font-size: 28rpx;
color: #fff;
font-weight: 600;
}
&__external-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.72);
}
&__external-btn {
height: 64rpx;
padding: 0 24rpx;
border-radius: 999rpx;
background: #185dff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text {
font-size: 24rpx;
color: #fff;
font-weight: 600;
}
}
&__grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
&__item {
width: calc(33.33% - 8rpx);
height: 220rpx;
border-radius: 12rpx;
}
}
.detail-tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
padding: 0 32rpx 24rpx;
&__item {
font-size: 22rpx;
color: #185dff;
background: #e5edff;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
}
.detail-ai {
margin: 0 32rpx 24rpx;
background: #f8faff;
border-radius: 16rpx;
padding: 24rpx;
border: 2rpx solid #e5edff;
&__header {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 16rpx;
}
&__icon {
font-size: 32rpx;
}
&__title {
font-size: 28rpx;
font-weight: 600;
color: #185dff;
}
&__cursor {
font-size: 28rpx;
color: #185dff;
animation: detailAiCursorBlink 0.8s step-end infinite;
}
&__body {
line-height: 1.8;
}
&__text {
font-size: 26rpx;
color: #333;
word-break: break-word;
white-space: pre-wrap;
}
}
.detail-stats {
display: flex;
align-items: center;
gap: 48rpx;
padding: 20rpx 32rpx;
border-top: 1rpx solid #f5f5f5;
border-bottom: 1rpx solid #f5f5f5;
&__item {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #262626;
&--delete {
color: #ff4d4f;
font-weight: 500;
}
}
&__active {
color: #185dff;
}
&__ai {
font-size: 24rpx;
color: #185dff;
}
}
.detail-divider {
height: 16rpx;
background: #f5f5f5;
}
.detail-comments {
background: #fff;
padding: 24rpx 32rpx 48rpx;
&__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
}
&__empty {
text-align: center;
padding: 60rpx 0;
font-size: 26rpx;
color: #999;
}
&__loading {
display: flex;
justify-content: center;
padding: 24rpx 0;
}
}
.comment-item {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&__avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
flex-shrink: 0;
}
&__body {
flex: 1;
margin-left: 16rpx;
}
&__name {
font-size: 26rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
&__text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
display: block;
}
&__meta {
display: flex;
align-items: center;
gap: 32rpx;
margin-top: 12rpx;
}
&__time {
font-size: 22rpx;
color: #999;
}
&__like {
display: flex;
align-items: center;
gap: 6rpx;
font-size: 22rpx;
color: #999;
}
&__reply-btn {
font-size: 22rpx;
color: #2979ff;
}
&__delete-btn {
font-size: 22rpx;
color: #ee3835;
}
}
.comment-replies {
margin-top: 16rpx;
padding: 16rpx;
background: #f8f8f8;
border-radius: 8rpx;
&__more {
font-size: 22rpx;
color: #2979ff;
display: block;
margin-top: 12rpx;
}
}
.comment-reply {
font-size: 24rpx;
color: #333;
line-height: 1.8;
&__name {
color: #2979ff;
font-weight: 500;
}
&__arrow {
color: #999;
}
&__text {
color: #333;
}
&__delete-btn {
color: #ee3835;
}
}
.detail-unlock-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24rpx;
margin: 0 32rpx 24rpx;
padding: 32rpx 28rpx;
height: 330rpx;
background: #e5edff;
border-radius: 12rpx;
&__text {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
&__price {
font-size: 28rpx;
color: #185dff;
font-weight: 500;
}
&__count {
font-size: 24rpx;
color: #185dff;
opacity: 0.5;
}
&__btn {
height: 76rpx;
padding: 0 40rpx;
display: flex;
align-items: center;
justify-content: center;
background: #185dff;
border-radius: 64rpx;
white-space: nowrap;
text {
font-size: 28rpx;
color: #fff;
}
&--disabled {
opacity: 0.6;
}
}
}
.detail-input-bar {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
padding-bottom: calc(env(safe-area-inset-bottom) + 16rpx);
background: #fff;
border-top: 1rpx solid #f0f0f0;
gap: 16rpx;
flex-shrink: 0;
&__input {
flex: 1;
height: 68rpx;
background: #f5f5f5;
border-radius: 34rpx;
padding: 0 28rpx;
font-size: 28rpx;
}
&__btn {
padding: 10rpx 28rpx;
background: #185dff;
color: #fff;
border-radius: 34rpx;
font-size: 28rpx;
flex-shrink: 0;
}
}
@keyframes detailAiCursorBlink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>