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
+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>