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(() => {