4 Commits d72c4c567b ... 5a1b823756

Author SHA1 Message Date
  chenhg 5a1b823756 1.文件性能优化 1 day ago
  chenhg fdb43d025b 1.新增主页视频后端维护 2 days ago
  chenhg a317bd7aaa 1.暂时不从缓存取轮播图 2 days ago
  chenhg 7f5537bbd0 1.优化出行攻略,分模块维护 2 days ago

+ 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


+ 21 - 0
src/components/FAQItem.tsx

@@ -0,0 +1,21 @@
+import React from 'react';
+
+export interface FAQItemProps {
+  question: string;
+  answer: string;
+}
+
+const FAQItem: React.FC<FAQItemProps> = ({ question, answer }) => {
+  return (
+    <div className="bg-white rounded-lg shadow-xl p-6">
+      <h3 className="text-xl font-serif text-vista-darkblue mb-3">
+        {question}
+      </h3>
+      <p className="text-vista-darkblue/70">
+        {answer}
+      </p>
+    </div>
+  );
+};
+
+export default FAQItem;

+ 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>

+ 28 - 0
src/components/TipsCategory.tsx

@@ -0,0 +1,28 @@
+import React from 'react';
+
+export interface TipsCategoryProps {
+  icon: string;
+  title: string;
+  items: string[];
+}
+
+const TipsCategory: React.FC<TipsCategoryProps> = ({ icon, title, items }) => {
+  return (
+    <div className="bg-white rounded-lg shadow-xl p-8 border border-vista-darkblue/10">
+      <h3 className="text-2xl font-serif text-vista-darkblue mb-6 flex items-center gap-3">
+        <span className="text-3xl">{icon}</span>
+        {title}
+      </h3>
+      <ul className="space-y-4 text-vista-darkblue/70">
+        {items.map((item, itemIndex) => (
+          <li key={itemIndex} className="flex items-start gap-3">
+            <span className="text-vista-gold font-bold mt-1">•</span>
+            <span>{item}</span>
+          </li>
+        ))}
+      </ul>
+    </div>
+  );
+};
+
+export default TipsCategory;

+ 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 }}>

+ 38 - 37
src/contexts/ThemeContext.tsx

@@ -82,33 +82,15 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) =
     return localStorage.getItem('vista_custom_footer_logo') || null;
   });
 
-  const [heroImages, setHeroImages] = useState<string[]>(() => {
-    const saved = localStorage.getItem('vista_hero_images');
-    return saved ? JSON.parse(saved) : HERO_IMAGES;
-  });
+  const [heroImages, setHeroImages] = useState<string[]>(HERO_IMAGES);
 
-  // Fetch hero images from API when component mounts
+  // Fetch hero images from API on every page refresh
   useEffect(() => {
-    // Check if we already have hero images in localStorage to avoid unnecessary API calls
-    const savedImages = localStorage.getItem('vista_hero_images');
-    if (savedImages) {
-      return; // Skip API call if we already have images saved
-    }
-
     const loadHeroImagesFromApi = async () => {
-      // Add a flag to prevent duplicate API calls in React Strict Mode
-      const apiCallFlag = 'vista_hero_images_api_called';
-      if (sessionStorage.getItem(apiCallFlag)) {
-        return; // Skip if we already made the call in this session
-      }
-
       try {
         // Import the functions dynamically to avoid circular dependencies
         const { fetchHotArticles, extractHeroImagesFromArticles } = 
             await import('../services/articleService.ts');
-        
-        // Mark that we're making the API call
-        sessionStorage.setItem(apiCallFlag, 'true');
 
         // Fetch hot articles from the API
         const articlesData = await fetchHotArticles(1, 6);
@@ -119,20 +101,46 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) =
         // If we got images from the API, use them
         if (apiHeroImages.length > 0) {
           setHeroImages(apiHeroImages);
-          // Save to localStorage so we don't fetch on every page load
-          localStorage.setItem('vista_hero_images', JSON.stringify(apiHeroImages));
         }
-        // Otherwise, keep the default HERO_IMAGES or saved images
+        // Otherwise, keep the default HERO_IMAGES
       } catch (error) {
         console.error('Failed to fetch hero images from API:', error);
-        // Fallback to existing images if API fails
-        // Clear the flag if the API call failed so we can try again
-        sessionStorage.removeItem('vista_hero_images_api_called');
+        // Fallback to default HERO_IMAGES if API fails
+        setHeroImages(HERO_IMAGES);
       }
     };
 
     loadHeroImagesFromApi();
-  }, []); // Empty dependency array means this runs only once when component mounts
+  }, []); // Empty dependency array means this runs on every component mount (page refresh)
+
+  // Fetch video content from API on every page refresh
+  useEffect(() => {
+    const loadVideoContentFromApi = async () => {
+      try {
+        // Import the functions dynamically to avoid circular dependencies
+        const { fetchVideoContent, extractVideoContentFromArticles } = 
+            await import('../services/articleService.ts');
+
+        // Fetch video content from the API (lang=1 for Chinese)
+        const videoData = await fetchVideoContent(1);
+        
+        // Extract video content from the articles data
+        const videoContent = extractVideoContentFromArticles(videoData);
+        
+        // If we got video content from the API, use it
+        if (videoContent) {
+          setVideoSection(videoContent);
+        }
+        // Otherwise, keep the default video content
+      } catch (error) {
+        console.error('Failed to fetch video content from API:', error);
+        // Fallback to default video content if API fails
+        setVideoSection(DEFAULT_VIDEO);
+      }
+    };
+
+    loadVideoContentFromApi();
+  }, []); // Empty dependency array means this runs on every component mount (page refresh)
 
   // --- Content Sections ---
   const [itineraries, setItineraries] = useState<Itinerary[]>(() => {
@@ -150,10 +158,7 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) =
     return saved ? JSON.parse(saved) : DEFAULT_DINING;
   });
 
-  const [videoSection, setVideoSection] = useState<VideoSectionContent>(() => {
-    const saved = localStorage.getItem('vista_video');
-    return saved ? JSON.parse(saved) : DEFAULT_VIDEO;
-  });
+  const [videoSection, setVideoSection] = useState<VideoSectionContent>(DEFAULT_VIDEO);
 
   const [shipsPageImages, setShipsPageImages] = useState<ShipsPageImages>(() => {
     const saved = localStorage.getItem('vista_ships_page_images');
@@ -171,9 +176,7 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) =
     else localStorage.removeItem('vista_custom_footer_logo');
   }, [customFooterLogo]);
 
-  useEffect(() => {
-    localStorage.setItem('vista_hero_images', JSON.stringify(heroImages));
-  }, [heroImages]);
+
 
   useEffect(() => {
     localStorage.setItem('vista_itineraries', JSON.stringify(itineraries));
@@ -187,9 +190,7 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) =
     localStorage.setItem('vista_dining', JSON.stringify(dining));
   }, [dining]);
 
-  useEffect(() => {
-    localStorage.setItem('vista_video', JSON.stringify(videoSection));
-  }, [videoSection]);
+
 
   useEffect(() => {
     localStorage.setItem('vista_ships_page_images', JSON.stringify(shipsPageImages));

+ 4 - 1
src/pages/AhoutUs.tsx

@@ -15,7 +15,6 @@ import TiktokQRCode from '@/src/assets/QRPicture/Tiktok-QR.png';
 import RedBookQRCode from '@/src/assets/QRPicture/redbook-QR.png';
 import CompanyLogo from '@/src/assets/logo/company.png';
 
-
 const AboutUs: React.FC = () => {
   const { language } = useLanguage();
   const { aboutHeroImage } = useTheme();
@@ -114,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>
@@ -135,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>
@@ -187,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">
@@ -235,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>

+ 26 - 32
src/pages/Guide.tsx

@@ -1,7 +1,9 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useLayoutEffect } from 'react';
 import Navbar from '@/src/components/Navbar.tsx';
 import Sidebar from '@/src/components/Sidebar.tsx';
 import Footer from '@/src/components/Footer.tsx';
+import TipsCategory from '@/src/components/TipsCategory.tsx';
+import FAQItem from '@/src/components/FAQItem.tsx';
 import { CONTENT } from '../constants.ts';
 import { useLanguage } from '@/src/contexts/LanguageContext.tsx';
 import { useTheme } from '@/src/contexts/ThemeContext.tsx';
@@ -13,7 +15,7 @@ const Guide: React.FC = () => {
   const location = useLocation();
   const t = CONTENT[language];
 
-  useEffect(() => {
+  useLayoutEffect(() => {
     const params = new URLSearchParams(location.search);
     const section = params.get('section');
     
@@ -25,12 +27,10 @@ const Guide: React.FC = () => {
     }
     
     if (elementId) {
-      setTimeout(() => {
-        const element = document.getElementById(elementId);
-        if (element) {
-          element.scrollIntoView({ behavior: 'smooth' });
-        }
-      }, 100);
+      const element = document.getElementById(elementId);
+      if (element) {
+        element.scrollIntoView({ behavior: 'smooth' });
+      }
     }
   }, [location.search]);
 
@@ -46,6 +46,11 @@ const Guide: React.FC = () => {
             src={guideHeroImage || "https://images.unsplash.com/photo-1578474843222-9593bc88d8b0?q=80&w=1920&auto=format&fit=crop"} 
             alt="Travel Guide" 
             className="w-full h-full object-cover object-center" 
+            loading="lazy"
+            onError={(e) => {
+              const target = e.target as HTMLImageElement;
+              target.src = "https://images.unsplash.com/photo-1578474843222-9593bc88d8b0?q=80&w=1920&auto=format&fit=crop";
+            }}
           />
           <div className="absolute inset-0 bg-black/40 pointer-events-none"></div>
         </div>
@@ -69,21 +74,13 @@ const Guide: React.FC = () => {
           </div>
           
           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
-            {t.guide.tips.categories.map((category, index) => (
-              <div key={index} className="bg-white rounded-lg shadow-xl p-8 border border-vista-darkblue/10">
-                <h3 className="text-2xl font-serif text-vista-darkblue mb-6 flex items-center gap-3">
-                  <span className="text-3xl">{category.icon}</span>
-                  {category.title}
-                </h3>
-                <ul className="space-y-4 text-vista-darkblue/70">
-                  {category.items.map((item, itemIndex) => (
-                    <li key={itemIndex} className="flex items-start gap-3">
-                      <span className="text-vista-gold font-bold mt-1">•</span>
-                      <span>{item}</span>
-                    </li>
-                  ))}
-                </ul>
-              </div>
+            {t.guide.tips.categories.map((category) => (
+              <TipsCategory 
+                key={category.title} 
+                icon={category.icon} 
+                title={category.title} 
+                items={category.items} 
+              />
             ))}
           </div>
         </section>
@@ -97,15 +94,12 @@ const Guide: React.FC = () => {
             </div>
             
             <div className="max-w-3xl mx-auto space-y-6">
-              {t.guide.faq.items.map((faq, index) => (
-                <div key={index} className="bg-white rounded-lg shadow-xl p-6">
-                  <h3 className="text-xl font-serif text-vista-darkblue mb-3">
-                    {faq.question}
-                  </h3>
-                  <p className="text-vista-darkblue/70">
-                    {faq.answer}
-                  </p>
-                </div>
+              {t.guide.faq.items.map((faq) => (
+                <FAQItem 
+                  key={faq.question} 
+                  question={faq.question} 
+                  answer={faq.answer} 
+                />
               ))}
             </div>
           </div>

+ 24 - 9
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>
@@ -214,14 +217,26 @@ const Home: React.FC = () => {
                   </div>
               </div>
               <div className="md:w-1/2 w-full relative group cursor-pointer">
-                  {/* Video Thumbnail Placeholder */}
+                  {/* Video Section */}
                   <div className="aspect-video w-full bg-black relative overflow-hidden shadow-2xl">
-                      <img src={videoSection.thumbnail} className="w-full h-full object-cover opacity-60 group-hover:opacity-40 transition-opacity" alt="Video Thumbnail" />
-                      <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" />
-                          </div>
-                      </div>
+                      {videoSection.video ? (
+                          <video 
+                              src={videoSection.video} 
+                              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" 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" />
+                                  </div>
+                              </div>
+                          </>
+                      )}
                   </div>
               </div>
           </div>

+ 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>

+ 126 - 10
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);
@@ -61,3 +118,62 @@ export const extractHeroImagesFromArticles = (articlesData: ApiResponse): string
     .filter(article => article.coverUrl) // 确保coverUrl存在
     .map(article => article.coverUrl);
 };
+
+// 获取视频内容
+export const fetchVideoContent = async (lang: number): Promise<ApiResponse> => {
+  try {
+    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);
+    throw error;
+  }
+};
+
+// 从文章数据中提取视频内容
+export const extractVideoContentFromArticles = (articlesData: ApiResponse): {
+  title: string;
+  titleItalic: string;
+  description: string;
+  thumbnail: string;
+  video?: string;
+} | null => {
+  // 检查API响应是否成功
+  if (articlesData.code !== 0 || !articlesData.data || !articlesData.data.list || articlesData.data.list.length === 0) {
+    return null;
+  }
+
+  // 获取第一篇文章作为视频内容
+  const videoArticle = articlesData.data.list[0];
+  
+  // 提取视频内容
+  return {
+    title: videoArticle.title || '',
+    titleItalic: '', // API中没有直接的titleItalic字段,可能需要从title中提取或使用默认值
+    description: videoArticle.summary || '',
+    thumbnail: videoArticle.coverUrl || '',
+    video: videoArticle.video || undefined
+  };
+};

+ 1 - 0
src/types.ts

@@ -54,6 +54,7 @@ export interface VideoSectionContent {
   titleItalic: string;
   description: string;
   thumbnail: string;
+  video?: string;
 }
 
 // New Interface for Ships Page Images

+ 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
       }
     };
 });