1241 lines
35 KiB
Vue
1241 lines
35 KiB
Vue
<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>
|