Files
tootaio.com/app/pages/index.vue
xiaomai 31a4103f9b feat(ui): implement dynamic dropdown navigation and refactor project cards
Replaced the static navigation with a dynamic, internationalized dropdown menu powered by a new `useNavLinks` composable. The
navigation items are now sourced from i18n files. The featured project cards on the homepage have been refactored to use the
`<UPageCard>` component, and the content schema is updated with `spotlight` and `highlight` options for enhanced display.
2025-11-06 07:44:41 +08:00

219 lines
5.8 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' }"
>
<UPageCard
class="my-2"
:title="item.title"
:description="item.description"
:highlight="item.highlight"
:spotlight="item.spotlight"
>
<img :src="item.image" :alt="item.title" />
<UButton
v-if="item.demoLink"
:href="item.demoLink"
target="_blank"
rel="noopener"
size="sm"
>
{{ $t("index.featuredProjects.viewDemo") }}
</UButton>
</UPageCard>
</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>