feat: 全局移动端适配 AdminLayout 汉堡抽屉 + 67 页通用规则

This commit is contained in:
guoju0808
2026-08-26 11:13:12 +00:00
parent d2fe5a4035
commit 8a08c8378f
5 changed files with 513 additions and 4 deletions
+135
View File
@@ -266,4 +266,139 @@ h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 600; color: var(--el-text-color
text-align: center;
color: var(--el-text-color-secondary);
font-size: 13px;
}
/* =============================================
全局移动端适配 (≤768px) — 一次定义, 67 个页面生效
- 列表页 (el-table / el-pagination / filter-form)
- 详情/编辑 (el-dialog / el-form)
- 业务页 (el-card / el-tabs / el-descriptions / el-table)
============================================= */
@media (max-width: 768px) {
/* —— filter-form 已在前面定义, 这里再覆盖一次以防页面局部 scoped 抢占 —— */
.filter-form {
flex-direction: column !important;
align-items: stretch !important;
gap: 10px !important;
}
.filter-form .el-form-item {
margin-right: 0 !important;
margin-bottom: 0 !important;
width: 100%;
}
.filter-form .el-input,
.filter-form .el-select,
.filter-form .el-date-editor,
.filter-form .el-cascader,
.filter-form > .el-button,
.filter-form > button {
width: 100% !important;
min-width: 0 !important;
}
.filter-form .filter-actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
.filter-form .filter-actions .el-button { flex: 1; }
/* —— el-pagination 移动端: 全宽, 紧凑 —— */
.el-pagination {
flex-wrap: wrap;
justify-content: center;
gap: 8px;
padding: 12px 0 !important;
}
.el-pagination .el-pagination__sizes,
.el-pagination .el-pagination__jump { display: none !important; }
.el-pagination .el-pagination__total { font-size: 12px !important; }
.el-pagination .btn-prev,
.el-pagination .btn-next,
.el-pagination .el-pager li { min-width: 28px !important; height: 28px !important; line-height: 28px !important; font-size: 12px !important; }
/* —— el-card 移动端: 减小内边距 —— */
.el-card {
border-radius: 4px !important;
}
.el-card__header {
padding: 12px 14px !important;
}
.el-card__body {
padding: 14px !important;
}
/* —— el-dialog 移动端: 全屏化 —— */
.el-dialog {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
border-radius: 0 !important;
position: fixed !important;
inset: 0 !important;
height: 100% !important;
max-height: 100% !important;
}
.el-dialog__header {
padding: 14px 16px !important;
}
.el-dialog__title { font-size: 15px !important; }
.el-dialog__body {
padding: 14px 16px !important;
max-height: calc(100vh - 110px) !important;
overflow-y: auto !important;
}
.el-dialog__footer {
padding: 12px 16px !important;
}
/* dialog 关闭按钮稍大, 便于手指点击 */
.el-dialog__headerbtn { font-size: 18px !important; width: 32px !important; height: 32px !important; }
/* —— el-form-item 移动端: label 上方对齐 —— */
.el-form-item .el-form-item__label {
line-height: 1.5 !important;
padding-bottom: 4px !important;
}
.el-form.el-form--label-top .el-form-item__label {
padding: 0 0 4px !important;
}
/* —— el-table 移动端: 字号收紧 + 滚动提示 —— */
.el-table {
font-size: 12px !important;
}
.el-table th.el-table__cell { font-size: 12px !important; padding: 6px 4px !important; }
.el-table td.el-table__cell { padding: 6px 4px !important; }
/* —— el-tabs 移动端: 滚动 + 字号 —— */
.el-tabs__nav-wrap--scrollable .el-tabs__nav-scroll { padding: 0 4px !important; }
.el-tabs__item { padding: 0 12px !important; font-size: 13px !important; height: 36px !important; line-height: 36px !important; }
/* —— el-descriptions 移动端: 单列堆叠 —— */
.el-descriptions {
font-size: 13px !important;
}
.el-descriptions__label,
.el-descriptions__content {
padding: 10px 8px !important;
word-break: break-word;
}
.el-descriptions-item__label.is-cell { font-size: 12px !important; }
/* —— el-page-header / 通用业务页 header —— */
.app-page-header,
.page-header {
flex-direction: column !important;
align-items: flex-start !important;
gap: 8px !important;
padding: 12px 0 !important;
}
.app-page-header h2,
.page-header h2 { font-size: 17px !important; }
/* —— 通用容器/卡片 padding 收紧 —— */
.app-container,
.page-container { padding: 0 !important; }
/* —— 操作列 el-link 仍行内不折行 —— */
.table-actions { flex-wrap: nowrap !important; }
}
+35 -2
View File
@@ -12,13 +12,13 @@
</div>
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国</p>
</div>
<div class="footer-col">
<div class="footer-col col-nav">
<h4>快速导航</h4>
<a @click.prevent="goHome">年度项目规划</a>
<a @click.prevent="goPublicity">项目公示</a>
<a href="https://v2.bahim.org.cn/computer/#/home" target="_blank" rel="noopener">北京整合医学学会首页</a>
</div>
<div class="footer-col">
<div class="footer-col col-projects">
<h4>学会项目</h4>
<a href="https://bxxcx.bahim.org.cn/#/" target="_blank" rel="noopener">百姓巡常行</a>
<a href="https://v2.bahim.org.cn/computer/#/project/home/title=%E5%81%A5%E5%BA%B7%E4%B8%AD%E5%9B%BD%E8%A1%8C%E5%8A%A8%E7%B3%BB%E5%88%97%E9%87%8D%E7%82%B9%E9%A1%B9%E7%9B%AE?columnId=59&projectId=5" target="_blank" rel="noopener">健康中国行动</a>
@@ -109,4 +109,37 @@ function goPublicity() { router.push('/publicity') }
@media (max-width: 900px) {
.container { padding: 0 24px; }
}
/* 手机端深度适配 */
@media (max-width: 768px) {
.container { padding: 0 16px; }
.footer { padding: 32px 0 0; }
/* footer-main 改为单列, 隐藏"快速导航"和"学会项目" */
.footer-main {
grid-template-columns: 1fr;
gap: 24px;
padding-bottom: 24px;
}
/* 桌面: 快速导航(2)/学会项目(3) 列, 移动端隐藏 */
.footer-col.col-nav,
.footer-col.col-projects { display: none; }
/* 联系方式列保持在品牌区下 */
.footer-brand .brand-row { margin-bottom: 12px; }
.footer-brand-name { font-size: 15px; }
.footer-brand-en { display: none; }
.footer-desc { font-size: 12px; line-height: 1.8; }
/* 二维码列居中, 宽度受限 */
.footer-qr { display: flex; align-items: center; gap: 12px; justify-content: flex-start; }
.qr-image { width: 80px; height: 80px; }
.qr-label { margin-top: 0; font-size: 12px; }
/* 备案/版权底部: 垂直堆叠 */
.footer-bottom {
flex-direction: column;
align-items: flex-start;
gap: 6px;
padding: 14px 0;
font-size: 11px;
line-height: 1.6;
}
}
</style>
+106
View File
@@ -4,13 +4,24 @@
v-if="!route.meta?.hideMenu"
width="220px"
class="aside"
:class="{ 'aside-mobile-open': mobileAsideOpen }"
>
<transition name="aside-fade">
<div v-if="mobileAsideOpen" class="aside-mask" @click="mobileAsideOpen = false"></div>
</transition>
<div class="aside-logo">
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
<div class="logo-text">
<div class="logo-name">合规管理系统</div>
<div class="logo-role">{{ roleLabel }}</div>
</div>
<!-- 手机端关闭抽屉按钮 -->
<button class="aside-close" type="button" aria-label="关闭菜单" @click="mobileAsideOpen = false">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="6" y1="18" x2="18" y2="6"/>
</svg>
</button>
</div>
<el-menu :default-active="route.path" router class="aside-menu">
<template v-for="m in menu" :key="m.path">
@@ -36,6 +47,13 @@
<el-container>
<el-header v-if="!route.meta?.hideMenu" class="topbar" height="56px">
<div class="topbar-left">
<button class="hamburger" type="button" aria-label="打开菜单" @click="mobileAsideOpen = true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<el-icon class="topbar-back" @click="$router.back()"><ArrowLeft /></el-icon>
<span class="page-title-inline">{{ headerTitle }}</span>
</div>
@@ -93,6 +111,9 @@ const router = useRouter()
const store = useUserStore()
const role = computed(() => route.meta?.role || store.role)
// 移动端侧栏抽屉状态 (汉堡按钮触发)
const mobileAsideOpen = ref(false)
// 顶栏标题: 页面动态标题 (如会议 新建/修改/复制) 优先, 否则回退 route.meta.title
const dynamicTitle = usePageTitle()
const headerTitle = computed(() => dynamicTitle.value || route.meta?.title || '工作台')
@@ -164,6 +185,8 @@ onMounted(() => {
stopWatchRoute = watch(() => route.path, () => {
loadInitialUnread()
loadExpertAuditStatus()
// 移动端侧栏抽屉: 路由变化自动关闭 (避免点完菜单后抽屉还遮住主内容)
mobileAsideOpen.value = false
})
})
onBeforeUnmount(() => {
@@ -404,4 +427,87 @@ const onCommand = (cmd) => { if (cmd === 'logout') logout() }
.main { background: var(--brand-slate-50); padding: 16px 20px; min-height: calc(100vh - 56px); }
/* 手机页面 (hideMenu): 白底 + 0 padding, 让 form 直接撑满 */
.main-full { background: #fff; padding: 0; }
/* ========== 移动端适配 (≤768px) ========== */
/* 顶栏: padding 收窄 + 简化用户区 */
@media (max-width: 768px) {
.admin-layout {
flex-direction: column;
}
/* 侧栏: 桌面常驻, 手机转抽屉 (默认隐藏在屏幕左外) */
.aside {
position: fixed !important;
top: 0;
left: 0;
width: 240px !important;
max-width: 80vw;
height: 100vh;
z-index: 2000;
background: #fff;
transform: translateX(-100%);
transition: transform 0.25s ease;
box-shadow: none;
}
.aside.aside-mobile-open {
transform: translateX(0);
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
}
.aside-close {
display: inline-flex !important;
margin-left: auto;
background: transparent;
border: none;
color: var(--el-text-color-secondary);
padding: 6px;
border-radius: 4px;
cursor: pointer;
}
.aside-close:hover { background: var(--brand-slate-100); }
.aside-logo { padding: 16px 16px 14px !important; }
/* 顶栏: padding 收窄 + 隐藏门户首页 + 用户名只显示图标 */
.topbar { padding: 0 12px !important; height: 52px !important; }
.topbar-portal { display: none !important; } /* 顶部"门户首页"链接手机端隐藏, 改在抽屉 */
.topbar-portal + .el-divider { display: none !important; }
.page-title-inline { font-size: 14px !important; max-width: 50vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-name { padding: 0 6px !important; }
.user-name > span:not(.caret):not(.user-icon) { display: none !important; } /* 隐藏用户名文字, 只留头像图标 */
.user-name .user-icon { font-size: 18px !important; }
/* 汉堡按钮: 桌面隐藏, 手机显示 */
.hamburger {
display: inline-flex !important;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--el-text-color-regular);
padding: 6px;
border-radius: 4px;
cursor: pointer;
transition: background 0.18s, color 0.18s;
}
.hamburger:hover { background: var(--brand-slate-100); color: var(--brand-primary); }
/* 主体: padding 收窄 */
.main { padding: 12px !important; min-height: calc(100vh - 52px); }
}
/* aside-close 桌面隐藏 */
.aside-close { display: none; }
.hamburger { display: none; }
/* 移动端侧栏抽屉打开时的蒙层 (放在 layout 顶层, 透过 admin-layout 主结构显示) */
@media (max-width: 768px) {
.admin-layout { position: relative; }
.aside-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1999;
}
}
.aside-fade-enter-active, .aside-fade-leave-active { transition: opacity 0.2s; }
.aside-fade-enter-from, .aside-fade-leave-to { opacity: 0; }
</style>
+1 -1
View File
@@ -990,6 +990,6 @@ onUnmounted(() => {
.login-page .main-content { flex-direction: column !important; padding: 20px !important; gap: 0 !important; }
.login-page .banner-section { display: none !important; }
.login-page .login-section { width: 100% !important; max-width: 100% !important; padding: 0 !important; flex: 0 0 auto !important; }
.login-page .login-card { width: calc(100% - 10px) !important; max-width: none !important; margin: 0 auto !important; }
.login-page .login-card { width: calc(100% - 10px) !important; max-width: none !important; margin: 0 auto !important; height: auto !important; min-height: 0 !important; padding: 24px 20px !important; }
}
</style>
+236 -1
View File
@@ -34,8 +34,55 @@
</el-dropdown>
</template>
</div>
<!-- 手机端汉堡按钮 (桌面隐藏) -->
<button class="hamburger" type="button" aria-label="打开菜单" @click="drawerOpen = true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</header>
<!-- 手机端抽屉菜单 (桌面隐藏) -->
<transition name="drawer-fade">
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false"></div>
</transition>
<transition name="drawer-slide">
<aside v-if="drawerOpen" class="drawer-panel" role="dialog" aria-label="导航菜单">
<div class="drawer-header">
<span class="drawer-title">导航菜单</span>
<button class="drawer-close" type="button" aria-label="关闭菜单" @click="drawerOpen = false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="6" y1="18" x2="18" y2="6"/>
</svg>
</button>
</div>
<nav class="drawer-nav">
<a class="drawer-link" @click.prevent="goHomeAndClose">年度项目规划</a>
<a class="drawer-link" @click.prevent="goPublicityAndClose">项目公示</a>
</nav>
<div class="drawer-divider"></div>
<div class="drawer-tools">
<template v-if="!loggedIn">
<span class="drawer-login" @click="goLoginAndClose">登录</span>
</template>
<template v-else>
<div class="drawer-user">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>{{ userName }}</span>
</div>
<a class="drawer-link" @click.prevent="goAccountAndClose">我的主页</a>
<a class="drawer-link danger" @click.prevent="goLogoutAndClose">退出登录</a>
</template>
</div>
</aside>
</transition>
<div class="container">
<section class="hero">
<div>
@@ -97,6 +144,7 @@ const userStore = useUserStore()
const isScrolled = ref(false)
const specialPlans = ref([])
const drawerOpen = ref(false)
async function loadSpecialPlans() {
try {
@@ -183,13 +231,25 @@ function handleScroll() {
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
loadSpecialPlans()
// portal home 移动端适配: 解除 body min-width (避免手机端 1354px 横向滚动)
document.body.classList.add('home-page-body')
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
document.body.classList.remove('home-page-body')
})
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
function onHome() { isScrolled.value = false }
function goLogin() { router.push('/login') }
function goUser() { /* placeholder */ }
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
// 抽屉版导航: 跳转后关闭抽屉
function goHomeAndClose() { drawerOpen.value = false; onHome() }
function goPublicityAndClose() { drawerOpen.value = false; goPublicity() }
function goLoginAndClose() { drawerOpen.value = false; goLogin() }
function goAccountAndClose() { drawerOpen.value = false; onUserCmd('account') }
async function goLogoutAndClose() { drawerOpen.value = false; await goLogout() }
async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout()
if (cmd === 'account') {
@@ -604,4 +664,179 @@ a { color: inherit; text-decoration: none; }
text-decoration: none;
}
/* ========== 移动端适配 ========== */
@media (max-width: 768px) {
/* 解除 body 最小宽度 (桌面 1354px 在手机端会横向滚动) */
body { min-width: 0 !important; }
/* 顶栏: 高度收窄 + padding 减小 */
.top-nav { height: 56px !important; padding: 0 16px !important; }
.logo-title { font-size: 14px !important; }
.logo-subtitle { display: none; }
/* 桌面 nav-list + 顶部 tools 隐藏, 改用汉堡 */
.nav-list { display: none !important; }
.top-tools { display: none !important; }
.hamburger { display: inline-flex !important; }
/* 主体容器 padding 收紧 */
.container { padding: 0 16px !important; }
.specialty-inner { padding: 0 16px !important; }
/* Hero: 垂直堆叠 + 高度自适应 + CTA 不再绝对定位 */
.hero {
margin-top: 12px !important;
height: auto !important;
min-height: 220px !important;
padding: 24px 20px 20px !important;
flex-direction: column !important;
align-items: flex-start !important;
}
.hero-tag { margin-bottom: 10px !important; }
.hero-title { font-size: 24px !important; letter-spacing: 1px !important; }
.hero-subtitle { font-size: 13px !important; letter-spacing: 2px !important; margin-bottom: 14px !important; }
.hero-desc { font-size: 13px !important; line-height: 1.7 !important; padding-bottom: 16px !important; }
.hero-desc br { display: none; }
.hero-cta {
position: static !important;
transform: none !important;
flex-direction: row !important;
width: 100%;
margin-top: 8px;
}
.hero-cta .cta-btn { padding: 10px 28px !important; font-size: 13px !important; }
/* 总述区 */
.overview-section { padding: 32px 0 20px !important; }
.overview-text { padding: 18px 16px !important; font-size: 13px !important; }
.plan-image { padding: 16px !important; }
.section-title { font-size: 17px !important; margin-bottom: 16px !important; }
/* 七大专项: 单列 */
.specialty-section { padding: 32px 0 24px !important; }
.specialty-list { grid-template-columns: 1fr !important; gap: 12px !important; }
.specialty-no { width: 56px !important; font-size: 22px !important; }
.specialty-body { padding: 14px 16px !important; }
.specialty-title { font-size: 14px !important; }
}
/* 汉堡按钮 (桌面隐藏, 手机显示) */
.hamburger {
display: none;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: #fff;
padding: 8px;
cursor: pointer;
margin-left: auto;
border-radius: 4px;
transition: background 0.2s;
}
.hamburger:hover { background: rgba(255, 255, 255, 0.12); }
.hamburger:active { background: rgba(255, 255, 255, 0.2); }
/* ========== 抽屉 (手机端) ========== */
.drawer-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1999;
}
.drawer-panel {
position: fixed;
top: 0;
right: 0;
width: 260px;
max-width: 80vw;
height: 100vh;
background: #fff;
z-index: 2000;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
padding: 0 16px 0 20px;
border-bottom: 1px solid var(--brand-slate-200);
flex-shrink: 0;
}
.drawer-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.drawer-close {
background: transparent;
border: none;
color: var(--el-text-color-secondary);
cursor: pointer;
padding: 6px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
}
.drawer-close:hover { background: var(--brand-slate-100); color: var(--el-text-color-primary); }
.drawer-nav { display: flex; flex-direction: column; padding: 8px 0; }
.drawer-link {
display: block;
padding: 12px 20px;
font-size: 14px;
color: var(--el-text-color-primary);
cursor: pointer;
transition: background 0.15s, color 0.15s;
border-left: 3px solid transparent;
}
.drawer-link:hover { background: var(--brand-slate-50); }
.drawer-link.danger { color: var(--el-color-danger); }
.drawer-divider { height: 1px; background: var(--brand-slate-200); margin: 4px 20px; }
.drawer-tools { display: flex; flex-direction: column; padding: 8px 0; gap: 0; }
.drawer-user {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
font-size: 13px;
color: var(--el-text-color-secondary);
border-bottom: 1px solid var(--brand-slate-100);
margin-bottom: 4px;
}
.drawer-user svg { color: var(--brand-primary); }
.drawer-login {
display: block;
margin: 4px 20px;
padding: 10px 0;
text-align: center;
background: var(--brand-primary);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
transition: opacity 0.2s;
}
.drawer-login:hover { opacity: 0.9; }
/* 抽屉过渡 */
.drawer-fade-enter-active, .drawer-fade-leave-active { transition: opacity 0.2s; }
.drawer-fade-enter-from, .drawer-fade-leave-to { opacity: 0; }
.drawer-slide-enter-active, .drawer-slide-leave-active { transition: transform 0.25s ease; }
.drawer-slide-enter-from, .drawer-slide-leave-to { transform: translateX(100%); }
</style>
<!-- body 是顶层选择器, scoped 模式无法覆盖, 单独非 scoped 块处理移动端 min-width -->
<style>
@media (max-width: 768px) {
body.home-page-body { min-width: 0 !important; }
}
</style>