This commit refactors the index page to source its content from @nuxt/content, replacing the previous implementation that used i18n JSON files and hardcoded data within the component. Key changes: - Introduced `content.config.ts` to define collections and Zod schemas for type-safe content. - Moved page content into localized YAML files (`content/en-US/index.yml` and `content/zh-CN/index.yml`). - Updated `app/pages/index.vue` to fetch data dynamically using `useAsyncData` and `queryCollection`. - Removed redundant content from i18n JSON files and the Vue component script. This change decouples content from presentation, improves maintainability, and centralizes content management.
221 lines
5.9 KiB
Vue
221 lines
5.9 KiB
Vue
<template>
|
|
<div>
|
|
<!-- 全幅 Hero - 在 Page 布局之外 -->
|
|
<UPageHero
|
|
:title="page?.title"
|
|
:description="page?.description"
|
|
:ui="{
|
|
root: 'relative before:absolute before:inset-0 before:bg-[image:var(--bg-image)] before:bg-cover before:bg-center before:-z-10 before:opacity-40',
|
|
}"
|
|
:style="{
|
|
'--bg-image': `url('${currentBgImage}')`,
|
|
}"
|
|
/>
|
|
|
|
<UPageSection
|
|
:title="page?.capabilities.title"
|
|
:features="page?.capabilities.features"
|
|
/>
|
|
|
|
<UPageSection :title="page?.featuredProjects.title">
|
|
<UCarousel
|
|
v-slot="{ item }"
|
|
:items="page?.featuredProjects.projects"
|
|
:ui="{ item: 'basis-full sm:basis-1/2 lg:basis-1/3' }"
|
|
>
|
|
<UCard class="my-2">
|
|
<template #header>
|
|
<h3 class="text-2xl font-bold">{{ item.title }}</h3>
|
|
</template>
|
|
<template #default>
|
|
<img :src="item.image" :alt="item.title" />
|
|
<p class="mt-2 line-clamp-3">{{ item.description }}</p>
|
|
</template>
|
|
<template #footer>
|
|
<UButton
|
|
v-if="item.demoLink"
|
|
:href="item.demoLink"
|
|
target="_blank"
|
|
rel="noopener"
|
|
size="sm"
|
|
>
|
|
{{ $t("index.featuredProjects.viewDemo") }}
|
|
</UButton>
|
|
</template>
|
|
</UCard>
|
|
</UCarousel>
|
|
</UPageSection>
|
|
|
|
<UPageSection :title="page?.techStack.title">
|
|
<UMarquee>
|
|
<UIcon
|
|
v-for="icon in techIcons"
|
|
:key="icon"
|
|
:name="icon"
|
|
class="size-16"
|
|
/>
|
|
</UMarquee>
|
|
<UMarquee reverse>
|
|
<UIcon
|
|
v-for="icon in toolsIcons"
|
|
:key="icon"
|
|
:name="icon"
|
|
class="size-16"
|
|
/>
|
|
</UMarquee>
|
|
</UPageSection>
|
|
|
|
<UPageSection
|
|
:title="page?.whyChooseUs.title"
|
|
:features="page?.whyChooseUs.features"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import type { Collections } from "@nuxt/content";
|
|
|
|
const { locale } = useI18n();
|
|
|
|
const { data: page } = await useAsyncData(
|
|
"index-" + locale.value,
|
|
async () => {
|
|
// Build collection name based on current locale
|
|
let localeSuffix = "";
|
|
switch (locale.value) {
|
|
case "en":
|
|
localeSuffix = "en";
|
|
break;
|
|
case "zh-CN":
|
|
localeSuffix = "zh";
|
|
break;
|
|
default:
|
|
localeSuffix = "en";
|
|
break;
|
|
}
|
|
const collection = ("index_" + localeSuffix) as keyof Collections;
|
|
const content = await queryCollection(collection).first();
|
|
|
|
// Optional: fallback to default locale if content is missing
|
|
if (!content && locale.value !== "en") {
|
|
return await queryCollection("index_en").first();
|
|
}
|
|
|
|
return content;
|
|
},
|
|
{
|
|
watch: [locale], // Refetch when locale changes
|
|
}
|
|
);
|
|
if (!page.value) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: `Page not found, the index_${locale.value} couldn't be found.`,
|
|
fatal: true,
|
|
});
|
|
}
|
|
|
|
useSeoMeta({
|
|
title: page.value?.seo.title,
|
|
});
|
|
|
|
const colorMode = useColorMode();
|
|
|
|
const backgroundImages = [
|
|
"http://img.tootaio.com/i/2025/11/05/avc5ld.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avcaff.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avcjbw.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avcp16.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avcv1q.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avd47a.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avdx6a.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avegxy.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avemgn.png",
|
|
"http://img.tootaio.com/i/2025/11/05/avf3wl.png",
|
|
];
|
|
|
|
const currentBgImage = ref<string | undefined>("");
|
|
|
|
// 随机选择背景
|
|
const randomBg = () => {
|
|
const randomIndex = Math.floor(Math.random() * backgroundImages.length);
|
|
currentBgImage.value = backgroundImages[randomIndex];
|
|
};
|
|
|
|
// 轮播背景
|
|
onMounted(() => {
|
|
randomBg();
|
|
});
|
|
|
|
const techIcons = computed(() => [
|
|
"skill-icons:html",
|
|
"skill-icons:css",
|
|
"skill-icons:javascript",
|
|
"skill-icons:typescript",
|
|
"skill-icons:docker",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:vuejs-dark"
|
|
: "skill-icons:vuejs-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:nuxtjs-dark"
|
|
: "skill-icons:nuxtjs-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:tailwindcss-dark"
|
|
: "skill-icons:tailwindcss-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:nodejs-dark"
|
|
: "skill-icons:nodejs-light",
|
|
"skill-icons:cs",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:python-dark"
|
|
: "skill-icons:python-light",
|
|
]);
|
|
|
|
const toolsIcons = ref([
|
|
"skill-icons:photoshop",
|
|
"skill-icons:illustrator",
|
|
"skill-icons:git",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:vscode-dark"
|
|
: "skill-icons:vscode-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:visualstudio-dark"
|
|
: "skill-icons:visualstudio-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:github-dark"
|
|
: "skill-icons:github-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:godot-dark"
|
|
: "skill-icons:godot-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:unity-dark"
|
|
: "skill-icons:unity-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:blender-dark"
|
|
: "skill-icons:blender-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:androidstudio-dark"
|
|
: "skill-icons:androidstudio-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:windows-dark"
|
|
: "skill-icons:windows-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:linux-dark"
|
|
: "skill-icons:linux-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:apple-dark"
|
|
: "skill-icons:apple-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:idea-dark"
|
|
: "skill-icons:idea-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:pycharm-dark"
|
|
: "skill-icons:pycharm-light",
|
|
colorMode.value === "dark"
|
|
? "skill-icons:rider-dark"
|
|
: "skill-icons:rider-light",
|
|
]);
|
|
</script>
|
|
|
|
<style></style>
|