Explorar el Código

feat(fatloss-miniprogram): add all UI pages (weight/diet/exercise/ai-coach/profile/chart/query)

Sisyphus hace 3 días
padre
commit
4f57c11adf

+ 86 - 0
xiaofat-miniprogram/pages/diet/create.vue

@@ -0,0 +1,86 @@
+<template>
+  <view class="create-page">
+    <view class="form">
+      <view class="form-item">
+        <text class="label">食物名称</text>
+        <input class="input" v-model="form.foodName" placeholder="请输入食物名称" />
+      </view>
+      <view class="form-item">
+        <text class="label">热量(kcal)</text>
+        <input class="input" type="number" v-model="form.calories" placeholder="请输入热量" />
+      </view>
+      <view class="form-item">
+        <text class="label">餐次</text>
+        <picker :range="mealTypes" @change="onMealChange">
+          <view class="picker">{{ form.mealType }}</view>
+        </picker>
+      </view>
+      <view class="form-item">
+        <text class="label">进餐日期</text>
+        <picker mode="date" :value="form.eatDate" @change="onDateChange">
+          <view class="picker">{{ form.eatDate }}</view>
+        </picker>
+      </view>
+    </view>
+
+    <button class="submit-btn" @click="submit">保存</button>
+  </view>
+</template>
+
+<script>
+import { createDietRecord } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+export default {
+  data() {
+    return {
+      mealTypes: ['早餐', '午餐', '晚餐', '加餐'],
+      form: {
+        foodName: '',
+        calories: '',
+        mealType: '早餐',
+        eatDate: today()
+      }
+    }
+  },
+  methods: {
+    onMealChange(e) {
+      this.form.mealType = this.mealTypes[e.detail.value]
+    },
+    onDateChange(e) {
+      this.form.eatDate = e.detail.value
+    },
+    submit() {
+      if (!this.form.foodName) {
+        uni.showToast({ title: '请输入食物名称', icon: 'none' })
+        return
+      }
+      if (!this.form.calories) {
+        uni.showToast({ title: '请输入热量', icon: 'none' })
+        return
+      }
+      var data = {
+        foodName: this.form.foodName,
+        calories: parseInt(this.form.calories),
+        mealType: this.form.mealType,
+        eatDate: this.form.eatDate
+      }
+      createDietRecord(data).then(() => {
+        uni.showToast({ title: '保存成功', icon: 'success' })
+        setTimeout(() => {
+          uni.navigateBack()
+        }, 1000)
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.create-page { padding: 30rpx; }
+.form { background: #fff; border-radius: 16rpx; padding: 0 30rpx; margin-bottom: 60rpx; }
+.form-item { display: flex; align-items: center; padding: 30rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.label { width: 200rpx; color: #333; }
+.input { flex: 1; text-align: right; }
+.picker { flex: 1; text-align: right; color: #333; }
+.submit-btn { background: #F97316; color: #fff; border-radius: 40rpx; }
+</style>

+ 64 - 0
xiaofat-miniprogram/pages/diet/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <view class="diet-page">
+    <view class="header">
+      <text class="title">饮食记录</text>
+      <button class="add-btn" size="mini" @click="goCreate">+ 记录</button>
+    </view>
+
+    <view class="list">
+      <view class="item" v-for="item in list" :key="item.id">
+        <view class="info">
+          <text class="food">{{ item.foodName }}</text>
+          <text class="meta">{{ item.mealType }} · {{ format(item.eatDate) }}</text>
+        </view>
+        <view class="cal">{{ item.calories }} kcal</view>
+      </view>
+    </view>
+
+    <view class="empty" v-if="list.length === 0">
+      <text>暂无饮食记录</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDietList } from '../../utils/api.js'
+import { formatDate } from '../../utils/format.js'
+export default {
+  data() {
+    return {
+      list: []
+    }
+  },
+  onShow() {
+    this.loadList()
+  },
+  methods: {
+    loadList() {
+      getDietList().then(res => {
+        if (res.data) this.list = res.data
+      })
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/diet/create' })
+    },
+    format(date) {
+      return formatDate(date)
+    }
+  }
+}
+</script>
+
+<style>
+.diet-page { padding: 30rpx; }
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
+.title { font-size: 36rpx; font-weight: 600; }
+.add-btn { background: #F97316; color: #fff; border-radius: 40rpx; }
+.list { background: #fff; border-radius: 16rpx; overflow: hidden; }
+.item { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
+.info { display: flex; flex-direction: column; }
+.food { font-size: 30rpx; font-weight: 600; margin-bottom: 8rpx; }
+.meta { font-size: 24rpx; color: #666; }
+.cal { color: #F97316; font-weight: 600; }
+.empty { text-align: center; color: #999; padding: 60rpx; }
+</style>

+ 80 - 0
xiaofat-miniprogram/pages/exercise/create.vue

@@ -0,0 +1,80 @@
+<template>
+  <view class="create-page">
+    <view class="form">
+      <view class="form-item">
+        <text class="label">运动类型</text>
+        <input class="input" v-model="form.exerciseType" placeholder="如:跑步、瑜伽等" />
+      </view>
+      <view class="form-item">
+        <text class="label">时长(分钟)</text>
+        <input class="input" type="number" v-model="form.durationMin" placeholder="请输入时长" />
+      </view>
+      <view class="form-item">
+        <text class="label">消耗热量(kcal)</text>
+        <input class="input" type="number" v-model="form.caloriesBurned" placeholder="请输入消耗热量" />
+      </view>
+      <view class="form-item">
+        <text class="label">运动日期</text>
+        <picker mode="date" :value="form.exerciseDate" @change="onDateChange">
+          <view class="picker">{{ form.exerciseDate }}</view>
+        </picker>
+      </view>
+    </view>
+
+    <button class="submit-btn" @click="submit">保存</button>
+  </view>
+</template>
+
+<script>
+import { createExerciseRecord } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+export default {
+  data() {
+    return {
+      form: {
+        exerciseType: '',
+        durationMin: '',
+        caloriesBurned: '',
+        exerciseDate: today()
+      }
+    }
+  },
+  methods: {
+    onDateChange(e) {
+      this.form.exerciseDate = e.detail.value
+    },
+    submit() {
+      if (!this.form.exerciseType) {
+        uni.showToast({ title: '请输入运动类型', icon: 'none' })
+        return
+      }
+      if (!this.form.durationMin) {
+        uni.showToast({ title: '请输入时长', icon: 'none' })
+        return
+      }
+      var data = {
+        exerciseType: this.form.exerciseType,
+        durationMin: parseInt(this.form.durationMin),
+        caloriesBurned: this.form.caloriesBurned ? parseInt(this.form.caloriesBurned) : 0,
+        exerciseDate: this.form.exerciseDate
+      }
+      createExerciseRecord(data).then(() => {
+        uni.showToast({ title: '保存成功', icon: 'success' })
+        setTimeout(() => {
+          uni.navigateBack()
+        }, 1000)
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.create-page { padding: 30rpx; }
+.form { background: #fff; border-radius: 16rpx; padding: 0 30rpx; margin-bottom: 60rpx; }
+.form-item { display: flex; align-items: center; padding: 30rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.label { width: 220rpx; color: #333; }
+.input { flex: 1; text-align: right; }
+.picker { flex: 1; text-align: right; color: #333; }
+.submit-btn { background: #F97316; color: #fff; border-radius: 40rpx; }
+</style>

+ 64 - 0
xiaofat-miniprogram/pages/exercise/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <view class="exercise-page">
+    <view class="header">
+      <text class="title">运动记录</text>
+      <button class="add-btn" size="mini" @click="goCreate">+ 记录</button>
+    </view>
+
+    <view class="list">
+      <view class="item" v-for="item in list" :key="item.id">
+        <view class="info">
+          <text class="type">{{ item.exerciseType }}</text>
+          <text class="meta">{{ item.durationMin }}分钟 · {{ format(item.exerciseDate) }}</text>
+        </view>
+        <view class="cal">{{ item.caloriesBurned }} kcal</view>
+      </view>
+    </view>
+
+    <view class="empty" v-if="list.length === 0">
+      <text>暂无运动记录</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getExerciseList } from '../../utils/api.js'
+import { formatDate } from '../../utils/format.js'
+export default {
+  data() {
+    return {
+      list: []
+    }
+  },
+  onShow() {
+    this.loadList()
+  },
+  methods: {
+    loadList() {
+      getExerciseList().then(res => {
+        if (res.data) this.list = res.data
+      })
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/exercise/create' })
+    },
+    format(date) {
+      return formatDate(date)
+    }
+  }
+}
+</script>
+
+<style>
+.exercise-page { padding: 30rpx; }
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
+.title { font-size: 36rpx; font-weight: 600; }
+.add-btn { background: #F97316; color: #fff; border-radius: 40rpx; }
+.list { background: #fff; border-radius: 16rpx; overflow: hidden; }
+.item { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
+.info { display: flex; flex-direction: column; }
+.type { font-size: 30rpx; font-weight: 600; margin-bottom: 8rpx; }
+.meta { font-size: 24rpx; color: #666; }
+.cal { color: #F97316; font-weight: 600; }
+.empty { text-align: center; color: #999; padding: 60rpx; }
+</style>

+ 147 - 0
xiaofat-miniprogram/pages/index/index.vue

@@ -0,0 +1,147 @@
+<template>
+  <view class="index-page">
+    <view class="header">
+      <view class="greeting">
+        <text class="hi">Hi,</text>
+        <text class="nickname">{{ nickname || '健康达人' }}</text>
+      </view>
+    </view>
+
+    <view class="summary-card" v-if="activeGoal">
+      <view class="summary-item">
+        <text class="label">起始体重</text>
+        <text class="value">{{ activeGoal.startWeight }} kg</text>
+      </view>
+      <view class="summary-item">
+        <text class="label">目标体重</text>
+        <text class="value">{{ activeGoal.targetWeight }} kg</text>
+      </view>
+      <view class="summary-item">
+        <text class="label">已记录</text>
+        <text class="value">{{ weightCount }} 次</text>
+      </view>
+    </view>
+
+    <view class="empty-goal" v-else>
+      <text>还没有减脂目标</text>
+      <button class="set-goal-btn" size="mini" @click="goToProfile">去设置</button>
+    </view>
+
+    <view class="quick-actions">
+      <view class="action-item" @click="goWeight">
+        <text class="icon">📝</text>
+        <text class="text">体重记录</text>
+      </view>
+      <view class="action-item" @click="goDiet">
+        <text class="icon">🍽️</text>
+        <text class="text">饮食记录</text>
+      </view>
+      <view class="action-item" @click="goExercise">
+        <text class="icon">🏃</text>
+        <text class="text">运动记录</text>
+      </view>
+      <view class="action-item" @click="goAI">
+        <text class="icon">🤖</text>
+        <text class="text">AI教练</text>
+      </view>
+    </view>
+
+    <view class="today-stats">
+      <view class="stat-box">
+        <text class="stat-label">今日摄入</text>
+        <text class="stat-value">{{ todayCalories }} kcal</text>
+      </view>
+      <view class="stat-box">
+        <text class="stat-label">最新体重</text>
+        <text class="stat-value">{{ latestWeight }} kg</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getActiveGoal, getWeightList, getCaloriesSummary } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+export default {
+  data() {
+    return {
+      nickname: '',
+      activeGoal: null,
+      weightList: [],
+      todayCalories: 0,
+      latestWeight: '--'
+    }
+  },
+  computed: {
+    weightCount() {
+      return this.weightList ? this.weightList.length : 0
+    }
+  },
+  onLoad() {
+    var userInfo = uni.getStorageSync('userInfo')
+    this.nickname = userInfo && userInfo.nickname ? userInfo.nickname : ''
+  },
+  onShow() {
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      getActiveGoal().then(res => {
+        if (res.data) this.activeGoal = res.data
+      })
+      getWeightList().then(res => {
+        if (res.data && res.data.length > 0) {
+          this.weightList = res.data
+          var sorted = [...res.data].sort((a,b) => {
+            var ta = new Date(a.measureDate || a.createTime).getTime()
+            var tb = new Date(b.measureDate || b.createTime).getTime()
+            return tb - ta
+          })
+          this.latestWeight = sorted[0].weight
+        } else {
+          this.weightList = []
+        }
+      })
+      getCaloriesSummary(today()).then(res => {
+        if (res.data) this.todayCalories = res.data
+      })
+    },
+    goToProfile() {
+      uni.navigateTo({ url: '/pages/profile/index' })
+    },
+    goWeight() {
+      uni.switchTab({ url: '/pages/weight/index' })
+    },
+    goDiet() {
+      uni.switchTab({ url: '/pages/diet/index' })
+    },
+    goExercise() {
+      uni.switchTab({ url: '/pages/exercise/index' })
+    },
+    goAI() {
+      uni.switchTab({ url: '/pages/ai-coach/index' })
+    }
+  }
+}
+</script>
+
+<style>
+.index-page { padding: 30rpx; }
+.header { margin-bottom: 30rpx; }
+.greeting { font-size: 36rpx; font-weight: 600; }
+.nickname { color: #F97316; }
+.summary-card { display: flex; justify-content: space-between; background: #fff; padding: 30rpx; border-radius: 16rpx; margin-bottom: 30rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05); }
+.summary-item { display: flex; flex-direction: column; align-items: center; }
+.label { font-size: 24rpx; color: #666; margin-bottom: 8rpx; }
+.value { font-size: 32rpx; font-weight: 600; color: #333; }
+.empty-goal { background: #fff; padding: 30rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: space-between; margin-bottom: 30rpx; }
+.set-goal-btn { background: #F97316; color: #fff; border-radius: 40rpx; }
+.quick-actions { display: flex; justify-content: space-between; background: #fff; padding: 30rpx 20rpx; border-radius: 16rpx; margin-bottom: 30rpx; }
+.action-item { display: flex; flex-direction: column; align-items: center; flex: 1; }
+.icon { font-size: 50rpx; margin-bottom: 8rpx; }
+.text { font-size: 24rpx; color: #333; }
+.today-stats { display: flex; justify-content: space-between; }
+.stat-box { background: #fff; padding: 30rpx; border-radius: 16rpx; flex: 1; margin: 0 10rpx; }
+.stat-label { font-size: 24rpx; color: #666; margin-bottom: 8rpx; display: block; }
+.stat-value { font-size: 36rpx; font-weight: 600; color: #F97316; }
+</style>

+ 76 - 0
xiaofat-miniprogram/pages/weight/create.vue

@@ -0,0 +1,76 @@
+<template>
+  <view class="create-page">
+    <view class="form">
+      <view class="form-item">
+        <text class="label">体重(kg)</text>
+        <input class="input" type="digit" v-model="form.weight" placeholder="请输入体重" />
+      </view>
+      <view class="form-item">
+        <text class="label">体脂率(%)</text>
+        <input class="input" type="digit" v-model="form.bodyFatPercent" placeholder="选填" />
+      </view>
+      <view class="form-item">
+        <text class="label">腰围(cm)</text>
+        <input class="input" type="digit" v-model="form.waistCircumference" placeholder="选填" />
+      </view>
+      <view class="form-item">
+        <text class="label">测量日期</text>
+        <picker mode="date" :value="form.measureDate" @change="onDateChange">
+          <view class="picker">{{ form.measureDate }}</view>
+        </picker>
+      </view>
+    </view>
+
+    <button class="submit-btn" @click="submit">保存</button>
+  </view>
+</template>
+
+<script>
+import { createWeightRecord } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+export default {
+  data() {
+    return {
+      form: {
+        weight: '',
+        bodyFatPercent: '',
+        waistCircumference: '',
+        measureDate: today()
+      }
+    }
+  },
+  methods: {
+    onDateChange(e) {
+      this.form.measureDate = e.detail.value
+    },
+    submit() {
+      if (!this.form.weight) {
+        uni.showToast({ title: '请输入体重', icon: 'none' })
+        return
+      }
+      var data = {
+        weight: this.form.weight,
+        measureDate: this.form.measureDate
+      }
+      if (this.form.bodyFatPercent) data.bodyFatPercent = this.form.bodyFatPercent
+      if (this.form.waistCircumference) data.waistCircumference = this.form.waistCircumference
+      createWeightRecord(data).then(() => {
+        uni.showToast({ title: '保存成功', icon: 'success' })
+        setTimeout(() => {
+          uni.navigateBack()
+        }, 1000)
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.create-page { padding: 30rpx; }
+.form { background: #fff; border-radius: 16rpx; padding: 0 30rpx; margin-bottom: 60rpx; }
+.form-item { display: flex; align-items: center; padding: 30rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.label { width: 200rpx; color: #333; }
+.input { flex: 1; text-align: right; }
+.picker { flex: 1; text-align: right; color: #333; }
+.submit-btn { background: #F97316; color: #fff; border-radius: 40rpx; }
+</style>

+ 76 - 0
xiaofat-miniprogram/pages/weight/index.vue

@@ -0,0 +1,76 @@
+<template>
+  <view class="weight-page">
+    <view class="header">
+      <text class="title">体重记录</text>
+      <button class="add-btn" size="mini" @click="goCreate">+ 记录</button>
+    </view>
+
+    <view class="list">
+      <view class="item" v-for="item in list" :key="item.id">
+        <view class="left">
+          <text class="date">{{ format(item.measureDate || item.createTime) }}</text>
+        </view>
+        <view class="right">
+          <text class="weight">{{ item.weight }} kg</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="empty" v-if="list.length === 0">
+      <text>暂无体重记录</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getWeightList, deleteWeightRecord } from '../../utils/api.js'
+import { formatDate } from '../../utils/format.js'
+export default {
+  data() {
+    return {
+      list: []
+    }
+  },
+  onShow() {
+    this.loadList()
+  },
+  methods: {
+    loadList() {
+      getWeightList().then(res => {
+        if (res.data) this.list = res.data
+      })
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/weight/create' })
+    },
+    format(date) {
+      return formatDate(date)
+    },
+    deleteItem(id) {
+      uni.showModal({
+        title: '提示',
+        content: '确定删除该记录吗?',
+        success: (res) => {
+          if (res.confirm) {
+            deleteWeightRecord(id).then(() => {
+              this.loadList()
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.weight-page { padding: 30rpx; }
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
+.title { font-size: 36rpx; font-weight: 600; }
+.add-btn { background: #F97316; color: #fff; border-radius: 40rpx; }
+.list { background: #fff; border-radius: 16rpx; overflow: hidden; }
+.item { display: flex; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
+.date { color: #666; }
+.weight { font-size: 32rpx; font-weight: 600; color: #F97316; }
+.empty { text-align: center; color: #999; padding: 60rpx; }
+</style>