Files
DDUp/docs/plans/2026-01-23-implementation-plan.md
liweiliang0905@gmail.com b8ff1f0ee9 docs: 添加详细实施计划
包含 4 个阶段 20+ 个任务:
- 阶段1: MySQL 数据库迁移 (7 tasks)
- 阶段2: 用户权限增强 (3 tasks)
- 阶段3: H5 移动端适配 (12 tasks)
- 阶段4: 测试验证 (2 tasks)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 18:36:38 +08:00

25 KiB
Raw Blame History

Vitals H5/MySQL/权限增强 实施计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 将 Vitals 应用改造为 H5 移动端友好、MySQL 数据库、增强用户权限隔离

Architecture:

  • 数据库层mysql-connector-python 替换 sqlite3保持裸 SQL 查询结构
  • API 层:添加用户权限过滤,非管理员仅能访问自己数据
  • UI 层:响应式 CSS 适配移动端,底部 Tab 导航

Tech Stack: Python 3.10+, FastAPI, mysql-connector-python, 原生 CSS 媒体查询


阶段 1: MySQL 数据库迁移

Task 1.1: 添加 mysql-connector-python 依赖

Files:

  • Modify: pyproject.toml

Step 1: 添加依赖

pyproject.tomldependencies 列表中添加:

"mysql-connector-python>=8.0.0",

Step 2: 安装依赖

Run: pip install -e . Expected: Successfully installed mysql-connector-python

Step 3: Commit

git add pyproject.toml
git commit -m "deps: 添加 mysql-connector-python 依赖"

Task 1.2: 重构 database.py - 连接管理

Files:

  • Modify: src/vitals/core/database.py:1-40

Step 1: 添加 MySQL 导入和配置

将文件开头改为:

"""数据库操作 - 支持 MySQL"""

import json
import os
from contextlib import contextmanager
from datetime import date, time, datetime, timedelta
from pathlib import Path
from typing import Optional

import mysql.connector
from mysql.connector import pooling

from .models import Exercise, Meal, Sleep, Weight, UserConfig, User, Reading, Invite


# 数据库连接池(全局)
_connection_pool = None


def get_mysql_config() -> dict:
    """获取 MySQL 配置"""
    return {
        "host": os.environ.get("MYSQL_HOST", "localhost"),
        "port": int(os.environ.get("MYSQL_PORT", "3306")),
        "user": os.environ.get("MYSQL_USER", "vitals"),
        "password": os.environ.get("MYSQL_PASSWORD", ""),
        "database": os.environ.get("MYSQL_DATABASE", "vitals"),
    }


def init_connection_pool():
    """初始化数据库连接池"""
    global _connection_pool
    if _connection_pool is None:
        config = get_mysql_config()
        _connection_pool = pooling.MySQLConnectionPool(
            pool_name="vitals_pool",
            pool_size=5,
            pool_reset_session=True,
            **config
        )
    return _connection_pool


@contextmanager
def get_connection():
    """获取数据库连接"""
    pool = init_connection_pool()
    conn = pool.get_connection()
    try:
        cursor = conn.cursor(dictionary=True)
        yield conn, cursor
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        cursor.close()
        conn.close()

Step 2: 验证语法

Run: python -c "from vitals.core.database import get_connection; print('OK')" Expected: 语法正确(连接会失败因为还没有 MySQL 服务器,这是预期的)

Step 3: Commit

git add src/vitals/core/database.py
git commit -m "refactor: database.py 添加 MySQL 连接池管理"

Task 1.3: 重构 database.py - 建表语句

Files:

  • Modify: src/vitals/core/database.py - init_db() 函数

Step 1: 改写 init_db 函数

def init_db():
    """初始化数据库表"""
    with get_connection() as (conn, cursor):
        # 运动记录表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS exercise (
                id INT PRIMARY KEY AUTO_INCREMENT,
                user_id INT DEFAULT 1,
                date DATE NOT NULL,
                type VARCHAR(100) NOT NULL,
                duration INT NOT NULL,
                calories INT DEFAULT 0,
                distance FLOAT,
                heart_rate_avg INT,
                source VARCHAR(50) DEFAULT '手动',
                raw_data TEXT,
                notes TEXT,
                INDEX idx_exercise_date (date)
            )
        """)

        # 饮食记录表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS meal (
                id INT PRIMARY KEY AUTO_INCREMENT,
                user_id INT DEFAULT 1,
                date DATE NOT NULL,
                meal_type VARCHAR(50) NOT NULL,
                description TEXT,
                calories INT DEFAULT 0,
                protein FLOAT,
                carbs FLOAT,
                fat FLOAT,
                photo_path VARCHAR(500),
                food_items TEXT,
                INDEX idx_meal_date (date)
            )
        """)

        # 睡眠记录表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS sleep (
                id INT PRIMARY KEY AUTO_INCREMENT,
                user_id INT DEFAULT 1,
                date DATE NOT NULL,
                bedtime VARCHAR(20),
                wake_time VARCHAR(20),
                duration FLOAT NOT NULL,
                quality INT DEFAULT 3,
                deep_sleep_mins INT,
                source VARCHAR(50) DEFAULT '手动',
                notes TEXT,
                INDEX idx_sleep_date (date)
            )
        """)

        # 体重记录表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS weight (
                id INT PRIMARY KEY AUTO_INCREMENT,
                user_id INT DEFAULT 1,
                date DATE NOT NULL,
                weight_kg FLOAT NOT NULL,
                body_fat_pct FLOAT,
                muscle_mass FLOAT,
                notes TEXT,
                INDEX idx_weight_date (date)
            )
        """)

        # 用户配置表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS config (
                `key` VARCHAR(100) PRIMARY KEY,
                value TEXT
            )
        """)

        # 用户表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INT PRIMARY KEY AUTO_INCREMENT,
                name VARCHAR(255) NOT NULL UNIQUE,
                created_at DATETIME NOT NULL,
                is_active TINYINT DEFAULT 0,
                gender VARCHAR(10),
                height_cm FLOAT,
                weight_kg FLOAT,
                age INT,
                password_hash VARCHAR(255),
                email VARCHAR(255),
                is_admin TINYINT DEFAULT 0,
                is_disabled TINYINT DEFAULT 0
            )
        """)

        # 邀请码表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS invites (
                id INT PRIMARY KEY AUTO_INCREMENT,
                code VARCHAR(100) NOT NULL UNIQUE,
                created_by INT NOT NULL,
                used_by INT,
                created_at DATETIME NOT NULL,
                expires_at DATETIME
            )
        """)

        # 阅读记录表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS reading (
                id INT PRIMARY KEY AUTO_INCREMENT,
                user_id INT DEFAULT 1,
                date DATE NOT NULL,
                title VARCHAR(500) NOT NULL,
                author VARCHAR(255),
                cover_url VARCHAR(1000),
                duration INT DEFAULT 0,
                mood VARCHAR(50),
                notes TEXT,
                INDEX idx_reading_date (date)
            )
        """)

Step 2: Commit

git add src/vitals/core/database.py
git commit -m "refactor: init_db 改为 MySQL 建表语句"

Task 1.4: 重构 database.py - 所有查询占位符

Files:

  • Modify: src/vitals/core/database.py - 所有 CRUD 函数

Step 1: 批量替换占位符

将所有 SQL 查询中的 ? 替换为 %s

主要改动点(示例):

# 改前
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))

# 改后
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

Step 2: 修改所有函数的连接获取方式

将所有函数中的:

with get_connection() as conn:
    cursor = conn.cursor()

改为:

with get_connection() as (conn, cursor):

Step 3: 修改 row 访问方式

MySQL 返回 dictionary直接用 row["field"] 访问(与现有代码兼容)。

移除所有 conn.row_factory = sqlite3.Row 相关代码。

Step 4: 修改 lastrowid 获取

# MySQL 方式
cursor.execute(...)
return cursor.lastrowid

Step 5: Commit

git add src/vitals/core/database.py
git commit -m "refactor: 所有 SQL 查询改为 MySQL 语法"

Task 1.5: 移除 SQLite 特有代码

Files:

  • Modify: src/vitals/core/database.py

Step 1: 移除 SQLite 导入和函数

删除:

  • import sqlite3
  • get_db_path() 函数
  • PRAGMA 相关代码
  • migrate_auth_fields() 中的 SQLite 特有逻辑

Step 2: 简化 migrate_auth_fields

def migrate_auth_fields():
    """迁移:为现有 users 表添加认证字段MySQL 版本)"""
    with get_connection() as (conn, cursor):
        # 检查列是否存在MySQL 方式
        cursor.execute("""
            SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
            WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
        """)
        columns = [row["COLUMN_NAME"] for row in cursor.fetchall()]

        if "password_hash" not in columns:
            cursor.execute("ALTER TABLE users ADD COLUMN password_hash VARCHAR(255)")
        if "email" not in columns:
            cursor.execute("ALTER TABLE users ADD COLUMN email VARCHAR(255)")
        if "is_admin" not in columns:
            cursor.execute("ALTER TABLE users ADD COLUMN is_admin TINYINT DEFAULT 0")
        if "is_disabled" not in columns:
            cursor.execute("ALTER TABLE users ADD COLUMN is_disabled TINYINT DEFAULT 0")

Step 3: Commit

git add src/vitals/core/database.py
git commit -m "refactor: 移除 SQLite 特有代码,完成 MySQL 迁移"

Task 1.6: 创建数据迁移脚本

Files:

  • Create: scripts/migrate_sqlite_to_mysql.py

Step 1: 编写迁移脚本

#!/usr/bin/env python3
"""SQLite 到 MySQL 数据迁移脚本"""

import sqlite3
import os
from pathlib import Path

import mysql.connector


def get_sqlite_path() -> Path:
    """获取 SQLite 数据库路径"""
    env_path = os.environ.get("VITALS_DB_PATH")
    if env_path:
        return Path(env_path)
    return Path.home() / ".vitals" / "vitals.db"


def migrate():
    # SQLite 连接
    sqlite_path = get_sqlite_path()
    if not sqlite_path.exists():
        print(f"SQLite 数据库不存在: {sqlite_path}")
        return

    sqlite_conn = sqlite3.connect(sqlite_path)
    sqlite_conn.row_factory = sqlite3.Row
    sqlite_cursor = sqlite_conn.cursor()

    # MySQL 连接
    mysql_conn = mysql.connector.connect(
        host=os.environ.get("MYSQL_HOST", "localhost"),
        port=int(os.environ.get("MYSQL_PORT", "3306")),
        user=os.environ.get("MYSQL_USER", "vitals"),
        password=os.environ.get("MYSQL_PASSWORD", ""),
        database=os.environ.get("MYSQL_DATABASE", "vitals"),
    )
    mysql_cursor = mysql_conn.cursor()

    tables = ["users", "exercise", "meal", "sleep", "weight", "reading", "invites", "config"]

    for table in tables:
        print(f"迁移表: {table}")
        try:
            sqlite_cursor.execute(f"SELECT * FROM {table}")
            rows = sqlite_cursor.fetchall()

            if not rows:
                print(f"  - 无数据")
                continue

            columns = [desc[0] for desc in sqlite_cursor.description]
            placeholders = ", ".join(["%s"] * len(columns))
            column_names = ", ".join([f"`{c}`" for c in columns])

            insert_sql = f"INSERT INTO {table} ({column_names}) VALUES ({placeholders})"

            for row in rows:
                values = tuple(row)
                try:
                    mysql_cursor.execute(insert_sql, values)
                except mysql.connector.IntegrityError as e:
                    print(f"  - 跳过重复记录: {e}")

            mysql_conn.commit()
            print(f"  - 迁移 {len(rows)} 条记录")

        except sqlite3.OperationalError as e:
            print(f"  - 表不存在或错误: {e}")

    sqlite_conn.close()
    mysql_conn.close()
    print("迁移完成!")


if __name__ == "__main__":
    migrate()

Step 2: Commit

git add scripts/migrate_sqlite_to_mysql.py
git commit -m "feat: 添加 SQLite 到 MySQL 数据迁移脚本"

Task 1.7: 更新 Docker 配置

Files:

  • Modify: docker-compose.yml
  • Modify: .env.example

Step 1: 更新 docker-compose.yml

添加 MySQL 服务:

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-vitals}
      MYSQL_USER: ${MYSQL_USER:-vitals}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-vitalspassword}
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  vitals:
    # ... 现有配置 ...
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      MYSQL_HOST: mysql
      MYSQL_PORT: 3306
      MYSQL_USER: ${MYSQL_USER:-vitals}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-vitalspassword}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-vitals}

volumes:
  mysql_data:

Step 2: 更新 .env.example

# MySQL 配置
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=vitals
MYSQL_PASSWORD=your_password_here
MYSQL_DATABASE=vitals
MYSQL_ROOT_PASSWORD=rootpassword

Step 3: Commit

git add docker-compose.yml .env.example
git commit -m "config: 添加 MySQL Docker 配置"

阶段 2: 用户权限增强

Task 2.1: 修改 GET /api/users 接口

Files:

  • Modify: src/vitals/web/app.py:1699-1717

Step 1: 添加权限过滤

@app.get("/api/users", response_model=list[UserResponse])
async def get_users(current_user: User = Depends(get_current_user)):
    """获取用户列表(非管理员仅返回自己)"""
    if current_user.is_admin:
        users = db.get_users()
    else:
        users = [db.get_user(current_user.id)]

    return [
        UserResponse(
            id=u.id,
            name=u.name,
            created_at=u.created_at.isoformat(),
            is_active=u.is_active,
            gender=u.gender,
            height_cm=u.height_cm,
            weight_kg=u.weight_kg,
            age=u.age,
            bmi=u.bmi,
            bmi_status=u.bmi_status,
        )
        for u in users if u
    ]

Step 2: Commit

git add src/vitals/web/app.py
git commit -m "feat: GET /api/users 非管理员仅返回自己"

Task 2.2: 修改 GET /api/users/{user_id} 接口

Files:

  • Modify: src/vitals/web/app.py:1740-1758

Step 1: 添加权限检查

@app.get("/api/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, current_user: User = Depends(get_current_user)):
    """获取指定用户(非管理员只能查自己)"""
    if not current_user.is_admin and user_id != current_user.id:
        raise HTTPException(status_code=403, detail="无权查看其他用户")

    user = db.get_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    return UserResponse(
        id=user.id,
        name=user.name,
        created_at=user.created_at.isoformat(),
        is_active=user.is_active,
        gender=user.gender,
        height_cm=user.height_cm,
        weight_kg=user.weight_kg,
        age=user.age,
        bmi=user.bmi,
        bmi_status=user.bmi_status,
    )

Step 2: Commit

git add src/vitals/web/app.py
git commit -m "feat: GET /api/users/{id} 非管理员只能查自己"

Task 2.3: 隐藏非管理员的管理入口

Files:

  • Modify: src/vitals/web/app.py - 导航栏 HTML 生成函数

Step 1: 找到导航栏生成代码

搜索 get_navbar_html 或类似函数,修改管理链接的显示条件。

Step 2: 添加 is_admin 参数

在导航栏生成函数中,仅当 is_admin=True 时显示管理链接:

def get_navbar_html(active_page: str = "", is_admin: bool = False) -> str:
    # ... 现有代码 ...

    admin_link = ""
    if is_admin:
        admin_link = f'<a href="/admin" class="nav-link {"active" if active_page == "admin" else ""}">管理</a>'

    # 在导航栏 HTML 中使用 admin_link

Step 3: 更新所有调用处

在所有页面渲染时传入当前用户的 is_admin 状态。

Step 4: Commit

git add src/vitals/web/app.py
git commit -m "feat: 非管理员隐藏导航栏管理入口"

阶段 3: H5 移动端适配

Task 3.1: 添加全局移动端基础样式

Files:

  • Modify: src/vitals/web/app.py - 通用 CSS 函数

Step 1: 创建移动端基础 CSS

get_common_styles() 或类似函数中添加:

/* 移动端基础 */
* {
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
}

html {
    font-size: 16px;
    -webkit-text-size-adjust: 100%;
}

/* 响应式断点 */
@media (max-width: 768px) {
    html { font-size: 14px; }

    .container {
        padding: 0 16px;
        max-width: 100%;
    }

    /* 隐藏桌面导航 */
    .desktop-nav { display: none; }

    /* 显示移动端底部导航 */
    .mobile-nav { display: flex; }

    /* 内容区域留出底部导航空间 */
    .main-content {
        padding-bottom: 80px;
    }
}

@media (min-width: 769px) {
    .mobile-nav { display: none; }
    .desktop-nav { display: flex; }
}

/* 触摸目标 */
.touch-target {
    min-height: 44px;
    min-width: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* 安全区域 */
.safe-area-bottom {
    padding-bottom: env(safe-area-inset-bottom, 0);
}

Step 2: Commit

git add src/vitals/web/app.py
git commit -m "style: 添加移动端基础 CSS 样式"

Task 3.2: 创建底部 Tab 导航组件

Files:

  • Modify: src/vitals/web/app.py

Step 1: 创建底部导航 HTML 函数

def get_mobile_nav_html(active_page: str = "", is_admin: bool = False) -> str:
    """生成移动端底部导航栏"""

    def nav_item(href: str, icon: str, label: str, page: str) -> str:
        active_class = "active" if active_page == page else ""
        return f'''
            <a href="{href}" class="mobile-nav-item {active_class}">
                <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    {icon}
                </svg>
                <span class="nav-label">{label}</span>
            </a>
        '''

    # SVG 图标路径
    icons = {
        "home": '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
        "exercise": '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
        "meal": '<path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/>',
        "sleep": '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>',
        "more": '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
    }

    return f'''
    <nav class="mobile-nav safe-area-bottom">
        {nav_item("/", icons["home"], "首页", "home")}
        {nav_item("/exercise", icons["exercise"], "运动", "exercise")}
        {nav_item("/meal", icons["meal"], "饮食", "meal")}
        {nav_item("/sleep", icons["sleep"], "睡眠", "sleep")}
        <div class="mobile-nav-item more-trigger" onclick="toggleMoreMenu()">
            <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                {icons["more"]}
            </svg>
            <span class="nav-label">更多</span>
        </div>
    </nav>

    <div id="more-menu" class="more-menu hidden">
        <a href="/weight" class="more-menu-item">体重</a>
        <a href="/reading" class="more-menu-item">阅读</a>
        <a href="/report" class="more-menu-item">报告</a>
        <a href="/settings" class="more-menu-item">设置</a>
        {"<a href='/admin' class='more-menu-item'>管理</a>" if is_admin else ""}
    </div>

    <style>
    .mobile-nav {{
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        height: 64px;
        background: white;
        border-top: 1px solid #E2E8F0;
        display: none;
        justify-content: space-around;
        align-items: center;
        z-index: 50;
    }}

    .mobile-nav-item {{
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        min-width: 64px;
        min-height: 44px;
        color: #64748B;
        text-decoration: none;
        cursor: pointer;
    }}

    .mobile-nav-item.active {{
        color: #3B82F6;
    }}

    .nav-icon {{
        width: 24px;
        height: 24px;
        margin-bottom: 2px;
    }}

    .nav-label {{
        font-size: 10px;
        font-weight: 500;
    }}

    .more-menu {{
        position: fixed;
        bottom: 72px;
        right: 16px;
        background: white;
        border-radius: 12px;
        box-shadow: 0 4px 20px rgba(0,0,0,0.15);
        padding: 8px 0;
        z-index: 51;
    }}

    .more-menu.hidden {{
        display: none;
    }}

    .more-menu-item {{
        display: block;
        padding: 12px 24px;
        color: #1E293B;
        text-decoration: none;
    }}

    .more-menu-item:hover {{
        background: #F1F5F9;
    }}

    @media (max-width: 768px) {{
        .mobile-nav {{ display: flex; }}
    }}
    </style>

    <script>
    function toggleMoreMenu() {{
        const menu = document.getElementById('more-menu');
        menu.classList.toggle('hidden');
    }}

    // 点击其他地方关闭菜单
    document.addEventListener('click', function(e) {{
        if (!e.target.closest('.more-trigger') && !e.target.closest('.more-menu')) {{
            document.getElementById('more-menu').classList.add('hidden');
        }}
    }});
    </script>
    '''

Step 2: Commit

git add src/vitals/web/app.py
git commit -m "feat: 创建移动端底部 Tab 导航组件"

Task 3.3: 适配登录/注册页面

Files:

  • Modify: src/vitals/web/app.py - get_login_page_html(), get_register_page_html()

Step 1: 添加移动端适配样式

在登录页面 CSS 中添加:

@media (max-width: 768px) {
    .login-container {
        min-height: 100vh;
        padding: 24px 16px;
        display: flex;
        flex-direction: column;
        justify-content: center;
    }

    .login-card {
        width: 100%;
        max-width: none;
        box-shadow: none;
        padding: 24px 16px;
    }

    .login-input {
        height: 48px;
        font-size: 16px; /* 防止 iOS 缩放 */
    }

    .login-button {
        height: 48px;
        font-size: 16px;
    }
}

Step 2: Commit

git add src/vitals/web/app.py
git commit -m "style: 登录/注册页面移动端适配"

Task 3.4: 适配首页 Dashboard

Files:

  • Modify: src/vitals/web/app.py - 首页 HTML 生成

Step 1: 添加移动端适配样式

@media (max-width: 768px) {
    .dashboard-grid {
        grid-template-columns: 1fr;
        gap: 16px;
    }

    .stat-card {
        padding: 16px;
    }

    .stat-value {
        font-size: 28px;
    }

    .chart-container {
        height: 200px;
    }
}

Step 2: 在页面中添加底部导航

Step 3: Commit

git add src/vitals/web/app.py
git commit -m "style: 首页 Dashboard 移动端适配"

Task 3.5-3.10: 适配其余页面

按相同模式适配以下页面:

  • Task 3.5: 运动页面 (/exercise)
  • Task 3.6: 饮食页面 (/meal)
  • Task 3.7: 睡眠页面 (/sleep)
  • Task 3.8: 体重页面 (/weight)
  • Task 3.9: 阅读页面 (/reading)
  • Task 3.10: 设置页面 (/settings)
  • Task 3.11: 报告页面 (/report)
  • Task 3.12: 管理页面 (/admin)

每个页面的适配要点:

  1. 表格改为卡片列表
  2. 表单输入框加大
  3. 按钮符合 44px 触摸标准
  4. 添加底部导航
  5. 提交 commit

阶段 4: 测试与验证

Task 4.1: 本地 MySQL 测试

Step 1: 启动 MySQL

Run: docker-compose up mysql -d

Step 2: 初始化数据库

Run: python -c "from vitals.core.database import init_db; init_db()"

Step 3: 运行应用

Run: python -m vitals.web.app

Step 4: 验证功能

  • 登录/注册
  • 数据 CRUD
  • 权限隔离

Task 4.2: 移动端真机测试

Step 1: 获取本地 IP

Run: ifconfig | grep "inet " | grep -v 127.0.0.1

Step 2: 手机浏览器访问

访问 http://<本地IP>:8080

Step 3: 测试清单

  • 底部导航显示正常
  • 表单输入无缩放问题
  • 按钮点击区域足够
  • 页面无横向滚动

完成标准

  • MySQL 数据库正常连接和操作
  • 非管理员仅能看到自己的用户信息
  • 所有 11 个页面移动端适配完成
  • 底部 Tab 导航正常工作
  • 真机测试通过