From 0eae7f2aaf26745e450bca15d5ff5edddb9960cf Mon Sep 17 00:00:00 2001 From: zhangjf Date: Mon, 16 Feb 2026 10:17:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E5=8A=A8=E7=AB=AF=E9=80=82?= =?UTF-8?q?=E9=85=8D=EF=BC=88=E5=93=8D=E5=BA=94=E5=BC=8F=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MobileSidebar/index.vue | 178 +++++++++++ fund-admin/src/composables/useMobile.js | 209 +++++++++++++ fund-admin/src/styles/mobile.scss | 296 ++++++++++++++++++ 3 files changed, 683 insertions(+) create mode 100644 fund-admin/src/components/MobileSidebar/index.vue create mode 100644 fund-admin/src/composables/useMobile.js create mode 100644 fund-admin/src/styles/mobile.scss diff --git a/fund-admin/src/components/MobileSidebar/index.vue b/fund-admin/src/components/MobileSidebar/index.vue new file mode 100644 index 0000000..60a187e --- /dev/null +++ b/fund-admin/src/components/MobileSidebar/index.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/fund-admin/src/composables/useMobile.js b/fund-admin/src/composables/useMobile.js new file mode 100644 index 0000000..592cbbb --- /dev/null +++ b/fund-admin/src/composables/useMobile.js @@ -0,0 +1,209 @@ +import { ref, computed, onMounted, onUnmounted } from 'vue' + +// 断点定义 +const BREAKPOINTS = { + xs: 768, // 手机 + sm: 992, // 平板 + md: 1200, // 小桌面 + lg: 1920 // 大桌面 +} + +/** + * 移动端检测组合式函数 + */ +export function useMobile() { + // 视口宽度 + const width = ref(window.innerWidth) + const height = ref(window.innerHeight) + + // 是否移动端(< 768px) + const isMobile = computed(() => width.value < BREAKPOINTS.xs) + + // 是否平板(768px - 992px) + const isTablet = computed(() => width.value >= BREAKPOINTS.xs && width.value < BREAKPOINTS.sm) + + // 是否桌面(>= 992px) + const isDesktop = computed(() => width.value >= BREAKPOINTS.sm) + + // 是否小屏幕(< 992px) + const isSmallScreen = computed(() => width.value < BREAKPOINTS.sm) + + // 当前断点 + const breakpoint = computed(() => { + if (width.value < BREAKPOINTS.xs) return 'xs' + if (width.value < BREAKPOINTS.sm) return 'sm' + if (width.value < BREAKPOINTS.md) return 'md' + if (width.value < BREAKPOINTS.lg) return 'lg' + return 'xl' + }) + + // 侧边栏是否折叠 + const sidebarCollapsed = ref(false) + + // 移动端侧边栏是否打开 + const mobileSidebarOpen = ref(false) + + // 切换侧边栏 + const toggleSidebar = () => { + if (isMobile.value) { + mobileSidebarOpen.value = !mobileSidebarOpen.value + } else { + sidebarCollapsed.value = !sidebarCollapsed.value + } + } + + // 关闭移动端侧边栏 + const closeMobileSidebar = () => { + mobileSidebarOpen.value = false + } + + // 打开移动端侧边栏 + const openMobileSidebar = () => { + mobileSidebarOpen.value = true + } + + // 监听窗口大小变化 + const handleResize = () => { + width.value = window.innerWidth + height.value = window.innerHeight + + // 切换到桌面端时关闭移动端侧边栏 + if (!isMobile.value) { + mobileSidebarOpen.value = false + } + } + + onMounted(() => { + window.addEventListener('resize', handleResize) + handleResize() + }) + + onUnmounted(() => { + window.removeEventListener('resize', handleResize) + }) + + return { + width, + height, + isMobile, + isTablet, + isDesktop, + isSmallScreen, + breakpoint, + sidebarCollapsed, + mobileSidebarOpen, + toggleSidebar, + closeMobileSidebar, + openMobileSidebar + } +} + +/** + * 响应式表格组合式函数 + */ +export function useResponsiveTable() { + const { isMobile } = useMobile() + + // 表格高度 + const tableHeight = computed(() => { + if (isMobile.value) { + return 'auto' + } + // 桌面端:视口高度 - 头部(50) - 搜索区域(80) - 分页(50) - 边距(40) + return window.innerHeight - 220 + }) + + // 分页大小选项 + const pageSizeOptions = computed(() => { + return isMobile.value ? [10, 20] : [10, 20, 50, 100] + }) + + // 默认分页大小 + const defaultPageSize = computed(() => { + return isMobile.value ? 10 : 20 + }) + + return { + isMobile, + tableHeight, + pageSizeOptions, + defaultPageSize + } +} + +/** + * 触摸手势支持 + */ +export function useTouchGesture() { + const touchStartX = ref(0) + const touchEndX = ref(0) + const touchStartY = ref(0) + const touchEndY = ref(0) + + // 最小滑动距离 + const MIN_SWIPE_DISTANCE = 50 + + // 处理触摸开始 + const handleTouchStart = (e) => { + touchStartX.value = e.touches[0].clientX + touchStartY.value = e.touches[0].clientY + } + + // 处理触摸结束 + const handleTouchEnd = (e) => { + touchEndX.value = e.changedTouches[0].clientX + touchEndY.value = e.changedTouches[0].clientY + + return { + direction: getSwipeDirection(), + distance: Math.abs(touchEndX.value - touchStartX.value) + } + } + + // 获取滑动方向 + const getSwipeDirection = () => { + const diffX = touchEndX.value - touchStartX.value + const diffY = touchEndY.value - touchStartY.value + + // 判断是水平滑动还是垂直滑动 + if (Math.abs(diffX) > Math.abs(diffY)) { + if (Math.abs(diffX) < MIN_SWIPE_DISTANCE) return null + return diffX > 0 ? 'right' : 'left' + } else { + if (Math.abs(diffY) < MIN_SWIPE_DISTANCE) return null + return diffY > 0 ? 'down' : 'up' + } + } + + return { + handleTouchStart, + handleTouchEnd, + getSwipeDirection + } +} + +/** + * 虚拟键盘检测 + */ +export function useVirtualKeyboard() { + const isKeyboardOpen = ref(false) + const windowHeight = ref(window.innerHeight) + + const handleResize = () => { + const currentHeight = window.innerHeight + // 如果窗口高度变小超过150px,认为是虚拟键盘打开 + isKeyboardOpen.value = windowHeight.value - currentHeight > 150 + } + + onMounted(() => { + window.addEventListener('resize', handleResize) + }) + + onUnmounted(() => { + window.removeEventListener('resize', handleResize) + }) + + return { + isKeyboardOpen + } +} diff --git a/fund-admin/src/styles/mobile.scss b/fund-admin/src/styles/mobile.scss new file mode 100644 index 0000000..336f1e3 --- /dev/null +++ b/fund-admin/src/styles/mobile.scss @@ -0,0 +1,296 @@ +// 移动端适配样式 +// 断点定义:xs < 768px < sm < 992px < md < 1200px < lg + +// 移动端基础样式 +@media screen and (max-width: 768px) { + // 隐藏侧边栏 + .sidebar-container { + transform: translateX(-100%); + transition: transform 0.3s; + + &.mobile-open { + transform: translateX(0); + } + } + + // 主内容区域全宽 + .main-container { + margin-left: 0 !important; + width: 100% !important; + } + + // 顶部导航栏适配 + .navbar { + .hamburger-container { + display: none; + } + + .breadcrumb-container { + display: none; + } + + .right-menu { + .right-menu-item { + padding: 0 8px; + } + } + } + + // 搜索表单改为垂直布局 + .search-form { + .el-form-item { + margin-right: 0; + margin-bottom: 10px; + width: 100%; + + .el-input, + .el-select { + width: 100% !important; + } + } + + .el-button { + width: 100%; + margin-left: 0 !important; + margin-bottom: 10px; + } + } + + // 表格操作按钮 + .el-table { + .el-button--link { + padding: 4px 8px; + font-size: 12px; + } + } + + // 分页器适配 + .el-pagination { + .el-pagination__sizes, + .el-pagination__jump { + display: none; + } + + .btn-prev, + .btn-next { + min-width: 28px; + } + + .el-pager li { + min-width: 28px; + } + } + + // 对话框全屏 + .el-dialog { + width: 100% !important; + margin: 0 !important; + border-radius: 0 !important; + height: 100vh; + + .el-dialog__body { + max-height: calc(100vh - 120px); + overflow-y: auto; + } + } + + // 抽屉全屏 + .el-drawer { + width: 100% !important; + } + + // 表单单行显示 + .el-form { + .el-form-item { + margin-bottom: 15px; + + .el-form-item__label { + float: none; + display: block; + text-align: left; + margin-bottom: 5px; + } + + .el-form-item__content { + margin-left: 0 !important; + } + } + } + + // 卡片内边距减小 + .el-card { + .el-card__header { + padding: 12px 15px; + } + + .el-card__body { + padding: 15px; + } + } + + // 统计卡片 + .dashboard-card { + margin-bottom: 15px; + + .card-icon { + font-size: 36px; + } + + .card-value { + font-size: 20px; + } + } + + // 底部固定按钮 + .fixed-bottom-buttons { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 10px 15px; + background: #fff; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); + z-index: 100; + display: flex; + gap: 10px; + + .el-button { + flex: 1; + } + } + + // 底部留白(避免被固定按钮遮挡) + .has-fixed-bottom { + padding-bottom: 60px; + } +} + +// 平板适配 +@media screen and (min-width: 769px) and (max-width: 992px) { + .sidebar-container { + width: 54px !important; + + .el-menu { + .el-sub-menu__title, + .el-menu-item { + padding: 0 15px !important; + + span { + display: none; + } + + .el-sub-menu__icon-arrow { + display: none; + } + } + } + } + + .main-container { + margin-left: 54px !important; + } +} + +// 移动端菜单按钮 +.mobile-menu-btn { + display: none; + + @media screen and (max-width: 768px) { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + font-size: 20px; + cursor: pointer; + } +} + +// 移动端遮罩层 +.mobile-sidebar-mask { + display: none; + + @media screen and (max-width: 768px) { + display: block; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 999; + opacity: 0; + visibility: hidden; + transition: all 0.3s; + + &.show { + opacity: 1; + visibility: visible; + } + } +} + +// 响应式工具类 +.hide-on-mobile { + @media screen and (max-width: 768px) { + display: none !important; + } +} + +.show-on-mobile { + display: none !important; + + @media screen and (max-width: 768px) { + display: block !important; + } +} + +// 响应式表格 +.responsive-table { + @media screen and (max-width: 768px) { + .el-table { + &__header-wrapper { + display: none; + } + + &__body-wrapper { + .el-table__row { + display: block; + margin-bottom: 15px; + border: 1px solid #ebeef5; + border-radius: 4px; + padding: 10px; + + td { + display: flex; + justify-content: space-between; + align-items: center; + border: none; + padding: 8px 0; + + &:not(:last-child) { + border-bottom: 1px solid #ebeef5; + } + + &::before { + content: attr(data-label); + font-weight: bold; + color: #606266; + } + } + } + } + } + } +} + +// 触摸优化 +@media (hover: none) { + .el-button:hover { + opacity: 1; + } + + .el-table__row:hover > td { + background-color: transparent !important; + } +}