Files
c/miniprogram/pages/recommend/index.js
刘正航 b5237f9038 1
2026-04-21 22:45:19 +08:00

188 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 })
}
}
})