| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- <template>
- <view class="page">
- <view class="member-bar" v-if="memberList.length > 1">
- <view class="member-tab" :class="{'active': currentMemberId === (item.id || item.userId)}"
- v-for="(item, mi) in memberList" :key="mi"
- @tap="switchMember(item)">
- <text class="member-name">{{ item.nickname || item.roleLabel || '全部' }}</text>
- </view>
- </view>
- <!-- 周历 -->
- <view class="week-strip">
- <view class="week-day" v-for="(d, i) in weekDays" :key="d.date"
- :class="{'week-day-today': d.today, 'week-day-active': d.date === selectedDate, 'week-day-has': d.hasRecords}">
- <text class="week-day-label">{{ d.label }}</text>
- <text class="week-day-num">{{ d.num }}</text>
- <view class="week-dot-wrap" v-if="d.hasRecords">
- <view class="week-dot"></view>
- <text class="week-day-count">{{ d.count }}</text>
- </view>
- </view>
- </view>
- <scroll-view class="content" scroll-y>
- <!-- 食材摄入提示 -->
- <view class="alert-card" v-if="alertData.overConsumed.length > 0 || alertData.underConsumed.length > 0">
- <view class="alert-header">
- <text class="alert-icon">⚠️</text>
- <text class="alert-title">饮食提示</text>
- </view>
- <view class="alert-section" v-if="alertData.overConsumed.length > 0">
- <text class="alert-label over">摄入偏多 / 需戒备</text>
- <view class="alert-tags">
- <view class="alert-tag tag-over" v-for="(item, i) in alertData.overConsumed" :key="i">
- <text>{{ item.foodName }}</text>
- <text class="tag-count">{{ item.count }}次</text>
- </view>
- </view>
- <text class="alert-hint">{{ alertData.overConsumed[0] && alertData.overConsumed[0].reason || '' }}</text>
- </view>
- <view class="alert-section" v-if="alertData.underConsumed.length > 0">
- <text class="alert-label under">摄入偏少</text>
- <view class="alert-tags">
- <view class="alert-tag tag-under" v-for="(item, i) in alertData.underConsumed" :key="i">
- <text>{{ item.foodName }}</text>
- <text class="tag-count">{{ item.count }}次</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 本周营养汇总 -->
- <view class="card" v-if="weeklySummary.recordCount > 0">
- <view class="card-accent"></view>
- <view class="card-body">
- <text class="card-title">本周饮食汇总({{ weeklySummary.startDate }} ~ {{ weeklySummary.endDate }})</text>
- <view class="stat-row">
- <view class="stat-item">
- <text class="stat-val">{{ weeklySummary.totalCalories || 0 }}</text>
- <text class="stat-unit">kcal</text>
- <text class="stat-label">总热量</text>
- </view>
- <view class="stat-item">
- <text class="stat-val">{{ weeklySummary.activeDays || 0 }}</text>
- <text class="stat-unit">天</text>
- <text class="stat-label">打卡天数</text>
- </view>
- <view class="stat-item">
- <text class="stat-val">{{ weeklySummary.totalProtein || 0 }}</text>
- <text class="stat-unit">g</text>
- <text class="stat-label">蛋白质</text>
- </view>
- <view class="stat-item">
- <text class="stat-val">{{ weeklySummary.totalFat || 0 }}</text>
- <text class="stat-unit">g</text>
- <text class="stat-label">脂肪</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 高频食材 -->
- <view class="card" v-if="weeklySummary.topFoods.length > 0">
- <view class="card-accent" style="background: linear-gradient(180deg, #FF8C42, #FFB366);"></view>
- <view class="card-body">
- <text class="card-title">本周高频食材(前10)</text>
- <view class="top-foods-list">
- <view class="top-food-item" v-for="(f, i) in weeklySummary.topFoods" :key="f.foodName">
- <text class="top-food-rank">{{ i + 1 }}</text>
- <text class="top-food-name">{{ f.foodName }}</text>
- <text class="top-food-count">{{ f.count }}次</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 日期切换 -->
- <view class="date-bar">
- <view class="date-prev" @tap="prevDay">‹ 前一天</view>
- <text class="date-display">{{ selectedDate }}</text>
- <view class="date-next" @tap="nextDay">下一天 ›</view>
- </view>
- <!-- 当日统计 -->
- <view class="card daily-summary-card" v-if="dailyStats.totalCalories > 0 || dailyStats.totalProtein > 0">
- <view class="card-accent"></view>
- <view class="card-body">
- <text class="card-title">{{ selectedDate }} 当日统计</text>
- <view class="stat-row">
- <view class="stat-item">
- <text class="stat-val">{{ dailyStats.totalCalories || 0 }}</text>
- <text class="stat-unit">kcal</text>
- <text class="stat-label">热量</text>
- </view>
- <view class="stat-item">
- <text class="stat-val">{{ dailyStats.totalProtein || 0 }}</text>
- <text class="stat-unit">g</text>
- <text class="stat-label">蛋白</text>
- </view>
- <view class="stat-item">
- <text class="stat-val">{{ dailyStats.totalFat || 0 }}</text>
- <text class="stat-unit">g</text>
- <text class="stat-label">脂肪</text>
- </view>
- <view class="stat-item">
- <text class="stat-val">{{ dailyStats.totalCarbs || 0 }}</text>
- <text class="stat-unit">g</text>
- <text class="stat-label">碳水</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 记录列表(按日期分组) -->
- <view class="records-section" v-if="records.length > 0">
- <view class="records-title">
- <text class="records-label">{{ selectedDate }} 饮食记录</text>
- <text class="records-count">{{ records.length }} 条</text>
- </view>
- <view class="record-card" v-for="r in records" :key="r.id" @tap="goDetail(r)">
- <view class="record-card-header">
- <view class="meal-badge" :class="'meal-' + r.mealType">
- <text>{{ mealLabel(r.mealType) }}</text>
- </view>
- <view class="record-meta">
- <text class="record-time">{{ formatTime(r.createdAt) }}</text>
- <text class="record-backfill" v-if="r.isBackfill">补打</text>
- </view>
- </view>
- <view class="record-body">
- <text class="record-foods">{{ formatFoodItems(r.foodItems) }}</text>
- </view>
- <view class="record-footer" v-if="r.totalCalories > 0 || r.proteinG > 0">
- <text class="record-cal">🔥 {{ r.totalCalories }}kcal</text>
- <text class="record-nut" v-if="r.proteinG > 0">蛋白 {{ r.proteinG }}g</text>
- </view>
- <view class="record-remark" v-if="r.remark">{{ r.remark }}</view>
- </view>
- </view>
- <view class="empty-state" v-else-if="!loading">
- <text class="empty-icon">🍽️</text>
- <text class="empty-text">当天暂无饮食记录</text>
- <text class="empty-hint">点击右下角「+」打卡记录</text>
- </view>
- <view class="loading-state" v-if="loading">
- <text class="loading-text">加载中...</text>
- </view>
- <view class="bottom-spacer"></view>
- </scroll-view>
- <view class="fab" @tap="addRecord">
- <text class="fab-icon">+</text>
- </view>
- </view>
- </template>
- <script>
- import { getMealList, getMealDaily, getMealWeekly, getMealAlerts, getCautionFamilyMembers, createMealRecord } from '../../utils/api.js'
- import { parseDate } from '../../utils/format.js'
- export default {
- data() {
- return {
- memberId: null,
- currentMemberId: null,
- memberList: [],
- selectedDate: this.formatDate(new Date()),
- weekDays: [],
- records: [],
- dailyStats: {},
- weeklySummary: {},
- alertData: { overConsumed: [], underConsumed: [] },
- loading: false,
- _lastLoadDate: null
- }
- },
- onLoad: function(options) {
- this.memberId = options.memberId ? parseInt(options.memberId) : null
- this.currentMemberId = this.memberId
- this.loadMemberList()
- this.initWeekDays()
- this.loadWeekly()
- this.loadAlerts()
- this.loadDayRecords()
- },
- methods: {
- goBack: function() { uni.navigateBack() },
- mealLabel: function(t) {
- var m = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' }
- return m[t] || t || '饮食'
- },
- switchMember: function(item) {
- this.currentMemberId = item.id || item.userId
- this.loadWeekly()
- this.loadAlerts()
- this.loadDayRecords()
- },
- async loadMemberList() {
- try {
- var res = await getCautionFamilyMembers()
- if (res && res.code === 200 && res.data && res.data.length > 0) {
- var allItem = { id: -1, userId: -1, nickname: '全部', roleLabel: '全部' }
- this.memberList = [allItem].concat(res.data)
- if (!this.currentMemberId) this.currentMemberId = -1
- }
- } catch(e) {}
- },
- initWeekDays: function() {
- var self = this
- var today = new Date()
- var dayOfWeek = today.getDay() || 7 // 1=周一 7=周日
- var monday = new Date(today)
- monday.setDate(today.getDate() - (dayOfWeek - 1))
- var days = []
- var labels = ['一', '二', '三', '四', '五', '六', '日']
- for (var i = 0; i < 7; i++) {
- var d = new Date(monday)
- d.setDate(monday.getDate() + i)
- days.push({
- date: self.formatDate(d),
- label: '周' + labels[i],
- num: d.getDate(),
- today: self.formatDate(today) === self.formatDate(d),
- hasRecords: false,
- count: 0
- })
- }
- this.weekDays = days
- },
- updateWeekDots: function(records) {
- var self = this
- var countByDate = {}
- for (var i = 0; i < records.length; i++) {
- var r = records[i]
- var d = r.recordDate || self.formatDate(r.createdAt)
- countByDate[d] = (countByDate[d] || 0) + 1
- }
- for (var j = 0; j < self.weekDays.length; j++) {
- var wd = self.weekDays[j]
- wd.hasRecords = (countByDate[wd.date] || 0) > 0
- wd.count = countByDate[wd.date] || 0
- }
- self.$forceUpdate()
- },
- loadWeekly: function() {
- var self = this
- var mid = self.currentMemberId > 0 ? self.currentMemberId : null
- getMealWeekly({ memberId: mid, startDate: self.weekDays[0].date, endDate: self.weekDays[6].date })
- .then(function(res) {
- if (res && res.code === 200 && res.data) {
- self.weeklySummary = res.data
- self.updateWeekDots([])
- }
- }).catch(function() {})
- },
- loadAlerts: function() {
- var self = this
- var mid = self.currentMemberId > 0 ? self.currentMemberId : null
- getMealAlerts({ memberId: mid, startDate: self.weekDays[0].date, endDate: self.weekDays[6].date })
- .then(function(res) {
- if (res && res.code === 200 && res.data) self.alertData = res.data
- }).catch(function() {})
- },
- loadDayRecords: function() {
- var self = this
- self.loading = true
- var mid = self.currentMemberId > 0 ? self.currentMemberId : null
- getMealDaily({ memberId: mid, date: self.selectedDate })
- .then(function(res) {
- self.loading = false
- if (res && res.code === 200 && res.data) {
- self.records = (res.data || [])
- var stats = self._calcDailyStats(self.records)
- self.dailyStats = stats
- } else {
- self.records = []
- self.dailyStats = {}
- }
- })
- .catch(function() { self.loading = false })
- },
- _calcDailyStats: function(records) {
- var s = { totalCalories: 0, totalProtein: 0, totalFat: 0, totalCarbs: 0, totalFiber: 0 }
- for (var i = 0; i < records.length; i++) {
- var r = records[i]
- if (r.totalCalories != null) s.totalCalories += r.totalCalories
- if (r.proteinG != null) s.totalProtein += r.proteinG
- if (r.fatG != null) s.totalFat += r.fatG
- if (r.carbsG != null) s.totalCarbs += r.carbsG
- if (r.fiberG != null) s.totalFiber += r.fiberG
- }
- return s
- },
- prevDay: function() {
- var d = parseDate(this.selectedDate)
- if (!d) return
- d.setDate(d.getDate() - 1)
- this.selectedDate = this.formatDate(d)
- this.loadDayRecords()
- },
- nextDay: function() {
- var d = parseDate(this.selectedDate)
- if (!d) return
- d.setDate(d.getDate() + 1)
- this.selectedDate = this.formatDate(d)
- this.loadDayRecords()
- },
- addRecord: function() {
- var url = '/pages/growth/diet-checkin'
- if (this.selectedDate) url += '?date=' + this.selectedDate
- uni.navigateTo({ url: url })
- },
- goDetail: function(record) {
- // 饮食记录详情暂时直接展示,不跳转
- var foods = this.formatFoodItems(record.foodItems)
- var msg = (this.mealLabel(record.mealType) || '') + ' · ' + this.formatTime(record.createdAt)
- if (foods) msg += '\n' + foods
- if (record.remark) msg += '\n' + record.remark
- if (record.isBackfill) msg += '\n(补打,无能量值)'
- uni.showModal({ title: msg, content: '', showCancel: false })
- },
- formatFoodItems: function(txt) {
- if (!txt) return ''
- var clean = txt.replace(/^\[|\]$/g, '').replace(/"/g, '')
- // 尝试解析 JSON
- try {
- var arr = JSON.parse(txt)
- if (Array.isArray(arr)) {
- return arr.map(function(f) { return f.name || f }).join('、')
- }
- } catch(e) {}
- // 纯文本:去掉数量单位
- return clean.replace(/\d+[克斤两碗份盘个]/g, '').replace(/[、,,;;]/g, '、').replace(/\s+/g, ' ').trim()
- },
- formatTime: function(ts) {
- if (!ts) return ''
- var d = parseDate(ts)
- if (!d) return ''
- return (d.getHours() || 0).toString().padStart(2, '0') + ':' + (d.getMinutes() || 0).toString().padStart(2, '0')
- },
- formatDate: function(d) {
- if (!d) return ''
- var date = typeof d === 'string' ? parseDate(d) : d
- if (!date) return ''
- var m = (date.getMonth() + 1).toString().padStart(2, '0')
- var day = date.getDate().toString().padStart(2, '0')
- return date.getFullYear() + '-' + m + '-' + day
- }
- }
- }
- </script>
- <style scoped>
- .page { min-height: 100vh; background: #FFF7ED; display: flex; flex-direction: column; }
- /* 成员切换栏 */
- .member-bar {
- display: flex; overflow-x: auto; padding: 16rpx 30rpx 8rpx; background: #fff;
- border-bottom: 1rpx solid #f0f0f0; gap: 16rpx; -webkit-overflow-scrolling: touch;
- }
- .member-tab {
- flex-shrink: 0; padding: 8rpx 20rpx; border-radius: 20rpx; background: #f5f5f5; color: #666;
- font-size: 26rpx; font-weight: 500;
- }
- .member-tab.active { background: linear-gradient(135deg, #FF8C42, #FFB366); color: #fff; }
- /* 周历条 */
- .week-strip {
- display: flex; align-items: center; justify-content: space-around;
- background: #fff; padding: 12rpx 20rpx 16rpx; border-bottom: 1rpx solid #f0f0f0;
- }
- .week-day {
- display: flex; flex-direction: column; align-items: center;
- padding: 8rpx 12rpx; border-radius: 16rpx; min-width: 68rpx;
- }
- .week-day-today .week-day-label { color: #FF8C42; font-weight: 700; }
- .week-day-active { background: linear-gradient(135deg, #FF8C42, #FFB366); }
- .week-day-active .week-day-label,
- .week-day-active .week-day-num { color: #fff; }
- .week-day-has .week-dot-wrap { margin-top: 6rpx; }
- .week-day-label { font-size: 20rpx; color: #999; }
- .week-day-num { font-size: 28rpx; font-weight: 600; color: #333; }
- .week-dot-wrap { display: flex; align-items: center; gap: 6rpx; }
- .week-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #FF8C42; }
- .week-day-count { font-size: 18rpx; color: #C2410C; }
- /* 内容区 */
- .content { flex: 1; padding: 24rpx 24rpx 180rpx; overflow-y: auto; }
- /* 通用卡片 */
- .card {
- display: flex; background: #fff; border-radius: 20rpx; margin-bottom: 24rpx;
- box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05); overflow: hidden;
- }
- .card-accent { width: 8rpx; background: linear-gradient(180deg, #FF8C42, #FFB366); flex-shrink: 0; }
- .card-body { flex: 1; padding: 24rpx 28rpx; }
- .card-title { font-size: 28rpx; font-weight: 600; color: #333; margin-bottom: 16rpx; display: block; }
- /* 统计行 */
- .stat-row { display: flex; justify-content: space-around; }
- .stat-item { text-align: center; }
- .stat-val { font-size: 36rpx; font-weight: 700; color: #FF8C42; display: block; }
- .stat-unit { font-size: 20rpx; color: #999; }
- .stat-label { font-size: 22rpx; color: #666; display: block; margin-top: 4rpx; }
- /* 食材提示 */
- .alert-card {
- background: #fff; border-radius: 20rpx; margin-bottom: 24rpx;
- box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05); overflow: hidden;
- }
- .alert-header { display: flex; align-items: center; gap: 12rpx; padding: 24rpx 28rpx 16rpx; border-bottom: 1rpx solid #FFF3E0; }
- .alert-icon { font-size: 36rpx; }
- .alert-title { font-size: 28rpx; font-weight: 600; color: #333; }
- .alert-section { padding: 16rpx 28rpx; }
- .alert-label { font-size: 24rpx; font-weight: 600; display: block; margin-bottom: 12rpx; }
- .alert-label.over { color: #C62828; }
- .alert-label.under { color: #1565C0; }
- .alert-tags { display: flex; flex-wrap: wrap; gap: 12rpx; }
- .alert-tag { display: flex; align-items: center; gap: 6rpx; padding: 8rpx 16rpx; border-radius: 20rpx; font-size: 24rpx; }
- .tag-over { background: #FFEBEE; color: #C62828; }
- .tag-under { background: #E3F2FD; color: #1565C0; }
- .tag-count { font-size: 20rpx; opacity: 0.8; }
- .alert-hint { font-size: 22rpx; color: #888; margin-top: 8rpx; display: block; }
- /* 高频食材 */
- .top-foods-list { display: flex; flex-direction: column; gap: 12rpx; }
- .top-food-item { display: flex; align-items: center; gap: 16rpx; }
- .top-food-rank {
- width: 36rpx; height: 36rpx; border-radius: 50%; background: #FFF1E6; color: #FF8C42;
- font-size: 22rpx; font-weight: 700; display: flex; align-items: center; justify-content: center;
- }
- .top-food-name { flex: 1; font-size: 26rpx; color: #333; }
- .top-food-count { font-size: 24rpx; color: #999; }
- /* 日期切换栏 */
- .date-bar {
- display: flex; align-items: center; justify-content: space-between;
- background: #fff; border-radius: 16rpx; padding: 16rpx 24rpx; margin-bottom: 20rpx;
- box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
- }
- .date-prev, .date-next { font-size: 26rpx; color: #FF8C42; font-weight: 600; padding: 8rpx 16rpx; }
- .date-display { font-size: 28rpx; font-weight: 600; color: #333; }
- /* 记录卡片 */
- .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
- .records-label { font-size: 28rpx; font-weight: 600; color: #333; }
- .records-count { font-size: 24rpx; color: #999; }
- .record-card {
- background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 16rpx;
- box-shadow: 0 2rpx 8rpx rgba(255,140,66,0.06); border-left: 6rpx solid #FF8C42;
- }
- .record-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10rpx; }
- .meal-badge { padding: 6rpx 18rpx; border-radius: 20rpx; font-size: 24rpx; font-weight: 600; }
- .meal-breakfast { background: #FFF3E0; color: #E65100; }
- .meal-lunch { background: #FFF8E1; color: #F57C00; }
- .meal-dinner { background: #F3E5F5; color: #7B1FA2; }
- .meal-snack { background: #E8F5E9; color: #2E7D32; }
- .record-meta { display: flex; align-items: center; gap: 12rpx; }
- .record-time { font-size: 24rpx; color: #999; }
- .record-backfill { font-size: 20rpx; padding: 2rpx 10rpx; background: #FFF3E0; color: #E65100; border-radius: 8rpx; }
- .record-body { margin-bottom: 8rpx; }
- .record-foods { font-size: 26rpx; color: #333; line-height: 1.5; }
- .record-footer { display: flex; gap: 20rpx; margin-bottom: 6rpx; }
- .record-cal { font-size: 22rpx; color: #FF8C42; font-weight: 600; }
- .record-nut { font-size: 22rpx; color: #999; }
- .record-remark { font-size: 24rpx; color: #888; font-style: italic; }
- /* 空态 */
- .empty-state { text-align: center; padding: 80rpx 40rpx; }
- .empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
- .empty-text { font-size: 28rpx; color: #333; font-weight: 500; display: block; margin-bottom: 8rpx; }
- .empty-hint { font-size: 24rpx; color: #999; display: block; }
- .loading-state { text-align: center; padding: 80rpx 0; }
- .loading-text { font-size: 26rpx; color: #999; }
- .bottom-spacer { height: 40rpx; }
- /* 浮动按钮 */
- .fab {
- position: fixed; right: 40rpx; bottom: 60rpx; width: 100rpx; height: 100rpx;
- background: linear-gradient(135deg, #FF8C42, #FFB366); 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: #fff; font-weight: 300; }
- </style>
|