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:
@@ -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 =====
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user