Commit 87aeaf1a authored by shrabbit's avatar shrabbit
Browse files

feat(home): build home page with hero, wiki cards, and data flow diagram

- WikiCard.vue: reusable card with tone-aware aurora background and lucide icon
- DataFlowDiagram.vue: SVG-based animated flow with 5 nodes (official → archive)
- SectionHeading.vue: shared section title with index/subtitle/line
- index.vue: hero + intro stats + 3 wiki cards + data flow + outro quote
- fonts: add font.im mirror fallback for CN network, strengthen system fallbacks
parent 38d16c01
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -38,9 +38,9 @@
  --color-line-faint: rgba(241, 233, 210, 0.06);

  /* —— 字体 —— */
  --font-serif: 'Cormorant Garamond', 'Noto Serif SC', serif;
  --font-sans: 'Noto Sans SC', 'IBM Plex Sans', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
  --font-serif: 'Cormorant Garamond', 'Noto Serif SC', 'Songti SC', 'Source Han Serif SC', 'STSong', serif;
  --font-sans: 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Menlo', Consolas, monospace;

  /* —— 间距(4 的倍数) —— */
  --space-1: 0.25rem;
+225 −0
Original line number Diff line number Diff line
<template>
  <div class="dataflow">
    <svg
      class="dataflow__svg"
      viewBox="0 0 1200 280"
      preserveAspectRatio="xMidYMid meet"
      aria-hidden="true"
    >
      <defs>
        <linearGradient :id="gradientId" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stop-color="#74c2a8" />
          <stop offset="50%" stop-color="#d4a45a" />
          <stop offset="100%" stop-color="#9a6ec5" />
        </linearGradient>
        <filter :id="glowId" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="4" result="b" />
          <feMerge>
            <feMergeNode in="b" />
            <feMergeNode in="SourceGraphic" />
          </feMerge>
        </filter>
      </defs>

      <!-- 主连接线 -->
      <path
        class="dataflow__line"
        :d="mainPath"
        fill="none"
        :stroke="`url(#${gradientId})`"
        stroke-width="1.5"
        stroke-linecap="round"
      />

      <!-- 节点 -->
      <g
        v-for="(node, i) in nodes"
        :key="i"
        :transform="`translate(${node.x}, ${node.y})`"
      >
        <circle
          class="dataflow__node-dot"
          r="5"
          fill="var(--color-bg-base)"
          :stroke="node.color"
          stroke-width="1.5"
          :filter="`url(#${glowId})`"
          :style="{ animationDelay: `${0.4 + i * 0.4}s` }"
        />
        <circle
          r="2"
          :fill="node.color"
          opacity="0.9"
        />
      </g>
    </svg>

    <div class="dataflow__labels">
      <div
        v-for="(node, i) in nodes"
        :key="i"
        class="dataflow__label"
        :data-reveal="''"
        :data-stagger="i + 1"
      >
        <span class="dataflow__label-tag" :style="{ color: node.color }">{{ node.tag }}</span>
        <span class="dataflow__label-title">{{ node.title }}</span>
        <span class="dataflow__label-desc">{{ node.desc }}</span>
      </div>
    </div>

    <!-- 移动的光点 -->
    <div class="dataflow__pulse">
      <span
        v-for="i in 3"
        :key="i"
        class="dataflow__pulse-dot"
        :style="{ animationDelay: `${i * 1.4}s` }"
      ></span>
    </div>
  </div>
</template>

<script setup lang="ts">
const gradientId = 'dataflow-grad'
const glowId = 'dataflow-glow'

const nodes = [
  { x: 60, y: 140, color: '#74c2a8', tag: '01', title: '米哈游官网', desc: 'ys.mihoyo.com/main/' },
  { x: 360, y: 140, color: '#d4a45a', tag: '02', title: 'JS 频道表', desc: 'CHARACTER / NOTE 字典' },
  { x: 660, y: 140, color: '#e4a672', tag: '03', title: 'Content API', desc: '角色 / 世界 / 游记' },
  { x: 960, y: 140, color: '#9a6ec5', tag: '04', title: '本地存档', desc: 'sExt → 结构化数据' },
  { x: 1140, y: 140, color: '#5ca7d4', tag: '05', title: '社区聚合', desc: '活动 / 表情 / 新闻' }
]

// 用 cubic bezier 平滑连接 5 个节点
const mainPath = computed(() => {
  return nodes
    .map((n, i) => {
      if (i === 0) return `M ${n.x} ${n.y}`
      const prev = nodes[i - 1]
      const midX = (prev.x + n.x) / 2
      return `C ${midX} ${prev.y}, ${midX} ${n.y}, ${n.x} ${n.y}`
    })
    .join(' ')
})
</script>

<style scoped>
.dataflow {
  position: relative;
  padding: var(--space-12) var(--space-4) var(--space-16);
  overflow: hidden;
}

.dataflow__svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}

.dataflow__line {
  stroke-dasharray: 1800;
  stroke-dashoffset: 1800;
  animation: draw-flow 2.4s var(--ease-out-expo) forwards;
  --len: 1800;
}

@keyframes draw-flow {
  to { stroke-dashoffset: 0; }
}

.dataflow__node-dot {
  opacity: 0;
  transform-origin: center;
  transform-box: fill-box;
  animation: node-pop 0.6s var(--ease-out-quint) forwards;
}

@keyframes node-pop {
  0% {
    opacity: 0;
    transform: scale(0);
  }
  60% {
    opacity: 1;
    transform: scale(1.4);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

.dataflow__labels {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--space-2);
  margin-top: var(--space-4);
  padding: 0 var(--space-2);
}

.dataflow__label {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 2px;
}

.dataflow__label-tag {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.2em;
}

.dataflow__label-title {
  font-family: var(--font-serif);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.dataflow__label-desc {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--color-text-muted);
}

/* 移动光点 */
.dataflow__pulse {
  position: absolute;
  top: calc(50% - 8px);
  left: 0;
  right: 0;
  pointer-events: none;
}

.dataflow__pulse-dot {
  position: absolute;
  top: 0;
  left: 4%;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 16px var(--color-accent), 0 0 32px rgba(212, 164, 90, 0.4);
  animation: pulse-travel 4.2s var(--ease-in-out) infinite;
  opacity: 0;
}

@keyframes pulse-travel {
  0% { left: 4%; opacity: 0; }
  10% { opacity: 1; }
  90% { opacity: 1; }
  100% { left: 96%; opacity: 0; }
}

@media (max-width: 768px) {
  .dataflow__labels {
    grid-template-columns: 1fr;
    gap: var(--space-3);
  }
}
</style>
+81 −0
Original line number Diff line number Diff line
<template>
  <div class="section-heading" :data-reveal="reveal">
    <span class="section-heading__index">{{ index }}</span>
    <div class="section-heading__title-group">
      <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">
interface Props {
  index?: string
  title?: string
  subtitle?: string
  reveal?: string
}
withDefaults(defineProps<Props>(), {
  index: '00',
  title: '',
  subtitle: '',
  reveal: ''
})
</script>

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

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

.section-heading__title-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.section-heading__title {
  font-size: clamp(1.75rem, 3vw, 2.25rem);
  font-family: var(--font-serif);
  font-weight: 600;
  line-height: 1.15;
  color: var(--color-text-primary);
}

.section-heading__subtitle {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  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>
+207 −0
Original line number Diff line number Diff line
<template>
  <NuxtLink :to="card.to" class="wiki-card" :data-tone="card.tone">
    <div class="wiki-card__bg" aria-hidden="true">
      <div class="wiki-card__grid"></div>
      <div class="wiki-card__aurora"></div>
    </div>

    <div class="wiki-card__inner">
      <div class="wiki-card__header">
        <span class="wiki-card__index">{{ card.index }}</span>
        <span class="wiki-card__icon" aria-hidden="true">
          <component :is="iconComp" :size="18" :stroke-width="1.4" />
        </span>
      </div>

      <h3 class="wiki-card__title">{{ card.label }}</h3>
      <p class="wiki-card__desc">{{ card.desc }}</p>

      <div class="wiki-card__footer">
        <span class="wiki-card__cta">
          进入
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
            <path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
          </svg>
        </span>
        <span class="wiki-card__line"></span>
      </div>
    </div>
  </NuxtLink>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import * as icons from 'lucide-vue-next'
import type { NavGroup } from '~/composables/useNavigation'

const props = defineProps<{ card: NavGroup }>()
const card = props.card

const iconComp = computed(() => {
  // lucide-vue-next 中所有图标都是 PascalCase
  const pascal = card.icon
    .split(/[-_ ]/)
    .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
    .join('')
  return (icons as Record<string, unknown>)[pascal] ?? icons.Circle
})
</script>

<style scoped>
.wiki-card {
  position: relative;
  display: block;
  padding: var(--space-8);
  background: var(--color-bg-elev);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  color: var(--color-text-primary);
  text-decoration: none;
  overflow: hidden;
  isolation: isolate;
  transition: transform var(--duration-base) var(--ease-out-quint),
              border-color var(--duration-base) var(--ease-out-quint),
              box-shadow var(--duration-base) var(--ease-out-quint);
}

.wiki-card:hover {
  transform: translateY(-4px);
  border-color: var(--color-line-strong);
  box-shadow: var(--shadow-hover);
}

/* —— 背景层 —— */
.wiki-card__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.6;
  transition: opacity var(--duration-base) var(--ease-out-quint);
}

.wiki-card:hover .wiki-card__bg { opacity: 1; }

.wiki-card__grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(currentColor 1px, transparent 1px),
    linear-gradient(90deg, currentColor 1px, transparent 1px);
  background-size: 28px 28px;
  opacity: 0.04;
  color: var(--color-text-primary);
  mask-image: radial-gradient(ellipse at top right, black 30%, transparent 70%);
  -webkit-mask-image: radial-gradient(ellipse at top right, black 30%, transparent 70%);
}

.wiki-card__aurora {
  position: absolute;
  top: -30%;
  right: -30%;
  width: 200%;
  height: 200%;
  background: radial-gradient(circle at center, var(--tone-color, var(--color-accent)) 0%, transparent 50%);
  opacity: 0.12;
  filter: blur(40px);
  animation: rotate-aurora 24s linear infinite;
}

/* —— Tone —— */
.wiki-card[data-tone='anemo'] { --tone-color: var(--color-anemo); }
.wiki-card[data-tone='geo'] { --tone-color: var(--color-geo); }
.wiki-card[data-tone='electro'] { --tone-color: var(--color-electro); }
.wiki-card[data-tone='dendro'] { --tone-color: var(--color-dendro); }
.wiki-card[data-tone='hydro'] { --tone-color: var(--color-hydro); }
.wiki-card[data-tone='pyro'] { --tone-color: var(--color-pyro); }
.wiki-card[data-tone='cryo'] { --tone-color: var(--color-cryo); }

/* —— 内容 —— */
.wiki-card__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 240px;
}

.wiki-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-8);
}

.wiki-card__index {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.2em;
  color: var(--color-text-muted);
}

.wiki-card__icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--color-line);
  border-radius: 50%;
  color: var(--tone-color, var(--color-accent));
  transition: transform var(--duration-base) var(--ease-out-quint);
}

.wiki-card:hover .wiki-card__icon {
  transform: rotate(45deg) scale(1.1);
}

.wiki-card__title {
  font-size: 1.5rem;
  font-family: var(--font-serif);
  font-weight: 600;
  color: var(--color-text-primary);
  margin-bottom: var(--space-3);
  letter-spacing: 0.02em;
}

.wiki-card__desc {
  font-size: 0.875rem;
  line-height: 1.7;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-6);
  flex: 1;
}

.wiki-card__footer {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.wiki-card__cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--tone-color, var(--color-accent));
}

.wiki-card__cta svg {
  transition: transform var(--duration-base) var(--ease-out-quint);
}

.wiki-card:hover .wiki-card__cta svg {
  transform: translateX(4px);
}

.wiki-card__line {
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, currentColor, transparent);
  color: var(--color-line-strong);
  opacity: 0.5;
}
</style>
+444 −19

File changed.

Preview size limit exceeded, changes collapsed.

Loading