feat: 移动端适配(响应式优化)
This commit is contained in:
parent
c7904a9f8b
commit
0eae7f2aaf
178
fund-admin/src/components/MobileSidebar/index.vue
Normal file
178
fund-admin/src/components/MobileSidebar/index.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="mobile-sidebar-wrapper">
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
class="mobile-sidebar-mask"
|
||||
:class="{ show: modelValue }"
|
||||
@click="handleClose"
|
||||
/>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div
|
||||
class="mobile-sidebar"
|
||||
:class="{ open: modelValue }"
|
||||
>
|
||||
<div class="mobile-sidebar-header">
|
||||
<div class="logo">
|
||||
<img src="@/assets/logo.png" alt="logo" v-if="false">
|
||||
<span>资金服务平台</span>
|
||||
</div>
|
||||
<el-button link @click="handleClose">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="mobile-sidebar-content">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="false"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in routes"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
</el-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import SidebarItem from '@/layout/components/Sidebar/SidebarItem.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 当前激活的菜单
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
return path
|
||||
})
|
||||
|
||||
// 路由列表
|
||||
const routes = computed(() => {
|
||||
// 从路由配置中获取菜单
|
||||
const allRoutes = route.matched[0]?.children || []
|
||||
return allRoutes.filter(item => !item.hidden)
|
||||
})
|
||||
|
||||
// 关闭侧边栏
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mobile-sidebar-wrapper {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-sidebar-mask {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 260px;
|
||||
background: #304156;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&-header {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
background: #2b3a4d;
|
||||
|
||||
.logo {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
:deep(.el-menu) {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: #bfcbd9;
|
||||
|
||||
&:hover {
|
||||
background: #263445;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: #409eff;
|
||||
background: #263445;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
209
fund-admin/src/composables/useMobile.js
Normal file
209
fund-admin/src/composables/useMobile.js
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
296
fund-admin/src/styles/mobile.scss
Normal file
296
fund-admin/src/styles/mobile.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user