Commit 04ea09b6 authored by shrabbit's avatar shrabbit
Browse files

fix: 修复布局与路由切换动画失效等多个问题

1. SectionHeading: 改为纵向 flex 布局,让 index/line/h2/p 全部垂直堆叠,解决 span 跑到左侧把标题挤到右侧的问题

2. useScrollReveal: 引入 MutationObserver 监听 DOM 变化,新增 isVisible() 同步判定,解决客户端路由切换后新页面元素卡在 opacity:0 不显示的问题

3. StepIndicator: 重写为 flex 布局(圆点 + 内容),用伪元素绘制连接线,确保内容列宽能正常扩张,修复文本被挤成一字一行的问题

4. CodeBlock 与各页面 grid 容器: 给 .code-block 根与所有容纳 CodeBlock 的 grid 项加 min-width:0,防止长代码行把网格列撑破溢出屏幕
parent c1c75928
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -91,6 +91,9 @@ const highlight = (line: string) => {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  line-height: 1.7;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  transition: border-color var(--duration-base) var(--ease-out-quint);
}

+23 −31
Original line number Diff line number Diff line
<template>
  <div class="section-heading" :data-reveal="reveal">
    <div class="section-heading__head">
      <span class="section-heading__index">{{ index }}</span>
    <div class="section-heading__title-group">
      <span class="section-heading__line" aria-hidden="true"></span>
    </div>
    <h2 class="section-heading__title">
      <slot name="title">{{ title }}</slot>
    </h2>
    <p v-if="subtitle" class="section-heading__subtitle">{{ subtitle }}</p>
  </div>
    <span class="section-heading__line"></span>
  </div>
</template>

<script setup lang="ts">
@@ -28,25 +28,32 @@ withDefaults(defineProps<Props>(), {

<style scoped>
.section-heading {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-5);
  align-items: end;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-12);
}

.section-heading__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.section-heading__index {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--color-accent);
  letter-spacing: 0.2em;
  padding-bottom: 6px;
  white-space: nowrap;
}

.section-heading__title-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
.section-heading__line {
  display: block;
  flex: 1;
  max-width: clamp(60px, 18vw, 220px);
  height: 1px;
  background: linear-gradient(90deg, var(--color-accent), transparent);
}

.section-heading__title {
@@ -63,19 +70,4 @@ withDefaults(defineProps<Props>(), {
  font-family: var(--font-mono);
  letter-spacing: 0.05em;
}

.section-heading__line {
  display: block;
  height: 1px;
  width: clamp(60px, 12vw, 180px);
  background: linear-gradient(90deg, var(--color-accent), transparent);
  margin-bottom: 14px;
}

@media (max-width: 768px) {
  .section-heading {
    grid-template-columns: auto 1fr;
  }
  .section-heading__line { display: none; }
}
</style>
+37 −51
Original line number Diff line number Diff line
<template>
  <div class="steps">
    <div class="steps__rail" aria-hidden="true">
      <span class="steps__rail-line"></span>
      <span class="steps__rail-progress" :style="{ height: progressHeight }"></span>
    </div>

    <ol class="steps__list">
      <li
        v-for="(step, i) in steps"
@@ -12,7 +7,7 @@
        class="steps__item"
        :class="{ 'is-active': active === i, 'is-done': active > i }"
      >
        <span class="steps__dot">
        <span class="steps__dot" aria-hidden="true">
          <span class="steps__dot-inner"></span>
        </span>
        <div class="steps__content">
@@ -37,45 +32,13 @@ const props = defineProps<{
}>()

const active = computed(() => props.active ?? props.steps.length - 1)

const progressHeight = computed(() => {
  const total = props.steps.length - 1
  const ratio = total > 0 ? active.value / total : 1
  return `${ratio * 100}%`
})
</script>

<style scoped>
.steps {
  position: relative;
  display: grid;
  grid-template-columns: 24px 1fr;
  gap: var(--space-4);
}

.steps__rail {
  position: relative;
  width: 1px;
  margin-left: 11px;
}

.steps__rail-line {
  position: absolute;
  top: 12px;
  bottom: 12px;
  left: 0;
  width: 1px;
  background: var(--color-line);
}

.steps__rail-progress {
  position: absolute;
  top: 12px;
  left: 0;
  width: 1px;
  background: linear-gradient(180deg, var(--color-accent), var(--color-accent-deep));
  transition: height 0.8s var(--ease-out-expo);
  box-shadow: 0 0 8px var(--color-accent-glow);
  width: 100%;
  min-width: 0;
}

.steps__list {
@@ -85,21 +48,39 @@ const progressHeight = computed(() => {
}

.steps__item {
  display: grid;
  grid-template-columns: 0 1fr;
  align-items: start;
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  padding: 0 0 var(--space-8) 0;
  position: relative;
}

.steps__item:last-child { padding-bottom: 0; }

/* 因为 rail 是另外一列,这里需要把圆点拉回到 rail 上 */
.steps__dot {
/* —— 连接线(伪元素,穿过每个 item) —— */
.steps__item::before {
  content: '';
  position: absolute;
  left: -27px;
  top: 8px;
  left: 11px;
  top: 22px;
  bottom: 0;
  width: 1px;
  background: var(--color-line);
}

.steps__item:last-child::before { display: none; }

.steps__item.is-active::before,
.steps__item.is-done::before {
  background: linear-gradient(180deg, var(--color-accent), var(--color-accent-deep));
  box-shadow: 0 0 8px var(--color-accent-glow);
}

/* —— 圆点 —— */
.steps__dot {
  position: relative;
  z-index: 1;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
@@ -107,8 +88,8 @@ const progressHeight = computed(() => {
  border: 1px solid var(--color-line-strong);
  display: grid;
  place-items: center;
  margin-top: 4px;
  transition: all var(--duration-base) var(--ease-out-quint);
  z-index: 1;
}

.steps__dot-inner {
@@ -138,7 +119,10 @@ const progressHeight = computed(() => {
  background: var(--color-bg-base);
}

/* —— 文本内容 —— */
.steps__content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
@@ -159,10 +143,11 @@ const progressHeight = computed(() => {

.steps__title {
  font-family: var(--font-serif);
  font-size: 1.25rem;
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--color-text-primary);
  line-height: 1.3;
  word-break: break-word;
}

.steps__item:not(.is-active):not(.is-done) .steps__title {
@@ -170,9 +155,10 @@ const progressHeight = computed(() => {
}

.steps__desc {
  font-size: 0.875rem;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  line-height: 1.6;
  margin-top: 4px;
  word-break: break-word;
}
</style>
+47 −8
Original line number Diff line number Diff line
/**
 * useScrollReveal
 * 基于 IntersectionObserver 实现滚动进入动画
 * 基于 IntersectionObserver 实现滚动进入动画
 * 使用方式:HTML 元素上加 [data-reveal] 属性即可,可选值:
 *   data-reveal=""          → fade-in-up(默认)
 *   data-reveal="fade"      → 仅 fade
@@ -8,9 +8,39 @@
 *   data-reveal="left"      → 从左滑入
 *   data-reveal="right"     → 从右滑入
 *   data-stagger="N"        → 错峰 N 步
 *
 * 内部通过 MutationObserver 监听 DOM 变化,保证:
 *   - 首次加载时所有 [data-reveal] 被观察
 *   - 客户端路由切换时新挂载的 [data-reveal] 自动被观察
 *   - 异步插入的内容(如 v-if 切换)也会被捕获
 */
export function useScrollReveal() {
  let observer: IntersectionObserver | null = null
  let mo: MutationObserver | null = null

  const isVisible = (el: Element): boolean => {
    const rect = (el as HTMLElement).getBoundingClientRect()
    const vh = window.innerHeight || document.documentElement.clientHeight
    const vw = window.innerWidth || document.documentElement.clientWidth
    // 至少 5% 进入视口视为可见(避免 0 阈值导致的边界跳变)
    return rect.top < vh * 0.95 && rect.bottom > vh * 0.05 && rect.left < vw && rect.right > 0
  }

  const observe = (el: Element) => {
    if (!observer) return
    if (el.classList.contains('is-visible')) return
    // 立即检查:如果元素已经在视口内,直接标记可见,避免 IntersectionObserver 异步延迟
    if (isVisible(el)) {
      el.classList.add('is-visible')
      return
    }
    observer.observe(el)
  }

  const scan = (root: ParentNode = document) => {
    const els = root.querySelectorAll('[data-reveal]:not(.is-visible)')
    els.forEach((el) => observe(el))
  }

  const onIntersect: IntersectionObserverCallback = (entries) => {
    for (const entry of entries) {
@@ -21,11 +51,6 @@ export function useScrollReveal() {
    }
  }

  const scan = () => {
    const els = document.querySelectorAll('[data-reveal]:not(.is-visible)')
    els.forEach((el) => observer?.observe(el))
  }

  const init = () => {
    if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
      // SSR 或不支持时,直接显示
@@ -38,13 +63,27 @@ export function useScrollReveal() {
      rootMargin: '0px 0px -8% 0px'
    })

    // 初次扫描
    scan()

    // 路由切换后重新扫描
    // 使用 MutationObserver 监听后续 DOM 变化(覆盖路由切换 + v-if/v-show + 异步数据)
    mo = new MutationObserver((mutations) => {
      for (const m of mutations) {
        m.addedNodes.forEach((node) => {
          if (node.nodeType !== Node.ELEMENT_NODE) return
          const el = node as Element
          if (el.matches?.('[data-reveal]:not(.is-visible)')) observe(el)
          scan(el)
        })
      }
    })
    mo.observe(document.body, { childList: true, subtree: true })

    // 路由切换后兜底再扫一次(处理 transition/keep-alive 等边缘情况)
    if (import.meta.client) {
      const router = useRouter()
      router.afterEach(() => {
        nextTick(() => setTimeout(scan, 80))
        nextTick(() => setTimeout(() => scan(), 120))
      })
    }
  }
+3 −0
Original line number Diff line number Diff line
@@ -409,6 +409,8 @@ const categories = [
  align-items: start;
}

.grid-2 > * { min-width: 0; }

@media (max-width: 960px) {
  .grid-2 { grid-template-columns: 1fr; }
}
@@ -607,6 +609,7 @@ const categories = [

.grid-code {
  max-width: 100%;
  min-width: 0;
}

/* —— End card —— */
Loading