feat(ui/ux): 优化用户体验和可访问性

- 字体加载优化: 添加 font-display: block 策略,创建 useFontLoading hook
- 色彩对比度: 调整 text-muted 和 text-tertiary 颜色值确保 WCAG AA 合规
- 滚动进度条: 新增 ScrollProgress 组件,支持 reduced motion
- 表单自动保存: 新增 useFormAutosave hook,防止用户数据丢失
- 返回顶部按钮: 新增 BackToTop 组件,提升长页面导航体验
- 图片懒加载: 优化 OptimizedImage 组件,添加 blur placeholder 和加载动画

所有新组件均包含完整测试,1450+ 测试通过
This commit is contained in:
张翔
2026-03-28 11:21:04 +08:00
parent ebaa7f3c50
commit a003f1192e
15 changed files with 1280 additions and 234 deletions
+51 -10
View File
@@ -9,7 +9,8 @@ import { Toast } from '@/components/ui/toast';
import { sanitizeInput } from '@/lib/sanitize';
import { generateCSRFToken, setCSRFTokenToStorage, getCSRFTokenFromStorage } from '@/lib/csrf';
import { generateCaptcha } from '@/lib/security/captcha';
import { Mail, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2, RefreshCw } from 'lucide-react';
import { useFormAutosave } from '@/hooks/use-form-autosave';
import { Mail, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2, RefreshCw, Save } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
const contactFormSchema = z.object({
@@ -36,17 +37,29 @@ export function ContactSection() {
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [toastType, setToastType] = useState<'success' | 'error'>('success');
const [formData, setFormData] = useState<ContactFormData>({
name: '',
phone: '',
email: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [captcha, setCaptcha] = useState(generateCaptcha('simple'));
const [captchaAnswer, setCaptchaAnswer] = useState('');
const sectionRef = useRef<HTMLElement>(null);
// 使用表单自动保存功能
const {
data: formData,
updateData,
lastSaved,
isRestored,
clearSavedData,
} = useFormAutosave<ContactFormData>({
key: 'contact_form',
initialData: {
name: '',
phone: '',
email: '',
message: '',
},
debounceMs: 1000,
});
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
@@ -83,7 +96,7 @@ export function ContactSection() {
const handleChange = (field: keyof ContactFormData, value: string) => {
const sanitizedValue = sanitizeInput(value);
setFormData((prev) => ({ ...prev, [field]: sanitizedValue }));
updateData({ [field]: sanitizedValue });
if (errors[field]) {
validateField(field, sanitizedValue);
}
@@ -163,6 +176,7 @@ export function ContactSection() {
setIsSubmitting(false);
setIsSubmitted(true);
clearSavedData(); // 提交成功后清除保存的数据
setToastMessage('表单提交成功!我们会尽快与您联系。');
setToastType('success');
setShowToast(true);
@@ -277,7 +291,7 @@ export function ContactSection() {
</div>
</div>
<div
<div
className={`
lg:col-span-3 flex flex-col
opacity-0 translate-y-4
@@ -285,7 +299,34 @@ export function ContactSection() {
`}
>
<div className="bg-[#F5F7FA] p-6 sm:p-8 rounded-lg border border-[#E2E8F0] flex-1 flex flex-col">
<h3 className="text-lg font-semibold text-[#1A1A2E] mb-6"></h3>
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-[#1A1A2E]"></h3>
{/* 自动保存状态指示器 */}
<div className="flex items-center gap-2 text-sm text-[#595959]">
{lastSaved && (
<>
<Save className="w-4 h-4" />
<span> {lastSaved.toLocaleTimeString()}</span>
</>
)}
</div>
</div>
{/* 数据恢复提示 */}
{isRestored && (
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-between">
<span className="text-sm text-blue-700">
</span>
<button
type="button"
onClick={clearSavedData}
className="text-sm text-blue-600 hover:text-blue-800 underline"
>
</button>
</div>
)}
{isSubmitted ? (
<div className="text-center py-12 flex-1 flex items-center justify-center" data-testid="success-message">