188 lines
5.8 KiB
JavaScript
188 lines
5.8 KiB
JavaScript
const { request } = require('../../utils/request')
|
||
|
||
const REC_TYPES = [
|
||
{
|
||
key: 'current',
|
||
label: '当前状态推荐',
|
||
desc: '基于你最近一次饮食状态,用随机森林给出当前最匹配的食谱。',
|
||
scene: '日常实时决策',
|
||
requirement: '建议先填写今天的体重、体脂、摄入和消耗'
|
||
},
|
||
{
|
||
key: 'health',
|
||
label: '健康状态推荐',
|
||
desc: '按体脂、摄入和运动平衡进行规则评分,优先推荐营养结构更均衡的方案。',
|
||
scene: '想吃得更健康',
|
||
requirement: '推荐先有基础饮食状态数据'
|
||
},
|
||
{
|
||
key: 'history',
|
||
label: '历史计划推荐',
|
||
desc: '根据近一段时间的体重和摄入趋势,给出偏减脂或偏增肌的策略型推荐。',
|
||
scene: '需要阶段性调整',
|
||
requirement: '至少有 3 天历史记录'
|
||
},
|
||
{
|
||
key: 'goal',
|
||
label: '未来目标推荐',
|
||
desc: '围绕你的目标(减脂/增肌/生酮/维持)进行宏量营养匹配。',
|
||
scene: '目标导向饮食',
|
||
requirement: '可跟随个人资料,也可手动指定目标'
|
||
},
|
||
{
|
||
key: 'occupation',
|
||
label: '职业行业推荐',
|
||
desc: '根据职业活动强度和作息特点,推荐更贴近日常能量需求的食谱。',
|
||
scene: '工作日场景优化',
|
||
requirement: '可跟随个人资料,也可手动指定职业'
|
||
}
|
||
]
|
||
|
||
const TOP_K_OPTIONS = [
|
||
{ value: 3, label: '3 条' },
|
||
{ value: 5, label: '5 条' },
|
||
{ value: 10, label: '10 条' }
|
||
]
|
||
|
||
const GOAL_OPTIONS = [
|
||
{ value: '', label: '跟随个人资料(推荐)' },
|
||
{ value: 'lose_fat', label: '减脂(lose_fat)' },
|
||
{ value: 'gain_muscle', label: '增肌(gain_muscle)' },
|
||
{ value: 'keto', label: '生酮(keto)' },
|
||
{ value: 'maintain', label: '维持(maintain)' }
|
||
]
|
||
|
||
const OCCUPATION_OPTIONS = [
|
||
{ value: '', label: '跟随个人资料(推荐)' },
|
||
{ value: 'office', label: '办公室(office)' },
|
||
{ value: 'developer', label: '程序员(developer)' },
|
||
{ value: 'fitness', label: '健身行业(fitness)' },
|
||
{ value: 'teacher', label: '教师(teacher)' },
|
||
{ value: 'student', label: '学生(student)' },
|
||
{ value: 'manual', label: '体力劳动(manual)' }
|
||
]
|
||
|
||
Page({
|
||
data: {
|
||
recType: 'current',
|
||
recTypes: REC_TYPES,
|
||
activeType: REC_TYPES[0],
|
||
selectedLabel: REC_TYPES[0].label,
|
||
topK: 5,
|
||
topKLabel: '5 条',
|
||
topKOptions: TOP_K_OPTIONS,
|
||
goalValue: '',
|
||
goalLabel: GOAL_OPTIONS[0].label,
|
||
goalOptions: GOAL_OPTIONS,
|
||
occupationValue: '',
|
||
occupationLabel: OCCUPATION_OPTIONS[0].label,
|
||
occupationOptions: OCCUPATION_OPTIONS,
|
||
loading: false,
|
||
payload: null,
|
||
summary: null,
|
||
errorText: '',
|
||
lastUpdatedAt: ''
|
||
},
|
||
|
||
onShow() {
|
||
this.fetchRecommend()
|
||
},
|
||
|
||
onTypeChange(e) {
|
||
const idx = Number(e.detail.value)
|
||
const item = this.data.recTypes[idx]
|
||
if (!item) return
|
||
this.setData({
|
||
recType: item.key,
|
||
selectedLabel: item.label,
|
||
activeType: item
|
||
})
|
||
this.fetchRecommend()
|
||
},
|
||
|
||
onTopKChange(e) {
|
||
const idx = Number(e.detail.value)
|
||
const item = this.data.topKOptions[idx]
|
||
if (!item) return
|
||
this.setData({ topK: item.value, topKLabel: item.label })
|
||
this.fetchRecommend()
|
||
},
|
||
|
||
onGoalChange(e) {
|
||
const idx = Number(e.detail.value)
|
||
const item = this.data.goalOptions[idx]
|
||
if (!item) return
|
||
this.setData({ goalValue: item.value, goalLabel: item.label })
|
||
this.fetchRecommend()
|
||
},
|
||
|
||
onOccupationChange(e) {
|
||
const idx = Number(e.detail.value)
|
||
const item = this.data.occupationOptions[idx]
|
||
if (!item) return
|
||
this.setData({ occupationValue: item.value, occupationLabel: item.label })
|
||
this.fetchRecommend()
|
||
},
|
||
|
||
buildUrl() {
|
||
const topK = this.data.topK || 5
|
||
const type = this.data.recType
|
||
if (type === 'current') return `/recommend/current?top_k=${topK}`
|
||
if (type === 'health') return `/recommend/health?top_k=${topK}`
|
||
if (type === 'history') return `/recommend/plan/history?top_k=${topK}`
|
||
if (type === 'goal') {
|
||
const extra = this.data.goalValue ? `&goal=${encodeURIComponent(this.data.goalValue)}` : ''
|
||
return `/recommend/plan/goal?top_k=${topK}${extra}`
|
||
}
|
||
const occ = this.data.occupationValue ? `&occupation=${encodeURIComponent(this.data.occupationValue)}` : ''
|
||
return `/recommend/plan/occupation?top_k=${topK}${occ}`
|
||
},
|
||
|
||
buildSummary(items = []) {
|
||
if (!items.length) return null
|
||
const totalCalories = items.reduce((sum, row) => sum + Number(row.calories || 0), 0)
|
||
const totalProtein = items.reduce((sum, row) => sum + Number(row.protein || 0), 0)
|
||
return {
|
||
count: items.length,
|
||
avgCalories: Math.round((totalCalories / items.length) * 10) / 10,
|
||
avgProtein: Math.round((totalProtein / items.length) * 10) / 10
|
||
}
|
||
},
|
||
|
||
formatNow() {
|
||
const d = new Date()
|
||
const pad = (n) => String(n).padStart(2, '0')
|
||
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||
},
|
||
|
||
async fetchRecommend() {
|
||
this.setData({ loading: true, errorText: '' })
|
||
try {
|
||
const payload = await request({ url: this.buildUrl() })
|
||
const items = payload && Array.isArray(payload.items) ? payload.items : []
|
||
const normalizedItems = items.map((row, idx) => {
|
||
const score = Number(row.rf_score ?? row.score ?? 0)
|
||
return {
|
||
...row,
|
||
rank: idx + 1,
|
||
displayScore: score.toFixed(1)
|
||
}
|
||
})
|
||
this.setData({
|
||
payload: { ...payload, items: normalizedItems },
|
||
summary: this.buildSummary(normalizedItems),
|
||
lastUpdatedAt: this.formatNow(),
|
||
errorText: ''
|
||
})
|
||
} catch (err) {
|
||
this.setData({
|
||
payload: null,
|
||
summary: null,
|
||
errorText: (err && err.message) || '推荐请求失败,请稍后重试'
|
||
})
|
||
} finally {
|
||
this.setData({ loading: false })
|
||
}
|
||
}
|
||
})
|