feat(pages): add web development services page
This commit introduces a new page at `/webDev` to display web development services and pricing plans. To support this, a new reusable composable `useLocalizedCollection` has been created to simplify fetching localized content from Nuxt Content. The index page has been refactored to use this new composable. - Adds `webDev.vue` page and corresponding `webDev.yml` content files for EN and ZH. - Defines a Zod schema in `content.config.ts` for the new content type. - Updates the navigation link to point to the new page.
This commit is contained in:
66
app/composables/LocalizedCollection.ts
Normal file
66
app/composables/LocalizedCollection.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// /composables/useLocalizedCollection.ts
|
||||
import type { Collections } from "@nuxt/content";
|
||||
|
||||
export type UseLocalizedOptions = {
|
||||
/** 默认 locale -> suffix 映射 */
|
||||
localeMap?: Record<string, string>;
|
||||
/** 回退 locale 的 suffix(例如 'en') */
|
||||
fallbackSuffix?: string;
|
||||
/** 当找不到内容时是否抛错,默认 true */
|
||||
throwOnMissing?: boolean;
|
||||
/** useAsyncData 的 key 前缀(默认等于 baseName) */
|
||||
keyPrefix?: string;
|
||||
};
|
||||
|
||||
function asCollectionKey(key: string) {
|
||||
return key as keyof Collections;
|
||||
}
|
||||
|
||||
/**
|
||||
* 带类型安全的多语言内容加载器
|
||||
* @example
|
||||
* const { data: page } = await useLocalizedCollection('index')
|
||||
*/
|
||||
export function useLocalizedCollection<
|
||||
B extends string, // 基础名称
|
||||
>(baseName: B, opts: UseLocalizedOptions = {}) {
|
||||
const { locale } = useI18n();
|
||||
|
||||
const localeMap = opts.localeMap ?? { en: "en", "zh-CN": "zh" };
|
||||
const fallbackSuffix = opts.fallbackSuffix ?? "en";
|
||||
const keyPrefix = opts.keyPrefix ?? baseName;
|
||||
const throwOnMissing = opts.throwOnMissing ?? true;
|
||||
|
||||
// 🔥 自动推断对应集合类型
|
||||
type LocalizedKey = keyof {
|
||||
[K in keyof Collections as K extends `${B}_${string}` ? K : never]: any;
|
||||
};
|
||||
type Schema = Collections[LocalizedKey];
|
||||
|
||||
return useAsyncData<Schema | null>(
|
||||
`${keyPrefix}-${locale.value}`,
|
||||
async () => {
|
||||
const suffix =
|
||||
localeMap[locale.value] ?? locale.value.split("-")[0] ?? "en";
|
||||
const key = asCollectionKey(`${baseName}_${suffix}`);
|
||||
let content = (await queryCollection(key).first()) as Schema | null;
|
||||
|
||||
if (!content && suffix !== fallbackSuffix) {
|
||||
const fallbackKey = asCollectionKey(`${baseName}_${fallbackSuffix}`);
|
||||
content = (await queryCollection(fallbackKey).first()) as Schema | null;
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
{ watch: [locale] }
|
||||
).then((res) => {
|
||||
if (throwOnMissing && res && !res.data?.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `Page not found: ${baseName} for locale ${locale.value}`,
|
||||
fatal: true,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export const useNavLinks = () => {
|
||||
label: t("common.header.services.children.webDev.label"),
|
||||
description: t("common.header.services.children.webDev.description"),
|
||||
icon: "mdi:web",
|
||||
to: "/webDev"
|
||||
},
|
||||
{
|
||||
label: t("common.header.services.children.softwareDev.label"),
|
||||
|
||||
@@ -71,47 +71,7 @@
|
||||
</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,
|
||||
});
|
||||
}
|
||||
const { data: page } = await useLocalizedCollection("index");
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.seo.title,
|
||||
|
||||
23
app/pages/webDev.vue
Normal file
23
app/pages/webDev.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<UContainer>
|
||||
<UPageHero :title="page?.title" :description="page?.description" />
|
||||
|
||||
<p class="text-muted py-4 text-center">{{ page?.remarks }}</p>
|
||||
|
||||
<UTabs :items="page?.services.map((s) => ({ ...s, slot: s.id }))">
|
||||
<template
|
||||
v-for="service in page?.services"
|
||||
:key="service.id"
|
||||
#[service.id]
|
||||
>
|
||||
<UPricingPlans :plans="service.plans" />
|
||||
</template>
|
||||
</UTabs>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { data: page } = await useLocalizedCollection("webDev");
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
Reference in New Issue
Block a user