React 19 + Vite 6 构建现代化旅行应用智旅(1)

项目概述

智旅是一款基于React 19 + Vite 6构建的现代化移动端旅行应用,集成AI智能助手、用户认证系统、高质量图片内容和流畅的用户体验。项目技术栈先进,架构完善,是学习现代前端开发的优秀案例。

项目结构

csharp 复制代码
ZhiNen_trip/                           # 项目根目录
├── 📁 api/                            # Vercel无服务函数
│   ├── 📁 coze/                       # Coze AI工作流API
│   ├── 📁 doubao/                     # 豆包图像生成API  
│   └── 📁 pexels/                     # Pexels图片API
├── 📁 public/                         # 静态资源目录
│   └── 📄 vite.svg                    # Vite图标
├── 📁 src/                            # 源码目录
│   ├── 📁 api/                        # API接口封装
│   ├── 📁 assets/                     # 静态资源(图片/图标)
│   ├── 📁 components/                 # 组件库
│   │   ├── 📁 Business/               # 业务组件
│   │   ├── 📁 Dev/                    # 开发调试组件
│   │   ├── 📁 ErrorBoundary/          # 错误边界组件
│   │   ├── 📁 JWTDebugPanel/          # JWT调试面板
│   │   ├── 📁 JWTProvider/            # JWT认证提供者
│   │   ├── 📁 Layout/                 # 布局组件
│   │   ├── 📁 MainLayout/             # 主布局组件
│   │   ├── 📁 ProtectedRoute/         # 路由保护组件
│   │   ├── 📁 Providers/              # 全局状态提供者
│   │   ├── 📁 UI/                     # 通用UI组件
│   │   │   ├── 📁 EmptyState/         # 空状态组件
│   │   │   ├── 📁 LazyImage/          # 懒加载图片组件
│   │   │   ├── 📁 LoadingSpinner/     # 加载动画组件
│   │   │   ├── 📁 UserAvatar/         # 用户头像组件
│   │   │   └── 📄 index.js            # UI组件导出
│   │   ├── 📁 WaterfallLayout/        # 瀑布流布局组件
│   │   └── 📄 index.js                # 组件统一导出
│   ├── 📁 constants/                  # 常量配置
│   ├── 📁 contexts/                   # React上下文
│   ├── 📁 hooks/                      # 自定义Hook
│   ├── 📁 llm/                        # AI大模型相关
│   ├── 📁 pages/                      # 页面组件
│   │   ├── 📁 AI_chat/                # AI聊天页面
│   │   ├── 📁 Account/                # 个人中心页面
│   │   ├── 📁 Article/                # 旅记详情页面
│   │   ├── 📁 Flight/                 # 机票页面
│   │   ├── 📁 Home/                   # 首页
│   │   ├── 📁 Hotel/                  # 酒店页面
│   │   ├── 📁 Login/                  # 登录注册页面
│   │   ├── 📁 Search/                 # 搜索页面
│   │   ├── 📁 Taxi/                   # 打车页面
│   │   ├── 📁 Tourism/                # 旅游页面
│   │   ├── 📁 Train/                  # 火车票页面
│   │   ├── 📁 Trip/                   # 行程页面
│   │   ├── 📁 Welcome/                # 欢迎页面
│   │   └── 📁 WriteArticle/           # 写文章页面
│   ├── 📁 services/                   # 服务层
│   ├── 📁 stores/                     # 状态管理(Zustand)
│   ├── 📁 utils/                      # 工具函数
│   ├── 📄 App.css                     # 应用样式
│   ├── 📄 App.jsx                     # 应用根组件
│   ├── 📄 index.css                   # 全局样式
│   └── 📄 main.jsx                    # 应用入口
├── 📄 .gitignore                      # Git忽略配置
├── 📄 README.md                       # 项目说明文档
├── 📄 build-test.js                   # 构建测试脚本
├── 📄 eslint.config.js                # ESLint配置
├── 📄 index.html                      # HTML模板
├── 📄 package.json                    # 项目依赖配置
├── 📄 pnpm-lock.yaml                  # 依赖锁定文件
├── 📄 postcss.config.cjs              # PostCSS配置
├── 📄 start-dev.bat                   # Windows启动脚本
├── 📄 start-dev.ps1                   # PowerShell启动脚本
├── 📄 vercel.json                     # Vercel部署配置
└── 📄 vite.config.js                  # Vite构建配置

核心目录分析

/api/ - Vercel无服务函数

  • 专门用于Vercel部署的API路由
  • 安全处理第三方API密钥
  • 支持Coze、豆包、Pexels三大API服务

/src/components/ - 组件架构

  • UI/: 可复用的基础UI组件
  • Business/: 业务逻辑组件
  • Layout/: 布局相关组件
  • Providers/: 全局状态提供者组件

/src/pages/ - 页面模块

  • 采用功能模块化设计
  • 每个页面独立目录管理
  • 覆盖旅行应用完整功能链路

/src/stores/ - 状态管理

  • 基于Zustand的轻量级状态管理
  • 模块化状态设计
  • 支持状态持久化

架构特点

  1. 模块化设计: 功能模块清晰分离
  2. 组件复用: UI组件高度可复用
  3. 状态集中: 统一的状态管理方案
  4. API分层: 前端API封装 + 后端代理
  5. 工程化: 完善的构建和部署配置

项目背景与技术选型

json 复制代码
//package.json
{
  "name": "zhinen-trip",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "start": "vite",
    "build": "vite build",
    "build:analyze": "ANALYZE=true vite build",
    "build:test": "node build-test.js",
    "build:report": "vite build && npx serve dist",
    "preview": "vite preview",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "performance:test": "node -e \"import('./src/utils/performanceTest.js').then(m => m.quickPerformanceTest())\"",
    "performance:mid": "node -e \"import('./src/utils/midPriorityTest.js').then(m => m.quickMidPriorityTest())\"",
    "size:analyze": "vite build --mode analyze"
  },
  "dependencies": {
    "@react-vant/icons": "^0.1.0",
    "axios": "^1.11.0",
    "js-cookie": "^3.0.5",
    "jsonwebtoken": "^9.0.2",
    "lib-flexible": "^0.3.2",
    "prop-types": "^15.8.1",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-router-dom": "^7.7.1",
    "react-vant": "^3.3.5",
    "zustand": "^5.0.7"
  },
  "devDependencies": {
    "@eslint/js": "^9.25.0",
    "@types/react": "^19.1.2",
    "@types/react-dom": "^19.1.2",
    "@vitejs/plugin-react": "^4.4.1",
    "eslint": "^9.25.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.19",
    "globals": "^16.0.0",
    "postcss": "^8.4.47",
    "autoprefixer": "^10.4.20",
    "postcss-pxtorem": "^6.1.0",
    "rollup-plugin-visualizer": "^6.0.3",
    "terser": "^5.43.1",
    "vite": "^6.3.5",
    "vite-plugin-chunk-split": "^0.5.0",
    "vite-plugin-mock": "^3.0.2"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

核心框架版本策略

React 19.1.0 - 采用最新稳定版本,获得最新特性和性能优化,如并发渲染、自动批处理等新特性。

React Router 7.7.1 - 使用最新版本,支持新的路由特性和更好的类型安全。

UI组件库策略

React Vant 3.3.5 - 选择移动端优化的组件库,适合构建移动应用,版本相对保守但稳定。

状态管理策略

Zustand 5.0.7 - 选择轻量级状态管理方案,相比Redux更简洁,版本较新但已稳定。

构建工具策略

Vite 6.3.5 - 使用最新版本获得最快的开发体验和构建性能。

ESLint 9.25.0 - 保持代码质量工具的最新版本。

移动端适配策略

lib-flexible + postcss-pxtorem - 传统但成熟的移动端适配方案,确保兼容性。

应用根组件路由配置,展示页面级组件懒加载

jsx 复制代码
// app.jsx
import { lazy, Suspense } from 'react'
import {
  Routes,
  Route,
  Navigate
} from 'react-router-dom'
import { LoadingSpinner } from '@/components/UI'
import ZustandProvider from '@/components/Providers/ZustandProvider'
import { ProtectedRoute } from '@/components'
import ErrorBoundary from '@/components/ErrorBoundary'
import JWTProvider from '@/components/JWTProvider'


import { useRoutePreloader } from '@/hooks/useRoutePreloader'
import './App.css'

// 页面组件懒加载
const MainLayout = lazy(() => import('@/components/Layout/MainLayout'))
const Login = lazy(() => import('@/pages/Login'))
const Home = lazy(() => import('@/pages/Home'))
const Article = lazy(() => import('@/pages/Article'))
const WriteArticle = lazy(() => import('@/pages/WriteArticle'))
const Trip = lazy(() => import('@/pages/Trip'))
const Account = lazy(() => import('@/pages/Account'))
const Search = lazy(() => import('@/pages/Search'))
const Hotel = lazy(() => import('@/pages/Hotel'))
const Flight = lazy(() => import('@/pages/Flight'))
const Train = lazy(() => import('@/pages/Train'))
const Taxi = lazy(() => import('@/pages/Taxi'))
const Tourism = lazy(() => import('@/pages/Tourism'))
const Coze = lazy(() => import('@/pages/AI_chat/coze'))

// 主应用组件包装器
const AppContent = () => {
  // 启用路由预加载(保持后台运行,但不暴露到全局)
  useRoutePreloader()

  return (
    <>
      <Suspense fallback={
        <LoadingSpinner 
          type="ball" 
          size="medium"
          text="正在加载..."
          fullScreen={true}
        />
      }>
        <Routes>
          {/* 登录页面 - 不需要认证 */}
          <Route path='/login' element={<Login />} />
          
          {/* 主应用布局 - 混合权限控制 */}
          <Route element={<MainLayout />}>
            <Route path='/' element={<Navigate to="/home" />} />
            {/* 首页和旅记页 - 未登录也可访问 */}
            <Route path='/home' element={<Home />} />
            <Route path='/article' element={<Article />} />
            {/* 行程页和我的页 - 需要认证 */}
            <Route path='/trip' element={
              <ProtectedRoute>
                <Trip />
              </ProtectedRoute>
            } />
            <Route path='/account' element={
              <ProtectedRoute>
                <Account />
              </ProtectedRoute>
            } />
          </Route>
          
          {/* 独立页面 - 需要认证 */}
          <Route path='/write-article' element={
            <ProtectedRoute>
              <WriteArticle />
            </ProtectedRoute>
          } />
          <Route path='/search' element={
            <ProtectedRoute>
              <Search />
            </ProtectedRoute>
          } />
          <Route path='/hotel' element={
            <ProtectedRoute>
              <Hotel />
            </ProtectedRoute>
          } />
          <Route path='/flight' element={
            <ProtectedRoute>
              <Flight />
            </ProtectedRoute>
          } />
          <Route path='/train' element={
            <ProtectedRoute>
              <Train />
            </ProtectedRoute>
          } />
          <Route path='/taxi' element={
            <ProtectedRoute>
              <Taxi />
            </ProtectedRoute>
          } />
          <Route path='/tourism' element={
            <ProtectedRoute>
              <Tourism />
            </ProtectedRoute>
          } />
          <Route path='/coze' element={
            <ProtectedRoute>
              <Coze />
            </ProtectedRoute>
          } />
          
          {/* 404页面重定向到首页 */}
          <Route path='*' element={<Navigate to="/" replace />} />
        </Routes>
      </Suspense>
      

    </>
  )
}

function App() {
  return (
    <ErrorBoundary fallbackMessage="智旅应用遇到了问题,我们正在努力修复">
      <ZustandProvider>
        <JWTProvider>
          <AppContent />
        </JWTProvider>
      </ZustandProvider>
    </ErrorBoundary>
  )
}

export default App

架构设计

分层架构:使用多个 Provider 层次化管理应用状态

  • ErrorBoundary:全局错误处理
  • ZustandProvider:状态管理
  • JWTProvider:JWT 认证管理

核心功能

懒加载优化 :所有页面组件都采用 lazy() 动态导入,配合 Suspense 提供加载状态,提升应用性能

混合权限控制

  • 公开页面:登录页、首页、旅记页(未登录也可访问)
  • 受保护页面:使用 ProtectedRoute 组件包装,需要认证后才能访问

路由结构

  • 主布局路由:包含导航的页面(首页、旅记、行程、我的)
  • 独立页面:全屏显示的功能页面(搜索、预订、AI聊天等)

页面分类

  • 内容页面:首页、旅记、写文章
  • 功能页面:搜索、个人账户
  • 预订服务:酒店、机票、火车票、出租车、旅游
  • AI功能:Coze 聊天页面

用户体验优化

  • 使用 useRoutePreloader 钩子预加载路由
  • 统一的加载动画(球形加载器)
  • 404 页面自动重定向到首页
  • 全局错误边界处理异常情况

Zustand状态管理架构,轻量级状态持久化方案

认证状态管理

js 复制代码
// src/stores/authStore.js
/**
 * 认证状态管理 - Zustand实现 + JWT集成
 */

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { getRandomAvatar } from '../api/pexels'
import { generateTravelAvatar } from '../api'
import { 
  generateJWT, 
  verifyJWT, 
  parseJWT, 
  tokenManager, 
  getUserFromToken,
  getTokenRemainingTime 
} from '../utils/jwt'

const useAuthStore = create(
  persist(
    (set, get) => ({
      // 状态
      user: null,
      isAuthenticated: false,
      isLoading: false,
      token: null,
      tokenExpiresIn: 0,

      // Actions
      /**
       * 初始化认证状态 - JWT版本
       */
      initializeAuth: () => {
        set({ isLoading: true })
        
        try {
          // 优先使用JWT token
          const jwtToken = tokenManager.getToken()
          
          if (jwtToken && verifyJWT(jwtToken)) {
            // JWT token有效,从token中提取用户信息
            const userData = getUserFromToken(jwtToken)
            const remainingTime = getTokenRemainingTime(jwtToken)
            
            console.log('✅ JWT认证初始化成功:', userData)
            console.log('🕐 Token剩余时间:', Math.floor(remainingTime / 60), '分钟')
            
            set({
              user: userData,
              token: jwtToken,
              tokenExpiresIn: remainingTime,
              isAuthenticated: true,
              isLoading: false
            })
            
            // 启动自动刷新机制
            tokenManager.startAutoRefresh(jwtToken)
          } else {
            // JWT token无效,尝试从旧的localStorage读取并迁移
            const storedUser = localStorage.getItem('zhilvUser')
            const oldToken = localStorage.getItem('zhilvToken')
            
            if (storedUser && oldToken) {
              console.log('🔄 检测到旧版token,正在迁移到JWT...')
              const userData = JSON.parse(storedUser)
              
              // 生成新的JWT token
              const newJwtToken = generateJWT({
                id: userData.id,
                username: userData.username,
                email: userData.email,
                nickname: userData.nickname,
                avatar: userData.avatar,
                phone: userData.phone,
                preferences: userData.preferences
              })
              
              // 保存新的JWT token
              tokenManager.setToken(newJwtToken)
              
              // 清除旧的存储
              localStorage.removeItem('zhilvUser')
              localStorage.removeItem('zhilvToken')
              
              set({
                user: userData,
                token: newJwtToken,
                tokenExpiresIn: getTokenRemainingTime(newJwtToken),
                isAuthenticated: true,
                isLoading: false
              })
              
              console.log('✅ 成功迁移到JWT认证')
            } else {
              // 没有任何有效的认证信息
              set({ 
                user: null,
                token: null,
                tokenExpiresIn: 0,
                isAuthenticated: false, 
                isLoading: false 
              })
            }
          }
        } catch (error) {
          console.error('初始化认证状态失败:', error)
          // 清除所有认证数据
          tokenManager.removeToken()
          localStorage.removeItem('zhilvUser')
          localStorage.removeItem('zhilvToken')
          
          set({ 
            user: null,
            token: null,
            tokenExpiresIn: 0,
            isAuthenticated: false, 
            isLoading: false 
          })
        }
      },

      /**
       * 登录 - JWT版本
       * @param {Object} credentials - 登录凭据
       * @returns {Promise<Object>} 登录结果
       */
      login: async (credentials) => {
        set({ isLoading: true })
        
        try {
          const { username, password } = credentials
          
          if (username && password) {
            // 生成随机头像
            const avatar = await getRandomAvatar()
            
            const userData = {
              id: Date.now(),
              username: username,
              email: username.includes('@') ? username : `${username}@zhilv.com`,
              avatar: avatar,
              phone: '',
              nickname: username,
              createTime: new Date().toISOString(),
              lastLoginTime: new Date().toISOString(),
              preferences: {
                favoriteDestinations: [],
                interests: [],
                travelStyle: ''
              }
            }
            
            // 生成JWT token(24小时有效期)
            const jwtToken = generateJWT(userData, 24 * 60 * 60)
            const tokenExpiresIn = getTokenRemainingTime(jwtToken)
            
            // 使用JWT token管理器保存
            tokenManager.setToken(jwtToken)
            
            console.log('✅ JWT登录成功:', userData)
            console.log('🎫 生成的JWT Token长度:', jwtToken.length)
            console.log('🕐 Token有效期:', Math.floor(tokenExpiresIn / 3600), '小时')
            
            set({
              user: userData,
              token: jwtToken,
              tokenExpiresIn: tokenExpiresIn,
              isAuthenticated: true,
              isLoading: false
            })
            
            return { success: true, user: userData, token: jwtToken }
          } else {
            throw new Error('用户名和密码不能为空')
          }
        } catch (error) {
          console.error('登录失败:', error)
          set({ 
            isLoading: false,
            token: null,
            tokenExpiresIn: 0
          })
          return { success: false, error: error.message }
        }
      },

      /**
       * 注册 - JWT版本
       * @param {Object} registrationData - 注册数据
       * @returns {Promise<Object>} 注册结果
       */
      register: async (registrationData) => {
        set({ isLoading: true })
        
        try {
          const { username, password, phone } = registrationData
          
          if (!username || !password) {
            throw new Error('用户名和密码不能为空')
          }
          
          // 检查是否已存在用户(简化版本,实际项目应该调用后端API)
          const existingToken = tokenManager.getToken()
          if (existingToken && verifyJWT(existingToken)) {
            const existingUser = getUserFromToken(existingToken)
            if (existingUser && existingUser.username === username) {
              throw new Error('用户名已存在')
            }
          }
          
          // 生成随机头像
          const avatar = await getRandomAvatar()
          
          const userData = {
            id: Date.now(),
            username: username,
            email: `${username}@zhilv.com`,
            phone: phone || '',
            avatar: avatar,
            nickname: username,
            createTime: new Date().toISOString(),
            lastLoginTime: new Date().toISOString(),
            preferences: {
              favoriteDestinations: [],
              interests: [],
              travelStyle: ''
            }
          }
          
          // 生成JWT token(24小时有效期)
          const jwtToken = generateJWT(userData, 24 * 60 * 60)
          const tokenExpiresIn = getTokenRemainingTime(jwtToken)
          
          // 使用JWT token管理器保存
          tokenManager.setToken(jwtToken)
          
          console.log('✅ JWT注册成功:', userData)
          console.log('🎫 生成的JWT Token长度:', jwtToken.length)
          
          set({
            user: userData,
            token: jwtToken,
            tokenExpiresIn: tokenExpiresIn,
            isAuthenticated: true,
            isLoading: false
          })
          
          return { success: true, user: userData, token: jwtToken }
        } catch (error) {
          console.error('注册失败:', error)
          set({ 
            isLoading: false,
            token: null,
            tokenExpiresIn: 0
          })
          return { success: false, error: error.message }
        }
      },

      /**
       * 登出 - JWT版本
       */
      logout: () => {
        console.log('🔄 AuthStore: 开始执行JWT logout')
        
        // 使用JWT token管理器清除
        tokenManager.removeToken()
        
        // 清除旧版localStorage数据(兼容性)
        localStorage.removeItem('zhilvUser')
        localStorage.removeItem('zhilvToken')
        localStorage.removeItem('userExtendedInfo')
        
        // 重置状态
        set({
          user: null,
          token: null,
          tokenExpiresIn: 0,
          isAuthenticated: false,
          isLoading: false
        })
        
        console.log('✅ AuthStore: JWT logout完成,状态已重置')
      },

      /**
       * 更新用户信息 - JWT版本
       * @param {Object} updatedData - 更新的用户数据
       * @returns {Object} 更新结果
       */
      updateUser: (updatedData) => {
        const { user, token } = get()
        
        if (user && token) {
          const updatedUser = { ...user, ...updatedData }
          
          // 生成新的JWT token包含更新后的用户信息
          const newJwtToken = generateJWT(updatedUser, 24 * 60 * 60)
          const tokenExpiresIn = getTokenRemainingTime(newJwtToken)
          
          // 更新token管理器
          tokenManager.setToken(newJwtToken)
          
          set({ 
            user: updatedUser,
            token: newJwtToken,
            tokenExpiresIn: tokenExpiresIn
          })
          
          console.log('✅ 用户信息已更新,JWT token已刷新')
          
          return { success: true, user: updatedUser, token: newJwtToken }
        }
        
        return { success: false, error: '用户未登录' }
      },

      /**
       * 生成AI头像
       * @returns {Promise<Object>} 生成结果
       */
      generateAvatar: async () => {
        const { user } = get()
        
        if (!user) return { success: false, error: '用户未登录' }
        
        try {
          // 基于用户信息生成个性化提示词
          const userPrompt = `friendly ${user.nickname || user.username}, travel enthusiast, outdoor adventurer`
          
          // 使用豆包API生成AI旅行头像
          const result = await generateTravelAvatar(userPrompt)
          
          if (result.success) {
            const updatedUser = { ...user, avatar: result.url }
            localStorage.setItem('zhilvUser', JSON.stringify(updatedUser))
            
            set({ user: updatedUser })
            
            return { 
              success: true, 
              avatar: result.url,
              prompt: result.prompt,
              isAI: true 
            }
          } else {
            // 降级使用Pexels随机头像
            console.warn('豆包AI生成失败,使用Pexels随机头像')
            const fallbackUrl = await getRandomAvatar()
            
            const updatedUser = { ...user, avatar: fallbackUrl }
            localStorage.setItem('zhilvUser', JSON.stringify(updatedUser))
            
            set({ user: updatedUser })
            
            return { 
              success: true, 
              avatar: fallbackUrl,
              fallback: true,
              error: result.error 
            }
          }
        } catch (error) {
          console.error('生成头像失败:', error)
          
          // 最终降级方案
          try {
            const fallbackUrl = await getRandomAvatar()
            const updatedUser = { ...user, avatar: fallbackUrl }
            localStorage.setItem('zhilvUser', JSON.stringify(updatedUser))
            
            set({ user: updatedUser })
            
            return { 
              success: true, 
              avatar: fallbackUrl,
              fallback: true,
              error: error.message 
            }
          } catch (fallbackError) {
            return { success: false, error: fallbackError.message }
          }
        }
      },

      /**
       * 设置加载状态
       * @param {boolean} loading - 加载状态
       */
      setLoading: (loading) => {
        set({ isLoading: loading })
      },

      /**
       * 刷新JWT Token
       * @returns {boolean} 刷新是否成功
       */
      refreshToken: () => {
        const { token } = get()
        
        if (token && verifyJWT(token)) {
          const newToken = refreshJWT(token)
          
          if (newToken) {
            const userData = getUserFromToken(newToken)
            const tokenExpiresIn = getTokenRemainingTime(newToken)
            
            tokenManager.setToken(newToken)
            
            set({
              user: userData,
              token: newToken,
              tokenExpiresIn: tokenExpiresIn
            })
            
            console.log('✅ JWT Token手动刷新成功')
            return true
          }
        }
        
        console.warn('❌ JWT Token手动刷新失败')
        return false
      },

      /**
       * 获取Token状态信息
       * @returns {Object} Token状态
       */
      getTokenStatus: () => {
        const { token, tokenExpiresIn } = get()
        
        if (!token) {
          return { hasToken: false, isValid: false, remainingTime: 0 }
        }
        
        const isValid = verifyJWT(token)
        const remainingTime = getTokenRemainingTime(token)
        
        return {
          hasToken: true,
          isValid,
          remainingTime,
          expiresAt: new Date(Date.now() + remainingTime * 1000).toLocaleString(),
          isExpiringSoon: remainingTime < 900 // 15分钟内过期
        }
      },

      /**
       * 验证当前认证状态
       * @returns {boolean} 是否有效认证
       */
      validateAuth: () => {
        const { token, isAuthenticated } = get()
        
        if (!isAuthenticated || !token) {
          return false
        }
        
        if (!verifyJWT(token)) {
          // Token无效,清除认证状态
          get().logout()
          return false
        }
        
        return true
      }
    }),
    {
      name: 'auth-storage', // 本地存储的key
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({
        // 注意:JWT token由tokenManager单独管理,不在这里持久化
        // 只持久化必要的状态信息
        user: state.user,
        isAuthenticated: state.isAuthenticated
      })
    }
  )
)

export default useAuthStore

核心功能: 完整的用户认证和授权状态管理

主要特性:

  1. JWT集成认证:
  • 支持JWT token生成、验证和自动刷新
  • 从旧版localStorage迁移到JWT的兼容处理
  • Token过期时间管理和监控
  1. 用户状态管理:
  • 用户信息存储和更新
  • 登录/注册/登出流程
  • 头像生成和更新(集成豆包API)
  1. 持久化存储:
  • 使用Zustand persist中间件
  • 安全的token存储策略
  • 跨会话状态保持
  1. 安全特性:
  • 自动token刷新机制
  • 认证状态同步
  • 错误处理和降级策略

瀑布流状态管理

js 复制代码
// /src/stores/waterfallStore.js
/**
 * 瀑布流状态管理 - Zustand实现
 * 管理瀑布流数据、加载状态等
 */

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { getGuidePhotos } from '../services/pexelsApi'

const useWaterfallStore = create(
  devtools(
    (set, get) => ({
      // 状态
      items: [],
      loading: false,
      initialLoading: true,
      hasMore: true,
      page: 1,
      error: null,

      // 批处理相关状态
      batchQueue: [],
      batchProcessing: false,
      lastLoadTime: 0,
      loadingLock: false,

      // Actions
      /**
       * 设置项目数据
       * @param {Array} items - 项目数组
       */
      setItems: (items) => {
        set({ items }, false, 'setItems')
      },

      /**
       * 添加项目到列表
       * @param {Array} newItems - 新项目数组
       */
      addItems: (newItems) => {
        set((state) => ({
          items: [...state.items, ...newItems]
        }), false, 'addItems')
      },

      /**
       * 设置加载状态
       * @param {boolean} loading - 加载状态
       */
      setLoading: (loading) => {
        set({ loading }, false, 'setLoading')
      },

      /**
       * 设置初始加载状态
       * @param {boolean} initialLoading - 初始加载状态
       */
      setInitialLoading: (initialLoading) => {
        set({ initialLoading }, false, 'setInitialLoading')
      },

      /**
       * 设置是否有更多数据
       * @param {boolean} hasMore - 是否有更多数据
       */
      setHasMore: (hasMore) => {
        set({ hasMore }, false, 'setHasMore')
      },

      /**
       * 设置当前页码
       * @param {number} page - 页码
       */
      setPage: (page) => {
        set({ page }, false, 'setPage')
      },

      /**
       * 设置错误信息
       * @param {string|null} error - 错误信息
       */
      setError: (error) => {
        set({ error }, false, 'setError')
      },

      /**
       * 设置加载锁
       * @param {boolean} locked - 是否锁定
       */
      setLoadingLock: (locked) => {
        set({ loadingLock: locked }, false, 'setLoadingLock')
      },

      /**
       * 更新最后加载时间
       */
      updateLastLoadTime: () => {
        set({ lastLoadTime: Date.now() }, false, 'updateLastLoadTime')
      },

      /**
       * 添加到批处理队列
       * @param {Array} items - 要添加的项目
       */
      addToBatchQueue: (items) => {
        set((state) => ({
          batchQueue: [...state.batchQueue, ...items]
        }), false, 'addToBatchQueue')
      },

      /**
       * 清空批处理队列
       */
      clearBatchQueue: () => {
        set({ batchQueue: [] }, false, 'clearBatchQueue')
      },

      /**
       * 设置批处理状态
       * @param {boolean} processing - 是否正在处理
       */
      setBatchProcessing: (processing) => {
        set({ batchProcessing: processing }, false, 'setBatchProcessing')
      },

      /**
       * 从批处理队列取出一批数据
       * @param {number} batchSize - 批次大小
       * @returns {Array} 一批数据
       */
      takeFromBatchQueue: (batchSize = 5) => {
        const { batchQueue } = get()
        const batch = batchQueue.slice(0, batchSize)
        
        set((state) => ({
          batchQueue: state.batchQueue.slice(batchSize)
        }), false, 'takeFromBatchQueue')
        
        return batch
      },

      /**
       * 检查是否应该加载
       * @returns {boolean} 是否应该加载
       */
      shouldLoad: () => {
        const { loading, loadingLock, lastLoadTime } = get()
        const now = Date.now()
        
        // 多重检查:正在加载、加载锁、距离上次加载时间太短
        if (loading || loadingLock || (now - lastLoadTime < 500)) {
          return false
        }
        
        return true
      },

      /**
       * 加载数据
       * @param {number} pageNum - 页码
       * @param {boolean} isLoadMore - 是否是加载更多
       * @returns {Promise<boolean>} 加载是否成功
       */
      loadData: async (pageNum = 1, isLoadMore = false) => {
        const state = get()
        
        if (!state.shouldLoad()) {
          console.log('跳过加载:条件不满足')
          return false
        }
        
        // 设置加载状态和锁
        set({
          loading: true,
          loadingLock: true,
          error: null
        }, false, 'loadData_start')
        
        get().updateLastLoadTime()
        
        try {
          // 加载数据
          const newItems = await getGuidePhotos(12, pageNum)
          
          if (newItems && newItems.length > 0) {
            // 为每个项目添加随机高度类型
            const itemsWithHeight = newItems.map(item => ({
              ...item,
              heightType: item.heightType || (
                Math.random() > 0.7 ? 'tall' : 
                Math.random() > 0.4 ? 'medium' : 'short'
              )
            }))
            
            if (!isLoadMore) {
              // 首次加载,清空现有数据并直接设置新数据
              set({ 
                items: itemsWithHeight,
                initialLoading: false 
              }, false, 'loadData_firstLoad')
            } else {
              // 加载更多时,添加到现有数据
              set((state) => ({
                items: [...state.items, ...itemsWithHeight]
              }), false, 'loadData_loadMore')
            }
            
            // 更新页码
            set({ page: pageNum + 1 }, false, 'loadData_updatePage')
            
            // 检查是否还有更多数据
            const isDefaultData = newItems.some(item => item.id && item.id.includes('default'))
            if (isLoadMore && newItems.length < 12 && !isDefaultData) {
              set({ hasMore: false }, false, 'loadData_noMore')
            }
            
            return true
          } else {
            set({ 
              hasMore: false,
              initialLoading: false 
            }, false, 'loadData_noData')
            return false
          }
        } catch (error) {
          console.error('加载瀑布流数据失败:', error)
          set({ 
            error: error.message,
            hasMore: false,
            initialLoading: false 
          }, false, 'loadData_error')
          return false
        } finally {
          set({ 
            loading: false,
            loadingLock: false 
          }, false, 'loadData_end')
        }
      },

      /**
       * 处理批次数据 - 简化版本
       * @param {Array} batch - 批次数据
       * @param {boolean} isLoadMore - 是否是加载更多
       */
      processBatch: async (batch, isLoadMore = false) => {
        // 简化逻辑,直接添加数据
        if (isLoadMore) {
          get().addItems(batch)
        } else {
          get().setItems(batch)
        }
        
        // 短暂延迟等待UI更新
        await new Promise(resolve => setTimeout(resolve, 100))
      },

      /**
       * 处理批处理队列
       * @param {boolean} isLoadMore - 是否是加载更多
       */
      processBatchQueue: async (isLoadMore = false) => {
        const { batchProcessing, batchQueue } = get()
        
        if (batchProcessing || batchQueue.length === 0) {
          return
        }
        
        set({ batchProcessing: true }, false, 'processBatchQueue_start')
        
        try {
          while (get().batchQueue.length > 0) {
            // 取出一批数据
            const batch = get().takeFromBatchQueue(5)
            
            // 处理这一批
            await get().processBatch(batch, isLoadMore)
            
            // 批次间延迟
            if (get().batchQueue.length > 0) {
              await new Promise(resolve => setTimeout(resolve, 150))
            }
          }
        } finally {
          set({ batchProcessing: false }, false, 'processBatchQueue_end')
        }
      },

      /**
       * 初始化数据 - 简化版本
       */
      initialize: async () => {
        console.log('🔄 WaterfallStore: 开始初始化')
        
        // 重置所有状态
        set({
          items: [],
          loading: false,
          initialLoading: true,
          hasMore: true,
          page: 1,
          error: null,
          batchQueue: [],
          batchProcessing: false,
          lastLoadTime: 0,
          loadingLock: false
        }, false, 'initialize')
        
        // 开始加载数据
        await get().loadData(1, false)
      },

      /**
       * 加载更多数据
       */
      loadMore: () => {
        const { hasMore, loading, loadingLock } = get()
        
        if (!loading && !loadingLock && hasMore) {
          const { page } = get()
          get().loadData(page, true)
        }
      },

      /**
       * 重置状态
       */
      reset: () => {
        set({
          items: [],
          loading: false,
          initialLoading: true,
          hasMore: true,
          page: 1,
          error: null,
          batchQueue: [],
          batchProcessing: false,
          lastLoadTime: 0,
          loadingLock: false
        }, false, 'reset')
      }
    }),
    {
      name: 'waterfall-store', // DevTools中显示的名称
      enabled: process.env.NODE_ENV === 'development' // 只在开发环境启用DevTools
    }
  )
)

export default useWaterfallStore

核心功能: 管理瀑布流组件的数据和交互状态

主要特性:

  1. 数据管理:
  • 图片列表的增量加载
  • 分页和无限滚动支持
  • 加载状态和错误处理
  1. 性能优化:
  • 批处理队列机制
  • 加载防重复锁
  • 时间戳控制和节流
  1. 交互状态:
  • 初始加载状态管理
  • 更多数据检测
  • 用户操作反馈
  1. 集成Pexels API:
  • 调用getGuidePhotos获取图片
  • 错误降级到Mock数据
  • 数据格式标准化

主题状态管理

js 复制代码
// src/stores/themeStore.js
/**
 * 主题状态管理 - Zustand实现
 * 管理应用主题、颜色方案等
 */

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

const useThemeStore = create(
  persist(
    (set, get) => ({
      // 状态
      theme: 'light', // 'light' | 'dark' | 'auto'
      primaryColor: '#6FE164',
      secondaryColor: '#70E3DC',
      fontSize: 'medium', // 'small' | 'medium' | 'large'
      colorScheme: 'default', // 'default' | 'blue' | 'green' | 'purple'

      // Actions
      /**
       * 设置主题
       * @param {string} theme - 主题类型
       */
      setTheme: (theme) => {
        set({ theme })
        
        // 应用到document
        if (theme === 'dark') {
          document.documentElement.setAttribute('data-theme', 'dark')
        } else if (theme === 'light') {
          document.documentElement.setAttribute('data-theme', 'light')
        } else {
          // auto - 根据系统主题
          const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
          document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light')
        }
      },

      /**
       * 设置主色调
       * @param {string} color - 主色调
       */
      setPrimaryColor: (color) => {
        set({ primaryColor: color })
        document.documentElement.style.setProperty('--primary-color', color)
      },

      /**
       * 设置次色调
       * @param {string} color - 次色调
       */
      setSecondaryColor: (color) => {
        set({ secondaryColor: color })
        document.documentElement.style.setProperty('--secondary-color', color)
      },

      /**
       * 设置字体大小
       * @param {string} size - 字体大小
       */
      setFontSize: (size) => {
        set({ fontSize: size })
        
        const sizeMap = {
          small: '14px',
          medium: '16px',
          large: '18px'
        }
        
        document.documentElement.style.setProperty('--base-font-size', sizeMap[size])
      },

      /**
       * 设置配色方案
       * @param {string} scheme - 配色方案
       */
      setColorScheme: (scheme) => {
        set({ colorScheme: scheme })
        
        const colorSchemes = {
          default: {
            primary: '#6FE164',
            secondary: '#70E3DC'
          },
          blue: {
            primary: '#1976D2',
            secondary: '#42A5F5'
          },
          green: {
            primary: '#388E3C',
            secondary: '#66BB6A'
          },
          purple: {
            primary: '#7B1FA2',
            secondary: '#AB47BC'
          }
        }
        
        const colors = colorSchemes[scheme] || colorSchemes.default
        get().setPrimaryColor(colors.primary)
        get().setSecondaryColor(colors.secondary)
      },

      /**
       * 重置主题设置
       */
      resetTheme: () => {
        set({
          theme: 'light',
          primaryColor: '#6FE164',
          secondaryColor: '#70E3DC',
          fontSize: 'medium',
          colorScheme: 'default'
        })
        
        // 重置CSS变量
        document.documentElement.setAttribute('data-theme', 'light')
        document.documentElement.style.setProperty('--primary-color', '#6FE164')
        document.documentElement.style.setProperty('--secondary-color', '#70E3DC')
        document.documentElement.style.setProperty('--base-font-size', '16px')
      },

      /**
       * 初始化主题
       */
      initTheme: () => {
        const { theme, primaryColor, secondaryColor, fontSize } = get()
        
        // 应用保存的主题设置
        get().setTheme(theme)
        get().setPrimaryColor(primaryColor)
        get().setSecondaryColor(secondaryColor)
        get().setFontSize(fontSize)
      },

      /**
       * 切换暗色模式
       */
      toggleDarkMode: () => {
        const { theme } = get()
        const newTheme = theme === 'dark' ? 'light' : 'dark'
        get().setTheme(newTheme)
      }
    }),
    {
      name: 'theme-storage',
      storage: createJSONStorage(() => localStorage)
    }
  )
)

export default useThemeStore

核心功能: 应用主题、配色和视觉效果的统一管理

主要特性:

  1. 主题系统:
  • 亮色/暗色/自动主题切换
  • 系统主题检测和跟随
  • CSS变量动态更新
  1. 配色方案:
  • 预设4种配色方案(默认/蓝色/绿色/紫色)
  • 主色调和次色调自定义
  • 实时颜色预览
  1. 字体设置:
  • 三种字体大小(小/中/大)
  • 动态字体大小应用
  • 无障碍访问支持
  1. 持久化存储:
  • 主题设置本地保存
  • 应用启动时自动恢复
  • 跨设备同步

应用设置状态管理

js 复制代码
// src/stores/appStore.js
/**
 * 应用设置状态管理 - Zustand实现
 * 管理应用级别的配置和状态
 */

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

const useAppStore = create(
  persist(
    (set, get) => ({
      // 状态
      isFirstVisit: true,
      showWelcomeGuide: true,
      enableAnimations: true,
      enableNotifications: true,
      enableSoundEffects: false,
      dataUsageMode: 'normal', // 'normal' | 'saving'
      language: 'zh-CN',
      lastActiveTime: Date.now(),
      routeHistory: [],
      bookmarks: [],
      searchHistory: [],

      // 性能相关设置
      imageQuality: 'medium', // 'low' | 'medium' | 'high'
      enableLazyLoading: true,
      enableCaching: true,
      preloadImages: true,

      // Actions
      /**
       * 设置首次访问状态
       * @param {boolean} isFirst - 是否首次访问
       */
      setFirstVisit: (isFirst) => {
        set({ isFirstVisit: isFirst })
      },

      /**
       * 设置欢迎引导显示
       * @param {boolean} show - 是否显示
       */
      setShowWelcomeGuide: (show) => {
        set({ showWelcomeGuide: show })
      },

      /**
       * 切换动画效果
       */
      toggleAnimations: () => {
        set((state) => ({ enableAnimations: !state.enableAnimations }))
      },

      /**
       * 切换通知
       */
      toggleNotifications: () => {
        set((state) => ({ enableNotifications: !state.enableNotifications }))
      },

      /**
       * 切换音效
       */
      toggleSoundEffects: () => {
        set((state) => ({ enableSoundEffects: !state.enableSoundEffects }))
      },

      /**
       * 设置数据使用模式
       * @param {string} mode - 数据使用模式
       */
      setDataUsageMode: (mode) => {
        set({ dataUsageMode: mode })
      },

      /**
       * 设置语言
       * @param {string} lang - 语言代码
       */
      setLanguage: (lang) => {
        set({ language: lang })
      },

      /**
       * 更新最后活跃时间
       */
      updateLastActiveTime: () => {
        set({ lastActiveTime: Date.now() })
      },

      /**
       * 添加路由历史
       * @param {string} route - 路由路径
       */
      addRouteHistory: (route) => {
        set((state) => {
          const newHistory = [route, ...state.routeHistory.filter(r => r !== route)]
          return {
            routeHistory: newHistory.slice(0, 20) // 保留最近20条记录
          }
        })
      },

      /**
       * 添加书签
       * @param {Object} bookmark - 书签对象
       */
      addBookmark: (bookmark) => {
        set((state) => {
          const exists = state.bookmarks.some(b => b.id === bookmark.id)
          if (!exists) {
            return {
              bookmarks: [...state.bookmarks, { ...bookmark, createdAt: Date.now() }]
            }
          }
          return state
        })
      },

      /**
       * 移除书签
       * @param {string} bookmarkId - 书签ID
       */
      removeBookmark: (bookmarkId) => {
        set((state) => ({
          bookmarks: state.bookmarks.filter(b => b.id !== bookmarkId)
        }))
      },

      /**
       * 添加搜索历史
       * @param {string} query - 搜索查询
       */
      addSearchHistory: (query) => {
        if (!query || query.trim() === '') return
        
        set((state) => {
          const newHistory = [query, ...state.searchHistory.filter(q => q !== query)]
          return {
            searchHistory: newHistory.slice(0, 10) // 保留最近10条搜索记录
          }
        })
      },

      /**
       * 清除搜索历史
       */
      clearSearchHistory: () => {
        set({ searchHistory: [] })
      },

      /**
       * 设置图片质量
       * @param {string} quality - 图片质量
       */
      setImageQuality: (quality) => {
        set({ imageQuality: quality })
      },

      /**
       * 切换懒加载
       */
      toggleLazyLoading: () => {
        set((state) => ({ enableLazyLoading: !state.enableLazyLoading }))
      },

      /**
       * 切换缓存
       */
      toggleCaching: () => {
        set((state) => ({ enableCaching: !state.enableCaching }))
      },

      /**
       * 切换图片预加载
       */
      togglePreloadImages: () => {
        set((state) => ({ preloadImages: !state.preloadImages }))
      },

      /**
       * 获取性能设置
       * @returns {Object} 性能设置对象
       */
      getPerformanceSettings: () => {
        const { imageQuality, enableLazyLoading, enableCaching, preloadImages, dataUsageMode } = get()
        return {
          imageQuality,
          enableLazyLoading,
          enableCaching,
          preloadImages,
          dataUsageMode
        }
      },

      /**
       * 重置应用设置
       */
      resetAppSettings: () => {
        set({
          enableAnimations: true,
          enableNotifications: true,
          enableSoundEffects: false,
          dataUsageMode: 'normal',
          language: 'zh-CN',
          imageQuality: 'medium',
          enableLazyLoading: true,
          enableCaching: true,
          preloadImages: true
        })
      },

      /**
       * 清除用户数据(保留设置)
       */
      clearUserData: () => {
        set({
          routeHistory: [],
          bookmarks: [],
          searchHistory: [],
          lastActiveTime: Date.now()
        })
      },

      /**
       * 完全重置应用状态
       */
      fullReset: () => {
        set({
          isFirstVisit: true,
          showWelcomeGuide: true,
          enableAnimations: true,
          enableNotifications: true,
          enableSoundEffects: false,
          dataUsageMode: 'normal',
          language: 'zh-CN',
          lastActiveTime: Date.now(),
          routeHistory: [],
          bookmarks: [],
          searchHistory: [],
          imageQuality: 'medium',
          enableLazyLoading: true,
          enableCaching: true,
          preloadImages: true
        })
      }
    }),
    {
      name: 'app-storage',
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({
        isFirstVisit: state.isFirstVisit,
        showWelcomeGuide: state.showWelcomeGuide,
        enableAnimations: state.enableAnimations,
        enableNotifications: state.enableNotifications,
        enableSoundEffects: state.enableSoundEffects,
        dataUsageMode: state.dataUsageMode,
        language: state.language,
        lastActiveTime: state.lastActiveTime,
        bookmarks: state.bookmarks,
        searchHistory: state.searchHistory,
        imageQuality: state.imageQuality,
        enableLazyLoading: state.enableLazyLoading,
        enableCaching: state.enableCaching,
        preloadImages: state.preloadImages
      })
    }
  )
)

export default useAppStore

核心功能: 应用级别的配置、用户偏好和系统设置管理

主要特性:

  1. 用户体验设置:
  • 首次访问引导
  • 动画效果开关
  • 音效和通知控制
  1. 性能配置:
  • 图片质量设置(低/中/高)
  • 懒加载开关
  • 缓存策略控制
  • 预加载配置
  1. 用户行为追踪:
  • 路由历史记录(最近20条)
  • 搜索历史管理
  • 书签收藏功能
  • 最后活跃时间
  1. 数据管理:
  • 数据使用模式(普通/节省)
  • 语言设置(国际化支持)
  • 清理和重置功能

统一导出入口

js 复制代码
// src/stores/index.js
/**
 * Zustand Store 统一导出
 * 方便在应用中使用各种状态管理
 */

// 认证状态管理
export { default as useAuthStore } from './authStore'

// 瀑布流状态管理  
export { default as useWaterfallStore } from './waterfallStore'

// 应用主题状态管理
export { default as useThemeStore } from './themeStore'

// 应用设置状态管理
export { default as useAppStore } from './appStore'

功能: 作为 stores 模块的统一导出点,方便在应用中导入和使用各种状态管理特点:

  • 导出4个核心状态管理 store
  • 简化导入路径,提供一致的API接口
  • 便于后续扩展和维护

移动端适配方案

lib-flexible初始化,动态rem计算机制

jsx 复制代码
// src/main.jsx
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import 'lib-flexible'  // 移动端适配
import {
  BrowserRouter as Router,
} from 'react-router-dom'

createRoot(document.getElementById('root')).render(
  
    <Router>
      <App />
    </Router>
)

PostCSS配置详解,px到rem的自动转换规则

cjs 复制代码
// postcss.config.cjs
module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-pxtorem': {
      rootValue: 37.5, // 与 lib-flexible 配合,375 设计稿
      unitPrecision: 5,
      propList: ['*'],
      selectorBlackList: ['.no-rem'],
      replace: true,
      mediaQuery: false,
      minPixelValue: 2
    }
  }
}

viewport meta标签配置,移动端视窗适配

html 复制代码
// index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    // 双击放大功能移除
    <title>智旅</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

开发环境搭建

Vite 6 高级配置,别名设置、代理配置、构建优化

js 复制代码
//vite.config.js
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path' // vite 工程化工具 node
import { visualizer } from 'rollup-plugin-visualizer'
import { chunkSplitPlugin } from 'vite-plugin-chunk-split'

// https://vite.dev/config/
export default defineConfig(({ command, mode }) => {
  // 加载环境变量
  const env = loadEnv(mode, process.cwd(), '')
  
  return {
    plugins: [
      react(),
      
       // 智能代码分割插件 
       chunkSplitPlugin({
         strategy: 'split-by-experience',
         customSplitting: {
           'react-vendor': ['react', 'react-dom'],
           'router-vendor': ['react-router-dom'],
           'ui-vendor': ['react-vant', '@react-vant/icons'],
           'utils-vendor': ['axios', 'zustand'],
           'api-vendor': ['@api']
         }
       }),
      
      // 构建分析插件(仅在分析模式或构建时启用)
      ...(mode === 'analyze' || (command === 'build' && process.env.ANALYZE) ? [
        visualizer({
          filename: 'dist/stats.html',
          open: true,
          gzipSize: true,
          brotliSize: true,
          template: 'treemap' // 可选: treemap, sunburst, network
        })
      ] : [])
    ],
    resolve: {
      // 别名
      alias: {
        '@': path.resolve(__dirname, './src'),
        '@components': path.resolve(__dirname, './src/components'),
        '@pages': path.resolve(__dirname, './src/pages'),
        '@contexts': path.resolve(__dirname, './src/contexts'),
        '@constants': path.resolve(__dirname, './src/constants'),
        '@api': path.resolve(__dirname, './src/api'),
        '@utils': path.resolve(__dirname, './src/utils'),
        '@hooks': path.resolve(__dirname, './src/hooks'),
        '@styles': path.resolve(__dirname, './src/styles'),
      },
    },
    server: {
      // 预热常用文件
      warmup: {
        clientFiles: [
          './src/App.jsx',
          './src/main.jsx',
          './src/components/**/*.jsx',
          './src/pages/**/*.jsx'
        ]
      },
      proxy: {
        // 代理Doubao API请求
        "/api/doubao": {
          target: "https://ark.cn-beijing.volces.com",
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api\/doubao/, "/api"),
          configure: (proxy, options) => {
            proxy.on("proxyReq", (proxyReq, req, res) => {
              // 安全地添加API密钥到请求头
              const apiKey = env.VITE_DOUBAO_IMAGE_API_KEY;
              
              if (apiKey && apiKey.trim() !== '') {
                proxyReq.setHeader("Authorization", `Bearer ${apiKey}`);
                console.log('✅ 代理请求已添加Authorization头,图像生成模型: ep-20250804182253-ckvjk');
                console.log('🔗 目标URL:', req.url);
                console.log('📊 请求方法:', req.method);
              } else {
                console.error('❌ 未找到VITE_DOUBAO_IMAGE_API_KEY环境变量或为空');
                console.error('请在.env.local文件中设置: VITE_DOUBAO_IMAGE_API_KEY=your-api-key');
              }
            });
          },
        },
        // 代理Coze API请求
        '/api/coze': {
          target: 'https://api.coze.cn',
          changeOrigin: true,
          secure: false,
          rewrite: (path) => {
            // 根据不同的路径使用不同的重写规则
            if (path.includes('/api/coze/workflow')) {
              return path.replace(/^\/api\/coze\/workflow/, '/v1/workflow');
            } else {
              return path.replace(/^\/api\/coze/, '/api/v1');
            }
          },
          headers: {
            'Origin': 'https://api.coze.cn',
            'Referer': 'https://api.coze.cn/'
          },
          configure: (proxy, options) => {
            proxy.on('error', (err, req, res) => {
              console.log('❌ Coze代理错误:', err);
            });
            proxy.on('proxyReq', (proxyReq, req, res) => {
              // 安全地添加PAT Token到请求头
              const patToken = env.VITE_PAT_TOKEN;
              
              if (patToken && patToken.trim() !== '') {
                proxyReq.setHeader('Authorization', `Bearer ${patToken}`);
                console.log('✅ Coze代理请求已添加Authorization头');
                console.log('🔗 目标URL:', req.url);
                console.log('📊 请求方法:', req.method);
              } else {
                console.error('❌ 未找到VITE_PAT_TOKEN环境变量或为空');
                console.error('请在.env.local文件中设置: VITE_PAT_TOKEN=your-pat-token');
              }
            });
            proxy.on('proxyRes', (proxyRes, req, res) => {
              console.log('📥 Coze代理响应:', req.url, proxyRes.statusCode);
            });
          }
        },
      },
    },

    // 构建优化配置
    build: {
      // 目标浏览器
      target: 'es2015',
      
      // 启用 CSS 代码分割
      cssCodeSplit: true,
      
      // 构建后的文件大小报告
      reportCompressedSize: true,
      
      // 块大小警告限制 (KB)
      chunkSizeWarningLimit: 1000,
      
      // Rollup 配置
      rollupOptions: {
        output: {
          // 手动配置代码分割
          manualChunks(id) {
            // 将 node_modules 中的包分割到 vendor chunk
            if (id.includes('node_modules')) {
              // React 相关
              if (id.includes('react') || id.includes('react-dom')) {
                return 'react-vendor'
              }
              
              // 路由相关
              if (id.includes('react-router')) {
                return 'router-vendor'
              }
              
              // UI 库
              if (id.includes('react-vant') || id.includes('@react-vant')) {
                return 'ui-vendor'
              }
              
              // 工具库
              if (id.includes('axios') || id.includes('zustand')) {
                return 'utils-vendor'
              }
              
              // 其他第三方库
              return 'vendor'
            }
          },
          
          // 文件命名规则
          chunkFileNames: (chunkInfo) => {
            const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/').pop() : 'chunk'
            return `js/[name]-[hash].js`
          },
          entryFileNames: 'js/[name]-[hash].js',
          assetFileNames: (assetInfo) => {
            const extType = assetInfo.name.split('.').pop()
            
            // 根据文件类型分类存放
            if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
              return `images/[name]-[hash].[ext]`
            }
            if (/woff2?|eot|ttf|otf/i.test(extType)) {
              return `fonts/[name]-[hash].[ext]`
            }
            return `assets/[name]-[hash].[ext]`
          }
        }
      },
      
      // 压缩配置
      minify: 'terser',
      terserOptions: {
        compress: {
          // 生产环境移除 console 和 debugger
          drop_console: command === 'build',
          drop_debugger: command === 'build',
          // 移除未使用的代码
          pure_funcs: command === 'build' ? ['console.log', 'console.info'] : []
        },
        mangle: {
          // 混淆变量名
          safari10: true
        }
      }
    },

    // 预构建优化
    optimizeDeps: {
      include: [
        'react',
        'react-dom',
        'react-router-dom',
        'react-vant',
        '@react-vant/icons',
        'axios',
        'zustand'
      ],
      exclude: ['@api'] // 排除我们的API模块,让它保持动态导入
    },



    // 开发环境性能优化
    esbuild: {
      // 开发环境保留 console,生产环境移除
      drop: command === 'build' ? ['console', 'debugger'] : []
    }
  }
})

ESLint9.x新版配置,React专用规则配置

js 复制代码
// eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'

export default [
  { ignores: ['dist'] },
  {
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...js.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  },
]

跨平台开发脚本,自动环境检测

bash 复制代码
// start-dev.bat/.ps1
@echo off
echo 启动智旅开发服务器...
cd /d "%~dp0"
echo 当前目录: %CD%
npm run dev
pause

Q1:为什么选择React 19 而不是Vue 3?

  1. 并发性优势: React 19 的并发渲染在移动端有显著性能提升,特别是在处理大量图片的瀑布流场景下,能够避免主线程阻塞。
  2. 生态系统成熟:React-Vant提供了完整的移动端组件库,相比Vue 3的移动端方案更加成熟。

Q2: Vite 6相比Webpack有什么优势?

  1. 开发启动速度: 冷启动时间从Webpack的30s降低到3s
  2. 热更新性能: 文件修改后的热更新响应时间在100ms以内
  3. 构建体积优化: 通过Tree Shaking和代码分割,最终bundle体积减少40%
  4. 现代浏览器优化: 原生支持ES模块,减少了转译开销

Q3: 如何解决移动端1px边框问题?

  1. PostCSS自动转换 : 通过postcss-pxtorem将设计稿的1px自动转换为0.026667rem
  2. 伪元素缩放 : 在关键UI组件中使用::before伪元素配合transform: scale(0.5)
  3. border-image: 对于复杂边框使用CSS的border-image属性
  4. 动态适配: 结合lib-flexible根据设备像素比动态调整

未完待续...

相关推荐
北'辰32 分钟前
DeepSeek智能考试系统智能体
前端·后端·架构·开源·github·deepseek
前端历劫之路1 小时前
🔥 1.30 分!我的 JS 库 Mettle.js 杀入全球性能榜,紧追 Vue
前端·javascript·vue.js
爱敲代码的小旗2 小时前
Webpack 5 高性能配置方案
前端·webpack·node.js
Murray的菜鸟笔记2 小时前
【Vue Router】路由模式、懒加载、守卫、权限、缓存
前端·vue router
苏格拉没有底了3 小时前
由频繁创建3D火焰造成的内存泄漏问题
前端
阿彬爱学习3 小时前
大模型在垂直场景的创新应用:搜索、推荐、营销与客服新玩法
前端·javascript·easyui
橙序员小站3 小时前
通过trae开发你的第一个Chrome扩展插件
前端·javascript·后端
Lazy_zheng3 小时前
一文掌握:JavaScript 数组常用方法的手写实现
前端·javascript·面试
是晓晓吖3 小时前
关于Chrome Extension option的一些小事
前端·chrome
MrSkye3 小时前
🔥从菜鸟到高手:彻底搞懂 JavaScript 事件循环只需这一篇(下)
前端·javascript·面试