Initial commit: MSH System\n\n- msh_single_uniapp: Vue 2 + UniApp 前端(微信小程序/H5/App/支付宝小程序)\n- msh_crmeb_22: Spring Boot 2.2 后端(C端API/管理端/业务逻辑)\n- models-integration: AI服务集成(Coze/KieAI/腾讯ASR)\n- docs: 产品文档与设计稿
This commit is contained in:
855
msh_single_uniapp/pages/tool_main/community.vue
Normal file
855
msh_single_uniapp/pages/tool_main/community.vue
Normal file
@@ -0,0 +1,855 @@
|
||||
<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>
|
||||
724
msh_single_uniapp/pages/tool_main/index.vue
Normal file
724
msh_single_uniapp/pages/tool_main/index.vue
Normal file
@@ -0,0 +1,724 @@
|
||||
<template>
|
||||
<view class="tool-page">
|
||||
<!-- 用户健康卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-card-content">
|
||||
<view class="user-avatar" @tap="goToMyProfile">
|
||||
<image class="avatar-img" :src='userInfo.avatar' v-if="userInfo.avatar && uid" mode="aspectFill"></image>
|
||||
<image v-else class="avatar-img" :src="urlDomain+'crmebimage/perset/staticImg/f.png'" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="user-name" v-if="userInfo && uid">
|
||||
{{userInfo.nickname}}
|
||||
<!-- <view class="vip-tag" v-if="userInfo.vip">
|
||||
<image :src="userInfo.vipIcon" mode="aspectFit" class="vip-icon"></image>
|
||||
<text class="vip-text">{{userInfo.vipName || ''}}</text>
|
||||
</view> -->
|
||||
</view>
|
||||
<view class="user-name" v-else @tap="openAuto">请点击登录</view>
|
||||
<view class="user-desc" :class="{ 'completed': userHealthStatus.hasProfile }">
|
||||
{{ userHealthStatus.profileStatus || '尚未完成健康档案' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="checkin-btn" @tap="handleCheckin">
|
||||
<text>打卡</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 四大功能入口 -->
|
||||
<view class="function-grid">
|
||||
<view class="function-item calculator" @tap="goToCalculator">
|
||||
<view class="function-content">
|
||||
<view class="function-text">
|
||||
<view class="function-title">食谱计算器</view>
|
||||
<view class="function-desc">个性化营养方案</view>
|
||||
</view>
|
||||
<view class="function-icon">
|
||||
<text class="icon">📊</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="function-item ai-nutritionist" @tap="goToAINutritionist">
|
||||
<view class="function-content">
|
||||
<view class="function-text">
|
||||
<view class="function-title">AI营养师</view>
|
||||
<view class="function-desc">智慧知肾健康</view>
|
||||
</view>
|
||||
<view class="function-icon">
|
||||
<text class="icon">💬</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="function-item food-encyclopedia" @tap="goToFoodEncyclopedia">
|
||||
<view class="function-content">
|
||||
<view class="function-text">
|
||||
<view class="function-title">食物百科</view>
|
||||
<view class="function-desc">营养成分查询</view>
|
||||
</view>
|
||||
<view class="function-icon">
|
||||
<text class="icon">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="function-item nutrition-knowledge" @tap="goToNutritionKnowledge">
|
||||
<view class="function-content">
|
||||
<view class="function-text">
|
||||
<view class="function-title">营养知识</view>
|
||||
<view class="function-desc">专业营养指导</view>
|
||||
</view>
|
||||
<view class="function-icon">
|
||||
<text class="icon">💡</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选食谱 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精选食谱</text>
|
||||
<view class="section-more" @tap="goToRecipeList">
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="recipe-list">
|
||||
<view class="recipe-item" v-for="(item, index) in recipeList" :key="index" @tap="goToRecipeDetail(item)">
|
||||
<view class="recipe-image">
|
||||
<image :src="item.coverImage" mode="aspectFill"></image>
|
||||
<view class="recipe-tag" :class="item.source === 'calculator' ? 'tag-mine' : 'tag-recommend'">
|
||||
{{ item.source === 'calculator' ? '我的配餐' : '推荐' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="recipe-info">
|
||||
<view class="recipe-title">{{ item.name }}</view>
|
||||
<view class="recipe-desc">{{ item.description || '' }}</view>
|
||||
<view class="recipe-meta">
|
||||
<view class="meta-item" v-if="item.totalProtein">
|
||||
<text class="meta-icon">🥩</text>
|
||||
<text class="meta-text">蛋白质 {{ item.totalProtein }}g</text>
|
||||
</view>
|
||||
<view class="meta-item" v-if="item.totalEnergy">
|
||||
<text class="meta-icon">🔥</text>
|
||||
<text class="meta-text">{{ item.totalEnergy }}kcal</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营养方案领取卡片 -->
|
||||
<view class="promotion-card" @tap="goToPromotion">
|
||||
<view class="promotion-content">
|
||||
<view class="promotion-text">
|
||||
<view class="promotion-title">慢生活营养专家</view>
|
||||
<view class="promotion-desc">专业个性化营养方案</view>
|
||||
</view>
|
||||
<view class="promotion-btn">
|
||||
<text>立即领取福利</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康知识 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">健康知识</text>
|
||||
<view class="section-more" @tap="goToKnowledgeList">
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="knowledge-list">
|
||||
<view class="knowledge-item" v-for="(item, index) in knowledgeList" :key="index" @tap="goToKnowledgeDetail(item)">
|
||||
<view class="knowledge-icon">
|
||||
<text>{{ item.icon }}</text>
|
||||
</view>
|
||||
<view class="knowledge-info">
|
||||
<view class="knowledge-title">{{ item.title }}</view>
|
||||
<view class="knowledge-desc">{{ item.desc }}</view>
|
||||
<view class="knowledge-meta">
|
||||
<text class="meta-text">{{ item.time }}</text>
|
||||
<text class="meta-dot">·</text>
|
||||
<text class="meta-text">{{ item.views }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<view class="safe-bottom"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getRecommendedRecipes,
|
||||
getRecommendedKnowledge,
|
||||
getUserHealthStatus
|
||||
} from '@/api/tool.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { toLogin, checkLogin } from '@/libs/login.js';
|
||||
import Cache from '@/utils/cache';
|
||||
import { BACK_URL } from '@/config/cache';
|
||||
|
||||
export default {
|
||||
name: 'ToolIndex',
|
||||
computed: {
|
||||
...mapGetters(['userInfo','uid','isLogin'])
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
urlDomain: this.$Cache.get("imgHost"),
|
||||
recipeList: [],
|
||||
knowledgeList: [],
|
||||
userHealthStatus: {
|
||||
hasProfile: false,
|
||||
profileStatus: '尚未完成健康档案'
|
||||
},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadData();
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadData().finally(() => {
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
// 打开授权
|
||||
openAuto() {
|
||||
Cache.set(BACK_URL, '')
|
||||
toLogin();
|
||||
},
|
||||
// 加载页面数据
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 并行加载数据
|
||||
const [recipesRes, knowledgeRes, healthRes] = await Promise.all([
|
||||
getRecommendedRecipes({ limit: 2 }).catch(() => ({ data: [] })),
|
||||
getRecommendedKnowledge({ limit: 2 }).catch(() => ({ data: [] })),
|
||||
getUserHealthStatus().catch(() => ({ data: { hasProfile: false, profileStatus: '尚未完成健康档案' } }))
|
||||
]);
|
||||
|
||||
this.recipeList = recipesRes.data?.list || recipesRes.data || [];
|
||||
this.knowledgeList = knowledgeRes.data?.list || knowledgeRes.data || [];
|
||||
this.userHealthStatus = healthRes.data || { hasProfile: false, profileStatus: '尚未完成健康档案' };
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
// 跳转到我的页面
|
||||
goToMyProfile() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/my-profile'
|
||||
})
|
||||
},
|
||||
// 跳转到打卡页面
|
||||
handleCheckin() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/checkin'
|
||||
})
|
||||
},
|
||||
// 跳转到食谱计算器
|
||||
goToCalculator() {
|
||||
if (!checkLogin()) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => toLogin(), 500);
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/calculator'
|
||||
})
|
||||
},
|
||||
// 跳转到AI营养师
|
||||
goToAINutritionist() {
|
||||
if (!checkLogin()) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => toLogin(), 500);
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/ai-nutritionist'
|
||||
})
|
||||
},
|
||||
// 跳转到食物百科
|
||||
goToFoodEncyclopedia() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/food-encyclopedia'
|
||||
})
|
||||
},
|
||||
// 跳转到营养知识
|
||||
goToNutritionKnowledge() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/nutrition-knowledge'
|
||||
})
|
||||
},
|
||||
// 跳转到食谱列表(功能开发中)
|
||||
goToRecipeList() {
|
||||
uni.showToast({
|
||||
title: '食谱列表功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
// 跳转到食谱详情
|
||||
goToRecipeDetail(item) {
|
||||
if (!item) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/tool/recipe-detail?id=${item.id || 1}`
|
||||
})
|
||||
},
|
||||
// 跳转到会员福利
|
||||
goToPromotion() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/welcome-gift'
|
||||
})
|
||||
},
|
||||
// 跳转到营养知识列表
|
||||
goToKnowledgeList() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/nutrition-knowledge'
|
||||
})
|
||||
},
|
||||
// 跳转到营养知识详情
|
||||
goToKnowledgeDetail(item) {
|
||||
if (!item) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/tool/nutrition-knowledge?id=${item.id || 1}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tool-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f4f5f7;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 用户健康卡片 */
|
||||
.user-card {
|
||||
margin: 32rpx 32rpx 0;
|
||||
height: 192rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff7a4a 50%, #d64820 100%);
|
||||
box-shadow: 0 16rpx 48rpx rgba(255, 107, 53, 0.25);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.user-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 5rpx solid #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
|
||||
.user-name {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.vip-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.vip-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
|
||||
.vip-text {
|
||||
font-size: 20rpx;
|
||||
color: #ffe157;
|
||||
}
|
||||
}
|
||||
|
||||
.user-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-btn {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.9);
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(255, 107, 53, 0.3);
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 四大功能入口 */
|
||||
.function-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
margin: 32rpx 32rpx 0;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
height: 168rpx;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.function-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 40rpx;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.function-text {
|
||||
flex: 1;
|
||||
|
||||
.function-title {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.function-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.function-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.icon {
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calculator {
|
||||
background: linear-gradient(135deg, #4ecdc4 0%, #44b8b0 100%);
|
||||
box-shadow: 0 16rpx 48rpx rgba(78, 205, 196, 0.25);
|
||||
}
|
||||
|
||||
.ai-nutritionist {
|
||||
background: linear-gradient(135deg, #5b9bf3 0%, #4a8ae8 100%);
|
||||
box-shadow: 0 16rpx 48rpx rgba(91, 155, 243, 0.25);
|
||||
}
|
||||
|
||||
.food-encyclopedia {
|
||||
background: linear-gradient(135deg, #ffb84d 0%, #ffa726 100%);
|
||||
box-shadow: 0 16rpx 48rpx rgba(255, 184, 77, 0.25);
|
||||
}
|
||||
|
||||
.nutrition-knowledge {
|
||||
background: linear-gradient(135deg, #ff6b7a 0%, #ff5252 100%);
|
||||
box-shadow: 0 16rpx 48rpx rgba(255, 107, 122, 0.25);
|
||||
}
|
||||
|
||||
/* 通用区块样式 */
|
||||
.section {
|
||||
margin: 32rpx 32rpx 0;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
color: #2e3e5c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 32rpx;
|
||||
color: #9fa5c0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 精选食谱 */
|
||||
.recipe-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.recipe-item {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -4rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.recipe-image {
|
||||
width: 192rpx;
|
||||
height: 192rpx;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1), 0 4rpx 8rpx -2rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.recipe-tag {
|
||||
position: absolute;
|
||||
left: 16rpx;
|
||||
top: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -4rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.tag-recommend {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8c5a 100%);
|
||||
}
|
||||
|
||||
&.tag-mine {
|
||||
background: linear-gradient(135deg, #4ecdc4 0%, #44b8b0 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recipe-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 4rpx 0;
|
||||
|
||||
.recipe-title {
|
||||
font-size: 28rpx;
|
||||
color: #2e3e5c;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.recipe-desc {
|
||||
font-size: 24rpx;
|
||||
color: #9fa5c0;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.recipe-meta {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
align-items: center;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.meta-icon {
|
||||
font-size: 24rpx;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 24rpx;
|
||||
color: #9fa5c0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 营养方案领取卡片 */
|
||||
.promotion-card {
|
||||
margin: 32rpx 32rpx 0;
|
||||
height: 192rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff7a4a 50%, #d64820 100%);
|
||||
box-shadow: 0 40rpx 50rpx -10rpx rgba(0, 0, 0, 0.1), 0 16rpx 20rpx -6rpx rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.promotion-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.promotion-text {
|
||||
.promotion-title {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.promotion-desc {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.promotion-btn {
|
||||
background: #ffffff;
|
||||
border: 2rpx solid rgba(255, 107, 53, 0.2);
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(255, 107, 53, 0.3);
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #ff6b35;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 健康知识 */
|
||||
.knowledge-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.knowledge-item {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -4rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.knowledge-icon {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #f4f5f7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
text {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.knowledge-title {
|
||||
font-size: 28rpx;
|
||||
color: #2e3e5c;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.knowledge-desc {
|
||||
font-size: 24rpx;
|
||||
color: #9fa5c0;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.knowledge-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
|
||||
.meta-text {
|
||||
font-size: 24rpx;
|
||||
color: #9fa5c0;
|
||||
}
|
||||
|
||||
.meta-dot {
|
||||
font-size: 24rpx;
|
||||
color: #d0dbea;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部安全距离 */
|
||||
.safe-bottom {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user