feat: 首页仪表盘改造为深色科技风格 + Apple Health 步数同步

UI 改造:
- 首页采用深色科技风格 (Dark Tech)
- Tailwind CSS + Glass Morphism 毛玻璃效果
- 左侧边栏导航 (PC端) / 底部导航 (移动端)
- Material Symbols 图标 + Space Grotesk 字体
- Chart.js 深色主题适配

新功能:
- Apple Health 步数同步 API (/api/steps)
- 设置页面添加 iOS 快捷指令配置说明

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 16:57:13 +08:00
parent 3b1f7fb416
commit 02426e0a59
3 changed files with 685 additions and 488 deletions

View File

@@ -153,6 +153,9 @@ class DeepSeekVisionAnalyzer:
| GET | `/api/meals/nutrition` | 营养统计 |
| POST | `/api/exercise` | 添加运动记录 |
| GET | `/api/exercises` | 查询运动记录 |
| POST | `/api/steps` | 同步步数iOS 快捷指令调用) |
| GET | `/api/steps` | 查询步数记录 |
| GET | `/api/steps/today` | 获取今日步数 |
| POST | `/api/sleep` | 添加睡眠记录 |
| POST | `/api/weight` | 添加体重记录 |
| GET | `/api/config` | 获取用户配置 |
@@ -216,6 +219,9 @@ weight (id, user_id, date, weight, body_fat, notes)
-- 阅读记录
reading (id, user_id, date, book_title, pages, duration, notes)
-- 步数记录
steps (id, user_id, date, steps, distance, calories, source, synced_at)
-- 用户配置
user_config (id, user_id, height, weight, age, gender, activity_level, goal)
@@ -258,6 +264,11 @@ invite (id, code, created_by, used_by, used_at, expires_at)
## 最近更新
- **Apple Health 步数同步**:支持通过 iOS 快捷指令同步 Apple Health 步数数据
- 数据库:新增 `steps` 表存储每日步数
- API`/api/steps` (POST 同步, GET 查询), `/api/steps/today` (今日步数)
- 前端:运动页面显示今日步数卡片,设置页面提供 iOS 快捷指令配置说明
- **文字 AI 识别功能**:在饮食页面添加"文字AI识别"按钮,支持输入文字描述后自动识别卡路里
- 前端:`web/app.py` (第 5041 行按钮,第 5166 行函数)
- 后端:`/api/meal/recognize` 支持 `text` 参数

View File

@@ -200,6 +200,22 @@ def init_db():
)
""")
# 步数记录表
cursor.execute("""
CREATE TABLE IF NOT EXISTS steps (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT DEFAULT 1,
date DATE NOT NULL,
steps INT NOT NULL DEFAULT 0,
distance FLOAT,
calories INT,
source VARCHAR(50) DEFAULT 'apple_health',
synced_at DATETIME NOT NULL,
UNIQUE KEY unique_user_date (user_id, date),
INDEX idx_steps_date (date)
)
""")
# 创建默认管理员用户(仅在用户表为空时)
cursor.execute("SELECT COUNT(*) as count FROM users")
if cursor.fetchone()["count"] == 0:
@@ -277,6 +293,74 @@ def delete_exercise(exercise_id: int):
cursor.execute("DELETE FROM exercise WHERE id = %s", (exercise_id,))
# ===== 步数记录 =====
def add_or_update_steps(user_id: int, record_date: date, steps: int, distance: float = None, calories: int = None, source: str = "apple_health") -> int:
"""添加或更新步数记录(同一天只保留一条记录)"""
with get_connection() as (conn, cursor):
cursor.execute("""
INSERT INTO steps (user_id, date, steps, distance, calories, source, synced_at)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
steps = VALUES(steps),
distance = VALUES(distance),
calories = VALUES(calories),
source = VALUES(source),
synced_at = VALUES(synced_at)
""", (
user_id,
record_date.isoformat(),
steps,
distance,
calories,
source,
datetime.now().isoformat(),
))
return cursor.lastrowid
def get_steps(start_date: Optional[date] = None, end_date: Optional[date] = None, user_id: Optional[int] = None) -> list[dict]:
"""查询步数记录"""
with get_connection() as (conn, cursor):
query = "SELECT * FROM steps WHERE 1=1"
params = []
if user_id:
query += " AND user_id = %s"
params.append(user_id)
if start_date:
query += " AND date >= %s"
params.append(start_date.isoformat())
if end_date:
query += " AND date <= %s"
params.append(end_date.isoformat())
query += " ORDER BY date DESC"
cursor.execute(query, params)
return [
{
"id": row["id"],
"date": _parse_date(row["date"]).isoformat() if isinstance(row["date"], str) else row["date"].isoformat(),
"steps": row["steps"],
"distance": row["distance"],
"calories": row["calories"],
"source": row["source"],
"synced_at": row["synced_at"].isoformat() if row["synced_at"] else None,
}
for row in cursor.fetchall()
]
def get_today_steps(user_id: int) -> dict:
"""获取今日步数"""
today = date.today()
records = get_steps(start_date=today, end_date=today, user_id=user_id)
if records:
return records[0]
return {"date": today.isoformat(), "steps": 0, "distance": None, "calories": None}
# ===== 饮食记录 =====
def add_meal(meal: Meal, user_id: int = 1) -> int:

File diff suppressed because it is too large Load Diff