From 75e75ad1900935b016361b8889c11604002316a0 Mon Sep 17 00:00:00 2001 From: rosercode Date: Wed, 30 Sep 2026 17:10:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E9=80=82=E9=85=8D?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E4=B8=8E=E5=B0=8F=E5=B1=8F=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A7=A6=E5=B1=8F=E5=9B=BE=E8=A1=A8=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增断点基础设施与响应式布局,使手机(<768px)与平板(768–1023px)下可正常浏览和操作,桌面端(≥1024px)布局保持原有形态不变;引入此前未被入口引用的全局样式文件,使全局小屏兜底规则生效,并修复小屏分页控件被裁切、触屏端图表点击误触直接跳转的问题。 变更内容: - 断点基础设施:新增 `frontend/src/composables/useBreakpoints.ts`(isMobile / isTablet / isDesktop / viewportWidth / isTouch),作为全站统一断点与触屏判定来源 - 全局样式:`frontend/src/main.ts` 引入 `./style.css`(此前从未被引入,导致其中规则一直无效),使弹层 max-width 92vw、body 防横向溢出等兜底规则实际生效 - 主布局 `views/Layout.vue`:小屏侧栏收起为 NDrawer、Header 增加菜单按钮,切回桌面自动收起;内容区高度改用 dvh,内边距随断点收窄;新增三语文案 `layout.menu` - 表格:为列宽可能超出窄屏的 NDataTable 统一补 `:scroll-x`(收支记录、邮箱账单、规则/分类/标签/渠道、账单预览、AI 使用、关系编辑等) - 弹窗:为固定宽度弹窗补 `max-width: 92vw`(分类/标签/渠道/规则、收支新增编辑与原始数据与一键设置、通知渠道、登录与注册卡片) - 栅格与图表:`Dashboard.vue` / `SqlQuery.vue` 栅格改用断点列(responsive="screen"),图表高度随断点降低 - 浮窗:`FloatingPanel.vue` 按视口约束宽高、最小尺寸随视口下调;`AiQueryWindow.vue` / `EmojiPicker.vue` 小屏自适应 - 表单与列表:左标签表单小屏改为顶部标签(AI 配置、数据库配置、通知配置、邮箱账单弹窗);未换行 NSpace 与关系编辑器成员行小屏可换行 - 分页修复:≤1023px 下 `.n-pagination` 允许换行、居中、高度自适应(naive-ui 运行期注入样式优先级更高,用 !important 覆盖),表格内置分页容器同步居中,避免页大小选择与跳页输入被裁到视口外 - 触屏图表交互:`Dashboard.vue` 触屏端点击趋势柱或饼图扇区时按点击坐标弹出确认气泡(「查看明细 / 取消」),确认后才跳转;`handleTrendBlankClick` 在触屏端直接返回、禁用「绘图区整列可点」;桌面端保持点击即跳转;新增三语文案 `dashboard.chartActionHint` / `dashboard.chartActionView` 说明: - 桌面端布局与跳转逻辑不变,点击柱子直接跳转、不显示气泡 - 分页控件在窄屏换行显示,页大小可切换、跳页输入可见可用 - 触屏端滑动/缩放不触发跳转(仅图形本体点击且需二次确认) - 采用「基础可用适配」范围,未做移动端卡片式重排;断点统一走 `useBreakpoints`,后续新增页面按工作区 `AGENTS.md`《前端响应式适配(移动端 / 小屏)》约定实现 验证: - 测试环境(5174)以 375 / 768 / 800 / 1280 宽度遍历统计面板、收支记录、分类管理、设置、邮箱账单、日志、智能查数等页面,均无横向溢出;手机端侧栏抽屉可开合;分页控件均在视口内且页大小可切换 - 触屏(375,hasTouch)点柱子/饼图扇区弹出确认气泡且不跳转,点空白不弹不跳,点「查看明细」跳转 `/transactions` 并写入筛选偏好;桌面(1280)点柱子直接跳转 - `./package.sh` 构建成功(bootJar + bootGuiJar,含前端 build) - `npx vue-tsc -b --force` 类型检查通过 --- .../src/components/AccountSecurityPanel.vue | 2 +- frontend/src/components/AiConfigPanel.vue | 5 +- frontend/src/components/AiUsagePanel.vue | 1 + frontend/src/components/BillPreviewPanel.vue | 1 + .../src/components/DatabaseConfigPanel.vue | 5 +- frontend/src/components/EmojiPicker.vue | 18 ++- frontend/src/components/FloatingPanel.vue | 43 +++++-- .../components/NotificationConfigPanel.vue | 9 +- .../TransactionRelationEditorModal.vue | 3 +- frontend/src/composables/useBreakpoints.ts | 61 +++++++++ frontend/src/locales/messages.ts | 3 + frontend/src/main.ts | 1 + frontend/src/style.css | 40 ++++++ frontend/src/views/CategoryManage.vue | 4 +- frontend/src/views/ChannelManage.vue | 3 +- frontend/src/views/Dashboard.vue | 117 ++++++++++++++++-- frontend/src/views/EmailBillManage.vue | 10 +- frontend/src/views/Layout.vue | 83 +++++++++++-- frontend/src/views/Login.vue | 2 +- frontend/src/views/Register.vue | 2 +- frontend/src/views/RuleManage.vue | 3 +- frontend/src/views/SqlQuery.vue | 13 +- frontend/src/views/TagManage.vue | 3 +- frontend/src/views/TransactionList.vue | 9 +- 24 files changed, 393 insertions(+), 48 deletions(-) create mode 100644 frontend/src/composables/useBreakpoints.ts diff --git a/frontend/src/components/AccountSecurityPanel.vue b/frontend/src/components/AccountSecurityPanel.vue index 7fbbc49..6158190 100644 --- a/frontend/src/components/AccountSecurityPanel.vue +++ b/frontend/src/components/AccountSecurityPanel.vue @@ -139,7 +139,7 @@ async function handleSubmit() { /> - + - +
diff --git a/frontend/src/components/AiUsagePanel.vue b/frontend/src/components/AiUsagePanel.vue index a234626..01d7d72 100644 --- a/frontend/src/components/AiUsagePanel.vue +++ b/frontend/src/components/AiUsagePanel.vue @@ -160,6 +160,7 @@ onMounted(loadUsage) :loading="usageLoading" size="small" :bordered="false" + :scroll-x="900" :striped="true" class="usage-table" > diff --git a/frontend/src/components/BillPreviewPanel.vue b/frontend/src/components/BillPreviewPanel.vue index ea2013a..6b345ef 100644 --- a/frontend/src/components/BillPreviewPanel.vue +++ b/frontend/src/components/BillPreviewPanel.vue @@ -392,6 +392,7 @@ onMounted(() => { :columns="columns" :data="filteredRows" :max-height="420" + :scroll-x="1020" :row-key="(row) => row.rowNo" size="small" :bordered="false" diff --git a/frontend/src/components/DatabaseConfigPanel.vue b/frontend/src/components/DatabaseConfigPanel.vue index f15f9fa..d5111a8 100644 --- a/frontend/src/components/DatabaseConfigPanel.vue +++ b/frontend/src/components/DatabaseConfigPanel.vue @@ -15,6 +15,7 @@ import { getDatabaseStatus } from '../api/database' import type { DatabaseConfigPayload, DatabaseType } from '../api/database' import { useDatabaseStore } from '../store/database' import { openSse } from '../utils/sse' +import { useBreakpoints } from '../composables/useBreakpoints' const props = defineProps<{ setup?: boolean }>() @@ -22,6 +23,8 @@ const router = useRouter() const message = useMessage() const { t } = useI18n() const databaseStore = useDatabaseStore() +// 小屏时表单标签置顶,避免左侧固定标签挤压输入区 +const { isMobile } = useBreakpoints() const testing = ref(false) const saving = ref(false) @@ -259,7 +262,7 @@ onBeforeUnmount(() => { - + SQLite diff --git a/frontend/src/components/EmojiPicker.vue b/frontend/src/components/EmojiPicker.vue index 4ef18a5..ec88871 100644 --- a/frontend/src/components/EmojiPicker.vue +++ b/frontend/src/components/EmojiPicker.vue @@ -2,6 +2,11 @@ // Emoji 选择器组件:提供常用 Emoji 分组快速点选与自定义文本输入,供分类图标等场景复用 import { computed, ref, watch } from 'vue' import { NPopover, NInput, NButton, NScrollbar } from 'naive-ui' +import { useBreakpoints } from '../composables/useBreakpoints' + +// 弹层宽度随视口收敛,避免小屏右侧溢出 +const { viewportWidth } = useBreakpoints() +const popoverWidth = computed(() => Math.min(360, Math.max(240, viewportWidth.value - 24))) /** 组件属性 */ const props = withDefaults(defineProps<{ @@ -68,7 +73,7 @@ function handleClear() { + + diff --git a/frontend/src/views/EmailBillManage.vue b/frontend/src/views/EmailBillManage.vue index 53a5bba..d474dda 100644 --- a/frontend/src/views/EmailBillManage.vue +++ b/frontend/src/views/EmailBillManage.vue @@ -33,11 +33,14 @@ import { migrateKey, writeString } from '../utils/storage' import { LEGACY_STORAGE_KEYS, STORAGE_KEYS } from '../utils/storageKeys' import { usePageSizeOptions } from '../composables/usePageSizeOptions' import { useTablePageSize } from '../composables/useTablePageSize' +import { useBreakpoints } from '../composables/useBreakpoints' const message = useMessage() const dialog = useDialog() const { t } = useI18n() const route = useRoute() +// 小屏时表单标签置顶,避免左侧固定标签挤压输入区 +const { isMobile } = useBreakpoints() const router = useRouter() // 所有表格共用的每页条数候选列表 @@ -769,6 +772,7 @@ onUnmounted(() => { :row-key="(r: EmailBillConfig) => r.id" size="small" :bordered="false" + :scroll-x="1140" :pagination="configPagination" @update:page="handleConfigPageChange" @update:page-size="handleConfigPageSizeChange" @@ -803,6 +807,7 @@ onUnmounted(() => { :row-key="(r: EmailBillSenderRule) => r.id" size="small" :bordered="false" + :scroll-x="780" :pagination="rulePagination" @update:page="handleRulePageChange" @update:page-size="handleRulePageSizeChange" @@ -836,6 +841,7 @@ onUnmounted(() => { size="small" :bordered="false" remote + :scroll-x="960" :pagination="{ page: messagePage, pageSize: messageSize, itemCount: messageTotal, pageSizes: pageSizeOptions, showSizePicker: true, @@ -849,7 +855,7 @@ onUnmounted(() => { - + @@ -923,7 +929,7 @@ onUnmounted(() => { - + diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue index 1a7418a..2757842 100644 --- a/frontend/src/views/Layout.vue +++ b/frontend/src/views/Layout.vue @@ -3,9 +3,14 @@ import { h, computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' -import { NLayout, NLayoutSider, NLayoutContent, NLayoutHeader, NMenu, NSpace, NIcon, NDropdown } from 'naive-ui' +import { + NLayout, NLayoutSider, NLayoutContent, NLayoutHeader, NMenu, NSpace, NIcon, NDropdown, + NDrawer, NDrawerContent, NButton, +} from 'naive-ui' import type { MenuOption } from 'naive-ui' -import { DocumentTextOutline, SettingsOutline, LogOutOutline, ChevronDownOutline, WalletOutline } from '@vicons/ionicons5' +import { + DocumentTextOutline, SettingsOutline, LogOutOutline, ChevronDownOutline, WalletOutline, MenuOutline, +} from '@vicons/ionicons5' import { useUserStore } from '../store/user' import LocaleSwitch from '../components/LocaleSwitch.vue' import { getChannels } from '../api/transaction' @@ -13,11 +18,15 @@ import { readSessionString, writeSessionString } from '../utils/storage' import { scrollKey } from '../utils/storageKeys' import { aiEnabled, refreshAiEnabled } from '../composables/useAiEnabled' import { navEntries, loadNavMenu } from '../composables/useNavMenu' +import { useBreakpoints } from '../composables/useBreakpoints' const router = useRouter() const route = useRoute() const userStore = useUserStore() const { t } = useI18n() +// 小屏断点:手机端侧边栏收起为抽屉 +const { isMobile } = useBreakpoints() +const showDrawer = ref(false) // 根据当前路由路径计算菜单激活项 const activeKey = computed(() => { @@ -54,6 +63,8 @@ const menuOptions = computed(() => { /** 菜单点击事件:根据 key 导航到对应路由 */ function handleMenuUpdate(key: string) { router.push(`/${key}`) + // 手机端选择菜单后收起抽屉 + showDrawer.value = false } // 顶部用户下拉菜单配置项(随语言切换响应式更新) @@ -80,6 +91,9 @@ function handleLogout() { router.push('/login') } +// 内容区样式:小屏收窄内边距,高度用 dvh 兜底移动端地址栏伸缩 +const contentStyle = computed(() => `padding: ${isMobile.value ? 12 : 24}px;`) + // 内容区滚动位置记忆:按路由分别记录,进入页面时自动恢复 const contentRef = ref | null>(null) // 内容区滚动容器 let scrollRaf = 0 // scroll 事件的 rAF 节流句柄 @@ -161,6 +175,11 @@ function handleBeforeUnload() { saveScroll() } +// 切回桌面宽度时收起抽屉,避免残留浮层 +watch(isMobile, (mobile) => { + if (!mobile) showDrawer.value = false +}) + // 路由切换:先保存离开页的位置,再在 DOM 更新后恢复目标页的位置 watch(() => route.path, (to, from) => { saveScroll(from) @@ -199,9 +218,10 @@ onBeforeUnmount(() => {