余白 UI:手机端适配(☰ 全屏菜单 + 筛选条收口)+ 若干层叠与卡片修正
手机端(≤720 / ≤640) - 窄屏导航不再平铺:设计稿在窄屏把导航整个 display:none,手机上会没有任何 入口,之前的做法是在品牌行下再摊一条横向滚动行,5 个中文项挤成一条读起来 很碎。改为顶栏只留「头像 + ☰」,点 ☰ 开全屏菜单:竖排大字 + 英文小标、 当前项高亮、底部放主题切换;顶行复用同款标头(关掉时视觉不跳), Esc / 点击空白 / 路由变化 / 窗口拉宽回桌面 都会关闭, 打开时锁背景滚动并把焦点交给对话框(preventScroll) - 顶栏主题切换 ◐ 在窄屏收进 ☰:两边各一个就是重复。桌面没有 ☰, 且那枚 ◐ 是设计稿顶栏自带的(index.html:782),所以只在窄屏藏 - 筛选条:≤640 收起标签 chips(会折三四行)与搜索框,只留类型分段; 标签仍有 /tags 整页入口 - viewport 补 viewport-fit=cover / interactive-widget;触屏补 -webkit-tap-highlight-color、touch-action:manipulation、44px 可点目标、 16px 输入框(防 iOS 聚焦缩放) 层叠(这次量出来的真问题) - 新增 mobile.css 并在 main.ts 里最后 import:.hero/.wrap/.paper/.about-card/ .toolbar 的窄屏覆盖原本写在 chrome.css(第 2 个 import),同特指度被 home/article/pages.css 全程压掉 —— 媒体查询不加特指度,「后导入者赢」, 所以手机上左右留白一直是 28px 而不是设计意图的 18px,圆角/内边距也没换过。 现统一到 20px,实测 hero / wrap / showcase 左边缘对齐; 同时删掉 home.css 里那条 ≤860 的 showcase 18px(与同宽度下 hero 的 28px 错开) - .icon-btn 的 44px 触屏尺寸从 tokens.css 挪回 chrome.css 自己那边: 原位置被后导入的同特指度 36px 反压,触屏上实际是 36×44 的椭圆 - .toolbar 防线补 border:0:只中和 top 会漏出左右两道边框 - 后台 3 处 100vh 补 100dvh 兜底;9 处 transition:all 展开为显式属性列表; 19 处 :hover 收进 @media (hover:hover) and (pointer:fine)(触屏不再粘住 hover) 组件修正 - 首页作品卡片改用与 /projects 同一套结构:文字包进 .body(.proj 自身 padding:0,原来 h3/p 直接贴左边框)、有封面才渲染 .cov(原来无封面时留 150px 空色块)、无封面时用 .st-plain 显示状态 - .proj-grid 轨道下限改 min(300px,100%):320px 视口下裸 300px 会顶破容器, 被 body 的 overflow-x:hidden 裁掉右边 - 「全部作品 / 查看所有作品」改 RouterLink(原为 <a href>,会整页重载) 清理 - 删除 panels.css:.panels/.panel/.cloud/.plist/.about-box 在 vivid 里已无任何 使用者(后台走 data-ui=classic,命中不到),连带 tokens.css 里的死规则与 main.ts 的 import - 修正注释里残留的 panels 描述 验证:headless Chromium 同源 iframe 探针 + 截图,390 / 320 / 1200 三档; /admin 确认 classic 未被带坏;npm run build 无警告。
This commit is contained in:
@@ -1,33 +1,78 @@
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { site } from '../../site'
|
||||
|
||||
// 余白顶栏。对应设计稿的 <header class="top">:品牌 / 导航 / 操作三块,
|
||||
// 滚动过 8px 加 .scrolled(设计稿的「四态里的第一态」)。
|
||||
// 样式在 ui/yohaku/chrome.css,选择器已收敛到 html[data-ui="vivid"] 下。
|
||||
//
|
||||
// 窄屏例外:设计稿在窄屏把导航整个 display:none 掉了,手机上会没有任何导航入口。
|
||||
// 这里补一个 ☰ + 全屏菜单(chrome.css 的 .menu / .menu-btn)。
|
||||
|
||||
const route = useRoute()
|
||||
const scrolled = ref(false)
|
||||
const menuOpen = ref(false)
|
||||
const burger = ref(null)
|
||||
const menuEl = ref(null)
|
||||
|
||||
const links = [
|
||||
{ to: '/', label: '时间线' },
|
||||
{ to: '/archive', label: '归档' },
|
||||
{ to: '/tags', label: '标签' },
|
||||
{ to: '/projects', label: '作品' },
|
||||
{ to: '/about', label: '关于' }
|
||||
{ to: '/', label: '时间线', en: 'Timeline' },
|
||||
{ to: '/archive', label: '归档', en: 'Archive' },
|
||||
{ to: '/tags', label: '标签', en: 'Tags' },
|
||||
{ to: '/projects', label: '作品', en: 'Projects' },
|
||||
{ to: '/about', label: '关于', en: 'About' }
|
||||
]
|
||||
|
||||
const initial = () => (site.author_name || site.site_title || 'O').trim().slice(0, 1).toUpperCase()
|
||||
const title = () => site.site_title?.split(/[·|]/)[0]?.trim() || 'ONE'
|
||||
|
||||
function isOn(l) {
|
||||
return l.to === '/' ? route.path === '/' : route.path.startsWith(l.to)
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
scrolled.value = (window.scrollY || 0) > 8
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
menuOpen.value = true
|
||||
// 焦点交给对话框本身(aria-modal 承诺过),Tab 第一下才落到「时间线」。
|
||||
// 不直接 focus 第一个链接:即使 touch 上不长出焦点环,也会让首项看起来「被选中」,
|
||||
// 和 .on 的高亮抢意思。preventScroll 防止 iOS 顺手滚背景。
|
||||
nextTick(() => menuEl.value?.focus({ preventScroll: true }))
|
||||
}
|
||||
function closeMenu() {
|
||||
menuOpen.value = false
|
||||
nextTick(() => burger.value?.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 wide = window.matchMedia('(min-width: 721px)')
|
||||
function onWide(e) { if (e.matches) menuOpen.value = false }
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
window.addEventListener('keydown', onKey)
|
||||
wide.addEventListener('change', onWide)
|
||||
onScroll()
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('scroll', onScroll))
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
window.removeEventListener('keydown', onKey)
|
||||
wide.removeEventListener('change', onWide)
|
||||
document.body.style.overflow = ''
|
||||
menuOpen.value = false
|
||||
})
|
||||
|
||||
// 设计稿里主题键是 ◐ / ◑:亮色时显示 ◐(点它去暗),暗色时显示 ◑。
|
||||
function toggleTheme() {
|
||||
@@ -43,19 +88,66 @@ function toggleTheme() {
|
||||
<div class="top-in">
|
||||
<RouterLink class="brand" to="/">
|
||||
<span class="av">{{ initial() }}</span>
|
||||
<span>{{ site.site_title?.split(/[·|]/)[0]?.trim() || 'ONE' }}</span>
|
||||
<span>{{ title() }}</span>
|
||||
</RouterLink>
|
||||
<nav class="nav" aria-label="站点导航">
|
||||
<RouterLink
|
||||
v-for="l in links"
|
||||
:key="l.to"
|
||||
:to="l.to"
|
||||
:class="{ on: l.to === '/' ? route.path === '/' : route.path.startsWith(l.to) }"
|
||||
:class="{ on: isOn(l) }"
|
||||
>{{ l.label }}</RouterLink>
|
||||
</nav>
|
||||
<div class="top-acts">
|
||||
<button class="icon-btn" type="button" title="切换明暗" aria-label="切换明暗" @click="toggleTheme">◐</button>
|
||||
<button class="icon-btn theme-btn" type="button" title="切换明暗" aria-label="切换明暗" @click="toggleTheme">◐</button>
|
||||
<button
|
||||
ref="burger"
|
||||
class="icon-btn menu-btn"
|
||||
type="button"
|
||||
aria-label="打开菜单"
|
||||
aria-controls="v-menu"
|
||||
:aria-expanded="menuOpen"
|
||||
@click="openMenu"
|
||||
>☰</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 窄屏全屏菜单。必须待在 .top 外面:.top 有 backdrop-filter,
|
||||
会给 position:fixed 的后代当包含块,铺不满屏。 -->
|
||||
<Transition name="v-menu">
|
||||
<div
|
||||
v-if="menuOpen"
|
||||
id="v-menu"
|
||||
ref="menuEl"
|
||||
class="menu"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="站点导航"
|
||||
tabindex="-1"
|
||||
@click.self="closeMenu"
|
||||
>
|
||||
<div class="menu-head">
|
||||
<span class="m-brand">
|
||||
<span class="av">{{ initial() }}</span>
|
||||
<span>{{ title() }}</span>
|
||||
</span>
|
||||
<button class="icon-btn" type="button" aria-label="关闭菜单" @click="closeMenu">✕</button>
|
||||
</div>
|
||||
<nav class="menu-in">
|
||||
<RouterLink
|
||||
v-for="l in links"
|
||||
:key="l.to"
|
||||
:to="l.to"
|
||||
:class="{ on: isOn(l) }"
|
||||
>
|
||||
<span>{{ l.label }}</span>
|
||||
<em>{{ l.en }}</em>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
<div class="menu-foot">
|
||||
<button class="chip" type="button" @click="toggleTheme">◐ 切换明暗</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user