Files
msh-system/msh_single_uniapp/pages/tool_main/index.vue
scottpan 6f2dc27fbc chore: update pom.xml Lombok config and deploy settings
- Update Maven compiler plugin to support Lombok annotation processing
- Add deploy.conf for automated deployment
- Update backend models and controllers
- Update frontend pages and API
2026-03-04 12:21:29 +08:00

746 lines
17 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="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>
<!-- 四大功能入口根据系统配置 eb_system_config.field01=1 时显示 -->
<view class="function-grid" v-if="showFunctionEntries">
<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">
<image v-if="item.coverImage" class="knowledge-cover" :src="item.coverImage" mode="aspectFill"></image>
<text v-else>{{ item.icon || '💡' }}</text>
</view>
<view class="knowledge-info">
<view class="knowledge-title">{{ item.title }}</view>
<view class="knowledge-desc">{{ item.desc || item.summary || '' }}</view>
<view class="knowledge-meta">
<text class="meta-text">{{ item.time || '' }}</text>
<text class="meta-dot" v-if="item.time && item.views">·</text>
<text class="meta-text">{{ item.views != null ? item.views : '' }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 底部安全距离 -->
<view class="safe-bottom"></view>
</view>
</template>
<script>
import {
getRecommendedRecipes,
getRecommendedKnowledge,
getUserHealthStatus,
getHomeDisplayConfig
} 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: '尚未完成健康档案'
},
showFunctionEntries: false,
loading: false
}
},
onLoad() {
this.loadData();
},
onPullDownRefresh() {
this.loadData().finally(() => {
uni.stopPullDownRefresh();
});
},
methods: {
// 打开授权
openAuto() {
Cache.set(BACK_URL, '')
toLogin();
},
// 加载页面数据
async loadData() {
this.loading = true;
try {
// 并行加载数据含首页展示配置field01=1 时显示四大功能入口)
const [recipesRes, knowledgeRes, healthRes, displayConfigRes] = await Promise.all([
getRecommendedRecipes({ limit: 2 }).catch(() => ({ data: [] })),
getRecommendedKnowledge({ limit: 2 }).catch(() => ({ data: [] })),
getUserHealthStatus().catch(() => ({ data: { hasProfile: false, profileStatus: '尚未完成健康档案' } })),
getHomeDisplayConfig().catch(() => ({ data: { showFunctionEntries: false } }))
]);
this.recipeList = recipesRes.data?.list || recipesRes.data || [];
const rawKnowledge = knowledgeRes.data?.list || knowledgeRes.data || [];
this.knowledgeList = rawKnowledge.map(item => ({
...item,
desc: item.summary ?? item.desc ?? '',
icon: item.icon || '💡'
}));
this.userHealthStatus = healthRes.data || { hasProfile: false, profileStatus: '尚未完成健康档案' };
this.showFunctionEntries = !!(displayConfigRes.data && displayConfigRes.data.showFunctionEntries);
} catch (error) {
console.error('加载数据失败:', error);
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
this.loading = false;
}
},
// 跳转到我的页面
goToMyProfile() {
uni.navigateTo({
url: '/pages/tool/my-profile'
})
},
// 跳转到打卡页面
handleCheckin() {
if (!checkLogin()) {
uni.showToast({ title: '请先登录', icon: 'none' });
setTimeout(() => toLogin(), 500);
return;
}
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/knowledge-detail?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;
overflow: hidden;
.knowledge-cover {
width: 100%;
height: 100%;
}
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>