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.
This commit is contained in:
xiaomai
2025-11-06 00:17:34 +08:00
parent 78bc2c34a0
commit 31a4103f9b
10 changed files with 276 additions and 108 deletions

View File

@@ -0,0 +1,85 @@
// composables/useNavLinks.ts
import type { NavigationMenuItem } from "@nuxt/ui";
export const useNavLinks = () => {
const { t } = useI18n();
const navLinks = computed<NavigationMenuItem[]>(() => [
{
label: t("common.header.services.label"),
icon: "mdi:briefcase-outline",
children: [
{
label: t("common.header.services.children.webDev.label"),
description: t("common.header.services.children.webDev.description"),
icon: "mdi:web",
},
{
label: t("common.header.services.children.softwareDev.label"),
description: t(
"common.header.services.children.softwareDev.description"
),
icon: "mdi:tools",
},
{
label: t("common.header.services.children.eventVisual.label"),
description: t(
"common.header.services.children.eventVisual.description"
),
icon: "mdi:monitor-dashboard",
},
{
label: t("common.header.services.children.lab.label"),
description: t("common.header.services.children.lab.description"),
icon: "mdi:test-tube-off",
},
],
},
{
label: t("common.header.projects.label"),
icon: "mdi:lightbulb-group-outline",
children: [
{
label: t("common.header.projects.children.commercialWebsite.label"),
description: t(
"common.header.projects.children.commercialWebsite.description"
),
icon: "mdi:web",
},
{
label: t("common.header.projects.children.gameDev.label"),
description: t("common.header.projects.children.gameDev.description"),
icon: "mdi:gamepad-variant-outline",
},
{
label: t("common.header.projects.children.tools.label"),
description: t("common.header.projects.children.tools.description"),
icon: "mdi:tools",
},
{
label: t("common.header.projects.children.special.label"),
description: t("common.header.projects.children.special.description"),
icon: "mdi:star",
},
],
},
{
label: t("common.header.insights.label"),
icon: "mdi:brain",
children: [
{
label: t("common.header.insights.children.xiaomaiBlog.label"),
description: t(
"common.header.insights.children.xiaomaiBlog.description"
),
icon: "mdi:pencil-outline",
to: "https://xiaomai.tootaio.com/",
type: "link",
target: "_blank",
},
],
},
]);
return navLinks;
};

View File

@@ -1,9 +1,19 @@
<template>
<UPage>
<UHeader>
<UHeader
:ui="{
left: 'flex items-center gap-1.5',
center: 'hidden lg:flex lg:flex-16',
right: 'flex items-center justify-end gap-1.5',
}"
>
<template #title> Tootaio Studio </template>
<template #default>
<UNavigationMenu :items="navLinks" variant="link" />
<UNavigationMenu
:items="navLinks"
variant="link"
class="w-full justify-center"
/>
</template>
<template #body>
<UNavigationMenu :items="navLinks" orientation="vertical" />
@@ -42,6 +52,7 @@
<script lang="ts" setup>
const { setLocale } = useI18n();
const navLinks = useNavLinks();
</script>
<style></style>

View File

@@ -23,26 +23,24 @@
: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>
<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>

View File

@@ -1,12 +0,0 @@
import type {NavigationMenuItem} from "@nuxt/ui";
export const navLinks: NavigationMenuItem[] = [{
label: "Services",
icon: "mdi:briefcase-outline"
}, {
label: "Projects",
icon: "mdi:lightbulb-group-outline"
}, {
label: "Insights",
icon: "mdi:brain"
}]

View File

@@ -1,86 +1,56 @@
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
const defineIndexSchema = () =>
z.object({
capabilities: z.object({
title: z.string(),
features: z.array(
z.object({
title: z.string(),
description: z.string(),
icon: z.string(),
})
),
}),
featuredProjects: z.object({
title: z.string(),
projects: z.array(
z.object({
title: z.string(),
description: z.string(),
image: z.string(),
demoLink: z.string(),
highlight: z.boolean(),
spotlight: z.boolean(),
})
),
}),
techStack: z.object({
title: z.string(),
}),
whyChooseUs: z.object({
title: z.string(),
features: z.array(
z.object({
title: z.string(),
description: z.string(),
icon: z.string(),
})
),
}),
});
export default defineContentConfig({
collections: {
index_en: defineCollection({
type: "page",
source: "en-US/index.yml",
schema: z.object({
capabilities: z.object({
title: z.string(),
features: z.array(
z.object({
title: z.string(),
description: z.string(),
icon: z.string(),
})
),
}),
featuredProjects: z.object({
title: z.string(),
projects: z.array(
z.object({
title: z.string(),
description: z.string(),
image: z.string(),
demoLink: z.string(),
})
),
}),
techStack: z.object({
title: z.string(),
}),
whyChooseUs: z.object({
title: z.string(),
features: z.array(
z.object({
title: z.string(),
description: z.string(),
icon: z.string(),
})
),
}),
}),
schema: defineIndexSchema(),
}),
index_zh: defineCollection({
type: "page",
source: "zh-CN/index.yml",
schema: z.object({
capabilities: z.object({
title: z.string(),
features: z.array(
z.object({
title: z.string(),
description: z.string(),
icon: z.string(),
})
),
}),
featuredProjects: z.object({
title: z.string(),
projects: z.array(
z.object({
title: z.string(),
description: z.string(),
image: z.string(),
demoLink: z.string(),
})
),
}),
techStack: z.object({
title: z.string(),
}),
whyChooseUs: z.object({
title: z.string(),
features: z.array(
z.object({
title: z.string(),
description: z.string(),
icon: z.string(),
})
),
}),
}),
schema: defineIndexSchema(),
}),
},
});

View File

@@ -28,6 +28,7 @@ featuredProjects:
description: "An interactive guide which help more than 5000 students back to China Mainland to continue their study during pandemic COVID-19 in year 2022. This project is supported by The Ministry of Foreign Affairs (MFA) in Malaysia"
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
demoLink: "https://tootaio.github.io"
spotlight: true
- title: "Light Chasing"
description: "This is an entry from the 2023 G-bits University GameJam Challenge, based on the Godot engine."
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"

View File

@@ -34,6 +34,7 @@ featuredProjects:
description: "2022 年疫情期间,为马来西亚留学生开发的返校攻略网站。帮助 5000+ 名留学生顺利返校。并获得马来西亚外交部推荐。"
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
demoLink: "https://tootaio.github.io"
spotlight: true
- title: "光追"
description: "基于 Godot 引擎的 2023 年吉比特高校挑战赛参赛作品。"
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"

View File

@@ -0,0 +1,57 @@
{
"common": {
"header": {
"insights": {
"label": "Insights",
"children": {
"xiaomaiBlog": {
"label": "Founder's Blog",
"description": "The blog sites of our studio founder - Xiaomai."
}
}
},
"services": {
"label": "Services",
"children": {
"softwareDev": {
"label": "Software Development",
"description": "Software and Tools / Automation Development"
},
"eventVisual": {
"label": "Event Big Screen Visualization",
"description": "Display the content whichever you want on the big screen of your events!"
},
"lab": {
"label": "Labs and Tests",
"description": "Unavailable yet..."
},
"webDev": {
"label": "Web Development",
"description": "Customise Web design + CMS"
}
}
},
"projects": {
"label": "Projects",
"children": {
"commercialWebsite": {
"label": "Commercial Website",
"description": "Fully customize website showcases."
},
"tools": {
"label": "Tools and Softwares",
"description": "Some QoL tools we developed"
},
"special": {
"label": "Special Projects",
"description": "Special projects to meet customer needs"
},
"gameDev": {
"label": "Indie Games",
"description": "Have a look at our indie games!"
}
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
{
"common": {
"header": {
"services": {
"label": "服务",
"children": {
"softwareDev": {
"label": "软件开发",
"description": "软件和工具/自动化开发"
},
"eventVisual": {
"label": "活动大屏可视化",
"description": "在您的活动大屏幕上显示您想要的内容!"
},
"lab": {
"label": "实验室和测试",
"description": "还无法使用..."
},
"webDev": {
"label": "网站开发",
"description": "定制网页设计 + CMS"
}
}
},
"projects": {
"label": "案例",
"children": {
"commercialWebsite": {
"label": "商业网站",
"description": "完全定制网站展示。"
},
"tools": {
"label": "工具和软件",
"description": "我们开发的一些生活质量工具"
},
"special": {
"label": "特别项目",
"description": "特殊项目满足客户需求"
},
"gameDev": {
"label": "独立游戏",
"description": "看看我们的独立游戏!"
}
}
},
"insights": {
"label": "博客",
"children": {
"xiaomaiBlog": {
"label": "创始人博客",
"description": "我们工作室创始人小麦的博客网站。"
}
}
}
}
}
}

View File

@@ -36,13 +36,13 @@ export default defineNuxtConfig({
code: "en",
iso: "en-US",
name: "English",
files: ["en-US/index.json"],
files: ["en-US/common.json", "zh-CN/index.json"],
},
{
code: "zh-CN",
iso: "zh-CN",
name: "简体中文",
files: ["zh-CN/index.json"],
files: ["zh-CN/common.json", "zh-CN/index.json"],
},
],
strategy: "no_prefix"