v3 增加并优化了人工评价逻辑,增加了运动量(PAL)判断
This commit is contained in:
1 parent
86567eece5
commit
2e69000c5b
22 files changed
+2200
-282
No files matched your search
+11
-9
@@ -1,12 +1,3 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
@@ -14,6 +5,17 @@ dist-ssr
|
|||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.venv/
|
||||||
|
output/
|
||||||
|
temp_pie.png
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
/cypress/videos/
|
/cypress/videos/
|
||||||
/cypress/screenshots/
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,115 @@
|
|||||||
# vue-project
|
# 营养健康评估系统
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
运动员食物摄入记录与营养报告生成系统。前端使用 Vue 3 + Vite,后端使用 Flask,支持 PDF / Excel 报告导出。
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## 功能
|
||||||
|
|
||||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
- 运动员信息录入(姓名、性别、年龄、身高、体重、运动量 PAL)
|
||||||
|
- BMI 自动计算与展示
|
||||||
|
- 从 API 动态加载食物分类与食物列表
|
||||||
|
- 食物摄入记录与清单管理
|
||||||
|
- 自动评价 / 手动评价两种模式
|
||||||
|
- **自动评价**:根据 `config/nutrition_template.json` 及女性 EER 表自动生成评价
|
||||||
|
- **手动评价**:后端先计算各类摄入量,用户对照填写评价后生成报告
|
||||||
|
- PDF / Excel 营养健康评估报告
|
||||||
|
|
||||||
## Customize configuration
|
## 技术栈
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
| 层级 | 技术 |
|
||||||
|
|------|------|
|
||||||
|
| 前端 | Vue 3、Vite、xlsx |
|
||||||
|
| 后端 | Flask、ReportLab、Matplotlib、OpenPyXL |
|
||||||
|
| 数据 | `food_db.json`、配置文件 JSON |
|
||||||
|
|
||||||
## Project Setup
|
## 项目结构
|
||||||
|
|
||||||
```sh
|
```
|
||||||
npm install
|
vue-project/
|
||||||
|
├── app.py # Flask 后端入口
|
||||||
|
├── requirements.txt # Python 依赖
|
||||||
|
├── food_db.json # 本地食物营养数据库
|
||||||
|
├── config/
|
||||||
|
│ ├── nutrition_template.json # 分类评价规则与建议模板
|
||||||
|
│ └── female_energy_eer.json # 中国女性膳食能量需要量 (EER)
|
||||||
|
├── services/
|
||||||
|
│ ├── nutrition_service.py # 营养计算与评价逻辑
|
||||||
|
│ └── report_service.py # 报告生成调度
|
||||||
|
├── builder/
|
||||||
|
│ ├── pdf_builder.py # PDF 报告构建
|
||||||
|
│ └── excel_builder.py # Excel 报告构建
|
||||||
|
├── static/images/ # 报告分类图片
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # Vue 组件
|
||||||
|
│ ├── services/ # API 调用(食物 API、报告 API)
|
||||||
|
│ └── utils/ # BMI、手动评价等工具
|
||||||
|
└── output/reports/ # 生成的报告输出目录(运行时自动创建)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
## 环境要求
|
||||||
|
|
||||||
```sh
|
- Node.js 16+
|
||||||
|
- Python 3.9+
|
||||||
|
|
||||||
|
## 安装与启动
|
||||||
|
|
||||||
|
### 1. 后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-project/vue-project
|
||||||
|
|
||||||
|
# 创建虚拟环境(推荐)
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate # Windows
|
||||||
|
# source .venv/bin/activate # macOS / Linux
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
后端默认运行在 `http://localhost:5000`
|
||||||
|
|
||||||
|
### 2. 前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-project/vue-project
|
||||||
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and Minify for Production
|
前端默认运行在 `http://localhost:5173`
|
||||||
|
|
||||||
```sh
|
> 食物列表 API 通过 Vite 代理转发到 `https://survey.femalesportshealth.cn`,配置见 `vite.config.js`。
|
||||||
npm run build
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/preview-intake` | 预览各类摄入量与营养素合计(手动评价参考) |
|
||||||
|
| POST | `/api/generate-pdf` | 生成 PDF 报告 |
|
||||||
|
| POST | `/api/generate-excel` | 生成 Excel 报告 |
|
||||||
|
|
||||||
|
请求体为前端组装的 JSON,包含 `user`、`meals`,以及可选的 `autoEvaluation`、`manualEvaluations`。
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
- **`food_db.json`**:食物名称 → 营养数据映射,报告计算依赖此文件,API 返回的食物名称需与之匹配
|
||||||
|
- **`config/nutrition_template.json`**:食物分类摄入量评价区间、建议模板、钙/能量默认评价规则
|
||||||
|
- **`config/female_energy_eer.json`**:女性按年龄段与运动量 (PAL I/II/III) 的每日能量标准
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # 前端生产构建,输出到 dist/
|
||||||
|
npm run preview # 预览构建结果
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**报告里某些食物没有计入营养?**
|
||||||
|
检查 API 返回的食物名称是否在 `food_db.json` 中存在对应条目。
|
||||||
|
|
||||||
|
**手动评价看不到摄入量?**
|
||||||
|
确保后端已启动,`/api/preview-intake` 可正常访问。
|
||||||
|
|
||||||
|
**PDF 中文或饼图乱码?**
|
||||||
|
Windows 环境需安装 SimHei 字体(`C:/Windows/Fonts/simhei.ttf`)。
|
||||||
@@ -15,6 +15,24 @@ CORS(app, resources={
|
|||||||
report_service = NutritionReportService()
|
report_service = NutritionReportService()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/preview-intake", methods=["POST"])
|
||||||
|
def preview_intake():
|
||||||
|
try:
|
||||||
|
front_json = request.get_json()
|
||||||
|
if not front_json:
|
||||||
|
return jsonify({"message": "没有收到 JSON 数据"}), 400
|
||||||
|
|
||||||
|
preview = report_service.preview_intake(front_json)
|
||||||
|
return jsonify(preview)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("预览计算报错:", e)
|
||||||
|
return jsonify({
|
||||||
|
"message": "计算失败",
|
||||||
|
"error": str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/generate-pdf", methods=["POST"])
|
@app.route("/api/generate-pdf", methods=["POST"])
|
||||||
def generate_pdf():
|
def generate_pdf():
|
||||||
try:
|
try:
|
||||||
|
|||||||
+19
-2
@@ -121,7 +121,7 @@ class NutritionPDFBuilder:
|
|||||||
|
|
||||||
[
|
[
|
||||||
Paragraph("总能量", normal),
|
Paragraph("总能量", normal),
|
||||||
Paragraph(f"{n['energy']['value']} kcal/day", normal),
|
Paragraph(self._format_energy_intake(n["energy"]), normal),
|
||||||
Paragraph(n["energy"]["evaluation"], normal)
|
Paragraph(n["energy"]["evaluation"], normal)
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -138,12 +138,22 @@ class NutritionPDFBuilder:
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
# ===== 解读(适中间距)=====
|
# ===== 解读 =====
|
||||||
|
if data.get("interpretation"):
|
||||||
|
text = "<br/>".join(
|
||||||
|
line.strip()
|
||||||
|
for line in data["interpretation"].splitlines()
|
||||||
|
if line.strip()
|
||||||
|
)
|
||||||
|
else:
|
||||||
interpretation = []
|
interpretation = []
|
||||||
|
|
||||||
if n["energy"]["evaluation"] == "不足":
|
if n["energy"]["evaluation"] == "不足":
|
||||||
interpretation.append("能量摄入不足,应增加碳水化合物摄入")
|
interpretation.append("能量摄入不足,应增加碳水化合物摄入")
|
||||||
|
|
||||||
|
if n["energy"]["evaluation"] == "偏高":
|
||||||
|
interpretation.append("能量摄入偏高,应适当控制总能量摄入")
|
||||||
|
|
||||||
if n["calcium"]["evaluation"] == "不足":
|
if n["calcium"]["evaluation"] == "不足":
|
||||||
interpretation.append("钙摄入不足,应增加奶制品摄入")
|
interpretation.append("钙摄入不足,应增加奶制品摄入")
|
||||||
|
|
||||||
@@ -180,6 +190,13 @@ class NutritionPDFBuilder:
|
|||||||
|
|
||||||
doc.build(story)
|
doc.build(story)
|
||||||
|
|
||||||
|
def _format_energy_intake(self, energy_info):
|
||||||
|
value = energy_info.get("value", "-")
|
||||||
|
standard = energy_info.get("standard")
|
||||||
|
if standard:
|
||||||
|
return f"{value} kcal/day<br/>(标准 {standard} kcal)"
|
||||||
|
return f"{value} kcal/day"
|
||||||
|
|
||||||
# ================= 图片 =================
|
# ================= 图片 =================
|
||||||
def _get_image(self, category):
|
def _get_image(self, category):
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
{
|
||||||
|
"title": "表 5-0-2 中国女性膳食能量需要量 (EER)",
|
||||||
|
"activity_levels": {
|
||||||
|
"1": { "label": "低强度", "description": "PAL I" },
|
||||||
|
"2": { "label": "中等强度", "description": "PAL II" },
|
||||||
|
"3": { "label": "高强度", "description": "PAL III" }
|
||||||
|
},
|
||||||
|
"age_groups": [
|
||||||
|
{
|
||||||
|
"age_min": 0,
|
||||||
|
"age_max": 0.5,
|
||||||
|
"label": "0岁~",
|
||||||
|
"levels": {
|
||||||
|
"2": { "type": "per_kg", "kcal": 90, "mj": 0.38 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 0.5,
|
||||||
|
"age_max": 1,
|
||||||
|
"label": "0.5岁~",
|
||||||
|
"levels": {
|
||||||
|
"2": { "type": "per_kg", "kcal": 75, "mj": 0.31 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 1,
|
||||||
|
"age_max": 2,
|
||||||
|
"label": "1岁~",
|
||||||
|
"levels": { "2": { "type": "fixed", "kcal": 800, "mj": 3.35 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 2,
|
||||||
|
"age_max": 3,
|
||||||
|
"label": "2岁~",
|
||||||
|
"levels": { "2": { "type": "fixed", "kcal": 1000, "mj": 4.18 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 3,
|
||||||
|
"age_max": 4,
|
||||||
|
"label": "3岁~",
|
||||||
|
"levels": { "2": { "type": "fixed", "kcal": 1150, "mj": 4.81 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 4,
|
||||||
|
"age_max": 5,
|
||||||
|
"label": "4岁~",
|
||||||
|
"levels": { "2": { "type": "fixed", "kcal": 1250, "mj": 5.23 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 5,
|
||||||
|
"age_max": 6,
|
||||||
|
"label": "5岁~",
|
||||||
|
"levels": { "2": { "type": "fixed", "kcal": 1300, "mj": 5.44 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 6,
|
||||||
|
"age_max": 7,
|
||||||
|
"label": "6岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1300, "mj": 5.44 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1450, "mj": 6.07 },
|
||||||
|
"3": { "type": "fixed", "kcal": 1650, "mj": 6.90 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 7,
|
||||||
|
"age_max": 8,
|
||||||
|
"label": "7岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1350, "mj": 5.65 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1550, "mj": 6.49 },
|
||||||
|
"3": { "type": "fixed", "kcal": 1750, "mj": 7.32 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 8,
|
||||||
|
"age_max": 9,
|
||||||
|
"label": "8岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1450, "mj": 6.07 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1700, "mj": 7.11 },
|
||||||
|
"3": { "type": "fixed", "kcal": 1900, "mj": 7.95 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 9,
|
||||||
|
"age_max": 10,
|
||||||
|
"label": "9岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1550, "mj": 6.49 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1800, "mj": 7.53 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2000, "mj": 8.37 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 10,
|
||||||
|
"age_max": 11,
|
||||||
|
"label": "10岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1650, "mj": 6.90 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1900, "mj": 7.95 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2100, "mj": 8.79 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 11,
|
||||||
|
"age_max": 12,
|
||||||
|
"label": "11岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1750, "mj": 7.32 },
|
||||||
|
"2": { "type": "fixed", "kcal": 2000, "mj": 8.37 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2250, "mj": 9.41 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 12,
|
||||||
|
"age_max": 15,
|
||||||
|
"label": "12岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1950, "mj": 8.16 },
|
||||||
|
"2": { "type": "fixed", "kcal": 2200, "mj": 9.20 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2450, "mj": 10.25 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 15,
|
||||||
|
"age_max": 18,
|
||||||
|
"label": "15岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 2100, "mj": 8.79 },
|
||||||
|
"2": { "type": "fixed", "kcal": 2350, "mj": 9.83 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2650, "mj": 11.09 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 18,
|
||||||
|
"age_max": 30,
|
||||||
|
"label": "18岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1700, "mj": 7.11 },
|
||||||
|
"2": { "type": "fixed", "kcal": 2100, "mj": 8.79 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2450, "mj": 10.25 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 30,
|
||||||
|
"age_max": 50,
|
||||||
|
"label": "30岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1700, "mj": 7.11 },
|
||||||
|
"2": { "type": "fixed", "kcal": 2050, "mj": 8.58 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2400, "mj": 10.04 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 50,
|
||||||
|
"age_max": 65,
|
||||||
|
"label": "50岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1600, "mj": 6.69 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1950, "mj": 8.16 },
|
||||||
|
"3": { "type": "fixed", "kcal": 2300, "mj": 9.62 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 65,
|
||||||
|
"age_max": 75,
|
||||||
|
"label": "65岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1550, "mj": 6.49 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1850, "mj": 7.74 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age_min": 75,
|
||||||
|
"age_max": 150,
|
||||||
|
"label": "75岁~",
|
||||||
|
"levels": {
|
||||||
|
"1": { "type": "fixed", "kcal": 1500, "mj": 6.28 },
|
||||||
|
"2": { "type": "fixed", "kcal": 1750, "mj": 7.32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"physiological_stages": {
|
||||||
|
"early_pregnancy": { "label": "孕早期", "bonus_kcal": 0, "bonus_mj": 0 },
|
||||||
|
"mid_pregnancy": { "label": "孕中期", "bonus_kcal": 250, "bonus_mj": 1.05 },
|
||||||
|
"late_pregnancy": { "label": "孕晚期", "bonus_kcal": 400, "bonus_mj": 1.67 },
|
||||||
|
"lactating": { "label": "乳母", "bonus_kcal": 400, "bonus_mj": 1.67 }
|
||||||
|
},
|
||||||
|
"evaluation_rules": {
|
||||||
|
"under_ratio": 0.9,
|
||||||
|
"over_ratio": 1.1,
|
||||||
|
"under_label": "不足",
|
||||||
|
"adequate_label": "适宜",
|
||||||
|
"over_label": "偏高"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite App</title>
|
<title>营养健康评估</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Generated
+105
-1
@@ -8,7 +8,8 @@
|
|||||||
"name": "vue-project",
|
"name": "vue-project",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.2.38"
|
"vue": "^3.2.38",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^3.0.3",
|
"@vitejs/plugin-vue": "^3.0.3",
|
||||||
@@ -198,6 +199,49 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz",
|
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz",
|
||||||
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="
|
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -576,6 +620,15 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -732,6 +785,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
@@ -812,6 +877,45 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"preview": "vite preview --port 4173"
|
"preview": "vite preview --port 4173"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.2.38"
|
"vue": "^3.2.38",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^3.0.3",
|
"@vitejs/plugin-vue": "^3.0.3",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Web API
|
||||||
|
Flask==3.1.3
|
||||||
|
flask-cors==6.0.2
|
||||||
|
|
||||||
|
# PDF 报告
|
||||||
|
reportlab==4.4.10
|
||||||
|
matplotlib==3.9.4
|
||||||
|
Pillow==11.3.0
|
||||||
|
|
||||||
|
# Excel 报告
|
||||||
|
openpyxl==3.1.5
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
@@ -10,23 +11,85 @@ class NutritionService:
|
|||||||
with open(config_path, "r", encoding="utf-8") as f:
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
self.config = json.load(f)
|
self.config = json.load(f)
|
||||||
|
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
eer_path = os.path.join(base_dir, "config", "female_energy_eer.json")
|
||||||
|
with open(eer_path, "r", encoding="utf-8") as f:
|
||||||
|
self.female_eer = json.load(f)
|
||||||
|
|
||||||
def build_report_data(self, front_json):
|
def build_report_data(self, front_json):
|
||||||
|
user = front_json.get("user", {})
|
||||||
meals = front_json.get("meals", [])
|
meals = front_json.get("meals", [])
|
||||||
|
auto_evaluation = front_json.get("autoEvaluation", True)
|
||||||
|
|
||||||
merged_meals = self._merge_meals(meals)
|
merged_meals = self._merge_meals(meals)
|
||||||
food_details, nutrition_total = self._calculate_nutrition(merged_meals)
|
food_details, nutrition_total = self._calculate_nutrition(merged_meals, user)
|
||||||
category_result = self._build_category_result_from_foods(food_details)
|
category_result = self._build_category_result_from_foods(food_details)
|
||||||
suggestions = self._generate_suggestions(category_result)
|
suggestions = self._generate_suggestions(category_result)
|
||||||
|
interpretation = None
|
||||||
|
|
||||||
return {
|
if not auto_evaluation:
|
||||||
"user": front_json.get("user", {}),
|
manual = front_json.get("manualEvaluations") or {}
|
||||||
|
category_result, suggestions, nutrition_total, interpretation = (
|
||||||
|
self._apply_manual_evaluations(
|
||||||
|
category_result, suggestions, nutrition_total, manual
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"user": user,
|
||||||
"date": front_json.get("date"),
|
"date": front_json.get("date"),
|
||||||
"foods": food_details,
|
"foods": food_details,
|
||||||
"category_result": category_result,
|
"category_result": category_result,
|
||||||
"suggestions": suggestions,
|
"suggestions": suggestions,
|
||||||
"nutrition": nutrition_total
|
"nutrition": nutrition_total,
|
||||||
|
"autoEvaluation": auto_evaluation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if interpretation is not None:
|
||||||
|
report["interpretation"] = interpretation
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
def build_intake_preview(self, front_json):
|
||||||
|
"""复用营养计算逻辑,返回各类摄入量与营养素合计(供手动评价参考)"""
|
||||||
|
user = front_json.get("user", {})
|
||||||
|
meals = front_json.get("meals", [])
|
||||||
|
|
||||||
|
merged_meals = self._merge_meals(meals)
|
||||||
|
food_details, nutrition_total = self._calculate_nutrition(merged_meals, user)
|
||||||
|
category_result = self._build_category_result_from_foods(food_details)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"category_result": category_result,
|
||||||
|
"nutrition": nutrition_total,
|
||||||
|
"foods": food_details,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _apply_manual_evaluations(self, category_result, suggestions, nutrition_total, manual):
|
||||||
|
category_evals = manual.get("categoryEvaluations") or {}
|
||||||
|
for cat, info in category_result.items():
|
||||||
|
if cat in category_evals and str(category_evals[cat]).strip():
|
||||||
|
info["evaluation"] = str(category_evals[cat]).strip()
|
||||||
|
|
||||||
|
overall = str(manual.get("overallSuggestions", "")).strip()
|
||||||
|
suggestions = [
|
||||||
|
line.strip()
|
||||||
|
for line in overall.splitlines()
|
||||||
|
if line.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
energy_eval = str(manual.get("energyEvaluation", "")).strip()
|
||||||
|
if energy_eval:
|
||||||
|
nutrition_total["energy"]["evaluation"] = energy_eval
|
||||||
|
|
||||||
|
calcium_eval = str(manual.get("calciumEvaluation", "")).strip()
|
||||||
|
if calcium_eval:
|
||||||
|
nutrition_total["calcium"]["evaluation"] = calcium_eval
|
||||||
|
|
||||||
|
interpretation = str(manual.get("interpretation", "")).strip()
|
||||||
|
|
||||||
|
return category_result, suggestions, nutrition_total, interpretation
|
||||||
|
|
||||||
def _merge_meals(self, meals):
|
def _merge_meals(self, meals):
|
||||||
meal_sum = defaultdict(float)
|
meal_sum = defaultdict(float)
|
||||||
|
|
||||||
@@ -46,7 +109,7 @@ class NutritionService:
|
|||||||
|
|
||||||
return meal_sum
|
return meal_sum
|
||||||
|
|
||||||
def _calculate_nutrition(self, merged_meals):
|
def _calculate_nutrition(self, merged_meals, user=None):
|
||||||
details = []
|
details = []
|
||||||
|
|
||||||
total_energy = 0.0
|
total_energy = 0.0
|
||||||
@@ -59,7 +122,6 @@ class NutritionService:
|
|||||||
food_info = self.food_db.get(food_name)
|
food_info = self.food_db.get(food_name)
|
||||||
|
|
||||||
if not food_info:
|
if not food_info:
|
||||||
# 如果没匹配到食物库,可以先跳过
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
factor = grams / 100.0
|
factor = grams / 100.0
|
||||||
@@ -89,10 +151,18 @@ class NutritionService:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
energy_standard = self._lookup_energy_eer(user or {})
|
||||||
|
if energy_standard is not None:
|
||||||
|
energy_eval = self._eval_energy_against_standard(total_energy, energy_standard)
|
||||||
|
else:
|
||||||
|
energy_standard = None
|
||||||
|
energy_eval = self._eval_nutrition(total_energy, "energy")
|
||||||
|
|
||||||
nutrition_total = {
|
nutrition_total = {
|
||||||
"energy": {
|
"energy": {
|
||||||
"value": round(total_energy, 2),
|
"value": round(total_energy, 2),
|
||||||
"evaluation": self._eval_nutrition(total_energy, "energy")
|
"standard": energy_standard,
|
||||||
|
"evaluation": energy_eval
|
||||||
},
|
},
|
||||||
"protein": round(total_protein, 2),
|
"protein": round(total_protein, 2),
|
||||||
"fat": round(total_fat, 2),
|
"fat": round(total_fat, 2),
|
||||||
@@ -105,6 +175,66 @@ class NutritionService:
|
|||||||
|
|
||||||
return details, nutrition_total
|
return details, nutrition_total
|
||||||
|
|
||||||
|
def _lookup_energy_eer(self, user):
|
||||||
|
gender = str(user.get("gender", "")).strip()
|
||||||
|
if gender != "female":
|
||||||
|
return None
|
||||||
|
|
||||||
|
age = self._safe_float(user.get("age"), default=-1)
|
||||||
|
weight = self._safe_float(user.get("weight"))
|
||||||
|
activity = str(user.get("activityLevel", "")).strip()
|
||||||
|
|
||||||
|
if age < 0 or not activity:
|
||||||
|
return None
|
||||||
|
|
||||||
|
age_group = None
|
||||||
|
for group in self.female_eer.get("age_groups", []):
|
||||||
|
if group["age_min"] <= age < group["age_max"]:
|
||||||
|
age_group = group
|
||||||
|
break
|
||||||
|
|
||||||
|
if not age_group:
|
||||||
|
return None
|
||||||
|
|
||||||
|
level_data = age_group.get("levels", {}).get(activity)
|
||||||
|
if not level_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if level_data.get("type") == "per_kg":
|
||||||
|
if weight <= 0:
|
||||||
|
return None
|
||||||
|
return round(level_data["kcal"] * weight, 1)
|
||||||
|
|
||||||
|
standard = level_data["kcal"]
|
||||||
|
|
||||||
|
stage = str(user.get("physiologicalStage", "")).strip()
|
||||||
|
stages = self.female_eer.get("physiological_stages", {})
|
||||||
|
if stage and stage in stages:
|
||||||
|
standard += stages[stage].get("bonus_kcal", 0)
|
||||||
|
|
||||||
|
return standard
|
||||||
|
|
||||||
|
def _eval_energy_against_standard(self, value, standard):
|
||||||
|
rules = self.female_eer.get("evaluation_rules", {})
|
||||||
|
under_ratio = rules.get("under_ratio", 0.9)
|
||||||
|
over_ratio = rules.get("over_ratio", 1.1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
standard = float(standard)
|
||||||
|
except Exception:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
if standard <= 0:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
ratio = value / standard
|
||||||
|
if ratio < under_ratio:
|
||||||
|
return rules.get("under_label", "不足")
|
||||||
|
if ratio <= over_ratio:
|
||||||
|
return rules.get("adequate_label", "适宜")
|
||||||
|
return rules.get("over_label", "偏高")
|
||||||
|
|
||||||
def _build_category_result_from_foods(self, food_details):
|
def _build_category_result_from_foods(self, food_details):
|
||||||
# 1. 先把配置里的所有分类都初始化为 0
|
# 1. 先把配置里的所有分类都初始化为 0
|
||||||
category_sum = {
|
category_sum = {
|
||||||
|
|||||||
@@ -24,3 +24,6 @@ class NutritionReportService:
|
|||||||
report_data = self.nutrition_service.build_report_data(front_json)
|
report_data = self.nutrition_service.build_report_data(front_json)
|
||||||
self.excel_builder.build(report_data, output_path)
|
self.excel_builder.build(report_data, output_path)
|
||||||
return report_data
|
return report_data
|
||||||
|
|
||||||
|
def preview_intake(self, front_json):
|
||||||
|
return self.nutrition_service.build_intake_preview(front_json)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="eval-choice">
|
||||||
|
<div class="eval-options">
|
||||||
|
<label
|
||||||
|
v-for="opt in presetOptions"
|
||||||
|
:key="opt"
|
||||||
|
class="eval-option"
|
||||||
|
:class="{ active: modelValue.choice === opt }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:name="name"
|
||||||
|
:value="opt"
|
||||||
|
:checked="modelValue.choice === opt"
|
||||||
|
@change="selectChoice(opt)"
|
||||||
|
>
|
||||||
|
<span>{{ opt }}</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="eval-option"
|
||||||
|
:class="{ active: modelValue.choice === CHOICE_OTHER }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:name="name"
|
||||||
|
:value="CHOICE_OTHER"
|
||||||
|
:checked="modelValue.choice === CHOICE_OTHER"
|
||||||
|
@change="selectChoice(CHOICE_OTHER)"
|
||||||
|
>
|
||||||
|
<span>其他</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-if="modelValue.choice === CHOICE_OTHER"
|
||||||
|
type="text"
|
||||||
|
class="eval-other-input"
|
||||||
|
:value="modelValue.other"
|
||||||
|
placeholder="请填写其他评价"
|
||||||
|
@input="updateOther($event.target.value)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { EVAL_PRESET_OPTIONS, CHOICE_OTHER } from '../utils/manualEvaluation.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EvalChoice',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ choice: '', other: '' })
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
presetOptions: EVAL_PRESET_OPTIONS,
|
||||||
|
CHOICE_OTHER
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectChoice(choice) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
choice,
|
||||||
|
other: choice === CHOICE_OTHER ? this.modelValue.other : ''
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateOther(other) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
other
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="records-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>📋 今日摄入清单</h3>
|
||||||
|
<span class="badge">{{ foodList.length }} 项</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="foodList.length === 0" class="empty-state">
|
||||||
|
<div>🍃</div>
|
||||||
|
<p>暂无食物记录,请从上方选择食物添加</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="food-list">
|
||||||
|
<div v-for="item in foodList" :key="item.id" class="food-item">
|
||||||
|
<div class="food-info">
|
||||||
|
<span class="food-name">{{ item.name }}</span>
|
||||||
|
<span class="food-amount">{{ item.amount }} <span>{{ item.unit || 'g' }}</span></span>
|
||||||
|
</div>
|
||||||
|
<button @click="$emit('delete', item.id)" class="delete-btn">
|
||||||
|
🗑️ 删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showDeleteModal" class="modal-overlay" @click.self="closeDeleteModal">
|
||||||
|
<div class="modal-container">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-icon">⚠️</span>
|
||||||
|
<h3>确认删除</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>确定要删除这条食物记录吗?</p>
|
||||||
|
<p class="modal-food-info" v-if="deletingId">
|
||||||
|
食物:<strong>{{ deletingFoodName }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button @click="confirm" class="modal-btn modal-btn-danger">确认删除</button>
|
||||||
|
<button @click="closeDeleteModal" class="modal-btn modal-btn-secondary">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FoodList',
|
||||||
|
props: {
|
||||||
|
foodList: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
emits: ['delete'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showDeleteModal: false,
|
||||||
|
deletingId: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
deletingFoodName() {
|
||||||
|
if (!this.deletingId) return ''
|
||||||
|
const food = this.foodList.find(f => f.id === this.deletingId)
|
||||||
|
return food ? food.name : ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showConfirm(id) {
|
||||||
|
this.deletingId = id
|
||||||
|
this.showDeleteModal = true
|
||||||
|
},
|
||||||
|
closeDeleteModal() {
|
||||||
|
this.showDeleteModal = false
|
||||||
|
this.deletingId = null
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
this.$emit('delete', this.deletingId)
|
||||||
|
this.closeDeleteModal()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
+949
-247
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<div class="manual-eval-section">
|
||||||
|
<h3 class="section-subtitle">✍️ 手动填写评价</h3>
|
||||||
|
<p class="manual-eval-hint">
|
||||||
|
下方已根据当前食物记录自动计算摄入量,请对照数值选择评价(不足 / 适宜 / 偏高,或选其他后自行填写)。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="loading" class="preview-status">⏳ 正在计算摄入量...</div>
|
||||||
|
<div v-else-if="previewError" class="preview-status preview-error">{{ previewError }}</div>
|
||||||
|
<div v-else-if="!intakePreview" class="preview-status">请先添加食物记录</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="manual-eval-block">
|
||||||
|
<h4>食物分类评价</h4>
|
||||||
|
<div class="eval-table eval-table-choice">
|
||||||
|
<div class="eval-table-header">
|
||||||
|
<span>种类</span>
|
||||||
|
<span>摄入量</span>
|
||||||
|
<span>评价</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="cat in categories" :key="cat" class="eval-table-row">
|
||||||
|
<span class="eval-label">{{ cat }}</span>
|
||||||
|
<span class="eval-intake">{{ formatCategoryIntake(cat) }}</span>
|
||||||
|
<EvalChoice
|
||||||
|
:name="`cat-${cat}`"
|
||||||
|
:model-value="modelValue.categoryEvaluations[cat]"
|
||||||
|
@update:model-value="updateCategory(cat, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manual-eval-block">
|
||||||
|
<h4>整体评估及建议</h4>
|
||||||
|
<textarea
|
||||||
|
:value="modelValue.overallSuggestions"
|
||||||
|
rows="4"
|
||||||
|
placeholder="每行一条建议,例如: 增加蔬菜摄入量 保持奶类摄入"
|
||||||
|
@input="updateField('overallSuggestions', $event.target.value)"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manual-eval-block">
|
||||||
|
<h4>能量及钙摄入量评价</h4>
|
||||||
|
<div class="eval-table eval-table-choice">
|
||||||
|
<div class="eval-table-header">
|
||||||
|
<span>项目</span>
|
||||||
|
<span>摄入量</span>
|
||||||
|
<span>评价</span>
|
||||||
|
</div>
|
||||||
|
<div class="eval-table-row">
|
||||||
|
<span class="eval-label">总能量</span>
|
||||||
|
<span class="eval-intake">{{ formatEnergyIntake() }}</span>
|
||||||
|
<EvalChoice
|
||||||
|
name="energy-eval"
|
||||||
|
:model-value="modelValue.energyEvaluation"
|
||||||
|
@update:model-value="updateField('energyEvaluation', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="eval-table-row">
|
||||||
|
<span class="eval-label">钙</span>
|
||||||
|
<span class="eval-intake">{{ formatCalciumIntake() }}</span>
|
||||||
|
<EvalChoice
|
||||||
|
name="calcium-eval"
|
||||||
|
:model-value="modelValue.calciumEvaluation"
|
||||||
|
@update:model-value="updateField('calciumEvaluation', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manual-eval-block">
|
||||||
|
<h4>解读</h4>
|
||||||
|
<textarea
|
||||||
|
:value="modelValue.interpretation"
|
||||||
|
rows="3"
|
||||||
|
placeholder="填写能量及钙摄入的解读说明,每行一条"
|
||||||
|
@input="updateField('interpretation', $event.target.value)"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EvalChoice from './EvalChoice.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ManualEvaluationForm',
|
||||||
|
components: { EvalChoice },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
intakePreview: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
previewError: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
methods: {
|
||||||
|
updateField(key, value) {
|
||||||
|
this.$emit('update:modelValue', { ...this.modelValue, [key]: value })
|
||||||
|
},
|
||||||
|
updateCategory(cat, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
categoryEvaluations: {
|
||||||
|
...this.modelValue.categoryEvaluations,
|
||||||
|
[cat]: value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatCategoryIntake(cat) {
|
||||||
|
const info = this.intakePreview?.category_result?.[cat]
|
||||||
|
if (!info) return '-'
|
||||||
|
return `${info.value} g`
|
||||||
|
},
|
||||||
|
formatEnergyIntake() {
|
||||||
|
const energy = this.intakePreview?.nutrition?.energy
|
||||||
|
if (!energy) return '-'
|
||||||
|
const value = energy.value ?? '-'
|
||||||
|
if (energy.standard) {
|
||||||
|
return `${value} kcal/天(标准 ${energy.standard} kcal)`
|
||||||
|
}
|
||||||
|
return `${value} kcal/天`
|
||||||
|
},
|
||||||
|
formatCalciumIntake() {
|
||||||
|
const calcium = this.intakePreview?.nutrition?.calcium
|
||||||
|
if (!calcium) return '-'
|
||||||
|
return `${calcium.value ?? '-'} mg/天`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-section">
|
||||||
|
<h3 class="section-subtitle">👤 运动员信息</h3>
|
||||||
|
<div class="user-info-grid">
|
||||||
|
|
||||||
|
<div class="input-field">
|
||||||
|
<label>📛 姓名</label>
|
||||||
|
<input type="text" v-model="name" placeholder="例如:张三"
|
||||||
|
:class="{ 'input-error': touched.userName && validationErrors.userName }"
|
||||||
|
@blur="$emit('blur', 'userName')">
|
||||||
|
<span v-if="touched.userName && validationErrors.userName" class="error-message">
|
||||||
|
{{ validationErrors.userName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-field">
|
||||||
|
<label>👥 性别</label>
|
||||||
|
<select v-model="gender" class="select-field"
|
||||||
|
:class="{ 'input-error': touched.userGender && validationErrors.userGender }"
|
||||||
|
@change="$emit('blur', 'userGender')">
|
||||||
|
<option value="" disabled>-- 请选择性别 --</option>
|
||||||
|
<option value="male">男</option>
|
||||||
|
<option value="female">女</option>
|
||||||
|
</select>
|
||||||
|
<span v-if="touched.userGender && validationErrors.userGender" class="error-message">
|
||||||
|
{{ validationErrors.userGender }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-field">
|
||||||
|
<label>🎂 年龄 (岁)</label>
|
||||||
|
<input type="number" v-model.number="age" placeholder="例如:20"
|
||||||
|
min="1" max="120" step="1"
|
||||||
|
:class="{ 'input-error': touched.userAge && validationErrors.userAge }"
|
||||||
|
@blur="$emit('blur', 'userAge')">
|
||||||
|
<span v-if="touched.userAge && validationErrors.userAge" class="error-message">
|
||||||
|
{{ validationErrors.userAge }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-field">
|
||||||
|
<label>📏 身高 (cm)</label>
|
||||||
|
<input type="number" v-model.number="height" placeholder="例如:180"
|
||||||
|
min="30" max="300" step="0.1"
|
||||||
|
:class="{ 'input-error': touched.userHeight && validationErrors.userHeight }"
|
||||||
|
@blur="$emit('blur', 'userHeight')">
|
||||||
|
<span v-if="touched.userHeight && validationErrors.userHeight" class="error-message">
|
||||||
|
{{ validationErrors.userHeight }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-field">
|
||||||
|
<label>⚖️ 体重 (kg)</label>
|
||||||
|
<input type="number" v-model.number="weight" placeholder="例如:70"
|
||||||
|
min="1" max="500" step="0.1"
|
||||||
|
:class="{ 'input-error': touched.userWeight && validationErrors.userWeight }"
|
||||||
|
@blur="$emit('blur', 'userWeight')">
|
||||||
|
<span v-if="touched.userWeight && validationErrors.userWeight" class="error-message">
|
||||||
|
{{ validationErrors.userWeight }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-field">
|
||||||
|
<label>🏃 运动量 (PAL)</label>
|
||||||
|
<select v-model="activityLevel" class="select-field"
|
||||||
|
:class="{ 'input-error': touched.userActivityLevel && validationErrors.userActivityLevel }"
|
||||||
|
@change="$emit('blur', 'userActivityLevel')">
|
||||||
|
<option value="" disabled>-- 请选择运动量 --</option>
|
||||||
|
<option value="1">I 级 · 低强度</option>
|
||||||
|
<option value="2">II 级 · 中等强度</option>
|
||||||
|
<option value="3">III 级 · 高强度</option>
|
||||||
|
</select>
|
||||||
|
<span v-if="touched.userActivityLevel && validationErrors.userActivityLevel" class="error-message">
|
||||||
|
{{ validationErrors.userActivityLevel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="bmi" class="bmi-info" :class="bmiClass">
|
||||||
|
<span>📊 BMI: {{ bmi }} ({{ bmiStatus }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="energyStandard" class="energy-standard-info">
|
||||||
|
<span>⚡ 推荐能量摄入: {{ energyStandard }} kcal/天</span>
|
||||||
|
<span v-if="energyStandardNote" class="energy-note">({{ energyStandardNote }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UserInfoForm',
|
||||||
|
props: {
|
||||||
|
userName: { type: String, default: '' },
|
||||||
|
userGender: { type: String, default: '' },
|
||||||
|
userAge: { type: [Number, String], default: null },
|
||||||
|
userHeight: { type: [Number, String], default: null },
|
||||||
|
userWeight: { type: [Number, String], default: null },
|
||||||
|
userActivityLevel: { type: [String, Number], default: '' },
|
||||||
|
validationErrors: { type: Object, default: () => ({}) },
|
||||||
|
touched: { type: Object, default: () => ({}) },
|
||||||
|
bmi: { type: [String, Number], default: '' },
|
||||||
|
bmiStatus: { type: String, default: '' },
|
||||||
|
bmiClass: { type: String, default: '' },
|
||||||
|
energyStandard: { type: [String, Number], default: null },
|
||||||
|
energyStandardNote: { type: String, default: '' },
|
||||||
|
},
|
||||||
|
emits: ['update:userName', 'update:userGender', 'update:userAge', 'update:userHeight', 'update:userWeight', 'update:userActivityLevel', 'blur'],
|
||||||
|
computed: {
|
||||||
|
name: {
|
||||||
|
get() { return this.userName },
|
||||||
|
set(v) { this.$emit('update:userName', v) }
|
||||||
|
},
|
||||||
|
gender: {
|
||||||
|
get() { return this.userGender },
|
||||||
|
set(v) { this.$emit('update:userGender', v); this.$emit('blur', 'userGender') }
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
get() { return this.userAge },
|
||||||
|
set(v) { this.$emit('update:userAge', v) }
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
get() { return this.userHeight },
|
||||||
|
set(v) { this.$emit('update:userHeight', v) }
|
||||||
|
},
|
||||||
|
weight: {
|
||||||
|
get() { return this.userWeight },
|
||||||
|
set(v) { this.$emit('update:userWeight', v) }
|
||||||
|
},
|
||||||
|
activityLevel: {
|
||||||
|
get() { return this.userActivityLevel },
|
||||||
|
set(v) { this.$emit('update:userActivityLevel', v); this.$emit('blur', 'userActivityLevel') }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
@@ -10,6 +10,15 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api/trophic': {
|
||||||
|
target: 'https://survey.femalesportshealth.cn',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user