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:
2026-02-28 05:40:21 +08:00
commit 14d29d51c0
2182 changed files with 482509 additions and 0 deletions

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