Prechádzať zdrojové kódy

feat: 统一全局状态管理和UI优化

- 使用 React Context 重写 useGlobalSettings,实现主题/语言跨页面同步
- 添加 ScrollToTop 组件,页面跳转时自动滚动到顶部
- 统一所有页面使用 useEmailSubmission hook 处理邮件提交
- 修复 Step2 亮主题下的 UI 问题,支持亮/暗主题切换
- 添加 StepProgress 组件的 i18n 翻译(nav.consulting, nav.tourism, nav.dev)
- 添加缺失的 themeClasses(marker, codeBlock, shadow)
- 修复 CSS 导入路径和未使用变量问题
- 添加字体优化、无障碍性和响应式样式文件
bob 4 mesiacov pred
rodič
commit
81d55da321

+ 20 - 14
src/App.tsx

@@ -5,26 +5,32 @@ import SimpleHome from './pages/SimpleHome';
 import Step1 from './pages/Step1';
 import Step2 from './pages/Step2';
 import Step3 from './pages/Step3';
+import ScrollToTop from './components/ScrollToTop';
+import { GlobalSettingsProvider } from './hooks/useGlobalSettings';
 
 function App() {
     return (
-        <Router>
-            <Routes>
-                {/* New Homepage: SimpleHome */}
-                <Route path="/" element={<SimpleHome />} />
+        <GlobalSettingsProvider>
+            <Router>
+                {/* 路由变化时自动滚动到顶部 */}
+                <ScrollToTop />
+                <Routes>
+                    {/* New Homepage: SimpleHome */}
+                    <Route path="/" element={<SimpleHome />} />
 
-                {/* Step 1 & Step 2 Pages */}
-                <Route path="/step1" element={<Step1 />} />
-                <Route path="/step2" element={<Step2 />} />
-                <Route path="/step3" element={<Step3 />} />
+                    {/* Step 1 & Step 2 Pages */}
+                    <Route path="/step1" element={<Step1 />} />
+                    <Route path="/step2" element={<Step2 />} />
+                    <Route path="/step3" element={<Step3 />} />
 
-                {/* Letter to Players (Formerly the homepage) */}
-                <Route path="/intro" element={<NewHome />} />
+                    {/* Letter to Players (Formerly the homepage) */}
+                    <Route path="/intro" element={<NewHome />} />
 
-                {/* Letter to letters (Formerly /letter) */}
-                <Route path="/letters" element={<Letters />} />
-            </Routes>
-        </Router>
+                    {/* Letter to letters (Formerly /letter) */}
+                    <Route path="/letters" element={<Letters />} />
+                </Routes>
+            </Router>
+        </GlobalSettingsProvider>
     );
 }
 

+ 23 - 0
src/components/ScrollToTop.tsx

@@ -0,0 +1,23 @@
+import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+
+/**
+ * 滚动到顶部组件
+ * 在路由变化时自动将页面滚动到顶部
+ */
+const ScrollToTop = () => {
+    const { pathname } = useLocation();
+
+    useEffect(() => {
+        // 路由变化时滚动到页面顶部
+        window.scrollTo({
+            top: 0,
+            left: 0,
+            behavior: 'instant' // 使用 'instant' 避免滚动动画干扰页面切换
+        });
+    }, [pathname]);
+
+    return null;
+};
+
+export default ScrollToTop;

+ 105 - 0
src/hooks/useEmailSubmission.ts

@@ -0,0 +1,105 @@
+import { useState } from 'react';
+import { apiService, type EmailSubmissionData, type ApiResponse } from '../services/api';
+
+export const useEmailSubmission = () => {
+    const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
+    const [message, setMessage] = useState<string>('');
+    const [lastResponse, setLastResponse] = useState<ApiResponse | null>(null);
+
+    /**
+     * 提交邮件
+     */
+    const submitEmail = async (data: EmailSubmissionData): Promise<ApiResponse> => {
+        if (!data.email) {
+            const errorMessage = '请输入有效的邮箱地址';
+            setStatus('error');
+            setMessage(errorMessage);
+            return { success: false, message: errorMessage };
+        }
+
+        // 基本邮箱验证
+        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+        if (!emailRegex.test(data.email)) {
+            const errorMessage = '请输入有效的邮箱地址';
+            setStatus('error');
+            setMessage(errorMessage);
+            return { success: false, message: errorMessage };
+        }
+
+        setStatus('loading');
+        setMessage('');
+
+        try {
+            const response = await apiService.sendWelcomeEmail(data);
+
+            setLastResponse(response);
+
+            if (response.success) {
+                setStatus('success');
+                setMessage(response.message);
+
+                // 如果提供了mailto链接,在新窗口打开(可选)
+                if (response.data?.mailtoLink && import.meta.env.PROD) {
+                    setTimeout(() => {
+                        window.open(response.data.mailtoLink, '_blank');
+                    }, 1000);
+                }
+            } else {
+                setStatus('error');
+                setMessage(response.message);
+            }
+
+            return response;
+        } catch (error) {
+            const errorMessage = error instanceof Error ? error.message : '发送失败,请稍后重试';
+            setStatus('error');
+            setMessage(errorMessage);
+
+            const errorResponse: ApiResponse = {
+                success: false,
+                message: errorMessage
+            };
+            setLastResponse(errorResponse);
+
+            return errorResponse;
+        }
+    };
+
+    /**
+     * 重置状态
+     */
+    const reset = () => {
+        setStatus('idle');
+        setMessage('');
+        setLastResponse(null);
+    };
+
+    /**
+     * 检查是否有本地存储的待发送邮件
+     */
+    const hasPendingSubmissions = (): boolean => {
+        const submissions = apiService.getLocalSubmissions();
+        return submissions.some((sub: any) => sub.status === 'pending');
+    };
+
+    /**
+     * 尝试重新发送待发送的邮件
+     */
+    const retryPendingSubmissions = async (): Promise<ApiResponse[]> => {
+        return await apiService.retryPendingSubmissions();
+    };
+
+    return {
+        status,
+        message,
+        lastResponse,
+        submitEmail,
+        reset,
+        hasPendingSubmissions,
+        retryPendingSubmissions,
+        isLoading: status === 'loading',
+        isSuccess: status === 'success',
+        isError: status === 'error',
+        isIdle: status === 'idle'
+    };
+};

+ 207 - 0
src/hooks/useGlobalSettings.tsx

@@ -0,0 +1,207 @@
+import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
+import i18n from '../i18n';
+
+// 安全的localStorage访问
+const safeGetLocalStorage = (key: string, defaultValue: any = null) => {
+  try {
+    if (typeof window !== 'undefined' && window.localStorage) {
+      return window.localStorage.getItem(key);
+    }
+  } catch (error) {
+    console.warn('localStorage access error:', error);
+  }
+  return defaultValue;
+};
+
+// 安全的localStorage设置
+const safeSetLocalStorage = (key: string, value: string) => {
+  try {
+    if (typeof window !== 'undefined' && window.localStorage) {
+      window.localStorage.setItem(key, value);
+    }
+  } catch (error) {
+    console.warn('localStorage set error:', error);
+  }
+};
+
+// 安全的系统主题检测
+const getSystemTheme = () => {
+  try {
+    if (typeof window !== 'undefined' && window.matchMedia) {
+      return window.matchMedia('(prefers-color-scheme: dark)').matches;
+    }
+  } catch (error) {
+    console.warn('System theme detection error:', error);
+  }
+  return false; // 默认浅色主题
+};
+
+// 主题类型定义
+interface ThemeClasses {
+  bg: string;
+  text: string;
+  textMuted: string;
+  textLight: string;
+  border: string;
+  borderStrong: string;
+  bgSecondary: string;
+  cardBg: string;
+  cardHover: string;
+  inputBorder: string;
+  buttonPrimary: string;
+  highlight: string;
+  navBg: string;
+  navBorder: string;
+  marker: string;
+  codeBlock: string;
+  shadow: string;
+}
+
+// Context 类型定义
+interface GlobalSettingsContextType {
+  isDark: boolean;
+  currentLang: 'zh' | 'en';
+  toggleTheme: () => void;
+  toggleLang: () => void;
+  setTheme: (dark: boolean) => void;
+  setLanguage: (lang: 'zh' | 'en') => void;
+  themeClasses: ThemeClasses;
+}
+
+// 创建 Context
+const GlobalSettingsContext = createContext<GlobalSettingsContextType | null>(null);
+
+// 生成主题类
+const getThemeClasses = (isDark: boolean): ThemeClasses => ({
+  bg: isDark ? 'bg-stone-900' : 'bg-[#FAFAFA]',
+  text: isDark ? 'text-stone-100' : 'text-stone-800',
+  textMuted: isDark ? 'text-stone-400' : 'text-stone-500',
+  textLight: isDark ? 'text-stone-500' : 'text-stone-400',
+  border: isDark ? 'border-stone-800' : 'border-stone-200',
+  borderStrong: isDark ? 'border-stone-700' : 'border-stone-300',
+  bgSecondary: isDark ? 'bg-stone-800' : 'bg-white',
+  cardBg: isDark ? 'bg-stone-800/50' : 'bg-white',
+  cardHover: isDark ? 'hover:bg-stone-800' : 'hover:bg-stone-50',
+  inputBorder: isDark ? 'border-stone-700 focus:border-stone-500' : 'border-stone-300 focus:border-stone-900',
+  buttonPrimary: isDark ? 'bg-stone-100 text-stone-900 hover:bg-white' : 'bg-stone-900 text-white hover:bg-stone-800',
+  highlight: isDark ? 'text-stone-100' : 'text-stone-900',
+  navBg: isDark ? 'bg-stone-900/90' : 'bg-white/90',
+  navBorder: isDark ? 'border-stone-800' : 'border-stone-100',
+  marker: isDark ? 'bg-stone-100' : 'bg-yellow-200',
+  codeBlock: isDark ? 'bg-stone-800 border-stone-700 text-stone-300' : 'bg-slate-50 border-slate-200 text-slate-700',
+  shadow: isDark ? 'shadow-none' : 'shadow-sm'
+});
+
+// Provider 组件
+export const GlobalSettingsProvider = ({ children }: { children: ReactNode }) => {
+  // 主题状态管理
+  const getInitialTheme = () => {
+    const saved = safeGetLocalStorage('theme');
+    if (saved) return saved === 'dark';
+    return getSystemTheme();
+  };
+
+  const [isDark, setIsDark] = useState(getInitialTheme);
+
+  // 语言状态管理
+  const getInitialLanguage = (): 'zh' | 'en' => {
+    const saved = safeGetLocalStorage('i18nextLng') || i18n.language;
+    return saved?.startsWith('zh') ? 'zh' : 'en';
+  };
+
+  const [currentLang, setCurrentLang] = useState<'zh' | 'en'>(getInitialLanguage);
+
+  // 同步主题到localStorage和document
+  useEffect(() => {
+    safeSetLocalStorage('theme', isDark ? 'dark' : 'light');
+    try {
+      if (typeof window !== 'undefined' && document.documentElement) {
+        document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
+      }
+    } catch (error) {
+      console.warn('Document theme setting error:', error);
+    }
+  }, [isDark]);
+
+  // 同步语言到i18n
+  useEffect(() => {
+    if (i18n.language !== currentLang) {
+      i18n.changeLanguage(currentLang);
+    }
+  }, [currentLang]);
+
+  // 监听系统主题变化
+  useEffect(() => {
+    try {
+      if (typeof window !== 'undefined' && window.matchMedia) {
+        const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+        const handleChange = (e: MediaQueryListEvent) => {
+          if (!safeGetLocalStorage('theme')) {
+            setIsDark(e.matches);
+          }
+        };
+
+        mediaQuery.addEventListener('change', handleChange);
+        return () => mediaQuery.removeEventListener('change', handleChange);
+      }
+    } catch (error) {
+      console.warn('System theme listener error:', error);
+    }
+  }, []);
+
+  // 监听i18n语言变化
+  useEffect(() => {
+    const handleLanguageChanged = (lng: string) => {
+      const newLang = lng?.startsWith('zh') ? 'zh' : 'en';
+      if (newLang !== currentLang) {
+        setCurrentLang(newLang);
+      }
+    };
+
+    i18n.on('languageChanged', handleLanguageChanged);
+    return () => {
+      i18n.off('languageChanged', handleLanguageChanged);
+    };
+  }, [currentLang]);
+
+  const toggleTheme = () => {
+    setIsDark(prev => !prev);
+  };
+
+  const toggleLang = () => {
+    setCurrentLang(prev => prev === 'zh' ? 'en' : 'zh');
+  };
+
+  const setTheme = (dark: boolean) => {
+    setIsDark(dark);
+  };
+
+  const setLanguage = (lang: 'zh' | 'en') => {
+    setCurrentLang(lang);
+  };
+
+  const value: GlobalSettingsContextType = {
+    isDark,
+    currentLang,
+    toggleTheme,
+    toggleLang,
+    setTheme,
+    setLanguage,
+    themeClasses: getThemeClasses(isDark)
+  };
+
+  return (
+    <GlobalSettingsContext.Provider value= { value } >
+    { children }
+    </GlobalSettingsContext.Provider>
+  );
+};
+
+// Hook 用于访问全局设置
+export const useGlobalSettings = (): GlobalSettingsContextType => {
+  const context = useContext(GlobalSettingsContext);
+  if (!context) {
+    throw new Error('useGlobalSettings must be used within a GlobalSettingsProvider');
+  }
+  return context;
+};

+ 4 - 1
src/index.css

@@ -1 +1,4 @@
-@import 'tailwindcss';
+@import 'tailwindcss';
+@import './styles/fonts.css';
+@import './styles/accessibility.css';
+@import './styles/responsive.css';

+ 4 - 1
src/locales/en.json

@@ -4,7 +4,10 @@
         "services": "Services",
         "story": "Philosophy",
         "contact": "Contact",
-        "book": "Book Demo"
+        "book": "Book Demo",
+        "consulting": "AI Consulting",
+        "tourism": "Tourism Ops",
+        "dev": "Infrastructure"
     },
     "hero": {
         "badge": "Efficiency Experts in Financial Consulting",

+ 4 - 1
src/locales/zh.json

@@ -4,7 +4,10 @@
         "services": "核心服务",
         "story": "品牌故事",
         "contact": "联系我们",
-        "book": "预约咨询"
+        "book": "预约咨询",
+        "consulting": "AI 咨询",
+        "tourism": "文旅运营",
+        "dev": "基础设施"
     },
     "hero": {
         "badge": "金融咨询行业的效能专家",

+ 12 - 60
src/pages/SimpleHome.tsx

@@ -18,7 +18,8 @@ import {
     BrainCircuit
 } from 'lucide-react';
 import { useNavigate, useLocation } from 'react-router-dom';
-import i18n from '../i18n';
+import { useGlobalSettings } from '../hooks/useGlobalSettings';
+import { useEmailSubmission } from '../hooks/useEmailSubmission';
 
 // 多语言配置
 const translations = {
@@ -188,19 +189,12 @@ const translations = {
 
 const SimpleHome = () => {
     const [isMenuOpen, setIsMenuOpen] = useState(false);
-    // Default to Light Mode (isDark = false)
-    const [isDark, setIsDark] = useState(localStorage.getItem('theme') === 'dark');
 
-    // Sync theme on mount/change
-    useEffect(() => {
-        localStorage.setItem('theme', isDark ? 'dark' : 'light');
-    }, [isDark]);
-
-    const [lang, setLang] = useState<'zh' | 'en'>(i18n.language?.startsWith('zh') ? 'zh' : 'en');
+    // 使用统一的全局状态管理
+    const { isDark, currentLang: lang, toggleTheme, toggleLang } = useGlobalSettings();
 
     const [email, setEmail] = useState('');
-    const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
-    const [message, setMessage] = useState('');
+    const { submitEmail, status, message } = useEmailSubmission();
 
     const navigate = useNavigate();
     const location = useLocation();
@@ -218,17 +212,6 @@ const SimpleHome = () => {
         }
     }, [location]);
 
-    // Handle global language sync if we want consistency with other pages using i18n
-    // But SimpleHome seems to use local specific translations object `translations`.
-    // Ideally SimpleHome should also use `useTranslation` hook so it shares state with Step1/Step2.
-    // However, refactoring SimpleHome to use i18n completely is a big change (large file).
-    // For now, let's at least make the Lang Toggle use the i18n instance if possible
-    // OR just keep local lang state. The user asked to "perfect" Step1 and Step2.
-    // If I change Lang in Step 1 (via i18n) and come back to Home, Home might still be default (zh).
-
-    // Let's assume for now SimpleHome keeps its local state, but we try to initialize it from i18n if possible
-    // or localStorage language detector.
-
     const t = translations[lang];
 
     const scrollToSection = (id: string) => {
@@ -239,49 +222,18 @@ const SimpleHome = () => {
         }
     };
 
-    const toggleTheme = () => setIsDark(!isDark);
-    const toggleLang = () => {
-        const newLang = lang === 'zh' ? 'en' : 'zh';
-        setLang(newLang);
-        i18n.changeLanguage(newLang);
-    };
+    // toggleTheme 和 toggleLang 已从 useGlobalSettings 获取
 
     const handleEmailSubmit = async () => {
         if (!email) return;
 
-        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-        if (!emailRegex.test(email)) {
-            setStatus('error');
-            setMessage('请输入有效的邮箱地址');
-            return;
-        }
+        const result = await submitEmail({
+            email,
+            source: 'SimpleHome_Contact'
+        });
 
-        setStatus('loading');
-        setMessage('');
-
-        try {
-            const response = await fetch('/api/send-welcome-email', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({ email }),
-            });
-
-            const data = await response.json();
-
-            if (data.success) {
-                setStatus('success');
-                setMessage(data.message);
-                setEmail('');
-            } else {
-                setStatus('error');
-                setMessage(data.message || '发送失败,请稍后重试');
-            }
-        } catch (error) {
-            console.error('API Error:', error);
-            setStatus('error');
-            setMessage('网络错误,请稍后重试');
+        if (result.success) {
+            setEmail('');
         }
     };
 

+ 24 - 78
src/pages/Step1.tsx

@@ -11,23 +11,14 @@ import SharedHeader from '../components/SharedHeader';
 import StepProgress from '../components/StepProgress';
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
-import { useState, useEffect } from 'react';
+import { useState } from 'react';
+import { useGlobalSettings } from '../hooks/useGlobalSettings';
+import { useEmailSubmission } from '../hooks/useEmailSubmission';
 
 const Step1 = () => {
-    const { t, i18n } = useTranslation();
+    const { t } = useTranslation();
     const navigate = useNavigate();
-
-    // Theme state
-    const [isDark, setIsDark] = useState(localStorage.getItem('theme') === 'dark');
-
-    useEffect(() => {
-        // Sync theme class to document body if needed, or just handle locally
-        // For consistency with SimpleHome, we might want to store in localStorage
-        localStorage.setItem('theme', isDark ? 'dark' : 'light');
-    }, [isDark]);
-
-    const toggleTheme = () => setIsDark(!isDark);
-    const toggleLang = () => i18n.changeLanguage(i18n.language === 'zh' ? 'en' : 'zh');
+    const { isDark, currentLang, toggleTheme, toggleLang, themeClasses } = useGlobalSettings();
 
     const scrollToSection = (id: string) => {
         const element = document.getElementById(id);
@@ -36,16 +27,14 @@ const Step1 = () => {
         }
     };
 
-    const currentLang = i18n.language === 'zh' ? 'zh' : 'en';
-
     // Form State
     const [formData, setFormData] = useState({
         name: '',
         contact: '',
         industry: ''
     });
-    const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
-    const [message, setMessage] = useState('');
+    const { submitEmail, status, message } = useEmailSubmission();
+    const [localValidationError, setLocalValidationError] = useState<string>('');
 
     const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
         const { name, value } = e.target;
@@ -54,74 +43,31 @@ const Step1 = () => {
 
     const handleSubmit = async (e: React.FormEvent) => {
         e.preventDefault();
+        setLocalValidationError('');
 
         // Basic validation
         if (!formData.contact) {
-            setStatus('error');
-            setMessage(currentLang === 'zh' ? '请填写联系方式' : 'Please enter your contact info');
+            setLocalValidationError(currentLang === 'zh' ? '请填写联系方式' : 'Please enter your contact info');
             return;
         }
 
-        // Email validation (assuming contact is email for the API)
-        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-        if (!emailRegex.test(formData.contact)) {
-            setStatus('error');
-            setMessage(currentLang === 'zh' ? '请输入有效的邮箱地址' : 'Please enter a valid email');
-            return;
-        }
+        // 使用新的API服务提交数据
+        const payload = {
+            email: formData.contact,
+            name: formData.name,
+            industry: formData.industry,
+            source: 'Step1_Consulting'
+        };
 
-        setStatus('loading');
-        setMessage('');
-
-        try {
-            // We verify email format, and pass other fields as extra data
-            const payload = {
-                email: formData.contact,
-                name: formData.name,
-                industry: formData.industry,
-                source: 'Step1_Consulting'
-            };
-
-            const response = await fetch('/api/send-welcome-email', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify(payload),
-            });
-
-            const data = await response.json();
-
-            if (data.success) {
-                setStatus('success');
-                setMessage(data.message || (currentLang === 'zh' ? '提交成功!我们会尽快联系您。' : 'Submitted successfully! We will contact you soon.'));
-                setFormData({ name: '', contact: '', industry: '' });
-            } else {
-                setStatus('error');
-                setMessage(data.message || (currentLang === 'zh' ? '提交失败,请稍后重试。' : 'Submission failed. Please try again later.'));
-            }
-        } catch (error) {
-            setStatus('error');
-            setMessage(currentLang === 'zh' ? '网络错误,请稍后重试。' : 'Network error. Please try again later.');
+        const response = await submitEmail(payload);
+
+        // 如果提交成功,清空表单
+        if (response.success) {
+            setFormData({ name: '', contact: '', industry: '' });
         }
     };
 
-    // Theme utility classes
-    const themeClasses = {
-        bg: isDark ? 'bg-stone-900' : 'bg-white',
-        text: isDark ? 'text-stone-100' : 'text-slate-900',
-        textMuted: isDark ? 'text-stone-400' : 'text-slate-600',
-        textLight: isDark ? 'text-stone-500' : 'text-slate-400',
-        border: isDark ? 'border-stone-800' : 'border-slate-100',
-        borderStrong: isDark ? 'border-stone-700' : 'border-slate-200',
-        bgSecondary: isDark ? 'bg-stone-800' : 'bg-slate-50',
-        cardBg: isDark ? 'bg-stone-900' : 'bg-white',
-        cardHover: isDark ? 'hover:bg-stone-800' : 'hover:bg-white',
-        inputBorder: isDark ? 'border-stone-700 focus:border-stone-500' : 'border-slate-300 focus:border-black',
-        buttonPrimary: isDark ? 'bg-stone-100 text-stone-900 hover:bg-white' : 'bg-black text-white hover:bg-slate-800',
-        highlight: isDark ? 'text-stone-100' : 'text-black',
-        codeBlock: isDark ? 'bg-stone-800 border-stone-700 text-stone-400' : 'bg-slate-50 border-slate-100 text-slate-500',
-        marker: isDark ? 'bg-blue-900 text-blue-200' : 'bg-yellow-200',
-        shadow: isDark ? 'shadow-none' : 'shadow-sm',
-    };
+    // Theme classes are now provided by useGlobalSettings hook
 
     return (
         <div className={`min-h-screen font-sans selection:bg-black selection:text-white transition-colors duration-300 ${themeClasses.bg} ${themeClasses.text}`}>
@@ -431,9 +377,9 @@ const Step1 = () => {
                                 </select>
                             </div>
 
-                            {message && (
+                            {(message || localValidationError) && (
                                 <div className={`text-sm font-medium ${status === 'success' ? 'text-green-500' : 'text-red-500'}`}>
-                                    {message}
+                                    {localValidationError || message}
                                 </div>
                             )}
 

+ 22 - 24
src/pages/Step2.tsx

@@ -3,34 +3,32 @@ import SharedHeader from '../components/SharedHeader';
 import StepProgress from '../components/StepProgress';
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
-import { useState, useEffect } from 'react';
+import { useGlobalSettings } from '../hooks/useGlobalSettings';
 
 const Step2 = () => {
-    const { t, i18n } = useTranslation();
+    const { t } = useTranslation();
     const navigate = useNavigate();
 
-    // Theme state - Default to DARK if not specified, because Step 2's soul is cyber-dark
-    const savedTheme = localStorage.getItem('theme');
-    const [isDark, setIsDark] = useState(savedTheme ? savedTheme === 'dark' : true);
+    // 使用统一的状态管理
+    const { isDark, currentLang, toggleTheme, toggleLang } = useGlobalSettings();
 
-    useEffect(() => {
-        localStorage.setItem('theme', isDark ? 'dark' : 'light');
-    }, [isDark]);
-
-    const toggleTheme = () => setIsDark(!isDark);
-    const toggleLang = () => i18n.changeLanguage(i18n.language === 'zh' ? 'en' : 'zh');
-
-    const currentLang = i18n.language === 'zh' ? 'zh' : 'en';
-
-    // CSS Variables for Theming
-    const themeStyles = {
-        '--bg-color': isDark ? '#050505' : '#ffffff',
-        '--text-primary': isDark ? '#ffffff' : '#000000',
-        '--text-secondary': isDark ? '#888888' : '#666666',
-        '--accent-color': isDark ? '#ccff00' : '#000000', // Black accent in light mode for brutalist look
-        '--grid-line': isDark ? '#222222' : '#e5e5e5',
-        '--card-bg': isDark ? '#050505' : '#f5f5f5',
-        '--card-border': isDark ? '#333333' : '#e5e5e5',
+    // CSS Variables for Theming - 根据主题动态设置
+    const themeStyles = isDark ? {
+        '--bg-color': '#050505',
+        '--text-primary': '#ffffff',
+        '--text-secondary': '#888888',
+        '--accent-color': '#ccff00',
+        '--grid-line': '#222222',
+        '--card-bg': '#0a0a0a',
+        '--card-border': '#333333',
+    } as React.CSSProperties : {
+        '--bg-color': '#FAFAFA',
+        '--text-primary': '#1a1a1a',
+        '--text-secondary': '#666666',
+        '--accent-color': '#16a34a', // 使用绿色保持活力感
+        '--grid-line': '#e5e5e5',
+        '--card-bg': '#ffffff',
+        '--card-border': '#d4d4d4',
     } as React.CSSProperties;
 
     // Helper for scrolling
@@ -42,7 +40,7 @@ const Step2 = () => {
     };
 
     return (
-        <div style={themeStyles} className="step2-page min-h-screen flex flex-col selection:bg-[var(--accent-color)] selection:text-[var(--bg-color)] transition-colors duration-500">
+        <div style={themeStyles} className={`step2-page min-h-screen flex flex-col selection:bg-[var(--accent-color)] selection:text-[var(--bg-color)] transition-colors duration-500`}>
             {/* 顶部导航 */}
             <SharedHeader
                 theme={isDark ? 'dark' : 'light'}

+ 14 - 60
src/pages/Step3.tsx

@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import {
     Terminal,
@@ -13,34 +13,14 @@ import {
 import SharedHeader from '../components/SharedHeader';
 import './step3.css';
 import StepProgress from '../components/StepProgress';
+import { useGlobalSettings } from '../hooks/useGlobalSettings';
+import { useEmailSubmission } from '../hooks/useEmailSubmission';
 
 const Step3 = () => {
-    const { t, i18n } = useTranslation();
-    const currentLang = i18n.language === 'zh' ? 'zh' : 'en';
+    const { t } = useTranslation();
 
-    // Theme state - read from localStorage, default to dark
-    const [isDark, setIsDark] = useState(() => {
-        const saved = localStorage.getItem('theme');
-        return saved ? saved === 'dark' : true; // default dark for Step 3
-    });
-
-    useEffect(() => {
-        localStorage.setItem('theme', isDark ? 'dark' : 'light');
-        if (isDark) {
-            document.body.classList.add('step3-mode');
-            document.body.classList.remove('step3-light');
-        } else {
-            document.body.classList.remove('step3-mode');
-            document.body.classList.add('step3-light');
-        }
-        return () => {
-            document.body.classList.remove('step3-mode');
-            document.body.classList.remove('step3-light');
-        };
-    }, [isDark]);
-
-    const toggleTheme = () => setIsDark(!isDark);
-    const toggleLang = () => i18n.changeLanguage(i18n.language === 'zh' ? 'en' : 'zh');
+    // 使用统一的全局状态管理
+    const { isDark, currentLang, toggleTheme, toggleLang } = useGlobalSettings();
 
     // Code Snippet State
     const [copied, setCopied] = useState(false);
@@ -77,46 +57,20 @@ const Step3 = () => {
         selection: isDark ? 'selection:bg-[#00f0ff] selection:text-black' : 'selection:bg-cyan-200 selection:text-cyan-900',
     };
 
-    // Email Submission Logic
+    // Email Submission Logic - 使用统一的 hook
     const [email, setEmail] = useState('');
-    const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
-    const [message, setMessage] = useState('');
+    const { submitEmail, status, message } = useEmailSubmission();
 
     const handleEmailSubmit = async () => {
         if (!email) return;
 
-        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-        if (!emailRegex.test(email)) {
-            setStatus('error');
-            setMessage(currentLang === 'zh' ? '请输入有效的邮箱地址' : 'Please enter a valid email address');
-            return;
-        }
-
-        setStatus('loading');
-        setMessage('');
-
-        try {
-            const response = await fetch('/api/send-welcome-email', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({ email }),
-            });
-
-            const data = await response.json();
+        const result = await submitEmail({
+            email,
+            source: 'Step3_Developer'
+        });
 
-            if (data.success) {
-                setStatus('success');
-                setMessage(data.message || (currentLang === 'zh' ? '发送成功!我们将尽快与您联系。' : 'Success! We will contact you soon.'));
-                setEmail('');
-            } else {
-                setStatus('error');
-                setMessage(data.message || (currentLang === 'zh' ? '发送失败,请稍后重试。' : 'Failed to send. Please try again later.'));
-            }
-        } catch (error) {
-            setStatus('error');
-            setMessage(currentLang === 'zh' ? '网络错误,请稍后重试。' : 'Network error. Please try again later.');
+        if (result.success) {
+            setEmail('');
         }
     };
 

+ 45 - 22
src/pages/step2.css

@@ -1,12 +1,6 @@
 @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Noto+Sans+SC:wght@300;500;900&display=swap');
 
-:root {
-  --bg-color: #050505;
-  --text-primary: #ffffff;
-  --text-secondary: #888888;
-  --accent-color: #ccff00; /* Cyber Lime */
-  --grid-line: #222222;
-}
+/* CSS 变量默认值 - 会被组件的 inline style 覆盖 */
 
 .step2-page {
   background-color: var(--bg-color);
@@ -36,14 +30,19 @@
 }
 
 @keyframes marquee {
-  0% { transform: translateX(0); }
-  100% { transform: translateX(-50%); }
+  0% {
+    transform: translateX(0);
+  }
+
+  100% {
+    transform: translateX(-50%);
+  }
 }
 
 /* 网格背景 */
 .step2-page .bg-grid {
   background-image: linear-gradient(var(--grid-line) 1px, transparent 1px),
-  linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
+    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
   background-size: 40px 40px;
 }
 
@@ -77,29 +76,51 @@
 }
 
 @keyframes glitch-anim {
-  0% { clip: rect(10px, 9999px, 30px, 0); }
-  20% { clip: rect(30px, 9999px, 10px, 0); }
-  40% { clip: rect(50px, 9999px, 80px, 0); }
-  100% { clip: rect(90px, 9999px, 100px, 0); }
+  0% {
+    clip: rect(10px, 9999px, 30px, 0);
+  }
+
+  20% {
+    clip: rect(30px, 9999px, 10px, 0);
+  }
+
+  40% {
+    clip: rect(50px, 9999px, 80px, 0);
+  }
+
+  100% {
+    clip: rect(90px, 9999px, 100px, 0);
+  }
 }
 
 @keyframes glitch-anim2 {
-  0% { clip: rect(80px, 9999px, 10px, 0); }
-  20% { clip: rect(20px, 9999px, 50px, 0); }
-  40% { clip: rect(10px, 9999px, 60px, 0); }
-  100% { clip: rect(40px, 9999px, 90px, 0); }
+  0% {
+    clip: rect(80px, 9999px, 10px, 0);
+  }
+
+  20% {
+    clip: rect(20px, 9999px, 50px, 0);
+  }
+
+  40% {
+    clip: rect(10px, 9999px, 60px, 0);
+  }
+
+  100% {
+    clip: rect(40px, 9999px, 90px, 0);
+  }
 }
 
 /* 卡片悬停效果 */
 .step2-page .service-card {
-  border: 1px solid #333;
+  border: 1px solid var(--card-border);
   transition: all 0.3s ease;
 }
 
 .step2-page .service-card:hover {
   border-color: var(--accent-color);
   transform: translateY(-5px);
-  box-shadow: 0 10px 30px -10px rgba(204, 255, 0, 0.2);
+  box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.3);
 }
 
 .step2-page .cursor-blink {
@@ -107,5 +128,7 @@
 }
 
 @keyframes blink {
-  50% { opacity: 0; }
-}
+  50% {
+    opacity: 0;
+  }
+}

+ 167 - 0
src/services/api.ts

@@ -0,0 +1,167 @@
+// API 服务,统一处理错误和降级方案
+
+export interface ApiResponse {
+    success: boolean;
+    message: string;
+    data?: any;
+}
+
+export interface EmailSubmissionData {
+    email: string;
+    name?: string;
+    industry?: string;
+    source?: string;
+}
+
+class ApiService {
+    // 可选:用于未来扩展的 API 基础 URL
+    // private baseUrl: string;
+
+    constructor() {
+        // 从环境变量或默认值获取API基础URL(预留扩展用)
+        // this.baseUrl = import.meta.env.VITE_API_URL || '';
+    }
+
+    /**
+     * 发送欢迎邮件
+     * 如果API不可用,提供降级方案
+     */
+    async sendWelcomeEmail(data: EmailSubmissionData): Promise<ApiResponse> {
+        try {
+            // 尝试调用真实API
+            const response = await fetch('/api/send-welcome-email', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify(data),
+            });
+
+            // 如果响应不存在,可能是CORS或网络错误
+            if (!response) {
+                throw new Error('Network error: Unable to connect to server');
+            }
+
+            const result = await response.json();
+
+            // 检查响应是否成功
+            if (!response.ok || !result.success) {
+                throw new Error(result.message || `HTTP ${response.status}: ${response.statusText}`);
+            }
+
+            return result;
+        } catch (error) {
+            console.warn('API call failed, falling back to local handling:', error);
+
+            // 降级方案:本地处理和存储
+            return this.handleEmailSubmissionFallback(data, error);
+        }
+    }
+
+    /**
+     * 邮件提交的降级方案
+     */
+    private handleEmailSubmissionFallback(data: EmailSubmissionData, originalError: any): ApiResponse {
+        try {
+            // 存储到localStorage作为备份
+            const submissions = JSON.parse(localStorage.getItem('email_submissions') || '[]');
+            submissions.push({
+                ...data,
+                timestamp: new Date().toISOString(),
+                status: 'pending' // 标记为待发送
+            });
+            localStorage.setItem('email_submissions', JSON.stringify(submissions));
+
+            // 创建mailto链接作为最后的降级方案
+            const subject = encodeURIComponent(`CCDW Inquiry - ${data.source || 'Website'}`);
+            const body = encodeURIComponent(
+                `Email: ${data.email}\n` +
+                `Name: ${data.name || 'Not provided'}\n` +
+                `Industry: ${data.industry || 'Not specified'}\n` +
+                `Source: ${data.source || 'Website'}\n` +
+                `Time: ${new Date().toLocaleString()}`
+            );
+            const mailtoLink = `mailto:contact@ccdw.xyz?subject=${subject}&body=${body}`;
+
+            // 提供用户友好的降级消息
+            const isDevelopment = import.meta.env.DEV;
+            const fallbackMessage = isDevelopment
+                ? `开发模式:邮件已记录到本地存储。\n\n详细信息:${JSON.stringify(data, null, 2)}`
+                : `感谢您的订阅!我们已记录您的信息,将尽快与您联系。\n\n如果需要立即联系我们,请发送邮件至:contact@ccdw.xyz`;
+
+            return {
+                success: true,
+                message: fallbackMessage,
+                data: {
+                    mailtoLink,
+                    storedLocally: true,
+                    originalError: originalError?.message || 'Unknown error'
+                }
+            };
+        } catch (fallbackError) {
+            // 如果连降级方案都失败了
+            console.error('Fallback also failed:', fallbackError);
+            return {
+                success: false,
+                message: '抱歉,服务暂时不可用。请稍后重试或直接发送邮件至 contact@ccdw.xyz'
+            };
+        }
+    }
+
+    /**
+     * 尝试重新发送本地存储的待发送邮件
+     */
+    async retryPendingSubmissions(): Promise<ApiResponse[]> {
+        const submissions = JSON.parse(localStorage.getItem('email_submissions') || '[]');
+        const pendingSubmissions = submissions.filter((sub: any) => sub.status === 'pending');
+
+        if (pendingSubmissions.length === 0) {
+            return [{ success: true, message: 'No pending submissions' }];
+        }
+
+        const results: ApiResponse[] = [];
+
+        for (const submission of pendingSubmissions) {
+            try {
+                const result = await this.sendWelcomeEmail(submission);
+                results.push(result);
+
+                // 如果成功,更新状态
+                if (result.success) {
+                    submission.status = 'sent';
+                    submission.sentAt = new Date().toISOString();
+                }
+            } catch (error) {
+                results.push({
+                    success: false,
+                    message: `Failed to resend: ${error}`
+                });
+            }
+        }
+
+        // 更新localStorage
+        localStorage.setItem('email_submissions', JSON.stringify(submissions));
+
+        return results;
+    }
+
+    /**
+     * 获取本地存储的提交历史
+     */
+    getLocalSubmissions(): any[] {
+        return JSON.parse(localStorage.getItem('email_submissions') || '[]');
+    }
+
+    /**
+     * 清除本地存储的提交数据
+     */
+    clearLocalSubmissions(): void {
+        localStorage.removeItem('email_submissions');
+    }
+}
+
+// 导出单例实例
+export const apiService = new ApiService();
+
+// 导出类型
+export default apiService;

+ 388 - 0
src/styles/accessibility.css

@@ -0,0 +1,388 @@
+/* 无障碍性优化 */
+
+/* 跳过导航链接 */
+.skip-link {
+  position: absolute;
+  top: -40px;
+  left: 6px;
+  background: #000;
+  color: #fff;
+  padding: 8px;
+  text-decoration: none;
+  border-radius: 4px;
+  z-index: 1000;
+  transition: top 0.3s;
+}
+
+.skip-link:focus {
+  top: 6px;
+}
+
+/* 焦点样式 */
+.focus-visible {
+  outline: 2px solid #2563eb;
+  outline-offset: 2px;
+}
+
+/* 高对比度模式下的焦点样式 */
+@media (prefers-contrast: high) {
+  .focus-visible {
+    outline: 3px solid #000;
+    outline-offset: 2px;
+  }
+}
+
+/* 减少动画偏好 */
+@media (prefers-reduced-motion: reduce) {
+  *,
+  *::before,
+  *::after {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+    scroll-behavior: auto !important;
+  }
+}
+
+/* 屏幕阅读器专用内容 */
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border: 0;
+}
+
+/* 不是屏幕阅读器专用但需要隐藏的元素 */
+.not-sr-only {
+  position: static;
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 0;
+  overflow: visible;
+  clip: auto;
+  white-space: normal;
+}
+
+/* 颜色和对比度 */
+.text-low-contrast {
+  opacity: 0.7;
+}
+
+/* 高对比度模式下的调整 */
+@media (prefers-contrast: high) {
+  .text-low-contrast {
+    opacity: 1;
+  }
+  
+  /* 增加边框对比度 */
+  .border-low-contrast {
+    border-width: 2px;
+  }
+}
+
+/* 键盘导航优化 */
+.keyboard-nav {
+  outline: none;
+}
+
+.keyboard-nav:focus-visible {
+  outline: 2px solid #2563eb;
+  outline-offset: 2px;
+  box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2);
+}
+
+/* 交互元素的最小尺寸 */
+[role="button"],
+button,
+a,
+input,
+select,
+textarea {
+  min-height: 44px;
+  min-width: 44px;
+}
+
+/* 表单标签优化 */
+.form-label {
+  display: block;
+  margin-bottom: 0.5rem;
+  font-weight: 500;
+}
+
+.form-input {
+  display: block;
+  width: 100%;
+  padding: 0.75rem;
+  border: 2px solid #d1d5db;
+  border-radius: 0.375rem;
+  font-size: 1rem;
+  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.form-input:focus {
+  border-color: #2563eb;
+  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
+  outline: none;
+}
+
+.form-input[aria-invalid="true"] {
+  border-color: #dc2626;
+}
+
+.form-input[aria-invalid="true"]:focus {
+  box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
+}
+
+/* 错误消息样式 */
+.error-message {
+  color: #dc2626;
+  font-size: 0.875rem;
+  margin-top: 0.25rem;
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+}
+
+/* 成功消息样式 */
+.success-message {
+  color: #059669;
+  font-size: 0.875rem;
+  margin-top: 0.25rem;
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+}
+
+/* 加载状态样式 */
+.loading-indicator {
+  display: inline-flex;
+  align-items: center;
+  gap: 0.5rem;
+}
+
+.loading-spinner {
+  width: 1rem;
+  height: 1rem;
+  border: 2px solid #e5e7eb;
+  border-top: 2px solid #2563eb;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 通知样式 */
+.notification {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  padding: 1rem 1.5rem;
+  border-radius: 0.5rem;
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+  z-index: 1000;
+  max-width: 400px;
+  transition: all 0.3s ease-in-out;
+}
+
+.notification-success {
+  background-color: #10b981;
+  color: white;
+}
+
+.notification-error {
+  background-color: #ef4444;
+  color: white;
+}
+
+.notification-warning {
+  background-color: #f59e0b;
+  color: #1f2937;
+}
+
+.notification-info {
+  background-color: #3b82f6;
+  color: white;
+}
+
+/* 深色模式下的调整 */
+.dark .form-input {
+  background-color: #1f2937;
+  border-color: #4b5563;
+  color: #f9fafb;
+}
+
+.dark .form-input:focus {
+  border-color: #60a5fa;
+  box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
+}
+
+.dark .error-message {
+  color: #f87171;
+}
+
+.dark .success-message {
+  color: #34d399;
+}
+
+/* 按钮状态 */
+.button {
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 0.5rem;
+  padding: 0.75rem 1.5rem;
+  font-size: 1rem;
+  font-weight: 500;
+  line-height: 1;
+  border: 2px solid transparent;
+  border-radius: 0.375rem;
+  cursor: pointer;
+  transition: all 0.15s ease-in-out;
+  text-decoration: none;
+}
+
+.button:focus-visible {
+  outline: 2px solid #2563eb;
+  outline-offset: 2px;
+  box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2);
+}
+
+.button:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.button-primary {
+  background-color: #2563eb;
+  color: white;
+}
+
+.button-primary:hover:not(:disabled) {
+  background-color: #1d4ed8;
+}
+
+.button-secondary {
+  background-color: transparent;
+  color: #2563eb;
+  border-color: #2563eb;
+}
+
+.button-secondary:hover:not(:disabled) {
+  background-color: #2563eb;
+  color: white;
+}
+
+/* 链接样式 */
+.link {
+  color: #2563eb;
+  text-decoration: underline;
+  text-underline-offset: 2px;
+}
+
+.link:hover,
+.link:focus-visible {
+  color: #1d4ed8;
+  text-decoration: underline;
+}
+
+.link:focus-visible {
+  outline: 2px solid #2563eb;
+  outline-offset: 2px;
+  border-radius: 2px;
+}
+
+/* 表格无障碍 */
+.table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.table th,
+.table td {
+  padding: 0.75rem;
+  text-align: left;
+  border-bottom: 1px solid #e5e7eb;
+}
+
+.table th {
+  font-weight: 600;
+  background-color: #f9fafb;
+}
+
+.table caption {
+  font-size: 0.875rem;
+  color: #6b7280;
+  margin-bottom: 0.75rem;
+  caption-side: bottom;
+}
+
+/* 列表无障碍 */
+.list {
+  padding-left: 1.5rem;
+  margin: 1rem 0;
+}
+
+.list li {
+  margin-bottom: 0.5rem;
+}
+
+/* 地标区域 */
+[role="main"],
+[role="navigation"],
+[role="complementary"],
+[role="contentinfo"] {
+  outline: 1px solid transparent;
+}
+
+[role="main"]:focus,
+[role="navigation"]:focus,
+[role="complementary"]:focus,
+[role="contentinfo"]:focus {
+  outline: 2px solid #2563eb;
+  outline-offset: 2px;
+}
+
+/* 模态框无障碍 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.modal {
+  background-color: white;
+  border-radius: 0.5rem;
+  padding: 2rem;
+  max-width: 500px;
+  width: 90vw;
+  max-height: 90vh;
+  overflow-y: auto;
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+}
+
+.modal:focus {
+  outline: 2px solid #2563eb;
+  outline-offset: 2px;
+}
+
+.dark .modal {
+  background-color: #1f2937;
+  color: #f9fafb;
+}

+ 275 - 0
src/styles/fonts.css

@@ -0,0 +1,275 @@
+/* 字体优化和加载策略 */
+
+/* 定义字体堆栈 */
+:root {
+  --font-primary: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+  --font-secondary: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  --font-mono: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', 'Ubuntu Mono', monospace;
+}
+
+/* 基础字体设置 */
+body {
+  font-family: var(--font-primary);
+  font-feature-settings: 'kern' 1, 'liga' 1;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* 防止字体闪烁(FOUT) */
+.fonts-loaded {
+  /* 字体加载完成后移除过渡 */
+}
+
+/* 字体加载前的临时样式 */
+.fonts-loading {
+  /* 在字体加载期间使用系统字体 */
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+/* 特定元素的字体设置 */
+.font-serif {
+  font-family: 'Noto Serif SC', Georgia, 'Times New Roman', serif;
+}
+
+.font-mono {
+  font-family: var(--font-mono);
+  font-feature-settings: 'tnum' 1; /* 等宽数字 */
+}
+
+.font-display {
+  font-family: var(--font-secondary);
+  font-weight: 700;
+  letter-spacing: -0.02em;
+}
+
+/* 字体大小和行高优化 */
+.text-responsive-xs {
+  font-size: clamp(0.75rem, 2vw, 0.875rem);
+}
+
+.text-responsive-sm {
+  font-size: clamp(0.875rem, 2.5vw, 1rem);
+}
+
+.text-responsive-base {
+  font-size: clamp(1rem, 3vw, 1.125rem);
+}
+
+.text-responsive-lg {
+  font-size: clamp(1.125rem, 3.5vw, 1.25rem);
+}
+
+.text-responsive-xl {
+  font-size: clamp(1.25rem, 4vw, 1.5rem);
+}
+
+.text-responsive-2xl {
+  font-size: clamp(1.5rem, 5vw, 2rem);
+}
+
+.text-responsive-3xl {
+  font-size: clamp(2rem, 6vw, 3rem);
+}
+
+.text-responsive-4xl {
+  font-size: clamp(2.5rem, 7vw, 4rem);
+}
+
+.text-responsive-5xl {
+  font-size: clamp(3rem, 8vw, 5rem);
+}
+
+/* 标题字体优化 */
+h1, h2, h3, h4, h5, h6 {
+  font-family: var(--font-secondary);
+  font-weight: 700;
+  line-height: 1.2;
+  letter-spacing: -0.02em;
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+/* 段落优化 */
+p {
+  line-height: 1.6;
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+/* 小字体优化 */
+small {
+  font-size: 0.875em;
+  line-height: 1.4;
+}
+
+/* 引用优化 */
+blockquote {
+  font-family: var(--font-serif);
+  font-style: italic;
+  line-height: 1.6;
+  border-left: 4px solid;
+  padding-left: 1rem;
+  margin: 1.5rem 0;
+}
+
+/* 代码字体优化 */
+code {
+  font-family: var(--font-mono);
+  font-size: 0.875em;
+  background-color: rgba(0, 0, 0, 0.05);
+  padding: 0.125rem 0.25rem;
+  border-radius: 0.25rem;
+}
+
+pre {
+  font-family: var(--font-mono);
+  font-size: 0.875em;
+  line-height: 1.5;
+  background-color: rgba(0, 0, 0, 0.05);
+  padding: 1rem;
+  border-radius: 0.5rem;
+  overflow-x: auto;
+}
+
+/* 暗色模式下的代码样式 */
+.dark code {
+  background-color: rgba(255, 255, 255, 0.1);
+}
+
+.dark pre {
+  background-color: rgba(255, 255, 255, 0.05);
+}
+
+/* 字体权重优化 */
+.font-light {
+  font-weight: 300;
+}
+
+.font-normal {
+  font-weight: 400;
+}
+
+.font-medium {
+  font-weight: 500;
+}
+
+.font-semibold {
+  font-weight: 600;
+}
+
+.font-bold {
+  font-weight: 700;
+}
+
+.font-black {
+  font-weight: 900;
+}
+
+/* 字符间距优化 */
+.tracking-tighter {
+  letter-spacing: -0.05em;
+}
+
+.tracking-tight {
+  letter-spacing: -0.025em;
+}
+
+.tracking-normal {
+  letter-spacing: 0;
+}
+
+.tracking-wide {
+  letter-spacing: 0.025em;
+}
+
+.tracking-wider {
+  letter-spacing: 0.05em;
+}
+
+.tracking-widest {
+  letter-spacing: 0.1em;
+}
+
+/* 行高优化 */
+.leading-none {
+  line-height: 1;
+}
+
+.leading-tight {
+  line-height: 1.25;
+}
+
+.leading-snug {
+  line-height: 1.375;
+}
+
+.leading-normal {
+  line-height: 1.5;
+}
+
+.leading-relaxed {
+  line-height: 1.625;
+}
+
+.leading-loose {
+  line-height: 2;
+}
+
+/* 字体加载动画 */
+@keyframes fontFadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(0.1em);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.fonts-loaded * {
+  animation: fontFadeIn 0.3s ease-out;
+}
+
+/* 性能优化 */
+.text-rendering-optimize {
+  text-rendering: optimizeLegibility;
+  font-feature-settings: 'kern' 1, 'liga' 1;
+}
+
+/* 高对比度模式支持 */
+@media (prefers-contrast: high) {
+  body {
+    font-weight: 500;
+  }
+}
+
+/* 减少动画偏好支持 */
+@media (prefers-reduced-motion: reduce) {
+  .fonts-loaded * {
+    animation: none;
+  }
+}
+
+/* 打印样式优化 */
+@media print {
+  body {
+    font-family: 'Times New Roman', serif;
+    font-size: 12pt;
+    line-height: 1.4;
+    color: black;
+    background: white;
+  }
+  
+  h1, h2, h3, h4, h5, h6 {
+    font-family: 'Times New Roman', serif;
+    font-weight: bold;
+    page-break-after: avoid;
+  }
+  
+  p {
+    orphans: 3;
+    widows: 3;
+  }
+}

+ 191 - 0
src/styles/responsive.css

@@ -0,0 +1,191 @@
+/* 移动端响应式修复 */
+
+/* 确保移动端导航菜单正确显示 */
+@media (max-width: 768px) {
+  /* 防止移动端横向滚动 */
+  body {
+    overflow-x: hidden;
+  }
+
+  /* 移动端导航菜单背景 */
+  .mobile-menu-backdrop {
+    backdrop-filter: blur(8px);
+    -webkit-backdrop-filter: blur(8px);
+  }
+
+  /* 移动端菜单项触摸优化 */
+  .mobile-menu-item {
+    min-height: 44px; /* iOS推荐的最小触摸目标 */
+    display: flex;
+    align-items: center;
+  }
+
+  /* 移动端按钮触摸优化 */
+  .mobile-button {
+    min-height: 44px;
+    min-width: 44px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  /* 移动端表单优化 */
+  .mobile-input {
+    font-size: 16px; /* 防止iOS缩放 */
+    padding: 12px 16px;
+    border-radius: 8px;
+  }
+
+  /* 移动端卡片布局优化 */
+  .mobile-card {
+    margin-bottom: 1rem;
+    border-radius: 12px;
+  }
+
+  /* 移动端文字大小优化 */
+  .mobile-text-sm {
+    font-size: 0.875rem;
+    line-height: 1.5;
+  }
+
+  .mobile-text-base {
+    font-size: 1rem;
+    line-height: 1.6;
+  }
+
+  /* 移动端间距优化 */
+  .mobile-spacing-tight {
+    padding: 1rem;
+  }
+
+  .mobile-spacing-normal {
+    padding: 1.5rem;
+  }
+
+  /* 移动端网格布局优化 */
+  .mobile-grid-single {
+    grid-template-columns: 1fr !important;
+    gap: 1rem !important;
+  }
+
+  /* 移动端隐藏元素 */
+  .mobile-hidden {
+    display: none !important;
+  }
+
+  /* 移动端显示元素 */
+  .mobile-only {
+    display: block !important;
+  }
+
+  /* 移动端全宽元素 */
+  .mobile-full-width {
+    width: 100% !important;
+    max-width: 100% !important;
+  }
+
+  /* 移动端滚动优化 */
+  .mobile-scroll-smooth {
+    -webkit-overflow-scrolling: touch;
+    scroll-behavior: smooth;
+  }
+
+  /* 移动端阴影优化 */
+  .mobile-shadow-soft {
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+
+  /* 移动端过渡优化 */
+  .mobile-transition {
+    transition: all 0.2s ease-in-out;
+  }
+
+  /* 移动端安全区域 */
+  .mobile-safe-top {
+    padding-top: env(safe-area-inset-top);
+  }
+
+  .mobile-safe-bottom {
+    padding-bottom: env(safe-area-inset-bottom);
+  }
+
+  .mobile-safe-left {
+    padding-left: env(safe-area-inset-left);
+  }
+
+  .mobile-safe-right {
+    padding-right: env(safe-area-inset-right);
+  }
+
+  /* 移动端视口高度修复 */
+  .mobile-vh-fix {
+    height: 100vh;
+    height: -webkit-fill-available;
+  }
+}
+
+/* 平板端优化 */
+@media (min-width: 768px) and (max-width: 1024px) {
+  .tablet-hidden {
+    display: none !important;
+  }
+
+  .tablet-only {
+    display: block !important;
+  }
+
+  /* 平板端网格布局 */
+  .tablet-grid-two {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+
+/* 桌面端专用样式 */
+@media (min-width: 1025px) {
+  .desktop-only {
+    display: block !important;
+  }
+
+  .mobile-only {
+    display: none !important;
+  }
+}
+
+/* 横屏模式优化 */
+@media (orientation: landscape) and (max-height: 500px) {
+  .landscape-compact {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+}
+
+/* 高DPI屏幕优化 */
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+  .high-dpi-border {
+    border-width: 0.5px;
+  }
+}
+
+/* 暗色模式下的移动端优化 */
+@media (max-width: 768px) {
+  .dark .mobile-menu-backdrop {
+    background-color: rgba(0, 0, 0, 0.8);
+  }
+
+  .dark .mobile-shadow-soft {
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+  }
+}
+
+/* 动画性能优化 */
+@media (prefers-reduced-motion: reduce) {
+  .mobile-transition {
+    transition: none;
+  }
+
+  * {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+  }
+}