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:
xiaomai
2025-11-06 09:02:50 +08:00
parent 31a4103f9b
commit 8cc04b7f59
7 changed files with 339 additions and 41 deletions

View 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;
});
}

View File

@@ -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"),

View File

@@ -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
View 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>

View File

@@ -40,6 +40,54 @@ const defineIndexSchema = () =>
}),
});
const defineWebDevSchema = () =>
z.object({
remarks: z.string(),
services: z.array(
z.object({
id: z.string().min(1),
label: z.string().min(1),
icon: z.string().optional(), // 比如 "lucide:mouse-pointer-click"
// 你原结构里通过 createService 包装,但最终是一个对象
plans: z
.array(
z.object({
title: z.string().min(1),
description: z.string().optional(),
price: z.string().optional(), // 保持为 string例如 "RM499 起"),如需更严可用 regex
discount: z.string().optional(),
tagline: z.string().optional(),
highlight: z.boolean().optional().default(false),
features: z.array(z.string()).optional().default([]),
button: z
.object({
label: z.string().min(1),
href: z.string().url().optional(), // 如果你可能会传链接
// 预留扩展字段例如variant、target 等)
variant: z
.enum([
"link",
"solid",
"outline",
"soft",
"subtle",
"ghost",
])
.default("subtle"),
target: z.enum(["_self", "_blank"]).optional(),
})
.partial({ href: true, variant: true, target: true }) // 全部可选除 label如果你希望 label 也可选,可把 min(1) 去掉或改 optional
.optional(),
})
)
.min(1),
// 预留扩展字段例如category、tags、hidden 等)
category: z.string().optional(),
tags: z.array(z.string()).optional(),
})
),
});
export default defineContentConfig({
collections: {
index_en: defineCollection({
@@ -52,5 +100,15 @@ export default defineContentConfig({
source: "zh-CN/index.yml",
schema: defineIndexSchema(),
}),
webDev_en: defineCollection({
type: "page",
source: "en-US/webDev.yml",
schema: defineWebDevSchema(),
}),
webDev_zh: defineCollection({
type: "page",
source: "zh-CN/webDev.yml",
schema: defineWebDevSchema(),
}),
},
});

95
content/en-US/webDev.yml Normal file
View File

@@ -0,0 +1,95 @@
seo:
title: "Web / Website Custom Development"
description: "Create a tailor-made online portal for your brand to boost visibility and conversion rates."
title: "Web / Website Custom Development"
description: "Create a tailor-made online portal for your brand to boost visibility and conversion rates."
remarks: "All plans include basic domain and server deployment for the first year. If server load becomes too high, we will assist with migration to a more stable third-party environment."
services:
- id: landing-page
label: "Landing Page"
icon: "mdi:cursor-default-click-outline"
plans:
- title: "Basic"
description: "Ideal for individuals or small businesses to launch a one-page showcase website quickly."
price: "From RM899"
tagline: "Includes domain & hosting"
features:
- "Single-page structure (13 sections)"
- "Responsive design (mobile / tablet / desktop)"
- "Basic text and image layout"
- "Contact form or WhatsApp button"
- "Google Analytics integration"
- "1 free revision"
- "Delivery within 7 days"
button:
label: "Contact Now"
- title: "Standard"
description: "Designed for brands and startups to create high-conversion pages."
price: "From RM1,599"
tagline: "Includes domain & hosting"
highlight: true
features:
- "Multi-section layout (46 sections)"
- "Custom brand styling and color scheme"
- "Lightweight animations and motion effects"
- "SEO optimization + performance tuning"
- "Tracking integration (GA / Pixel)"
- "2 free revisions"
- "Delivery within 14 days"
button:
label: "Request Quote"
- title: "Premium Custom"
description: "Comprehensive visual and marketing upgrade for established brands."
price: "From RM2,999"
features:
- "Exclusive visual design and interaction experience"
- "Complete brand style system"
- "A/B testing and conversion optimization"
- "Marketing tool integration (email, analytics, CRM)"
- "Multi-language / dynamic content support"
button:
label: "Book a Custom Plan"
- id: official-web
label: "Official Website"
icon: "lucide:globe"
plans:
- title: "Basic Website"
description: "Build a professional online presence for small and medium-sized businesses."
price: "From RM3,999"
tagline: "Includes domain & hosting"
features:
- "Up to 5 pages (Home, About, Services, Contact, etc.)"
- "Responsive design (desktop / tablet / mobile)"
- "Basic SEO setup"
- "Contact form + map + social media links"
button:
label: "Contact Now"
- title: "Standard Website"
description: "Ideal for brand upgrades and content-rich businesses."
price: "From RM6,999"
# discount: "From RM4,999"
tagline: "Includes domain & hosting"
highlight: true
features:
- "Around 812 pages (Case Studies, Blog, Team, etc.)"
- "Custom brand styling + UI/UX optimization"
- "Lightweight CMS for content management"
- "Advanced SEO optimization and performance acceleration"
button:
label: "Book Standard Plan"
- title: "Enterprise Custom"
description: "Fully tailored visual, functional, and interactive experience."
price: "From RM15,000"
features:
- "Fully custom UI / animation design"
- "Multi-language support / client login module"
- "API / third-party system integrations"
- "Enhanced security and automated backup mechanism"
button:
label: "Book Enterprise Plan"

95
content/zh-CN/webDev.yml Normal file
View File

@@ -0,0 +1,95 @@
seo:
title: "网页 / 网站定制开发"
description: "为您的品牌量身定做一套线上门户,增加产品曝光率与转化率。"
title: "网页 / 网站定制开发"
description: "为您的品牌量身定做一套线上门户,增加产品曝光率与转化率。"
remarks: "所有方案均含基础域名与服务器部署(首年)。若服务器负载过高,将协助迁移至更稳定的第三方环境。"
services:
- id: landing-page
label: "Landing Page"
icon: "mdi:cursor-default-click-outline"
plans:
- title: "基础版"
description: "适合个人、小型商家,快速上线单页展示网站。"
price: "RM899 起"
tagline: "含域名与服务器"
features:
- "单页面结构1-3 屏)"
- "响应式设计(手机 / 平板 / 桌面)"
- "基本图文排版"
- "联系表单或 WhatsApp 按钮"
- "Google Analytics 整合"
- "1 次免费修改"
- "7 日内交付"
button:
label: "立即咨询"
- title: "标准版"
description: "为品牌与创业项目打造高转化页面。"
price: "RM1,599 起"
tagline: "含域名与服务器"
highlight: true
features:
- "多区块结构4-6 屏)"
- "品牌定制风格与配色"
- "轻量动画与动效展示"
- "SEO 优化 + 加载性能优化"
- "整合追踪代码GA / Pixel"
- "2 次免费修改"
- "14 日内交付"
button:
label: "预约报价"
- title: "高级定制"
description: "为成熟品牌提供全面视觉与营销升级方案。"
price: "RM2,999 起"
features:
- "专属视觉设计与交互体验"
- "完整品牌风格系统"
- "A/B 测试与转化优化"
- "营销工具整合邮件、统计、CRM"
- "多语言 / 动态内容支持"
button:
label: "预约定制方案"
- id: official-web
label: "Official Website"
icon: "lucide:globe"
plans:
- title: "基础官网"
description: "为中小型企业建立专业在线形象。"
price: "RM3,999 起"
tagline: "含域名与服务器"
features:
- "最多 5 个页面(首页、关于、服务、联系等)"
- "响应式设计(桌面 / 平板 / 手机)"
- "基础 SEO 设置"
- "联系表单 + 地图 + 社交媒体链接"
button:
label: "立即咨询"
- title: "标准官网"
description: "适合品牌升级与内容扩展型企业。"
price: "RM6,999 起"
# discount: "RM4,999 起"
tagline: "含域名与服务器"
highlight: true
features:
- "约 8-12 个页面(案例、博客、团队等)"
- "品牌定制风格 + UI/UX 优化"
- "轻量 CMS 后台管理系统"
- "进阶 SEO 优化与性能加速"
button:
label: "预约标准方案"
- title: "企业定制"
description: "专属视觉、功能与交互体验整合。"
price: "RM15,000 起"
features:
- "完全定制 UI / 动效设计"
- "多语言支持 / 客户登录模块"
- "API / 第三方系统整合"
- "增强安全与自动备份机制"
button:
label: "预约企业方案"