项目概述
智旅是一款基于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的轻量级状态管理
- 模块化状态设计
- 支持状态持久化
架构特点
- 模块化设计: 功能模块清晰分离
- 组件复用: UI组件高度可复用
- 状态集中: 统一的状态管理方案
- API分层: 前端API封装 + 后端代理
- 工程化: 完善的构建和部署配置
项目背景与技术选型
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
核心功能: 完整的用户认证和授权状态管理
主要特性:
- JWT集成认证:
- 支持JWT token生成、验证和自动刷新
- 从旧版localStorage迁移到JWT的兼容处理
- Token过期时间管理和监控
- 用户状态管理:
- 用户信息存储和更新
- 登录/注册/登出流程
- 头像生成和更新(集成豆包API)
- 持久化存储:
- 使用Zustand persist中间件
- 安全的token存储策略
- 跨会话状态保持
- 安全特性:
- 自动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
核心功能: 管理瀑布流组件的数据和交互状态
主要特性:
- 数据管理:
- 图片列表的增量加载
- 分页和无限滚动支持
- 加载状态和错误处理
- 性能优化:
- 批处理队列机制
- 加载防重复锁
- 时间戳控制和节流
- 交互状态:
- 初始加载状态管理
- 更多数据检测
- 用户操作反馈
- 集成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
核心功能: 应用主题、配色和视觉效果的统一管理
主要特性:
- 主题系统:
- 亮色/暗色/自动主题切换
- 系统主题检测和跟随
- CSS变量动态更新
- 配色方案:
- 预设4种配色方案(默认/蓝色/绿色/紫色)
- 主色调和次色调自定义
- 实时颜色预览
- 字体设置:
- 三种字体大小(小/中/大)
- 动态字体大小应用
- 无障碍访问支持
- 持久化存储:
- 主题设置本地保存
- 应用启动时自动恢复
- 跨设备同步
应用设置状态管理
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
核心功能: 应用级别的配置、用户偏好和系统设置管理
主要特性:
- 用户体验设置:
- 首次访问引导
- 动画效果开关
- 音效和通知控制
- 性能配置:
- 图片质量设置(低/中/高)
- 懒加载开关
- 缓存策略控制
- 预加载配置
- 用户行为追踪:
- 路由历史记录(最近20条)
- 搜索历史管理
- 书签收藏功能
- 最后活跃时间
- 数据管理:
- 数据使用模式(普通/节省)
- 语言设置(国际化支持)
- 清理和重置功能
统一导出入口
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?
- 并发性优势: React 19 的并发渲染在移动端有显著性能提升,特别是在处理大量图片的瀑布流场景下,能够避免主线程阻塞。
- 生态系统成熟:React-Vant提供了完整的移动端组件库,相比Vue 3的移动端方案更加成熟。
Q2: Vite 6相比Webpack有什么优势?
- 开发启动速度: 冷启动时间从Webpack的30s降低到3s
- 热更新性能: 文件修改后的热更新响应时间在100ms以内
- 构建体积优化: 通过Tree Shaking和代码分割,最终bundle体积减少40%
- 现代浏览器优化: 原生支持ES模块,减少了转译开销
Q3: 如何解决移动端1px边框问题?
- PostCSS自动转换 : 通过
postcss-pxtorem
将设计稿的1px自动转换为0.026667rem - 伪元素缩放 : 在关键UI组件中使用
::before
伪元素配合transform: scale(0.5)
- border-image: 对于复杂边框使用CSS的border-image属性
- 动态适配: 结合lib-flexible根据设备像素比动态调整
未完待续...