|
@@ -1,13 +1,7 @@
|
|
|
<template>
|
|
<template>
|
|
|
<view class="recommendation-page" :style="{ '--font-scale': fontScale }">
|
|
<view class="recommendation-page" :style="{ '--font-scale': fontScale }">
|
|
|
|
|
|
|
|
- <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
|
|
|
|
- <!-- ===== 推荐食材 ===== -->
|
|
|
|
|
- <view class="section-title-bar">
|
|
|
|
|
- <text class="section-title">推荐食材</text>
|
|
|
|
|
- <text class="section-subtitle">查看家庭成员适合与戒备的食材</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
|
|
+ <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="loadMore">
|
|
|
<view class="food-section">
|
|
<view class="food-section">
|
|
|
<!-- 成员切换条(统一组件 MemberSelector:家庭总览 / 个人 / 各成员) -->
|
|
<!-- 成员切换条(统一组件 MemberSelector:家庭总览 / 个人 / 各成员) -->
|
|
|
<MemberSelector
|
|
<MemberSelector
|
|
@@ -17,9 +11,29 @@
|
|
|
:selected-member-id="currentMemberId"
|
|
:selected-member-id="currentMemberId"
|
|
|
:show-overview="true"
|
|
:show-overview="true"
|
|
|
:overview-active="currentView === 'family'"
|
|
:overview-active="currentView === 'family'"
|
|
|
|
|
+ self-label="我"
|
|
|
@overview="switchView('family')"
|
|
@overview="switchView('family')"
|
|
|
@select="switchMember" />
|
|
@select="switchMember" />
|
|
|
|
|
|
|
|
|
|
+ <!-- Tab 切换行:今日食谱 | 适合 | 戒备 | 全部 -->
|
|
|
|
|
+ <view class="filter-bar">
|
|
|
|
|
+ <view class="filter-item" :class="{'filter-active': currentTab === 'recipe'}" @tap="switchTab('recipe')">
|
|
|
|
|
+ <text>🍽️ 今日食谱</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="filter-item" :class="{'filter-active': currentTab === 'suitable'}" @tap="switchTab('suitable')">
|
|
|
|
|
+ <text>✅ 适合</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="filter-item" :class="{'filter-active': currentTab === 'unsuitable'}" @tap="switchTab('unsuitable')">
|
|
|
|
|
+ <text>❌ 戒备</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="filter-item" :class="{'filter-active': currentTab === 'all'}" @tap="switchTab('all')">
|
|
|
|
|
+ <text>📋 全部</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 食材区(适合 / 戒备 / 全部 Tab 内容) -->
|
|
|
|
|
+ <view class="food-section" v-if="currentTab !== 'recipe'">
|
|
|
<!-- 模式切换(仅家庭模式展示) -->
|
|
<!-- 模式切换(仅家庭模式展示) -->
|
|
|
<view class="mode-bar" v-if="familyMode">
|
|
<view class="mode-bar" v-if="familyMode">
|
|
|
<view class="mode-btn" :class="{'active': sortType === 'desc'}" @tap="setSort('desc')">
|
|
<view class="mode-btn" :class="{'active': sortType === 'desc'}" @tap="setSort('desc')">
|
|
@@ -61,19 +75,6 @@
|
|
|
</view>
|
|
</view>
|
|
|
</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 class="filter-item" :class="{'filter-active': filterType === 'all'}" @tap="setFilter('all')">
|
|
|
|
|
- <text>📋 全部</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
<!-- 家庭总览(取家庭成员该项食材推荐指数的最小值) -->
|
|
<!-- 家庭总览(取家庭成员该项食材推荐指数的最小值) -->
|
|
|
<view class="family-overview" v-if="currentView === 'family' && familyOverview && familyOverview.length > 0">
|
|
<view class="family-overview" v-if="currentView === 'family' && familyOverview && familyOverview.length > 0">
|
|
|
<view class="overview-title">
|
|
<view class="overview-title">
|
|
@@ -101,7 +102,7 @@
|
|
|
|
|
|
|
|
<!-- 食材列表 -->
|
|
<!-- 食材列表 -->
|
|
|
<view class="food-list" v-if="currentView === 'personal'">
|
|
<view class="food-list" v-if="currentView === 'personal'">
|
|
|
- <view class="food-card" v-for="(item, idx) in filteredFoods" :key="getFoodKey(item, idx)">
|
|
|
|
|
|
|
+ <view class="food-card" v-for="(item, idx) in pagedFoods" :key="getFoodKey(item, idx)">
|
|
|
<!-- 健康问卷个性化调整提示条:blocked 红 / warning 黄 / boost 绿(normal/空不渲染) -->
|
|
<!-- 健康问卷个性化调整提示条:blocked 红 / warning 黄 / boost 绿(normal/空不渲染) -->
|
|
|
<view v-if="item.adjustment === 'blocked'" class="health-adjust-bar">
|
|
<view v-if="item.adjustment === 'blocked'" class="health-adjust-bar">
|
|
|
<text class="health-adjust-text health-adjust-blocked">⚠ 健康问卷不建议:{{ item.adjustmentReason }}</text>
|
|
<text class="health-adjust-text health-adjust-blocked">⚠ 健康问卷不建议:{{ item.adjustmentReason }}</text>
|
|
@@ -142,10 +143,8 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- ===== 今日食谱 ===== -->
|
|
|
|
|
- <view class="section-title-bar">
|
|
|
|
|
- <text class="section-title">今日食谱</text>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <!-- 今日食谱区(recipe Tab 内容) -->
|
|
|
|
|
+ <view v-if="currentTab === 'recipe'">
|
|
|
|
|
|
|
|
<!-- 无推荐状态 -->
|
|
<!-- 无推荐状态 -->
|
|
|
<view class="empty-state" v-if="!recommendation">
|
|
<view class="empty-state" v-if="!recommendation">
|
|
@@ -243,6 +242,7 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ </view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
|
|
|
|
|
|
<!-- 共餐人选择弹层(需求7:家庭总览「生成食谱」先选共餐人,确认后生成) -->
|
|
<!-- 共餐人选择弹层(需求7:家庭总览「生成食谱」先选共餐人,确认后生成) -->
|
|
@@ -301,6 +301,9 @@ export default {
|
|
|
filterType: 'suitable',
|
|
filterType: 'suitable',
|
|
|
sortType: 'desc',
|
|
sortType: 'desc',
|
|
|
currentView: 'personal',
|
|
currentView: 'personal',
|
|
|
|
|
+ currentTab: 'recipe',
|
|
|
|
|
+ visibleCount: 20,
|
|
|
|
|
+ memberSelectLocked: false,
|
|
|
allFoods: [],
|
|
allFoods: [],
|
|
|
familyData: null,
|
|
familyData: null,
|
|
|
familyMembers: [],
|
|
familyMembers: [],
|
|
@@ -386,6 +389,9 @@ export default {
|
|
|
|
|
|
|
|
return result
|
|
return result
|
|
|
},
|
|
},
|
|
|
|
|
+ pagedFoods: function() {
|
|
|
|
|
+ return this.filteredFoods.slice(0, this.visibleCount)
|
|
|
|
|
+ },
|
|
|
// 共餐人选择弹层列表(原始 memberList 归一化为 {id, nickname, selected},避免模板内方法调用)
|
|
// 共餐人选择弹层列表(原始 memberList 归一化为 {id, nickname, selected},避免模板内方法调用)
|
|
|
pendingParticipantOptions: function() {
|
|
pendingParticipantOptions: function() {
|
|
|
var self = this
|
|
var self = this
|
|
@@ -481,12 +487,14 @@ export default {
|
|
|
// 个人/成员:只对当前选中人生成(participant_member_ids 只传当前成员)
|
|
// 个人/成员:只对当前选中人生成(participant_member_ids 只传当前成员)
|
|
|
self.loading = true
|
|
self.loading = true
|
|
|
var participantIds = self.currentMemberId ? [self.currentMemberId] : []
|
|
var participantIds = self.currentMemberId ? [self.currentMemberId] : []
|
|
|
- generateDietRecommendation({
|
|
|
|
|
|
|
+ uni.showLoading({ title: '正在生成食谱...' })
|
|
|
|
|
+ return generateDietRecommendation({
|
|
|
date: self.formatDate(new Date()),
|
|
date: self.formatDate(new Date()),
|
|
|
meal_type: 'all',
|
|
meal_type: 'all',
|
|
|
participant_member_ids: JSON.stringify(participantIds)
|
|
participant_member_ids: JSON.stringify(participantIds)
|
|
|
}).then(function(res) {
|
|
}).then(function(res) {
|
|
|
self.loading = false
|
|
self.loading = false
|
|
|
|
|
+ uni.hideLoading()
|
|
|
if (res && res.data && res.data.id) {
|
|
if (res && res.data && res.data.id) {
|
|
|
uni.showToast({ title: '食谱生成成功', icon: 'success' })
|
|
uni.showToast({ title: '食谱生成成功', icon: 'success' })
|
|
|
self.loadRecommendation()
|
|
self.loadRecommendation()
|
|
@@ -495,6 +503,7 @@ export default {
|
|
|
}
|
|
}
|
|
|
}).catch(function() {
|
|
}).catch(function() {
|
|
|
self.loading = false
|
|
self.loading = false
|
|
|
|
|
+ uni.hideLoading()
|
|
|
uni.showToast({ title: '生成失败', icon: 'none' })
|
|
uni.showToast({ title: '生成失败', icon: 'none' })
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
@@ -602,14 +611,17 @@ export default {
|
|
|
if (!token) return
|
|
if (!token) return
|
|
|
|
|
|
|
|
uni.request({
|
|
uni.request({
|
|
|
- url: config.API_BASE_URL + '/api/food/recommendation/members',
|
|
|
|
|
|
|
+ // config 在契约测试桩下可能为 undefined(stub 仅提供 default 键),防御性取址
|
|
|
|
|
+ url: ((config && config.API_BASE_URL) || '') + '/api/food/recommendation/members',
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|
header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|
|
success: function(res) {
|
|
success: function(res) {
|
|
|
if (res.data && res.data.code === 200 && res.data.data) {
|
|
if (res.data && res.data.code === 200 && res.data.data) {
|
|
|
self.memberList = res.data.data
|
|
self.memberList = res.data.data
|
|
|
- // 默认选中「个人」= isSelf 行(统一身份键 familyMemberId,不再用 userId)
|
|
|
|
|
- if (!self.currentMemberId && self.memberList.length > 0) {
|
|
|
|
|
|
|
+ // 默认选中「个人」= isSelf 行(统一身份键 familyMemberId,不再用 userId)。
|
|
|
|
|
+ // 仅限首次 onLoad:切到家庭总览(switchView('family') 置 memberSelectLocked)后,
|
|
|
|
|
+ // loadMembers 不得再自动默认选「个人」,个人/成员 chip 保持不高亮。
|
|
|
|
|
+ if (!self.memberSelectLocked && !self.currentMemberId && self.memberList.length > 0) {
|
|
|
var defaultMember = self.getSelfRow() || self.memberList[0]
|
|
var defaultMember = self.getSelfRow() || self.memberList[0]
|
|
|
self.currentMemberId = memberIdOf(defaultMember)
|
|
self.currentMemberId = memberIdOf(defaultMember)
|
|
|
}
|
|
}
|
|
@@ -760,6 +772,7 @@ export default {
|
|
|
},
|
|
},
|
|
|
switchCategory: function(category) {
|
|
switchCategory: function(category) {
|
|
|
this.activeCategory = category
|
|
this.activeCategory = category
|
|
|
|
|
+ this.visibleCount = 20
|
|
|
},
|
|
},
|
|
|
getCategoryKeywords: function(category) {
|
|
getCategoryKeywords: function(category) {
|
|
|
var map = {
|
|
var map = {
|
|
@@ -780,12 +793,34 @@ export default {
|
|
|
} else {
|
|
} else {
|
|
|
this.sortType = 'desc'
|
|
this.sortType = 'desc'
|
|
|
}
|
|
}
|
|
|
|
|
+ this.visibleCount = 20
|
|
|
},
|
|
},
|
|
|
setSort: function(type) {
|
|
setSort: function(type) {
|
|
|
this.sortType = type
|
|
this.sortType = type
|
|
|
},
|
|
},
|
|
|
|
|
+ switchTab: function(tab) {
|
|
|
|
|
+ // 4 Tab:今日食谱 | 适合 | 戒备 | 全部;切 Tab 重置分页(请求式上滑加载从首批重新开始)
|
|
|
|
|
+ this.currentTab = tab
|
|
|
|
|
+ this.visibleCount = 20
|
|
|
|
|
+ if (tab !== 'recipe') {
|
|
|
|
|
+ this.filterType = tab
|
|
|
|
|
+ this.sortType = tab === 'unsuitable' ? 'asc' : 'desc'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ loadMore: function() {
|
|
|
|
|
+ // 上滑触底加载更多:未达全量末尾时每批追加 20 条,到达末尾自动停止
|
|
|
|
|
+ if (this.visibleCount < this.filteredFoods.length) {
|
|
|
|
|
+ this.visibleCount += 20
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
switchView: function(view) {
|
|
switchView: function(view) {
|
|
|
this.currentView = view
|
|
this.currentView = view
|
|
|
|
|
+ if (view === 'family') {
|
|
|
|
|
+ // 家庭总览 ↔ 个人/成员互斥:清空成员选中态(个人/成员 chip 不高亮);
|
|
|
|
|
+ // 并锁定「不再自动默认选个人」,loadMembers 再次加载时不回灌默认选中
|
|
|
|
|
+ this.currentMemberId = null
|
|
|
|
|
+ this.memberSelectLocked = true
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
// 取「本人」行(isSelf):后端已按 user_id 折叠,此处仅取第一行 isSelf
|
|
// 取「本人」行(isSelf):后端已按 user_id 折叠,此处仅取第一行 isSelf
|
|
|
getSelfRow: function() {
|
|
getSelfRow: function() {
|
|
@@ -799,6 +834,7 @@ export default {
|
|
|
// 统一身份键:familyMemberId(= family_members.id);userId 仅用于折叠,不再作主键
|
|
// 统一身份键:familyMemberId(= family_members.id);userId 仅用于折叠,不再作主键
|
|
|
this.currentMemberId = memberIdOf(member)
|
|
this.currentMemberId = memberIdOf(member)
|
|
|
this.currentView = 'personal'
|
|
this.currentView = 'personal'
|
|
|
|
|
+ this.visibleCount = 20
|
|
|
this.loadFoods()
|
|
this.loadFoods()
|
|
|
},
|
|
},
|
|
|
onRefresh: function() {
|
|
onRefresh: function() {
|
|
@@ -1087,7 +1123,9 @@ export default {
|
|
|
margin-bottom: 20rpx;
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
}
|
|
|
.filter-item {
|
|
.filter-item {
|
|
|
- padding: 12rpx 24rpx;
|
|
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 12rpx 8rpx;
|
|
|
border: 1rpx solid $facade-border-soft;
|
|
border: 1rpx solid $facade-border-soft;
|
|
|
border-radius: 24rpx;
|
|
border-radius: 24rpx;
|
|
|
font-size: calc(24rpx * var(--font-scale, 1));
|
|
font-size: calc(24rpx * var(--font-scale, 1));
|