From ccfd2686820c1aa6cf7acc9fd0cc218797fe806f Mon Sep 17 00:00:00 2001 From: xiaomai Date: Fri, 7 Nov 2025 11:04:14 +0800 Subject: [PATCH] feat(webDev): add inquiry modal for pricing plans This commit introduces a 'Contact Sales' modal on the web development page, allowing users to inquire about specific service plans. - The pricing plan buttons now trigger this modal, pre-filled with the selected plan's details. - Users can add custom remarks to their inquiry. - On submission, a pre-formatted message is generated and opened in WhatsApp using a new `useWhatsAppMsgSender` composable. - Adds `NUXT_PUBLIC_WHATSAPP_NUMBER` to the runtime configuration. - Refactors content validation by introducing Zod schemas for `PricingPlan` and `Button` props to improve type safety. - Adds new i18n keys for the modal interface and message templates. --- .env.sample | 1 + .gitignore | 2 +- app/components/webDev/ContactSalesModal.vue | 206 ++++++++++++++++++++ app/composables/WhatsAppMsgSender.ts | 21 ++ app/layouts/default.vue | 2 +- app/pages/webDev.vue | 36 +++- app/schemas/buttonSchema.ts | 37 ++++ app/schemas/pricingPlanSchema.ts | 57 ++++++ content.config.ts | 32 +-- i18n/locales/en-US/common.json | 5 + i18n/locales/en-US/index.json | 10 + i18n/locales/zh-CN/common.json | 5 + i18n/locales/zh-CN/index.json | 10 + nuxt.config.ts | 9 +- 14 files changed, 393 insertions(+), 40 deletions(-) create mode 100644 .env.sample create mode 100644 app/components/webDev/ContactSalesModal.vue create mode 100644 app/composables/WhatsAppMsgSender.ts create mode 100644 app/schemas/buttonSchema.ts create mode 100644 app/schemas/pricingPlanSchema.ts diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..77ca41b --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +NUXT_PUBLIC_WHATSAPP_NUMBER=+60123456789 diff --git a/.gitignore b/.gitignore index 99b3c47..d29ccc6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ logs # Local env files .env -.env.* +# .env.* !.env.example repomix-output.xml \ No newline at end of file diff --git a/app/components/webDev/ContactSalesModal.vue b/app/components/webDev/ContactSalesModal.vue new file mode 100644 index 0000000..f34e14f --- /dev/null +++ b/app/components/webDev/ContactSalesModal.vue @@ -0,0 +1,206 @@ + + + diff --git a/app/composables/WhatsAppMsgSender.ts b/app/composables/WhatsAppMsgSender.ts new file mode 100644 index 0000000..106df7d --- /dev/null +++ b/app/composables/WhatsAppMsgSender.ts @@ -0,0 +1,21 @@ +import { createSharedComposable } from "@vueuse/core"; + +const _useWhatsAppMsgSender = () => { + const config = useRuntimeConfig(); + const phone = config.public.whatsappNumber; + + // --- WhatsApp 自动消息逻辑 --- + const sendMessage = (message: string) => { + const text = encodeURIComponent(message); + const url = `https://api.whatsapp.com/send?phone=${phone}&text=${text}`; + window.open(url, "_blank"); + }; + + return { + sendMessage, + }; +}; + +export const useWhatsAppMsgSender = createSharedComposable( + _useWhatsAppMsgSender +); diff --git a/app/layouts/default.vue b/app/layouts/default.vue index b930397..60bed11 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -3,7 +3,7 @@ diff --git a/app/pages/webDev.vue b/app/pages/webDev.vue index 6a2781b..28ed7ed 100644 --- a/app/pages/webDev.vue +++ b/app/pages/webDev.vue @@ -4,20 +4,44 @@

{{ page?.remarks }}

- - diff --git a/app/schemas/buttonSchema.ts b/app/schemas/buttonSchema.ts new file mode 100644 index 0000000..3343baf --- /dev/null +++ b/app/schemas/buttonSchema.ts @@ -0,0 +1,37 @@ +// buttonSchema.ts +import { z } from "zod"; + +const ButtonSize = z.enum(["2xs", "xs", "sm", "md", "lg", "xl"]); +const ButtonColor = z.enum([ + "error", + "primary", + "secondary", + "success", + "info", + "warning", + "neutral", +]); +const ButtonVariant = z.enum(["solid", "outline", "soft", "ghost", "link"]); + +export const ButtonPropsSchema = z.object({ + size: ButtonSize.optional().default("sm"), + type: z.enum(["button", "submit", "reset"]).optional().default("button"), + label: z.string().optional().default(""), + color: ButtonColor.optional().default("primary"), + variant: ButtonVariant.optional().default("solid"), + icon: z.string().optional().default(""), + leading: z.boolean().optional().default(false), + trailing: z.boolean().optional().default(false), + disabled: z.boolean().optional().default(false), + loading: z.boolean().optional().default(false), + loadingIcon: z.string().optional().default("i-heroicons-arrow-path-20-solid"), + block: z.boolean().optional().default(false), + to: z.string().optional().default(""), + target: z.string().optional().default(""), + padded: z.boolean().optional().default(true), + square: z.boolean().optional().default(false), + truncate: z.boolean().optional().default(false), + attrs: z.record(z.any()).optional().default({}), +}); + +export type ButtonProps = z.infer; diff --git a/app/schemas/pricingPlanSchema.ts b/app/schemas/pricingPlanSchema.ts new file mode 100644 index 0000000..5bbddeb --- /dev/null +++ b/app/schemas/pricingPlanSchema.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; +import { ButtonPropsSchema } from "./buttonSchema"; + +/** + * Zod schema for UPricingPlan component (basic props only) + * Reference: https://ui.nuxt.com/docs/components/pricing-plan#props + */ +export const PricingPlanPropsSchema = z.object({ + /** The title of the pricing plan. */ + title: z.string(), + /** The description text shown under the title. */ + description: z.string().optional(), + /** The current price of the plan. */ + price: z.string().optional(), + /** + * Discounted price. + * When set, the main price will appear with a strikethrough. + */ + discount: z.string().optional(), + /** The unit period (e.g. /month) displayed next to price. */ + billingCycle: z.string().optional(), + /** Additional billing text above the billing cycle. */ + billingPeriod: z.string().optional(), + /** + * List of plan features. + * Can be an array of strings or array of objects (feature items). + */ + features: z.array(z.string()).optional().default([]), + /** The button displayed at the bottom (ButtonProps). */ + button: ButtonPropsSchema.optional(), + /** + * Visual variant of the pricing plan. + * @default "outline" + */ + variant: z.enum(["soft", "solid", "outline", "subtle"]).optional().default("outline"), + /** + * Layout orientation of the component. + * @default "vertical" + */ + orientation: z.enum(["vertical", "horizontal"]).optional().default("vertical"), + /** Optional tagline text displayed above price. */ + tagline: z.string().optional(), + /** Terms or disclaimer text displayed below features. */ + terms: z.string().optional(), + /** + * Highlights the pricing plan visually (adds a ring around it). + * @default false + */ + highlight: z.boolean().optional().default(false), + /** + * Enlarges the plan card slightly for emphasis. + * @default false + */ + scale: z.boolean().optional().default(false), +}); + +export type PricingPlanProps = z.infer; diff --git a/content.config.ts b/content.config.ts index 073d870..49ac46c 100644 --- a/content.config.ts +++ b/content.config.ts @@ -1,4 +1,5 @@ import { defineContentConfig, defineCollection, z } from "@nuxt/content"; +import { PricingPlanPropsSchema } from "./app/schemas/PricingPlanSchema"; const defineIndexSchema = () => z.object({ @@ -50,36 +51,7 @@ const defineWebDevSchema = () => 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(), - }) - ) + .array(PricingPlanPropsSchema) .min(1), // 预留扩展字段(例如:category、tags、hidden 等) category: z.string().optional(), diff --git a/i18n/locales/en-US/common.json b/i18n/locales/en-US/common.json index 236ec9f..6d1bcd3 100644 --- a/i18n/locales/en-US/common.json +++ b/i18n/locales/en-US/common.json @@ -52,6 +52,11 @@ } } } + }, + "button": { + "cancel": "Cancel", + "submit": "Submit", + "saving": "Saving..." } } } diff --git a/i18n/locales/en-US/index.json b/i18n/locales/en-US/index.json index 7a79a43..15c2fa5 100644 --- a/i18n/locales/en-US/index.json +++ b/i18n/locales/en-US/index.json @@ -4,5 +4,15 @@ "featuredProjects": { "viewDemo": "Visit Site" } + }, + "webDev": { + "know_more_description": "I need to know more about the pricing and service about {plan} plan.", + "know_more_title": "Know more {plan} plan", + "contact_intro": "Hi — I'd like to learn more about {service} {plan} plan.", + "which_provides": "Which provides:", + "extra_remarks_title": "Besides that, I'd like to add:", + "remarks_placeholder": "Enter your remarks or requirements, one per line", + "loading_plan": "Loading plan...", + "whatsapp_message": "Hello! I’m interested in the **{plan}** plan under your **{service}** service.\n\nHere are the plan details:\n💰 Price: {price}\n✨ Key Features:\n{featureList}\nAdditionally, I would like to request:\n{remarkList}\nPlease provide more detailed information. Thank you!" } } diff --git a/i18n/locales/zh-CN/common.json b/i18n/locales/zh-CN/common.json index 2966cdf..ca809a9 100644 --- a/i18n/locales/zh-CN/common.json +++ b/i18n/locales/zh-CN/common.json @@ -52,6 +52,11 @@ } } } + }, + "button": { + "cancel": "取消", + "submit": "提交", + "saving": "保存..." } } } diff --git a/i18n/locales/zh-CN/index.json b/i18n/locales/zh-CN/index.json index e359c40..60d1abd 100644 --- a/i18n/locales/zh-CN/index.json +++ b/i18n/locales/zh-CN/index.json @@ -4,5 +4,15 @@ "featuredProjects": { "viewDemo": "访问页面" } + }, + "webDev": { + "know_more_description": "我需要了解有关 {plan} 的定价和服务的更多信息。", + "know_more_title": "了解更多{plan}", + "contact_intro": "你好——我想了解更多关于 {service} {plan}", + "which_provides": "其中包含:", + "extra_remarks_title": "除此之外,我还想补充:", + "remarks_placeholder": "输入您的备注或要求,每行一个", + "loading_plan": "配套加载中。。。", + "whatsapp_message": "您好!我对您的【{service}】服务中的【{plan}】方案感兴趣。\n\n方案详情:\n💰 价格:{price}\n✨ 主要功能:\n{featureList}\n另外我还需要:\n{remarkList}\n请提供更多详细信息,谢谢!" } } diff --git a/nuxt.config.ts b/nuxt.config.ts index ff1b4ac..771dc49 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -19,6 +19,11 @@ export default defineNuxtConfig({ "@nuxtjs/i18n", "@nuxtjs/seo", ], + runtimeConfig: { + public: { + whatsappNumber: "+601234567890", + }, + }, css: ["@/assets/css/main.css"], app: { head: { @@ -36,7 +41,7 @@ export default defineNuxtConfig({ code: "en", iso: "en-US", name: "English", - files: ["en-US/common.json", "zh-CN/index.json"], + files: ["en-US/common.json", "en-US/index.json"], }, { code: "zh-CN", @@ -45,7 +50,7 @@ export default defineNuxtConfig({ files: ["zh-CN/common.json", "zh-CN/index.json"], }, ], - strategy: "no_prefix" + strategy: "no_prefix", }, seo: { meta: {