From f8c611573760744330fd1194cc869d67b0b0f822 Mon Sep 17 00:00:00 2001 From: mei Date: Sun, 26 Jul 2026 22:57:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 侧边栏在窄屏改为抽屉:汉堡按钮唤起、遮罩点击关闭、切换路由自动收起 - 页头纵向排列,操作按钮与筛选控件撑满一行 - 对话框改用视口百分比宽度,表单标签改为顶部对齐,内容区限高可滚动 - 表格隐藏次要列并把操作列固定在右侧,仍可横向滚动查看全部 - 仪表盘倒计时环与信息上下排列,统计卡改两列 - 分页器在窄屏收缩为精简布局 - 输入框字号提到 16px,避免 iOS Safari 聚焦时自动缩放 - 新增 useIsMobile composable 处理 CSS 覆盖不到的部分 顺带修复汉堡按钮在桌面端未隐藏:.menu-btn 与后声明的 .icon-btn 特异性相同被覆盖,改用 .icon-btn.menu-btn 复合选择器。 Co-Authored-By: Claude Fable 5 --- web/frontend/src/assets/main.css | 108 ++++++++++++++++ web/frontend/src/composables/useBreakpoint.ts | 36 ++++++ web/frontend/src/layouts/MainLayout.vue | 119 +++++++++++++++++- web/frontend/src/views/AccountsView.vue | 16 ++- web/frontend/src/views/DashboardView.vue | 75 ++++++++++- web/frontend/src/views/LoginView.vue | 28 ++++- web/frontend/src/views/LogsView.vue | 56 +++++++-- web/frontend/src/views/RulesView.vue | 39 ++++-- web/frontend/src/views/SettingsView.vue | 7 +- web/frontend/src/views/TimersView.vue | 65 +++++++++- 10 files changed, 507 insertions(+), 42 deletions(-) create mode 100644 web/frontend/src/composables/useBreakpoint.ts diff --git a/web/frontend/src/assets/main.css b/web/frontend/src/assets/main.css index d5a6fdd..80f7439 100644 --- a/web/frontend/src/assets/main.css +++ b/web/frontend/src/assets/main.css @@ -59,6 +59,7 @@ body, display: flex; align-items: center; justify-content: space-between; + gap: 12px; margin-bottom: 20px; } @@ -204,3 +205,110 @@ body, ::-webkit-scrollbar-track { background: transparent; } + +/* ============================================ + 移动端适配 (<= 768px) + ============================================ */ + +@media (max-width: 768px) { + /* 页头改为纵向排列,标题与操作按钮各占一行 */ + .page-header { + flex-direction: column; + align-items: stretch; + gap: 14px; + } + + .page-header h2 { + font-size: 19px; + } + + /* 页头里的按钮组撑满一行,方便单手点击 */ + .page-header .el-button, + .page-header .el-select, + .page-header .el-input { + width: 100%; + } + + /* 圆形图标按钮不参与拉伸 */ + .page-header .el-button.is-circle { + width: 34px; + flex: none; + } + + /* 对话框贴合屏幕,避免固定 px 宽度溢出 */ + .el-dialog { + --el-dialog-width: 92vw !important; + width: 92vw !important; + max-width: 92vw; + margin-top: 8vh !important; + border-radius: 14px; + } + + .el-dialog__body { + max-height: 66vh; + overflow-y: auto; + padding: 16px 18px; + } + + .el-dialog__footer .el-button { + min-width: 88px; + } + + /* 确认弹窗同样收窄 */ + .el-message-box { + width: 90vw !important; + max-width: 90vw; + } + + /* 消息提示避开顶栏 */ + .el-message { + min-width: 0; + width: calc(100vw - 32px); + } + + /* 表单标签改为顶部对齐,横向空间太窄放不下 */ + .el-form--label-top .el-form-item__label { + padding-bottom: 4px; + } + + /* 卡片内边距收紧 */ + .el-card__body { + padding: 16px; + } + + /* 下拉菜单项加大点击区域 */ + .el-dropdown-menu__item { + padding: 10px 18px; + } + + /* 输入字号不小于 16px,否则 iOS Safari 聚焦时会自动放大整个页面 */ + .el-input__inner, + .el-textarea__inner, + .el-select__input { + font-size: 16px; + } + + /* 表格横向滚动更顺滑,并收紧内边距塞下更多列 */ + .el-table__body-wrapper { + -webkit-overflow-scrolling: touch; + } + + .el-table .cell { + padding: 0 8px; + } + + .el-table .el-button + .el-button { + margin-left: 6px; + } +} + +/* 超窄屏(<=380px)进一步压缩 */ +@media (max-width: 380px) { + .el-card__body { + padding: 14px; + } + + .page-header h2 { + font-size: 18px; + } +} diff --git a/web/frontend/src/composables/useBreakpoint.ts b/web/frontend/src/composables/useBreakpoint.ts new file mode 100644 index 0000000..130d79f --- /dev/null +++ b/web/frontend/src/composables/useBreakpoint.ts @@ -0,0 +1,36 @@ +import { onUnmounted, ref } from 'vue' + +/** 移动端断点,与 main.css 中的媒体查询保持一致 */ +export const MOBILE_BREAKPOINT = 768 + +/** + * 响应式的移动端判断。 + * + * 用于那些光靠 CSS 改不了的地方,比如表格列的显隐、分页器布局、表单标签位置。 + * + * 除了监听 matchMedia 的 change,额外挂一个 resize 兜底: + * 某些环境(内嵌 WebView、开发者工具的设备模拟)改视口时不会派发 change, + * 只靠 change 会导致旋转屏幕后布局不跟着变。 + */ +export function useIsMobile() { + const query = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`) + const isMobile = ref(query.matches) + + const sync = () => { + if (isMobile.value !== query.matches) { + isMobile.value = query.matches + } + } + + query.addEventListener('change', sync) + window.addEventListener('resize', sync, { passive: true }) + window.addEventListener('orientationchange', sync) + + onUnmounted(() => { + query.removeEventListener('change', sync) + window.removeEventListener('resize', sync) + window.removeEventListener('orientationchange', sync) + }) + + return isMobile +} diff --git a/web/frontend/src/layouts/MainLayout.vue b/web/frontend/src/layouts/MainLayout.vue index 1d9b544..eb8f160 100644 --- a/web/frontend/src/layouts/MainLayout.vue +++ b/web/frontend/src/layouts/MainLayout.vue @@ -1,5 +1,5 @@