初始化

This commit is contained in:
binghuai
2026-02-04 12:13:56 +08:00
commit 85db3f71d4
703 changed files with 73395 additions and 0 deletions

36
food_nutrition-main/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Flask
instance/
.webassets-cache

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -0,0 +1,46 @@
# 食物营养成分查询小册H5
## 小册介绍
本小册是通过AI集成开发环境工具Trae)结合从聚合数据JUHE.CN免费下载的食物营养成分数据库快速实现了一个简单的食物营养成分查询小册H5。(基本是与AI对话Trae自动实现的😁)
主要功能查询食物的每100克的营养成分信息如能量、蛋白质、脂肪、碳水化合物、维生素、矿物质等含量。
- Trae IDE介绍
Trae 是一款字节跳动推出了一款面向海外市场的AI编程工具。旨在改变您的工作方式通过协作帮助您提高工作效率运行更迅速。[【访问官网】](https://www.trae.ai)
- 聚合数据介绍:
一家综合性API数据流通服务商致力于为客户提供数据处理标准化技术服务和数据处理定制化技术服务。Juhe.CN平台提供很多免费的API和数据集下载。[【访问官网】](https://www.juhe.cn/market/product/id/11087)
##目录结构
```
├── backend 后端代码目录
│ ├── food_nutrition.db 食物营养成本SQLite3数据库文件可以从聚合数据下载最新的替换
│ ├── main.py
│ ├── food_api.py
│ └── requirements.txt
│ └── ......
├── frontend 前端代码目录
│ └── ......
```
## 运行前端服务(Vue.js)
```shell
cd frontend
npm install
npm run dev
```
## 运行后段服务(Python FastAPI)
```shell
cd backend
pip install -r requirements.txt
python -m uvicorn main:app --reload
```
## 浏览器访问
```
http://localhost:5173
```
## 最终效果图
![截图](3in1.png)

View File

@@ -0,0 +1,261 @@
from fastapi import APIRouter, HTTPException
import sqlite3
from typing import List
from pydantic import BaseModel
router = APIRouter()
class Food(BaseModel):
id: int
name: str
category_name: str
category_id: int
father_id: int
father_category_name: str
alias_name: str | None
english_name: str | None
edible_part: str | None
water: str | None
energy: str | None
protein: str | None
fat: str | None
cholesterol: str | None
ash: str | None
carbohydrate: str | None
dietary_fiber: str | None
carotene: str | None
vitamin_a: str | None
vitamin_e: str | None
thiamin: str | None
riboflavin: str | None
niacin: str | None
vitamin_c: str | None
calcium: str | None
phosphorus: str | None
potassium: str | None
sodium: str | None
magnesium: str | None
iron: str | None
zinc: str | None
selenium: str | None
copper: str | None
manganese: str | None
iodine: str | None
sfa: str | None
mufa: str | None
pufa: str | None
fatty_acids_total: str | None
@router.get("/api/foods", response_model=List[Food])
async def get_foods(category_id: int):
try:
conn = sqlite3.connect("food_nutrition.db")
cursor = conn.cursor()
# 查询食物数据并关联分类表获取分类名称
cursor.execute("""
SELECT f.id, f.food_name as name, c.title as category_name, f.cate_id, c.father_id, d.title as father_category_name,
f.alias_name, f.english_name, f.edible_part, f.water, f.energy, f.protein, f.fat, f.cholesterol, f.ash,
f.carbohydrate, f.dietary_fiber, f.carotene, f.vitamin_a, f.vitamin_e, f.thiamin, f.riboflavin, f.niacin,
f.vitamin_c, f.calcium, f.phosphorus, f.potassium, f.sodium, f.magnesium, f.iron, f.zinc, f.selenium,
f.copper, f.manganese, f.iodine, f.sfa, f.mufa, f.pufa, f.fatty_acids_total
FROM j_food_nutrition f
LEFT JOIN j_food_categories c ON f.cate_id = c.cate_id
LEFT JOIN j_food_categories d ON d.father_id = c.father_id and d.cate_id=0
WHERE f.cate_id = ?
""", (category_id,))
foods = cursor.fetchall()
conn.close()
return [
Food(
id=food[0],
name=food[1],
category_name=food[2],
category_id=food[3],
father_id=food[4],
father_category_name=food[5],
alias_name=food[6],
english_name=food[7],
edible_part=food[8],
water=food[9],
energy=food[10],
protein=food[11],
fat=food[12],
cholesterol=food[13],
ash=food[14],
carbohydrate=food[15],
dietary_fiber=food[16],
carotene=food[17],
vitamin_a=food[18],
vitamin_e=food[19],
thiamin=food[20],
riboflavin=food[21],
niacin=food[22],
vitamin_c=food[23],
calcium=food[24],
phosphorus=food[25],
potassium=food[26],
sodium=food[27],
magnesium=food[28],
iron=food[29],
zinc=food[30],
selenium=food[31],
copper=food[32],
manganese=food[33],
iodine=food[34],
sfa=food[35],
mufa=food[36],
pufa=food[37],
fatty_acids_total=food[38]
)
for food in foods
]
except sqlite3.Error as e:
raise HTTPException(status_code=500, detail=f"数据库错误: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}")
@router.get("/api/food", response_model=Food)
async def get_food(id: int):
try:
conn = sqlite3.connect("food_nutrition.db")
cursor = conn.cursor()
# 查询食物数据并关联分类表获取分类名称
cursor.execute("""
SELECT f.id, f.food_name as name, c.title as category_name, f.cate_id, c.father_id, d.title as father_category_name,
f.alias_name, f.english_name, f.edible_part, f.water, f.energy, f.protein, f.fat, f.cholesterol, f.ash,
f.carbohydrate, f.dietary_fiber, f.carotene, f.vitamin_a, f.vitamin_e, f.thiamin, f.riboflavin, f.niacin,
f.vitamin_c, f.calcium, f.phosphorus, f.potassium, f.sodium, f.magnesium, f.iron, f.zinc, f.selenium,
f.copper, f.manganese, f.iodine, f.sfa, f.mufa, f.pufa, f.fatty_acids_total
FROM j_food_nutrition f
LEFT JOIN j_food_categories c ON f.cate_id = c.cate_id
LEFT JOIN j_food_categories d ON d.father_id = c.father_id and d.cate_id=0
WHERE f.id = ?
""", (id,))
food = cursor.fetchone()
conn.close()
if food is None:
raise HTTPException(status_code=404, detail="未找到该食物")
return Food(
id=food[0],
name=food[1],
category_name=food[2],
category_id=food[3],
father_id=food[4],
father_category_name=food[5],
alias_name=food[6],
english_name=food[7],
edible_part=food[8],
water=food[9],
energy=food[10],
protein=food[11],
fat=food[12],
cholesterol=food[13],
ash=food[14],
carbohydrate=food[15],
dietary_fiber=food[16],
carotene=food[17],
vitamin_a=food[18],
vitamin_e=food[19],
thiamin=food[20],
riboflavin=food[21],
niacin=food[22],
vitamin_c=food[23],
calcium=food[24],
phosphorus=food[25],
potassium=food[26],
sodium=food[27],
magnesium=food[28],
iron=food[29],
zinc=food[30],
selenium=food[31],
copper=food[32],
manganese=food[33],
iodine=food[34],
sfa=food[35],
mufa=food[36],
pufa=food[37],
fatty_acids_total=food[38]
)
except sqlite3.Error as e:
raise HTTPException(status_code=500, detail=f"数据库错误: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}")
@router.get("/api/search", response_model=List[Food])
async def get_foods(keyword: str):
try:
conn = sqlite3.connect("food_nutrition.db")
cursor = conn.cursor()
# 查询食物数据并关联分类表获取分类名称
cursor.execute("""
SELECT f.id, f.food_name as name, c.title as category_name, f.cate_id, c.father_id, d.title as father_category_name,
f.alias_name, f.english_name, f.edible_part, f.water, f.energy, f.protein, f.fat, f.cholesterol, f.ash,
f.carbohydrate, f.dietary_fiber, f.carotene, f.vitamin_a, f.vitamin_e, f.thiamin, f.riboflavin, f.niacin,
f.vitamin_c, f.calcium, f.phosphorus, f.potassium, f.sodium, f.magnesium, f.iron, f.zinc, f.selenium,
f.copper, f.manganese, f.iodine, f.sfa, f.mufa, f.pufa, f.fatty_acids_total
FROM j_food_nutrition f
LEFT JOIN j_food_categories c ON f.cate_id = c.cate_id
LEFT JOIN j_food_categories d ON d.father_id = c.father_id and d.cate_id=0
WHERE f.food_name like ?
""", (f'%{keyword}%',))
foods = cursor.fetchall()
conn.close()
return [
Food(
id=food[0],
name=food[1],
category_name=food[2],
category_id=food[3],
father_id=food[4],
father_category_name=food[5],
alias_name=food[6],
english_name=food[7],
edible_part=food[8],
water=food[9],
energy=food[10],
protein=food[11],
fat=food[12],
cholesterol=food[13],
ash=food[14],
carbohydrate=food[15],
dietary_fiber=food[16],
carotene=food[17],
vitamin_a=food[18],
vitamin_e=food[19],
thiamin=food[20],
riboflavin=food[21],
niacin=food[22],
vitamin_c=food[23],
calcium=food[24],
phosphorus=food[25],
potassium=food[26],
sodium=food[27],
magnesium=food[28],
iron=food[29],
zinc=food[30],
selenium=food[31],
copper=food[32],
manganese=food[33],
iodine=food[34],
sfa=food[35],
mufa=food[36],
pufa=food[37],
fatty_acids_total=food[38]
)
for food in foods
]
except sqlite3.Error as e:
raise HTTPException(status_code=500, detail=f"数据库错误: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}")

Binary file not shown.

View File

@@ -0,0 +1,81 @@
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import sqlite3
from typing import List
from pydantic import BaseModel
from food_api import router as food_router
app = FastAPI()
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 在生产环境中应该设置具体的源
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 数据模型
class Category(BaseModel):
id: int
title: str
cate_id: int
father_id: int
# 注册食物API路由
app.include_router(food_router)
@app.get("/api/categoryinfo", response_model=List[Category])
async def get_categoryinfo(cate_id: int = 0):
try:
conn = sqlite3.connect("food_nutrition.db")
cursor = conn.cursor()
cursor.execute(
"SELECT id, title, cate_id, father_id FROM j_food_categories WHERE cate_id = 0 and father_id = ?",
(cate_id,)
)
categoryinfo = cursor.fetchall()
conn.close()
return [
Category(id=cat[0], title=cat[1], cate_id=cat[2], father_id=cat[3])
for cat in categoryinfo
]
except sqlite3.Error as e:
raise HTTPException(status_code=500, detail=f"数据库错误: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}")
@app.get("/api/categories", response_model=List[Category])
async def get_categories(cate_id: int = 0, father_id: int = None):
try:
# 连接数据库
conn = sqlite3.connect("food_nutrition.db")
cursor = conn.cursor()
# 查询分类数据
if father_id is not None:
cursor.execute(
"SELECT id, title, cate_id,father_id FROM j_food_categories WHERE cate_id<>0 and father_id = ?",
(father_id,)
)
else:
cursor.execute(
"SELECT id, title, cate_id,father_id FROM j_food_categories WHERE cate_id = ?",
(cate_id,)
)
categories = cursor.fetchall()
# 关闭数据库连接
conn.close()
# 转换为响应格式
return [
Category(id=cat[0], title=cat[1], cate_id=cat[2], father_id=cat[3])
for cat in categories
]
except sqlite3.Error as e:
raise HTTPException(status_code=500, detail=f"数据库错误: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}")

View File

@@ -0,0 +1,3 @@
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.1

24
food_nutrition-main/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>食物营养成分查询小册</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"element-plus": "^2.9.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.5"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,19 @@
<script setup>
import BottomNav from './components/BottomNav.vue'
import TopNav from './components/TopNav.vue'
</script>
<template>
<TopNav />
<router-view></router-view>
<BottomNav />
</template>
<style>
#app {
max-width: 1280px;
margin: 0 auto;
padding: 1rem;
padding-bottom: 0;
}
</style>

View File

@@ -0,0 +1,10 @@
<svg width="1920" height="200" viewBox="0 0 1920 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1920" height="200" fill="#409EFF"/>
<path d="M0 120C320 120 480 160 800 160C1120 160 1280 120 1600 120C1920 120 1920 160 1920 160V200H0V120Z" fill="#2E8AFF" fill-opacity="0.3"/>
<path d="M0 140C320 140 480 180 800 180C1120 180 1280 140 1600 140C1920 140 1920 180 1920 180V200H0V140Z" fill="#2E8AFF" fill-opacity="0.2"/>
<circle cx="200" cy="60" r="4" fill="white" fill-opacity="0.3"/>
<circle cx="600" cy="40" r="3" fill="white" fill-opacity="0.3"/>
<circle cx="1000" cy="80" r="5" fill="white" fill-opacity="0.3"/>
<circle cx="1400" cy="30" r="3" fill="white" fill-opacity="0.3"/>
<circle cx="1800" cy="70" r="4" fill="white" fill-opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 785 B

View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="14" fill="#FFE5D0"/>
<path d="M10 14C10 11.7909 11.7909 10 14 10H18C20.2091 10 22 11.7909 22 14V18C22 20.2091 20.2091 22 18 22H14C11.7909 22 10 20.2091 10 18V14Z" fill="#FFA94D"/>
<path d="M13 16C13 14.8954 13.8954 14 15 14H17C18.1046 14 19 14.8954 19 16V16C19 17.1046 18.1046 18 17 18H15C13.8954 18 13 17.1046 13 16V16Z" fill="#FF922B"/>
</svg>

After

Width:  |  Height:  |  Size: 474 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 1L3 9H8L7.5 15L13 7H8L8.5 1Z" fill="#FFB800" stroke="#FFB800" stroke-width="1" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3C10.5 3 9 4 9 6C9 8 10.5 9 12 9C13.5 9 15 8 15 6C15 4 13.5 3 12 3Z" fill="#FFD700"/>
<path d="M7 8C5.5 8 4 9 4 11C4 13 5.5 14 7 14C8.5 14 10 13 10 11C10 9 8.5 8 7 8Z" fill="#FFD700"/>
<path d="M17 8C15.5 8 14 9 14 11C14 13 15.5 14 17 14C18.5 14 20 13 20 11C20 9 18.5 8 17 8Z" fill="#FFD700"/>
<path d="M12 15L12 21" stroke="#8B4513" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 8C7 8 9 7 12 7C15 7 17 8 17 8C17 8 19 12 19 14C19 16 17 17 12 17C7 17 5 16 5 14C5 12 7 8 7 8Z" fill="#CD853F"/>
<path d="M9 11C9 11 10 10.5 12 10.5C14 10.5 15 11 15 11" stroke="#8B4513" stroke-width="1" stroke-linecap="round"/>
<circle cx="10" cy="13" r="1" fill="#8B4513"/>
<circle cx="14" cy="13" r="1" fill="#8B4513"/>
</svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="12" cy="12" rx="4" ry="6" fill="#8B4513"/>
<path d="M12 6C10 6 8.5 8 8.5 12C8.5 16 10 18 12 18" stroke="#CD853F" stroke-width="1"/>
<circle cx="10.5" cy="10" r="1" fill="#CD853F"/>
<circle cx="10.5" cy="14" r="1" fill="#CD853F"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4C10 4 8 5 8 8C8 11 10 12 12 12C14 12 16 11 16 8C16 5 14 4 12 4Z" fill="#90EE90"/>
<path d="M8 10C6 10 5 11 5 14C5 17 6 18 8 18C10 18 11 17 11 14C11 11 10 10 8 10Z" fill="#90EE90"/>
<path d="M16 10C14 10 13 11 13 14C13 17 14 18 16 18C18 18 19 17 19 14C19 11 18 10 16 10Z" fill="#90EE90"/>
<path d="M12 14L12 20" stroke="#FF6B6B" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5C8 5 5 7 5 10C5 13 8 14 12 14C16 14 19 13 19 10C19 7 16 5 12 5Z" fill="#DEB887"/>
<path d="M11 14L10 19" stroke="#8B4513" stroke-width="2" stroke-linecap="round"/>
<path d="M13 14L14 19" stroke="#8B4513" stroke-width="2" stroke-linecap="round"/>
<circle cx="9" cy="9" r="1" fill="#FFFFFF"/>
<circle cx="15" cy="9" r="1" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4C11 4 10 5 10 6C10 7 11 8 12 8C13 8 14 7 14 6C14 5 13 4 12 4Z" fill="#228B22"/>
<path d="M12 7C8 7 5 9 5 13C5 17 8 19 12 19C16 19 19 17 19 13C19 9 16 7 12 7Z" fill="#FF0000"/>
<path d="M12 7L12 5" stroke="#228B22" stroke-width="2" stroke-linecap="round"/>
<path d="M9 11C9 11 10 12 12 12C14 12 15 11 15 11" stroke="#8B0000" stroke-width="1" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5C9 5 7 6 7 8C7 10 9 11 12 11C15 11 17 10 17 8C17 6 15 5 12 5Z" fill="#D2691E"/>
<path d="M8 10C6 10 5 11 5 14C5 17 6 18 8 18C10 18 11 17 11 14C11 11 10 10 8 10Z" fill="#8B4513"/>
<path d="M16 10C14 10 13 11 13 14C13 17 14 18 16 18C18 18 19 17 19 14C19 11 18 10 16 10Z" fill="#8B4513"/>
<circle cx="10" cy="8" r="1" fill="#FFFFFF"/>
<circle cx="14" cy="8" r="1" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 8C6 8 8 7 12 7C16 7 18 8 18 8C18 8 20 10 20 12C20 14 18 15 12 15C6 15 4 14 4 12C4 10 6 8 6 8Z" fill="#FF6B6B"/>
<path d="M8 10C8 10 10 11 12 11C14 11 16 10 16 10" stroke="#8B0000" stroke-width="1" stroke-linecap="round"/>
<path d="M7 12L17 12" stroke="#8B0000" stroke-width="1" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5C9 5 7 7 7 10C7 13 9 15 12 15C15 15 17 13 17 10C17 7 15 5 12 5Z" fill="#FFE4B5"/>
<path d="M10 14L8 19" stroke="#DEB887" stroke-width="2" stroke-linecap="round"/>
<path d="M14 14L16 19" stroke="#DEB887" stroke-width="2" stroke-linecap="round"/>
<circle cx="10" cy="9" r="1" fill="#8B4513"/>
<circle cx="14" cy="9" r="1" fill="#8B4513"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 4H16V7C16 7 18 8 18 10V18C18 20 16 20 12 20C8 20 6 20 6 18V10C6 8 8 7 8 7V4Z" fill="#FFFFFF" stroke="#000000" stroke-width="1"/>
<path d="M8 4H16" stroke="#000000" stroke-width="1"/>
<path d="M10 12H14" stroke="#000000" stroke-width="1"/>
<path d="M10 15H14" stroke="#000000" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6C9 6 7 8 7 12C7 16 9 18 12 18C15 18 17 16 17 12C17 8 15 6 12 6Z" fill="#FFFFFF" stroke="#FFD700" stroke-width="1"/>
<circle cx="12" cy="12" r="3" fill="#FFD700"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12C5 12 8 8 12 8C16 8 19 12 19 12C19 12 16 16 12 16C8 16 5 12 5 12Z" fill="#87CEEB"/>
<circle cx="10" cy="12" r="1" fill="#000000"/>
<path d="M19 12L21 10M19 12L21 14" stroke="#87CEEB" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H14V6C14 6 16 7 16 9V18C16 20 14 20 12 20C10 20 8 20 8 18V9C8 7 10 6 10 6V4Z" fill="#FFB6C1" stroke="#FF69B4" stroke-width="1"/>
<path d="M11 8H13" stroke="#FF69B4" stroke-width="1"/>
<path d="M10 11H14" stroke="#FF69B4" stroke-width="1"/>
<circle cx="12" cy="15" r="2" fill="#FF69B4"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="8" fill="#D2B48C" stroke="#8B4513" stroke-width="1"/>
<circle cx="9" cy="10" r="1" fill="#8B4513"/>
<circle cx="15" cy="10" r="1" fill="#8B4513"/>
<path d="M9 14C9 14 10.5 15 12 15C13.5 15 15 14 15 14" stroke="#8B4513" stroke-width="1" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 8H18L17 18H7L6 8Z" fill="#FF4444" stroke="#CC0000" stroke-width="1"/>
<path d="M5 8H19V10H5V8Z" fill="#FF6666" stroke="#CC0000" stroke-width="1"/>
<path d="M8 12H16" stroke="white" stroke-width="1"/>
<path d="M8 14H16" stroke="white" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 6H16L15 18H9L8 6Z" fill="#87CEEB" stroke="#4682B4" stroke-width="1"/>
<path d="M7 6H17V8H7V6Z" fill="#B0E0E6" stroke="#4682B4" stroke-width="1"/>
<path d="M11 10C11 10 12 11 12 11C12 11 13 10 13 10" stroke="#4682B4" stroke-width="1"/>
<path d="M9 18H15V20H9V18Z" fill="#87CEEB" stroke="#4682B4" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 4H16L12 12V18H16V20H8V18H12V12L8 4Z" fill="#9370DB" stroke="#483D8B" stroke-width="1"/>
<path d="M9 6L15 6" stroke="#483D8B" stroke-width="1"/>
<circle cx="12" cy="8" r="1" fill="#483D8B"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 8C8 6 10 4 12 4C14 4 16 6 16 8C16 10 14 12 12 12C10 12 8 10 8 8Z" fill="#FFB6C1" stroke="#FF69B4" stroke-width="1"/>
<path d="M8 16C8 14 10 12 12 12C14 12 16 14 16 16C16 18 14 20 12 20C10 20 8 18 8 16Z" fill="#FFB6C1" stroke="#FF69B4" stroke-width="1"/>
<path d="M10 8H14" stroke="#FF69B4" stroke-width="1"/>
<path d="M10 16H14" stroke="#FF69B4" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H14V8C14 8 16 9 16 11V18C16 20 14 20 12 20C10 20 8 20 8 18V11C8 9 10 8 10 8V4Z" fill="#FFD700" stroke="#DAA520" stroke-width="1"/>
<path d="M11 6H13" stroke="#DAA520" stroke-width="1"/>
<path d="M10 10H14" stroke="#DAA520" stroke-width="1"/>
<path d="M9 14H15" stroke="#DAA520" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H14V6C14 6 16 7 16 9V18C16 20 14 20 12 20C10 20 8 20 8 18V9C8 7 10 6 10 6V4Z" fill="#CD5C5C" stroke="#8B0000" stroke-width="1"/>
<path d="M11 8H13" stroke="#8B0000" stroke-width="1"/>
<path d="M10 11H14" stroke="#8B0000" stroke-width="1"/>
<circle cx="12" cy="15" r="2" fill="#8B0000"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2C4.5 2 2 6 2 9C2 12 4.5 14 8 14C11.5 14 14 12 14 9C14 6 11.5 2 8 2Z" stroke="#34C759" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5C6.5 5 5 7 5 9C5 11 6.5 12 8 12" stroke="#34C759" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="7" stroke="#4A90E2" stroke-width="1.5"/>
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11" stroke="#4A90E2" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,111 @@
<script setup>
import { ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { House, Search, InfoFilled } from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
const activeTab = ref('/')
const handleTabClick = (tab) => {
router.push(tab)
}
watch(() => route.path, (newPath) => {
activeTab.value = newPath
}, { immediate: true })
</script>
<template>
<div class="bottom-nav">
<el-tabs v-model="activeTab" @tab-click="(tab) => handleTabClick(tab.props.name)" class="bottom-tabs">
<el-tab-pane label="首页" name="/">
<template #label>
<div class="tab-item">
<el-icon><House /></el-icon>
<span>首页</span>
</div>
</template>
</el-tab-pane>
<el-tab-pane label="食物搜索" name="/search">
<template #label>
<div class="tab-item">
<el-icon><Search /></el-icon>
<span>食物搜索</span>
</div>
</template>
</el-tab-pane>
<el-tab-pane label="关于小册" name="/about">
<template #label>
<div class="tab-item">
<el-icon><InfoFilled /></el-icon>
<span>关于小册</span>
</div>
</template>
</el-tab-pane>
</el-tabs>
</div>
</template>
<style scoped>
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
z-index: 100;
padding: 0;
}
.bottom-tabs {
width: 100%;
}
.bottom-tabs :deep(.el-tabs__nav-wrap),
.bottom-tabs :deep(.el-tabs__nav-scroll) {
padding-bottom: 0;
margin-bottom: 0;
}
.bottom-tabs :deep(.el-tabs__nav) {
width: 100%;
display: flex;
justify-content: space-around;
margin-bottom: 0;
}
.bottom-tabs :deep(.el-tabs__item) {
flex: 1;
text-align: center;
padding: 8px 0;
height: auto;
transition: all 0.3s ease;
}
.bottom-tabs :deep(.el-tabs__header) {
margin: 0;
}
.bottom-tabs :deep(.el-tabs__item.is-active) {
color: var(--el-color-primary);
transform: translateY(-2px);
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.tab-item .el-icon {
font-size: 20px;
}
.tab-item span {
font-size: 12px;
line-height: 1.2;
}
</style>

View File

@@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -0,0 +1,39 @@
<script setup>
import { ElDivider } from 'element-plus'
</script>
<template>
<div class="top-nav">
<div class="nav-content">
<h1 class="app-title">食物营养成分查询手册</h1>
</div>
</div>
</template>
<style scoped>
.top-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background-color: #409EFF;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nav-content {
max-width: 1280px;
margin: 0 auto;
padding: 0.8rem 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.app-title {
margin: 0;
font-size: 1.25rem;
color: white;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,11 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,41 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/search',
name: 'Search',
component: () => import('../views/Search.vue')
},
{
path: '/category/:id',
name: 'Category',
component: () => import('../views/Category.vue')
},
{
path: '/foods/:id',
name: 'FoodList',
component: () => import('../views/FoodList.vue')
},
{
path: '/food/:id',
name: 'Food',
component: () => import('../views/Food.vue')
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@@ -0,0 +1,79 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: #ffffff;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #f9f9f9;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,124 @@
<script setup>
import { ElCard, ElDivider } from 'element-plus'
</script>
<template>
<div class="about-container">
<el-card class="info-card">
<template #header>
<div class="card-header">
<span>数据来源</span>
</div>
</template>
<div class="card-content">
聚合数据<a href="https://www.juhe.cn/market/product/id/11087" target="_blank">JUHE.CN</a>
</div>
</el-card>
<el-card class="info-card">
<template #header>
<div class="card-header">
<span>AI开发工具</span>
</div>
</template>
<div class="card-content">
TRAE<a href="https://www.trae.ai/" target="_blank">TRAE.AI</a>
</div>
</el-card>
<el-card class="info-card">
<template #header>
<div class="card-header">
<span>技术栈</span>
</div>
</template>
<div class="card-content tech-stack">
<div class="tech-item">
<span class="tech-name">Vue 3</span>
<span class="tech-desc">前端框架</span>
</div>
<el-divider></el-divider>
<div class="tech-item">
<span class="tech-name">Element Plus</span>
<span class="tech-desc">UI组件库</span>
</div>
<el-divider></el-divider>
<div class="tech-item">
<span class="tech-name">Vite</span>
<span class="tech-desc">构建工具</span>
</div>
<el-divider></el-divider>
<div class="tech-item">
<span class="tech-name">FastAPI</span>
<span class="tech-desc">后端框架</span>
</div>
<el-divider></el-divider>
<div class="tech-item">
<span class="tech-name">SQLite</span>
<span class="tech-desc">数据库</span>
</div>
</div>
</el-card>
</div>
</template>
<style scoped>
.about-container {
width: 100%;
max-width: 480px;
min-width: 300px;
padding: 8px;
margin-bottom: 50px;
margin-top: 50px;
}
.page-title {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
font-size: 28px;
}
.info-card {
margin-bottom: 16px;
border-radius: 8px;
width: 100%;
}
.card-header {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
.card-content {
font-size: 16px;
color: #606266;
line-height: 1.6;
padding: 16px 0;
}
.tech-stack {
padding: 8px 0;
}
.tech-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.tech-name {
font-weight: 600;
color: #2c3e50;
}
.tech-desc {
color: #909399;
}
</style>

View File

@@ -0,0 +1,151 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElCard, ElRow, ElCol, ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const categories = ref([])
const loading = ref(false)
const parentCategory = ref('')
const fetchParentCategory = async () => {
try {
const response = await fetch(`/api/categoryinfo?cate_id=${route.params.id}`)
if (!response.ok) throw new Error('获取父级分类数据失败')
const data = await response.json()
if (data && data.length > 0) {
parentCategory.value = data[0].title
}
} catch (error) {
ElMessage.error(error.message)
console.error('获取父级分类数据错误:', error)
}
}
const fetchCategories = async () => {
loading.value = true
try {
const response = await fetch(`/api/categories?father_id=${route.params.id}`)
if (!response.ok) throw new Error('获取分类数据失败')
const data = await response.json()
categories.value = data.map(category => ({
id: category.id,
name: category.title,
cate_id: category.cate_id,
father_id: category.father_id,
icon: `/src/assets/f${category.father_id}.svg`
}))
} catch (error) {
ElMessage.error(error.message)
console.error('获取分类数据错误:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchCategories()
fetchParentCategory()
})
</script>
<template>
<div class="home">
<!--
<div class="banner">
<img src="../assets/banner.svg" alt="Banner" class="banner-image" />
<h1 class="title">{{ parentCategory }}</h1>
</div>
-->
<el-row :gutter="16" justify="start">
<el-col :xs="12" :sm="12" :md="12" :lg="12" v-for="category in categories" :key="category.id">
<el-card class="category-card" shadow="hover" @click="router.push(`/foods/${category.cate_id}`)">
<div class="category-content">
<img :src="category.icon" class="category-icon" alt="分类图标" />
<span class="category-name">{{ category.name }}</span>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped>
.home {
padding: 8px;
max-width: 480px;
margin: 50px auto;
overflow-x: hidden;
}
.el-row {
width: 100%;
}
.banner {
position: relative;
margin: -8px -8px 16px -8px;
}
.banner-image {
width: 100%;
height: auto;
display: block;
}
.title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
color: #2c3e50;
margin-bottom: 24px;
font-size: clamp(20px, 5vw, 32px);
}
.category-card {
margin-bottom: 8px;
cursor: pointer;
transition: transform 0.3s;
height: 140px;
}
.category-card:hover {
transform: translateY(-5px);
}
.category-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 12px;
width: 100%;
box-sizing: border-box;
}
.category-icon {
width: clamp(48px, 12vw, 64px);
height: clamp(48px, 12vw, 64px);
margin-bottom: 12px;
flex-shrink: 0;
display: block;
}
.category-name {
font-size: clamp(12px, 3vw, 14px);
color: #2c3e50;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
padding: 0 4px;
display: block;
margin: 0;
}
</style>

View File

@@ -0,0 +1,399 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElCard, ElDivider, ElSkeleton, ElMessage } from 'element-plus'
const route = useRoute()
const food = ref(null)
const loading = ref(false)
const fetchFoodDetail = async () => {
loading.value = true
try {
const response = await fetch(`/api/food?id=${route.params.id}`)
if (!response.ok) throw new Error('获取食物详情失败')
const data = await response.json()
food.value = {
...data,
icon: `/src/assets/f${data.father_id}.svg`
}
} catch (error) {
ElMessage.error(error.message)
console.error('获取食物详情错误:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchFoodDetail()
})
</script>
<template>
<div class="food-detail-page">
<el-skeleton :loading="loading" animated>
<template #template>
<div class="skeleton-content">
<el-skeleton-item variant="image" style="width: 240px; height: 240px" />
<el-skeleton-item variant="h1" style="width: 50%" />
<el-skeleton-item variant="text" style="width: 80%" />
<el-skeleton-item variant="text" style="width: 60%" />
</div>
</template>
<template #default>
<el-card v-if="food" class="food-detail-card">
<div class="food-header">
<img :src="food.icon" class="food-icon" alt="食物图标" />
<div class="food-title">
<h1>{{ food.name }}</h1>
<p class="category">{{ food.father_category_name }}{{ food.category_name }}</p>
</div>
</div>
<el-divider>
<span class="divider-title">基本营养成分</span><br/><span style="color:#666; font-size:12px;">(每100g)</span>
</el-divider>
<!-- 能量与相关成分 -->
<div class="nutrition-grid">
<div class="nutrition-item">
<img src="../assets/energy-icon.svg" alt="能量" />
<div class="nutrition-info">
<span class="label">能量 / Energy</span>
<span class="value">{{ food.energy }}</span>
</div>
</div>
<div class="nutrition-item">
<img src="../assets/protein-icon.svg" alt="蛋白质" />
<div class="nutrition-info">
<span class="label">蛋白质 / Protein</span>
<span class="value">{{ food.protein }}</span>
</div>
</div>
<div class="nutrition-item">
<img src="../assets/fat-icon.svg" alt="脂肪" />
<div class="nutrition-info">
<span class="label">脂肪 / Fat</span>
<span class="value">{{ food.fat }}</span>
</div>
</div>
<div class="nutrition-item">
<img src="../assets/carbs-icon.svg" alt="碳水化合物" />
<div class="nutrition-info">
<span class="label">碳水化合物 / CHO</span>
<span class="value">{{ food.carbohydrate }}</span>
</div>
</div>
</div>
<el-divider>
<span class="divider-title">维生素</span>
</el-divider>
<div class="detailed-nutrition">
<div class="nutrition-row">
<span class="label">维生素A / Vitamin A</span>
<span class="value">{{ food.vitamin_a }}</span>
</div>
<div class="nutrition-row">
<span class="label">维生素C / Vitamin C</span>
<span class="value">{{ food.vitamin_c }}</span>
</div>
<div class="nutrition-row">
<span class="label">维生素E / Vitamin E</span>
<span class="value">{{ food.vitamin_e }}</span>
</div>
<div class="nutrition-row">
<span class="label">硫胺素 / Thiamin</span>
<span class="value">{{ food.thiamin }}</span>
</div>
<div class="nutrition-row">
<span class="label">核黄素 / Riboflavin</span>
<span class="value">{{ food.riboflavin }}</span>
</div>
<div class="nutrition-row">
<span class="label">烟酸 / Niacin</span>
<span class="value">{{ food.niacin }}</span>
</div>
</div>
<el-divider>
<span class="divider-title">矿物质</span>
</el-divider>
<div class="detailed-nutrition">
<div class="nutrition-row">
<span class="label"> / Calcium</span>
<span class="value">{{ food.calcium }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Iron</span>
<span class="value">{{ food.iron }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Zinc</span>
<span class="value">{{ food.zinc }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Phosphorus</span>
<span class="value">{{ food.phosphorus }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Potassium</span>
<span class="value">{{ food.potassium }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Sodium</span>
<span class="value">{{ food.sodium }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Magnesium</span>
<span class="value">{{ food.magnesium }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Selenium</span>
<span class="value">{{ food.selenium }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Copper</span>
<span class="value">{{ food.copper }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Manganese</span>
<span class="value">{{ food.manganese }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Iodine</span>
<span class="value">{{ food.iodine }}</span>
</div>
</div>
<el-divider>
<span class="divider-title">脂肪酸</span>
</el-divider>
<div class="detailed-nutrition">
<div class="nutrition-row">
<span class="label">饱和脂肪酸 / SFA</span>
<span class="value">{{ food.sfa }}</span>
</div>
<div class="nutrition-row">
<span class="label">单不饱和脂肪酸 / MUFA</span>
<span class="value">{{ food.mufa }}</span>
</div>
<div class="nutrition-row">
<span class="label">多不饱和脂肪酸 / PUFA</span>
<span class="value">{{ food.pufa }}</span>
</div>
<div class="nutrition-row">
<span class="label">脂肪酸合计 / Total</span>
<span class="value">{{ food.fatty_acids_total }}</span>
</div>
</div>
<el-divider>
<span class="divider-title">详细营养信息</span>
</el-divider>
<div class="detailed-nutrition">
<div class="nutrition-row">
<span class="label">膳食纤维 / Dietary Fiber</span>
<span class="value">{{ food.fiber }}</span>
</div>
<div class="nutrition-row">
<span class="label">胆固醇 / Cholesterol</span>
<span class="value">{{ food.cholesterol }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Calcium</span>
<span class="value">{{ food.calcium }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Iron</span>
<span class="value">{{ food.iron }}</span>
</div>
<div class="nutrition-row">
<span class="label"> / Zinc</span>
<span class="value">{{ food.zinc }}</span>
</div>
<div class="nutrition-row">
<span class="label">维生素A / Vitamin A</span>
<span class="value">{{ food.vitamin_a }}</span>
</div>
<div class="nutrition-row">
<span class="label">维生素B1 / Vitamin B1</span>
<span class="value">{{ food.vitamin_b1 }}</span>
</div>
<div class="nutrition-row">
<span class="label">维生素B2 / Vitamin B2</span>
<span class="value">{{ food.vitamin_b2 }}</span>
</div>
<div class="nutrition-row">
<span class="label">维生素C / Vitamin C</span>
<span class="value">{{ food.vitamin_c }}</span>
</div>
</div>
<el-divider>
<span class="divider-title">备注说明</span>
</el-divider>
<div class="notes-section">
<div class="notes-content">
<div class="notes-item">"—"表示未检测</div>
<div class="notes-item">"Tr"表示未检出</div>
<div class="notes-item">"un"仅对维生素E和能量在计算时表示不能得出结果</div>
<div class="notes-item">"0"表示测定后修约的0值计算后的0值理论上估计的0值</div>
</div>
</div>
</el-card>
</template>
</el-skeleton>
</div>
</template>
<style scoped>
.food-detail-page {
max-width: 480px;
margin: 0 auto;
padding: 0;
box-sizing: border-box;
margin-bottom: 60px;
margin-top: 50px;
}
.skeleton-content {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
padding: 0px;
margin: 0 auto;
max-width: 100%;
}
.food-detail-card {
background-color: #fff;
border-radius: 8px;
padding: 0px;
width: 100%;
margin: 0 auto;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.container {
max-width: 100%;
padding: 0 8px;
margin: 0 auto;
}
.food-info {
padding: 16px;
margin: 0 -12px;
background-color: #fff;
}
.food-header {
margin-bottom: 24px;
}
.food-title {
font-size: clamp(24px, 6vw, 32px);
margin: 0 0 8px;
color: #2c3e50;
}
.food-title h1 {
font-size: 1.2em;
}
.food-title p.category {
font-size: 0.6em;
}
.food-category {
color: #666;
font-size: clamp(14px, 3.5vw, 16px);
}
.nutrition-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin: 16px 0;
}
.nutrition-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.nutrition-item img {
width: 32px;
height: 32px;
}
.nutrition-info {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 8px;
}
.nutrition-info .label {
text-align: left;
flex: 1;
}
.nutrition-info .value {
text-align: right;
min-width: 60px;
}
.detailed-nutrition .nutrition-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: #f8f9fa;
border-radius: 6px;
}
.detailed-nutrition .value {
min-width: 80px;
text-align: right;
}
.notes-section {
margin-top: 24px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.notes-title {
font-weight: 600;
color: #2c3e50;
margin-bottom: 12px;
}
.notes-content {
font-size: 14px;
color: #666;
line-height: 1.6;
}
.notes-item {
margin-bottom: 8px;
}
.label {
color: #666;
font-size: 14px;
}
.value {
font-weight: 500;
color: #2c3e50;
}
</style>

View File

@@ -0,0 +1,195 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElCard, ElRow, ElCol, ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const foods = ref([])
const loading = ref(false)
const fetchFoods = async () => {
loading.value = true
try {
const response = await fetch(`/api/foods?category_id=${route.params.id}`)
if (!response.ok) throw new Error('获取食物数据失败')
const data = await response.json()
foods.value = data.map(food => ({
id: food.id,
name: food.name,
category_name: food.category_name,
father_category_name: food.father_category_name,
icon: `/src/assets/f${food.father_id}.svg`,
energy: food.energy,
protein: food.protein,
fat: food.fat
}))
} catch (error) {
ElMessage.error(error.message)
console.error('获取食物数据错误:', error)
} finally {
loading.value = false
}
}
const goToFoodDetail = (foodId) => {
router.push(`/food/${foodId}`)
}
onMounted(() => {
fetchFoods()
})
</script>
<template>
<div class="food-list-page">
<!--
<div class="banner">
<img src="../assets/banner.svg" alt="Banner" class="banner-image" />
<h1 class="title">食物清单</h1>
</div>
-->
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="24" :lg="24" v-for="food in foods" :key="food.id">
<el-card class="food-card" shadow="hover" @click="goToFoodDetail(food.id)">
<div class="food-content">
<div style="display: flex; flex-direction: column; align-items: center;">
<img :src="food.icon" class="food-icon" alt="食物图标" />
<div class="unit-text">每100克</div>
</div>
<div class="text-content">
<h3 class="food-name">{{ food.name }}</h3>
<p class="category-name">{{ food.father_category_name }}{{ food.category_name }}</p>
<div class="nutrition-info">
<div class="nutrition-item">
<img src="../assets/energy-icon.svg" alt="能量" class="nutrition-icon" />
<span>{{ food.energy }}</span>
</div>
<div class="nutrition-item">
<img src="../assets/protein-icon.svg" alt="蛋白质" class="nutrition-icon" />
<span>{{ food.protein }}</span>
</div>
<div class="nutrition-item">
<img src="../assets/fat-icon.svg" alt="脂肪" class="nutrition-icon" />
<span>{{ food.fat }}</span>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped>
.food-list-page {
padding: 8px;
max-width: 480px; /* 修改这行,设置为移动端的典型宽度 */
overflow-x: hidden;
margin-bottom: 50px;
margin-top:50px;
}
.banner {
position: relative;
margin: -8px -8px 16px -8px;
}
.banner-image {
width: 100%;
height: auto;
display: block;
}
.title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
color: #2c3e50;
font-size: clamp(20px, 5vw, 32px);
}
.food-card {
margin-bottom: 12px;
cursor: pointer;
transition: transform 0.3s;
}
.food-card:hover {
transform: translateY(-5px);
}
.food-content {
display: flex;
align-items: flex-start;
text-align: left;
padding: 12px;
}
.food-icon {
width: clamp(48px, 12vw, 64px);
height: clamp(48px, 12vw, 64px);
margin-right: 16px;
flex-shrink: 0;
}
.unit-text {
font-size: 12px;
color: #666;
text-align: center;
margin: 4px 16px 0 0;
flex-shrink: 0;
width: clamp(48px, 12vw, 64px);
}
.text-content {
flex-grow: 1;
min-width: 0;
}
.food-name {
margin: 0 0 4px;
font-size: clamp(16px, 4vw, 18px);
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width:200px
}
.category-name {
color: #666;
font-size: clamp(14px, 3.5vw, 16px);
margin: 0 0 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nutrition-info {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.nutrition-item {
display: flex;
align-items: center;
gap: 4px;
background-color: #f5f7fa;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: #666;
}
.nutrition-icon {
width: 16px;
height: 16px;
}
</style>

View File

@@ -0,0 +1,132 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElCard, ElRow, ElCol, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const categories = ref([])
const loading = ref(false)
const fetchCategories = async () => {
loading.value = true
try {
const response = await fetch('/api/categories?cate_id=0')
if (!response.ok) throw new Error('获取分类数据失败')
const data = await response.json()
categories.value = data.map(category => ({
id: category.id,
name: category.title,
cate_id: category.cate_id,
father_id: category.father_id,
icon: `/src/assets/f${category.father_id}.svg`
}))
} catch (error) {
ElMessage.error(error.message)
console.error('获取分类数据错误:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchCategories()
})
</script>
<template>
<div class="home">
<!--<div class="banner">
<img src="../assets/banner.svg" alt="Banner" class="banner-image" />
<h1 class="title">食物营养成分查询手册</h1>
</div>-->
<el-row :gutter="8">
<el-col :xs="12" :sm="12" :md="12" :lg="12" v-for="category in categories" :key="category.id">
<el-card class="category-card" shadow="hover" @click="router.push(`/category/${category.father_id}`)">
<div class="category-content">
<img :src="category.icon" class="category-icon" alt="分类图标" />
<span class="category-name">{{ category.name }}</span>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped>
.home {
padding: 8px;
max-width: 480px;
overflow-x: hidden;
margin-bottom:50px;
margin-top:50px;
}
.banner {
position: relative;
margin: -8px -8px 16px -8px;
}
.banner-image {
width: 100%;
height: auto;
display: block;
}
.title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
color: #2c3e50;
margin-bottom: 24px;
font-size: clamp(20px, 5vw, 32px);
}
.category-card {
margin-bottom: 8px;
cursor: pointer;
transition: transform 0.3s;
height: 140px;
}
.category-card:hover {
transform: translateY(-5px);
}
.category-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 12px;
width: 100%;
box-sizing: border-box;
}
.category-icon {
width: clamp(48px, 12vw, 64px);
height: clamp(48px, 12vw, 64px);
margin-bottom: 12px;
flex-shrink: 0;
display: block;
}
.category-name {
font-size: clamp(12px, 3vw, 14px);
color: #2c3e50;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
padding: 0 4px;
display: block;
margin: 0;
}
</style>

View File

@@ -0,0 +1,223 @@
<script setup>
import { ref } from 'vue'
import { ElInput, ElCard, ElRow, ElCol, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const searchQuery = ref('')
const searchResults = ref([])
const loading = ref(false)
const handleSearch = async () => {
if (!searchQuery.value.trim()) return
loading.value = true
try {
const response = await fetch(`/api/search?keyword=${encodeURIComponent(searchQuery.value)}`)
if (!response.ok) throw new Error('搜索失败')
const data = await response.json()
searchResults.value = data.map(food => ({
id: food.id,
name: food.name,
category_name: food.category_name,
father_category_name: food.father_category_name,
icon: `/src/assets/f${food.father_id}.svg`,
energy: food.energy,
protein: food.protein,
fat: food.fat
}))
} catch (error) {
ElMessage.error(error.message)
console.error('搜索错误:', error)
} finally {
loading.value = false
}
}
const goToFoodDetail = (foodId) => {
router.push(`/food/${foodId}`)
}
</script>
<template>
<div class="search-page">
<!--
<div class="banner">
<img src="../assets/banner.svg" alt="Banner" class="banner-image" />
<h1 class="title">搜索食物</h1>
</div>
-->
<div class="search-box">
<el-input
v-model="searchQuery"
placeholder="食物名称关键词"
clearable
size="large"
@keyup.enter="handleSearch"
>
<template #append>
<el-button @click="handleSearch" :loading="loading">搜索</el-button>
</template>
</el-input>
</div>
<el-row :gutter="16" v-if="searchResults.length > 0">
<el-col :xs="24" :sm="24" :md="24" :lg="24" v-for="food in searchResults" :key="food.id">
<el-card class="food-card" shadow="hover" @click="goToFoodDetail(food.id)">
<div class="food-content">
<div style="display: flex; flex-direction: column; align-items: center;">
<img :src="food.icon" class="food-icon" alt="食物图标" />
<div class="unit-text">每100克</div>
</div>
<div class="text-content">
<h3 class="food-name">{{ food.name }}</h3>
<p class="category-name">{{ food.father_category_name }}{{ food.category_name }}</p>
<div class="nutrition-info">
<div class="nutrition-item">
<img src="../assets/energy-icon.svg" alt="能量" class="nutrition-icon" />
<span>{{ food.energy }}</span>
</div>
<div class="nutrition-item">
<img src="../assets/protein-icon.svg" alt="蛋白质" class="nutrition-icon" />
<span>{{ food.protein }}</span>
</div>
<div class="nutrition-item">
<img src="../assets/fat-icon.svg" alt="脂肪" class="nutrition-icon" />
<span>{{ food.fat }}</span>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped>
.search-page {
padding: 8px;
max-width: 480px;
overflow-x: hidden;
margin-bottom: 50px;
}
.banner {
position: relative;
margin: -8px -8px 16px -8px;
}
.banner-image {
width: 100%;
height: auto;
display: block;
}
.title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
color: #2c3e50;
font-size: clamp(20px, 5vw, 32px);
}
.search-box {
margin: 24px auto;
max-width: 800px;
padding: 0 20px;
}
.food-card {
margin-bottom: 12px;
cursor: pointer;
transition: transform 0.3s;
}
.food-card:hover {
transform: translateY(-5px);
}
.food-content {
display: flex;
align-items: flex-start;
text-align: left;
padding: 12px;
}
.food-icon {
width: clamp(48px, 12vw, 64px);
height: clamp(48px, 12vw, 64px);
margin-right: 16px;
flex-shrink: 0;
}
.unit-text {
font-size: 12px;
color: #666;
text-align: center;
margin: 4px 16px 0 0;
flex-shrink: 0;
width: clamp(48px, 12vw, 64px);
}
.text-content {
flex-grow: 1;
min-width: 0;
max-width: 198px;
}
.food-name {
margin: 0 0 4px;
font-size: 16px;
font-weight: 600;
color: #2c3e50;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.category-name {
margin: 0 0 8px;
font-size: 12px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nutrition-info {
display: flex;
gap: 16px;
}
.nutrition-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #666;
}
.nutrition-icon {
width: 16px;
height: 16px;
}
:deep(.el-input) {
--el-input-height: 56px;
font-size: 18px;
}
:deep(.el-input__wrapper) {
border-radius: 24px 0 0 24px;
}
:deep(.el-input-group__append) {
border-radius: 0 24px 24px 0;
padding: 0 32px;
font-size: 18px;
}
</style>

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0',
allowedHosts: [
'frpc.binghuai.xyz'
],
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true
}
}
}
})

6
food_nutrition-main/package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "food_nutrition-main",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}