refactor(content): migrate index page to Nuxt Content
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.
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
<div>
|
||||
<!-- 全幅 Hero - 在 Page 布局之外 -->
|
||||
<UPageHero
|
||||
title="Tootaio Studio"
|
||||
:description="$t('index.heroDescription')"
|
||||
: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',
|
||||
}"
|
||||
@@ -13,14 +13,14 @@
|
||||
/>
|
||||
|
||||
<UPageSection
|
||||
:title="$t('index.capabilities.title')"
|
||||
:features="capabilitiesFeatures"
|
||||
:title="page?.capabilities.title"
|
||||
:features="page?.capabilities.features"
|
||||
/>
|
||||
|
||||
<UPageSection :title="$t('index.featuredProjects.title')">
|
||||
<UPageSection :title="page?.featuredProjects.title">
|
||||
<UCarousel
|
||||
v-slot="{ item }"
|
||||
:items="featuredProjects"
|
||||
:items="page?.featuredProjects.projects"
|
||||
:ui="{ item: 'basis-full sm:basis-1/2 lg:basis-1/3' }"
|
||||
>
|
||||
<UCard class="my-2">
|
||||
@@ -46,7 +46,7 @@
|
||||
</UCarousel>
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection :title="$t('index.techStack.title')">
|
||||
<UPageSection :title="page?.techStack.title">
|
||||
<UMarquee>
|
||||
<UIcon
|
||||
v-for="icon in techIcons"
|
||||
@@ -66,17 +66,57 @@
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection
|
||||
:title="$t('index.whyChooseUs.title')"
|
||||
:features="whyChooseUsFeatures"
|
||||
:title="page?.whyChooseUs.title"
|
||||
:features="page?.whyChooseUs.features"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PageFeatureProps } from "@nuxt/ui";
|
||||
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: $t("index.seo.title"),
|
||||
title: page.value?.seo.title,
|
||||
});
|
||||
|
||||
const colorMode = useColorMode();
|
||||
@@ -94,7 +134,7 @@ const backgroundImages = [
|
||||
"http://img.tootaio.com/i/2025/11/05/avf3wl.png",
|
||||
];
|
||||
|
||||
const currentBgImage = ref("");
|
||||
const currentBgImage = ref<string | undefined>("");
|
||||
|
||||
// 随机选择背景
|
||||
const randomBg = () => {
|
||||
@@ -107,62 +147,6 @@ onMounted(() => {
|
||||
randomBg();
|
||||
});
|
||||
|
||||
const capabilitiesFeatures = computed<PageFeatureProps[]>(() => [
|
||||
{
|
||||
title: $t("index.capabilities.features[0].title"),
|
||||
description: $t("index.capabilities.features[0].description"),
|
||||
icon: "mdi:web",
|
||||
},
|
||||
{
|
||||
title: $t("index.capabilities.features[1].title"),
|
||||
description: $t("index.capabilities.features[1].description"),
|
||||
icon: "mdi:cog-outline",
|
||||
},
|
||||
{
|
||||
title: $t("index.capabilities.features[2].title"),
|
||||
description: $t("index.capabilities.features[2].description"),
|
||||
icon: "mdi:gamepad-variant-outline",
|
||||
},
|
||||
{
|
||||
title: $t("index.capabilities.features[3].title"),
|
||||
description: $t("index.capabilities.features[3].description"),
|
||||
icon: "mdi:monitor-dashboard",
|
||||
},
|
||||
{
|
||||
title: $t("index.capabilities.features[4].title"),
|
||||
description: $t("index.capabilities.features[4].description"),
|
||||
icon: "mdi:flask-outline",
|
||||
},
|
||||
{
|
||||
title: $t("index.capabilities.features[5].title"),
|
||||
description: $t("index.capabilities.features[5].description"),
|
||||
icon: "mdi:lightbulb-outline",
|
||||
},
|
||||
]);
|
||||
|
||||
const featuredProjects = ref([
|
||||
{
|
||||
title: "永中校友会官方网站",
|
||||
description:
|
||||
"永平中学校友会官方网站的设计与开发项目,整体价值 RM28,000,由本工作室创办人无偿捐赠予校友会永久使用。",
|
||||
image: "http://img.tootaio.com/i/2025/11/05/d9kurl.png",
|
||||
demoLink: "https://yphsalumni.org",
|
||||
},
|
||||
{
|
||||
title: "留华生来华资料汇总",
|
||||
description:
|
||||
"2022 年疫情期间,为马来西亚留学生开发的返校攻略网站。帮助 5000+ 名留学生顺利返校。并获得马来西亚外交部推荐。",
|
||||
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png",
|
||||
demoLink: "https://tootaio.github.io",
|
||||
},
|
||||
{
|
||||
title: "光追",
|
||||
description: "基于 Godot 引擎的 2023 年吉比特高校挑战赛参赛作品。",
|
||||
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png",
|
||||
// demoLink: "https://tootaio.com/projects/ray-tracing",
|
||||
},
|
||||
]);
|
||||
|
||||
const techIcons = computed(() => [
|
||||
"skill-icons:html",
|
||||
"skill-icons:css",
|
||||
@@ -231,39 +215,6 @@ const toolsIcons = ref([
|
||||
? "skill-icons:rider-dark"
|
||||
: "skill-icons:rider-light",
|
||||
]);
|
||||
|
||||
const whyChooseUsFeatures = computed<PageFeatureProps[]>(() => [
|
||||
{
|
||||
title: $t("index.whyChooseUs.features[0].title"),
|
||||
description: $t("index.whyChooseUs.features[0].description"),
|
||||
icon: "mdi:brush-variant", // Fully Custom-Built
|
||||
},
|
||||
{
|
||||
title: $t("index.whyChooseUs.features[1].title"),
|
||||
description: $t("index.whyChooseUs.features[1].description"),
|
||||
icon: "mdi:cog-sync-outline", // Tech-Driven
|
||||
},
|
||||
{
|
||||
title: $t("index.whyChooseUs.features[2].title"),
|
||||
description: $t("index.whyChooseUs.features[2].description"),
|
||||
icon: "mdi:gamepad-variant-outline", // Cross-Domain
|
||||
},
|
||||
{
|
||||
title: $t("index.whyChooseUs.features[3].title"),
|
||||
description: $t("index.whyChooseUs.features[3].description"),
|
||||
icon: "mdi:rocket-launch-outline", // End-to-End Service
|
||||
},
|
||||
{
|
||||
title: $t("index.whyChooseUs.features[4].title"),
|
||||
description: $t("index.whyChooseUs.features[4].description"),
|
||||
icon: "mdi:chart-timeline-variant", // Proven Project Value
|
||||
},
|
||||
{
|
||||
title: $t("index.whyChooseUs.features[5].title"),
|
||||
description: $t("index.whyChooseUs.features[5].description"),
|
||||
icon: "mdi:lightbulb-on-outline", // Future-Oriented
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
Reference in New Issue
Block a user