-
+ 👤 运动员信息
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
- ⚖️ 摄入量 ({{ currentUnit }})
+
-
+
+
+
+
-
-
+ 📋 今日摄入清单
- {{ foodList.length }} 项 +🤖 自动评价
++ {{ autoEvaluation ? '评价内容由系统根据配置自动生成' : '系统已计算各类摄入量,请对照填写评价' }} +
-
-
-
- 🍃
- 暂无食物记录,请从上方选择食物添加
-
-
+
-
-
- {{ item.name }}
- {{ item.amount }} g
-
-
-
-
-
-
-
-
- 📡
- 待发送后端的 JSON 数据
-
-
+
+
+
+
- {{ prettyJson }}
{{ prettyJson }}
-
+
+
@@ -171,136 +164,468 @@
-
\ No newline at end of file
+
+.modal-preview {
+ max-width: 900px !important;
+ width: 90vw;
+ max-height: 85vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.preview-body {
+ max-height: 60vh;
+ overflow-y: auto;
+ padding: 1rem;
+}
+
+.excel-preview {
+ font-size: 0.85rem;
+}
+
+.preview-sheet {
+ margin-bottom: 1.5rem;
+}
+
+.sheet-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: #2d6a4f;
+ margin-bottom: 0.5rem;
+ padding-bottom: 0.3rem;
+ border-bottom: 2px solid #2d6a4f;
+}
+
+.table-wrapper {
+ overflow-x: auto;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+}
+
+.preview-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.8rem;
+}
+
+.preview-table td {
+ padding: 0.4rem 0.6rem;
+ border: 1px solid #e0e0e0;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.preview-table td.header-cell {
+ background-color: #4F75B5;
+ color: white;
+ font-weight: 600;
+}
+
+.preview-table tr:nth-child(even) td {
+ background-color: #f8f9fa;
+}
+
+.preview-table tr:hover td {
+ background-color: #e8f4ea;
+}
+
diff --git a/src/components/ManualEvaluationForm.vue b/src/components/ManualEvaluationForm.vue
new file mode 100644
index 0000000..236e39c
--- /dev/null
+++ b/src/components/ManualEvaluationForm.vue
@@ -0,0 +1,148 @@
+
+
- ⚠️
-
- 确认删除
+ 📊 +Excel 预览
-
确定要删除这条食物记录吗?
-- 食物:{{ getFoodNameById(deletingFoodId) }} -
+
+
+
+
+ {{ sheetName }}
+
+
+
+
+
+ | + {{ cell }} + | +
-
-
+
+
+
+
diff --git a/src/components/UserInfoForm.vue b/src/components/UserInfoForm.vue
new file mode 100644
index 0000000..58fac4d
--- /dev/null
+++ b/src/components/UserInfoForm.vue
@@ -0,0 +1,137 @@
+
+ ✍️ 手动填写评价
++ 下方已根据当前食物记录自动计算摄入量,请对照数值选择评价(不足 / 适宜 / 偏高,或选其他后自行填写)。 +
+ +⏳ 正在计算摄入量...
+ {{ previewError }}
+ 请先添加食物记录
+
+
+
+
+
+ 食物分类评价
+
+
+
+ 种类
+ 摄入量
+ 评价
+
+
+ {{ cat }}
+ {{ formatCategoryIntake(cat) }}
+
+
+
+
+
+ 整体评估及建议
+ +
+
+
+ 能量及钙摄入量评价
+
+
+
+ 项目
+ 摄入量
+ 评价
+
+
+ 总能量
+ {{ formatEnergyIntake() }}
+
+
+
+ 钙
+ {{ formatCalciumIntake() }}
+
+
+
+
+
+ 解读
+ +
+
+
+
+
diff --git a/src/services/api.js b/src/services/api.js
new file mode 100644
index 0000000..d1e0dfc
--- /dev/null
+++ b/src/services/api.js
@@ -0,0 +1,50 @@
+const BASE_URL = '/api/trophic'
+const REQUEST_TIMEOUT = 15000
+
+const LIQUID_CATEGORIES = ['饮料', '奶类', '酒类']
+
+function inferUnit(categoryName) {
+ return LIQUID_CATEGORIES.includes(categoryName) ? 'ml' : 'g'
+}
+
+async function fetchWithTimeout(url, options = {}) {
+ const controller = new AbortController()
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT)
+ try {
+ const response = await fetch(url, { ...options, signal: controller.signal })
+ return response
+ } finally {
+ clearTimeout(timer)
+ }
+}
+
+export async function fetchCategories(name = '') {
+ const params = new URLSearchParams({ pageNum: '1', pageSize: '50' })
+ if (name) params.set('name', name)
+ const response = await fetchWithTimeout(`${BASE_URL}/category?${params}`)
+ const json = await response.json()
+ if (json.code !== 200) throw new Error(json.message || '获取分类失败')
+ return json.data.records.map(r => ({
+ id: r.id,
+ name: r.name
+ }))
+}
+
+export async function fetchFoodsByCategory(categoryId, categoryName, name = '') {
+ const params = new URLSearchParams({ categoryId, pageNum: '1', pageSize: '200' })
+ if (name) params.set('name', name)
+ const response = await fetchWithTimeout(`${BASE_URL}/goodsInfo?${params}`)
+ const json = await response.json()
+ if (json.code !== 200) throw new Error(json.message || '获取食物失败')
+ return json.data.records.map(r => ({
+ id: r.id,
+ name: r.name,
+ categoryId: r.categoryId,
+ unit: inferUnit(categoryName),
+ energy: r.energy,
+ protein: r.protein,
+ fat: r.fat,
+ carbohydrate: r.carbohydrate,
+ calcium: r.calcium
+ }))
+}
diff --git a/src/services/reportApi.js b/src/services/reportApi.js
new file mode 100644
index 0000000..cc56794
--- /dev/null
+++ b/src/services/reportApi.js
@@ -0,0 +1,16 @@
+const REPORT_BASE = 'http://localhost:5000/api'
+
+export async function fetchIntakePreview(payload) {
+ const response = await fetch(`${REPORT_BASE}/preview-intake`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ })
+
+ if (!response.ok) {
+ const err = await response.json().catch(() => ({}))
+ throw new Error(err.message || '摄入量计算失败')
+ }
+
+ return response.json()
+}
diff --git a/src/utils/energyEer.js b/src/utils/energyEer.js
new file mode 100644
index 0000000..8d6d183
--- /dev/null
+++ b/src/utils/energyEer.js
@@ -0,0 +1,46 @@
+import eerConfig from '../../config/female_energy_eer.json'
+
+function safeFloat(value, defaultValue = -1) {
+ const n = parseFloat(value)
+ return isNaN(n) ? defaultValue : n
+}
+
+export function lookupFemaleEnergyEer(user) {
+ if (!user) return null
+
+ const gender = user.gender || user.userGender
+ if (gender !== 'female') return null
+
+ const age = safeFloat(user.age ?? user.userAge)
+ const weight = safeFloat(user.weight ?? user.userWeight, 0)
+ const activity = String(user.activityLevel ?? user.userActivityLevel ?? '').trim()
+
+ if (age < 0 || !activity) return null
+
+ const ageGroup = eerConfig.age_groups.find(
+ g => g.age_min <= age && age < g.age_max
+ )
+ if (!ageGroup) return null
+
+ const levelData = ageGroup.levels[activity]
+ if (!levelData) return null
+
+ const activityLabel = eerConfig.activity_levels[activity]?.label || ''
+
+ if (levelData.type === 'per_kg') {
+ if (weight <= 0) return null
+ return {
+ kcal: Math.round(levelData.kcal * weight * 10) / 10,
+ ageLabel: ageGroup.label,
+ activityLabel,
+ note: `${levelData.kcal} kcal/kg·d`
+ }
+ }
+
+ return {
+ kcal: levelData.kcal,
+ ageLabel: ageGroup.label,
+ activityLabel,
+ note: `${ageGroup.label} · ${activityLabel}`
+ }
+}
diff --git a/src/utils/manualEvaluation.js b/src/utils/manualEvaluation.js
new file mode 100644
index 0000000..9eab5c4
--- /dev/null
+++ b/src/utils/manualEvaluation.js
@@ -0,0 +1,77 @@
+import nutritionTemplate from '../../config/nutrition_template.json'
+
+export const REPORT_CATEGORIES = Object.keys(nutritionTemplate.food_category_map)
+export const EVAL_PRESET_OPTIONS = ['不足', '适宜', '偏高']
+export const CHOICE_OTHER = '__other__'
+
+function emptyChoice() {
+ return { choice: '', other: '' }
+}
+
+export function createEmptyManualEvaluations() {
+ const categoryEvaluations = {}
+ REPORT_CATEGORIES.forEach(cat => {
+ categoryEvaluations[cat] = emptyChoice()
+ })
+ return {
+ categoryEvaluations,
+ overallSuggestions: '',
+ energyEvaluation: emptyChoice(),
+ calciumEvaluation: emptyChoice(),
+ interpretation: ''
+ }
+}
+
+export function resolveEvalValue(field) {
+ if (!field || !field.choice) return ''
+ if (field.choice === CHOICE_OTHER) {
+ return (field.other || '').trim()
+ }
+ return field.choice
+}
+
+/** 转为后端可用的纯文本格式 */
+export function resolveManualEvaluationsForSubmit(manual) {
+ const categoryEvaluations = {}
+ REPORT_CATEGORIES.forEach(cat => {
+ categoryEvaluations[cat] = resolveEvalValue(manual.categoryEvaluations[cat])
+ })
+ return {
+ categoryEvaluations,
+ overallSuggestions: manual.overallSuggestions || '',
+ energyEvaluation: resolveEvalValue(manual.energyEvaluation),
+ calciumEvaluation: resolveEvalValue(manual.calciumEvaluation),
+ interpretation: manual.interpretation || ''
+ }
+}
+
+function validateChoiceField(field, label, errors) {
+ if (!field?.choice) {
+ errors.push(`请选择「${label}」的评价`)
+ return
+ }
+ if (field.choice === CHOICE_OTHER && !field.other?.trim()) {
+ errors.push(`请填写「${label}」的其他评价内容`)
+ }
+}
+
+export function validateManualEvaluations(manual) {
+ const errors = []
+
+ REPORT_CATEGORIES.forEach(cat => {
+ validateChoiceField(manual.categoryEvaluations[cat], cat, errors)
+ })
+
+ if (!manual.overallSuggestions?.trim()) {
+ errors.push('请填写整体评估及建议')
+ }
+
+ validateChoiceField(manual.energyEvaluation, '总能量', errors)
+ validateChoiceField(manual.calciumEvaluation, '钙', errors)
+
+ if (!manual.interpretation?.trim()) {
+ errors.push('请填写解读内容')
+ }
+
+ return errors
+}
diff --git a/temp_pie.png b/temp_pie.png
index 5cecc93..40dd708 100644
Binary files a/temp_pie.png and b/temp_pie.png differ
diff --git a/vite.config.js b/vite.config.js
index 3b90a22..23b5f32 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -10,6 +10,15 @@ export default defineConfig({
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
+ },
+ server: {
+ proxy: {
+ '/api/trophic': {
+ target: 'https://survey.femalesportshealth.cn',
+ changeOrigin: true,
+ secure: false
+ }
+ }
}
})
👤 运动员信息
+
+
+
+
+
+
+
+
+ {{ validationErrors.userName }}
+
+
+
+
+
+
+ -- 请选择性别 --
+ 男
+ 女
+
+
+ {{ validationErrors.userGender }}
+
+
+
+
+
+
+
+ {{ validationErrors.userAge }}
+
+
+
+
+
+
+
+ {{ validationErrors.userHeight }}
+
+
+
+
+
+
+
+ {{ validationErrors.userWeight }}
+
+
+
+
+
+
+ -- 请选择运动量 --
+ I 级 · 低强度
+ II 级 · 中等强度
+ III 级 · 高强度
+
+
+ {{ validationErrors.userActivityLevel }}
+
+
+
+
+ 📊 BMI: {{ bmi }} ({{ bmiStatus }})
+
+
+
+ ⚡ 推荐能量摄入: {{ energyStandard }} kcal/天
+ ({{ energyStandardNote }})
+
+