소스 검색

1.主页轮播图添加API请求后端

chenhg 1 일 전
부모
커밋
3321692dee
4개의 변경된 파일219개의 추가작업 그리고 1개의 파일을 삭제
  1. 30 0
      src/contexts/ThemeContext.tsx
  2. 120 0
      src/pages/Guide.tsx
  3. 6 1
      src/pages/Home.tsx
  4. 63 0
      src/services/articleService.ts

+ 30 - 0
src/contexts/ThemeContext.tsx

@@ -87,6 +87,36 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) =
     return saved ? JSON.parse(saved) : HERO_IMAGES;
   });
 
+  // Fetch hero images from API when component mounts
+  useEffect(() => {
+    const loadHeroImagesFromApi = async () => {
+      try {
+        // Import the functions dynamically to avoid circular dependencies
+        const { fetchHotArticles, extractHeroImagesFromArticles } =
+            await import('../services/articleService.ts');
+        
+        // Fetch hot articles from the API
+        const articlesData = await fetchHotArticles(1, 6);
+        
+        // Extract hero images from the articles' coverUrl field
+        const apiHeroImages = extractHeroImagesFromArticles(articlesData);
+        
+        // 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
+      } catch (error) {
+        console.error('Failed to fetch hero images from API:', error);
+        // Fallback to existing images if API fails
+      }
+    };
+
+    loadHeroImagesFromApi();
+  }, []); // Empty dependency array means this runs only once when component mounts
+
   // --- Content Sections ---
   const [itineraries, setItineraries] = useState<Itinerary[]>(() => {
     const saved = localStorage.getItem('vista_itineraries');

+ 120 - 0
src/pages/Guide.tsx

@@ -0,0 +1,120 @@
+import React, { useEffect } from 'react';
+import Navbar from '@/src/components/Navbar.tsx';
+import Sidebar from '@/src/components/Sidebar.tsx';
+import Footer from '@/src/components/Footer.tsx';
+import { CONTENT } from '../constants.ts';
+import { useLanguage } from '@/src/contexts/LanguageContext.tsx';
+import { useTheme } from '@/src/contexts/ThemeContext.tsx';
+import { useLocation } from 'react-router-dom';
+
+const Guide: React.FC = () => {
+  const { language } = useLanguage();
+  const { guideHeroImage } = useTheme();
+  const location = useLocation();
+  const t = CONTENT[language];
+
+  useEffect(() => {
+    const params = new URLSearchParams(location.search);
+    const section = params.get('section');
+    
+    let elementId = '';
+    if (section === 'tips') {
+      elementId = 'guide-tips';
+    } else if (section === 'faq') {
+      elementId = 'guide-faq';
+    }
+    
+    if (elementId) {
+      setTimeout(() => {
+        const element = document.getElementById(elementId);
+        if (element) {
+          element.scrollIntoView({ behavior: 'smooth' });
+        }
+      }, 100);
+    }
+  }, [location.search]);
+
+  return (
+    <div className="min-h-screen bg-white font-sans text-vista-darkblue overflow-x-hidden">
+      <Navbar />
+      <Sidebar />
+
+      {/* Hero Section */}
+      <section className="relative h-[50vh] w-full overflow-hidden">
+        <div className="absolute inset-0 z-0">
+          <img 
+            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" 
+          />
+          <div className="absolute inset-0 bg-black/40 pointer-events-none"></div>
+        </div>
+        <div className="relative z-10 h-full flex flex-col justify-center items-center text-center px-4 drop-shadow-md">
+          <h1 className="text-white text-4xl md:text-6xl font-serif leading-tight italic">
+            {t.guide.hero.title}
+          </h1>
+          <p className="text-white text-xl md:text-2xl mt-4 max-w-2xl">
+            {t.guide.hero.subtitle}
+          </p>
+        </div>
+      </section>
+
+      {/* Main Content */}
+      <main className="py-20">
+        {/* Travel Tips Section */}
+        <section id="guide-tips" className="max-w-7xl mx-auto px-6 mb-24">
+          <div className="text-center mb-16">
+            <span className="text-vista-gold uppercase tracking-widest text-sm font-bold">{t.guide.tips.sectionTitle}</span>
+            <h2 className="text-4xl font-serif text-vista-darkblue mt-3">{t.guide.tips.sectionSubtitle}</h2>
+          </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>
+            ))}
+          </div>
+        </section>
+
+        {/* FAQ Section */}
+        <section id="guide-faq" className="py-24 bg-vista-teal">
+          <div className="max-w-7xl mx-auto px-6">
+            <div className="text-center mb-16">
+              <span className="text-white uppercase tracking-widest text-sm font-bold">{t.guide.faq.sectionTitle}</span>
+              <h2 className="text-4xl font-serif text-white mt-3">{t.guide.faq.sectionSubtitle}</h2>
+            </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>
+              ))}
+            </div>
+          </div>
+        </section>
+      </main>
+
+      <Footer />
+    </div>
+  );
+};
+
+export default Guide;

+ 6 - 1
src/pages/Home.tsx

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
 import Navbar from '@/src/components/Navbar.tsx';
 import Sidebar from '@/src/components/Sidebar.tsx';
 import Footer from '@/src/components/Footer.tsx';
-import { CONTENT } from '../constants.ts';
+import { CONTENT, HERO_IMAGES } from '../constants.ts';
 import { ChevronRight, Play } from 'lucide-react';
 import { useLanguage } from '@/src/contexts/LanguageContext.tsx';
 import { useTheme } from '@/src/contexts/ThemeContext.tsx';
@@ -45,6 +45,11 @@ const Home: React.FC = () => {
               src={img} 
               alt={`Slide ${index + 1}`} 
               className="w-full h-full object-cover object-center scale-105 animate-slow-zoom" 
+              onError={(e) => {
+                // Fallback to default image if API image fails to load
+                const target = e.target as HTMLImageElement;
+                target.src = HERO_IMAGES[Math.min(index, HERO_IMAGES.length - 1)];
+              }}
             />
             {/* Text Readability Overlay: Light black tint (30%) to contrast white text */}
             <div className="absolute inset-0 bg-black/30 pointer-events-none"></div>

+ 63 - 0
src/services/articleService.ts

@@ -0,0 +1,63 @@
+import axios from 'axios';
+
+// 定义API响应的类型接口
+interface Article {
+  id: string;
+  articleType: string;
+  title: string;
+  summary: string;
+  coverUrl: string;
+  sliderImage: string | null;
+  video: string | null;
+  content: string;
+  subContent: string | null;
+  orderNum: number;
+  status: string;
+  remark: string | null;
+  createTime: number;
+  lang: string;
+  relationList: any | null;
+}
+
+interface ApiResponse {
+  code: number;
+  data: {
+    list: Article[];
+    total: number;
+    totalDes: string | null;
+  };
+  msg: string;
+}
+
+// 获取热门文章列表
+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
+        }
+      }
+    );
+    return response.data;
+  } catch (error) {
+    console.error('Failed to fetch hot articles:', error);
+    throw error;
+  }
+};
+
+// 从文章数据中提取轮播图URL
+export const extractHeroImagesFromArticles = (articlesData: ApiResponse): string[] => {
+  // 检查API响应是否成功
+  if (articlesData.code !== 0 || !articlesData.data || !articlesData.data.list) {
+    return [];
+  }
+
+  // 从文章列表中提取coverUrl作为轮播图
+  return articlesData.data.list
+    .filter(article => article.coverUrl) // 确保coverUrl存在
+    .map(article => article.coverUrl);
+};