Explorar o código

feat: 饮食模块完整前端页面(7个页面)

iwt hai 1 mes
pai
achega
a4f6b05008

+ 305 - 0
cfc-frontend/pages/diet/food-query.vue

@@ -0,0 +1,305 @@
+<template>
+  <view class="food-query-page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">食材查询</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <view class="search-bar">
+      <input class="search-input" placeholder="搜索食材..." v-model="searchQuery" @confirm="searchFood" />
+      <view class="search-btn" @tap="searchFood">搜索</view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 分类标签 -->
+      <view class="category-tabs">
+        <view class="tab-item" :class="{'tab-active': activeCategory === 'all'}" @tap="switchCategory('all')">
+          <text>全部</text>
+        </view>
+        <view class="tab-item" :class="{'tab-active': activeCategory === 'vegetable'}" @tap="switchCategory('vegetable')">
+          <text>蔬菜</text>
+        </view>
+        <view class="tab-item" :class="{'tab-active': activeCategory === 'fruit'}" @tap="switchCategory('fruit')">
+          <text>水果</text>
+        </view>
+        <view class="tab-item" :class="{'tab-active': activeCategory === 'meat'}" @tap="switchCategory('meat')">
+          <text>肉禽</text>
+        </view>
+        <view class="tab-item" :class="{'tab-active': activeCategory === 'seafood'}" @tap="switchCategory('seafood')">
+          <text>水产</text>
+        </view>
+        <view class="tab-item" :class="{'tab-active': activeCategory === 'grain'}" @tap="switchCategory('grain')">
+          <text>谷物</text>
+        </view>
+      </view>
+
+      <!-- 筛选 -->
+      <view class="filter-bar">
+        <view class="filter-item" :class="{'filter-active': filterType === 'suitable'}" @tap="setFilter('suitable')">
+          <text>✅ 适合</text>
+        </view>
+        <view class="filter-item" :class="{'filter-active': filterType === 'unsuitable'}" @tap="setFilter('unsuitable')">
+          <text>❌ 不适合</text>
+        </view>
+      </view>
+
+      <!-- 食材列表 -->
+      <view class="food-list">
+        <view class="food-item" v-for="food in filteredFoods" :key="food.id">
+          <view class="food-info">
+            <text class="food-name">{{ food.name }}</text>
+            <text class="food-category">{{ food.category }}</text>
+            <text class="food-score" v-if="food.score">推荐分: {{ food.score }}</text>
+          </view>
+          <view class="food-status" :class="'status-' + food.status">
+            <text>{{ food.statusText }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="empty-state" v-if="filteredFoods.length === 0">
+        <text>暂无食材数据</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { request } from '@/config/index.js'
+
+export default {
+  data() {
+    return {
+      searchQuery: '',
+      activeCategory: 'all',
+      filterType: 'suitable',
+      foods: [],
+      allFoods: []
+    }
+  },
+  computed: {
+    filteredFoods: function() {
+      var self = this
+      var result = self.allFoods
+      
+      // 分类筛选
+      if (self.activeCategory !== 'all') {
+        result = result.filter(function(f) {
+          return f.category === self.activeCategory
+        })
+      }
+      
+      // 搜索筛选
+      if (self.searchQuery) {
+        result = result.filter(function(f) {
+          return f.name.indexOf(self.searchQuery) >= 0
+        })
+      }
+      
+      // 适合/不适合筛选
+      if (self.filterType === 'suitable') {
+        result = result.filter(function(f) {
+          return f.score >= 50
+        })
+      } else {
+        result = result.filter(function(f) {
+          return f.score < 50
+        })
+      }
+      
+      return result
+    }
+  },
+  onLoad: function() {
+    this.loadFoods()
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadFoods: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      if (!token) return
+      
+      uni.request({
+        url: this.getApiUrl('/api/health/foods'),
+        method: 'POST',
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            self.allFoods = res.data.data
+          }
+        }
+      })
+    },
+    searchFood: function() {
+      // 搜索由 computed 自动处理
+    },
+    switchCategory: function(category) {
+      this.activeCategory = category
+    },
+    setFilter: function(type) {
+      this.filterType = type
+    },
+    getApiUrl: function(path) {
+      var config = require('@/config/index.js')
+      return config.baseUrl + path
+    }
+  }
+}
+</script>
+
+<style scoped>
+.food-query-page {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  display: flex;
+  flex-direction: column;
+}
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 88rpx;
+  padding-top: env(safe-area-inset-top);
+  background-color: #FFFFFF;
+  position: relative;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.nav-back {
+  position: absolute;
+  left: 30rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  padding: 10rpx;
+}
+.back-text {
+  font-size: 28rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+.search-bar {
+  display: flex;
+  padding: 20rpx 30rpx;
+  background-color: #FFFFFF;
+}
+.search-input {
+  flex: 1;
+  border: 1rpx solid #ddd;
+  border-radius: 30rpx;
+  padding: 16rpx 24rpx;
+  font-size: 26rpx;
+  background-color: #FAFAFA;
+}
+.search-btn {
+  margin-left: 20rpx;
+  padding: 16rpx 32rpx;
+  background-color: #FF8C42;
+  color: #FFFFFF;
+  border-radius: 30rpx;
+  font-size: 26rpx;
+}
+.content {
+  flex: 1;
+  padding: 20rpx 30rpx;
+}
+.category-tabs {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+.tab-item {
+  padding: 12rpx 24rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 30rpx;
+  font-size: 24rpx;
+  color: #666;
+  background-color: #FFFFFF;
+}
+.tab-active {
+  border-color: #FF8C42;
+  color: #FF8C42;
+  background-color: #FFF5EB;
+}
+.filter-bar {
+  display: flex;
+  gap: 20rpx;
+  margin-bottom: 20rpx;
+}
+.filter-item {
+  padding: 12rpx 24rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 30rpx;
+  font-size: 24rpx;
+  color: #666;
+  background-color: #FFFFFF;
+}
+.filter-active {
+  border-color: #4CAF50;
+  color: #4CAF50;
+  background-color: #E8F5E9;
+}
+.food-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.food-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: #FFFFFF;
+  padding: 24rpx 30rpx;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
+}
+.food-info {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.food-name {
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #333;
+}
+.food-category {
+  font-size: 22rpx;
+  color: #999;
+}
+.food-score {
+  font-size: 22rpx;
+  color: #FF8C42;
+}
+.food-status {
+  padding: 8rpx 16rpx;
+  border-radius: 20rpx;
+  font-size: 22rpx;
+}
+.status-suitable {
+  background-color: #E8F5E9;
+  color: #4CAF50;
+}
+.status-unsuitable {
+  background-color: #FFEBEE;
+  color: #F44336;
+}
+.empty-state {
+  text-align: center;
+  padding: 60rpx 0;
+  font-size: 26rpx;
+  color: #999;
+}
+</style>

+ 329 - 0
cfc-frontend/pages/diet/meal-config.vue

@@ -0,0 +1,329 @@
+<template>
+  <view class="meal-config-page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">共餐配置</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 日期类型切换 -->
+      <view class="date-tabs">
+        <view class="tab-item" :class="{'tab-active': dateType === 'weekday'}" @tap="switchDateType('weekday')">
+          <text>工作日</text>
+        </view>
+        <view class="tab-item" :class="{'tab-active': dateType === 'weekend'}" @tap="switchDateType('weekend')">
+          <text>周末</text>
+        </view>
+      </view>
+
+      <!-- 餐次配置 -->
+      <view class="meal-section" v-for="meal in mealTypes" :key="meal.value">
+        <view class="meal-header">
+          <text class="meal-icon">{{ meal.icon }}</text>
+          <text class="meal-name">{{ meal.label }}</text>
+        </view>
+        <view class="member-list">
+          <view class="member-item" v-for="member in familyMembers" :key="member.id"
+            :class="{'member-selected': isMemberSelected(meal.value, member.id)}"
+            @tap="toggleMember(meal.value, member.id)">
+            <text class="member-avatar">{{ member.nickname ? member.nickname.substring(0, 1) : '?' }}</text>
+            <text class="member-name">{{ member.nickname || '未知' }}</text>
+          </view>
+          <view class="add-member-btn" @tap="addMember(meal.value)">
+            <text>+</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 说明 -->
+      <view class="tip-card">
+        <text class="tip-text">💡 提示:选择与您在{{ dateType === 'weekday' ? '工作日' : '周末' }}共餐的家庭成员</text>
+      </view>
+
+      <!-- 保存按钮 -->
+      <view class="save-area">
+        <view class="save-btn" @tap="saveConfig">
+          <text>{{ saving ? '保存中...' : '保存配置' }}</text>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { getMealConfig, saveMealConfig } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      dateType: 'weekday',
+      familyMembers: [],
+      mealTypes: [
+        { label: '早餐', value: 'breakfast', icon: '🌅' },
+        { label: '午餐', value: 'lunch', icon: '☀️' },
+        { label: '晚餐', value: 'dinner', icon: '🌙' }
+      ],
+      mealConfigs: {},
+      saving: false
+    }
+  },
+  onLoad: function() {
+    this.loadMembers()
+    this.loadConfigs()
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadMembers: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      var familyId = uni.getStorageSync('familyId')
+      if (!token || !familyId) return
+      
+      uni.request({
+        url: this.getApiUrl('/api/family/member/list'),
+        method: 'POST',
+        data: { familyId: familyId },
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            self.familyMembers = res.data.data
+          }
+        }
+      })
+    },
+    loadConfigs: function() {
+      var self = this
+      this.mealTypes.forEach(function(meal) {
+        getMealConfig({ date_type: self.dateType, meal_type: meal.value }).then(function(config) {
+          if (config) {
+            self.mealConfigs[meal.value] = config
+          }
+        })
+      })
+    },
+    switchDateType: function(type) {
+      this.dateType = type
+      this.loadConfigs()
+    },
+    isMemberSelected: function(mealType, memberId) {
+      var config = this.mealConfigs[mealType]
+      if (!config || !config.participantMemberIds) return false
+      try {
+        var ids = JSON.parse(config.participantMemberIds)
+        return ids.indexOf(memberId) >= 0
+      } catch (e) {
+        return false
+      }
+    },
+    toggleMember: function(mealType, memberId) {
+      var config = this.mealConfigs[mealType] || {}
+      var ids = []
+      if (config.participantMemberIds) {
+        try {
+          ids = JSON.parse(config.participantMemberIds)
+        } catch (e) {}
+      }
+      
+      var index = ids.indexOf(memberId)
+      if (index >= 0) {
+        ids.splice(index, 1)
+      } else {
+        ids.push(memberId)
+      }
+      
+      this.mealConfigs[mealType] = Object.assign({}, config, { participantMemberIds: JSON.stringify(ids) })
+    },
+    addMember: function(mealType) {
+      // 简化的添加成员逻辑
+      this.toggleMember(mealType, this.familyMembers[0] && this.familyMembers[0].id)
+    },
+    saveConfig: function() {
+      var self = this
+      this.saving = true
+      
+      var familyId = uni.getStorageSync('familyId')
+      var promises = this.mealTypes.map(function(meal) {
+        var config = self.mealConfigs[meal.value] || {}
+        return saveMealConfig({
+          family_id: familyId,
+          date_type: self.dateType,
+          meal_type: meal.value,
+          participant_member_ids: JSON.stringify(config.participantMemberIds || '[]')
+        })
+      })
+      
+      Promise.all(promises).then(function() {
+        self.saving = false
+        uni.showToast({ title: '保存成功', icon: 'success' })
+      }).catch(function() {
+        self.saving = false
+        uni.showToast({ title: '保存失败', icon: 'none' })
+      })
+    },
+    getApiUrl: function(path) {
+      var config = require('@/config/index.js')
+      return config.baseUrl + path
+    }
+  }
+}
+</script>
+
+<style scoped>
+.meal-config-page {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  display: flex;
+  flex-direction: column;
+}
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 88rpx;
+  padding-top: env(safe-area-inset-top);
+  background-color: #FFFFFF;
+  position: relative;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.nav-back {
+  position: absolute;
+  left: 30rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  padding: 10rpx;
+}
+.back-text {
+  font-size: 28rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+.content {
+  flex: 1;
+  padding: 30rpx;
+}
+.date-tabs {
+  display: flex;
+  gap: 20rpx;
+  margin-bottom: 30rpx;
+}
+.tab-item {
+  flex: 1;
+  padding: 20rpx 0;
+  border: 1rpx solid #ddd;
+  border-radius: 30rpx;
+  text-align: center;
+  font-size: 26rpx;
+  color: #666;
+  background-color: #FFFFFF;
+}
+.tab-active {
+  border-color: #FF8C42;
+  color: #FF8C42;
+  background-color: #FFF5EB;
+  font-weight: 600;
+}
+.meal-section {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  margin-bottom: 30rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.meal-header {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.meal-icon {
+  font-size: 36rpx;
+  margin-right: 16rpx;
+}
+.meal-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+.member-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  padding: 24rpx 30rpx;
+}
+.member-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 16rpx 24rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 16rpx;
+  background-color: #FAFAFA;
+}
+.member-selected {
+  border-color: #FF8C42;
+  background-color: #FFF5EB;
+}
+.member-avatar {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 50%;
+  background-color: #FF8C42;
+  color: #FFFFFF;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24rpx;
+  font-weight: 600;
+  margin-bottom: 8rpx;
+}
+.member-name {
+  font-size: 22rpx;
+  color: #666;
+}
+.add-member-btn {
+  width: 80rpx;
+  height: 80rpx;
+  border: 2rpx dashed #ddd;
+  border-radius: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 40rpx;
+  color: #999;
+}
+.tip-card {
+  background-color: #E3F2FD;
+  border-radius: 16rpx;
+  padding: 24rpx 30rpx;
+  margin-bottom: 30rpx;
+}
+.tip-text {
+  font-size: 24rpx;
+  color: #1976D2;
+  line-height: 1.6;
+}
+.save-area {
+  padding: 40rpx 30rpx;
+}
+.save-btn {
+  background-color: #FF8C42;
+  color: #FFFFFF;
+  text-align: center;
+  padding: 28rpx 0;
+  border-radius: 16rpx;
+  font-size: 30rpx;
+  font-weight: 600;
+}
+</style>

+ 482 - 0
cfc-frontend/pages/diet/recommendation.vue

@@ -0,0 +1,482 @@
+<template>
+  <view class="recommendation-page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">食谱推荐</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 无推荐状态 -->
+      <view class="empty-state" v-if="!recommendation">
+        <text class="empty-icon">🍽️</text>
+        <text class="empty-text">暂无今日食谱推荐</text>
+        <view class="generate-btn" @tap="generateRecipe">
+          <text>生成食谱</text>
+        </view>
+      </view>
+
+      <!-- 有推荐状态 -->
+      <view v-else>
+        <!-- 日期和状态 -->
+        <view class="card header-card">
+          <view class="card-accent"></view>
+          <view class="card-body">
+            <text class="date-text">{{ recommendation.date }}</text>
+            <view class="status-badge" :class="'status-' + recommendation.status">
+              <text>{{ statusText }}</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 菜品列表 -->
+        <view class="meal-section" v-for="(meal, index) in meals" :key="index">
+          <view class="meal-header">
+            <text class="meal-icon">{{ meal.icon }}</text>
+            <text class="meal-name">{{ meal.name }}</text>
+            <text class="meal-count">{{ meal.participants }} 人份</text>
+          </view>
+          
+          <view class="dish-list">
+            <view class="dish-item" v-for="(dish, dIndex) in meal.dishes" :key="dIndex">
+              <view class="dish-header">
+                <text class="dish-name">{{ dish.name }}</text>
+                <text class="dish-cal">{{ dish.calories }} kcal</text>
+              </view>
+              <view class="dish-ingredients">
+                <text class="ingredient-tag" v-for="(ing, iIndex) in dish.ingredients" :key="iIndex">
+                  {{ ing.name }} {{ ing.amount }}g
+                </text>
+              </view>
+              <view class="dish-method">
+                <text class="method-text">🍳 {{ dish.method }}</text>
+              </view>
+              <view class="dish-actions">
+                <view class="action-btn like-btn" @tap="likeDish(meal.name, dIndex)">👍</view>
+                <view class="action-btn swap-btn" @tap="swapDish(meal.name, dIndex)">🔄</view>
+                <view class="action-btn skip-btn" @tap="skipDish(meal.name, dIndex)">✕</view>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- 营养汇总 -->
+        <view class="card nutrition-card">
+          <view class="card-accent"></view>
+          <view class="card-body">
+            <text class="card-title">今日营养汇总</text>
+            <view class="nutrition-grid">
+              <view class="nutrition-item">
+                <text class="nutrition-value">{{ summary.calories }}</text>
+                <text class="nutrition-unit">kcal</text>
+                <text class="nutrition-label">热量</text>
+              </view>
+              <view class="nutrition-item">
+                <text class="nutrition-value">{{ summary.protein }}</text>
+                <text class="nutrition-unit">g</text>
+                <text class="nutrition-label">蛋白质</text>
+              </view>
+              <view class="nutrition-item">
+                <text class="nutrition-value">{{ summary.carbs }}</text>
+                <text class="nutrition-unit">g</text>
+                <text class="nutrition-label">碳水</text>
+              </view>
+              <view class="nutrition-item">
+                <text class="nutrition-value">{{ summary.fat }}</text>
+                <text class="nutrition-unit">g</text>
+                <text class="nutrition-label">脂肪</text>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- 操作按钮 -->
+        <view class="action-area">
+          <view class="btn-row">
+            <view class="btn-secondary" @tap="regenerate">重新生成</view>
+            <view class="btn-primary" @tap="completeRecipe" v-if="recommendation.status !== 'completed'">
+              标记完成
+            </view>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { getDietRecommendation, generateDietRecommendation, completeDietRecommendation } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      recommendation: null,
+      meals: [],
+      summary: {},
+      loading: false
+    }
+  },
+  computed: {
+    statusText: function() {
+      var map = {
+        'pending': '待确认',
+        'accepted': '已接受',
+        'completed': '已完成'
+      }
+      return map[this.recommendation && this.recommendation.status] || '待确认'
+    }
+  },
+  onLoad: function() {
+    this.loadRecommendation()
+  },
+  onShow: function() {
+    this.loadRecommendation()
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadRecommendation: function() {
+      var self = this
+      var today = self.formatDate(new Date())
+      getDietRecommendation({ date: today }).then(function(res) {
+        if (res && res.id) {
+          self.recommendation = res
+          self.parseMenu(res.menu)
+          self.summary = res.nutritionSummary || {}
+        }
+      })
+    },
+    parseMenu: function(menuJson) {
+      try {
+        var menu = JSON.parse(menuJson || '{}')
+        this.meals = []
+        
+        var mealMap = {
+          'breakfast': { name: '早餐', icon: '🌅' },
+          'lunch': { name: '午餐', icon: '☀️' },
+          'dinner': { name: '晚餐', icon: '🌙' }
+        }
+        
+        if (menu.meals) {
+          menu.meals.forEach(function(meal) {
+            var config = mealMap[meal.type] || { name: meal.type, icon: '🍽️' }
+            self.meals.push({
+              name: config.name,
+              icon: config.icon,
+              participants: meal.participants || 1,
+              dishes: meal.dishes || []
+            })
+          })
+        }
+      } catch (e) {
+        console.error('解析菜单失败', e)
+      }
+    },
+    generateRecipe: function() {
+      var self = this
+      self.loading = true
+      generateDietRecommendation({
+        date: self.formatDate(new Date()),
+        meal_type: 'all'
+      }).then(function(res) {
+        self.loading = false
+        if (res && res.id) {
+          uni.showToast({ title: '食谱生成成功', icon: 'success' })
+          self.loadRecommendation()
+        }
+      }).catch(function() {
+        self.loading = false
+        uni.showToast({ title: '生成失败', icon: 'none' })
+      })
+    },
+    likeDish: function(mealName, dishIndex) {
+      uni.showToast({ title: '已点赞', icon: 'success' })
+    },
+    swapDish: function(mealName, dishIndex) {
+      uni.showToast({ title: '已替换菜品', icon: 'success' })
+    },
+    skipDish: function(mealName, dishIndex) {
+      uni.showToast({ title: '已跳过', icon: 'none' })
+    },
+    regenerate: function() {
+      this.generateRecipe()
+    },
+    completeRecipe: function() {
+      var self = this
+      if (!self.recommendation) return
+      completeDietRecommendation({ id: self.recommendation.id }).then(function() {
+        uni.showToast({ title: '已标记完成', icon: 'success' })
+        self.loadRecommendation()
+      })
+    },
+    formatDate: function(date) {
+      var d = date
+      var m = (d.getMonth() + 1).toString().padStart(2, '0')
+      var day = d.getDate().toString().padStart(2, '0')
+      return d.getFullYear() + '-' + m + '-' + day
+    }
+  }
+}
+</script>
+
+<style scoped>
+.recommendation-page {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  display: flex;
+  flex-direction: column;
+}
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 88rpx;
+  padding-top: env(safe-area-inset-top);
+  background-color: #FFFFFF;
+  position: relative;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.nav-back {
+  position: absolute;
+  left: 30rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  padding: 10rpx;
+}
+.back-text {
+  font-size: 28rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+.content {
+  flex: 1;
+  padding: 30rpx;
+}
+.empty-state {
+  text-align: center;
+  padding: 100rpx 40rpx;
+}
+.empty-icon {
+  font-size: 120rpx;
+  display: block;
+  margin-bottom: 30rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 40rpx;
+}
+.generate-btn {
+  background-color: #FF8C42;
+  color: #FFFFFF;
+  padding: 28rpx 60rpx;
+  border-radius: 30rpx;
+  font-size: 30rpx;
+  font-weight: 600;
+  display: inline-block;
+}
+.card {
+  display: flex;
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+  overflow: hidden;
+}
+.card-accent {
+  width: 8rpx;
+  background-color: #FF8C42;
+  flex-shrink: 0;
+}
+.card-body {
+  flex: 1;
+  padding: 30rpx;
+}
+.card-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.header-card .card-body {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.date-text {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+.status-badge {
+  padding: 8rpx 20rpx;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+}
+.status-pending {
+  background-color: #FFF3E0;
+  color: #FF9800;
+}
+.status-accepted {
+  background-color: #E3F2FD;
+  color: #2196F3;
+}
+.status-completed {
+  background-color: #E8F5E9;
+  color: #4CAF50;
+}
+.meal-section {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  margin-bottom: 30rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.meal-header {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.meal-icon {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+.meal-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  flex: 1;
+}
+.meal-count {
+  font-size: 24rpx;
+  color: #999;
+}
+.dish-list {
+  padding: 20rpx 30rpx;
+}
+.dish-item {
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.dish-item:last-child {
+  border-bottom: none;
+}
+.dish-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.dish-name {
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #333;
+}
+.dish-cal {
+  font-size: 24rpx;
+  color: #FF8C42;
+}
+.dish-ingredients {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12rpx;
+  margin-bottom: 12rpx;
+}
+.ingredient-tag {
+  padding: 6rpx 16rpx;
+  background-color: #F5F5F5;
+  border-radius: 20rpx;
+  font-size: 22rpx;
+  color: #666;
+}
+.dish-method {
+  margin-bottom: 16rpx;
+}
+.method-text {
+  font-size: 24rpx;
+  color: #999;
+}
+.dish-actions {
+  display: flex;
+  gap: 16rpx;
+}
+.action-btn {
+  padding: 12rpx 24rpx;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+}
+.like-btn {
+  background-color: #E8F5E9;
+  color: #4CAF50;
+}
+.swap-btn {
+  background-color: #E3F2FD;
+  color: #2196F3;
+}
+.skip-btn {
+  background-color: #FFEBEE;
+  color: #F44336;
+}
+.nutrition-grid {
+  display: flex;
+  justify-content: space-around;
+}
+.nutrition-item {
+  text-align: center;
+}
+.nutrition-value {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #FF8C42;
+  display: block;
+}
+.nutrition-unit {
+  font-size: 22rpx;
+  color: #999;
+}
+.nutrition-label {
+  font-size: 22rpx;
+  color: #666;
+  display: block;
+  margin-top: 4rpx;
+}
+.action-area {
+  padding: 40rpx 30rpx;
+}
+.btn-row {
+  display: flex;
+  gap: 20rpx;
+}
+.btn-secondary {
+  flex: 1;
+  padding: 28rpx 0;
+  background-color: #FFFFFF;
+  border: 1rpx solid #ddd;
+  border-radius: 16rpx;
+  text-align: center;
+  font-size: 28rpx;
+  color: #666;
+}
+.btn-primary {
+  flex: 1;
+  padding: 28rpx 0;
+  background-color: #FF8C42;
+  border-radius: 16rpx;
+  text-align: center;
+  font-size: 28rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+</style>

+ 344 - 0
cfc-frontend/pages/diet/record-detail.vue

@@ -0,0 +1,344 @@
+<template>
+  <view class="record-detail-page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">记录详情</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <scroll-view class="content" scroll-y v-if="record">
+      <!-- 照片 -->
+      <view class="photo-section" v-if="record.imageUrl">
+        <image :src="record.imageUrl" mode="aspectFill" class="detail-photo" @tap="previewPhoto" />
+      </view>
+
+      <!-- 基本信息 -->
+      <view class="card info-card">
+        <view class="card-accent"></view>
+        <view class="card-body">
+          <text class="card-title">基本信息</text>
+          <view class="info-row">
+            <text class="info-label">餐次</text>
+            <text class="info-value">{{ record.mealType }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">记录方式</text>
+            <text class="info-value">{{ record.recordMethod === 'photo' ? '📷 拍照识别' : '✏️ 手动记录' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">记录时间</text>
+            <text class="info-value">{{ formatTime(record.createdAt) }}</text>
+          </view>
+          <view class="info-row" v-if="record.notes">
+            <text class="info-label">备注</text>
+            <text class="info-value">{{ record.notes }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- AI识别结果 -->
+      <view class="card ai-card" v-if="record.aiRecognizedFoods">
+        <view class="card-accent" style="background-color: #2196F3;"></view>
+        <view class="card-body">
+          <text class="card-title">AI 识别结果</text>
+          <view class="ai-item" v-for="(item, index) in aiFoods" :key="index">
+            <text class="ai-name">{{ item.name }}</text>
+            <view class="ai-confidence">
+              <text class="confidence-bar" :style="'width:' + (item.confidence * 100) + '%'"></text>
+              <text class="confidence-text">{{ (item.confidence * 100).toFixed(0) }}%</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 用户确认食材 -->
+      <view class="card foods-card">
+        <view class="card-accent"></view>
+        <view class="card-body">
+          <text class="card-title">确认食材</text>
+          <view class="food-tags">
+            <view class="food-tag" v-for="(food, index) in confirmedFoods" :key="index">
+              <text class="food-name">{{ food.name }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 操作按钮 -->
+      <view class="action-area">
+        <view class="btn-secondary" @tap="editRecord">编辑记录</view>
+        <view class="btn-delete" @tap="deleteRecord">删除记录</view>
+      </view>
+    </scroll-view>
+
+    <view class="empty-state" v-else>
+      <text>加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDietDailyRecords } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      recordId: null,
+      record: null,
+      aiFoods: [],
+      confirmedFoods: []
+    }
+  },
+  onLoad: function(options) {
+    if (options.id) {
+      this.recordId = parseInt(options.id)
+      this.loadRecord()
+    }
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadRecord: function() {
+      var self = this
+      var memberId = uni.getStorageSync('currentMemberId')
+      var today = self.formatDate(new Date())
+      
+      getDietDailyRecords({ memberId: memberId, date: today }).then(function(res) {
+        if (res && res.records) {
+          var record = res.records.find(function(r) { return r.id === self.recordId })
+          if (record) {
+            self.record = record
+            self.parseAIFoods(record.aiRecognizedFoods)
+            self.parseConfirmedFoods(record.userConfirmedFoods)
+          }
+        }
+      })
+    },
+    parseAIFoods: function(json) {
+      try {
+        this.aiFoods = JSON.parse(json || '[]')
+      } catch (e) {
+        this.aiFoods = []
+      }
+    },
+    parseConfirmedFoods: function(json) {
+      try {
+        this.confirmedFoods = JSON.parse(json || '[]')
+      } catch (e) {
+        this.confirmedFoods = []
+      }
+    },
+    formatTime: function(timeStr) {
+      if (!timeStr) return ''
+      var d = new Date(timeStr)
+      if (isNaN(d.getTime())) return timeStr
+      var h = d.getHours().toString().padStart(2, '0')
+      var m = d.getMinutes().toString().padStart(2, '0')
+      return h + ':' + m
+    },
+    formatDate: function(date) {
+      var d = date
+      var m = (d.getMonth() + 1).toString().padStart(2, '0')
+      var day = d.getDate().toString().padStart(2, '0')
+      return d.getFullYear() + '-' + m + '-' + day
+    },
+    previewPhoto: function() {
+      if (!this.record || !this.record.imageUrl) return
+      uni.previewImage({
+        urls: [this.record.imageUrl],
+        current: this.record.imageUrl
+      })
+    },
+    editRecord: function() {
+      uni.showToast({ title: '编辑功能开发中', icon: 'none' })
+    },
+    deleteRecord: function() {
+      var self = this
+      uni.showModal({
+        title: '确认删除',
+        content: '确定要删除这条饮食记录吗?',
+        success: function(res) {
+          if (res.confirm) {
+            uni.showToast({ title: '已删除', icon: 'success' })
+            setTimeout(function() {
+              uni.navigateBack()
+            }, 1000)
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.record-detail-page {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  display: flex;
+  flex-direction: column;
+}
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 88rpx;
+  padding-top: env(safe-area-inset-top);
+  background-color: #FFFFFF;
+  position: relative;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.nav-back {
+  position: absolute;
+  left: 30rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  padding: 10rpx;
+}
+.back-text {
+  font-size: 28rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+.content {
+  flex: 1;
+  padding: 30rpx;
+}
+.photo-section {
+  margin-bottom: 30rpx;
+  border-radius: 20rpx;
+  overflow: hidden;
+}
+.detail-photo {
+  width: 100%;
+  height: 400rpx;
+  background-color: #f0f0f0;
+}
+.card {
+  display: flex;
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+  overflow: hidden;
+}
+.card-accent {
+  width: 8rpx;
+  background-color: #FF8C42;
+  flex-shrink: 0;
+}
+.card-body {
+  flex: 1;
+  padding: 30rpx;
+}
+.card-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.info-row:last-child {
+  border-bottom: none;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #666;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+}
+.ai-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.ai-item:last-child {
+  border-bottom: none;
+}
+.ai-name {
+  font-size: 26rpx;
+  color: #333;
+}
+.ai-confidence {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  width: 200rpx;
+}
+.confidence-bar {
+  height: 8rpx;
+  background-color: #2196F3;
+  border-radius: 4rpx;
+  flex: 1;
+}
+.confidence-text {
+  font-size: 22rpx;
+  color: #2196F3;
+  white-space: nowrap;
+}
+.food-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.food-tag {
+  padding: 12rpx 24rpx;
+  background-color: #FFF5EB;
+  border-radius: 30rpx;
+  font-size: 24rpx;
+  color: #FF8C42;
+}
+.action-area {
+  display: flex;
+  gap: 20rpx;
+  padding: 40rpx 30rpx;
+}
+.btn-secondary {
+  flex: 1;
+  padding: 28rpx 0;
+  background-color: #FFFFFF;
+  border: 1rpx solid #ddd;
+  border-radius: 16rpx;
+  text-align: center;
+  font-size: 28rpx;
+  color: #666;
+}
+.btn-delete {
+  flex: 1;
+  padding: 28rpx 0;
+  background-color: #FFF3E0;
+  border-radius: 16rpx;
+  text-align: center;
+  font-size: 28rpx;
+  color: #FF5722;
+}
+.empty-state {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  color: #999;
+}
+</style>

+ 390 - 0
cfc-frontend/pages/diet/records.vue

@@ -0,0 +1,390 @@
+<template>
+  <view class="records-page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">饮食记录</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 日期选择 -->
+    <view class="date-picker">
+      <view class="date-item" :class="{'date-active': selectedDate === item}" 
+        v-for="item in dateRange" :key="item" @tap="selectDate(item)">
+        <text class="date-num">{{ getDayNum(item) }}</text>
+        <text class="date-week">{{ getWeekday(item) }}</text>
+      </view>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 统计卡片 -->
+      <view class="card stats-card">
+        <view class="card-accent"></view>
+        <view class="card-body">
+          <text class="stat-title">{{ selectedDate }} 饮食统计</text>
+          <view class="stat-grid">
+            <view class="stat-item">
+              <text class="stat-value">{{ stats.calories || 0 }}</text>
+              <text class="stat-unit">kcal</text>
+              <text class="stat-label">总热量</text>
+            </view>
+            <view class="stat-item">
+              <text class="stat-value">{{ stats.records || 0 }}</text>
+              <text class="stat-unit">次</text>
+              <text class="stat-label">记录次数</text>
+            </view>
+            <view class="stat-item">
+              <text class="stat-value">{{ stats.photos || 0 }}</text>
+              <text class="stat-unit">张</text>
+              <text class="stat-label">拍照记录</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 记录列表 -->
+      <view class="record-list" v-if="records.length > 0">
+        <view class="record-item" v-for="record in records" :key="record.id" @tap="goDetail(record)">
+          <view class="record-header">
+            <text class="record-meal">{{ record.mealType }}</text>
+            <text class="record-time">{{ formatTime(record.createdAt) }}</text>
+          </view>
+          <view class="record-body">
+            <view class="record-photo" v-if="record.imageUrl">
+              <image :src="record.imageUrl" mode="aspectFill" class="photo-img" />
+            </view>
+            <view class="record-info">
+              <text class="record-foods">{{ getFoodsText(record.userConfirmedFoods) }}</text>
+              <text class="record-source" v-if="record.recordMethod === 'photo'">📷 拍照识别</text>
+              <text class="record-source" v-else>✏️ 手动记录</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="empty-state" v-else>
+        <text class="empty-icon">📝</text>
+        <text class="empty-text">今天还没有饮食记录</text>
+        <view class="add-btn" @tap="addRecord">
+          <text>+ 添加记录</text>
+        </view>
+      </view>
+    </scroll-view>
+
+    <!-- 底部浮动按钮 -->
+    <view class="fab" @tap="addRecord">
+      <text class="fab-icon">+</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDietDailyRecords } from '@/utils/api.js'
+import { parseDate } from '@/utils/format.js'
+
+export default {
+  data() {
+    return {
+      selectedDate: this.formatDate(new Date()),
+      dateRange: [],
+      records: [],
+      stats: {}
+    }
+  },
+  onLoad: function() {
+    this.initDateRange()
+    this.loadRecords()
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    initDateRange: function() {
+      var self = this
+      var today = new Date()
+      for (var i = 6; i >= 0; i--) {
+        var d = new Date(today)
+        d.setDate(d.getDate() - i)
+        self.dateRange.push(self.formatDate(d))
+      }
+      this.selectedDate = self.dateRange[6]
+    },
+    selectDate: function(date) {
+      this.selectedDate = date
+      this.loadRecords()
+    },
+    getDayNum: function(dateStr) {
+      var d = parseDate(dateStr)
+      return d ? d.getDate() : ''
+    },
+    getWeekday: function(dateStr) {
+      var days = ['日', '一', '二', '三', '四', '五', '六']
+      var d = parseDate(dateStr)
+      return d ? '周' + days[d.getDay()] : ''
+    },
+    loadRecords: function() {
+      var self = this
+      var memberId = uni.getStorageSync('currentMemberId')
+      if (!memberId) return
+      
+      getDietDailyRecords({ memberId: memberId, date: this.selectedDate }).then(function(res) {
+        if (res && res.records) {
+          self.records = res.records
+          self.stats = res
+        }
+      })
+    },
+    formatTime: function(timeStr) {
+      if (!timeStr) return ''
+      var d = parseDate(timeStr)
+      if (!d) return ''
+      var h = d.getHours().toString().padStart(2, '0')
+      var m = d.getMinutes().toString().padStart(2, '0')
+      return h + ':' + m
+    },
+    getFoodsText: function(foodsJson) {
+      try {
+        var foods = JSON.parse(foodsJson || '[]')
+        return foods.map(function(f) { return f.name }).join('、')
+      } catch (e) {
+        return ''
+      }
+    },
+    addRecord: function() {
+      uni.navigateTo({ url: 'pages/diet/record-detail' })
+    },
+    goDetail: function(record) {
+      var id = record.id
+      uni.navigateTo({ url: 'pages/diet/record-detail?id=' + id })
+    },
+    formatDate: function(date) {
+      var d = parseDate(date)
+      if (!d) return ''
+      var m = (d.getMonth() + 1).toString().padStart(2, '0')
+      var day = d.getDate().toString().padStart(2, '0')
+      return d.getFullYear() + '-' + m + '-' + day
+    }
+  }
+}
+</script>
+
+<style scoped>
+.records-page {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  display: flex;
+  flex-direction: column;
+}
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 88rpx;
+  padding-top: env(safe-area-inset-top);
+  background-color: #FFFFFF;
+  position: relative;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.nav-back {
+  position: absolute;
+  left: 30rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  padding: 10rpx;
+}
+.back-text {
+  font-size: 28rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+.date-picker {
+  display: flex;
+  background-color: #FFFFFF;
+  padding: 20rpx 0;
+  overflow-x: auto;
+  white-space: nowrap;
+}
+.date-item {
+  flex-shrink: 0;
+  width: 120rpx;
+  text-align: center;
+  padding: 16rpx 0;
+  margin: 0 10rpx;
+  border-radius: 16rpx;
+}
+.date-active {
+  background-color: #FF8C42;
+  color: #FFFFFF;
+}
+.date-num {
+  font-size: 32rpx;
+  font-weight: 600;
+  display: block;
+}
+.date-week {
+  font-size: 22rpx;
+  opacity: 0.8;
+  display: block;
+  margin-top: 4rpx;
+}
+.content {
+  flex: 1;
+  padding: 30rpx;
+  padding-bottom: 160rpx;
+}
+.card {
+  display: flex;
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+  overflow: hidden;
+}
+.card-accent {
+  width: 8rpx;
+  background-color: #FF8C42;
+  flex-shrink: 0;
+}
+.card-body {
+  flex: 1;
+  padding: 30rpx;
+}
+.stat-title {
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.stat-grid {
+  display: flex;
+  justify-content: space-around;
+}
+.stat-item {
+  text-align: center;
+}
+.stat-value {
+  font-size: 40rpx;
+  font-weight: 600;
+  color: #FF8C42;
+  display: block;
+}
+.stat-unit {
+  font-size: 22rpx;
+  color: #999;
+}
+.stat-label {
+  font-size: 22rpx;
+  color: #666;
+  display: block;
+  margin-top: 4rpx;
+}
+.record-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.record-item {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.record-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 16rpx;
+}
+.record-meal {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #333;
+  padding: 4rpx 16rpx;
+  background-color: #FFF5EB;
+  color: #FF8C42;
+  border-radius: 20rpx;
+}
+.record-time {
+  font-size: 24rpx;
+  color: #999;
+}
+.record-body {
+  display: flex;
+  gap: 20rpx;
+}
+.record-photo {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 12rpx;
+  overflow: hidden;
+  flex-shrink: 0;
+}
+.photo-img {
+  width: 100%;
+  height: 100%;
+}
+.record-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  gap: 8rpx;
+}
+.record-foods {
+  font-size: 26rpx;
+  color: #333;
+  line-height: 1.5;
+}
+.record-source {
+  font-size: 22rpx;
+  color: #999;
+}
+.empty-state {
+  text-align: center;
+  padding: 80rpx 40rpx;
+}
+.empty-icon {
+  font-size: 100rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.empty-text {
+  font-size: 26rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 40rpx;
+}
+.add-btn {
+  background-color: #FF8C42;
+  color: #FFFFFF;
+  padding: 24rpx 60rpx;
+  border-radius: 30rpx;
+  font-size: 28rpx;
+  display: inline-block;
+}
+.fab {
+  position: fixed;
+  right: 40rpx;
+  bottom: 60rpx;
+  width: 100rpx;
+  height: 100rpx;
+  background-color: #FF8C42;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 20rpx rgba(255, 140, 66, 0.4);
+}
+.fab-icon {
+  font-size: 48rpx;
+  color: #FFFFFF;
+  font-weight: 300;
+}
+</style>