From ce324c9bb51d1d5eba2948a20afe03397b916a1a Mon Sep 17 00:00:00 2001 From: echo Date: Wed, 5 Nov 2025 17:24:55 +0800 Subject: [PATCH] feat: update --- .env.development | 4 + .env.example | 27 +- .env.production | 4 + .prettierignore | 55 ++++ .prettierrc.json | 15 + README.md | 91 ++++-- app.json | 5 +- app/(tabs)/_layout.tsx | 3 +- app/(tabs)/demo.tsx | 125 ++++---- app/(tabs)/index.tsx | 43 ++- app/(tabs)/paper.tsx | 31 +- app/_layout.tsx | 26 +- components/EditScreenInfo.tsx | 12 +- constants/network.ts | 38 +++ docs/LIBRARIES.md | 56 ++-- docs/USAGE_EXAMPLES.md | 3 +- eas.json | 1 - metro.config.js | 19 ++ package.json | 14 +- pnpm-lock.yaml | 490 +++++++++++++++++++++++++++- scripts/proxy-server.js | 59 ++++ src/hooks/useDebounce.ts | 2 - src/hooks/useHaptics.ts | 9 +- src/hooks/useRequest.ts | 291 +++++++++++++++++ src/hooks/useThrottle.ts | 5 +- src/index.ts | 14 +- src/schemas/auth.ts | 31 +- src/schemas/user.ts | 12 +- src/services/appService.ts | 39 +++ src/services/authService.ts | 25 +- src/services/userService.ts | 17 +- src/stores/settingsStore.ts | 7 +- src/stores/userStore.ts | 1 - src/types/api.ts | 73 +++++ src/types/index.ts | 1 - src/utils/api.ts | 144 --------- src/utils/common.ts | 0 src/utils/config.ts | 130 ++++++++ src/utils/date.ts | 1 - src/utils/network/api.ts | 589 ++++++++++++++++++++++++++++++++++ src/utils/storage.ts | 1 - tsconfig.json | 11 +- 42 files changed, 2077 insertions(+), 447 deletions(-) create mode 100644 .env.development create mode 100644 .env.production create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 constants/network.ts create mode 100644 metro.config.js create mode 100644 scripts/proxy-server.js create mode 100644 src/hooks/useRequest.ts create mode 100644 src/services/appService.ts create mode 100644 src/types/api.ts delete mode 100644 src/utils/api.ts create mode 100644 src/utils/common.ts create mode 100644 src/utils/config.ts create mode 100644 src/utils/network/api.ts diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..f2a414e --- /dev/null +++ b/.env.development @@ -0,0 +1,4 @@ +# 开发环境配置 +EXPO_PUBLIC_API_URL=/ +EXPO_PUBLIC_API_TIMEOUT=10000 + diff --git a/.env.example b/.env.example index c49ff80..1ab56cd 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,34 @@ +# 环境变量示例文件 +# 复制此文件为 .env 并填入实际值 + +# ============================================ # API 配置 -EXPO_PUBLIC_API_URL=https://api.example.com +# ============================================ + +# API 基础 URL +# 开发环境推荐使用相对路径 "/" 配合代理服务器 +# 生产环境根据实际情况配置: +# - 如果前后端同域:使用 "/" +# - 如果前后端分离:使用完整 URL "https://api.yourdomain.com/api" +EXPO_PUBLIC_API_URL=/ + +# API 请求超时时间(毫秒) +EXPO_PUBLIC_API_TIMEOUT=10000 +# ============================================ # 应用信息 +# ============================================ + EXPO_PUBLIC_APP_NAME=RN Demo EXPO_PUBLIC_APP_VERSION=1.0.0 -# 其他配置 +# ============================================ +# 其他配置(可选) +# ============================================ + +# Sentry 错误追踪 # EXPO_PUBLIC_SENTRY_DSN= + +# 分析工具 ID # EXPO_PUBLIC_ANALYTICS_ID= diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..c6ee707 --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +# 生产环境配置 +EXPO_PUBLIC_API_URL=/ +EXPO_PUBLIC_API_TIMEOUT=10000 + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..22aa4c9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,55 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Build outputs +.expo +.expo-shared +dist +build +*.tsbuildinfo + +# Cache +.cache +.parcel-cache +.next +.nuxt + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# Environment +.env +.env.local +.env.*.local + +# Generated files +coverage +.nyc_output + +# Lock files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Misc +*.min.js +*.min.css +public + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0f536b2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "bracketSpacing": true, + "endOfLine": "lf", + "jsxSingleQuote": false, + "bracketSameLine": false, + "quoteProps": "as-needed", + "proseWrap": "preserve" +} diff --git a/README.md b/README.md index 83fb0b6..27c3c43 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,19 @@ pnpm start #### 方法 2:使用模拟器 **Android 模拟器:** + ```bash pnpm android ``` **iOS 模拟器(仅 macOS):** + ```bash pnpm ios ``` **Web 浏览器:** + ```bash pnpm web ``` @@ -156,12 +159,15 @@ rn-demo/ ### 关键目录说明 #### 📱 `app/` - 路由和页面 + - **`app/_layout.tsx`** - 应用启动时自动检查更新 - **`app/(tabs)/index.tsx`** - 热更新演示页面 - **`app/(tabs)/demo.tsx`** - 完整示例页面,展示所有工具的使用 🎯 #### 💻 `src/` - 源代码目录 🎯 + 项目的核心业务逻辑代码,包含: + - **`utils/`** - 工具函数(API、存储、日期) - **`stores/`** - 状态管理(用户、设置) - **`schemas/`** - 数据验证(认证、用户) @@ -171,7 +177,9 @@ rn-demo/ - **`index.ts`** - 统一导出所有模块 #### 📚 `docs/` - 项目文档 🎯 + 完善的项目文档,包含: + - **使用指南** - 如何使用各个工具 - **代码示例** - 实际的代码示例 - **配置说明** - 项目配置详解 @@ -179,12 +187,14 @@ rn-demo/ ### 核心文件说明 #### 热更新相关 ⭐ + - **`app.json`** - Expo 配置,包含热更新设置 - **`eas.json`** - EAS 构建和更新通道配置 - **`app/_layout.tsx`** - 自动检查更新逻辑 - **`app/(tabs)/index.tsx`** - 手动检查更新功能 #### 工具配置 🎯 + - **`src/index.ts`** - 统一导出,从这里导入所有工具 - **`.env.example`** - 环境变量配置示例 - **`tsconfig.json`** - 配置了路径别名 `@/*` @@ -193,17 +203,17 @@ rn-demo/ 项目已安装并配置好以下工具库: -| 类别 | 工具库 | 用途 | -|------|--------|------| -| **工具类** | lodash-es | JavaScript 工具函数 | -| | dayjs | 日期处理 | -| | axios | HTTP 请求 | -| **状态管理** | zustand | 轻量级状态管理 | -| **表单处理** | react-hook-form | 表单管理 | -| | zod | 数据验证 | -| **原生功能** | @react-native-async-storage/async-storage | 本地存储 | -| | expo-image | 优化的图片组件 | -| | expo-haptics | 触觉反馈 | +| 类别 | 工具库 | 用途 | +| ------------ | ----------------------------------------- | ------------------- | +| **工具类** | lodash-es | JavaScript 工具函数 | +| | dayjs | 日期处理 | +| | axios | HTTP 请求 | +| **状态管理** | zustand | 轻量级状态管理 | +| **表单处理** | react-hook-form | 表单管理 | +| | zod | 数据验证 | +| **原生功能** | @react-native-async-storage/async-storage | 本地存储 | +| | expo-image | 优化的图片组件 | +| | expo-haptics | 触觉反馈 | ### 快速开始 @@ -243,22 +253,26 @@ eas init ``` 这会: + - 创建一个唯一的项目 ID - 自动更新 `app.json` 中的 `extra.eas.projectId` ### 步骤 3:构建开发版本 **Android 开发构建:** + ```bash eas build --profile development --platform android ``` **iOS 开发构建(需要 macOS 和 Apple 开发者账号):** + ```bash eas build --profile development --platform ios ``` 构建过程需要 **10-20 分钟**。完成后: + 1. 在 [expo.dev](https://expo.dev) 控制台下载构建的 APK/IPA 文件 2. 安装到你的设备上 @@ -291,11 +305,11 @@ eas update --channel production --message "v1.0.1: 添加了新功能" 项目配置了三个更新通道(在 `eas.json` 中定义): -| 通道 | 用途 | 适用场景 | -|------|------|----------| +| 通道 | 用途 | 适用场景 | +| --------------- | -------- | -------------- | | **development** | 开发环境 | 日常开发和测试 | -| **preview** | 预览环境 | 内部测试和 QA | -| **production** | 生产环境 | 正式发布给用户 | +| **preview** | 预览环境 | 内部测试和 QA | +| **production** | 生产环境 | 正式发布给用户 | 不同的构建配置会订阅不同的更新通道: @@ -404,12 +418,9 @@ button: { const [count, setCount] = useState(0); // 在 JSX 中添加 - setCount(count + 1)} -> + setCount(count + 1)}> 点击次数: {count} - +; ``` 修改后,运行 `eas update` 发布更新,然后在应用中检查更新即可看到变化。 @@ -459,16 +470,16 @@ Alert.alert( "name": "rn-demo", "slug": "rn-demo", "version": "1.0.0", - "newArchEnabled": true, // 启用 React Native 新架构 + "newArchEnabled": true, // 启用 React Native 新架构 "updates": { - "url": "https://u.expo.dev/your-project-id" // EAS Update 服务器 + "url": "https://u.expo.dev/your-project-id" // EAS Update 服务器 }, "runtimeVersion": { - "policy": "appVersion" // 运行时版本策略 + "policy": "appVersion" // 运行时版本策略 }, "plugins": [ - "expo-router", // Expo Router 插件 - "expo-updates" // 热更新插件 + "expo-router", // Expo Router 插件 + "expo-updates" // 热更新插件 ], "ios": { "bundleIdentifier": "com.rndemo.app" @@ -481,6 +492,7 @@ Alert.alert( ``` **配置说明:** + - `updates.url` - EAS Update 服务器地址(运行 `eas init` 后自动生成) - `runtimeVersion.policy` - 运行时版本策略,确保更新兼容性 - `appVersion` - 基于 `version` 字段(当前使用) @@ -496,22 +508,23 @@ Alert.alert( { "build": { "development": { - "developmentClient": true, // 开发客户端 - "distribution": "internal", // 内部分发 - "channel": "development" // 订阅 development 更新通道 + "developmentClient": true, // 开发客户端 + "distribution": "internal", // 内部分发 + "channel": "development" // 订阅 development 更新通道 }, "preview": { "distribution": "internal", - "channel": "preview" // 订阅 preview 更新通道 + "channel": "preview" // 订阅 preview 更新通道 }, "production": { - "channel": "production" // 订阅 production 更新通道 + "channel": "production" // 订阅 production 更新通道 } } } ``` **配置说明:** + - `developmentClient` - 是否为开发客户端(包含开发工具) - `distribution` - 分发方式(`internal` 或 `store`) - `channel` - 更新通道,决定应用接收哪个通道的更新 @@ -521,6 +534,7 @@ Alert.alert( ### 何时使用热更新 ✅ **适合热更新的场景:** + - ✅ 修复 JavaScript/TypeScript 代码 bug - ✅ 更新 UI 样式和布局 - ✅ 修改业务逻辑 @@ -529,6 +543,7 @@ Alert.alert( - ✅ 添加新的 JS 功能 ❌ **不适合热更新的场景(需要重新构建):** + - ❌ 添加/删除原生依赖(如 `react-native-camera`) - ❌ 修改原生代码(iOS/Android) - ❌ 更改应用权限(如相机、位置权限) @@ -539,11 +554,13 @@ Alert.alert( ### 版本管理策略 **appVersion 策略**(当前使用): + - 基于 `app.json` 中的 `version` 字段 - ✅ 优点:简单直观,易于理解 - ⚠️ 注意:每次原生构建需要手动更新版本号 **nativeVersion 策略**: + - 基于原生构建号(iOS 的 `buildNumber`,Android 的 `versionCode`) - ✅ 优点:自动管理,无需手动更新 - ⚠️ 注意:需要配置原生构建号 @@ -564,6 +581,7 @@ Alert.alert( ### Q: 更新后没有生效? **A:** 检查以下几点: + 1. 确保应用完全关闭后重新打开(不是后台切换) 2. 检查更新通道是否匹配(development/preview/production) 3. 确保 `runtimeVersion` 匹配 @@ -573,6 +591,7 @@ Alert.alert( ### Q: 如何回滚到之前的版本? **A:** + ```bash # 查看更新历史 eas update:list --channel production @@ -584,6 +603,7 @@ eas update --channel production --branch main --message "回滚到稳定版本" ### Q: 热更新的大小限制是多少? **A:** EAS Update 没有严格的大小限制,但建议: + - 保持更新包 < 10MB 以确保良好的用户体验 - 避免在更新中包含大量图片或资源文件 - 使用 CDN 托管大型资源 @@ -599,7 +619,7 @@ if (update.isAvailable) { '发现新版本', '请更新到最新版本', [{ text: '立即更新', onPress: async () => await Updates.reloadAsync() }], - { cancelable: false } // 不可取消 + { cancelable: false } // 不可取消 ); } ``` @@ -607,6 +627,7 @@ if (update.isAvailable) { ### Q: 更新检查的频率是多少? **A:** + - **应用启动时**:自动检查(在 `app/_layout.tsx` 中配置) - **手动检查**:用户点击"检查更新"按钮 - **后台检查**:可以配置定时检查(需要自己实现) @@ -614,6 +635,7 @@ if (update.isAvailable) { ### Q: 如何在开发时测试热更新? **A:** + 1. 构建 Development Build:`eas build --profile development --platform android` 2. 安装到设备 3. 修改代码 @@ -635,6 +657,7 @@ if (update.isAvailable) { ## 📚 相关资源 ### 官方文档 + - [Expo 官方文档](https://docs.expo.dev/) - [Expo Router 文档](https://docs.expo.dev/router/introduction/) - [EAS Update 文档](https://docs.expo.dev/eas-update/introduction/) @@ -642,11 +665,13 @@ if (update.isAvailable) { - [React Native 文档](https://reactnative.dev/) ### 学习资源 + - [Expo Router 最佳实践](https://docs.expo.dev/router/best-practices/) - [EAS Update 最佳实践](https://docs.expo.dev/eas-update/best-practices/) - [React Native 新架构](https://reactnative.dev/docs/the-new-architecture/landing-page) ### 社区 + - [Expo Discord](https://chat.expo.dev/) - [Expo Forums](https://forums.expo.dev/) - [React Native Community](https://reactnative.dev/community/overview) @@ -658,10 +683,8 @@ if (update.isAvailable) { - **[使用示例](./docs/USAGE_EXAMPLES.md)** - 实际代码示例 - **[工具库使用指南](./docs/LIBRARIES.md)** - 详细的工具库使用方法和示例 - - --- + **祝你开发愉快!** 🎉 如有问题,请查看 [常见问题](#-常见问题) 或访问 [Expo 官方文档](https://docs.expo.dev/)。 - diff --git a/app.json b/app.json index b9ebfad..7c7b316 100644 --- a/app.json +++ b/app.json @@ -31,10 +31,7 @@ "output": "static", "favicon": "./assets/images/favicon.png" }, - "plugins": [ - "expo-router", - "expo-updates" - ], + "plugins": ["expo-router", "expo-updates"], "experiments": { "typedRoutes": true }, diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index d15a67a..2252c02 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -25,7 +25,8 @@ export default function TabLayout() { // Disable the static render of the header on web // to prevent a hydration error in React Navigation v6. headerShown: useClientOnlyValue(false, true), - }}> + }} + > state.login); const logout = useUserStore((state) => state.logout); - + // 设置状态 const theme = useTheme(); const language = useLanguage(); @@ -66,7 +68,7 @@ export default function DemoScreen() { const setTheme = useSettingsStore((state) => state.setTheme); const setLanguage = useSettingsStore((state) => state.setLanguage); const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled); - + // 本地状态 const [searchText, setSearchText] = useState(''); const [searchResults, setSearchResults] = useState([]); @@ -89,6 +91,7 @@ export default function DemoScreen() { // 防抖搜索示例 const debouncedSearch = useDebounce(async (text: string) => { + console.log('防抖搜索:', text); if (!text.trim()) { setSearchResults([]); return; @@ -97,13 +100,24 @@ export default function DemoScreen() { console.log('执行搜索:', text); // 模拟 API 调用 await new Promise((resolve) => setTimeout(resolve, 500)); - setSearchResults([ - `结果 1: ${text}`, - `结果 2: ${text}`, - `结果 3: ${text}`, - ]); + setSearchResults([`结果 1: ${text}`, `结果 2: ${text}`, `结果 3: ${text}`]); }, 500); + useEffect(() => { + console.log('=== useEffect 开始执行 ==='); + console.log('appService:', appService); + console.log('getPlatformData 方法:', appService.getPlatformData); + + appService + .getPlatformData() + .then((res: any) => { + console.log('getPlatformData 成功:', res); + }) + .catch((err: any) => { + console.error('getPlatformData 失败:', err); + }); + }, []); + // 监听搜索文本变化 useEffect(() => { debouncedSearch(searchText); @@ -124,11 +138,11 @@ export default function DemoScreen() { // 模拟登录 API 调用 console.log('登录数据:', data); - + // 实际项目中使用: // const { user, token } = await authService.login(data); // login(user, token); - + // 模拟登录成功 const mockUser = { id: '1', @@ -138,7 +152,7 @@ export default function DemoScreen() { nickname: '演示用户', createdAt: new Date().toISOString(), }; - + login(mockUser, 'mock-token-123456'); haptics.success(); Alert.alert('成功', '登录成功!'); @@ -153,20 +167,16 @@ export default function DemoScreen() { // 登出处理 const handleLogout = () => { haptics.warning(); - Alert.alert( - '确认', - '确定要退出登录吗?', - [ - { text: '取消', style: 'cancel' }, - { - text: '确定', - onPress: () => { - logout(); - haptics.success(); - }, + Alert.alert('确认', '确定要退出登录吗?', [ + { text: '取消', style: 'cancel' }, + { + text: '确定', + onPress: () => { + logout(); + haptics.success(); }, - ] - ); + }, + ]); }; // 存储示例 @@ -178,7 +188,7 @@ export default function DemoScreen() { timestamp: new Date().toISOString(), counter, }; - + await Storage.setObject(STORAGE_KEYS.USER_PREFERENCES, testData); haptics.success(); Alert.alert('成功', '数据已保存到本地存储'); @@ -192,7 +202,7 @@ export default function DemoScreen() { try { haptics.light(); const data = await Storage.getObject(STORAGE_KEYS.USER_PREFERENCES); - + if (data) { setStorageValue(JSON.stringify(data, null, 2)); haptics.success(); @@ -234,11 +244,7 @@ export default function DemoScreen() { {isLoggedIn ? ( {user?.avatar && ( - + )} {user?.nickname} @@ -247,10 +253,7 @@ export default function DemoScreen() { 注册时间: {formatRelativeTime(user?.createdAt || '')} - + 退出 @@ -263,7 +266,7 @@ export default function DemoScreen() { {!isLoggedIn && ( 🔐 登录表单 (React Hook Form + Zod) - + - {errors.email && ( - {errors.email.message} - )} + {errors.email && {errors.email.message}} )} /> @@ -299,10 +297,7 @@ export default function DemoScreen() { onChangeText={onChange} placeholder="请输入密码" secureTextEntry - style={[ - styles.input, - errors.password && styles.inputError, - ]} + style={[styles.input, errors.password && styles.inputError]} /> {errors.password && ( {errors.password.message} @@ -349,10 +344,7 @@ export default function DemoScreen() { ⏱️ 节流点击 (useThrottle) 点击次数: {counter} - + 快速点击测试(1秒节流) @@ -387,18 +379,14 @@ export default function DemoScreen() { 当前时间: {formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')} - - 相对时间: {formatRelativeTime(new Date())} - - - 聊天时间: {formatChatTime(Date.now())} - + 相对时间: {formatRelativeTime(new Date())} + 聊天时间: {formatChatTime(Date.now())} {/* 设置示例 */} ⚙️ 应用设置 - + 主题: {theme} - - 查看代码了解更多使用方法 📖 - + 查看代码了解更多使用方法 📖 @@ -607,7 +593,7 @@ const styles = StyleSheet.create({ }, buttonRow: { flexDirection: 'row', - gap: 12, + justifyContent: 'space-between', }, halfButton: { flex: 1, @@ -651,27 +637,31 @@ const styles = StyleSheet.create({ buttonGrid: { flexDirection: 'row', flexWrap: 'wrap', - gap: 8, + marginHorizontal: -4, }, hapticsButton: { backgroundColor: '#5856D6', flex: 1, minWidth: '30%', + margin: 4, }, successButton: { backgroundColor: '#34C759', flex: 1, + margin: 4, minWidth: '30%', }, warningButton: { backgroundColor: '#FF9500', flex: 1, minWidth: '30%', + margin: 4, }, errorButton: { backgroundColor: '#FF3B30', flex: 1, minWidth: '30%', + margin: 4, }, footer: { marginTop: 24, @@ -683,4 +673,3 @@ const styles = StyleSheet.create({ color: '#999', }, }); - diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index a0f5c8d..b9185e5 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { StyleSheet, TouchableOpacity, Alert, ActivityIndicator } from 'react-native'; import * as Updates from 'expo-updates'; @@ -24,23 +24,19 @@ export default function TabOneScreen() { setUpdateInfo('发现新版本,正在下载...'); await Updates.fetchUpdateAsync(); - Alert.alert( - '更新完成', - '新版本已下载完成,是否立即重启应用?', - [ - { - text: '稍后', - style: 'cancel', - onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'), + Alert.alert('更新完成', '新版本已下载完成,是否立即重启应用?', [ + { + text: '稍后', + style: 'cancel', + onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'), + }, + { + text: '立即重启', + onPress: async () => { + await Updates.reloadAsync(); }, - { - text: '立即重启', - onPress: async () => { - await Updates.reloadAsync(); - }, - }, - ] - ); + }, + ]); } else { setUpdateInfo('当前已是最新版本'); } @@ -53,13 +49,8 @@ export default function TabOneScreen() { }; const getUpdateInfo = () => { - const { - isEmbeddedLaunch, - isEmergencyLaunch, - updateId, - channel, - runtimeVersion, - } = Updates.useUpdates(); + const { isEmbeddedLaunch, isEmergencyLaunch, updateId, channel, runtimeVersion } = + Updates.useUpdates(); return ` 运行模式: ${__DEV__ ? '开发模式' : '生产模式'} @@ -71,6 +62,10 @@ export default function TabOneScreen() { `.trim(); }; + useEffect(() => { + console.log('=== TabOneScreen 组件已渲染 ==='); + }, []); + return ( 🚀 热更新演示 diff --git a/app/(tabs)/paper.tsx b/app/(tabs)/paper.tsx index 5b16eea..829bf97 100644 --- a/app/(tabs)/paper.tsx +++ b/app/(tabs)/paper.tsx @@ -82,7 +82,7 @@ export default function PaperDemo() { 🔘 按钮 - - - @@ -141,18 +137,13 @@ export default function PaperDemo() { - {}}> + {}} style={{ marginRight: 8, marginBottom: 8 }}> 收藏 - {}}> + {}} style={{ marginRight: 8, marginBottom: 8 }}> 喜欢 - {}} - onClose={() => {}} - closeIcon="close-circle" - > + {}} onClose={() => {}} closeIcon="close-circle" style={{ marginBottom: 8 }}> 可关闭 @@ -221,12 +212,7 @@ export default function PaperDemo() { {/* 浮动操作按钮 */} - setSnackbarVisible(true)} - label="添加" - /> + setSnackbarVisible(true)} label="添加" /> {/* 提示条 */} { + await Updates.reloadAsync(); }, - { - text: '立即重启', - onPress: async () => { - await Updates.reloadAsync(); - }, - }, - ] - ); + }, + ]); } } catch (error) { // 处理更新检查错误 diff --git a/components/EditScreenInfo.tsx b/components/EditScreenInfo.tsx index 430b609..50def1e 100644 --- a/components/EditScreenInfo.tsx +++ b/components/EditScreenInfo.tsx @@ -14,21 +14,24 @@ export default function EditScreenInfo({ path }: { path: string }) { + darkColor="rgba(255,255,255,0.8)" + > Open up the code for this screen: + lightColor="rgba(0,0,0,0.05)" + > {path} + darkColor="rgba(255,255,255,0.8)" + > Change any of the text, save the file, and your app will automatically update. @@ -36,7 +39,8 @@ export default function EditScreenInfo({ path }: { path: string }) { + href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet" + > Tap here if your app doesn't automatically update after making changes diff --git a/constants/network.ts b/constants/network.ts new file mode 100644 index 0000000..98bb945 --- /dev/null +++ b/constants/network.ts @@ -0,0 +1,38 @@ +// 请求相关 + +export enum NetworkTypeEnum { + ERROR = 'error', + SUCCESS = 'success', +} + +// 冻结账号相关接口 +export const FREEZE_CMDID = [ + '314501', + '7242031', + '621116', + '396101', + '420029', + '724209', + '621112', + '377003', + '7242026', + '390004', + '3740012', + '321543', + '310400', + '325308', +]; + +export const WITHDRAWAL_CMDID = ['325308']; + +export const NO_CANCEL_CMDID = ['370730']; // 不需要取消的请求集合 + +export const TIPS_CON = [ + '请完成短信验证之后再参与', + '请填写真实姓名之后再参与', + '请完成绑定银行卡之后再参与', + '请完成生日设置之后再参与', + '请绑定虚拟货币之后再参与', + '请绑定收款方式之后再参与', + '同登录IP仅可领取一次,不可重复领取', +]; diff --git a/docs/LIBRARIES.md b/docs/LIBRARIES.md index 300c86e..c53b0a7 100644 --- a/docs/LIBRARIES.md +++ b/docs/LIBRARIES.md @@ -5,25 +5,28 @@ ## 📦 已安装的库列表 ### 工具类库 -| 库名 | 版本 | 用途 | -|------|------|------| -| **lodash-es** | ^4.17.21 | JavaScript 工具函数库(ES modules 版本) | -| **dayjs** | ^1.11.19 | 轻量级日期处理库 | -| **axios** | ^1.13.1 | HTTP 请求库 | -| **zustand** | ^5.0.8 | 轻量级状态管理 | -| **react-hook-form** | ^7.66.0 | 表单处理库 | -| **zod** | ^4.1.12 | TypeScript 数据验证库 | + +| 库名 | 版本 | 用途 | +| ------------------- | -------- | ---------------------------------------- | +| **lodash-es** | ^4.17.21 | JavaScript 工具函数库(ES modules 版本) | +| **dayjs** | ^1.11.19 | 轻量级日期处理库 | +| **axios** | ^1.13.1 | HTTP 请求库 | +| **zustand** | ^5.0.8 | 轻量级状态管理 | +| **react-hook-form** | ^7.66.0 | 表单处理库 | +| **zod** | ^4.1.12 | TypeScript 数据验证库 | ### Expo 原生模块 -| 库名 | 版本 | 用途 | -|------|------|------| -| **@react-native-async-storage/async-storage** | ^2.2.0 | 本地存储 | -| **expo-image** | ^3.0.10 | 优化的图片组件 | -| **expo-haptics** | ^15.0.7 | 触觉反馈 | + +| 库名 | 版本 | 用途 | +| --------------------------------------------- | ------- | -------------- | +| **@react-native-async-storage/async-storage** | ^2.2.0 | 本地存储 | +| **expo-image** | ^3.0.10 | 优化的图片组件 | +| **expo-haptics** | ^15.0.7 | 触觉反馈 | ### 开发工具 -| 库名 | 版本 | 用途 | -|------|------|------| + +| 库名 | 版本 | 用途 | +| -------------------- | -------- | ----------------------------- | | **@types/lodash-es** | ^4.17.12 | Lodash-ES TypeScript 类型定义 | --- @@ -39,13 +42,13 @@ import { map, filter, uniq, pick, cloneDeep, debounce } from 'lodash-es'; // 数组操作 -map([1, 2, 3], n => n * 2); // [2, 4, 6] -filter([1, 2, 3, 4], n => n % 2 === 0); // [2, 4] -uniq([1, 2, 2, 3]); // [1, 2, 3] +map([1, 2, 3], (n) => n * 2); // [2, 4, 6] +filter([1, 2, 3, 4], (n) => n % 2 === 0); // [2, 4] +uniq([1, 2, 2, 3]); // [1, 2, 3] // 对象操作 -pick({ a: 1, b: 2, c: 3 }, ['a', 'b']); // { a: 1, b: 2 } -cloneDeep(obj); // 深拷贝 +pick({ a: 1, b: 2, c: 3 }, ['a', 'b']); // { a: 1, b: 2 } +cloneDeep(obj); // 深拷贝 // 防抖和节流 const handleSearch = debounce((text) => { @@ -54,7 +57,7 @@ const handleSearch = debounce((text) => { // 也可以全量导入(不推荐,会增加包体积) import _ from 'lodash-es'; -_.map([1, 2, 3], n => n * 2); +_.map([1, 2, 3], (n) => n * 2); ``` ### 2. Day.js - 日期处理 @@ -71,12 +74,12 @@ dayjs.locale('zh-cn'); dayjs().format('YYYY-MM-DD HH:mm:ss'); // 相对时间 -dayjs().fromNow(); // '几秒前' -dayjs().subtract(1, 'day').fromNow(); // '1天前' +dayjs().fromNow(); // '几秒前' +dayjs().subtract(1, 'day').fromNow(); // '1天前' // 日期操作 -dayjs().add(7, 'day'); // 7天后 -dayjs().startOf('month'); // 本月第一天 +dayjs().add(7, 'day'); // 7天后 +dayjs().startOf('month'); // 本月第一天 ``` ### 3. Axios - HTTP 请求 @@ -329,7 +332,7 @@ export const formatRelativeTime = (date: Date | string | number) => { export const formatChatTime = (timestamp: number) => { const date = dayjs(timestamp); const now = dayjs(); - + if (date.isSame(now, 'day')) { return date.format('HH:mm'); } else if (date.isSame(now.subtract(1, 'day'), 'day')) { @@ -384,4 +387,3 @@ export const formatChatTime = (timestamp: number) => { --- **提示**:所有库都已安装并配置好,可以直接在项目中使用!🎉 - diff --git a/docs/USAGE_EXAMPLES.md b/docs/USAGE_EXAMPLES.md index bfa5c3e..a21878e 100644 --- a/docs/USAGE_EXAMPLES.md +++ b/docs/USAGE_EXAMPLES.md @@ -339,7 +339,7 @@ export default function SettingsScreen() { const theme = useSettingsStore((state) => state.theme); const notificationsEnabled = useSettingsStore((state) => state.notificationsEnabled); const hapticsEnabled = useSettingsStore((state) => state.hapticsEnabled); - + const setTheme = useSettingsStore((state) => state.setTheme); const setNotificationsEnabled = useSettingsStore((state) => state.setNotificationsEnabled); const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled); @@ -426,4 +426,3 @@ export default function SettingsScreen() { --- **提示**:这些示例都是可以直接使用的代码,复制到你的项目中即可! - diff --git a/eas.json b/eas.json index bb0e199..0b96b7f 100644 --- a/eas.json +++ b/eas.json @@ -20,4 +20,3 @@ "production": {} } } - diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..8f9d696 --- /dev/null +++ b/metro.config.js @@ -0,0 +1,19 @@ +const { getDefaultConfig } = require('expo/metro-config'); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); + +// 自定义 Metro 配置 +config.resolver = { + ...config.resolver, + // 可以在这里添加自定义解析规则 +}; + +// 开发服务器配置 +config.server = { + ...config.server, + // Metro 服务器端口(默认 8081) + port: 8081, +}; + +module.exports = config; diff --git a/package.json b/package.json index 2cdaa87..5333829 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", - "web": "expo start --web" + "web": "expo start --web", + "proxy": "node scripts/proxy-server.js", + "dev": "concurrently \"npm run proxy\" \"npm run start\"", + "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", + "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" }, "dependencies": { "@expo/vector-icons": "^15.0.3", @@ -14,6 +18,7 @@ "@react-native-async-storage/async-storage": "^2.2.0", "@react-navigation/native": "^7.1.8", "axios": "^1.13.1", + "crypto-js": "^4.2.0", "dayjs": "^1.11.19", "expo": "~54.0.22", "expo-constants": "~18.0.10", @@ -27,6 +32,7 @@ "expo-updates": "^29.0.12", "expo-web-browser": "~15.0.9", "lodash-es": "^4.17.21", + "md5": "^2.3.0", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.66.0", @@ -41,8 +47,14 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@types/crypto-js": "^4.2.2", "@types/lodash-es": "^4.17.12", + "@types/md5": "^2.3.6", "@types/react": "~19.1.0", + "concurrently": "^9.2.1", + "express": "^5.1.0", + "http-proxy-middleware": "^3.0.5", + "prettier": "^3.6.2", "react-test-renderer": "19.1.0", "typescript": "~5.9.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9e72f6..d8ebc4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: axios: specifier: ^1.13.1 version: 1.13.1 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 dayjs: specifier: ^1.11.19 version: 1.11.19 @@ -62,6 +65,9 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 + md5: + specifier: ^2.3.0 + version: 2.3.0 react: specifier: 19.1.0 version: 19.1.0 @@ -99,12 +105,30 @@ importers: specifier: ^5.0.8 version: 5.0.8(@types/react@19.1.17)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 + '@types/md5': + specifier: ^2.3.6 + version: 2.3.6 '@types/react': specifier: ~19.1.0 version: 19.1.17 + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + express: + specifier: ^5.1.0 + version: 5.1.0 + http-proxy-middleware: + specifier: ^3.0.5 + version: 3.0.5 + prettier: + specifier: ^3.6.2 + version: 3.6.2 react-test-renderer: specifier: 19.1.0 version: 19.1.0(react@19.1.0) @@ -1192,9 +1216,15 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/http-proxy@1.17.17': + resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -1210,6 +1240,9 @@ packages: '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/md5@2.3.6': + resolution: {integrity: sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA==} + '@types/node@24.10.0': resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} @@ -1248,6 +1281,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -1409,6 +1446,10 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -1452,6 +1493,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -1471,6 +1516,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1562,13 +1610,34 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + connect@3.7.0: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-js-compat@3.46.0: resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} @@ -1579,6 +1648,12 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -1749,6 +1824,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + exec-async@2.2.0: resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==} @@ -1918,6 +1996,10 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1945,6 +2027,10 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1976,6 +2062,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + freeport-async@2.0.0: resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==} engines: {node: '>=8'} @@ -1984,6 +2074,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2085,6 +2179,14 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-middleware@3.0.5: + resolution: {integrity: sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -2092,6 +2194,14 @@ packages: hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2124,9 +2234,16 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -2136,10 +2253,18 @@ packages: engines: {node: '>=8'} hasBin: true + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2148,6 +2273,13 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -2358,12 +2490,23 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-options@3.0.4: resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} engines: {node: '>=10'} @@ -2503,6 +2646,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -2557,6 +2704,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + nested-error-stacks@2.0.1: resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} @@ -2602,6 +2753,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -2683,6 +2838,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2713,6 +2871,11 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} @@ -2739,6 +2902,10 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2750,6 +2917,10 @@ packages: resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} hasBin: true + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -2761,6 +2932,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -2930,6 +3105,9 @@ packages: resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} engines: {node: '>= 4.0.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2962,9 +3140,19 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.2: resolution: {integrity: sha512-FySGAa0RGcFiN6zfrO9JvK1r7TB59xuzCcTHOBXBNoKgDejlOQCR2KL/FGk3/iDlsqyYg1ELZpOmlg09B01Czw==} @@ -2998,6 +3186,10 @@ packages: resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -3006,6 +3198,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -3034,6 +3230,22 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3207,6 +3419,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -3225,6 +3441,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -4938,10 +5158,16 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/crypto-js@4.2.2': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 24.10.0 + '@types/http-proxy@1.17.17': + dependencies: + '@types/node': 24.10.0 + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -4958,6 +5184,8 @@ snapshots: '@types/lodash@4.17.20': {} + '@types/md5@2.3.6': {} + '@types/node@24.10.0': dependencies: undici-types: 7.16.0 @@ -4999,6 +5227,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn@8.15.0: {} agent-base@7.1.4: {} @@ -5056,7 +5289,7 @@ snapshots: axios@1.13.1: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -5201,6 +5434,20 @@ snapshots: big-integer@1.6.52: {} + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + bplist-creator@0.1.0: dependencies: stream-buffers: 2.2.0 @@ -5252,6 +5499,11 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + camelcase@5.3.1: {} camelcase@6.3.0: {} @@ -5269,6 +5521,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + charenc@0.0.2: {} + chownr@3.0.0: {} chrome-launcher@0.15.2: @@ -5368,6 +5622,15 @@ snapshots: concat-map@0.0.1: {} + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + connect@3.7.0: dependencies: debug: 2.6.9 @@ -5377,8 +5640,18 @@ snapshots: transitivePeerDependencies: - supports-color + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + core-js-compat@3.46.0: dependencies: browserslist: 4.27.0 @@ -5395,6 +5668,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + + crypto-js@4.2.0: {} + crypto-random-string@2.0.0: {} css-in-js-utils@3.1.0: @@ -5504,6 +5781,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + exec-async@2.2.0: {} expo-asset@12.0.9(expo@54.0.22)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): @@ -5718,6 +5997,38 @@ snapshots: exponential-backoff@3.1.3: {} + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -5758,6 +6069,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -5770,7 +6092,9 @@ snapshots: flow-enums-runtime@0.0.6: {} - follow-redirects@1.15.11: {} + follow-redirects@1.15.11(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 fontfaceobserver@2.3.0: {} @@ -5787,10 +6111,14 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded@0.2.0: {} + freeport-async@2.0.0: {} fresh@0.5.2: {} + fresh@2.0.0: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -5894,6 +6222,25 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-middleware@3.0.5: + dependencies: + '@types/http-proxy': 1.17.17 + debug: 4.4.3 + http-proxy: 1.18.1(debug@4.4.3) + is-glob: 4.0.3 + is-plain-object: 5.0.0 + micromatch: 4.0.8 + transitivePeerDependencies: + - supports-color + + http-proxy@1.18.1(debug@4.4.3): + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.11(debug@4.4.3) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -5903,6 +6250,14 @@ snapshots: hyphenate-style-name@1.1.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -5930,20 +6285,34 @@ snapshots: dependencies: loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} + is-arrayish@0.3.4: {} + is-buffer@1.1.6: {} + is-core-module@2.16.1: dependencies: hasown: 2.0.2 is-docker@2.2.1: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-number@7.0.0: {} is-plain-obj@2.1.0: {} + is-plain-object@5.0.0: {} + + is-promise@4.0.0: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -6159,10 +6528,20 @@ snapshots: math-intrinsics@1.1.0: {} + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + media-typer@1.1.0: {} + memoize-one@5.2.1: {} memoize-one@6.0.0: {} + merge-descriptors@2.0.0: {} + merge-options@3.0.4: dependencies: is-plain-obj: 2.1.0 @@ -6532,6 +6911,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mimic-fn@1.2.0: {} @@ -6570,6 +6953,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + nested-error-stacks@2.0.1: {} node-fetch@2.7.0: @@ -6603,6 +6988,8 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -6680,6 +7067,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -6704,6 +7093,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier@3.6.2: {} + pretty-bytes@5.6.0: {} pretty-format@29.7.0: @@ -6729,12 +7120,21 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} punycode@2.3.1: {} qrcode-terminal@0.11.0: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -6748,6 +7148,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -6971,6 +7378,8 @@ snapshots: rc: 1.2.8 resolve: 1.7.1 + requires-port@1.0.0: {} + resolve-from@5.0.0: {} resolve-global@1.0.0: @@ -7000,8 +7409,24 @@ snapshots: dependencies: glob: 7.2.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + sax@1.4.2: {} scheduler@0.26.0: {} @@ -7050,6 +7475,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serialize-error@2.1.0: {} serve-static@1.16.2: @@ -7061,6 +7502,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + server-only@0.0.1: {} setimmediate@1.0.5: {} @@ -7079,6 +7529,34 @@ snapshots: shell-quote@1.8.3: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -7239,6 +7717,8 @@ snapshots: tr46@0.0.3: {} + tree-kill@1.2.2: {} + ts-interface-checker@0.1.13: {} tslib@2.8.1: {} @@ -7249,6 +7729,12 @@ snapshots: type-fest@0.7.1: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typescript@5.9.3: {} ua-parser-js@1.0.41: {} diff --git a/scripts/proxy-server.js b/scripts/proxy-server.js new file mode 100644 index 0000000..a2eee18 --- /dev/null +++ b/scripts/proxy-server.js @@ -0,0 +1,59 @@ +/** + * 开发环境代理服务器 + * 用于解决跨域问题和统一 API 请求 + */ + +const express = require('express'); +const { createProxyMiddleware } = require('http-proxy-middleware'); + +const app = express(); +const PORT = 8080; + +// 目标 API 服务器地址 +const API_TARGET = process.env.API_TARGET || 'http://localhost:3000'; + +// 配置代理 +app.use( + '/api', + createProxyMiddleware({ + target: API_TARGET, + changeOrigin: true, + pathRewrite: { + '^/api': '/api', // 保持路径不变,或者根据需要重写 + }, + onProxyReq: (proxyReq, req, res) => { + console.log(`[Proxy] ${req.method} ${req.url} → ${API_TARGET}${req.url}`); + }, + onProxyRes: (proxyRes, req, res) => { + console.log(`[Proxy] ${req.method} ${req.url} ← ${proxyRes.statusCode}`); + }, + onError: (err, req, res) => { + console.error('[Proxy Error]', err.message); + res.status(500).json({ + error: 'Proxy Error', + message: err.message, + }); + }, + }) +); + +// 健康检查 +app.get('/health', (req, res) => { + res.json({ + status: 'ok', + proxy: API_TARGET, + timestamp: new Date().toISOString(), + }); +}); + +app.listen(PORT, () => { + console.log(` +╔════════════════════════════════════════════════════════╗ +║ 🚀 Proxy Server Running ║ +╠════════════════════════════════════════════════════════╣ +║ Local: http://localhost:${PORT} ║ +║ Target: ${API_TARGET.padEnd(40)} ║ +║ Health: http://localhost:${PORT}/health ║ +╚════════════════════════════════════════════════════════╝ + `); +}); diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts index 0d081cf..7f95f42 100644 --- a/src/hooks/useDebounce.ts +++ b/src/hooks/useDebounce.ts @@ -80,5 +80,3 @@ export function useDebounce any>( * console.log('Searching:', text); * }, 500); */ - - diff --git a/src/hooks/useHaptics.ts b/src/hooks/useHaptics.ts index 376e0aa..0a330a0 100644 --- a/src/hooks/useHaptics.ts +++ b/src/hooks/useHaptics.ts @@ -90,20 +90,20 @@ export function useHaptics() { /** * 使用示例: - * + * * function MyComponent() { * const haptics = useHaptics(); - * + * * const handlePress = () => { * haptics.light(); * // 执行其他操作 * }; - * + * * const handleSuccess = () => { * haptics.success(); * // 显示成功消息 * }; - * + * * return ( * * Press me @@ -111,4 +111,3 @@ export function useHaptics() { * ); * } */ - diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts new file mode 100644 index 0000000..d50e246 --- /dev/null +++ b/src/hooks/useRequest.ts @@ -0,0 +1,291 @@ +/** + * 请求 Hook + * 提供统一的请求状态管理 + */ + +import { useState, useCallback, useRef, useEffect } from 'react'; +import { AxiosError } from 'axios'; +import type { RequestConfig } from '@/src/utils/network/api'; + +/** + * 请求状态 + */ +export interface RequestState { + data: T | null; + loading: boolean; + error: Error | null; +} + +/** + * 请求选项 + */ +export interface UseRequestOptions extends RequestConfig { + /** 是否立即执行 */ + immediate?: boolean; + /** 成功回调 */ + onSuccess?: (data: T) => void; + /** 失败回调 */ + onError?: (error: Error) => void; + /** 完成回调(无论成功失败) */ + onFinally?: () => void; + /** 默认数据 */ + defaultData?: T; +} + +/** + * 请求 Hook + * + * @example + * ```tsx + * const { data, loading, error, run, refresh } = useRequest( + * () => request.get('/api/users'), + * { immediate: true } + * ); + * ``` + */ +export function useRequest( + requestFn: () => Promise, + options: UseRequestOptions = {} +) { + const { immediate = false, onSuccess, onError, onFinally, defaultData = null } = options; + + const [state, setState] = useState>({ + data: defaultData, + loading: false, + error: null, + }); + + const requestRef = useRef(requestFn); + requestRef.current = requestFn; + + const abortControllerRef = useRef(null); + + /** + * 执行请求 + */ + const run = useCallback( + async (...args: any[]) => { + // 取消之前的请求 + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // 创建新的 AbortController + abortControllerRef.current = new AbortController(); + + setState((prev) => ({ + ...prev, + loading: true, + error: null, + })); + + try { + const data = await requestRef.current(); + + setState({ + data, + loading: false, + error: null, + }); + + onSuccess?.(data); + + return data; + } catch (error) { + const err = error as Error; + + setState((prev) => ({ + ...prev, + loading: false, + error: err, + })); + + onError?.(err); + + throw error; + } finally { + onFinally?.(); + } + }, + [onSuccess, onError, onFinally] + ); + + /** + * 刷新(重新执行请求) + */ + const refresh = useCallback(() => { + return run(); + }, [run]); + + /** + * 重置状态 + */ + const reset = useCallback(() => { + setState({ + data: defaultData, + loading: false, + error: null, + }); + }, [defaultData]); + + /** + * 取消请求 + */ + const cancel = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }, []); + + // 立即执行 + useEffect(() => { + if (immediate) { + run(); + } + + // 组件卸载时取消请求 + return () => { + cancel(); + }; + }, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps + + return { + ...state, + run, + refresh, + reset, + cancel, + }; +} + +/** + * 分页请求 Hook + * + * @example + * ```tsx + * const { data, loading, loadMore, refresh, hasMore } = usePagination( + * (page, pageSize) => request.get('/api/users', { params: { page, pageSize } }) + * ); + * ``` + */ +export function usePagination( + requestFn: ( + page: number, + pageSize: number + ) => Promise<{ + list: T[]; + total: number; + hasMore: boolean; + }>, + options: { + pageSize?: number; + immediate?: boolean; + onSuccess?: (data: T[]) => void; + onError?: (error: Error) => void; + } = {} +) { + const { pageSize = 20, immediate = false, onSuccess, onError } = options; + + const [state, setState] = useState({ + data: [] as T[], + loading: false, + loadingMore: false, + error: null as Error | null, + page: 1, + total: 0, + hasMore: true, + }); + + /** + * 加载数据 + */ + const load = useCallback( + async (page: number, append = false) => { + setState((prev) => ({ + ...prev, + loading: !append, + loadingMore: append, + error: null, + })); + + try { + const result = await requestFn(page, pageSize); + + setState((prev) => ({ + ...prev, + data: append ? [...prev.data, ...result.list] : result.list, + loading: false, + loadingMore: false, + page, + total: result.total, + hasMore: result.hasMore, + })); + + onSuccess?.(result.list); + + return result; + } catch (error) { + const err = error as Error; + + setState((prev) => ({ + ...prev, + loading: false, + loadingMore: false, + error: err, + })); + + onError?.(err); + + throw error; + } + }, + [requestFn, pageSize, onSuccess, onError] + ); + + /** + * 加载更多 + */ + const loadMore = useCallback(async () => { + if (state.loadingMore || !state.hasMore) { + return; + } + + return load(state.page + 1, true); + }, [state.loadingMore, state.hasMore, state.page, load]); + + /** + * 刷新(重新加载第一页) + */ + const refresh = useCallback(async () => { + return load(1, false); + }, [load]); + + /** + * 重置 + */ + const reset = useCallback(() => { + setState({ + data: [], + loading: false, + loadingMore: false, + error: null, + page: 1, + total: 0, + hasMore: true, + }); + }, []); + + // 立即执行 + useEffect(() => { + if (immediate) { + load(1, false); + } + }, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps + + return { + ...state, + loadMore, + refresh, + reset, + }; +} diff --git a/src/hooks/useThrottle.ts b/src/hooks/useThrottle.ts index ee578bf..02a54fa 100644 --- a/src/hooks/useThrottle.ts +++ b/src/hooks/useThrottle.ts @@ -49,13 +49,12 @@ export function useThrottle any>( /** * 使用示例: - * + * * const handleScroll = useThrottle((event) => { * console.log('Scrolling:', event); * }, 200); - * + * * * ... * */ - diff --git a/src/index.ts b/src/index.ts index b95f51b..520f64d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,16 @@ */ // Utils -export { default as api, request } from './utils/api'; +export { + default as api, + request, + cancelAllRequests, + cancelRequest, + createRetryRequest, +} from './utils/network/api'; +export type { ApiResponse, ApiError, RequestConfig } from './utils/network/api'; export { default as Storage, STORAGE_KEYS } from './utils/storage'; +export { default as config, printConfig } from './utils/config'; export * from './utils/date'; // Stores @@ -18,12 +26,14 @@ export * from './schemas/user'; // Services export { default as authService } from './services/authService'; export { default as userService } from './services/userService'; +export { default as appService } from './services/appService'; // Hooks export * from './hooks/useDebounce'; export * from './hooks/useThrottle'; export * from './hooks/useHaptics'; +export * from './hooks/useRequest'; // Types export * from './types'; - +export * from './types/api'; diff --git a/src/schemas/auth.ts b/src/schemas/auth.ts index 506cf04..377bafe 100644 --- a/src/schemas/auth.ts +++ b/src/schemas/auth.ts @@ -8,14 +8,8 @@ import { z } from 'zod'; * 登录表单 Schema */ export const loginSchema = z.object({ - email: z - .string() - .min(1, '请输入邮箱') - .email('请输入有效的邮箱地址'), - password: z - .string() - .min(6, '密码至少6个字符') - .max(20, '密码最多20个字符'), + email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'), + password: z.string().min(6, '密码至少6个字符').max(20, '密码最多20个字符'), rememberMe: z.boolean().optional(), }); @@ -29,10 +23,7 @@ export const registerSchema = z .min(3, '用户名至少3个字符') .max(20, '用户名最多20个字符') .regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'), - email: z - .string() - .min(1, '请输入邮箱') - .email('请输入有效的邮箱地址'), + email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'), password: z .string() .min(6, '密码至少6个字符') @@ -54,10 +45,7 @@ export const registerSchema = z * 忘记密码 Schema */ export const forgotPasswordSchema = z.object({ - email: z - .string() - .min(1, '请输入邮箱') - .email('请输入有效的邮箱地址'), + email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'), }); /** @@ -70,10 +58,7 @@ export const resetPasswordSchema = z .min(6, '验证码为6位') .max(6, '验证码为6位') .regex(/^\d{6}$/, '验证码必须是6位数字'), - password: z - .string() - .min(6, '密码至少6个字符') - .max(20, '密码最多20个字符'), + password: z.string().min(6, '密码至少6个字符').max(20, '密码最多20个字符'), confirmPassword: z.string().min(1, '请确认密码'), }) .refine((data) => data.password === data.confirmPassword, { @@ -87,10 +72,7 @@ export const resetPasswordSchema = z export const changePasswordSchema = z .object({ oldPassword: z.string().min(1, '请输入当前密码'), - newPassword: z - .string() - .min(6, '新密码至少6个字符') - .max(20, '新密码最多20个字符'), + newPassword: z.string().min(6, '新密码至少6个字符').max(20, '新密码最多20个字符'), confirmPassword: z.string().min(1, '请确认新密码'), }) .refine((data) => data.newPassword === data.confirmPassword, { @@ -127,4 +109,3 @@ export type ForgotPasswordFormData = z.infer; export type ResetPasswordFormData = z.infer; export type ChangePasswordFormData = z.infer; export type PhoneLoginFormData = z.infer; - diff --git a/src/schemas/user.ts b/src/schemas/user.ts index b906777..4c1fc79 100644 --- a/src/schemas/user.ts +++ b/src/schemas/user.ts @@ -21,11 +21,7 @@ export const userSchema = z.object({ * 更新用户资料 Schema */ export const updateProfileSchema = z.object({ - nickname: z - .string() - .min(2, '昵称至少2个字符') - .max(20, '昵称最多20个字符') - .optional(), + nickname: z.string().min(2, '昵称至少2个字符').max(20, '昵称最多20个字符').optional(), avatar: z.string().url('请输入有效的头像URL').optional(), phone: z .string() @@ -55,10 +51,7 @@ export const bindPhoneSchema = z.object({ * 绑定邮箱 Schema */ export const bindEmailSchema = z.object({ - email: z - .string() - .min(1, '请输入邮箱') - .email('请输入有效的邮箱地址'), + email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'), code: z .string() .min(6, '验证码为6位') @@ -73,4 +66,3 @@ export type User = z.infer; export type UpdateProfileFormData = z.infer; export type BindPhoneFormData = z.infer; export type BindEmailFormData = z.infer; - diff --git a/src/services/appService.ts b/src/services/appService.ts new file mode 100644 index 0000000..7bbc6f0 --- /dev/null +++ b/src/services/appService.ts @@ -0,0 +1,39 @@ +/** + * 基础服务 + * 处理应用相关的 API 请求 + */ + +import { request } from '@/src/utils/network/api'; +import type { User, UpdateProfileFormData } from '@/src/schemas/user'; + +/** + * API 响应接口 + */ +interface ApiResponse { + code: number; + message: string; + data: T; +} + +/** + * 用户服务类 + */ +class AppService { + /** + * 获取当前用户信息 + */ + getPlatformData(data?: Record): Promise { + return request.post('/v2', data, { + headers: { + cmdId: 371130, + headerType: 1, + apiName: 'getPlatformData', + tid: '', + }, + }); + } +} + +// 导出单例 +export const appService = new AppService(); +export default appService; diff --git a/src/services/authService.ts b/src/services/authService.ts index dd56927..f2360de 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -3,7 +3,7 @@ * 处理登录、注册等认证相关的 API 请求 */ -import { request } from '@/src/utils/api'; +import { request } from '@/src/utils/network/api'; import type { LoginFormData, RegisterFormData, @@ -40,10 +40,7 @@ class AuthService { * 邮箱登录 */ async login(data: LoginFormData): Promise { - const response = await request.post>( - '/auth/login', - data - ); + const response = await request.post>('/auth/login', data); return response.data; } @@ -51,10 +48,7 @@ class AuthService { * 手机号登录 */ async phoneLogin(data: PhoneLoginFormData): Promise { - const response = await request.post>( - '/auth/phone-login', - data - ); + const response = await request.post>('/auth/phone-login', data); return response.data; } @@ -62,10 +56,7 @@ class AuthService { * 注册 */ async register(data: RegisterFormData): Promise { - const response = await request.post>( - '/auth/register', - data - ); + const response = await request.post>('/auth/register', data); return response.data; } @@ -108,10 +99,9 @@ class AuthService { * 刷新 token */ async refreshToken(refreshToken: string): Promise<{ token: string }> { - const response = await request.post>( - '/auth/refresh-token', - { refreshToken } - ); + const response = await request.post>('/auth/refresh-token', { + refreshToken, + }); return response.data; } @@ -131,4 +121,3 @@ class AuthService { // 导出单例 export const authService = new AuthService(); export default authService; - diff --git a/src/services/userService.ts b/src/services/userService.ts index 02e231f..0ee023c 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -3,7 +3,7 @@ * 处理用户信息相关的 API 请求 */ -import { request } from '@/src/utils/api'; +import { request } from '@/src/utils/network/api'; import type { User, UpdateProfileFormData } from '@/src/schemas/user'; /** @@ -50,15 +50,11 @@ class UserService { const formData = new FormData(); formData.append('avatar', file); - const response = await request.post>( - '/user/avatar', - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, - } - ); + const response = await request.post>('/user/avatar', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); return response.data; } @@ -87,4 +83,3 @@ class UserService { // 导出单例 export const userService = new UserService(); export default userService; - diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index 517e276..05ba86c 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -127,12 +127,10 @@ export const useNotificationsEnabled = () => useSettingsStore((state) => state.notificationsEnabled); // 获取声音状态 -export const useSoundEnabled = () => - useSettingsStore((state) => state.soundEnabled); +export const useSoundEnabled = () => useSettingsStore((state) => state.soundEnabled); // 获取触觉反馈状态 -export const useHapticsEnabled = () => - useSettingsStore((state) => state.hapticsEnabled); +export const useHapticsEnabled = () => useSettingsStore((state) => state.hapticsEnabled); // 获取设置操作方法 export const useSettingsActions = () => @@ -144,4 +142,3 @@ export const useSettingsActions = () => setHapticsEnabled: state.setHapticsEnabled, resetSettings: state.resetSettings, })); - diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index e57e599..8b01136 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -139,4 +139,3 @@ export const useUserActions = () => logout: state.logout, updateUser: state.updateUser, })); - diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..d388cc6 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,73 @@ +/** + * API 相关类型定义 + */ + +/** + * 分页请求参数 + */ +export interface PaginationParams { + page: number; + pageSize: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} + +/** + * 分页响应数据 + */ +export interface PaginationResponse { + list: T[]; + total: number; + page: number; + pageSize: number; + totalPages: number; + hasMore: boolean; +} + +/** + * 列表响应数据 + */ +export interface ListResponse { + items: T[]; + total: number; +} + +/** + * ID 参数 + */ +export interface IdParams { + id: string | number; +} + +/** + * 批量操作参数 + */ +export interface BatchParams { + ids: (string | number)[]; +} + +/** + * 搜索参数 + */ +export interface SearchParams { + keyword: string; + filters?: Record; +} + +/** + * 上传响应 + */ +export interface UploadResponse { + url: string; + filename: string; + size: number; + mimeType: string; +} + +/** + * 通用操作响应 + */ +export interface OperationResponse { + success: boolean; + message?: string; +} diff --git a/src/types/index.ts b/src/types/index.ts index c8fe4d1..9ee9dab 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -78,4 +78,3 @@ declare global { } export {}; - diff --git a/src/utils/api.ts b/src/utils/api.ts deleted file mode 100644 index d022633..0000000 --- a/src/utils/api.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Axios API 配置 - * 统一管理 HTTP 请求 - */ - -import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; -import AsyncStorage from '@react-native-async-storage/async-storage'; - -// API 基础配置 -const API_CONFIG = { - baseURL: process.env.EXPO_PUBLIC_API_URL || 'https://api.example.com', - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - }, -}; - -// 创建 axios 实例 -const api = axios.create(API_CONFIG); - -/** - * 请求拦截器 - * 在请求发送前添加 token 等信息 - */ -api.interceptors.request.use( - async (config) => { - try { - // 从本地存储获取 token - const token = await AsyncStorage.getItem('auth_token'); - - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - - // 打印请求信息(开发环境) - if (__DEV__) { - console.log('📤 API Request:', { - method: config.method?.toUpperCase(), - url: config.url, - data: config.data, - }); - } - - return config; - } catch (error) { - console.error('Request interceptor error:', error); - return config; - } - }, - (error) => { - console.error('Request error:', error); - return Promise.reject(error); - } -); - -/** - * 响应拦截器 - * 统一处理响应和错误 - */ -api.interceptors.response.use( - (response: AxiosResponse) => { - // 打印响应信息(开发环境) - if (__DEV__) { - console.log('📥 API Response:', { - url: response.config.url, - status: response.status, - data: response.data, - }); - } - - // 返回响应数据 - return response.data; - }, - async (error: AxiosError) => { - // 打印错误信息 - console.error('❌ API Error:', { - url: error.config?.url, - status: error.response?.status, - message: error.message, - data: error.response?.data, - }); - - // 处理不同的错误状态码 - if (error.response) { - switch (error.response.status) { - case 401: - // 未授权,清除 token 并跳转到登录页 - await AsyncStorage.removeItem('auth_token'); - // TODO: 导航到登录页 - // router.replace('/login'); - break; - - case 403: - // 禁止访问 - console.error('Access forbidden'); - break; - - case 404: - // 资源不存在 - console.error('Resource not found'); - break; - - case 500: - // 服务器错误 - console.error('Server error'); - break; - - default: - console.error('Unknown error'); - } - } else if (error.request) { - // 请求已发送但没有收到响应 - console.error('No response received'); - } else { - // 请求配置出错 - console.error('Request configuration error'); - } - - return Promise.reject(error); - } -); - -/** - * 通用请求方法 - */ -export const request = { - get: (url: string, config?: AxiosRequestConfig) => - api.get(url, config), - - post: (url: string, data?: any, config?: AxiosRequestConfig) => - api.post(url, data, config), - - put: (url: string, data?: any, config?: AxiosRequestConfig) => - api.put(url, data, config), - - delete: (url: string, config?: AxiosRequestConfig) => - api.delete(url, config), - - patch: (url: string, data?: any, config?: AxiosRequestConfig) => - api.patch(url, data, config), -}; - -export default api; - diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000..5f09bd1 --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,130 @@ +/** + * 应用配置工具 + * 统一管理环境变量和配置 + */ + +import Constants from 'expo-constants'; +import { Platform } from 'react-native'; + +/** + * 环境类型 + */ +export type Environment = 'development' | 'staging' | 'production'; + +/** + * 获取当前环境 + */ +export const getEnvironment = (): Environment => { + if (__DEV__) { + return 'development'; + } + + // 可以通过环境变量或其他方式判断 staging 环境 + const env = process.env.EXPO_PUBLIC_ENV; + if (env === 'staging') { + return 'staging'; + } + + return 'production'; +}; + +/** + * 获取 API 基础 URL + */ +export const getApiBaseUrl = (): string => { + // 1. 优先使用环境变量 + const envApiUrl = process.env.EXPO_PUBLIC_API_URL; + if (envApiUrl) { + return envApiUrl; + } + + // 2. 根据环境返回不同的 URL + const env = getEnvironment(); + + switch (env) { + case 'development': + // 开发环境 + if (Platform.OS === 'web') { + // Web 平台使用相对路径(会被 webpack devServer 代理) + return '/api'; + } else { + // iOS/Android 使用本机 IP + // ⚠️ 重要:需要替换为你的本机 IP 地址 + // 查看本机 IP: + // - Windows: ipconfig + // - Mac/Linux: ifconfig + // - 或者使用 Metro Bundler 显示的 IP + return 'http://192.168.1.100:3000/api'; + } + + case 'staging': + // 预发布环境 + return 'https://staging-api.yourdomain.com/api'; + + case 'production': + // 生产环境 + return 'https://api.yourdomain.com/api'; + + default: + return '/api'; + } +}; + +/** + * 获取 API 超时时间 + */ +export const getApiTimeout = (): number => { + const timeout = process.env.EXPO_PUBLIC_API_TIMEOUT; + return timeout ? Number(timeout) : 10000; +}; + +/** + * 应用配置 + */ +export const config = { + // 环境 + env: getEnvironment(), + isDev: __DEV__, + + // API 配置 + api: { + baseURL: getApiBaseUrl(), + timeout: getApiTimeout(), + }, + + // 应用信息 + app: { + name: process.env.EXPO_PUBLIC_APP_NAME || 'RN Demo', + version: process.env.EXPO_PUBLIC_APP_VERSION || '1.0.0', + bundleId: Constants.expoConfig?.ios?.bundleIdentifier || '', + packageName: Constants.expoConfig?.android?.package || '', + vk: 'fT6phq0wkOPRlAoyToidAnkogUV7ttGo', + nc: 1, + aseqId: '7', + }, + + // 平台信息 + platform: { + os: Platform.OS, + version: Platform.Version, + isWeb: Platform.OS === 'web', + isIOS: Platform.OS === 'ios', + isAndroid: Platform.OS === 'android', + }, +}; + +/** + * 打印配置信息(仅开发环境) + */ +export const printConfig = () => { + if (__DEV__) { + console.log('📋 App Configuration:', { + environment: config.env, + apiBaseURL: config.api.baseURL, + platform: config.platform.os, + version: config.app.version, + }); + } +}; + +export default config; diff --git a/src/utils/date.ts b/src/utils/date.ts index d58e919..f70b208 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -216,4 +216,3 @@ export const nowInSeconds = (): number => { }; export default dayjs; - diff --git a/src/utils/network/api.ts b/src/utils/network/api.ts new file mode 100644 index 0000000..cae89e5 --- /dev/null +++ b/src/utils/network/api.ts @@ -0,0 +1,589 @@ +/** + * Axios API 配置 + * 统一管理 HTTP 请求 + * + * 功能特性: + * - 自动添加 Token + * - Token 自动刷新 + * - 请求重试机制 + * - 请求取消功能 + * - 统一错误处理 + * - 请求/响应日志 + * - Loading 状态管理 + */ + +import axios, { + AxiosError, + AxiosRequestConfig, + AxiosResponse, + InternalAxiosRequestConfig, + CancelTokenSource, + AxiosRequestHeaders, +} from 'axios'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { router } from 'expo-router'; +import { config } from '../config'; +import { transformRequest, parseResponse } from './helper'; +import { cloneDeep, pick } from 'lodash-es'; +import md5 from 'md5'; + +/** + * API 响应数据结构 + */ +export interface ApiResponse { + code: number; + message: string; + data: T; + success?: boolean; +} + +/** + * API 错误响应 + */ +export interface ApiError { + code: number; + message: string; + errors?: Record; +} + +/** + * 请求配置扩展 + */ +export interface RequestConfig extends AxiosRequestConfig { + /** 是否显示 loading */ + showLoading?: boolean; + /** 是否显示错误提示 */ + showError?: boolean; + /** 是否重试 */ + retry?: boolean; + /** 重试次数 */ + retryCount?: number; + /** 是否需要 token */ + requiresAuth?: boolean; + /** 自定义错误处理 */ + customErrorHandler?: (error: AxiosError) => void; +} + +// API 基础配置 +const API_CONFIG = { + baseURL: config.api.baseURL, + timeout: config.api.timeout, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Accept: 'application/json, application/xml, text/play, text/html, *.*', + }, +}; + +// 创建 axios 实例 +const api = axios.create(API_CONFIG); + +// 请求队列(用于取消请求) +const pendingRequests = new Map(); + +// 是否正在刷新 token +let isRefreshing = false; + +// 刷新 token 时的请求队列 +let refreshSubscribers: Array<(token: string) => void> = []; + +/** + * 生成请求唯一标识 + */ +function generateRequestKey(config: InternalAxiosRequestConfig): string { + const cmdId = config.headers.cmdId || config.url; + const data = cloneDeep(config.method === 'post' ? config.data : config.params); + return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`; +} + +/** + * 添加请求到队列 + */ +function addPendingRequest(config: InternalAxiosRequestConfig): void { + const requestKey = generateRequestKey(config); + + // 如果已存在相同请求,取消之前的请求 + if (pendingRequests.has(requestKey)) { + const source = pendingRequests.get(requestKey); + source?.cancel('重复请求已取消'); + } + + // 创建新的取消令牌 + const source = axios.CancelToken.source(); + config.cancelToken = source.token; + pendingRequests.set(requestKey, source); +} + +/** + * 从队列中移除请求 + */ +function removePendingRequest(config: InternalAxiosRequestConfig | AxiosRequestConfig): void { + const requestKey = generateRequestKey(config as InternalAxiosRequestConfig); + pendingRequests.delete(requestKey); +} + +/** + * 订阅 token 刷新 + */ +function subscribeTokenRefresh(callback: (token: string) => void): void { + refreshSubscribers.push(callback); +} + +/** + * 通知所有订阅者 token 已刷新 + */ +function onTokenRefreshed(token: string): void { + refreshSubscribers.forEach((callback) => callback(token)); + refreshSubscribers = []; +} + +/** + * 刷新 token + */ +async function refreshAccessToken(): Promise { + try { + const refreshToken = await AsyncStorage.getItem('refresh_token'); + + if (!refreshToken) { + throw new Error('No refresh token'); + } + + // 调用刷新 token 接口 + const response = await axios.post>( + `${config.api.baseURL}/auth/refresh-token`, + { refreshToken } + ); + + const { token, refreshToken: newRefreshToken } = response.data.data; + + // 保存新的 token + await AsyncStorage.setItem('auth_token', token); + await AsyncStorage.setItem('refresh_token', newRefreshToken); + + return token; + } catch (error) { + // 刷新失败,清除所有 token + await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); + return null; + } +} + +/** + * 请求拦截器 + * 在请求发送前添加 token 等信息 + */ +api.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + try { + // 添加到请求队列(防止重复请求) + addPendingRequest(config); + + const { apiName } = config.headers; + + const { headers, data } = transformRequest(pick(config, ['headers', 'data'])); + + config.headers = { + ...headers, + ...(__DEV__ ? { apiName } : {}), + } as AxiosRequestHeaders; + + config.data = data; + + if (Number(config.headers.cmdId) !== 381120) { + config.url = '/v2/'; + } + + if (__DEV__ && apiName) { + config.url = `${config.url}?${apiName}`; + } + + // // 从本地存储获取 token + // const token = await AsyncStorage.getItem('auth_token'); + // + // // 添加 token 到请求头 + // if (token && config.headers) { + // config.headers.Authorization = `Bearer ${token}`; + // } + + // 添加请求时间戳(用于计算请求耗时) + (config as any).metadata = { startTime: Date.now() }; + + // 打印请求信息(开发环境) + // if (__DEV__) { + // console.log('📤 API Request:', { + // method: config.method?.toUpperCase(), + // url: config.url, + // baseURL: config.baseURL, + // params: config.params, + // data: config.data, + // headers: config.headers, + // }); + // } + + return config; + } catch (error) { + console.error('❌ Request interceptor error:', error); + return Promise.reject(error); + } + }, + (error) => { + console.error('❌ Request error:', error); + return Promise.reject(error); + } +); + +/** + * 响应拦截器 + * 统一处理响应和错误 + */ +api.interceptors.response.use( + async (response: AxiosResponse) => { + // 从请求队列中移除 + removePendingRequest(response.config); + + // 计算请求耗时 + const duration = Date.now() - (response.config as any).metadata?.startTime; + + const resData: any = await parseResponse(response); + + // 打印响应信息(开发环境) + // if (__DEV__) { + // console.log('📥 API Response:', { + // url: response.config.url, + // status: response.status, + // duration: `${duration}ms`, + // data: response.data, + // }); + // } + + // 统一处理响应数据格式 + // const apiResponse = response.data as ApiResponse; + + // 如果后端返回的数据结构包含 code 和 data + // if (apiResponse && typeof apiResponse === 'object' && 'code' in apiResponse) { + // // 检查业务状态码 + // if (apiResponse.code !== 0 && apiResponse.code !== 200) { + // // 业务错误 + // const error = new Error(apiResponse.message || '请求失败') as any; + // error.code = apiResponse.code; + // error.response = response; + // return Promise.reject(error); + // } + // + // // 返回 data 字段 + // return apiResponse.data; + // } + + // 直接返回响应数据 + // return response.data; + return Promise.resolve(resData); + }, + async (error: AxiosError) => { + // 从请求队列中移除 + if (error.config) { + removePendingRequest(error.config); + } + + // 如果是取消的请求,直接返回 + if (axios.isCancel(error)) { + if (__DEV__) { + console.log('🚫 Request cancelled:', error.message); + } + return Promise.reject(error); + } + + const originalRequest = error.config as RequestConfig & { _retry?: boolean }; + + // 打印错误信息 + if (__DEV__) { + console.error('❌ API Error:', { + method: error.config?.method, + cmdId: error.config?.headers?.cmdId, + status: error.response?.status, + message: error.message, + data: error.response?.data, + }); + } + + // 处理不同的错误状态码 + if (error.response) { + const { status, data } = error.response; + + switch (status) { + case 401: { + // Token 过期,尝试刷新 + if (!originalRequest._retry) { + if (isRefreshing) { + // 如果正在刷新,将请求加入队列 + return new Promise((resolve) => { + subscribeTokenRefresh((token: string) => { + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${token}`; + } + resolve(api(originalRequest)); + }); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + try { + const newToken = await refreshAccessToken(); + + if (newToken) { + // Token 刷新成功 + isRefreshing = false; + onTokenRefreshed(newToken); + + // 重试原请求 + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${newToken}`; + } + return api(originalRequest); + } else { + // Token 刷新失败,跳转到登录页 + isRefreshing = false; + await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); + + // 跳转到登录页 + if (router.canGoBack()) { + router.replace('/(auth)/login' as any); + } + } + } catch (refreshError) { + isRefreshing = false; + await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); + + // 跳转到登录页 + if (router.canGoBack()) { + router.replace('/(auth)/login' as any); + } + + return Promise.reject(refreshError); + } + } + break; + } + + case 403: + // 禁止访问 + console.error('❌ 403: 没有权限访问该资源'); + break; + + case 404: + // 资源不存在 + console.error('❌ 404: 请求的资源不存在'); + break; + + case 422: + // 表单验证错误 + console.error('❌ 422: 表单验证失败', data); + break; + + case 429: + // 请求过于频繁 + console.error('❌ 429: 请求过于频繁,请稍后再试'); + break; + + case 500: + // 服务器错误 + console.error('❌ 500: 服务器内部错误'); + break; + + case 502: + // 网关错误 + console.error('❌ 502: 网关错误'); + break; + + case 503: + // 服务不可用 + console.error('❌ 503: 服务暂时不可用'); + break; + + default: + console.error(`❌ ${status}: 未知错误`); + } + } else if (error.request) { + // 请求已发送但没有收到响应 + console.error('❌ 网络错误: 请检查网络连接'); + } else { + // 请求配置出错 + console.error('❌ 请求配置错误:', error.message); + } + + // 自定义错误处理 + if (originalRequest?.customErrorHandler) { + originalRequest.customErrorHandler(error); + } + + return Promise.reject(error); + } +); + +/** + * 取消所有待处理的请求 + */ +export function cancelAllRequests(message = '请求已取消'): void { + pendingRequests.forEach((source) => { + source.cancel(message); + }); + pendingRequests.clear(); +} + +/** + * 取消指定 URL 的请求 + */ +export function cancelRequest(url: string): void { + pendingRequests.forEach((source, key) => { + if (key.includes(url)) { + source.cancel('请求已取消'); + pendingRequests.delete(key); + } + }); +} + +/** + * 通用请求方法(增强版) + */ +export const request = { + /** + * GET 请求 + */ + get: (url: string, config?: RequestConfig) => api.get(url, config), + + /** + * POST 请求 + */ + post: (url: string, data?: any, config?: RequestConfig) => + api.post(url, data, config), + + /** + * PUT 请求 + */ + put: (url: string, data?: any, config?: RequestConfig) => + api.put(url, data, config), + + /** + * DELETE 请求 + */ + delete: (url: string, config?: RequestConfig) => api.delete(url, config), + + /** + * PATCH 请求 + */ + patch: (url: string, data?: any, config?: RequestConfig) => + api.patch(url, data, config), + + /** + * 上传文件 + */ + upload: ( + url: string, + file: File | Blob, + onProgress?: (progress: number) => void, + config?: RequestConfig + ) => { + const formData = new FormData(); + formData.append('file', file); + + return api.post(url, formData, { + ...config, + headers: { + 'Content-Type': 'multipart/form-data', + ...config?.headers, + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); + onProgress(progress); + } + }, + }); + }, + + /** + * 下载文件 + */ + download: async ( + url: string, + filename?: string, + onProgress?: (progress: number) => void, + config?: RequestConfig + ) => { + const response = await api.get(url, { + ...config, + responseType: 'blob', + onDownloadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); + onProgress(progress); + } + }, + }); + + // 创建下载链接 + const blob = new Blob([response]); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = filename || 'download'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + + return response; + }, + + /** + * 并发请求 + */ + all: (requests: Promise[]) => Promise.all(requests), + + /** + * 串行请求 + */ + series: async (requests: (() => Promise)[]): Promise => { + const results: T[] = []; + for (const request of requests) { + const result = await request(); + results.push(result); + } + return results; + }, +}; + +/** + * 创建带重试的请求 + */ +export function createRetryRequest( + requestFn: () => Promise, + maxRetries = 3, + retryDelay = 1000 +): Promise { + return new Promise((resolve, reject) => { + let retries = 0; + + const attempt = async () => { + try { + const result = await requestFn(); + resolve(result); + } catch (error) { + retries++; + + if (retries < maxRetries) { + if (__DEV__) { + console.log(`🔄 Retrying request (${retries}/${maxRetries})...`); + } + setTimeout(attempt, retryDelay * retries); + } else { + reject(error); + } + } + }; + + attempt(); + }); +} + +export default api; diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 20222fe..d210c1f 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -178,4 +178,3 @@ class Storage { } export default Storage; - diff --git a/tsconfig.json b/tsconfig.json index 909e901..ce27fee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,15 +3,8 @@ "compilerOptions": { "strict": true, "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, - "include": [ - "**/*.ts", - "**/*.tsx", - ".expo/types/**/*.ts", - "expo-env.d.ts" - ] + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] }