包含 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>
25 KiB
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.toml 的 dependencies 列表中添加:
"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 sqlite3get_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)
每个页面的适配要点:
- 表格改为卡片列表
- 表单输入框加大
- 按钮符合 44px 触摸标准
- 添加底部导航
- 提交 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 导航正常工作
- 真机测试通过