feat: 重构联系页面并增强安全性

refactor: 优化导航和路由逻辑

fix: 修复移动端样式问题

perf: 优化字体加载和性能

test: 添加安全性和可访问性测试

style: 调整按钮和表单样式

chore: 更新依赖版本

ci: 添加安全头配置

build: 优化构建配置

docs: 更新常量信息
This commit is contained in:
张翔
2026-03-01 10:56:54 +08:00
parent 13c4a2ca49
commit 9cbc80742a
24 changed files with 1087 additions and 440 deletions
+1 -1
View File
@@ -270,7 +270,7 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
className="w-full bg-white text-[#C41E3A] hover:bg-white/90"
asChild
>
<Link href="/#contact">
<Link href="/contact">
</Link>
</Button>
+2 -2
View File
@@ -101,7 +101,7 @@ export default function CasesPage() {
</p>
<div className="flex justify-center gap-4">
<Link href="/#contact">
<Link href="/contact">
<Button
size="lg"
variant="outline"
@@ -109,7 +109,7 @@ export default function CasesPage() {
</Button>
</Link>
<Link href="/#contact">
<Link href="/contact">
<Button
size="lg"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white"
+297 -257
View File
@@ -1,80 +1,109 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { useInView } from 'framer-motion';
import { motion } from 'framer-motion';
import { COMPANY_INFO } from '@/lib/constants';
import { useState, useEffect, useRef } from 'react';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PageHeader } from '@/components/ui/page-header';
import { Mail, Phone, MapPin, Send, Loader2 } from 'lucide-react';
import { Toast } from '@/components/ui/toast';
import { sanitizeInput } from '@/lib/sanitize';
import { generateCSRFToken, setCSRFTokenToStorage, getCSRFTokenFromStorage } from '@/lib/csrf';
import { Mail, Phone, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2 } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
const contactFormSchema = z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码'),
email: z.string().email('请输入有效的邮箱地址'),
subject: z.string().min(2, '主题至少需要2个字符'),
message: z.string().min(10, '留言内容至少需要10个字符'),
});
type ContactFormData = z.infer<typeof contactFormSchema>;
interface FormErrors {
name?: string;
phone?: string;
email?: string;
subject?: string;
message?: string;
}
export default function ContactPage() {
const [isVisible, setIsVisible] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitResult, setSubmitResult] = useState<{ success: boolean; message?: string; error?: string } | null>(null);
const [mathAnswer, setMathAnswer] = useState('');
const [mathProblem, setMathProblem] = useState({ num1: 0, num2: 0, hash: '', timestamp: 0 });
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const isSubmitted = submitResult?.success || false;
const [isSubmitted, setIsSubmitted] = useState(false);
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [toastType, setToastType] = useState<'success' | 'error'>('success');
const [csrfToken, setCsrfToken] = useState<string>('');
const [formData, setFormData] = useState<ContactFormData>({
name: '',
phone: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
const num1 = Math.floor(Math.random() * 10) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
const answer = num1 + num2;
const timestamp = Date.now();
const hash = btoa(`${answer}-${timestamp}`);
setMathProblem({ num1, num2, hash, timestamp });
setIsVisible(true);
const token = generateCSRFToken();
setCsrfToken(token);
setCSRFTokenToStorage(token);
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const validateField = (field: keyof ContactFormData, value: string) => {
try {
contactFormSchema.shape[field].parse(value);
setErrors((prev) => ({ ...prev, [field]: undefined }));
} catch (error) {
if (error instanceof z.ZodError) {
const fieldError = error.issues[0];
if (fieldError) {
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
}
}
}
};
const handleChange = (field: keyof ContactFormData, value: string) => {
const sanitizedValue = sanitizeInput(value);
setFormData((prev) => ({ ...prev, [field]: sanitizedValue }));
if (errors[field]) {
validateField(field, sanitizedValue);
}
};
const handleBlur = (field: keyof ContactFormData, value: string) => {
validateField(field, value);
};
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
console.log('Form submission started');
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData);
const honeypot = formData.get('website') as string;
if (honeypot) {
console.log('Honeypot triggered');
if (!csrfToken) {
setToastMessage('安全验证失败,请刷新页面重试。');
setToastType('error');
setShowToast(true);
return;
}
const userAnswer = parseInt(formData.get('mathAnswer') as string);
if (isNaN(userAnswer)) {
setSubmitResult({ success: false, error: '请输入验证码' });
const result = contactFormSchema.safeParse(formData);
if (!result.success) {
const fieldErrors: FormErrors = {};
result.error.issues.forEach((issue) => {
const field = issue.path[0] as keyof ContactFormData;
fieldErrors[field] = issue.message;
});
setErrors(fieldErrors);
return;
}
const expectedHash = btoa(`${userAnswer}-${mathProblem.timestamp}`);
if (expectedHash !== mathProblem.hash) {
setSubmitResult({ success: false, error: '验证码错误,请重新计算' });
return;
}
const submitTime = formData.get('submitTime') as string;
const timeDiff = Date.now() - parseInt(submitTime);
if (timeDiff < 2000) {
console.log('Too fast submission');
setSubmitResult({ success: false, error: '提交过快,请稍后再试' });
return;
}
setIsSubmitting(true);
setSubmitResult(null);
const submitData = {
...data,
mathHash: mathProblem.hash,
mathTimestamp: mathProblem.timestamp,
mathAnswer: userAnswer,
submitTime: submitTime
};
console.log('FormData:', submitData);
try {
const response = await fetch('/api/contact', {
@@ -82,218 +111,229 @@ export default function ContactPage() {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submitData),
body: JSON.stringify({
...formData,
csrfToken: csrfToken,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '提交失败');
}
const newToken = generateCSRFToken();
setCsrfToken(newToken);
setCSRFTokenToStorage(newToken);
console.log('Response status:', response.status);
const result = await response.json();
console.log('Response result:', result);
console.log('Setting submitResult:', result);
setSubmitResult(result);
} catch (error) {
console.error('Form submission error:', error);
setSubmitResult({ success: false, error: '提交失败,请重试' });
} finally {
setIsSubmitting(false);
setIsSubmitted(true);
setToastMessage('表单提交成功!我们会尽快与您联系。');
setToastType('success');
setShowToast(true);
} catch (error) {
setIsSubmitting(false);
setToastMessage(error instanceof Error ? error.message : '提交失败,请稍后重试。');
setToastType('error');
setShowToast(true);
}
};
}
return (
<div className="min-h-screen bg-white">
<PageHeader
badge="联系我们"
title="与我们取得联系"
description="无论您有任何问题或合作意向,我们都很乐意与您交流"
/>
<main className="min-h-screen bg-white pt-16">
{showToast && (
<Toast
message={toastMessage}
type={toastType}
onClose={() => setShowToast(false)}
/>
)}
<section className="section-padding relative overflow-hidden" ref={sectionRef}>
<div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-0 bg-gradient-radial from-[rgba(79,70,229,0.03)] via-transparent to-transparent" />
</div>
<div className="container-wide relative z-10 py-16" ref={contentRef}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="grid grid-cols-1 lg:grid-cols-2 gap-12 max-w-6xl mx-auto"
>
{/* Contact Info */}
<div className="space-y-8">
<Card className="border-[#E5E5E5]">
<CardHeader>
<CardTitle className="text-[#1C1C1C]"></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-[#C41E3A] rounded-lg flex items-center justify-center flex-shrink-0">
<MapPin className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="font-semibold text-[#1C1C1C]"></h3>
<p className="text-[#5C5C5C]">{COMPANY_INFO.address}</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-[#C41E3A] rounded-lg flex items-center justify-center flex-shrink-0">
<Phone className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="font-semibold text-[#1C1C1C]"></h3>
<p className="text-[#5C5C5C]">{COMPANY_INFO.phone}</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-[#C41E3A] rounded-lg flex items-center justify-center flex-shrink-0">
<Mail className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="font-semibold text-[#1C1C1C]"></h3>
<p className="text-[#5C5C5C]">{COMPANY_INFO.email}</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border-[#E5E5E5]">
<CardHeader>
<CardTitle className="text-[#1C1C1C]"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-[#5C5C5C]"></span>
<span className="text-[#1C1C1C] font-medium">9:00 - 18:00</span>
</div>
<div className="flex justify-between">
<span className="text-[#5C5C5C]"></span>
<span className="text-[#1C1C1C] font-medium">9:00 - 12:00</span>
</div>
<div className="flex justify-between">
<span className="text-[#5C5C5C]"></span>
<span className="text-[#8C8C8C]"></span>
</div>
</div>
</CardContent>
</Card>
<div className="container-wide relative z-10">
<div
className={`
mb-16 opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up' : ''}
`}
>
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-px bg-gradient-to-r from-[#1C1C1C] to-[#C41E3A]" />
<span className="text-sm text-[#5C5C5C] tracking-wide"></span>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
<span className="text-[#C41E3A]"></span>
</h1>
<p className="mt-4 text-[#5C5C5C] max-w-2xl">
</p>
</div>
{/* Contact Form */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
>
<Card className="border-[#E5E5E5]">
<CardHeader>
<CardTitle className="text-[#1C1C1C]"></CardTitle>
</CardHeader>
<CardContent>
{isSubmitted ? (
<div className="text-center py-12">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Send className="w-8 h-8 text-green-600" />
<div className="grid lg:grid-cols-5 gap-12 lg:gap-16">
<div
className={`
lg:col-span-2 space-y-8 flex flex-col
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up stagger-1' : ''}
`}
>
<div>
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-6"></h3>
<div className="space-y-4">
<div className="flex items-start gap-4 group">
<div className="w-10 h-10 bg-[#C41E3A] rounded-md flex items-center justify-center flex-shrink-0 transition-transform duration-200 group-hover:scale-105">
<Mail className="w-5 h-5 text-white" />
</div>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-2"></h3>
<p className="text-[#5C5C5C]"></p>
<div>
<p className="text-sm text-[#5C5C5C] mb-1"></p>
<a href={`mailto:${COMPANY_INFO.email}`} className="text-[#1C1C1C] hover:text-[#C41E3A] transition-colors duration-200">
{COMPANY_INFO.email}
</a>
</div>
</div>
<div className="flex items-start gap-4 group">
<div className="w-10 h-10 bg-[#C41E3A] rounded-md flex items-center justify-center flex-shrink-0 transition-transform duration-200 group-hover:scale-105">
<Phone className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-sm text-[#5C5C5C] mb-1"></p>
<a href={`tel:${COMPANY_INFO.phone}`} className="text-[#1C1C1C] hover:text-[#C41E3A] transition-colors duration-200">
{COMPANY_INFO.phone}
</a>
</div>
</div>
<div className="flex items-start gap-4 group">
<div className="w-10 h-10 bg-[#C41E3A] rounded-md flex items-center justify-center flex-shrink-0 transition-transform duration-200 group-hover:scale-105">
<MapPin className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-sm text-[#5C5C5C] mb-1"></p>
<p className="text-[#1C1C1C]">{COMPANY_INFO.address}</p>
</div>
</div>
</div>
</div>
<div className="bg-[#FFFBF5] p-5 rounded-lg border border-[#E5E5E5]">
<div className="flex items-center gap-2 mb-3">
<Clock className="w-4 h-4 text-[#C41E3A]" />
<h4 className="text-sm font-medium text-[#1C1C1C]"></h4>
</div>
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-[#5C5C5C]"></span>
<span className="text-[#C41E3A]">9:00 - 18:00</span>
</div>
</div>
</div>
<div className="bg-[#FFFBF5] p-5 rounded-lg border border-[#E5E5E5]">
<div className="flex items-center gap-2 mb-3">
<HeadphonesIcon className="w-4 h-4 text-[#C41E3A]" />
<h4 className="text-sm font-medium text-[#1C1C1C]"></h4>
</div>
<div className="space-y-3">
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[#C41E3A] rounded-full mt-2 flex-shrink-0" />
<p className="text-sm text-[#5C5C5C]"> 2 </p>
</div>
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[#C41E3A] rounded-full mt-2 flex-shrink-0" />
<p className="text-sm text-[#5C5C5C]"></p>
</div>
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[#C41E3A] rounded-full mt-2 flex-shrink-0" />
<p className="text-sm text-[#5C5C5C]"></p>
</div>
</div>
</div>
</div>
<div
className={`
lg:col-span-3 flex flex-col
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up stagger-2' : ''}
`}
>
<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>
{isSubmitted ? (
<div className="text-center py-12 flex-1 flex items-center justify-center">
<div className="w-16 h-16 bg-[#C41E3A] rounded-full flex items-center justify-center mx-auto mb-4 animate-stamp-in">
<CheckCircle2 className="w-8 h-8 text-white" />
</div>
<h4 className="text-xl font-semibold text-[#1A1A2E] mb-2"></h4>
<p className="text-[#718096]"></p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
{submitResult && !submitResult.success && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
{submitResult.error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5 flex-1 flex flex-col">
<input type="hidden" name="_csrf" value={csrfToken} />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-[#1C1C1C] mb-2">
*
</label>
<Input
id="name"
name="name"
placeholder="请输入您的姓名"
required
/>
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-[#1C1C1C] mb-2">
</label>
<Input
id="phone"
name="phone"
type="tel"
placeholder="请输入您的电话"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-[#1C1C1C] mb-2">
*
</label>
<Input
id="email"
name="email"
type="email"
placeholder="请输入您的邮箱"
required
<Input
label="姓名"
id="name"
placeholder="请输入您的姓名"
required
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={(e) => handleBlur('name', e.target.value)}
error={errors.name}
/>
<Input
label="电话"
id="phone"
type="tel"
placeholder="请输入您的电话"
required
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
onBlur={(e) => handleBlur('phone', e.target.value)}
error={errors.phone}
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-[#1C1C1C] mb-2">
*
</label>
<Input
id="subject"
name="subject"
placeholder="请输入消息主题"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-[#1C1C1C] mb-2">
*
</label>
<Textarea
id="message"
name="message"
placeholder="请输入您想要咨询的内容"
rows={5}
required
/>
</div>
<div className="space-y-3">
<label htmlFor="mathAnswer" className="block text-sm font-medium text-[#1C1C1C] mb-2">
*
</label>
<div className="flex items-center gap-4">
<div className="bg-[#f9f9f9] px-4 py-2 rounded-lg text-[#1C1C1C] font-medium min-w-[120px] text-center">
{mathProblem.num1} + {mathProblem.num2} = ?
</div>
<Input
id="mathAnswer"
name="mathAnswer"
type="number"
placeholder="请输入答案"
value={mathAnswer}
onChange={(e) => setMathAnswer(e.target.value)}
required
className="flex-1"
/>
</div>
</div>
<input
type="hidden"
name="website"
value=""
<Input
label="邮箱"
id="email"
type="email"
placeholder="请输入您的邮箱"
required
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={(e) => handleBlur('email', e.target.value)}
error={errors.email}
/>
<input
type="hidden"
name="submitTime"
value={Date.now()}
<Input
label="主题"
id="subject"
placeholder="请输入消息主题"
required
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
onBlur={(e) => handleBlur('subject', e.target.value)}
error={errors.subject}
/>
<Textarea
label="留言内容"
id="message"
placeholder="请输入您想咨询的内容"
rows={5}
required
value={formData.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={(e) => handleBlur('message', e.target.value)}
error={errors.message}
/>
<Button
type="submit"
className="w-full bg-[#C41E3A] hover:bg-[#A01830] text-white"
size="lg"
className="w-full group mt-auto min-h-[52px] md:min-h-0"
disabled={isSubmitting}
>
{isSubmitting ? (
@@ -303,18 +343,18 @@ export default function ContactPage() {
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
<Send className="mr-2 h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</>
)}
</Button>
</form>
)}
</CardContent>
</Card>
</motion.div>
</motion.div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
);
}
@@ -99,7 +99,7 @@ export function NewsDetailClient({ news }: NewsDetailClientProps) {
</Button>
</Link>
<Link href="/#contact">
<Link href="/contact">
<Button size="lg" className="bg-[#C41E3A] hover:bg-[#A01830] text-white">
</Button>
+28 -10
View File
@@ -1,5 +1,7 @@
"use client";
import { Suspense, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import dynamic from 'next/dynamic';
import { HeroSection } from "@/components/sections/hero-section";
import { SectionSkeleton } from "@/components/ui/loading-skeleton";
@@ -44,15 +46,24 @@ const NewsSection = dynamic(
}
);
const ContactSection = dynamic(
() => import('@/components/sections/contact-section').then(mod => ({ default: mod.ContactSection })),
{
loading: () => <SectionSkeleton />,
ssr: false
}
);
export default function HomePage() {
function HomeContent() {
const searchParams = useSearchParams();
useEffect(() => {
const section = searchParams.get('section');
if (section) {
const timer = setTimeout(() => {
const targetElement = document.getElementById(section);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
return () => clearTimeout(timer);
}
return undefined;
}, [searchParams]);
return (
<main className="min-h-screen bg-white dark:bg-[var(--color-bg-primary)]">
<HeroSection />
@@ -61,7 +72,14 @@ export default function HomePage() {
<CasesSection />
<AboutSection />
<NewsSection />
<ContactSection />
</main>
);
}
export default function HomePage() {
return (
<Suspense fallback={<SectionSkeleton />}>
<HomeContent />
</Suspense>
);
}
+2 -2
View File
@@ -211,12 +211,12 @@ export default async function ProductDetailPage({ params }: { params: Promise<{
<div className="flex justify-center gap-4 pt-8 border-t border-[#E5E5E5]">
<Button variant="outline" size="lg" asChild>
<Link href="/#contact">
<Link href="/contact">
</Link>
</Button>
<Button size="lg" className="bg-[#C41E3A] hover:bg-[#A01830] text-white" asChild>
<Link href="/#contact">
<Link href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</Link>
+8 -15
View File
@@ -3,6 +3,7 @@
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/ui/page-header';
import { ArrowRight, Lightbulb, Cpu, Users, CheckCircle2 } from 'lucide-react';
@@ -252,27 +253,19 @@ export default function SolutionsPage() {
<Button
size="lg"
variant="outline"
onClick={() => {
const element = document.getElementById('contact');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}}
asChild
>
<Link href="/contact"></Link>
</Button>
<Button
size="lg"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white"
onClick={() => {
const element = document.getElementById('contact');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}}
asChild
>
<ArrowRight className="ml-2 w-4 h-4" />
<Link href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</Link>
</Button>
</div>
</div>
+66
View File
@@ -121,6 +121,7 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
@media (min-width: 640px) {
@@ -142,6 +143,8 @@
line-height: var(--line-height-normal);
letter-spacing: var(--letter-spacing-normal);
position: relative;
overflow-x: hidden;
width: 100%;
}
body::before {
@@ -1117,4 +1120,67 @@ body {
body {
padding-bottom: 64px;
}
/* 防止移动端内容溢出 */
.container-wide,
.container-full,
.container-narrow {
padding-left: 1rem;
padding-right: 1rem;
max-width: 100%;
overflow-x: hidden;
}
/* 优化移动端文字大小 */
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.75rem;
}
h3 {
font-size: 1.375rem;
}
/* 优化移动端按钮和链接的触摸目标 */
a, button {
min-height: 44px;
min-width: 44px;
}
/* 防止长文本溢出 */
p, li, span {
overflow-wrap: break-word;
word-wrap: break-word;
}
}
/* 平板端优化 (768px - 1023px) */
@media (min-width: 768px) and (max-width: 1023px) {
.container-wide,
.container-full {
padding-left: 2rem;
padding-right: 2rem;
}
/* 平板端文字大小调整 */
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.5rem;
}
/* 平板端section间距 */
.section-padding {
padding-top: 4rem;
padding-bottom: 4rem;
}
}
+4 -12
View File
@@ -10,14 +10,14 @@ import { ErrorBoundary } from "@/components/ui/error-boundary";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
display: "optional",
display: "swap",
preload: false,
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
display: "optional",
display: "swap",
preload: false,
});
@@ -33,7 +33,7 @@ const maShanZheng = Ma_Shan_Zheng({
weight: "400",
variable: "--font-ma-shan-zheng",
subsets: ["latin"],
display: "optional",
display: "swap",
preload: false,
});
@@ -42,7 +42,7 @@ const longCang = Long_Cang({
variable: "--font-long-cang",
subsets: ["latin"],
display: "swap",
preload: true,
preload: false,
});
export const metadata: Metadata = {
@@ -116,14 +116,6 @@ export default function RootLayout({
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/favicon.svg" />
{/* 预加载龙藏体,确保与 Logo 一致 */}
<link
rel="preload"
href="https://fonts.gstatic.com/s/longcang/v21/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.0.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<OrganizationSchema />
<WebsiteSchema />
<script