完善咨询表单与企业微信诉求推送
This commit is contained in:
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user