vivid 手机端导航改抽屉:顶部玻璃条(品牌+汉堡)+ 右侧滑入面板,大字栏目带 mono 英文脚注与当前项短杠,Esc/scrim/路由变更自动收起
This commit is contained in:
1 parent
d3fe454cbe
commit
f618bc48f5
2 files changed
+168
-43
No files matched your search
@@ -1,26 +1,31 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { RouterLink, useRoute } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
import { site } from '../../site'
|
import { site } from '../../site'
|
||||||
|
import NavBurger from '../../components/NavBurger.vue'
|
||||||
import AuthorAvatar from '../../components/AuthorAvatar.vue'
|
import AuthorAvatar from '../../components/AuthorAvatar.vue'
|
||||||
|
|
||||||
// 余白顶栏。对应设计稿的 <header class="top">:品牌 / 导航 / 操作三块,
|
// 余白顶栏。对应设计稿的 <header class="top">:品牌 / 导航 / 操作三块,
|
||||||
// 滚动过 8px 加 .scrolled(设计稿的「四态里的第一态」)。
|
// 滚动过 8px 加 .scrolled(设计稿的「四态里的第一态」)。
|
||||||
// 样式在 ui/yohaku/chrome.css,选择器已收敛到 html[data-ui="vivid"] 下。
|
// 样式在 ui/yohaku/chrome.css,选择器已收敛到 html[data-ui="vivid"] 下。
|
||||||
//
|
//
|
||||||
// 窄屏例外:顶栏整条退场,换成一枚常驻的顶部玻璃标签栏(.v-tabbar)——
|
// 窄屏例外:内联导航退场,换成一枚常驻的顶部玻璃条(品牌 + 汉堡)+
|
||||||
// 五个栏目一次点击直达,不再需要汉堡 + 上拉菜单。明暗切换在手机上挪到
|
// 右侧抽屉(.drawer,chrome.css)。抽屉是 aria-modal 对话框:开着时锁背景
|
||||||
// 页脚(YohakuFoot 的 .theme-toggle),桌面仍走顶栏这枚 ◐。
|
// 滚动、Esc 收起、焦点交给面板,路由一变自动收。
|
||||||
|
// 明暗切换在抽屉 footer 和页脚各有一枚(≤720px 才显示),桌面走顶栏 ◐。
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const scrolled = ref(false)
|
const scrolled = ref(false)
|
||||||
|
const menuOpen = ref(false)
|
||||||
|
const burger = ref(null)
|
||||||
|
const drawerEl = ref(null)
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ to: '/', label: '时间线' },
|
{ to: '/', label: '时间线', en: 'Timeline' },
|
||||||
{ to: '/archive', label: '归档' },
|
{ to: '/archive', label: '归档', en: 'Archive' },
|
||||||
{ to: '/tags', label: '标签' },
|
{ to: '/tags', label: '标签', en: 'Tags' },
|
||||||
{ to: '/projects', label: '作品' },
|
{ to: '/projects', label: '作品', en: 'Projects' },
|
||||||
{ to: '/about', label: '关于' }
|
{ to: '/about', label: '关于', en: 'About' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const title = () => site.site_title?.split(/[·|]/)[0]?.trim() || 'ONE'
|
const title = () => site.site_title?.split(/[·|]/)[0]?.trim() || 'ONE'
|
||||||
@@ -33,12 +38,47 @@ function onScroll() {
|
|||||||
scrolled.value = (window.scrollY || 0) > 8
|
scrolled.value = (window.scrollY || 0) > 8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
menuOpen.value ? closeMenu() : openMenu()
|
||||||
|
}
|
||||||
|
function openMenu() {
|
||||||
|
menuOpen.value = true
|
||||||
|
// 焦点交给对话框本身(aria-modal 承诺过),Tab 第一下才落到「时间线」。
|
||||||
|
// 不直接 focus 第一个链接:即使 touch 上不长出焦点环,也会让首项看起来
|
||||||
|
// 「被选中」,和 .on 的高亮抢意思。preventScroll 防止 iOS 顺手滚背景。
|
||||||
|
nextTick(() => drawerEl.value?.focus({ preventScroll: true }))
|
||||||
|
}
|
||||||
|
function closeMenu() {
|
||||||
|
menuOpen.value = false
|
||||||
|
const b = burger.value?.$el || burger.value
|
||||||
|
nextTick(() => b?.focus?.({ preventScroll: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽屉铺满全屏时锁住底层滚动(iOS 上不锁背景会跟着手指滑)
|
||||||
|
watch(menuOpen, (v) => { document.body.style.overflow = v ? 'hidden' : '' })
|
||||||
|
// 点了链接 → 路由变了 → 抽屉收起
|
||||||
|
watch(() => route.fullPath, () => { menuOpen.value = false })
|
||||||
|
|
||||||
|
function onKey(e) {
|
||||||
|
if (e.key === 'Escape') closeMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口拉宽回桌面必须自己退场:v-if 是 JS 状态,CSS 媒体查询管不着它
|
||||||
|
const mqWide = window.matchMedia('(min-width: 721px)')
|
||||||
|
function onWide(e) { if (e.matches) menuOpen.value = false }
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('scroll', onScroll, { passive: true })
|
window.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
mqWide.addEventListener('change', onWide)
|
||||||
onScroll()
|
onScroll()
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('scroll', onScroll)
|
window.removeEventListener('scroll', onScroll)
|
||||||
|
window.removeEventListener('keydown', onKey)
|
||||||
|
mqWide.removeEventListener('change', onWide)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
menuOpen.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设计稿里主题键是 ◐:亮色时显示(点它去暗),暗色时切回亮色。
|
// 设计稿里主题键是 ◐:亮色时显示(点它去暗),暗色时切回亮色。
|
||||||
@@ -71,16 +111,53 @@ function toggleTheme() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 窄屏:顶部玻璃标签栏。五个栏目平铺一次直达,当前项用分段高亮
|
<!-- 窄屏:顶部玻璃条(品牌 + 汉堡)。桌面 display:none。
|
||||||
(与首页工具条「全部 / 长文 / 短文」的分段控件同一语言)。
|
|
||||||
必须待在 .top 外面:.top 有 backdrop-filter,会给 fixed 后代当包含块。 -->
|
必须待在 .top 外面:.top 有 backdrop-filter,会给 fixed 后代当包含块。 -->
|
||||||
<nav class="v-tabbar" aria-label="站点导航">
|
<header class="top-m">
|
||||||
<RouterLink
|
<RouterLink class="top-m-brand" to="/">
|
||||||
v-for="l in links"
|
<span class="av"><AuthorAvatar /></span>
|
||||||
:key="l.to"
|
<span>{{ title() }}</span>
|
||||||
:to="l.to"
|
</RouterLink>
|
||||||
:class="{ on: isOn(l) }"
|
<NavBurger
|
||||||
:aria-current="isOn(l) ? 'page' : undefined"
|
ref="burger"
|
||||||
>{{ l.label }}</RouterLink>
|
:open="menuOpen"
|
||||||
</nav>
|
aria-controls="v-drawer"
|
||||||
|
:aria-label="menuOpen ? '关闭菜单' : '打开菜单'"
|
||||||
|
@toggle="toggleMenu"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 窄屏抽屉:scrim + 右侧滑入的面板。品牌条压在 scrim 之上不消失,
|
||||||
|
汉堡原位并成 ✕,不需要重复的菜单头。 -->
|
||||||
|
<Transition name="v-drawer">
|
||||||
|
<div
|
||||||
|
v-if="menuOpen"
|
||||||
|
id="v-drawer"
|
||||||
|
ref="drawerEl"
|
||||||
|
class="drawer-root"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="站点导航"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="scrim" @click="closeMenu" />
|
||||||
|
<aside class="drawer">
|
||||||
|
<nav class="drawer-nav" aria-label="站点导航">
|
||||||
|
<RouterLink
|
||||||
|
v-for="l in links"
|
||||||
|
:key="l.to"
|
||||||
|
:to="l.to"
|
||||||
|
:class="{ on: isOn(l) }"
|
||||||
|
>
|
||||||
|
<span class="zh">{{ l.label }}</span>
|
||||||
|
<em>{{ l.en }}</em>
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
<div class="drawer-foot">
|
||||||
|
<button class="chip" type="button" @click="toggleTheme">◐ 切换明暗</button>
|
||||||
|
<p v-if="site.site_desc" class="m-desc">{{ site.site_desc }}</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,36 +50,84 @@ html[data-ui='vivid'] .menu-btn {display:none}
|
|||||||
html[data-ui='vivid'] .top {display:none}
|
html[data-ui='vivid'] .top {display:none}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 窄屏:顶部玻璃标签栏 ----
|
/* ---- 窄屏:顶部玻璃条 + 右侧抽屉 ----
|
||||||
五个栏目平铺一次直达(替代旧的 dock + 汉堡 + 上拉菜单,那套要两跳才能到
|
条上只留品牌和汉堡(fixed,body 顶部让位),导航收进右侧抽屉:
|
||||||
目标页)。与桌面顶栏同构:全宽、毛玻璃、发丝线;当前项用分段高亮,
|
大字栏目 + 当前项左缘短杠 + mono 英文脚注,底部是明暗切换。
|
||||||
和首页工具条「全部 / 长文 / 短文」的分段控件同一语言。
|
条的 z-index 压在抽屉 scrim 之上:开着时汉堡原位并成 ✕,不消失。 */
|
||||||
fixed + body 顶部让位(sticky 的祖先链里只要有 overflow 容器就会失效,
|
html[data-ui='vivid'] .top-m {position:fixed;z-index:120;
|
||||||
fixed 最稳);safe-area-top 给刘海屏 PWA 留出来。 */
|
|
||||||
html[data-ui='vivid'] .v-tabbar {position:fixed;z-index:90;
|
|
||||||
top:0;left:0;right:0;
|
top:0;left:0;right:0;
|
||||||
display:none;align-items:stretch;justify-content:center;gap:2px;
|
display:none;align-items:center;justify-content:space-between;
|
||||||
padding:max(10px, env(safe-area-inset-top)) 10px 8px;
|
padding:max(8px, env(safe-area-inset-top)) 10px 8px 14px;
|
||||||
border-bottom:1px solid var(--line);
|
border-bottom:1px solid var(--line);
|
||||||
background:color-mix(in srgb,var(--card) 82%,transparent);
|
background:color-mix(in srgb,var(--card) 82%,transparent);
|
||||||
backdrop-filter:blur(20px) saturate(1.5);
|
backdrop-filter:blur(20px) saturate(1.5);
|
||||||
-webkit-backdrop-filter:blur(20px) saturate(1.5);}
|
-webkit-backdrop-filter:blur(20px) saturate(1.5);}
|
||||||
/* 窄屏才把标签栏摆出来;这条必须压在基准规则(display:none)之后 */
|
html[data-ui='vivid'] .top-m-brand {display:flex;align-items:center;gap:10px;
|
||||||
|
font-weight:700;font-size:14px;letter-spacing:.02em;color:var(--ink);}
|
||||||
|
html[data-ui='vivid'] .top-m-brand .av {width:28px;height:28px;flex:none;color:var(--ink);}
|
||||||
|
/* 窄屏才摆出来;这条必须压在基准规则(display:none)之后 */
|
||||||
@media (max-width:720px) {
|
@media (max-width:720px) {
|
||||||
html[data-ui='vivid'] .v-tabbar {display:flex}
|
html[data-ui='vivid'] .top-m {display:flex}
|
||||||
/* 内容区顶部让位:栏高 = 44 触达 + 18 上下内边距 + 1 发丝线 ≈ 63px */
|
/* 内容区顶部让位:条高 = 44 触达 + 16 上下内边距 + 1 发丝线 ≈ 61px */
|
||||||
html[data-ui='vivid'] body {padding-top:calc(max(10px, env(safe-area-inset-top)) + 63px)}
|
html[data-ui='vivid'] body {padding-top:calc(max(8px, env(safe-area-inset-top)) + 61px)}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 抽屉:scrim 全屏淡入,面板从右侧滑入(spring),玻璃面板圆左角 */
|
||||||
|
html[data-ui='vivid'] .drawer-root {position:fixed;inset:0;z-index:110;}
|
||||||
|
html[data-ui='vivid'] .drawer-root .scrim {position:absolute;inset:0;
|
||||||
|
background:color-mix(in srgb,var(--ink) 32%,transparent);}
|
||||||
|
html[data-ui='vivid'] .drawer {position:absolute;top:0;right:0;bottom:0;
|
||||||
|
width:min(80vw, 320px);
|
||||||
|
border-left:1px solid var(--line);
|
||||||
|
border-radius:24px 0 0 24px;
|
||||||
|
background:color-mix(in srgb,var(--card) 92%,transparent);
|
||||||
|
backdrop-filter:blur(28px) saturate(1.6);
|
||||||
|
-webkit-backdrop-filter:blur(28px) saturate(1.6);
|
||||||
|
box-shadow:var(--shadow-lg);
|
||||||
|
display:flex;flex-direction:column;
|
||||||
|
padding:calc(max(8px, env(safe-area-inset-top)) + 66px) 22px max(14px, env(safe-area-inset-bottom));
|
||||||
|
overscroll-behavior:contain;
|
||||||
|
overflow-y:auto;
|
||||||
|
scrollbar-width:none;}
|
||||||
|
html[data-ui='vivid'] .drawer:focus {outline:none}
|
||||||
|
html[data-ui='vivid'] .drawer-nav {display:flex;flex-direction:column;}
|
||||||
|
html[data-ui='vivid'] .drawer-nav a {position:relative;display:flex;align-items:baseline;gap:12px;
|
||||||
|
padding:15px 4px;font-size:24px;font-weight:800;letter-spacing:-.02em;
|
||||||
|
line-height:1.25;color:var(--soft);
|
||||||
|
transition:color .3s var(--ease);}
|
||||||
|
html[data-ui='vivid'] .drawer-nav a em {font-style:normal;font-size:11px;font-weight:700;
|
||||||
|
letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-left:auto;
|
||||||
|
transition:color .3s var(--ease);}
|
||||||
|
html[data-ui='vivid'] .drawer-nav a.on {color:var(--accent)}
|
||||||
|
html[data-ui='vivid'] .drawer-nav a.on em {color:var(--accent)}
|
||||||
|
/* 当前项的左缘短杠:面板内边距 22px,杠落在离屏边 10px 处 */
|
||||||
|
html[data-ui='vivid'] .drawer-nav a.on::before {content:"";position:absolute;left:-18px;top:50%;
|
||||||
|
width:3px;height:18px;border-radius:2px;background:var(--accent);
|
||||||
|
transform:translateY(-50%);}
|
||||||
|
html[data-ui='vivid'] .drawer-nav a:active {color:var(--accent)}
|
||||||
|
html[data-ui='vivid'] .drawer-foot {margin-top:auto;padding-top:16px;
|
||||||
|
display:flex;flex-direction:column;align-items:flex-start;gap:10px;}
|
||||||
|
html[data-ui='vivid'] .drawer-foot .chip {padding:6px 14px;border-radius:999px;
|
||||||
|
border:1px solid var(--line);background:transparent;
|
||||||
|
font-size:12.5px;font-weight:600;color:var(--soft);cursor:pointer;
|
||||||
|
transition:color .25s var(--ease),border-color .25s var(--ease);}
|
||||||
|
html[data-ui='vivid'] .drawer-foot .chip:active {color:var(--accent);border-color:var(--accent)}
|
||||||
|
html[data-ui='vivid'] .drawer-foot .m-desc {margin:0;font-size:12px;line-height:1.7;
|
||||||
|
color:var(--muted);}
|
||||||
|
/* 进:scrim 淡入 + 面板从右侧滑入;出:整块滑回去 */
|
||||||
|
html[data-ui='vivid'] .v-drawer-enter-active {transition:opacity .3s var(--ease)}
|
||||||
|
html[data-ui='vivid'] .v-drawer-enter-active .drawer {transition:transform .55s var(--spring),opacity .35s var(--ease)}
|
||||||
|
html[data-ui='vivid'] .v-drawer-enter-from {opacity:0}
|
||||||
|
html[data-ui='vivid'] .v-drawer-enter-from .drawer {transform:translateX(60px);opacity:0}
|
||||||
|
html[data-ui='vivid'] .v-drawer-leave-active {transition:opacity .22s var(--ease)}
|
||||||
|
html[data-ui='vivid'] .v-drawer-leave-active .drawer {transition:transform .22s var(--ease)}
|
||||||
|
html[data-ui='vivid'] .v-drawer-leave-to {opacity:0}
|
||||||
|
html[data-ui='vivid'] .v-drawer-leave-to .drawer {transform:translateX(48px)}
|
||||||
|
/* 全局 reduce-motion 只把 duration 压扁,delay 会留下来 → 单独清掉 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html[data-ui='vivid'] .v-drawer-enter-active,
|
||||||
|
html[data-ui='vivid'] .v-drawer-enter-active .drawer {transition-delay:0s !important}
|
||||||
}
|
}
|
||||||
html[data-ui='vivid'] .v-tabbar a {flex:1;display:flex;align-items:center;justify-content:center;
|
|
||||||
max-width:96px;
|
|
||||||
min-height:44px;padding:0 2px;border-radius:999px;
|
|
||||||
font-size:13px;font-weight:600;letter-spacing:.02em;
|
|
||||||
color:var(--soft);
|
|
||||||
transition:color .25s var(--ease),background-color .25s var(--ease),transform 120ms ease-out;}
|
|
||||||
/* 按下即有物理反馈(触屏没有 hover,:active 就是全部) */
|
|
||||||
html[data-ui='vivid'] .v-tabbar a:active {transform:scale(.94);color:var(--accent)}
|
|
||||||
html[data-ui='vivid'] .v-tabbar a.on {color:var(--accent);font-weight:700;
|
|
||||||
background:color-mix(in srgb,var(--accent) 11%,transparent);}
|
|
||||||
|
|
||||||
/* 品牌位只管尺寸:头像本身已裁成圆(AuthorAvatar 的 img),外面不加光环、
|
/* 品牌位只管尺寸:头像本身已裁成圆(AuthorAvatar 的 img),外面不加光环、
|
||||||
右下角也不加在线点——顶栏要的就是一张干净的圆头像。color 供 LogoMark
|
右下角也不加在线点——顶栏要的就是一张干净的圆头像。color 供 LogoMark
|
||||||
|
|||||||
Reference in new issue
Block a user