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>
|