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.
207 lines
5.5 KiB
Vue
207 lines
5.5 KiB
Vue
<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>
|