Files
msh-system/msh_single_uniapp/pages/tool/post-detail.vue

1387 lines
30 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="post-detail-page">
<!-- 加载中状态 -->
<view class="loading-container" v-if="isLoading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y v-else>
<!-- 视频播放器区域优先显示 -->
<view class="video-player-section" v-if="postData.videoUrl">
<video
class="video-player"
:src="postData.videoUrl"
:poster="postData.images && postData.images.length > 0 ? postData.images[0] : ''"
controls
objectFit="contain"
show-center-play-btn
enable-play-gesture
></video>
<!-- 餐次标签 -->
<view class="meal-tag" v-if="postData.mealType">
<text class="meal-icon">{{ getMealIcon(postData.mealType) }}</text>
<text class="meal-text">{{ postData.mealType }}</text>
</view>
</view>
<!-- 图片轮播区域无视频时显示 -->
<view class="image-carousel" v-else-if="postData.images && postData.images.length > 0">
<swiper
class="swiper"
:indicator-dots="postData.images.length > 1"
:autoplay="false"
:current="currentImageIndex"
@change="onImageChange"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#ffffff"
>
<swiper-item v-for="(image, index) in postData.images" :key="index">
<image class="carousel-image" :src="image" mode="aspectFill" @click="previewImage(index)"></image>
</swiper-item>
</swiper>
<!-- 餐次标签 -->
<view class="meal-tag" v-if="postData.mealType">
<text class="meal-icon">{{ getMealIcon(postData.mealType) }}</text>
<text class="meal-text">{{ postData.mealType }}</text>
</view>
<!-- 图片页码 -->
<view class="image-counter" v-if="postData.images.length > 1">
{{ currentImageIndex + 1 }} / {{ postData.images.length }}
</view>
</view>
<!-- 帖子内容区域 -->
<view class="post-content-section">
<!-- 无图片时显示类型标签 -->
<view class="type-tag-row" v-if="!postData.images || postData.images.length === 0">
<view class="type-tag">{{ postData.mealType || '分享' }}</view>
</view>
<!-- 标题和分数 -->
<view class="title-row">
<view class="post-title">{{ postData.title }}</view>
<view class="score-badge" v-if="postData.score > 0">
<text>{{ postData.score }}分</text>
</view>
</view>
<!-- 帖子描述 -->
<view class="post-description" v-if="postData.description">
<text>{{ postData.description }}</text>
</view>
<!-- 标签列表 -->
<view class="tag-list" v-if="postData.tags && postData.tags.length > 0">
<view
class="tag-item"
v-for="(tag, index) in postData.tags"
:key="index"
>
{{ tag }}
</view>
</view>
<!-- 作者信息 -->
<view class="author-section">
<view class="author-info">
<view class="author-avatar">
<image
v-if="postData.author.avatar && postData.author.avatar.startsWith('http')"
class="avatar-img"
:src="postData.author.avatar"
mode="aspectFill"
></image>
<text v-else class="avatar-icon">👤</text>
</view>
<view class="author-details">
<view class="author-name">{{ postData.author.name }}</view>
<view class="post-time">{{ postData.author.time }}</view>
</view>
</view>
<view
class="follow-btn"
:class="{ followed: isFollowed }"
@click="toggleFollow"
>
<text>{{ isFollowed ? '已关注' : '+ 关注' }}</text>
</view>
</view>
</view>
<!-- 营养统计卡片 -->
<view class="nutrition-stats-card" v-if="postData.nutritionStats && postData.nutritionStats.length > 0">
<view class="stats-header">
<view class="stats-title">
<text class="title-icon">📊</text>
<text class="title-text">营养统计</text>
</view>
<view class="stats-unit">
<text>基于100g</text>
</view>
</view>
<view class="stats-grid">
<view class="stat-item" v-for="(stat, index) in postData.nutritionStats" :key="index">
<view class="stat-value">{{ stat.value }}</view>
<view class="stat-label">{{ stat.label }}</view>
</view>
</view>
</view>
<!-- AI营养师点评 -->
<view class="ai-comment-card" v-if="postData.aiComment">
<view class="ai-header">
<view class="ai-avatar">AI</view>
<view class="ai-info">
<view class="ai-name-row">
<text class="ai-name">营养师点评</text>
<text class="ai-verified">已认证</text>
</view>
<view class="ai-comment-text">
{{ postData.aiComment }}
</view>
</view>
</view>
</view>
<!-- 一键借鉴打卡按钮 -->
<view class="copy-checkin-btn" @click="handleCopyCheckin">
<text class="btn-icon">🎬</text>
<text class="btn-text">一键借鉴打卡</text>
</view>
<!-- 分隔线 -->
<view class="divider"></view>
<!-- 评论区域 -->
<view class="comments-section">
<view class="comments-header">
<text class="comments-icon">💬</text>
<text class="comments-title">全部评论 {{ postData.comments.length }}</text>
</view>
<view class="comments-list">
<view
class="comment-item"
v-for="(comment, index) in postData.comments"
:key="index"
>
<view class="comment-avatar">
<text>{{ comment.avatar }}</text>
</view>
<view class="comment-content">
<view class="comment-header">
<text class="comment-name">{{ comment.name }}</text>
<text class="comment-time">{{ comment.time }}</text>
</view>
<view class="comment-text">{{ comment.text }}</view>
<view class="comment-actions">
<view class="like-btn" @click="toggleCommentLike(comment, index)">
<text class="like-icon">❤️</text>
<text class="like-count">{{ comment.likeCount }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 相关推荐 -->
<view class="related-section">
<view class="related-header">
<text class="related-icon">✨</text>
<text class="related-title">相关推荐</text>
</view>
<view class="related-list">
<view
class="related-item"
v-for="(item, index) in relatedPosts"
:key="index"
@click="goToRelatedPost(item)"
>
<image class="related-image" :src="item.image" mode="aspectFill"></image>
</view>
</view>
</view>
<!-- 底部安全距离 -->
<view class="safe-bottom"></view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="input-box" @click="focusInput">
<text class="input-placeholder">说点什么...</text>
</view>
<view class="action-buttons">
<view class="action-btn-item" @click="toggleLike">
<text class="action-icon">❤️</text>
<text class="action-count">{{ postData.likeCount }}</text>
</view>
<view class="action-btn-item" @click="toggleFavorite">
<text class="action-icon">⭐</text>
<text class="action-count">{{ postData.favoriteCount }}</text>
</view>
<!-- <view class="action-btn-item" @click="handleShare">
<text class="action-icon">📤</text>
</view> -->
</view>
</view>
</view>
</template>
<script>
import {
getCommunityDetail,
toggleLike as apiToggleLike,
toggleCollect,
toggleFollow as apiToggleFollow,
getCommentList,
addComment,
getCommunityList
} from '@/api/tool.js'
import { checkLogin, toLogin } from '@/libs/login.js'
export default {
data() {
return {
postId: null,
currentImageIndex: 0,
isLoading: true,
isFollowed: false,
isLiked: false,
isCollected: false,
postData: {
id: null,
images: [],
mealType: '',
title: '',
score: 0,
description: '',
tags: [],
author: {
id: null,
avatar: '👤',
name: '',
time: ''
},
nutritionStats: [],
aiComment: '',
comments: [],
likeCount: 0,
favoriteCount: 0,
commentCount: 0,
videoUrl: null,
videoStatus: null,
hasVideo: false
},
relatedPosts: [],
commentPage: 1,
commentLimit: 10,
hasMoreComments: true,
isLoadingComments: false,
showCommentInput: false,
commentText: ''
}
},
onLoad(options) {
if (options.id) {
this.postId = options.id
this.loadPostData(options.id)
}
},
methods: {
// 加载帖子详情
async loadPostData(id) {
this.isLoading = true
try {
const res = await getCommunityDetail(id)
const data = res.data || res
// 格式化帖子数据
this.formatPostData(data)
// 同步状态
this.isLiked = data.isLiked || false
this.isCollected = data.isCollected || false
this.isFollowed = data.isFollowed || false
// 加载评论
this.loadComments()
// 加载相关推荐
this.loadRelatedPosts()
} catch (error) {
console.error('加载帖子详情失败:', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
this.isLoading = false
}
},
// 格式化帖子数据
formatPostData(data) {
// 解析图片
let images = []
if (data.imagesJson) {
try {
images = typeof data.imagesJson === 'string'
? JSON.parse(data.imagesJson)
: data.imagesJson
} catch (e) {
images = []
}
}
if (data.coverImage && !images.includes(data.coverImage)) {
images.unshift(data.coverImage)
}
// 解析标签
let tags = []
if (data.tagsJson) {
try {
const parsedTags = typeof data.tagsJson === 'string'
? JSON.parse(data.tagsJson)
: data.tagsJson
tags = parsedTags.map(tag => {
if (typeof tag === 'string') {
return tag.startsWith('#') ? tag : '#' + tag
}
return tag.name ? '#' + tag.name : ''
}).filter(t => t)
} catch (e) {
tags = []
}
}
// 如果没有标签,从内容中提取关键字
if (tags.length === 0 && data.content) {
const keywordMatch = data.content.match(/关键字[:]\s*([^<\n]+)/i)
if (keywordMatch && keywordMatch[1]) {
const keywords = keywordMatch[1].split(/[,,、]/).map(k => k.trim()).filter(k => k)
tags = keywords.slice(0, 4).map(k => '#' + k)
}
}
// 解析营养数据
let nutritionStats = []
if (data.nutritionDataJson) {
try {
const nutritionData = typeof data.nutritionDataJson === 'string'
? JSON.parse(data.nutritionDataJson)
: data.nutritionDataJson
nutritionStats = [
{ value: nutritionData.calories || '-', label: '热量(kcal)' },
{ value: nutritionData.protein ? nutritionData.protein + 'g' : '-', label: '蛋白质' },
{ value: nutritionData.potassium ? nutritionData.potassium + 'mg' : '-', label: '钾' },
{ value: nutritionData.phosphorus ? nutritionData.phosphorus + 'mg' : '-', label: '磷' }
]
} catch (e) {
nutritionStats = []
}
}
// 提取纯文本内容
let description = ''
if (data.content) {
description = this.stripHtml(data.content)
// 移除关键字和简介标记
description = description
.replace(/关键字[:][^简介]*/i, '')
.replace(/简介[:]/i, '')
.trim()
}
this.postData = {
id: data.id || data.postId,
images: images.length > 0 ? images : [],
mealType: data.mealType || '分享',
title: data.title || '',
score: data.score || 0,
description: description,
tags: tags,
author: {
id: data.userId,
avatar: data.userAvatar || '👤',
name: data.userName || '匿名用户',
time: this.formatTime(data.createdAt)
},
nutritionStats: nutritionStats,
aiComment: data.aiComment || '',
comments: [],
likeCount: data.likeCount || 0,
favoriteCount: data.collectCount || 0,
commentCount: data.commentCount || 0,
checkInRecordId: data.checkInRecordId,
videoUrl: data.videoUrl || null,
videoStatus: data.videoStatus,
hasVideo: data.hasVideo || false
}
},
// 移除HTML标签
stripHtml(html) {
if (!html) return ''
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<hr\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/\s+/g, ' ')
.trim()
},
// 格式化时间
formatTime(timeStr) {
if (!timeStr) return ''
const date = new Date(timeStr.replace(/-/g, '/'))
const now = new Date()
const diff = now - date
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
if (diff < minute) {
return '刚刚'
} else if (diff < hour) {
return Math.floor(diff / minute) + '分钟前'
} else if (diff < day) {
return Math.floor(diff / hour) + '小时前'
} else if (diff < 7 * day) {
return Math.floor(diff / day) + '天前'
} else {
const month = date.getMonth() + 1
const dayOfMonth = date.getDate()
return `${month}月${dayOfMonth}日`
}
},
// 加载评论列表
async loadComments() {
if (this.isLoadingComments || !this.hasMoreComments) return
this.isLoadingComments = true
try {
const res = await getCommentList(this.postId, {
page: this.commentPage,
limit: this.commentLimit
})
const list = res.data?.list || res.data || []
const formattedComments = list.map(item => ({
id: item.id,
avatar: item.userAvatar || '👤',
name: item.userName || '匿名用户',
time: this.formatTime(item.createdAt),
text: item.content,
likeCount: item.likeCount || 0,
isLiked: item.isLiked || false,
userId: item.userId
}))
if (this.commentPage === 1) {
this.postData.comments = formattedComments
} else {
this.postData.comments = [...this.postData.comments, ...formattedComments]
}
this.hasMoreComments = list.length >= this.commentLimit
} catch (error) {
console.error('加载评论失败:', error)
} finally {
this.isLoadingComments = false
}
},
// 加载相关推荐
async loadRelatedPosts() {
try {
const res = await getCommunityList({
tab: 'recommend',
page: 1,
limit: 4
})
const list = res.data?.list || res.data || []
this.relatedPosts = list
.filter(item => item.id !== this.postData.id)
.slice(0, 2)
.map(item => {
let coverImage = item.coverImage || ''
if (!coverImage && item.imagesJson) {
try {
const images = typeof item.imagesJson === 'string'
? JSON.parse(item.imagesJson)
: item.imagesJson
coverImage = images[0] || ''
} catch (e) {}
}
return {
id: item.id || item.postId,
image: coverImage,
title: item.title
}
})
} catch (error) {
console.error('加载相关推荐失败:', error)
}
},
onImageChange(e) {
this.currentImageIndex = e.detail.current
},
// 关注/取消关注
async toggleFollow() {
if (!checkLogin()) {
uni.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => toLogin(), 1000)
return
}
const newFollowState = !this.isFollowed
this.isFollowed = newFollowState
try {
await apiToggleFollow(this.postData.author.id, newFollowState)
uni.showToast({
title: newFollowState ? '已关注' : '取消关注',
icon: 'none'
})
} catch (error) {
this.isFollowed = !newFollowState
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
},
// 一键借鉴打卡
handleCopyCheckin() {
if (!checkLogin()) {
uni.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => toLogin(), 1000)
return
}
const recordId = this.postData.checkInRecordId || this.postData.id
uni.navigateTo({
url: `/pages/tool/checkin-publish?sourcePostId=${this.postData.id}&sourceType=learn`
})
},
// 评论点赞
async toggleCommentLike(comment, index) {
if (!checkLogin()) {
uni.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => toLogin(), 1000)
return
}
const newLikeState = !comment.isLiked
const newLikeCount = newLikeState ? comment.likeCount + 1 : comment.likeCount - 1
// 乐观更新UI
this.$set(this.postData.comments, index, {
...comment,
isLiked: newLikeState,
likeCount: newLikeCount
})
// 调用评论点赞API复用帖子点赞接口传入评论ID
try {
await apiToggleLike(comment.id, newLikeState)
} catch (error) {
// 回滚
this.$set(this.postData.comments, index, {
...comment,
isLiked: !newLikeState,
likeCount: comment.likeCount
})
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
},
// 帖子点赞
async toggleLike() {
if (!checkLogin()) {
uni.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => toLogin(), 1000)
return
}
const newLikeState = !this.isLiked
const newLikeCount = newLikeState
? this.postData.likeCount + 1
: this.postData.likeCount - 1
// 乐观更新
this.isLiked = newLikeState
this.postData.likeCount = newLikeCount
try {
await apiToggleLike(this.postId, newLikeState)
} catch (error) {
// 回滚
this.isLiked = !newLikeState
this.postData.likeCount = newLikeState
? newLikeCount - 1
: newLikeCount + 1
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
},
// 收藏
async toggleFavorite() {
if (!checkLogin()) {
uni.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => toLogin(), 1000)
return
}
const newCollectState = !this.isCollected
const newCollectCount = newCollectState
? this.postData.favoriteCount + 1
: this.postData.favoriteCount - 1
// 乐观更新
this.isCollected = newCollectState
this.postData.favoriteCount = newCollectCount
try {
await toggleCollect(this.postId, newCollectState)
uni.showToast({
title: newCollectState ? '已收藏' : '取消收藏',
icon: 'none'
})
} catch (error) {
// 回滚
this.isCollected = !newCollectState
this.postData.favoriteCount = newCollectState
? newCollectCount - 1
: newCollectCount + 1
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
},
handleShare() {
uni.showShareMenu({
withShareTicket: true
})
},
// 点击评论输入框
focusInput() {
if (!checkLogin()) {
uni.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => toLogin(), 1000)
return
}
// 显示评论输入弹窗
uni.showModal({
title: '发表评论',
editable: true,
placeholderText: '说点什么...',
success: async (res) => {
if (res.confirm && res.content) {
await this.submitComment(res.content)
}
}
})
},
// 提交评论
async submitComment(content) {
if (!content.trim()) {
uni.showToast({ title: '请输入评论内容', icon: 'none' })
return
}
uni.showLoading({ title: '发送中...' })
try {
await addComment({
postId: this.postId,
content: content.trim()
})
uni.hideLoading()
uni.showToast({ title: '评论成功', icon: 'success' })
// 刷新评论列表
this.commentPage = 1
this.hasMoreComments = true
this.loadComments()
// 更新评论数
this.postData.commentCount++
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '评论失败,请重试',
icon: 'none'
})
}
},
previewImage(index) {
if (this.postData.images && this.postData.images.length > 0) {
uni.previewImage({
current: index,
urls: this.postData.images
})
}
},
getMealIcon(type) {
const map = {
'早餐': '🌅',
'午餐': '☀️',
'晚餐': '🌙',
'加餐': '🍪',
'breakfast': '🌅',
'lunch': '☀️',
'dinner': '🌙',
'snack': '🍪'
}
return map[type] || '🍽️'
},
goToRelatedPost(item) {
uni.navigateTo({
url: `/pages/tool/post-detail?id=${item.id}`
})
},
// 格式化数量显示
formatCount(count) {
if (!count) return '0'
if (count >= 10000) {
return (count / 10000).toFixed(1) + 'w'
} else if (count >= 1000) {
return (count / 1000).toFixed(1) + 'k'
}
return String(count)
}
}
}
</script>
<style lang="scss" scoped>
.post-detail-page {
min-height: 100vh;
background: linear-gradient(180deg, #fafafa 0%, #ffffff 100%);
display: flex;
flex-direction: column;
}
/* 内容滚动区域 */
.content-scroll {
flex: 1;
margin-bottom: 120rpx;
}
/* 视频播放器区域 */
.video-player-section {
width: 100%;
height: 750rpx;
position: relative;
background: #000000;
.video-player {
width: 100%;
height: 100%;
}
.meal-tag {
position: absolute;
left: 32rpx;
top: 32rpx;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1rpx solid rgba(255, 255, 255, 0.8);
border-radius: 50rpx;
padding: 8rpx 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
z-index: 10;
.meal-icon {
font-size: 32rpx;
}
.meal-text {
font-size: 28rpx;
color: #3e5481;
}
}
}
/* 图片轮播区域 */
.image-carousel {
width: 100%;
height: 750rpx;
position: relative;
background: #f4f5f7;
.swiper {
width: 100%;
height: 100%;
}
.carousel-image {
width: 100%;
height: 100%;
}
.meal-tag {
position: absolute;
left: 32rpx;
top: 32rpx;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1rpx solid rgba(255, 255, 255, 0.8);
border-radius: 50rpx;
padding: 8rpx 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.meal-icon {
font-size: 32rpx;
}
.meal-text {
font-size: 28rpx;
color: #3e5481;
}
}
.image-counter {
position: absolute;
right: 32rpx;
bottom: 32rpx;
background: rgba(0, 0, 0, 0.7);
border-radius: 50rpx;
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
}
/* 帖子内容区域 */
.post-content-section {
padding: 32rpx;
background: #ffffff;
}
.title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24rpx;
margin-bottom: 24rpx;
.post-title {
flex: 1;
font-size: 36rpx;
color: #2e3e5c;
line-height: 1.4;
font-weight: 400;
}
.score-badge {
background: linear-gradient(135deg, #ff8c5a 0%, #ff6b35 50%, #e85a2a 100%);
border-radius: 50rpx;
padding: 8rpx 20rpx;
font-size: 28rpx;
color: #ffffff;
white-space: nowrap;
}
}
.post-description {
font-size: 28rpx;
color: #3e5481;
line-height: 1.6;
margin-bottom: 32rpx;
white-space: pre-wrap;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 32rpx;
}
.tag-item {
background: linear-gradient(135deg, #fff5f0 0%, #ffe8dc 100%);
border: 1rpx solid rgba(255, 136, 68, 0.3);
border-radius: 50rpx;
padding: 8rpx 24rpx;
font-size: 24rpx;
color: #ff7722;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
}
.author-section {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 32rpx;
border-top: 1rpx solid #d0dbea;
}
.author-info {
display: flex;
align-items: center;
gap: 24rpx;
}
.author-avatar {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8dc 100%);
border: 2rpx solid #ff7722;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.avatar-icon {
font-size: 36rpx;
}
}
.author-details {
display: flex;
flex-direction: column;
gap: 8rpx;
.author-name {
font-size: 28rpx;
color: #2e3e5c;
}
.post-time {
font-size: 24rpx;
color: #9fa5c0;
}
}
.follow-btn {
background: linear-gradient(135deg, #ff8c5a 0%, #ff6b35 100%);
border-radius: 50rpx;
padding: 16rpx 32rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.3);
text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
/* 营养统计卡片 */
.nutrition-stats-card {
margin: 10rpx 32rpx;
padding: 32rpx;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8dc 50%, #ffffff 100%);
border: 2rpx solid rgba(255, 136, 68, 0.3);
border-radius: 32rpx;
box-shadow: 0 16rpx 60rpx rgba(255, 136, 68, 0.15);
}
.stats-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.stats-title {
display: flex;
align-items: center;
gap: 16rpx;
.title-icon {
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #ff9966 0%, #ff8844 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
box-shadow: 0 8rpx 12rpx rgba(0, 0, 0, 0.1);
}
.title-text {
font-size: 28rpx;
color: #2e3e5c;
}
}
.stats-unit {
background: rgba(255, 255, 255, 0.9);
border: 1rpx solid rgba(255, 136, 68, 0.3);
border-radius: 50rpx;
padding: 8rpx 20rpx;
font-size: 24rpx;
color: #ff7722;
}
.stats-grid {
display: flex;
gap: 24rpx;
}
.stat-item {
flex: 1;
background: rgba(255, 255, 255, 0.8);
border: 1rpx solid rgba(255, 136, 68, 0.2);
border-radius: 24rpx;
padding: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
.stat-value {
font-size: 32rpx;
color: #2e3e5c;
font-weight: 500;
}
.stat-label {
font-size: 24rpx;
color: #9fa5c0;
text-align: center;
}
}
/* AI营养师点评 */
.ai-comment-card {
margin: 32rpx;
padding: 32rpx;
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
}
.ai-header {
display: flex;
gap: 24rpx;
}
.ai-avatar {
width: 72rpx;
height: 72rpx;
background: #ff6b35;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #ffffff;
flex-shrink: 0;
}
.ai-info {
flex: 1;
}
.ai-name-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
.ai-name {
font-size: 28rpx;
color: #2e3e5c;
}
.ai-verified {
font-size: 24rpx;
color: #9fa5c0;
}
}
.ai-comment-text {
font-size: 28rpx;
color: #3e5481;
line-height: 1.6;
}
/* 一键借鉴打卡按钮 */
.copy-checkin-btn {
margin: 22rpx;
height: 94rpx;
background: linear-gradient(135deg, #ff8844 0%, #ff6611 50%, #ff7722 100%);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
box-shadow: 0 16rpx 60rpx rgba(255, 107, 53, 0.3);
.btn-icon {
font-size: 36rpx;
}
.btn-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
/* 分隔线 */
.divider {
height: 16rpx;
background: #f4f5f7;
}
/* 评论区域 */
.comments-section {
padding: 32rpx;
background: #ffffff;
}
.comments-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
.comments-icon {
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #ff9966 0%, #ff8844 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
box-shadow: 0 8rpx 12rpx rgba(0, 0, 0, 0.1);
}
.comments-title {
font-size: 28rpx;
color: #2e3e5c;
}
}
.comments-list {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.comment-item {
display: flex;
gap: 24rpx;
}
.comment-avatar {
width: 72rpx;
height: 72rpx;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8dc 100%);
border: 1rpx solid rgba(255, 136, 68, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
flex-shrink: 0;
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
.comment-name {
font-size: 28rpx;
color: #2e3e5c;
}
.comment-time {
font-size: 24rpx;
color: #9fa5c0;
}
}
.comment-text {
font-size: 28rpx;
color: #3e5481;
line-height: 1.6;
margin-bottom: 16rpx;
}
.comment-actions {
display: flex;
align-items: center;
gap: 8rpx;
}
.like-btn {
display: flex;
align-items: center;
gap: 8rpx;
.like-icon {
font-size: 28rpx;
}
.like-count {
font-size: 24rpx;
color: #9fa5c0;
}
}
/* 相关推荐 */
.related-section {
padding: 32rpx;
background: linear-gradient(180deg, #fafafa 0%, #f4f5f7 100%);
}
.related-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
.related-icon {
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #ff8c5a 0%, #ff6b35 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
box-shadow: 0 8rpx 12rpx rgba(0, 0, 0, 0.1);
}
.related-title {
font-size: 28rpx;
color: #2e3e5c;
}
}
.related-list {
display: flex;
gap: 24rpx;
}
.related-item {
flex: 1;
height: 440rpx;
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 32rpx;
overflow: hidden;
.related-image {
width: 100%;
height: 100%;
}
}
/* 底部安全距离 */
.safe-bottom {
height: 40rpx;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
background: rgba(255, 255, 255, 0.95);
border-top: 1rpx solid rgba(0, 0, 0, 0.1);
padding: 0 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
z-index: 1000;
}
.input-box {
flex: 1;
height: 72rpx;
background: #f4f5f7;
border-radius: 50rpx;
padding: 0 32rpx;
display: flex;
align-items: center;
.input-placeholder {
font-size: 28rpx;
color: #9fa5c0;
}
}
.action-buttons {
display: flex;
gap: 24rpx;
align-items: center;
}
.action-btn-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
.action-icon {
font-size: 40rpx;
}
.action-count {
font-size: 24rpx;
color: #9fa5c0;
}
}
</style>