style: 管理页面 H5 移动端适配

- 添加移动端 CSS: 隐藏桌面导航,响应式布局
- 表格改为卡片列表: 使用 data-label 属性显示字段名
- 按钮触摸目标: 最小 44px 高度
- 表单输入框: 48px 高度,font-size 16px
- 底部 Tab 导航: 包含更多菜单和管理入口
- Safe area 支持: 适配刘海屏设备
- 操作按钮组: 移动端水平排列

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 18:08:35 +08:00
parent 00d93f132a
commit f9e586582f

View File

@@ -3064,6 +3064,34 @@ def get_admin_page_html() -> str:
color: #ef4444; color: #ef4444;
margin-bottom: 16px; margin-bottom: 16px;
} }
/* 移动端适配 */
@media (max-width: 768px) {
.nav { display: none !important; }
body { padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px)); }
.container { padding: 12px; }
header { padding: 20px 16px; margin-bottom: 16px; }
header h1 { font-size: 1.5rem; }
.card { padding: 16px; margin-bottom: 16px; }
.card h2 { font-size: 1.1rem; flex-direction: column; align-items: flex-start; gap: 12px; }
/* 表格改为卡片列表 */
table, thead, tbody, th, td, tr { display: block; }
thead { display: none; }
tr { background: #f8f9fa; border-radius: 8px; padding: 12px; margin-bottom: 12px; border: 1px solid #e2e8f0; }
td { padding: 8px 0; border: none; display: flex; justify-content: space-between; align-items: center; }
td:before { content: attr(data-label); font-weight: 600; color: #64748b; font-size: 0.85rem; }
/* 按钮触摸目标 */
.btn { min-height: 44px; padding: 12px 16px; font-size: 1rem; }
.btn-sm { min-height: 40px; padding: 10px 14px; }
/* 表单输入框 */
.form-group input, .form-group select { font-size: 16px; min-height: 48px; padding: 14px 16px; }
/* 模态框 */
.modal-content { margin: 12px; padding: 20px; }
.modal-footer { flex-direction: column; gap: 10px; }
.modal-footer .btn { width: 100%; }
/* 操作按钮组 */
.action-buttons { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.action-buttons .btn { flex: 1; min-width: 80px; }
}
</style> </style>
</head> </head>
<body> <body>
@@ -3283,22 +3311,24 @@ def get_admin_page_html() -> str:
const tbody = document.getElementById('usersBody'); const tbody = document.getElementById('usersBody');
tbody.innerHTML = users.map(user => ` tbody.innerHTML = users.map(user => `
<tr> <tr>
<td>${user.id}</td> <td data-label="ID">${user.id}</td>
<td>${user.name}</td> <td data-label="用户名">${user.name}</td>
<td> <td data-label="状态">
<span class="status-badge ${user.is_disabled ? 'status-disabled' : 'status-active'}"> <span class="status-badge ${user.is_disabled ? 'status-disabled' : 'status-active'}">
${user.is_disabled ? '已禁用' : '正常'} ${user.is_disabled ? '已禁用' : '正常'}
</span> </span>
</td> </td>
<td>${user.is_admin ? '' : ''}</td> <td data-label="管理员">${user.is_admin ? '' : ''}</td>
<td>${user.created_at.split('T')[0]}</td> <td data-label="创建时间">${user.created_at.split('T')[0]}</td>
<td> <td data-label="操作">
${user.id !== currentUser.id ? ` ${user.id !== currentUser.id ? `
<div class="action-buttons">
${user.is_disabled ${user.is_disabled
? `<button class="btn btn-success btn-sm" onclick="enableUser(${user.id})">启用</button>` ? `<button class="btn btn-success btn-sm" onclick="enableUser(${user.id})">启用</button>`
: `<button class="btn btn-danger btn-sm" onclick="disableUser(${user.id})">禁用</button>` : `<button class="btn btn-danger btn-sm" onclick="disableUser(${user.id})">禁用</button>`
} }
<button class="btn btn-danger btn-sm" onclick="deleteUser(${user.id}, '${user.name}')">删除</button> <button class="btn btn-danger btn-sm" onclick="deleteUser(${user.id}, '${user.name}')">删除</button>
</div>
` : '<span style="color:#888">当前用户</span>'} ` : '<span style="color:#888">当前用户</span>'}
</td> </td>
</tr> </tr>
@@ -3353,13 +3383,13 @@ def get_admin_page_html() -> str:
return ` return `
<tr> <tr>
<td><span class="invite-code">${invite.code}</span></td> <td data-label="邀请码"><span class="invite-code">${invite.code}</span></td>
<td><span class="status-badge ${statusClass}">${status}</span></td> <td data-label="状态"><span class="status-badge ${statusClass}">${status}</span></td>
<td>${creator ? creator.name : invite.created_by}</td> <td data-label="创建者">${creator ? creator.name : invite.created_by}</td>
<td>${usedBy ? usedBy.name : '-'}</td> <td data-label="使用者">${usedBy ? usedBy.name : '-'}</td>
<td>${invite.created_at.split('T')[0]}</td> <td data-label="创建时间">${invite.created_at.split('T')[0]}</td>
<td> <td data-label="操作">
${!invite.is_used ? `<button class="btn btn-danger btn-sm" onclick="deleteInvite(${invite.id})">删除</button>` : ''} ${!invite.is_used ? `<button class="btn btn-danger btn-sm" onclick="deleteInvite(${invite.id})">删除</button>` : '-'}
</td> </td>
</tr> </tr>
`; `;
@@ -3410,6 +3440,38 @@ def get_admin_page_html() -> str:
loadInvites(); loadInvites();
} }
</script> </script>
<!-- 移动端底部导航 -->
<nav class="mobile-nav">
<a href="/" class="mobile-nav-item"><svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"/></svg><span class="nav-label">首页</span></a>
<a href="/exercise" class="mobile-nav-item"><svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg><span class="nav-label">运动</span></a>
<a href="/meal" class="mobile-nav-item"><svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"/></svg><span class="nav-label">饮食</span></a>
<a href="/sleep" class="mobile-nav-item"><svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg><span class="nav-label">睡眠</span></a>
<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"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></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" style="color:#667eea;font-weight:600;">管理</a>
</div>
<style>
.mobile-nav { position: fixed; bottom: 0; left: 0; right: 0; height: calc(64px + env(safe-area-inset-bottom, 0px)); padding-bottom: env(safe-area-inset-bottom, 0px); background: white; border-top: 1px solid #E2E8F0; display: none; justify-content: space-around; align-items: flex-start; padding-top: 8px; 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; -webkit-tap-highlight-color: transparent; }
.mobile-nav-item.active { color: #667eea; }
.nav-icon { width: 24px; height: 24px; margin-bottom: 2px; }
.nav-label { font-size: 10px; font-weight: 500; }
.more-menu { position: fixed; bottom: calc(72px + env(safe-area-inset-bottom, 0px)); 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: 14px 24px; color: #1E293B; text-decoration: none; min-height: 44px; }
.more-menu-item:hover, .more-menu-item:active { background: #F1F5F9; }
@media (max-width: 768px) { .mobile-nav { display: flex; } }
</style>
<script>
function toggleMoreMenu() { document.getElementById('more-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>
</body> </body>
</html> </html>
""" """