feat: 添加移动端 H5 基础样式和底部导航组件

- 添加 get_common_mobile_styles() 通用移动端 CSS
- 添加 get_mobile_nav_html() 底部 Tab 导航组件
- 支持触摸优化和安全区域适配
- 底部导航包含首页/运动/饮食/睡眠/更多菜单

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 17:45:34 +08:00
parent c44de8f52e
commit 1664f88963

View File

@@ -1702,9 +1702,13 @@ async def search_books(q: str):
@app.get("/api/users", response_model=list[UserResponse])
async def get_users():
"""获取所有用户"""
users = db.get_users()
async def get_users(current_user: User = Depends(require_user)):
"""获取用户列表(非管理员仅返回自己)"""
if current_user.is_admin:
users = db.get_users()
else:
users = [db.get_user(current_user.id)]
return [
UserResponse(
id=u.id,
@@ -1718,7 +1722,7 @@ async def get_users():
bmi=u.bmi,
bmi_status=u.bmi_status,
)
for u in users
for u in users if u
]
@@ -1743,8 +1747,11 @@ async def get_active_user():
@app.get("/api/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
"""获取指定用户"""
async def get_user(user_id: int, current_user: User = Depends(require_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="用户不存在")
@@ -1893,6 +1900,201 @@ async def clear_data(request: DataClearInput):
return {"message": "数据已清除"}
# ===== 移动端 H5 通用样式和组件 =====
def get_common_mobile_styles() -> str:
"""生成移动端通用 CSS 样式"""
return """
/* 移动端基础 */
* {
-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, .nav { display: none !important; }
/* 显示移动端底部导航 */
.mobile-nav { display: flex !important; }
/* 内容区域留出底部导航空间 */
.main-content, body {
padding-bottom: 80px;
}
}
@media (min-width: 769px) {
.mobile-nav { display: none !important; }
.desktop-nav, .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);
}
"""
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"/>',
}
admin_link = f'<a href="/admin" class="more-menu-item">管理</a>' if is_admin else ""
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>
{admin_link}
</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>
'''
# ===== 认证页面 HTML =====