完善咨询表单与企业微信诉求推送

This commit is contained in:
Rei
2026-09-30 18:23:01 +08:00
parent f35ee24510
commit 530f726130
8 changed files with 448 additions and 70 deletions
+176
View File
@@ -6,6 +6,173 @@
padding: 32px;
}
.contact-form-intro {
font-size: 13px;
color: #5a6b84;
}
.contact-form {
display: grid;
gap: 18px;
margin-top: 20px;
}
.contact-field {
display: grid;
gap: 7px;
color: #1a2332;
font-size: 14px;
font-weight: 600;
}
.contact-field input,
.contact-field textarea {
width: 100%;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #fff;
color: #1a2332;
font: inherit;
font-weight: 400;
line-height: 1.5;
padding: 11px 12px;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.contact-field textarea {
min-height: 108px;
resize: vertical;
}
.contact-field input::placeholder,
.contact-field textarea::placeholder {
color: #64748b;
}
.contact-field input:focus,
.contact-field textarea:focus {
border-color: #163e70;
box-shadow: 0 0 0 3px rgba(22, 62, 112, 0.12);
outline: none;
}
.contact-field input[aria-invalid="true"],
.contact-field textarea[aria-invalid="true"] {
border-color: #be123c;
}
.field-required {
color: #c81e3a;
font-weight: 800;
}
.field-optional,
.field-help {
color: #64748b;
font-size: 12px;
font-weight: 400;
}
.contact-needs {
min-width: 0;
border: 0;
}
.contact-needs legend {
color: #1a2332;
font-size: 14px;
font-weight: 600;
}
.contact-needs .field-help {
margin-top: 3px;
}
.contact-needs-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 10px;
}
.contact-need-option {
display: flex;
align-items: center;
min-height: 46px;
padding: 10px 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #fff;
color: #334155;
cursor: pointer;
font-size: 13px;
line-height: 1.4;
transition: border-color 0.18s ease, background-color 0.18s ease, color 0.18s ease;
}
.contact-need-option:hover {
border-color: #7691b2;
}
.contact-need-option.is-selected {
border-color: #163e70;
background: #eef4fb;
color: #163e70;
font-weight: 600;
}
.contact-need-option input {
width: 17px;
height: 17px;
margin: 0 9px 0 0;
accent-color: #163e70;
flex: 0 0 auto;
}
.field-error {
display: block;
margin-top: 5px;
color: #be123c;
font-size: 12px;
font-weight: 500;
}
.contact-other-need {
padding-left: 14px;
border-left: 3px solid #163e70;
}
.contact-submit {
width: 100%;
min-height: 46px;
}
.contact-submit:disabled {
cursor: wait;
opacity: 0.68;
}
.form-message {
border-radius: 8px;
padding: 10px 12px;
font-size: 13px;
}
.form-message-success {
background: #ecfdf5;
color: #0f6b5b;
}
.form-message-error {
background: #fff1f2;
color: #9f1239;
}
.contact-privacy {
color: #64748b;
font-size: 12px;
}
@media (max-width: 768px) {
.contact-form-container {
max-width: none;
@@ -14,4 +181,13 @@
.contact-form-card {
padding: 24px 20px;
}
.contact-needs-grid {
grid-template-columns: 1fr;
}
.contact-need-option {
min-height: 48px;
font-size: 14px;
}
}
+146 -50
View File
@@ -3,16 +3,65 @@ import { Link } from 'react-router-dom'
import { useCMSTexts } from '@/services/cms'
import './contact.css'
const NEED_OPTIONS = [
'IT数字化部署',
'AI智能体部署',
'经营体系建设',
'项目融资需求',
'实缴合规管理',
'获客自动化部署',
'其他个性需求',
] as const
const OTHER_NEED = '其他个性需求'
const PHONE_PATTERN = /^1[3-9]\d{9}$/
type ContactForm = {
name: string
phone: string
company: string
needs: string[]
otherNeed: string
}
const EMPTY_FORM: ContactForm = { name: '', phone: '', company: '', needs: [], otherNeed: '' }
export default function Contact() {
const { data: texts } = useCMSTexts()
const [form, setForm] = useState({ name: '', company: '', phone: '', need: '' })
const [form, setForm] = useState<ContactForm>(EMPTY_FORM)
const [submitted, setSubmitted] = useState(false)
const [error, setError] = useState('')
const [fieldErrors, setFieldErrors] = useState({ phone: '', needs: '', otherNeed: '' })
const [saving, setSaving] = useState(false)
const toggleNeed = (need: string) => {
setSubmitted(false)
setFieldErrors(current => ({ ...current, needs: '', otherNeed: '' }))
setForm(current => {
const selected = current.needs.includes(need)
return {
...current,
needs: selected ? current.needs.filter(item => item !== need) : [...current.needs, need],
otherNeed: selected && need === OTHER_NEED ? '' : current.otherNeed,
}
})
}
const validate = () => {
const nextErrors = {
phone: PHONE_PATTERN.test(form.phone) ? '' : '请输入合法的11位中国大陆手机号',
needs: form.needs.length > 0 ? '' : '请至少选择一项核心诉求',
otherNeed: form.needs.includes(OTHER_NEED) && !form.otherNeed.trim() ? '请填写其他个性需求' : '',
}
setFieldErrors(nextErrors)
return !Object.values(nextErrors).some(Boolean)
}
const submit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setSubmitted(false)
if (!validate()) return
setSaving(true)
try {
const response = await fetch('/api/contact/leads', {
@@ -23,7 +72,8 @@ export default function Contact() {
const result = await response.json()
if (!response.ok || result.code !== 0) throw new Error(result.msg || '提交失败')
setSubmitted(true)
setForm({ name: '', company: '', phone: '', need: '' })
setForm(EMPTY_FORM)
setFieldErrors({ phone: '', needs: '', otherNeed: '' })
} catch (e) {
setError(e instanceof Error ? e.message : '提交失败,请稍后再试')
} finally {
@@ -47,57 +97,103 @@ export default function Contact() {
</div>
<section className="section">
<div className="container contact-form-container">
<div className="card contact-form-card">
<h3>咨询获取方案</h3>
<p style={{ fontSize: 13, color: '#64748b' }}>提交后将进入顾问线索列表,请填写真实联系方式。</p>
<form onSubmit={submit} style={{ marginTop: 16, display: 'grid', gap: 12 }}>
<label style={{ fontSize: 13 }}>
姓名
<input
required
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
placeholder="请输入姓名"
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
/>
</label>
<label style={{ fontSize: 13 }}>
企业名称
<input
value={form.company}
onChange={e => setForm({ ...form, company: e.target.value })}
placeholder={texts['contact.company_placeholder'] || '选填'}
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
/>
</label>
<label style={{ fontSize: 13 }}>
联系电话
<input
required
value={form.phone}
onChange={e => setForm({ ...form, phone: e.target.value })}
placeholder="请输入手机号"
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
/>
</label>
<label style={{ fontSize: 13 }}>
需求简述
<div className="card contact-form-card">
<h3>咨询获取方案</h3>
<p className="contact-form-intro">请留下有效联系方式,并选择希望优先解决的核心诉求。</p>
<form className="contact-form" onSubmit={submit} noValidate>
<label className="contact-field">
<span>您的称呼 <span className="field-optional">(选填)</span></span>
<input
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
placeholder="请输入您的称呼"
autoComplete="name"
maxLength={80}
/>
</label>
<label className="contact-field">
<span>联系电话 <span className="field-required" aria-hidden="true">*</span></span>
<input
required
type="tel"
inputMode="numeric"
autoComplete="tel"
value={form.phone}
onChange={e => {
const phone = e.target.value.replace(/\D/g, '').slice(0, 11)
setForm({ ...form, phone })
setFieldErrors(current => ({ ...current, phone: '' }))
}}
onBlur={() => setFieldErrors(current => ({
...current,
phone: PHONE_PATTERN.test(form.phone) ? '' : '请输入合法的11位中国大陆手机号',
}))}
placeholder="请输入11位手机号"
maxLength={11}
aria-invalid={Boolean(fieldErrors.phone)}
aria-describedby={fieldErrors.phone ? 'phone-error' : undefined}
/>
{fieldErrors.phone && <span id="phone-error" className="field-error">{fieldErrors.phone}</span>}
</label>
<label className="contact-field">
<span>企业名 <span className="field-optional">(可选)</span></span>
<input
value={form.company}
onChange={e => setForm({ ...form, company: e.target.value })}
placeholder={texts['contact.company_placeholder'] || '请输入企业名称'}
autoComplete="organization"
maxLength={160}
/>
</label>
<fieldset className="contact-needs" aria-describedby={fieldErrors.needs ? 'needs-error' : 'needs-help'}>
<legend>核心诉求 <span className="field-required" aria-hidden="true">*</span></legend>
<p id="needs-help" className="field-help">可多选,请至少选择一项</p>
<div className="contact-needs-grid">
{NEED_OPTIONS.map(option => (
<label className={`contact-need-option${form.needs.includes(option) ? ' is-selected' : ''}`} key={option}>
<input
type="checkbox"
checked={form.needs.includes(option)}
onChange={() => toggleNeed(option)}
/>
<span>{option}</span>
</label>
))}
</div>
{fieldErrors.needs && <span id="needs-error" className="field-error">{fieldErrors.needs}</span>}
</fieldset>
{form.needs.includes(OTHER_NEED) && (
<label className="contact-field contact-other-need">
<span>其他个性需求 <span className="field-required" aria-hidden="true">*</span></span>
<textarea
value={form.need}
onChange={e => setForm({ ...form, need: e.target.value })}
placeholder={texts['contact.need_placeholder'] || '请简述你的企业现状与期望'}
rows={3}
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
required
value={form.otherNeed}
onChange={e => {
setForm({ ...form, otherNeed: e.target.value })
setFieldErrors(current => ({ ...current, otherNeed: '' }))
}}
placeholder="请具体描述您的个性需求"
rows={4}
maxLength={1000}
aria-invalid={Boolean(fieldErrors.otherNeed)}
aria-describedby={fieldErrors.otherNeed ? 'other-need-error' : undefined}
/>
{fieldErrors.otherNeed && <span id="other-need-error" className="field-error">{fieldErrors.otherNeed}</span>}
</label>
<button type="submit" className="btn btn-primary" style={{ width: '100%' }} disabled={saving}>
{saving ? '提交中…' : '提交咨询'}
</button>
{submitted && <div style={{ fontSize: 13, color: '#0f6b5b', background: '#ecfdf5', padding: 10, borderRadius: 8 }}>已提交,顾问将尽快联系。</div>}
{error && <div style={{ fontSize: 13, color: '#be123c', background: '#fff1f2', padding: 10, borderRadius: 8 }}>{error}</div>}
<p style={{ fontSize: 11, color: '#94a3b8' }}>{texts['contact.privacy_note'] || '提交即视为同意隐私与合规说明。'}</p>
</form>
</div>
)}
<button type="submit" className="btn btn-primary contact-submit" disabled={saving}>
{saving ? '提交中...' : '提交咨询'}
</button>
{submitted && <div className="form-message form-message-success" role="status">已提交,顾问将尽快联系。</div>}
{error && <div className="form-message form-message-error" role="alert">{error}</div>}
<p className="contact-privacy">{texts['contact.privacy_note'] || '提交即视为同意隐私与合规说明。'}</p>
</form>
</div>
</div>
</section>
<section className="cta-band">