856 lines
18 KiB
Vue
856 lines
18 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="community-page">
|
|||
|
|
<!-- 顶部Tab导航 -->
|
|||
|
|
<view class="tab-nav">
|
|||
|
|
<view
|
|||
|
|
class="tab-item"
|
|||
|
|
:class="{ active: currentTab === 'recommend' }"
|
|||
|
|
@click="switchTab('recommend')"
|
|||
|
|
>
|
|||
|
|
<!-- <text class="tab-icon">📊</text> -->
|
|||
|
|
<text class="tab-text">推荐</text>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
class="tab-item"
|
|||
|
|
:class="{ active: currentTab === 'latest' }"
|
|||
|
|
@click="switchTab('latest')"
|
|||
|
|
>
|
|||
|
|
<!-- <text class="tab-icon">🕐</text> -->
|
|||
|
|
<text class="tab-text">最新</text>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
class="tab-item"
|
|||
|
|
:class="{ active: currentTab === 'follow' }"
|
|||
|
|
@click="switchTab('follow')"
|
|||
|
|
>
|
|||
|
|
<!-- <text class="tab-icon">👥</text> -->
|
|||
|
|
<text class="tab-text">关注</text>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
class="tab-item"
|
|||
|
|
:class="{ active: currentTab === 'hot' }"
|
|||
|
|
@click="switchTab('hot')"
|
|||
|
|
>
|
|||
|
|
<!-- <text class="tab-icon">🔥</text> -->
|
|||
|
|
<text class="tab-text">热门</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 加载中状态 -->
|
|||
|
|
<view class="loading-container" v-if="isLoading && postList.length === 0">
|
|||
|
|
<view class="loading-spinner"></view>
|
|||
|
|
<text class="loading-text">加载中...</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 空状态 -->
|
|||
|
|
<view class="empty-container" v-else-if="isEmpty">
|
|||
|
|
<text class="empty-icon">📭</text>
|
|||
|
|
<text class="empty-text">暂无内容</text>
|
|||
|
|
<text class="empty-hint" v-if="currentTab === 'follow'">关注更多用户,查看他们的打卡动态</text>
|
|||
|
|
<text class="empty-hint" v-else>快来发布第一条打卡动态吧</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 打卡卡片网格 -->
|
|||
|
|
<view class="post-grid" v-else>
|
|||
|
|
<view
|
|||
|
|
class="post-card"
|
|||
|
|
:class="{ 'no-image': !item.image }"
|
|||
|
|
v-for="(item, index) in postList"
|
|||
|
|
:key="item.id"
|
|||
|
|
@click="goToPostDetail(item)"
|
|||
|
|
>
|
|||
|
|
<!-- 图片区域(有图片时显示) -->
|
|||
|
|
<view class="post-image" v-if="item.image">
|
|||
|
|
<image :src="item.image" mode="aspectFill" lazy-load></image>
|
|||
|
|
<!-- 类型标签 -->
|
|||
|
|
<view class="meal-tag">{{ item.mealType }}</view>
|
|||
|
|
<!-- 视频标记 -->
|
|||
|
|
<view class="video-badge" v-if="item.hasVideo || item.videoUrl">
|
|||
|
|
<text class="badge-icon">🎬</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 内容区域 -->
|
|||
|
|
<view class="post-content">
|
|||
|
|
<!-- 无图片时显示类型标签 -->
|
|||
|
|
<view class="type-tag" v-if="!item.image">{{ item.mealType }}</view>
|
|||
|
|
|
|||
|
|
<!-- 标题 -->
|
|||
|
|
<view class="post-title">{{ item.title }}</view>
|
|||
|
|
|
|||
|
|
<!-- 内容摘要(无图片时显示更多内容) -->
|
|||
|
|
<view class="post-summary" v-if="item.content && !item.image">
|
|||
|
|
{{ item.content }}
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 话题标签 -->
|
|||
|
|
<view class="tag-list" v-if="item.tags && item.tags.length > 0">
|
|||
|
|
<view
|
|||
|
|
class="tag-item"
|
|||
|
|
v-for="(tag, tagIndex) in item.tags.slice(0, 2)"
|
|||
|
|
:key="tagIndex"
|
|||
|
|
>
|
|||
|
|
{{ formatTag(tag) }}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 用户信息和互动数据 -->
|
|||
|
|
<view class="post-footer">
|
|||
|
|
<view class="user-info">
|
|||
|
|
<image
|
|||
|
|
v-if="item.userIcon && item.userIcon.startsWith('http')"
|
|||
|
|
class="user-avatar"
|
|||
|
|
:src="item.userIcon"
|
|||
|
|
mode="aspectFill"
|
|||
|
|
></image>
|
|||
|
|
<text v-else class="user-icon">👤</text>
|
|||
|
|
<text class="user-name">{{ item.userName }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="interaction-info">
|
|||
|
|
<view class="like-info" @click.stop="toggleLike(item, index)">
|
|||
|
|
<text class="like-icon" :class="{ liked: item.isLiked }">{{ item.isLiked ? '❤️' : '🤍' }}</text>
|
|||
|
|
<text class="like-count">{{ formatCount(item.likeCount) }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 发布时间 -->
|
|||
|
|
<view class="post-time" v-if="item.createTimeText">
|
|||
|
|
<text>{{ item.createTimeText }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 加载更多 -->
|
|||
|
|
<view class="load-more" v-if="postList.length > 0">
|
|||
|
|
<view v-if="isLoadingMore" class="loading-more">
|
|||
|
|
<view class="loading-spinner small"></view>
|
|||
|
|
<text>加载中...</text>
|
|||
|
|
</view>
|
|||
|
|
<text v-else-if="hasMore" @click="loadMore">上拉加载更多</text>
|
|||
|
|
<text v-else class="no-more">- 没有更多了 -</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 底部安全距离 -->
|
|||
|
|
<view class="safe-bottom"></view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { getCommunityList, toggleLike as apiToggleLike } from '@/api/tool.js'
|
|||
|
|
import { checkLogin, toLogin } from '@/libs/login.js'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
currentTab: 'recommend',
|
|||
|
|
postList: [],
|
|||
|
|
page: 1,
|
|||
|
|
limit: 10,
|
|||
|
|
isLoading: false,
|
|||
|
|
isLoadingMore: false,
|
|||
|
|
hasMore: true,
|
|||
|
|
isEmpty: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onLoad() {
|
|||
|
|
// 页面加载时获取数据
|
|||
|
|
this.loadPostList()
|
|||
|
|
},
|
|||
|
|
onShow() {
|
|||
|
|
// 页面显示时刷新数据(从详情页返回可能有变化)
|
|||
|
|
if (this.postList.length > 0) {
|
|||
|
|
this.refreshList()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onPullDownRefresh() {
|
|||
|
|
// 下拉刷新
|
|||
|
|
this.refreshList()
|
|||
|
|
},
|
|||
|
|
onReachBottom() {
|
|||
|
|
// 触底加载更多
|
|||
|
|
this.loadMore()
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
// 切换Tab
|
|||
|
|
switchTab(tab) {
|
|||
|
|
if (this.currentTab === tab) return
|
|||
|
|
|
|||
|
|
// 关注Tab需要登录
|
|||
|
|
if (tab === 'follow' && !checkLogin()) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请先登录查看关注内容',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
setTimeout(() => {
|
|||
|
|
toLogin()
|
|||
|
|
}, 1000)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.currentTab = tab
|
|||
|
|
this.page = 1
|
|||
|
|
this.hasMore = true
|
|||
|
|
this.postList = []
|
|||
|
|
this.loadPostList()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 刷新列表
|
|||
|
|
async refreshList() {
|
|||
|
|
this.page = 1
|
|||
|
|
this.hasMore = true
|
|||
|
|
await this.loadPostList(true)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 加载帖子列表
|
|||
|
|
async loadPostList(isRefresh = false) {
|
|||
|
|
if (this.isLoading) return
|
|||
|
|
|
|||
|
|
this.isLoading = true
|
|||
|
|
if (!isRefresh) {
|
|||
|
|
this.isEmpty = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await getCommunityList({
|
|||
|
|
tab: this.currentTab,
|
|||
|
|
page: this.page,
|
|||
|
|
limit: this.limit
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const list = res.data?.list || res.data || []
|
|||
|
|
const formattedList = this.formatPostList(list)
|
|||
|
|
|
|||
|
|
if (isRefresh || this.page === 1) {
|
|||
|
|
this.postList = formattedList
|
|||
|
|
} else {
|
|||
|
|
this.postList = [...this.postList, ...formattedList]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 判断是否还有更多
|
|||
|
|
this.hasMore = list.length >= this.limit
|
|||
|
|
this.isEmpty = this.postList.length === 0
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载社区列表失败:', error)
|
|||
|
|
// 如果是首次加载失败,显示空状态
|
|||
|
|
if (this.page === 1) {
|
|||
|
|
this.isEmpty = true
|
|||
|
|
}
|
|||
|
|
uni.showToast({
|
|||
|
|
title: error.message || '加载失败,请重试',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
} finally {
|
|||
|
|
this.isLoading = false
|
|||
|
|
uni.stopPullDownRefresh()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 格式化帖子数据
|
|||
|
|
formatPostList(list) {
|
|||
|
|
return list.map(item => {
|
|||
|
|
// 解析图片JSON
|
|||
|
|
let images = []
|
|||
|
|
if (item.imagesJson) {
|
|||
|
|
try {
|
|||
|
|
images = typeof item.imagesJson === 'string'
|
|||
|
|
? JSON.parse(item.imagesJson)
|
|||
|
|
: item.imagesJson
|
|||
|
|
} catch (e) {
|
|||
|
|
images = []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析标签JSON
|
|||
|
|
let tags = []
|
|||
|
|
if (item.tagsJson) {
|
|||
|
|
try {
|
|||
|
|
tags = typeof item.tagsJson === 'string'
|
|||
|
|
? JSON.parse(item.tagsJson)
|
|||
|
|
: item.tagsJson
|
|||
|
|
} catch (e) {
|
|||
|
|
tags = []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有标签,尝试从content中提取关键字
|
|||
|
|
if (tags.length === 0 && item.content) {
|
|||
|
|
const keywordMatch = item.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, 3) // 最多取3个关键字
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从HTML内容中提取纯文本摘要
|
|||
|
|
let contentText = ''
|
|||
|
|
if (item.content) {
|
|||
|
|
contentText = this.stripHtml(item.content)
|
|||
|
|
// 移除关键字和简介标记
|
|||
|
|
contentText = contentText
|
|||
|
|
.replace(/关键字[::][^简介]*/i, '')
|
|||
|
|
.replace(/简介[::]/i, '')
|
|||
|
|
.trim()
|
|||
|
|
.substring(0, 120) // 截取前120字符
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取封面图片
|
|||
|
|
const coverImage = item.coverImage || (images.length > 0 ? images[0] : '')
|
|||
|
|
|
|||
|
|
// 判断内容类型(根据checkInRecordId判断是否为打卡记录)
|
|||
|
|
const isCheckin = !!item.checkInRecordId
|
|||
|
|
const mealType = isCheckin ? '打卡' : '分享'
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: item.id || item.postId,
|
|||
|
|
postId: item.postId || item.id,
|
|||
|
|
image: coverImage,
|
|||
|
|
images: images,
|
|||
|
|
mealType: item.mealType || mealType,
|
|||
|
|
title: item.title || '',
|
|||
|
|
content: contentText,
|
|||
|
|
tags: tags,
|
|||
|
|
userIcon: item.userAvatar || '',
|
|||
|
|
userName: item.userName || '匿名用户',
|
|||
|
|
userId: item.userId,
|
|||
|
|
likeCount: item.likeCount || 0,
|
|||
|
|
isLiked: item.isLiked || false,
|
|||
|
|
commentCount: item.commentCount || 0,
|
|||
|
|
collectCount: item.collectCount || 0,
|
|||
|
|
shareCount: item.shareCount || 0,
|
|||
|
|
viewCount: item.viewCount || 0,
|
|||
|
|
createdAt: item.createdAt,
|
|||
|
|
createTimeText: this.formatTime(item.createdAt),
|
|||
|
|
isCheckin: isCheckin,
|
|||
|
|
hasVideo: item.hasVideo || false,
|
|||
|
|
videoUrl: item.videoUrl || '',
|
|||
|
|
enableAIVideo: item.enableAIVideo || false
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 移除HTML标签并解析实体
|
|||
|
|
stripHtml(html) {
|
|||
|
|
if (!html) return ''
|
|||
|
|
return html
|
|||
|
|
.replace(/<br\s*\/?>/gi, ' ') // 换行转空格
|
|||
|
|
.replace(/<hr\s*\/?>/gi, ' ') // 分隔线转空格
|
|||
|
|
.replace(/<[^>]+>/g, '') // 移除HTML标签
|
|||
|
|
.replace(/ /g, ' ')
|
|||
|
|
.replace(/</g, '<')
|
|||
|
|
.replace(/>/g, '>')
|
|||
|
|
.replace(/&/g, '&')
|
|||
|
|
.replace(/"/g, '"')
|
|||
|
|
.replace(/'/g, "'")
|
|||
|
|
.replace(/\s+/g, ' ') // 合并多个空格
|
|||
|
|
.trim()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 格式化时间显示
|
|||
|
|
formatTime(timeStr) {
|
|||
|
|
if (!timeStr) return ''
|
|||
|
|
|
|||
|
|
const date = new Date(timeStr.replace(/-/g, '/')) // 兼容iOS
|
|||
|
|
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 loadMore() {
|
|||
|
|
if (!this.hasMore || this.isLoadingMore || this.isLoading) return
|
|||
|
|
|
|||
|
|
this.isLoadingMore = true
|
|||
|
|
this.page++
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await getCommunityList({
|
|||
|
|
tab: this.currentTab,
|
|||
|
|
page: this.page,
|
|||
|
|
limit: this.limit
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const list = res.data?.list || res.data || []
|
|||
|
|
const formattedList = this.formatPostList(list)
|
|||
|
|
|
|||
|
|
this.postList = [...this.postList, ...formattedList]
|
|||
|
|
this.hasMore = list.length >= this.limit
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载更多失败:', error)
|
|||
|
|
this.page-- // 失败时回退页码
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '加载失败,请重试',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
} finally {
|
|||
|
|
this.isLoadingMore = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 跳转到帖子详情
|
|||
|
|
goToPostDetail(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)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 格式化标签显示
|
|||
|
|
formatTag(tag) {
|
|||
|
|
if (!tag) return ''
|
|||
|
|
// 如果标签已经以#开头,直接返回
|
|||
|
|
if (typeof tag === 'string' && tag.startsWith('#')) {
|
|||
|
|
return tag
|
|||
|
|
}
|
|||
|
|
// 如果是对象,取name属性
|
|||
|
|
if (typeof tag === 'object' && tag.name) {
|
|||
|
|
return '#' + tag.name
|
|||
|
|
}
|
|||
|
|
return '#' + String(tag)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 点赞/取消点赞
|
|||
|
|
async toggleLike(item, index) {
|
|||
|
|
// 检查登录状态
|
|||
|
|
if (!checkLogin()) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请先登录后点赞',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
setTimeout(() => {
|
|||
|
|
toLogin()
|
|||
|
|
}, 1000)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const newLikeState = !item.isLiked
|
|||
|
|
const newLikeCount = newLikeState ? item.likeCount + 1 : item.likeCount - 1
|
|||
|
|
|
|||
|
|
// 乐观更新UI
|
|||
|
|
this.$set(this.postList, index, {
|
|||
|
|
...item,
|
|||
|
|
isLiked: newLikeState,
|
|||
|
|
likeCount: newLikeCount
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await apiToggleLike(item.id, newLikeState)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('点赞失败:', error)
|
|||
|
|
// 失败时回滚
|
|||
|
|
this.$set(this.postList, index, {
|
|||
|
|
...item,
|
|||
|
|
isLiked: !newLikeState,
|
|||
|
|
likeCount: item.likeCount
|
|||
|
|
})
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '操作失败,请重试',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.community-page {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
background-color: #f4f5f7;
|
|||
|
|
padding-bottom: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Tab导航 */
|
|||
|
|
.tab-nav {
|
|||
|
|
background: #ffffff;
|
|||
|
|
border-bottom: 1rpx solid #d0dbea;
|
|||
|
|
display: flex;
|
|||
|
|
padding: 0 32rpx;
|
|||
|
|
height: 88rpx;
|
|||
|
|
align-items: center;
|
|||
|
|
position: sticky;
|
|||
|
|
top: 0;
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab-item {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
height: 72rpx;
|
|||
|
|
position: relative;
|
|||
|
|
|
|||
|
|
.tab-icon {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
margin-bottom: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab-text {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.active {
|
|||
|
|
.tab-text {
|
|||
|
|
color: #ff6b35;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&::after {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
width: 100rpx;
|
|||
|
|
height: 4rpx;
|
|||
|
|
background: #ff6b35;
|
|||
|
|
border-radius: 2rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 打卡卡片网格 */
|
|||
|
|
.post-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 32rpx;
|
|||
|
|
padding: 32rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-card {
|
|||
|
|
background: #ffffff;
|
|||
|
|
border: 1rpx solid #d0dbea;
|
|||
|
|
border-radius: 24rpx;
|
|||
|
|
overflow: hidden;
|
|||
|
|
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
|||
|
|
|
|||
|
|
&.no-image {
|
|||
|
|
.post-content {
|
|||
|
|
padding-top: 32rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-title {
|
|||
|
|
-webkit-line-clamp: 3;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-image {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 436rpx;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
|
|||
|
|
image {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
object-fit: cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meal-tag {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 16rpx;
|
|||
|
|
top: 16rpx;
|
|||
|
|
background: #ff6b35;
|
|||
|
|
border-radius: 40rpx;
|
|||
|
|
padding: 4rpx 16rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #ffffff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.video-badge {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 16rpx;
|
|||
|
|
top: 16rpx;
|
|||
|
|
background: rgba(0, 0, 0, 0.7);
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
padding: 8rpx 12rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
backdrop-filter: blur(8rpx);
|
|||
|
|
|
|||
|
|
.badge-icon {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-tag {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 16rpx;
|
|||
|
|
top: 16rpx;
|
|||
|
|
background: #ff9800;
|
|||
|
|
border-radius: 40rpx;
|
|||
|
|
padding: 4rpx 16rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 4rpx;
|
|||
|
|
|
|||
|
|
.score-icon {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score-text {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #ffffff;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-content {
|
|||
|
|
padding: 24rpx;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.type-tag {
|
|||
|
|
display: inline-block;
|
|||
|
|
background: #ff6b35;
|
|||
|
|
border-radius: 40rpx;
|
|||
|
|
padding: 4rpx 16rpx;
|
|||
|
|
font-size: 22rpx;
|
|||
|
|
color: #ffffff;
|
|||
|
|
align-self: flex-start;
|
|||
|
|
margin-bottom: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-title {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #2e3e5c;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
font-weight: 400;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
display: -webkit-box;
|
|||
|
|
-webkit-line-clamp: 2;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-summary {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
display: -webkit-box;
|
|||
|
|
-webkit-line-clamp: 3;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tag-list {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tag-item {
|
|||
|
|
background: #fff5f0;
|
|||
|
|
border: 1rpx solid rgba(255, 107, 53, 0.3);
|
|||
|
|
border-radius: 40rpx;
|
|||
|
|
padding: 4rpx 16rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #ff6b35;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-footer {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
margin-top: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12rpx;
|
|||
|
|
flex: 1;
|
|||
|
|
overflow: hidden;
|
|||
|
|
|
|||
|
|
.user-avatar {
|
|||
|
|
width: 40rpx;
|
|||
|
|
height: 40rpx;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-icon {
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-name {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.interaction-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 16rpx;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.like-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8rpx;
|
|||
|
|
|
|||
|
|
.like-icon {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
|
|||
|
|
&.liked {
|
|||
|
|
animation: likeAnimation 0.3s;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.like-count {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.post-time {
|
|||
|
|
font-size: 22rpx;
|
|||
|
|
color: #c0c5d0;
|
|||
|
|
margin-top: 4rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes likeAnimation {
|
|||
|
|
0% {
|
|||
|
|
transform: scale(1);
|
|||
|
|
}
|
|||
|
|
50% {
|
|||
|
|
transform: scale(1.3);
|
|||
|
|
}
|
|||
|
|
100% {
|
|||
|
|
transform: scale(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 加载中状态 */
|
|||
|
|
.loading-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
padding: 100rpx 0;
|
|||
|
|
|
|||
|
|
.loading-text {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
margin-top: 20rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-spinner {
|
|||
|
|
width: 60rpx;
|
|||
|
|
height: 60rpx;
|
|||
|
|
border: 4rpx solid #f0f0f0;
|
|||
|
|
border-top-color: #ff6b35;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
animation: spin 0.8s linear infinite;
|
|||
|
|
|
|||
|
|
&.small {
|
|||
|
|
width: 32rpx;
|
|||
|
|
height: 32rpx;
|
|||
|
|
border-width: 3rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes spin {
|
|||
|
|
to {
|
|||
|
|
transform: rotate(360deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 空状态 */
|
|||
|
|
.empty-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
padding: 120rpx 60rpx;
|
|||
|
|
|
|||
|
|
.empty-icon {
|
|||
|
|
font-size: 100rpx;
|
|||
|
|
margin-bottom: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-text {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #2e3e5c;
|
|||
|
|
font-weight: 500;
|
|||
|
|
margin-bottom: 12rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-hint {
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 加载更多 */
|
|||
|
|
.load-more {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 32rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
|
|||
|
|
.loading-more {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 12rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.no-more {
|
|||
|
|
color: #d0dbea;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 底部安全距离 */
|
|||
|
|
.safe-bottom {
|
|||
|
|
height: 40rpx;
|
|||
|
|
}
|
|||
|
|
</style>
|