486 lines
13 KiB
Vue
486 lines
13 KiB
Vue
<script setup>
|
||
const route = useRoute()
|
||
const { isOpen, context, form, closeConsultation } = useConsultation()
|
||
const formElement = ref(null)
|
||
const nameInput = ref(null)
|
||
const errors = ref({})
|
||
const status = ref('idle')
|
||
const submitError = ref('')
|
||
|
||
const industries = ['企业服务', '教育培训', '本地生活', '医疗健康', '电商零售', '其他行业']
|
||
const needs = ['品牌曝光', '精准获客转化', '全域营销', 'GEO 诊断', '内容与媒体分发']
|
||
|
||
function validate() {
|
||
const nextErrors = {}
|
||
if (!form.value.name.trim()) nextErrors.name = '请填写您的姓名'
|
||
if (!form.value.company.trim()) nextErrors.company = '请填写公司名称'
|
||
if (!form.value.contact.trim()) {
|
||
nextErrors.contact = '请填写手机号或邮箱'
|
||
} else if (
|
||
!/^(1\d{10}|[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}|[\d\s()+-]{6,})$/.test(form.value.contact.trim())
|
||
) {
|
||
nextErrors.contact = '联系方式格式不正确'
|
||
}
|
||
if (!form.value.consent) nextErrors.consent = '请确认允许工作人员与您联系'
|
||
errors.value = nextErrors
|
||
return Object.keys(nextErrors).length === 0
|
||
}
|
||
|
||
async function submitForm() {
|
||
submitError.value = ''
|
||
if (!validate()) return
|
||
|
||
status.value = 'submitting'
|
||
try {
|
||
// 后续接入 CRM 或官网线索接口时,在此替换为真实请求。
|
||
await new Promise((resolve) => setTimeout(resolve, 650))
|
||
status.value = 'success'
|
||
} catch {
|
||
status.value = 'error'
|
||
submitError.value = '暂时无法提交,请稍后重试或通过页脚联系方式咨询。'
|
||
}
|
||
}
|
||
|
||
function resetAndClose() {
|
||
status.value = 'idle'
|
||
errors.value = {}
|
||
submitError.value = ''
|
||
closeConsultation()
|
||
}
|
||
|
||
function onKeydown(event) {
|
||
if (event.key === 'Escape' && isOpen.value) resetAndClose()
|
||
}
|
||
|
||
watch(isOpen, async (open) => {
|
||
if (!import.meta.client) return
|
||
document.body.style.overflow = open ? 'hidden' : ''
|
||
if (open) {
|
||
status.value = 'idle'
|
||
errors.value = {}
|
||
await nextTick()
|
||
nameInput.value?.focus()
|
||
}
|
||
})
|
||
|
||
onMounted(() => window.addEventListener('keydown', onKeydown))
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('keydown', onKeydown)
|
||
document.body.style.overflow = ''
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<Teleport to="body">
|
||
<Transition name="consultation">
|
||
<div v-if="isOpen" class="consultation-mask" @mousedown.self="resetAndClose">
|
||
<section
|
||
class="consultation-panel"
|
||
:class="{ 'is-interior': route.path !== '/' }"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
:aria-labelledby="
|
||
status === 'success' ? 'consultation-success-title' : 'consultation-title'
|
||
"
|
||
>
|
||
<button
|
||
class="consultation-close"
|
||
type="button"
|
||
aria-label="关闭咨询弹窗"
|
||
@click="resetAndClose"
|
||
>
|
||
<AppIcon name="close" />
|
||
</button>
|
||
|
||
<div v-if="status === 'success'" class="consultation-success">
|
||
<div class="success-mark" aria-hidden="true"><AppIcon name="check" /></div>
|
||
<p class="modal-kicker">提交成功</p>
|
||
<h2 id="consultation-success-title">需求已经记录</h2>
|
||
<p>我们会根据您填写的行业与目标匹配顾问,请保持联系方式畅通。</p>
|
||
<button class="modal-primary" type="button" @click="resetAndClose">完成</button>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<header class="consultation-heading">
|
||
<p class="modal-kicker">1 对 1 增长沟通</p>
|
||
<h2 id="consultation-title">{{ context.title }}</h2>
|
||
<p>留下基本信息,我们会结合您的业务阶段提供针对性建议。</p>
|
||
</header>
|
||
|
||
<form
|
||
ref="formElement"
|
||
class="consultation-form"
|
||
novalidate
|
||
@submit.prevent="submitForm"
|
||
>
|
||
<div class="form-grid">
|
||
<label>
|
||
<span>姓名 <b>*</b></span>
|
||
<input
|
||
ref="nameInput"
|
||
v-model="form.name"
|
||
type="text"
|
||
autocomplete="name"
|
||
placeholder="怎么称呼您"
|
||
/>
|
||
<small v-if="errors.name">{{ errors.name }}</small>
|
||
</label>
|
||
<label>
|
||
<span>公司名称 <b>*</b></span>
|
||
<input
|
||
v-model="form.company"
|
||
type="text"
|
||
autocomplete="organization"
|
||
placeholder="请输入公司名称"
|
||
/>
|
||
<small v-if="errors.company">{{ errors.company }}</small>
|
||
</label>
|
||
</div>
|
||
|
||
<label>
|
||
<span>联系方式 <b>*</b></span>
|
||
<input
|
||
v-model="form.contact"
|
||
type="text"
|
||
autocomplete="tel"
|
||
placeholder="手机号或工作邮箱"
|
||
/>
|
||
<small v-if="errors.contact">{{ errors.contact }}</small>
|
||
</label>
|
||
|
||
<div class="form-grid">
|
||
<label>
|
||
<span>所属行业</span>
|
||
<select v-model="form.industry">
|
||
<option value="">请选择</option>
|
||
<option v-for="industry in industries" :key="industry" :value="industry">
|
||
{{ industry }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
<span>核心需求</span>
|
||
<select v-model="form.need">
|
||
<option value="">请选择</option>
|
||
<option v-for="need in needs" :key="need" :value="need">{{ need }}</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<label>
|
||
<span>当前问题与目标</span>
|
||
<textarea
|
||
v-model="form.message"
|
||
rows="3"
|
||
placeholder="例如:希望提升品牌在主流 AI 平台的推荐率"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="consent-row">
|
||
<input v-model="form.consent" type="checkbox" />
|
||
<span>我同意工作人员根据以上信息与我联系,并妥善使用本次咨询信息。</span>
|
||
</label>
|
||
<small v-if="errors.consent" class="consent-error">{{ errors.consent }}</small>
|
||
|
||
<p v-if="submitError" class="submit-error">{{ submitError }}</p>
|
||
<p class="source-note">咨询来源:{{ context.source }}</p>
|
||
|
||
<button class="modal-primary" type="submit" :disabled="status === 'submitting'">
|
||
<span v-if="status === 'submitting'" class="submit-dots" aria-hidden="true"
|
||
><i></i><i></i><i></i
|
||
></span>
|
||
<template v-else>提交咨询 <AppIcon name="arrow-right" /></template>
|
||
</button>
|
||
</form>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.consultation-mask {
|
||
position: fixed;
|
||
z-index: 80;
|
||
inset: 0;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 24px;
|
||
background: rgba(15, 35, 62, 0.48);
|
||
backdrop-filter: blur(12px);
|
||
}
|
||
.consultation-panel {
|
||
position: relative;
|
||
width: min(620px, 100%);
|
||
max-height: min(820px, calc(100dvh - 48px));
|
||
overflow-y: auto;
|
||
padding: 40px;
|
||
border: 1px solid rgba(255, 255, 255, 0.74);
|
||
border-radius: 20px;
|
||
background:
|
||
radial-gradient(circle at 88% 8%, rgba(78, 158, 255, 0.16), transparent 31%),
|
||
rgba(255, 255, 255, 0.98);
|
||
box-shadow:
|
||
0 30px 80px rgba(28, 83, 155, 0.24),
|
||
inset 0 1px 0 #fff;
|
||
}
|
||
.consultation-panel.is-interior {
|
||
border-radius: 8px;
|
||
}
|
||
.consultation-close {
|
||
position: absolute;
|
||
top: 18px;
|
||
right: 18px;
|
||
width: 38px;
|
||
height: 38px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 6px;
|
||
color: #4f647d;
|
||
background: #edf5ff;
|
||
}
|
||
.is-interior .consultation-close {
|
||
border: 1px solid rgba(72, 139, 219, 0.12);
|
||
border-radius: 5px;
|
||
}
|
||
.consultation-close :deep(.app-icon) {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
.consultation-heading {
|
||
padding-right: 38px;
|
||
}
|
||
.modal-kicker {
|
||
margin: 0 0 8px;
|
||
color: #207bf2;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.16em;
|
||
}
|
||
.consultation-heading h2,
|
||
.consultation-success h2 {
|
||
margin: 0;
|
||
color: #24364d;
|
||
font-size: clamp(25px, 4vw, 34px);
|
||
letter-spacing: -0.035em;
|
||
line-height: 1.25;
|
||
}
|
||
.consultation-heading > p:last-child,
|
||
.consultation-success > p {
|
||
max-width: 32em;
|
||
margin: 12px 0 0;
|
||
color: #758399;
|
||
font-size: 14px;
|
||
line-height: 1.75;
|
||
}
|
||
.consultation-form {
|
||
display: grid;
|
||
gap: 17px;
|
||
margin-top: 28px;
|
||
}
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 14px;
|
||
}
|
||
.consultation-form label {
|
||
display: grid;
|
||
gap: 7px;
|
||
}
|
||
.consultation-form label > span {
|
||
color: #41536a;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
.consultation-form label > span b {
|
||
color: #2a80ef;
|
||
}
|
||
.consultation-form input:not([type='checkbox']),
|
||
.consultation-form select,
|
||
.consultation-form textarea {
|
||
width: 100%;
|
||
border: 1px solid #dbe7f5;
|
||
border-radius: 8px;
|
||
outline: 0;
|
||
background: rgba(247, 251, 255, 0.82);
|
||
color: #2f4056;
|
||
font: inherit;
|
||
font-size: 14px;
|
||
transition:
|
||
border-color 0.2s,
|
||
box-shadow 0.2s,
|
||
background 0.2s;
|
||
}
|
||
.consultation-form input:not([type='checkbox']),
|
||
.consultation-form select {
|
||
height: 46px;
|
||
padding: 0 13px;
|
||
}
|
||
.consultation-form textarea {
|
||
resize: vertical;
|
||
min-height: 90px;
|
||
padding: 12px 13px;
|
||
line-height: 1.6;
|
||
}
|
||
.consultation-form input:focus,
|
||
.consultation-form select:focus,
|
||
.consultation-form textarea:focus {
|
||
border-color: #66a8fa;
|
||
background: #fff;
|
||
box-shadow: 0 0 0 4px rgba(43, 128, 239, 0.1);
|
||
}
|
||
.consultation-form small,
|
||
.consent-error,
|
||
.submit-error {
|
||
color: #d54b55;
|
||
font-size: 12px;
|
||
}
|
||
.consent-row {
|
||
grid-template-columns: 18px 1fr;
|
||
align-items: start;
|
||
gap: 9px !important;
|
||
cursor: pointer;
|
||
}
|
||
.consent-row input {
|
||
width: 16px;
|
||
height: 16px;
|
||
margin: 2px 0 0;
|
||
accent-color: #247df0;
|
||
}
|
||
.consent-row span {
|
||
color: #5f6e81 !important;
|
||
font-size: 13px !important;
|
||
font-weight: 400 !important;
|
||
line-height: 1.55;
|
||
}
|
||
.source-note {
|
||
margin: -4px 0 0;
|
||
color: #718095;
|
||
font-size: 12px;
|
||
}
|
||
.modal-primary {
|
||
width: 100%;
|
||
height: 50px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
border-radius: 7px;
|
||
color: #fff;
|
||
background: linear-gradient(100deg, #453cff, #00a7ef);
|
||
box-shadow: 0 12px 24px rgba(35, 111, 231, 0.2);
|
||
transition:
|
||
transform 0.2s,
|
||
box-shadow 0.2s,
|
||
opacity 0.2s;
|
||
}
|
||
.modal-primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 16px 30px rgba(35, 111, 231, 0.28);
|
||
}
|
||
.modal-primary:active {
|
||
transform: translateY(1px) scale(0.99);
|
||
}
|
||
.modal-primary:disabled {
|
||
cursor: wait;
|
||
opacity: 0.72;
|
||
}
|
||
.submit-dots {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
.submit-dots i {
|
||
width: 5px;
|
||
height: 5px;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
animation: submit-dot 0.8s infinite alternate;
|
||
}
|
||
.submit-dots i:nth-child(2) {
|
||
animation-delay: 0.15s;
|
||
}
|
||
.submit-dots i:nth-child(3) {
|
||
animation-delay: 0.3s;
|
||
}
|
||
.consultation-success {
|
||
min-height: 360px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
}
|
||
.consultation-success .modal-primary {
|
||
max-width: 220px;
|
||
margin-top: 28px;
|
||
}
|
||
.success-mark {
|
||
width: 68px;
|
||
height: 68px;
|
||
display: grid;
|
||
place-items: center;
|
||
margin-bottom: 20px;
|
||
border-radius: 22px;
|
||
color: #fff;
|
||
background: linear-gradient(145deg, #2a86f5, #65c7f4);
|
||
box-shadow: 0 16px 30px rgba(45, 131, 239, 0.25);
|
||
font-size: 30px;
|
||
}
|
||
.is-interior .success-mark {
|
||
border-radius: 8px;
|
||
}
|
||
.consultation-enter-active,
|
||
.consultation-leave-active {
|
||
transition: opacity 0.25s ease;
|
||
}
|
||
.consultation-enter-active .consultation-panel,
|
||
.consultation-leave-active .consultation-panel {
|
||
transition:
|
||
transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
|
||
opacity 0.22s;
|
||
}
|
||
.consultation-enter-from,
|
||
.consultation-leave-to {
|
||
opacity: 0;
|
||
}
|
||
.consultation-enter-from .consultation-panel,
|
||
.consultation-leave-to .consultation-panel {
|
||
opacity: 0;
|
||
transform: translateY(18px) scale(0.98);
|
||
}
|
||
@keyframes submit-dot {
|
||
to {
|
||
transform: translateY(-4px);
|
||
opacity: 0.45;
|
||
}
|
||
}
|
||
@media (max-width: 640px) {
|
||
.consultation-mask {
|
||
align-items: end;
|
||
padding: 0;
|
||
}
|
||
.consultation-panel {
|
||
width: 100%;
|
||
max-height: calc(100dvh - 24px);
|
||
padding: 32px 20px 24px;
|
||
border-radius: 22px 22px 0 0;
|
||
}
|
||
.consultation-panel.is-interior {
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
.form-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.consultation-close {
|
||
top: 13px;
|
||
right: 13px;
|
||
}
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.submit-dots i {
|
||
animation: none;
|
||
}
|
||
}
|
||
</style>
|