refactor(docs): home layout component (#1404)

---------

Signed-off-by: 秋雨落 <i@rain.cx>
Co-authored-by: Neko-233 <1260907335@qq.com>
Co-authored-by: Neko <neko@ayaka.moe>
This commit is contained in:
雨落
2026-03-17 17:40:16 +08:00
committed by GitHub
co-authored by Neko-233 Neko
parent b97c7a8826
commit 8648f761ed
9 changed files with 153 additions and 55 deletions
+26 -50
View File
@@ -3,7 +3,8 @@ import type { AnimatableObject } from 'animejs'
import { useLocalStorage, useWindowSize } from '@vueuse/core'
import { createAnimatable } from 'animejs'
import { onMounted, shallowRef, useTemplateRef, watch, watchEffect } from 'vue'
import { useData } from 'vitepress'
import { computed, onMounted, shallowRef, useTemplateRef, watch, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import homeBackgroundChristmas20251224 from '../assets/home-cover-2025-12-24-bg.avif'
@@ -12,6 +13,7 @@ import homeBackgroundChristmas20251224 from '../assets/home-cover-2025-12-24-bg.
// cached or erased in dev/prod, causing the mask pattern to not show up.
import homeBackgroundPatternGhost from '../assets/home-patterns-ghost.svg?no-inline'
import homeBackgroundPatternLollipop from '../assets/home-patterns-lollipop.svg?no-inline'
import HomeButtonSet from './HomeButtonSet.vue'
import ParallaxCover from './ParallaxCover.vue'
import ParallaxCoverChristmas20251224 from './ParallaxCoverChristmas20251224.vue'
import ParallaxCoverHalloween20251029 from './ParallaxCoverHalloween20251029.vue'
@@ -89,6 +91,13 @@ watchEffect((onCleanup) => {
}
}
})
const isShowHalloween = isBetweenHalloweenAndHalfOfNovember(new Date())
const isShowChristmas = isBetweenChristmasAndHalfOfJanuary(new Date())
const { theme, frontmatter } = useData()
const buttons = computed(() => theme.value.homepage?.buttons || [])
</script>
<template>
@@ -100,7 +109,7 @@ watchEffect((onCleanup) => {
<div class="relative w-full flex flex-col items-center justify-center text-center font-extrabold font-sans-rounded" text="4xl md:5xl">
<ClientOnly>
<div
v-if="isBetweenHalloweenAndHalfOfNovember(new Date())"
v-if="isShowHalloween"
:class="[
'w-fit',
'flex items-center gap-2',
@@ -113,7 +122,7 @@ watchEffect((onCleanup) => {
<div i-twemoji:jack-o-lantern />Happy Halloween!<div i-twemoji:jack-o-lantern />
</div>
<div
v-if="isBetweenChristmasAndHalfOfJanuary(new Date())"
v-if="isShowChristmas"
:class="[
'w-fit',
'flex items-center gap-2',
@@ -125,55 +134,22 @@ watchEffect((onCleanup) => {
>
<div i-twemoji:christmas-tree />Merry Christmas!<div i-twemoji:christmas-tree />
</div>
<div :class="[isBetweenHalloweenAndHalfOfNovember(new Date()) ? 'font-sans-serif-halloween' : '']">
Project AIRI
<div
v-if="frontmatter.title"
:class="[isShowHalloween ? 'font-sans-serif-halloween' : '']"
>
{{ frontmatter.title }}
</div>
</ClientOnly>
</div>
<div
v-if="frontmatter.slogan"
class="relative max-w-prose text-center text-slate-900 dark:text-white"
:class="[isBetweenHalloweenAndHalfOfNovember(new Date()) ? 'font-sans-serif-halloween-secondary' : '']"
:class="[isShowHalloween ? 'font-sans-serif-halloween-secondary' : '']"
>
{{ t('docs.theme.home.subtitle') }}
</div>
<div class="relative z-10 w-full flex justify-center gap-4">
<a
:class="[
'rounded-xl px-3 py-2 lg:px-5 lg:py-3 font-extrabold outline-none backdrop-blur-md active:scale-95 focus:outline-none text-nowrap text-sm md:text-base',
'text-slate-700 dark:text-cyan-200',
]"
bg="primary/20 dark:primary/30 dark:hover:primary/40"
transition="colors,transform duration-200 ease-in-out"
cursor-pointer
@click="() => handleClickTryLive()"
>
{{ t('docs.theme.home.try-live.title') }}
</a>
<a
href="https://github.com/moeru-ai/airi/releases/latest"
:class="[
'rounded-xl px-3 py-2 lg:px-5 lg:py-3 font-extrabold outline-none backdrop-blur-md active:scale-95 focus:outline-none text-nowrap text-sm md:text-base',
'text-slate-700 dark:text-slate-100',
]"
bg="black/4 dark:white/20 dark:hover:white/30"
cursor-pointer
transition="colors,transform duration-200 ease-in-out"
>
{{ t('docs.theme.home.download.title') }}
</a>
<a
href="./docs/overview/"
:class="[
'rounded-xl px-3 py-2 lg:px-5 lg:py-3 font-extrabold outline-none backdrop-blur-md active:scale-95 focus:outline-none text-nowrap text-sm md:text-base',
'text-slate-700 dark:text-slate-100',
]"
bg="black/4 dark:white/20 dark:hover:white/30"
transition="colors,transform duration-200 ease-in-out"
cursor-pointer
>
{{ t('docs.theme.home.get-started.title') }}
</a>
{{ frontmatter.slogan }}
</div>
<HomeButtonSet :items="buttons" />
</div>
</div>
</section>
@@ -181,10 +157,10 @@ watchEffect((onCleanup) => {
<div class="absolute inset-0 overflow-hidden -z-10">
<!-- Repeating, slowly scrolling SVG pattern background -->
<ClientOnly>
<template v-if="isBetweenHalloweenAndHalfOfNovember(new Date())">
<template v-if="isShowHalloween">
<div class="bg-icon-pattern pointer-events-none absolute inset-0 z-0 opacity-10 dark:opacity-10" :style="{ '--bg-mask-icon-pattern': `url(${homeBackgroundPatternGhost})` }" />
</template>
<template v-if="isBetweenChristmasAndHalfOfJanuary(new Date())">
<template v-if="isShowChristmas">
<img :src="homeBackgroundChristmas20251224" class="pointer-events-none absolute inset-0 z-0 h-full w-full object-cover">
</template>
<template v-else>
@@ -195,14 +171,14 @@ watchEffect((onCleanup) => {
:class="[
'absolute bottom-0 left-0 right-0 top-0 z-2',
'from-transparent to-white bg-gradient-to-t dark:to-[hsl(207_15%_5%)]',
isBetweenChristmasAndHalfOfJanuary(new Date()) ? 'h-[40%]' : 'h-[80%]',
isShowChristmas ? 'h-[40%]' : 'h-[80%]',
]"
style="--un-gradient-shape: to top in srgb;"
/>
<template v-if="isBetweenHalloweenAndHalfOfNovember(new Date())">
<template v-if="isShowHalloween">
<ParallaxCoverHalloween20251029 />
</template>
<template v-else-if="isBetweenChristmasAndHalfOfJanuary(new Date())">
<template v-else-if="isShowChristmas">
<Snowfall />
<ParallaxCoverChristmas20251224 />
</template>
+26
View File
@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { ButtonItem } from '../theme/config.ts'
defineProps<{
item: ButtonItem
}>()
</script>
<template>
<a
:href="item.link"
:class="[
'rounded-xl px-3 py-2 lg:px-5 lg:py-3 font-extrabold outline-none backdrop-blur-md active:scale-95 focus:outline-none text-nowrap text-sm md:text-base',
'text-slate-700 dark:text-slate-100',
]"
:bg="item.primary ? 'primary/20 dark:primary/30 dark:hover:primary/40' : 'black/4 dark:white/20 dark:hover:white/30'"
transition="colors,transform duration-200 ease-in-out"
cursor-pointer
>
{{ item.text }}
</a>
</template>
<style scoped>
</style>
@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { ButtonItem } from '../theme/config.ts'
import HomeButton from './HomeButton.vue'
defineProps<{
items: ButtonItem[]
}>()
</script>
<template>
<div class="relative z-10 w-full flex justify-center gap-4">
<HomeButton v-for="b of items" :key="b.text" :item="b" />
</div>
</template>
<style scoped>
</style>
+58 -1
View File
@@ -1,5 +1,7 @@
import type { DefaultTheme } from 'vitepress'
import type { ThemeConfig } from './theme/config.ts'
import { join, posix, resolve } from 'node:path'
import { env } from 'node:process'
@@ -13,6 +15,7 @@ import { tasklist } from '@mdit/plugin-tasklist'
import { defineConfig, postcssIsolateStyles } from 'vitepress'
import { version } from '../../package.json'
import { webLive } from './constants.ts'
import { teamMembers } from './contributors'
import {
discord,
@@ -36,7 +39,7 @@ function withBase(url: string) {
}
// https://vitepress.dev/reference/site-config
export default defineConfig({
export default defineConfig<ThemeConfig>({
cleanUrls: true,
ignoreDeadLinks: true,
title: projectName,
@@ -203,6 +206,24 @@ export default defineConfig({
link: withBase('/en/characters/'),
},
] as (DefaultTheme.SidebarItem & { icon?: string })[],
homepage: {
buttons: [
{
text: 'Try Live',
link: webLive,
primary: true,
},
{
text: 'Download',
link: withBase('/en/docs/overview/versions'),
},
{
text: 'Get Started',
link: withBase('/en/docs/overview'),
},
],
},
},
},
'zh-Hans': {
@@ -325,6 +346,24 @@ export default defineConfig({
link: withBase('/zh-Hans/characters/'),
},
] as (DefaultTheme.SidebarItem & { icon?: string })[],
homepage: {
buttons: [
{
text: '网页版',
link: webLive,
primary: true,
},
{
text: '下载',
link: withBase('/zh-Hans/docs/overview/versions'),
},
{
text: '使用教程',
link: withBase('/zh-Hans/docs/overview/'),
},
],
},
},
},
'ja': {
@@ -447,6 +486,24 @@ export default defineConfig({
link: withBase('/ja/characters/'),
},
] as (DefaultTheme.SidebarItem & { icon?: string })[],
homepage: {
buttons: [
{
text: 'ライブ版を試す',
link: webLive,
primary: true,
},
{
text: 'ダウンロード',
link: withBase('/ja/docs/overview/versions'),
},
{
text: 'はじめに',
link: withBase('/ja/docs/overview'),
},
],
},
},
},
},
+1
View File
@@ -0,0 +1 @@
export const webLive = 'https://airi.moeru.ai/'
+17
View File
@@ -0,0 +1,17 @@
import type { DefaultTheme } from 'vitepress'
interface ExtraThemeConfig {
homepage: HomePageConfig
}
interface HomePageConfig {
buttons: ButtonItem[]
}
export interface ButtonItem {
text?: string
link?: string
primary?: boolean
}
export type ThemeConfig = DefaultTheme.Config & ExtraThemeConfig
+2 -1
View File
@@ -1,4 +1,5 @@
---
layout: home
title: 'Project AIRI: A container of souls of AI waifu / virtual characters to bring them into our worlds.'
title: 'Project AIRI'
slogan: 'A container of souls of AI waifu / virtual characters to bring them into our worlds.'
---
+2 -1
View File
@@ -1,4 +1,5 @@
---
layout: home
title: 'Project AIRI: AI の「推し」やバーチャルキャラクターの魂を宿し、現実に連れてくるための器'
title: 'Project AIRI'
slogan: 'AI の「推し」やバーチャルキャラクターの魂を宿し、現実に連れてくるための器'
---
+2 -2
View File
@@ -1,5 +1,5 @@
---
layout: home
title: 'Project AIRI:
模型驱动的灵魂容器,什么都能做一点的桌宠:让 Neuro-sama 这样的虚拟伴侣也成为我们世界中的一员吧!'
title: 'Project AIRI'
slogan: '模型驱动的灵魂容器,什么都能做一点的桌宠:让 Neuro-sama 这样的虚拟伴侣也成为我们世界中的一员吧!'
---