feat(web): 移动端适配

- 侧边栏在窄屏改为抽屉:汉堡按钮唤起、遮罩点击关闭、切换路由自动收起
- 页头纵向排列,操作按钮与筛选控件撑满一行
- 对话框改用视口百分比宽度,表单标签改为顶部对齐,内容区限高可滚动
- 表格隐藏次要列并把操作列固定在右侧,仍可横向滚动查看全部
- 仪表盘倒计时环与信息上下排列,统计卡改两列
- 分页器在窄屏收缩为精简布局
- 输入框字号提到 16px,避免 iOS Safari 聚焦时自动缩放
- 新增 useIsMobile composable 处理 CSS 覆盖不到的部分

顺带修复汉堡按钮在桌面端未隐藏:.menu-btn 与后声明的 .icon-btn
特异性相同被覆盖,改用 .icon-btn.menu-btn 复合选择器。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mei
2026-07-26 22:57:52 +08:00
co-authored by Claude Fable 5
parent 0d766020bc
commit f8c6115737
10 changed files with 507 additions and 42 deletions
+114 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
@@ -12,6 +12,7 @@ import {
SwitchButton,
Moon,
Sunny,
Fold,
} from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import LogoMark from '@/components/LogoMark.vue'
@@ -20,6 +21,17 @@ const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
/** 移动端侧边栏抽屉的展开状态 */
const drawerOpen = ref(false)
// 切换路由后自动收起抽屉
watch(
() => route.path,
() => {
drawerOpen.value = false
},
)
const menus = [
{ path: '/dashboard', title: '仪表盘', icon: Odometer },
{ path: '/timers', title: '定时器', icon: TimerIcon },
@@ -53,8 +65,13 @@ async function handleLogout() {
<template>
<div class="layout">
<!-- 侧边栏 -->
<aside class="aside">
<!-- 移动端抽屉遮罩 -->
<Transition name="fade">
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false" />
</Transition>
<!-- 侧边栏:桌面端常驻,移动端为抽屉 -->
<aside class="aside" :class="{ open: drawerOpen }">
<div class="logo">
<div class="logo-mark"><LogoMark :size="22" /></div>
<div class="logo-name">
@@ -84,7 +101,12 @@ async function handleLogout() {
<!-- 主区域 -->
<div class="content">
<header class="header">
<div class="header-title">{{ route.meta.title ?? '' }}</div>
<div class="header-left">
<button class="icon-btn menu-btn" title="菜单" @click="drawerOpen = true">
<el-icon :size="18"><Fold /></el-icon>
</button>
<div class="header-title">{{ route.meta.title ?? '' }}</div>
</div>
<div class="header-actions">
<button class="icon-btn" :title="isDark ? '切换到亮色' : '切换到暗色'" @click="toggleDark">
<el-icon :size="17"><Moon v-if="!isDark" /><Sunny v-else /></el-icon>
@@ -92,7 +114,7 @@ async function handleLogout() {
<el-dropdown>
<span class="user-chip">
<span class="user-avatar">{{ userStore.user?.username?.[0]?.toUpperCase() }}</span>
{{ userStore.user?.username }}
<span class="user-name">{{ userStore.user?.username }}</span>
</span>
<template #dropdown>
<el-dropdown-menu>
@@ -125,6 +147,38 @@ async function handleLogout() {
display: flex;
}
/* 移动端抽屉遮罩 */
.drawer-mask {
position: fixed;
inset: 0;
z-index: 1999;
background: rgb(0 0 0 / 0.45);
backdrop-filter: blur(2px);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s var(--gb-ease);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 汉堡按钮只在移动端出现。
用复合选择器压过后面的 .icon-btn { display: flex },避免受声明顺序影响。 */
.icon-btn.menu-btn {
display: none;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
/* ---------- 侧边栏:墨色 + 药丸导航 ---------- */
.aside {
@@ -309,5 +363,60 @@ async function handleLogout() {
flex: 1;
overflow-y: auto;
padding: 24px 28px 40px;
-webkit-overflow-scrolling: touch;
}
/* ---------- 移动端 ---------- */
@media (max-width: 768px) {
/* 侧边栏脱离文档流,变成从左侧滑出的抽屉 */
.aside {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 2000;
width: 250px;
transform: translateX(-100%);
transition: transform 0.28s var(--gb-ease);
box-shadow: 4px 0 24px rgb(0 0 0 / 0.25);
}
.aside.open {
transform: translateX(0);
}
.icon-btn.menu-btn {
display: flex;
}
.header {
height: 54px;
padding: 0 14px;
}
.header-title {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-actions {
gap: 6px;
}
/* 窄屏只留头像,省出空间给标题 */
.user-name {
display: none;
}
.user-chip {
padding: 4px;
}
.main {
padding: 16px 14px 32px;
}
}
</style>