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.
This commit is contained in:
1
.env.sample
Normal file
1
.env.sample
Normal file
@@ -0,0 +1 @@
|
|||||||
|
NUXT_PUBLIC_WHATSAPP_NUMBER=+60123456789
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -20,7 +20,7 @@ logs
|
|||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
.env
|
.env
|
||||||
.env.*
|
# .env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
repomix-output.xml
|
repomix-output.xml
|
||||||
206
app/components/webDev/ContactSalesModal.vue
Normal file
206
app/components/webDev/ContactSalesModal.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<UModal
|
||||||
|
v-model:open="open"
|
||||||
|
:title="
|
||||||
|
$t('webDev.know_more_title', {
|
||||||
|
plan: pkg?.planTitle || '—',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:description="
|
||||||
|
$t('webDev.know_more_description', {
|
||||||
|
plan: pkg?.planTitle || '—',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
aria-label="Contact Sales Modal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="!pkg" class="py-6">
|
||||||
|
<!-- 占位 / loading -->
|
||||||
|
<div class="text-center text-sm text-muted">
|
||||||
|
{{ $t("webDev.loading_plan", "Loading plan...") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UForm
|
||||||
|
v-else
|
||||||
|
ref="form"
|
||||||
|
:schema="schema"
|
||||||
|
:state="state"
|
||||||
|
class="space-y-4"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-medium">
|
||||||
|
{{
|
||||||
|
$t("webDev.contact_intro", {
|
||||||
|
service: pkg.serviceTitle,
|
||||||
|
plan: pkg.planTitle,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-lg font-medium">
|
||||||
|
{{ $t("webDev.which_provides", "Which provides:") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li v-for="(feature, idx) in pkg.features" :key="idx">
|
||||||
|
<UIcon
|
||||||
|
size="16"
|
||||||
|
name="mdi:check-circle-outline"
|
||||||
|
class="text-primary-500 mr-2"
|
||||||
|
/>{{ feature }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="text-lg font-medium">
|
||||||
|
{{
|
||||||
|
$t("webDev.extra_remarks_title", "Besides that, I'd like to add:")
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UFormField name="remarks">
|
||||||
|
<UTextarea
|
||||||
|
v-model="state.remarks"
|
||||||
|
:placeholder="
|
||||||
|
$t(
|
||||||
|
'webDev.remarks_placeholder',
|
||||||
|
'Enter your remarks or requirements, one per line'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="w-full"
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</UForm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="w-full flex gap-4">
|
||||||
|
<UButton
|
||||||
|
:label="$t('common.button.cancel')"
|
||||||
|
variant="subtle"
|
||||||
|
color="neutral"
|
||||||
|
class="flex-1"
|
||||||
|
@click="closeModal"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
:label="
|
||||||
|
isSubmitting
|
||||||
|
? $t('common.button.saving', 'Saving...')
|
||||||
|
: $t('common.button.submit')
|
||||||
|
"
|
||||||
|
variant="solid"
|
||||||
|
color="success"
|
||||||
|
class="flex-1"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import * as z from "zod";
|
||||||
|
import type { FormSubmitEvent } from "@nuxt/ui";
|
||||||
|
|
||||||
|
/* ----- types ----- */
|
||||||
|
type ContactSalesModalProps = {
|
||||||
|
serviceTitle: string;
|
||||||
|
planTitle: string;
|
||||||
|
startingPrice?: string;
|
||||||
|
features: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----- reactive state ----- */
|
||||||
|
const pkg = ref<ContactSalesModalProps | null>(null);
|
||||||
|
const open = ref<boolean>(false);
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
|
/* form ref(保持和你原来用法一致) */
|
||||||
|
const form = useTemplateRef("form");
|
||||||
|
|
||||||
|
/* ----- zod schema ----- */
|
||||||
|
/* 注意:zod 的提示文本这里使用英文/固定字符串,若要用 i18n 需要在 validate 时把错误替换为 t(...) */
|
||||||
|
const schema = z.object({
|
||||||
|
remarks: z.string().optional(),
|
||||||
|
});
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
/* ----- 表单状态,初始化为空(openModal 时会填充) ----- */
|
||||||
|
const state = reactive<Partial<Schema>>({
|
||||||
|
remarks: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ----- 外部调用:打开 modal 并填充数据 ----- */
|
||||||
|
const openModal = (pricingPlan: ContactSalesModalProps) => {
|
||||||
|
pkg.value = { ...pricingPlan };
|
||||||
|
state.remarks = "";
|
||||||
|
open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----- 关闭并重置 ----- */
|
||||||
|
const closeModal = () => {
|
||||||
|
open.value = false;
|
||||||
|
// 延迟清理以防动画或确认逻辑中读取到空
|
||||||
|
setTimeout(() => {
|
||||||
|
pkg.value = null;
|
||||||
|
state.remarks = "";
|
||||||
|
// 如果需要也可以重置表单验证状态: form.value?.reset()
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----- 表单提交处理 ----- */
|
||||||
|
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||||
|
// event.data 已通过 schema 验证
|
||||||
|
try {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
const payload = {
|
||||||
|
planTitle: pkg.value?.planTitle,
|
||||||
|
features: pkg.value?.features,
|
||||||
|
remarks: (event.data.remarks || "")
|
||||||
|
.split("\n")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
// 可以加上更多 metadata,比如 timestamp / user id / source page
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: 在此发送到后端 API,例如:
|
||||||
|
// await $fetch('/api/contact-sales', { method: 'POST', body: payload });
|
||||||
|
|
||||||
|
// 临时方案,发送到 WhatsApp 去
|
||||||
|
const wa_msg = $t("webDev.whatsapp_message", {
|
||||||
|
service: pkg.value?.serviceTitle,
|
||||||
|
plan: pkg.value?.planTitle,
|
||||||
|
price: pkg.value?.startingPrice,
|
||||||
|
featureList: pkg.value?.features.map((f) => ` ✅ ${f}`).join("\n"),
|
||||||
|
remarkList: (event.data.remarks || "")
|
||||||
|
.split("\n")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((r) => `- ${r}`)
|
||||||
|
.join("\n"),
|
||||||
|
});
|
||||||
|
useWhatsAppMsgSender().sendMessage(wa_msg);
|
||||||
|
|
||||||
|
// 成功提示(如果你有全局 toast/notification): e.g. useToast().success(...)
|
||||||
|
// 关闭并清理
|
||||||
|
closeModal();
|
||||||
|
} catch (err) {
|
||||||
|
// 处理错误(显示错误 toast / 控制台)
|
||||||
|
console.error("submit failed", err);
|
||||||
|
// 这里可以显示友好的错误信息,例如: useToast().error(t('webDev.submit_failed'))
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* footer 按钮触发的提交(触发表单验证) */
|
||||||
|
async function handleSubmit() {
|
||||||
|
await form.value?.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暴露给父组件的 API */
|
||||||
|
defineExpose({ openModal, closeModal });
|
||||||
|
</script>
|
||||||
21
app/composables/WhatsAppMsgSender.ts
Normal file
21
app/composables/WhatsAppMsgSender.ts
Normal file
@@ -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
|
||||||
|
);
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<UHeader
|
<UHeader
|
||||||
:ui="{
|
:ui="{
|
||||||
left: 'flex items-center gap-1.5',
|
left: 'flex items-center gap-1.5',
|
||||||
center: 'hidden lg:flex lg:flex-16',
|
center: 'hidden lg:flex lg:flex-4',
|
||||||
right: 'flex items-center justify-end gap-1.5',
|
right: 'flex items-center justify-end gap-1.5',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,20 +4,44 @@
|
|||||||
|
|
||||||
<p class="text-muted py-4 text-center">{{ page?.remarks }}</p>
|
<p class="text-muted py-4 text-center">{{ page?.remarks }}</p>
|
||||||
|
|
||||||
<UTabs :items="page?.services.map((s) => ({ ...s, slot: s.id }))">
|
<UTabs :items="serviceTabs">
|
||||||
<template
|
<template v-for="service in serviceTabs" :key="service.id" #[service.id]>
|
||||||
v-for="service in page?.services"
|
|
||||||
:key="service.id"
|
|
||||||
#[service.id]
|
|
||||||
>
|
|
||||||
<UPricingPlans :plans="service.plans" />
|
<UPricingPlans :plans="service.plans" />
|
||||||
</template>
|
</template>
|
||||||
</UTabs>
|
</UTabs>
|
||||||
|
<WebDevContactSalesModal ref="webDevContactSalesModal" />
|
||||||
</UContainer>
|
</UContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import WebDevContactSalesModal from "~/components/webDev/ContactSalesModal.vue";
|
||||||
|
|
||||||
const { data: page } = await useLocalizedCollection("webDev");
|
const { data: page } = await useLocalizedCollection("webDev");
|
||||||
|
|
||||||
|
const webDevContactSalesModal = ref<InstanceType<
|
||||||
|
typeof WebDevContactSalesModal
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
|
const serviceTabs = computed(() =>
|
||||||
|
page.value?.services.map((srv) => ({
|
||||||
|
...srv,
|
||||||
|
plans: srv.plans.map((pln) => ({
|
||||||
|
...pln,
|
||||||
|
button: {
|
||||||
|
label: "立刻咨询", // TODO: i18n 适配
|
||||||
|
onClick: () => {
|
||||||
|
webDevContactSalesModal.value?.openModal({
|
||||||
|
serviceTitle: srv.label,
|
||||||
|
planTitle: pln.title,
|
||||||
|
startingPrice: pln.price,
|
||||||
|
features: pln.features ?? [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
slot: srv.id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
37
app/schemas/buttonSchema.ts
Normal file
37
app/schemas/buttonSchema.ts
Normal file
@@ -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<typeof ButtonPropsSchema>;
|
||||||
57
app/schemas/pricingPlanSchema.ts
Normal file
57
app/schemas/pricingPlanSchema.ts
Normal file
@@ -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<typeof PricingPlanPropsSchema>;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
||||||
|
import { PricingPlanPropsSchema } from "./app/schemas/PricingPlanSchema";
|
||||||
|
|
||||||
const defineIndexSchema = () =>
|
const defineIndexSchema = () =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -50,36 +51,7 @@ const defineWebDevSchema = () =>
|
|||||||
icon: z.string().optional(), // 比如 "lucide:mouse-pointer-click"
|
icon: z.string().optional(), // 比如 "lucide:mouse-pointer-click"
|
||||||
// 你原结构里通过 createService 包装,但最终是一个对象
|
// 你原结构里通过 createService 包装,但最终是一个对象
|
||||||
plans: z
|
plans: z
|
||||||
.array(
|
.array(PricingPlanPropsSchema)
|
||||||
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),
|
.min(1),
|
||||||
// 预留扩展字段(例如:category、tags、hidden 等)
|
// 预留扩展字段(例如:category、tags、hidden 等)
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"submit": "Submit",
|
||||||
|
"saving": "Saving..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,15 @@
|
|||||||
"featuredProjects": {
|
"featuredProjects": {
|
||||||
"viewDemo": "Visit Site"
|
"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!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"cancel": "取消",
|
||||||
|
"submit": "提交",
|
||||||
|
"saving": "保存..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,15 @@
|
|||||||
"featuredProjects": {
|
"featuredProjects": {
|
||||||
"viewDemo": "访问页面"
|
"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请提供更多详细信息,谢谢!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export default defineNuxtConfig({
|
|||||||
"@nuxtjs/i18n",
|
"@nuxtjs/i18n",
|
||||||
"@nuxtjs/seo",
|
"@nuxtjs/seo",
|
||||||
],
|
],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
whatsappNumber: "+601234567890",
|
||||||
|
},
|
||||||
|
},
|
||||||
css: ["@/assets/css/main.css"],
|
css: ["@/assets/css/main.css"],
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
@@ -36,7 +41,7 @@ export default defineNuxtConfig({
|
|||||||
code: "en",
|
code: "en",
|
||||||
iso: "en-US",
|
iso: "en-US",
|
||||||
name: "English",
|
name: "English",
|
||||||
files: ["en-US/common.json", "zh-CN/index.json"],
|
files: ["en-US/common.json", "en-US/index.json"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "zh-CN",
|
code: "zh-CN",
|
||||||
@@ -45,7 +50,7 @@ export default defineNuxtConfig({
|
|||||||
files: ["zh-CN/common.json", "zh-CN/index.json"],
|
files: ["zh-CN/common.json", "zh-CN/index.json"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
strategy: "no_prefix"
|
strategy: "no_prefix",
|
||||||
},
|
},
|
||||||
seo: {
|
seo: {
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
Reference in New Issue
Block a user