chenhg 15 годин тому
батько
коміт
5a1b823756

+ 1 - 1
src/App.tsx

@@ -4,7 +4,7 @@ import { HashRouter as Router, Routes, Route } from 'react-router-dom';
 import Home from '@/src/pages/Home.tsx';
 import Ships from '@/src/pages/Ships.tsx';
 import Itineraries from '@/src/pages/Itineraries.tsx';
-import AboutUs from '@/src/pages/AhoutUs.tsx';
+import AboutUs from '@/src/pages/AboutUs.tsx';
 import Guide from '@/src/pages/Guide.tsx';
 import { LanguageProvider } from '@/src/contexts/LanguageContext.tsx';
 import { ThemeProvider } from '@/src/contexts/ThemeContext.tsx';

BIN
src/assets/404/cat.mp4


+ 6 - 0
src/components/Sidebar.tsx

@@ -38,6 +38,7 @@ const Sidebar: React.FC = () => {
                   src={MiniProgramQRCode as string}  // 替换为你的图片路径
                   alt="公众号二维码"
                   className="w-24 h-24 object-cover rounded"
+                  loading="lazy"
               />
             </div>
 
@@ -48,6 +49,7 @@ const Sidebar: React.FC = () => {
               src={WeChatIcon as string}
               alt="微信"
               className="w-5 h-5 object-contain"
+              loading="lazy"
             />
           </button>
         </div>
@@ -62,6 +64,7 @@ const Sidebar: React.FC = () => {
                   src={RedBookQRCode as string}  // 替换为你的图片路径
                   alt="公众号二维码"
                   className="w-24 h-24 object-cover rounded"
+                  loading="lazy"
               />
             </div>
 
@@ -72,6 +75,7 @@ const Sidebar: React.FC = () => {
               src={RedBook as string}
               alt="小红书"
               className="w-8 h-8 object-contain"
+              loading="lazy"
             />
           </button>
         </div>
@@ -86,6 +90,7 @@ const Sidebar: React.FC = () => {
                   src={TiktokQRCode as string}  // 替换为你的图片路径
                   alt="公众号二维码"
                   className="w-24 h-24 object-cover rounded"
+                  loading="lazy"
               />
             </div>
           </div>
@@ -95,6 +100,7 @@ const Sidebar: React.FC = () => {
                 src={TiktokIcon as string}
                 alt="抖音"
                 className="w-8 h-8 object-contain"
+                loading="lazy"
             />
           </button>
         </div>

+ 16 - 3
src/contexts/LanguageContext.tsx

@@ -1,5 +1,6 @@
 import React, { createContext, useState, useContext, ReactNode } from 'react';
 import { Language } from '../types.ts';
+import { CONTENT } from '../constants.ts';
 
 interface LanguageContextType {
   language: Language;
@@ -17,9 +18,21 @@ export const LanguageProvider: React.FC<{ children: ReactNode }> = ({ children }
     setLanguage(prev => prev === 'zh' ? 'en' : 'zh');
   };
 
-  // Simple translation helper placeholder
-  // Real translations will be handled by selecting data objects in components
-  const t = (key: string) => key;
+  // Real translation helper that traverses the CONTENT object
+  const t = (key: string) => {
+    const keys = key.split('.');
+    let value: any = CONTENT[language];
+    
+    for (const k of keys) {
+      if (value && typeof value === 'object' && k in value) {
+        value = value[k];
+      } else {
+        return key; // Return original key as fallback if translation not found
+      }
+    }
+    
+    return typeof value === 'string' ? value : key;
+  };
 
   return (
     <LanguageContext.Provider value={{ language, setLanguage, toggleLanguage, t }}>

+ 4 - 0
src/pages/AhoutUs.tsx

@@ -113,6 +113,7 @@ const AboutUs: React.FC = () => {
             src={aboutHeroImage || "https://images.unsplash.com/photo-1578474843222-9593bc88d8b0?q=80&w=1920&auto=format&fit=crop"} 
             alt="About Us" 
             className="w-full h-full object-cover object-center" 
+            loading="lazy"
           />
           <div className="absolute inset-0 bg-black/40 pointer-events-none"></div>
         </div>
@@ -134,6 +135,7 @@ const AboutUs: React.FC = () => {
                     src={ CompanyLogo || "https://images.unsplash.com/photo-1599640845513-2627a35c602a?q=80&w=1920&auto=format&fit=crop"}
                   alt="Company" 
                   className="w-full h-auto rounded-lg shadow-xl"
+                  loading="lazy"
                 />
                 <div className="absolute -bottom-6 -right-6 bg-vista-gold text-white p-6 rounded-lg shadow-lg">
                   <span className="text-3xl font-serif">10+</span>
@@ -186,6 +188,7 @@ const AboutUs: React.FC = () => {
                       src={media.icon} 
                       alt={media.name} 
                       className="w-8 h-8 object-contain"
+                      loading="lazy"
                     />
                   </div>
                   <h3 className="text-xl font-serif text-vista-darkblue mb-2">
@@ -234,6 +237,7 @@ const AboutUs: React.FC = () => {
                   src={selectedMedia.qrCode as string} 
                   alt={`${language === 'zh' ? selectedMedia.name : selectedMedia.nameEn} QR Code`} 
                   className="w-64 h-64 object-contain border border-vista-gold/30 p-4"
+                  loading="lazy"
                 />
               </div>
             </div>

+ 7 - 3
src/pages/Home.tsx

@@ -46,6 +46,7 @@ const Home: React.FC = () => {
               src={img} 
               alt={`Slide ${index + 1}`} 
               className="w-full h-full object-cover object-center scale-105 animate-slow-zoom" 
+              loading="lazy"
               onError={(e) => {
                 // Fallback to default image if API image fails to load
                 const target = e.target as HTMLImageElement;
@@ -122,12 +123,14 @@ const Home: React.FC = () => {
                                 muted
                                 loop
                                 playsInline
+                                loading="lazy"
                               />
                             ) : (
                               <img 
                                 src={item.image} 
                                 alt={item.title} 
                                 className="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700" 
+                                loading="lazy"
                               />
                             )}
                             <div className="absolute top-4 left-4 bg-white px-3 py-1 text-xs font-bold tracking-widest uppercase z-10 text-vista-darkblue">
@@ -151,7 +154,7 @@ const Home: React.FC = () => {
       {/* Banner / Activity Highlight (Customizable Dining) */}
       <section className="relative py-32 flex items-center justify-center">
          <div className="absolute inset-0 z-0">
-             <img src={dining.image} alt="Dining" className="w-full h-full object-cover brightness-50" />
+             <img src={dining.image} alt="Dining" className="w-full h-full object-cover brightness-50" loading="lazy" />
          </div>
          <div className="relative z-10 text-center max-w-4xl px-6">
              <h2 className="text-4xl md:text-5xl font-serif text-white italic mb-6">{dining.title}</h2>
@@ -177,7 +180,7 @@ const Home: React.FC = () => {
                     <div key={ship.id} className="min-w-[85vw] md:min-w-[600px] relative group cursor-pointer overflow-hidden">
                         <div className="aspect-[16/9] overflow-hidden">
                             {/* Removed grayscale, added scale transform */}
-                            <img src={ship.image} alt={ship.name} className="w-full h-full object-cover transition-transform duration-1000 ease-out group-hover:scale-105" />
+                            <img src={ship.image} alt={ship.name} className="w-full h-full object-cover transition-transform duration-1000 ease-out group-hover:scale-105" loading="lazy" />
                         </div>
                         <div className="absolute bottom-0 left-0 bg-white p-6 md:p-8 max-w-md shadow-lg transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500">
                             <h3 className="text-3xl font-serif text-vista-darkblue mb-2">{ship.name}</h3>
@@ -222,10 +225,11 @@ const Home: React.FC = () => {
                               className="w-full h-full object-cover" 
                               controls 
                               poster={videoSection.thumbnail}
+                              loading="lazy"
                           />
                       ) : (
                           <>  
-                              <img src={videoSection.thumbnail} className="w-full h-full object-cover opacity-60 group-hover:opacity-40 transition-opacity" alt="Video Thumbnail" />
+                              <img src={videoSection.thumbnail} className="w-full h-full object-cover opacity-60 group-hover:opacity-40 transition-opacity" alt="Video Thumbnail" loading="lazy" />
                               <div className="absolute inset-0 flex items-center justify-center">
                                   <div className="w-20 h-20 rounded-full border border-white flex items-center justify-center group-hover:bg-white group-hover:text-vista-darkblue transition-all duration-300">
                                       <Play size={32} fill="currentColor" />

+ 3 - 0
src/pages/Itineraries.tsx

@@ -180,12 +180,14 @@ const Itineraries: React.FC = () => {
                       muted
                       loop
                       playsInline
+                      loading="lazy"
                     />
                   ) : (
                     <img 
                       src={item.image} 
                       alt={item.title} 
                       className="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700" 
+                      loading="lazy"
                     />
                   )}
                   <div className="absolute top-4 left-4 bg-white/95 backdrop-blur-sm px-4 py-2 rounded-full text-xs font-bold tracking-widest uppercase z-10 text-vista-darkblue shadow-sm">
@@ -371,6 +373,7 @@ const Itineraries: React.FC = () => {
                 src={selectedItinerary.image} 
                 alt={selectedItinerary.title} 
                 className="w-full h-full object-cover transition-transform duration-500 hover:scale-105"
+                loading="lazy"
               />
               <button 
                 className="absolute top-4 right-4 bg-white/90 backdrop-blur-sm rounded-full p-3 hover:bg-white transition-all duration-300 shadow-lg hover:shadow-xl"

+ 8 - 0
src/pages/Ships.tsx

@@ -48,6 +48,7 @@ const Ships: React.FC = () => {
             src="https://images.unsplash.com/photo-1548291616-3c0f5f743538?q=80&w=1920&auto=format&fit=crop"
             alt="Ships Hero"
             className="absolute inset-0 w-full h-full object-cover animate-slow-zoom"
+            loading="lazy"
          />
          <div className="relative z-20 text-center text-white px-4 animate-fade-in-up">
              <span className="block text-sm md:text-base tracking-[0.3em] uppercase mb-4 text-vista-gold">{t.hero_sub}</span>
@@ -95,6 +96,7 @@ const Ships: React.FC = () => {
                         src={shipsPageImages.lanyue[0]}
                         alt="Lanyue"
                         className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
+                        loading="lazy"
                     />
                     <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-90 transition-opacity"></div>
                     <div className="absolute bottom-8 left-8 text-white">
@@ -110,6 +112,7 @@ const Ships: React.FC = () => {
                         src={shipsPageImages.aurora}
                         alt="Aurora"
                         className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 grayscale group-hover:grayscale-0"
+                        loading="lazy"
                     />
                     <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-90 transition-opacity"></div>
                     <div className="absolute bottom-8 left-8 text-white">
@@ -167,6 +170,7 @@ const Ships: React.FC = () => {
                         src={shipsPageImages.lanyue[0]}
                         alt="Lanyue Exterior"
                         className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105"
+                        loading="lazy"
                       />
                   </div>
                   {/* Smaller Detail Images */}
@@ -175,6 +179,7 @@ const Ships: React.FC = () => {
                         src={shipsPageImages.lanyue[1]}
                         alt="Lanyue Interior"
                         className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105"
+                        loading="lazy"
                       />
                   </div>
                   <div className="lg:col-span-4 lg:row-span-1 relative group overflow-hidden shadow-lg">
@@ -182,6 +187,7 @@ const Ships: React.FC = () => {
                         src={shipsPageImages.lanyue[2]}
                         alt="Lanyue Deck"
                         className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105"
+                        loading="lazy"
                       />
                   </div>
                   <div className="lg:col-span-12 lg:row-span-1 relative group overflow-hidden shadow-lg md:hidden lg:block">
@@ -189,6 +195,7 @@ const Ships: React.FC = () => {
                         src={shipsPageImages.lanyue[3]}
                         alt="Lanyue Wide"
                         className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105"
+                        loading="lazy"
                       />
                   </div>
               </div>
@@ -208,6 +215,7 @@ const Ships: React.FC = () => {
                  src={shipsPageImages.aurora}
                  alt="Aurora"
                  className="w-full h-full object-cover animate-slow-zoom"
+                 loading="lazy"
                />
                <div className="absolute inset-0 bg-gradient-to-b from-black via-transparent to-black"></div>
           </div>

+ 90 - 19
src/services/articleService.ts

@@ -29,19 +29,76 @@ interface ApiResponse {
   msg: string;
 }
 
+// API响应缓存配置
+const CACHE_EXPIRATION = 10 * 60 * 1000; // 10分钟
+
+// 缓存接口定义
+interface CacheItem {
+  data: ApiResponse;
+  timestamp: number;
+}
+
+// 缓存对象
+const cache: Record<string, CacheItem> = {};
+
+// 生成缓存键
+const generateCacheKey = (url: string, params: Record<string, any>): string => {
+  const sortedParams = Object.keys(params).sort().reduce((acc, key) => {
+    acc[key] = params[key];
+    return acc;
+  }, {} as Record<string, any>);
+  return `${url}?${JSON.stringify(sortedParams)}`;
+};
+
+// 获取缓存数据
+const getCachedData = (key: string): ApiResponse | null => {
+  const cachedItem = cache[key];
+  if (!cachedItem) return null;
+  
+  const now = Date.now();
+  if (now - cachedItem.timestamp > CACHE_EXPIRATION) {
+    delete cache[key];
+    return null;
+  }
+  
+  return cachedItem.data;
+};
+
+// 设置缓存数据
+const setCachedData = (key: string, data: ApiResponse): void => {
+  cache[key] = {
+    data,
+    timestamp: Date.now()
+  };
+};
+
 // 获取热门文章列表
 export const fetchHotArticles = async (lang: number, pageSize: number): Promise<ApiResponse> => {
   try {
-    const response = await axios.get<ApiResponse>(
-      'http://localhost:48080/ship-api/cms/article/portal/page-list',
-      {
-        params: {
-          lang,
-          'type.remark': 'hot',
-          pageSize
-        }
-      }
-    );
+    const baseUrl = import.meta.env.VITE_SHIP_API_BASE_URL || 'http://localhost:48080';
+    const apiPath = import.meta.env.VITE_SHIP_API_PATH || '/ship-api';
+    const url = `${baseUrl}${apiPath}/cms/article/portal/page-list`;
+    const params = {
+      lang,
+      'type.remark': 'hot',
+      pageSize
+    };
+    
+    // 生成缓存键
+    const cacheKey = generateCacheKey(url, params);
+    
+    // 检查缓存
+    const cachedData = getCachedData(cacheKey);
+    if (cachedData) {
+      return cachedData;
+    }
+    
+    // 发起API请求
+    const response = await axios.get<ApiResponse>(url, { params });
+    
+    // 缓存响应数据
+    setCachedData(cacheKey, response.data);
+    
     return response.data;
   } catch (error) {
     console.error('Failed to fetch hot articles:', error);
@@ -65,15 +122,29 @@ export const extractHeroImagesFromArticles = (articlesData: ApiResponse): string
 // 获取视频内容
 export const fetchVideoContent = async (lang: number): Promise<ApiResponse> => {
   try {
-    const response = await axios.get<ApiResponse>(
-      'http://localhost:48080/ship-api/cms/article/portal/page-list',
-      {
-        params: {
-          lang,
-          'type.id': '2001571782243946498'
-        }
-      }
-    );
+    const baseUrl = import.meta.env.VITE_SHIP_API_BASE_URL || 'http://localhost:48080';
+    const apiPath = import.meta.env.VITE_SHIP_API_PATH || '/ship-api';
+    const url = `${baseUrl}${apiPath}/cms/article/portal/page-list`;
+    const params = {
+      lang,
+      'type.id': '2001571782243946498'
+    };
+    
+    // 生成缓存键
+    const cacheKey = generateCacheKey(url, params);
+    
+    // 检查缓存
+    const cachedData = getCachedData(cacheKey);
+    if (cachedData) {
+      return cachedData;
+    }
+    
+    // 发起API请求
+    const response = await axios.get<ApiResponse>(url, { params });
+    
+    // 缓存响应数据
+    setCachedData(cacheKey, response.data);
+    
     return response.data;
   } catch (error) {
     console.error('Failed to fetch video content:', error);

+ 23 - 0
vite.config.ts

@@ -18,6 +18,29 @@ export default defineConfig(({ mode }) => {
         alias: {
           '@': path.resolve(__dirname, '.'),
         }
+      },
+      build: {
+        // 启用代码分割
+        rollupOptions: {
+          output: {
+            manualChunks: {
+              // 将第三方库打包到单独的chunk中
+              'react-vendor': ['react', 'react-dom', 'react-router-dom'],
+              'utils': ['axios', 'qs'],
+              'icons': ['lucide-react']
+            }
+          }
+        },
+        // 启用压缩
+        minify: 'esbuild',
+        // 生成source map (生产环境可选)
+        sourcemap: false,
+        // 启用CSS代码分割
+        cssCodeSplit: true,
+        // 启用图片优化
+        assetsInlineLimit: 4096, // 小于4KB的资源内联
+        // 调整chunk大小警告阈值
+        chunkSizeWarningLimit: 1000
       }
     };
 });