Files
tootaio.com/app/components/webDev/ContactSalesModal.vue
xiaomai ccfd268682 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.
2025-11-07 11:04:14 +08:00

207 lines
5.5 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>