Compare commits

...

5 Commits

  1. 7
      .env.development
  2. 27
      .env.example
  3. 4
      .env.production
  4. 55
      .prettierignore
  5. 15
      .prettierrc.json
  6. 301
      README.md
  7. 16
      app.json
  8. 6
      app/(tabs)/_layout.tsx
  9. 337
      app/(tabs)/demo.tsx
  10. 43
      app/(tabs)/index.tsx
  11. 31
      app/(tabs)/paper.tsx
  12. 57
      app/_layout.tsx
  13. 119
      app/test-page.tsx
  14. 259
      app/theme-example.tsx
  15. 317
      app/theme-test.tsx
  16. 12
      components/EditScreenInfo.tsx
  17. 232
      components/ThemeDemo.tsx
  18. 90
      components/Themed.tsx
  19. 16
      components/index.ts
  20. 4
      components/useClientOnlyValue.ts
  21. 12
      components/useClientOnlyValue.web.ts
  22. 1
      components/useColorScheme.ts
  23. 8
      components/useColorScheme.web.ts
  24. 109
      constants/Colors.ts
  25. 38
      constants/network.ts
  26. 56
      docs/LIBRARIES.md
  27. 3
      docs/USAGE_EXAMPLES.md
  28. 1
      eas.json
  29. 26
      hooks/index.ts
  30. 23
      hooks/useClientOnlyValue.ts
  31. 34
      hooks/useClientOnlyValue.web.ts
  32. 2
      hooks/useDebounce.ts
  33. 11
      hooks/useHaptics.ts
  34. 291
      hooks/useRequest.ts
  35. 100
      hooks/useTheme.ts
  36. 5
      hooks/useThrottle.ts
  37. 19
      metro.config.js
  38. 15
      package.json
  39. 502
      pnpm-lock.yaml
  40. 31
      schemas/auth.ts
  41. 36
      schemas/index.ts
  42. 12
      schemas/user.ts
  43. 36
      screens/index.ts
  44. 111
      scripts/proxy-server.js
  45. 29
      services/authService.ts
  46. 8
      services/index.ts
  47. 43
      services/tenantService.ts
  48. 19
      services/userService.ts
  49. 29
      src/index.ts
  50. 147
      src/stores/settingsStore.ts
  51. 142
      src/stores/userStore.ts
  52. 144
      src/utils/api.ts
  53. 36
      stores/index.ts
  54. 167
      stores/settingsStore.ts
  55. 122
      stores/tenantStore.ts
  56. 161
      stores/userStore.ts
  57. 36
      theme/index.ts
  58. 261
      theme/styles.ts
  59. 122
      theme/utils.ts
  60. 11
      tsconfig.json
  61. 73
      types/api.ts
  62. 1
      types/index.ts
  63. 0
      utils/common.ts
  64. 132
      utils/config.ts
  65. 1
      utils/date.ts
  66. 38
      utils/index.ts
  67. 589
      utils/network/api.ts
  68. 492
      utils/network/des.ts
  69. 119
      utils/network/error.ts
  70. 434
      utils/network/helper.ts
  71. 220
      utils/sessionStorage.ts
  72. 19
      utils/storage.ts
  73. 242
      utils/storageManager.ts

7
.env.development

@ -0,0 +1,7 @@
# 开发环境配置
# EXPO_PUBLIC_API_URL=/api # 注释掉,让 config.ts 根据平台自动选择
EXPO_PUBLIC_API_TIMEOUT=10000
# 测试环境的域名
API_TARGET=https://51zhh5.notbug.org

27
.env.example

@ -1,11 +1,34 @@
# 环境变量示例文件
# 复制此文件为 .env 并填入实际值
# ============================================
# API 配置 # 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_NAME=RN Demo
EXPO_PUBLIC_APP_VERSION=1.0.0 EXPO_PUBLIC_APP_VERSION=1.0.0
# 其他配置 # ============================================
# 其他配置(可选)
# ============================================
# Sentry 错误追踪
# EXPO_PUBLIC_SENTRY_DSN= # EXPO_PUBLIC_SENTRY_DSN=
# 分析工具 ID
# EXPO_PUBLIC_ANALYTICS_ID= # EXPO_PUBLIC_ANALYTICS_ID=

4
.env.production

@ -0,0 +1,4 @@
# 生产环境配置
EXPO_PUBLIC_API_URL=/
EXPO_PUBLIC_API_TIMEOUT=10000

55
.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

15
.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"
}

301
README.md

@ -20,15 +20,28 @@
## ✨ 项目特性 ## ✨ 项目特性
### 核心特性
- 🎯 **Expo Router** - 文件路由系统,类似 Next.js,支持类型安全的导航 - 🎯 **Expo Router** - 文件路由系统,类似 Next.js,支持类型安全的导航
- 📘 **TypeScript** - 完整的类型支持,提升代码质量 - 📘 **TypeScript** - 完整的类型支持,提升代码质量
- 🔥 **EAS Update** - 热更新支持(CodePush 的官方替代方案) - 🔥 **EAS Update** - 热更新支持(CodePush 的官方替代方案)
- ⚡ **React Native 新架构** - 启用 Fabric 渲染器和 TurboModules - ⚡ **React Native 新架构** - 启用 Fabric 渲染器和 TurboModules
- 📦 **pnpm** - 快速、节省磁盘空间的包管理器 - 📦 **pnpm** - 快速、节省磁盘空间的包管理器
- 🧭 **标签导航** - 开箱即用的导航示例 - 🧭 **标签导航** - 开箱即用的导航示例
- 🎨 **主题支持** - 内置深色/浅色主题切换 - 🎨 **完整主题系统** - 深色/浅色/自动主题切换,完整的颜色配置 🎯
- 📱 **跨平台** - 支持 iOS、Android 和 Web - 📱 **跨平台** - 支持 iOS、Android 和 Web
### 架构特性 🎯
- 📁 **扁平化目录结构** - 清晰的模块组织,符合社区最佳实践
- 🔄 **模块化导出** - 每个目录独立导出,避免循环依赖
- 💾 **双存储系统** - AsyncStorage(持久化)+ SessionStorage(临时)
- 🎨 **主题化组件** - ThemedText 和 ThemedView,自动适配主题
- 🔐 **API 加密** - 请求/响应自动加密解密
- 📊 **状态管理** - Zustand + AsyncStorage 持久化
- ✅ **数据验证** - Zod 表单验证
- 🎯 **类型安全** - 完整的 TypeScript 类型定义
## 🚀 快速开始 ## 🚀 快速开始
### 前置要求 ### 前置要求
@ -69,16 +82,19 @@ pnpm start
#### 方法 2:使用模拟器 #### 方法 2:使用模拟器
**Android 模拟器:** **Android 模拟器:**
```bash ```bash
pnpm android pnpm android
``` ```
**iOS 模拟器(仅 macOS):** **iOS 模拟器(仅 macOS):**
```bash ```bash
pnpm ios pnpm ios
``` ```
**Web 浏览器:** **Web 浏览器:**
```bash ```bash
pnpm web pnpm web
``` ```
@ -91,53 +107,100 @@ rn-demo/
│ ├── (tabs)/ # 标签导航组 │ ├── (tabs)/ # 标签导航组
│ │ ├── index.tsx # 首页 - 热更新演示 ⭐ │ │ ├── index.tsx # 首页 - 热更新演示 ⭐
│ │ ├── two.tsx # 第二个标签页 │ │ ├── two.tsx # 第二个标签页
│ │ ├── demo.tsx # 完整示例页面 │ │ ├── demo.tsx # 完整示例页面 🎯
│ │ ├── paper.tsx # React Native Paper 示例
│ │ └── _layout.tsx # 标签布局 │ │ └── _layout.tsx # 标签布局
│ ├── test-page.tsx # 测试页面(无 tabs)🎯
│ ├── _layout.tsx # 根布局 - 自动检查更新 ⭐ │ ├── _layout.tsx # 根布局 - 自动检查更新 ⭐
│ ├── modal.tsx # 模态页面示例 │ ├── modal.tsx # 模态页面示例
│ ├── +html.tsx # Web HTML 模板 │ ├── +html.tsx # Web HTML 模板
│ └── +not-found.tsx # 404 页面 │ └── +not-found.tsx # 404 页面
├── 💻 src/ # 源代码目录 ├── 💻 业务代码目录
│ ├── utils/ # 工具函数 ├── utils/ # 工具函数
│ │ ├── api.ts # Axios API 配置 │ ├── network/ # 网络相关
│ │ ├── storage.ts # AsyncStorage 封装 │ │ ├── api.ts # Axios API 配置
│ │ └── date.ts # Day.js 日期工具 │ │ ├── helper.ts # 加密/解密工具
│ ├── stores/ # Zustand 状态管理 │ │ └── error.ts # 错误处理
│ │ ├── userStore.ts # 用户状态 │ ├── storage.ts # AsyncStorage 封装
│ │ └── settingsStore.ts # 应用设置 │ ├── sessionStorage.ts # Session Storage 实现 🎯
│ ├── schemas/ # Zod 验证规则 │ ├── storageManager.ts # 统一存储管理器 🎯
│ │ ├── auth.ts # 认证验证 │ ├── config.ts # 配置管理
│ │ └── user.ts # 用户验证 │ ├── date.ts # Day.js 日期工具
│ ├── services/ # API 服务层 │ ├── common.ts # 通用工具
│ │ ├── authService.ts # 认证服务 │ └── index.ts # 统一导出
│ │ └── userService.ts # 用户服务
│ ├── hooks/ # 自定义 Hooks ├── stores/ # Zustand 状态管理
│ │ ├── useDebounce.ts # 防抖 Hook │ ├── userStore.ts # 用户状态
│ │ ├── useThrottle.ts # 节流 Hook │ ├── settingsStore.ts # 应用设置
│ │ └── useHaptics.ts # 触觉反馈 Hook │ ├── tenantStore.ts # 租户状态 🎯
│ ├── types/ # TypeScript 类型 │ └── index.ts # 统一导出
│ │ └── index.ts # 全局类型定义
│ └── index.ts # 统一导出 ⭐ ├── schemas/ # Zod 验证规则
│ ├── auth.ts # 认证验证
│ ├── user.ts # 用户验证
│ └── index.ts # 统一导出
├── services/ # API 服务层
│ ├── authService.ts # 认证服务
│ ├── userService.ts # 用户服务
│ ├── tenantService.ts # 租户服务 🎯
│ └── index.ts # 统一导出
├── hooks/ # 自定义 Hooks
│ ├── useDebounce.ts # 防抖 Hook
│ ├── useThrottle.ts # 节流 Hook
│ ├── useHaptics.ts # 触觉反馈 Hook
│ ├── useRequest.ts # 请求 Hook 🎯
│ ├── useTheme.ts # 主题 Hooks 🎯
│ ├── useColorScheme.ts # 颜色方案 Hook 🎯
│ ├── useColorScheme.web.ts # Web 平台颜色方案 🎯
│ ├── useClientOnlyValue.ts # 客户端值 Hook 🎯
│ ├── useClientOnlyValue.web.ts # Web 平台客户端值 🎯
│ └── index.ts # 统一导出
├── types/ # TypeScript 类型
│ ├── api.ts # API 类型定义
│ └── index.ts # 统一导出
├── screens/ # 业务页面组件 🎯
│ └── index.ts # 统一导出
├── 🧩 components/ # 可复用组件 ├── 🧩 components/ # 可复用组件
│ ├── Themed.tsx # 主题化组件 │ ├── Themed.tsx # 主题化组件 🎯
│ ├── ThemeDemo.tsx # 主题演示组件 🎯
│ ├── ExternalLink.tsx # 外部链接组件 │ ├── ExternalLink.tsx # 外部链接组件
│ └── useColorScheme.ts # 主题 Hook │ └── index.ts # 统一导出 🎯
├── 🎨 theme/ # 主题系统 🎯 NEW!
│ ├── index.ts # 统一导出
│ ├── utils.ts # 主题工具函数
│ └── styles.ts # 样式工厂(类似 CSS 类名)
├── 🎯 constants/ # 常量配置 ├── 🎯 constants/ # 常量配置
│ └── Colors.ts # 颜色主题 │ ├── Colors.ts # 颜色主题(完整配置)🎯
│ └── network.ts # 网络常量
├── 🎨 assets/ # 静态资源 ├── 🎨 assets/ # 静态资源
│ ├── images/ # 图片资源 │ ├── images/ # 图片资源
│ └── fonts/ # 字体文件 │ └── fonts/ # 字体文件
├── 📚 docs/ # 项目文档 ├── 📚 docs/ # 项目文档 🎯
│ ├── USAGE_EXAMPLES.md # 使用示例 │ ├── USAGE_EXAMPLES.md # 使用示例
│ └── LIBRARIES.md # 工具库使用指南 │ ├── LIBRARIES.md # 工具库使用指南
│ ├── PROJECT_STRUCTURE_V2.md # 项目结构说明 🎯
│ ├── MIGRATION_TO_FLAT_STRUCTURE.md # 扁平化迁移报告 🎯
│ ├── STORES_ARCHITECTURE.md # Stores 架构设计 🎯
│ ├── STORAGE_GUIDE.md # 存储系统使用指南 🎯
│ ├── THEME_GUIDE.md # 主题系统使用指南 🎯 NEW!
│ └── ... 更多文档
├── 🔧 scripts/ # 脚本文件
│ └── proxy-server.js # 代理服务器 🎯
├── ⚙ 配置文件 ├── ⚙ 配置文件
│ ├── .env.example # 环境变量示例 🎯 NEW! │ ├── .env.development # 开发环境变量 🎯
│ ├── .env.production # 生产环境变量 🎯
│ ├── app.json # Expo 配置 ⭐ │ ├── app.json # Expo 配置 ⭐
│ ├── eas.json # EAS 构建和更新配置 ⭐ │ ├── eas.json # EAS 构建和更新配置 ⭐
│ ├── package.json # 项目依赖 │ ├── package.json # 项目依赖
@ -150,78 +213,134 @@ rn-demo/
└── CHANGELOG.md # 更新日志 └── CHANGELOG.md # 更新日志
⭐ = 热更新相关的关键文件 ⭐ = 热更新相关的关键文件
🎯 = 新增的文件/目录 🎯 = 新增/更新的文件/目录
``` ```
### 关键目录说明 ### 关键目录说明
#### 📱 `app/` - 路由和页面 #### 📱 `app/` - 路由和页面
- **`app/_layout.tsx`** - 应用启动时自动检查更新 - **`app/_layout.tsx`** - 应用启动时自动检查更新
- **`app/(tabs)/index.tsx`** - 热更新演示页面 - **`app/(tabs)/index.tsx`** - 热更新演示页面
- **`app/(tabs)/demo.tsx`** - 完整示例页面,展示所有工具的使用 🎯 - **`app/(tabs)/demo.tsx`** - 完整示例页面,展示所有工具的使用 🎯
- **`app/test-page.tsx`** - 测试页面示例(不包含底部 tabs)🎯
#### 💻 业务代码目录(扁平化结构)🎯
项目采用扁平化目录结构,所有业务模块都在根目录下:
- **`utils/`** - 工具函数
- 网络请求(Axios + 加密)
- 存储管理(AsyncStorage + SessionStorage)🎯
- 日期处理(Day.js)
- 配置管理
- **`stores/`** - 状态管理(Zustand)
- 用户状态(登录、用户信息)
- 应用设置(主题、语言、触觉反馈)
- 租户状态 🎯
- **`schemas/`** - 数据验证(Zod)
- 认证表单验证
- 用户数据验证
#### 💻 `src/` - 源代码目录 🎯
项目的核心业务逻辑代码,包含:
- **`utils/`** - 工具函数(API、存储、日期)
- **`stores/`** - 状态管理(用户、设置)
- **`schemas/`** - 数据验证(认证、用户)
- **`services/`** - API 服务层 - **`services/`** - API 服务层
- 认证服务
- 用户服务
- 租户服务 🎯
- **`hooks/`** - 自定义 Hooks - **`hooks/`** - 自定义 Hooks
- 防抖/节流
- 触觉反馈
- 请求管理 🎯
- 主题 Hooks(useColorScheme, useTheme, useClientOnlyValue)🎯
- **`types/`** - TypeScript 类型定义 - **`types/`** - TypeScript 类型定义
- **`index.ts`** - 统一导出所有模块
- **`screens/`** - 业务页面组件 🎯
- **`theme/`** - 主题系统 🎯 NEW!
- 统一的主题配置导出
- 主题工具函数
- 样式工厂(类似 CSS 类名)
每个目录都有 `index.ts` 文件,负责统一导出该目录下的所有模块。
#### 🧩 `components/` - 可复用组件
- **`Themed.tsx`** - 主题化组件(ThemedText, ThemedView)🎯
- **`ThemeDemo.tsx`** - 主题演示组件 🎯
#### 📚 `docs/` - 项目文档 🎯 #### 📚 `docs/` - 项目文档 🎯
完善的项目文档,包含: 完善的项目文档,包含:
- **使用指南** - 如何使用各个工具 - **使用指南** - 如何使用各个工具
- **代码示例** - 实际的代码示例 - **代码示例** - 实际的代码示例
- **配置说明** - 项目配置详解 - **配置说明** - 项目配置详解
- **架构设计** - Stores 架构、存储系统等
- **主题系统** - 主题配置和样式使用指南 🎯 NEW!
- **迁移报告** - 扁平化目录结构迁移
### 核心文件说明 ### 核心文件说明
#### 热更新相关 ⭐ #### 热更新相关 ⭐
- **`app.json`** - Expo 配置,包含热更新设置 - **`app.json`** - Expo 配置,包含热更新设置
- **`eas.json`** - EAS 构建和更新通道配置 - **`eas.json`** - EAS 构建和更新通道配置
- **`app/_layout.tsx`** - 自动检查更新逻辑 - **`app/_layout.tsx`** - 自动检查更新逻辑
- **`app/(tabs)/index.tsx`** - 手动检查更新功能 - **`app/(tabs)/index.tsx`** - 手动检查更新功能
#### 工具配置 🎯 #### 工具配置 🎯
- **`src/index.ts`** - 统一导出,从这里导入所有工具
- **`.env.example`** - 环境变量配置示例 - **各目录的 `index.ts`** - 统一导出,从这里导入所有工具
- **`.env.development` / `.env.production`** - 环境变量配置 🎯
- **`tsconfig.json`** - 配置了路径别名 `@/*` - **`tsconfig.json`** - 配置了路径别名 `@/*`
### 已安装的工具库 ### 已安装的工具库
项目已安装并配置好以下工具库: 项目已安装并配置好以下工具库:
| 类别 | 工具库 | 用途 | | 类别 | 工具库 | 用途 | 状态 |
|------|--------|------| | ------------ | ----------------------------------------- | ------------------- | ---- |
| **工具类** | lodash-es | JavaScript 工具函数 | | **工具类** | lodash-es | JavaScript 工具函数 | ✅ |
| | dayjs | 日期处理 | | | dayjs | 日期处理 | ✅ |
| | axios | HTTP 请求 | | | axios | HTTP 请求 | ✅ |
| **状态管理** | zustand | 轻量级状态管理 | | **状态管理** | zustand | 轻量级状态管理 | ✅ |
| **表单处理** | react-hook-form | 表单管理 | | **表单处理** | react-hook-form | 表单管理 | ✅ |
| | zod | 数据验证 | | | zod | 数据验证 | ✅ |
| **原生功能** | @react-native-async-storage/async-storage | 本地存储 | | **原生功能** | @react-native-async-storage/async-storage | 本地存储 | ✅ |
| | expo-image | 优化的图片组件 | | | expo-image | 优化的图片组件 | ✅ |
| | expo-haptics | 触觉反馈 | | | expo-haptics | 触觉反馈 | ✅ |
| **UI 组件** | react-native-paper | Material Design 组件 | ✅ |
### 快速开始 ### 快速开始
1. **查看完整示例** - 运行应用,点击 "完整示例" tab 🎯 1. **查看完整示例** - 运行应用,点击 "Demo" tab 🎯
2. **阅读文档** - 查看 [docs/](./docs/) 目录中的文档 2. **阅读文档** - 查看 [docs/](./docs/) 目录中的文档
3. **使用工具** - 从 `@/src` 导入所需的工具 3. **使用工具** - 从各个模块目录导入所需的工具
```typescript ```typescript
// 示例:导入工具 // ✅ 推荐:从模块目录导入
import { Storage, SessionStorage, StorageManager, formatDate } from '@/utils';
import { useUser, useTheme, useLanguage } from '@/stores';
import { authService, userService } from '@/services';
import { useDebounce, useHaptics, useColorScheme, useThemeColors, useClientOnlyValue } from '@/hooks';
import { loginSchema } from '@/schemas';
// ✅ 主题系统:统一从 theme 目录导入 🎯 NEW!
import { import {
api, useColorScheme,
Storage, useThemeColors,
formatDate, ThemedText,
useUserStore, ThemedView,
authService, commonStyles,
useDebounce, createThemeStyles,
useHaptics, } from '@/theme';
} from '@/src';
// ✅ 也可以:直接从具体文件导入
import { useUser } from '@/stores/userStore';
import { formatDate } from '@/utils/date';
``` ```
## 🔥 热更新使用指南 ## 🔥 热更新使用指南
@ -243,22 +362,26 @@ eas init
``` ```
这会: 这会:
- 创建一个唯一的项目 ID - 创建一个唯一的项目 ID
- 自动更新 `app.json` 中的 `extra.eas.projectId` - 自动更新 `app.json` 中的 `extra.eas.projectId`
### 步骤 3:构建开发版本 ### 步骤 3:构建开发版本
**Android 开发构建:** **Android 开发构建:**
```bash ```bash
eas build --profile development --platform android eas build --profile development --platform android
``` ```
**iOS 开发构建(需要 macOS 和 Apple 开发者账号):** **iOS 开发构建(需要 macOS 和 Apple 开发者账号):**
```bash ```bash
eas build --profile development --platform ios eas build --profile development --platform ios
``` ```
构建过程需要 **10-20 分钟**。完成后: 构建过程需要 **10-20 分钟**。完成后:
1. 在 [expo.dev](https://expo.dev) 控制台下载构建的 APK/IPA 文件 1. 在 [expo.dev](https://expo.dev) 控制台下载构建的 APK/IPA 文件
2. 安装到你的设备上 2. 安装到你的设备上
@ -291,11 +414,11 @@ eas update --channel production --message "v1.0.1: 添加了新功能"
项目配置了三个更新通道(在 `eas.json` 中定义): 项目配置了三个更新通道(在 `eas.json` 中定义):
| 通道 | 用途 | 适用场景 | | 通道 | 用途 | 适用场景 |
|------|------|----------| | --------------- | -------- | -------------- |
| **development** | 开发环境 | 日常开发和测试 | | **development** | 开发环境 | 日常开发和测试 |
| **preview** | 预览环境 | 内部测试和 QA | | **preview** | 预览环境 | 内部测试和 QA |
| **production** | 生产环境 | 正式发布给用户 | | **production** | 生产环境 | 正式发布给用户 |
不同的构建配置会订阅不同的更新通道: 不同的构建配置会订阅不同的更新通道:
@ -404,12 +527,9 @@ button: {
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
// 在 JSX 中添加 // 在 JSX 中添加
<TouchableOpacity <TouchableOpacity style={styles.button} onPress={() => setCount(count + 1)}>
style={styles.button}
onPress={() => setCount(count + 1)}
>
<Text style={styles.buttonText}>点击次数: {count}</Text> <Text style={styles.buttonText}>点击次数: {count}</Text>
</TouchableOpacity> </TouchableOpacity>;
``` ```
修改后,运行 `eas update` 发布更新,然后在应用中检查更新即可看到变化。 修改后,运行 `eas update` 发布更新,然后在应用中检查更新即可看到变化。
@ -459,16 +579,16 @@ Alert.alert(
"name": "rn-demo", "name": "rn-demo",
"slug": "rn-demo", "slug": "rn-demo",
"version": "1.0.0", "version": "1.0.0",
"newArchEnabled": true, // 启用 React Native 新架构 "newArchEnabled": true, // 启用 React Native 新架构
"updates": { "updates": {
"url": "https://u.expo.dev/your-project-id" // EAS Update 服务器 "url": "https://u.expo.dev/your-project-id" // EAS Update 服务器
}, },
"runtimeVersion": { "runtimeVersion": {
"policy": "appVersion" // 运行时版本策略 "policy": "appVersion" // 运行时版本策略
}, },
"plugins": [ "plugins": [
"expo-router", // Expo Router 插件 "expo-router", // Expo Router 插件
"expo-updates" // 热更新插件 "expo-updates" // 热更新插件
], ],
"ios": { "ios": {
"bundleIdentifier": "com.rndemo.app" "bundleIdentifier": "com.rndemo.app"
@ -481,6 +601,7 @@ Alert.alert(
``` ```
**配置说明:** **配置说明:**
- `updates.url` - EAS Update 服务器地址(运行 `eas init` 后自动生成) - `updates.url` - EAS Update 服务器地址(运行 `eas init` 后自动生成)
- `runtimeVersion.policy` - 运行时版本策略,确保更新兼容性 - `runtimeVersion.policy` - 运行时版本策略,确保更新兼容性
- `appVersion` - 基于 `version` 字段(当前使用) - `appVersion` - 基于 `version` 字段(当前使用)
@ -496,22 +617,23 @@ Alert.alert(
{ {
"build": { "build": {
"development": { "development": {
"developmentClient": true, // 开发客户端 "developmentClient": true, // 开发客户端
"distribution": "internal", // 内部分发 "distribution": "internal", // 内部分发
"channel": "development" // 订阅 development 更新通道 "channel": "development" // 订阅 development 更新通道
}, },
"preview": { "preview": {
"distribution": "internal", "distribution": "internal",
"channel": "preview" // 订阅 preview 更新通道 "channel": "preview" // 订阅 preview 更新通道
}, },
"production": { "production": {
"channel": "production" // 订阅 production 更新通道 "channel": "production" // 订阅 production 更新通道
} }
} }
} }
``` ```
**配置说明:** **配置说明:**
- `developmentClient` - 是否为开发客户端(包含开发工具) - `developmentClient` - 是否为开发客户端(包含开发工具)
- `distribution` - 分发方式(`internal` 或 `store` - `distribution` - 分发方式(`internal` 或 `store`
- `channel` - 更新通道,决定应用接收哪个通道的更新 - `channel` - 更新通道,决定应用接收哪个通道的更新
@ -521,6 +643,7 @@ Alert.alert(
### 何时使用热更新 ### 何时使用热更新
✅ **适合热更新的场景:** ✅ **适合热更新的场景:**
- ✅ 修复 JavaScript/TypeScript 代码 bug - ✅ 修复 JavaScript/TypeScript 代码 bug
- ✅ 更新 UI 样式和布局 - ✅ 更新 UI 样式和布局
- ✅ 修改业务逻辑 - ✅ 修改业务逻辑
@ -529,6 +652,7 @@ Alert.alert(
- ✅ 添加新的 JS 功能 - ✅ 添加新的 JS 功能
❌ **不适合热更新的场景(需要重新构建):** ❌ **不适合热更新的场景(需要重新构建):**
- ❌ 添加/删除原生依赖(如 `react-native-camera` - ❌ 添加/删除原生依赖(如 `react-native-camera`
- ❌ 修改原生代码(iOS/Android) - ❌ 修改原生代码(iOS/Android)
- ❌ 更改应用权限(如相机、位置权限) - ❌ 更改应用权限(如相机、位置权限)
@ -539,11 +663,13 @@ Alert.alert(
### 版本管理策略 ### 版本管理策略
**appVersion 策略**(当前使用): **appVersion 策略**(当前使用):
- 基于 `app.json` 中的 `version` 字段 - 基于 `app.json` 中的 `version` 字段
- ✅ 优点:简单直观,易于理解 - ✅ 优点:简单直观,易于理解
- ⚠ 注意:每次原生构建需要手动更新版本号 - ⚠ 注意:每次原生构建需要手动更新版本号
**nativeVersion 策略**: **nativeVersion 策略**:
- 基于原生构建号(iOS 的 `buildNumber`,Android 的 `versionCode` - 基于原生构建号(iOS 的 `buildNumber`,Android 的 `versionCode`
- ✅ 优点:自动管理,无需手动更新 - ✅ 优点:自动管理,无需手动更新
- ⚠ 注意:需要配置原生构建号 - ⚠ 注意:需要配置原生构建号
@ -564,6 +690,7 @@ Alert.alert(
### Q: 更新后没有生效? ### Q: 更新后没有生效?
**A:** 检查以下几点: **A:** 检查以下几点:
1. 确保应用完全关闭后重新打开(不是后台切换) 1. 确保应用完全关闭后重新打开(不是后台切换)
2. 检查更新通道是否匹配(development/preview/production) 2. 检查更新通道是否匹配(development/preview/production)
3. 确保 `runtimeVersion` 匹配 3. 确保 `runtimeVersion` 匹配
@ -573,6 +700,7 @@ Alert.alert(
### Q: 如何回滚到之前的版本? ### Q: 如何回滚到之前的版本?
**A:** **A:**
```bash ```bash
# 查看更新历史 # 查看更新历史
eas update:list --channel production eas update:list --channel production
@ -584,6 +712,7 @@ eas update --channel production --branch main --message "回滚到稳定版本"
### Q: 热更新的大小限制是多少? ### Q: 热更新的大小限制是多少?
**A:** EAS Update 没有严格的大小限制,但建议: **A:** EAS Update 没有严格的大小限制,但建议:
- 保持更新包 < 10MB 以确保良好的用户体验 - 保持更新包 < 10MB 以确保良好的用户体验
- 避免在更新中包含大量图片或资源文件 - 避免在更新中包含大量图片或资源文件
- 使用 CDN 托管大型资源 - 使用 CDN 托管大型资源
@ -599,7 +728,7 @@ if (update.isAvailable) {
'发现新版本', '发现新版本',
'请更新到最新版本', '请更新到最新版本',
[{ text: '立即更新', onPress: async () => await Updates.reloadAsync() }], [{ text: '立即更新', onPress: async () => await Updates.reloadAsync() }],
{ cancelable: false } // 不可取消 { cancelable: false } // 不可取消
); );
} }
``` ```
@ -607,6 +736,7 @@ if (update.isAvailable) {
### Q: 更新检查的频率是多少? ### Q: 更新检查的频率是多少?
**A:** **A:**
- **应用启动时**:自动检查(在 `app/_layout.tsx` 中配置) - **应用启动时**:自动检查(在 `app/_layout.tsx` 中配置)
- **手动检查**:用户点击"检查更新"按钮 - **手动检查**:用户点击"检查更新"按钮
- **后台检查**:可以配置定时检查(需要自己实现) - **后台检查**:可以配置定时检查(需要自己实现)
@ -614,6 +744,7 @@ if (update.isAvailable) {
### Q: 如何在开发时测试热更新? ### Q: 如何在开发时测试热更新?
**A:** **A:**
1. 构建 Development Build:`eas build --profile development --platform android` 1. 构建 Development Build:`eas build --profile development --platform android`
2. 安装到设备 2. 安装到设备
3. 修改代码 3. 修改代码
@ -635,6 +766,7 @@ if (update.isAvailable) {
## 📚 相关资源 ## 📚 相关资源
### 官方文档 ### 官方文档
- [Expo 官方文档](https://docs.expo.dev/) - [Expo 官方文档](https://docs.expo.dev/)
- [Expo Router 文档](https://docs.expo.dev/router/introduction/) - [Expo Router 文档](https://docs.expo.dev/router/introduction/)
- [EAS Update 文档](https://docs.expo.dev/eas-update/introduction/) - [EAS Update 文档](https://docs.expo.dev/eas-update/introduction/)
@ -642,11 +774,13 @@ if (update.isAvailable) {
- [React Native 文档](https://reactnative.dev/) - [React Native 文档](https://reactnative.dev/)
### 学习资源 ### 学习资源
- [Expo Router 最佳实践](https://docs.expo.dev/router/best-practices/) - [Expo Router 最佳实践](https://docs.expo.dev/router/best-practices/)
- [EAS Update 最佳实践](https://docs.expo.dev/eas-update/best-practices/) - [EAS Update 最佳实践](https://docs.expo.dev/eas-update/best-practices/)
- [React Native 新架构](https://reactnative.dev/docs/the-new-architecture/landing-page) - [React Native 新架构](https://reactnative.dev/docs/the-new-architecture/landing-page)
### 社区 ### 社区
- [Expo Discord](https://chat.expo.dev/) - [Expo Discord](https://chat.expo.dev/)
- [Expo Forums](https://forums.expo.dev/) - [Expo Forums](https://forums.expo.dev/)
- [React Native Community](https://reactnative.dev/community/overview) - [React Native Community](https://reactnative.dev/community/overview)
@ -655,13 +789,14 @@ if (update.isAvailable) {
更多详细文档请查看 [docs](./docs/) 目录: 更多详细文档请查看 [docs](./docs/) 目录:
### 使用指南
- **[使用示例](./docs/USAGE_EXAMPLES.md)** - 实际代码示例 - **[使用示例](./docs/USAGE_EXAMPLES.md)** - 实际代码示例
- **[工具库使用指南](./docs/LIBRARIES.md)** - 详细的工具库使用方法和示例 - **[工具库使用指南](./docs/LIBRARIES.md)** - 详细的工具库使用方法和示例
- **[存储系统使用指南](./docs/STORAGE_GUIDE.md)** - AsyncStorage 和 SessionStorage 使用 🎯
--- ---
**祝你开发愉快!** 🎉 **祝你开发愉快!** 🎉
如有问题,请查看 [常见问题](#-常见问题) 或访问 [Expo 官方文档](https://docs.expo.dev/)。 如有问题,请查看 [常见问题](#-常见问题) 或访问 [Expo 官方文档](https://docs.expo.dev/)。

16
app.json

@ -31,23 +31,9 @@
"output": "static", "output": "static",
"favicon": "./assets/images/favicon.png" "favicon": "./assets/images/favicon.png"
}, },
"plugins": [ "plugins": ["expo-router"],
"expo-router",
"expo-updates"
],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true
},
"updates": {
"url": "https://u.expo.dev/your-project-id"
},
"runtimeVersion": {
"policy": "appVersion"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
} }
} }
} }

6
app/(tabs)/_layout.tsx

@ -4,8 +4,7 @@ import { Link, Tabs } from 'expo-router';
import { Pressable } from 'react-native'; import { Pressable } from 'react-native';
import Colors from '@/constants/Colors'; import Colors from '@/constants/Colors';
import { useColorScheme } from '@/components/useColorScheme'; import { useColorScheme, useClientOnlyValue } from '@/hooks';
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
function TabBarIcon(props: { function TabBarIcon(props: {
@ -25,7 +24,8 @@ export default function TabLayout() {
// Disable the static render of the header on web // Disable the static render of the header on web
// to prevent a hydration error in React Navigation v6. // to prevent a hydration error in React Navigation v6.
headerShown: useClientOnlyValue(false, true), headerShown: useClientOnlyValue(false, true),
}}> }}
>
<Tabs.Screen <Tabs.Screen
name="index" name="index"
options={{ options={{

337
app/(tabs)/demo.tsx

@ -18,17 +18,23 @@ import {
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { useRouter } from 'expo-router';
// 导入所有工具 // ✅ 扁平化导入:从根目录的各个模块导入
// 工具函数
import { import {
// 工具函数
Storage, Storage,
STORAGE_KEYS, STORAGE_KEYS,
SessionStorage,
SESSION_KEYS,
formatDate, formatDate,
formatRelativeTime, formatRelativeTime,
formatChatTime, formatChatTime
} from '@/utils';
// 状态管理
// 状态管理
import {
useUserStore, useUserStore,
useUser, useUser,
useIsLoggedIn, useIsLoggedIn,
@ -36,43 +42,54 @@ import {
useTheme, useTheme,
useLanguage, useLanguage,
useHapticsEnabled, useHapticsEnabled,
useSettingsActions,
// 验证规则 useTenantStates,
loginSchema, useTenantInfo,
type LoginFormData, } from '@/stores';
// API 服务 // 验证规则
authService, import { loginSchema } from '@/schemas';
import type { LoginFormData } from '@/schemas';
// 自定义 Hooks
useDebounce, // API 服务
useThrottle, import { authService } from '@/services';
useHaptics,
} from '@/src'; // 自定义 Hooks
import { useDebounce, useThrottle, useHaptics } from '@/hooks';
// 主题组件
import { ThemeDemo } from '@/components/ThemeDemo';
export default function DemoScreen() { export default function DemoScreen() {
console.log('=== DemoScreen 组件已渲染 ===');
const haptics = useHaptics(); const haptics = useHaptics();
const router = useRouter();
// 状态管理示例 // 状态管理示例
const user = useUser(); const user = useUser();
const isLoggedIn = useIsLoggedIn(); const isLoggedIn = useIsLoggedIn();
const login = useUserStore((state) => state.login); const login = useUserStore((state) => state.login);
const logout = useUserStore((state) => state.logout); const logout = useUserStore((state) => state.logout);
const { tenantLoad } = useTenantStates();
const tenantInfo = useTenantInfo();
// 设置状态 // 设置状态
const theme = useTheme(); const theme = useTheme();
const language = useLanguage(); const language = useLanguage();
const hapticsEnabled = useHapticsEnabled(); const hapticsEnabled = useHapticsEnabled();
const setTheme = useSettingsStore((state) => state.setTheme); const { setTheme, setLanguage, setHapticsEnabled } = useSettingsActions();
const setLanguage = useSettingsStore((state) => state.setLanguage); // const setTheme = useSettingsStore((state) => state.setTheme);
const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled); // const setLanguage = useSettingsStore((state) => state.setLanguage);
// const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
// 本地状态 // 本地状态
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [searchResults, setSearchResults] = useState<string[]>([]); const [searchResults, setSearchResults] = useState<string[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [counter, setCounter] = useState(0); const [counter, setCounter] = useState(0);
const [storageValue, setStorageValue] = useState(''); const [storageValue, setStorageValue] = useState('');
const [sessionValue, setSessionValue] = useState('');
// 表单配置 // 表单配置
const { const {
@ -89,6 +106,7 @@ export default function DemoScreen() {
// 防抖搜索示例 // 防抖搜索示例
const debouncedSearch = useDebounce(async (text: string) => { const debouncedSearch = useDebounce(async (text: string) => {
console.log('防抖搜索:', text);
if (!text.trim()) { if (!text.trim()) {
setSearchResults([]); setSearchResults([]);
return; return;
@ -97,11 +115,7 @@ export default function DemoScreen() {
console.log('执行搜索:', text); console.log('执行搜索:', text);
// 模拟 API 调用 // 模拟 API 调用
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
setSearchResults([ setSearchResults([`结果 1: ${text}`, `结果 2: ${text}`, `结果 3: ${text}`]);
`结果 1: ${text}`,
`结果 2: ${text}`,
`结果 3: ${text}`,
]);
}, 500); }, 500);
// 监听搜索文本变化 // 监听搜索文本变化
@ -124,11 +138,11 @@ export default function DemoScreen() {
// 模拟登录 API 调用 // 模拟登录 API 调用
console.log('登录数据:', data); console.log('登录数据:', data);
// 实际项目中使用: // 实际项目中使用:
// const { user, token } = await authService.login(data); // const { user, token } = await authService.login(data);
// login(user, token); // login(user, token);
// 模拟登录成功 // 模拟登录成功
const mockUser = { const mockUser = {
id: '1', id: '1',
@ -138,7 +152,7 @@ export default function DemoScreen() {
nickname: '演示用户', nickname: '演示用户',
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}; };
login(mockUser, 'mock-token-123456'); login(mockUser, 'mock-token-123456');
haptics.success(); haptics.success();
Alert.alert('成功', '登录成功!'); Alert.alert('成功', '登录成功!');
@ -153,20 +167,16 @@ export default function DemoScreen() {
// 登出处理 // 登出处理
const handleLogout = () => { const handleLogout = () => {
haptics.warning(); haptics.warning();
Alert.alert( Alert.alert('确认', '确定要退出登录吗?', [
'确认', { text: '取消', style: 'cancel' },
'确定要退出登录吗?', {
[ text: '确定',
{ text: '取消', style: 'cancel' }, onPress: () => {
{ logout();
text: '确定', haptics.success();
onPress: () => {
logout();
haptics.success();
},
}, },
] },
); ]);
}; };
// 存储示例 // 存储示例
@ -178,7 +188,7 @@ export default function DemoScreen() {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
counter, counter,
}; };
await Storage.setObject(STORAGE_KEYS.USER_PREFERENCES, testData); await Storage.setObject(STORAGE_KEYS.USER_PREFERENCES, testData);
haptics.success(); haptics.success();
Alert.alert('成功', '数据已保存到本地存储'); Alert.alert('成功', '数据已保存到本地存储');
@ -192,7 +202,7 @@ export default function DemoScreen() {
try { try {
haptics.light(); haptics.light();
const data = await Storage.getObject<any>(STORAGE_KEYS.USER_PREFERENCES); const data = await Storage.getObject<any>(STORAGE_KEYS.USER_PREFERENCES);
if (data) { if (data) {
setStorageValue(JSON.stringify(data, null, 2)); setStorageValue(JSON.stringify(data, null, 2));
haptics.success(); haptics.success();
@ -206,6 +216,59 @@ export default function DemoScreen() {
} }
}; };
// SessionStorage 处理函数
const handleSaveToSession = () => {
try {
haptics.light();
const testData = {
formDraft: {
title: '草稿标题',
content: '这是一个表单草稿示例',
},
timestamp: new Date().toISOString(),
counter: Math.floor(Math.random() * 100),
};
SessionStorage.setObject(SESSION_KEYS.FORM_DRAFT, testData);
haptics.success();
Alert.alert('成功', '数据已保存到会话存储(应用重启后会丢失)');
} catch (error) {
haptics.error();
Alert.alert('失败', '保存失败');
}
};
const handleLoadFromSession = () => {
try {
haptics.light();
const data = SessionStorage.getObject<any>(SESSION_KEYS.FORM_DRAFT);
if (data) {
setSessionValue(JSON.stringify(data, null, 2));
haptics.success();
} else {
setSessionValue('暂无数据(会话存储为空)');
haptics.warning();
}
} catch (error) {
haptics.error();
Alert.alert('失败', '读取失败');
}
};
const handleClearSession = () => {
try {
haptics.light();
SessionStorage.clear();
setSessionValue('');
haptics.success();
Alert.alert('成功', '会话存储已清空');
} catch (error) {
haptics.error();
Alert.alert('失败', '清空失败');
}
};
// 主题切换 // 主题切换
const handleThemeChange = () => { const handleThemeChange = () => {
haptics.selection(); haptics.selection();
@ -228,18 +291,76 @@ export default function DemoScreen() {
<Text style={styles.title}>🎯 </Text> <Text style={styles.title}>🎯 </Text>
<Text style={styles.subtitle}>使</Text> <Text style={styles.subtitle}>使</Text>
{/* 页面导航 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>📱 </Text>
<TouchableOpacity
style={[styles.button, styles.primaryButton]}
onPress={() => {
haptics.light();
router.push('/test-page');
}}
>
<Text style={styles.buttonText}> </Text>
</TouchableOpacity>
<Text style={styles.infoText}>
tabs
</Text>
<TouchableOpacity
style={[styles.button, { backgroundColor: '#9333ea', marginTop: 12 }]}
onPress={() => {
haptics.light();
router.push('/theme-test');
}}
>
<Text style={styles.buttonText}>🎨 </Text>
</TouchableOpacity>
<Text style={styles.infoText}>
</Text>
<TouchableOpacity
style={[styles.button, { backgroundColor: '#06b6d4', marginTop: 12 }]}
onPress={() => {
haptics.light();
router.push('/theme-example');
}}
>
<Text style={styles.buttonText}>📚 </Text>
</TouchableOpacity>
<Text style={styles.infoText}>
使 CSS
</Text>
</View>
{/* 租户信息显示 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>🏢 </Text>
<Text style={styles.infoText}>
: {tenantLoad ? '✅ 已加载' : '❌ 未加载'}
</Text>
{tenantInfo ? (
<>
<Text style={styles.infoText}>TID: {tenantInfo.tid || '无'}</Text>
<Text style={styles.infoText}>
: {tenantInfo.create_time || '无'}
</Text>
<Text style={styles.infoText}>
: {tenantInfo.domain_addr || '无'}
</Text>
</>
) : null}
</View>
{/* 用户状态显示 */} {/* 用户状态显示 */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>👤 (Zustand)</Text> <Text style={styles.sectionTitle}>👤 (Zustand)</Text>
{isLoggedIn ? ( {isLoggedIn ? (
<View style={styles.userInfo}> <View style={styles.userInfo}>
{user?.avatar && ( {user?.avatar ? (
<Image <Image source={{ uri: user.avatar }} style={styles.avatar} contentFit="cover" />
source={{ uri: user.avatar }} ) : null}
style={styles.avatar}
contentFit="cover"
/>
)}
<View style={styles.userDetails}> <View style={styles.userDetails}>
<Text style={styles.userName}>{user?.nickname}</Text> <Text style={styles.userName}>{user?.nickname}</Text>
<Text style={styles.userEmail}>{user?.email}</Text> <Text style={styles.userEmail}>{user?.email}</Text>
@ -247,10 +368,7 @@ export default function DemoScreen() {
: {formatRelativeTime(user?.createdAt || '')} : {formatRelativeTime(user?.createdAt || '')}
</Text> </Text>
</View> </View>
<TouchableOpacity <TouchableOpacity style={[styles.button, styles.logoutButton]} onPress={handleLogout}>
style={[styles.button, styles.logoutButton]}
onPress={handleLogout}
>
<Text style={styles.buttonText}>退</Text> <Text style={styles.buttonText}>退</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -260,10 +378,10 @@ export default function DemoScreen() {
</View> </View>
{/* 登录表单 */} {/* 登录表单 */}
{!isLoggedIn && ( {!isLoggedIn ? (
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>🔐 (React Hook Form + Zod)</Text> <Text style={styles.sectionTitle}>🔐 (React Hook Form + Zod)</Text>
<Controller <Controller
control={control} control={control}
name="email" name="email"
@ -276,14 +394,9 @@ export default function DemoScreen() {
placeholder="请输入邮箱" placeholder="请输入邮箱"
keyboardType="email-address" keyboardType="email-address"
autoCapitalize="none" autoCapitalize="none"
style={[ style={[styles.input, errors.email && styles.inputError]}
styles.input,
errors.email && styles.inputError,
]}
/> />
{errors.email && ( {errors.email && <Text style={styles.errorText}>{errors.email.message}</Text>}
<Text style={styles.errorText}>{errors.email.message}</Text>
)}
</View> </View>
)} )}
/> />
@ -299,14 +412,11 @@ export default function DemoScreen() {
onChangeText={onChange} onChangeText={onChange}
placeholder="请输入密码" placeholder="请输入密码"
secureTextEntry secureTextEntry
style={[ style={[styles.input, errors.password && styles.inputError]}
styles.input,
errors.password && styles.inputError,
]}
/> />
{errors.password && ( {errors.password ? (
<Text style={styles.errorText}>{errors.password.message}</Text> <Text style={styles.errorText}>{errors.password.message}</Text>
)} ) : null}
</View> </View>
)} )}
/> />
@ -323,7 +433,7 @@ export default function DemoScreen() {
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} ) : null}
{/* 搜索示例 */} {/* 搜索示例 */}
<View style={styles.section}> <View style={styles.section}>
@ -334,7 +444,7 @@ export default function DemoScreen() {
placeholder="输入搜索内容..." placeholder="输入搜索内容..."
style={styles.input} style={styles.input}
/> />
{searchResults.length > 0 && ( {searchResults.length > 0 ? (
<View style={styles.searchResults}> <View style={styles.searchResults}>
{searchResults.map((result, index) => ( {searchResults.map((result, index) => (
<Text key={index} style={styles.searchResult}> <Text key={index} style={styles.searchResult}>
@ -342,17 +452,14 @@ export default function DemoScreen() {
</Text> </Text>
))} ))}
</View> </View>
)} ) : null}
</View> </View>
{/* 节流点击示例 */} {/* 节流点击示例 */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}> (useThrottle)</Text> <Text style={styles.sectionTitle}> (useThrottle)</Text>
<Text style={styles.infoText}>: {counter}</Text> <Text style={styles.infoText}>: {counter}</Text>
<TouchableOpacity <TouchableOpacity style={[styles.button, styles.primaryButton]} onPress={throttledClick}>
style={[styles.button, styles.primaryButton]}
onPress={throttledClick}
>
<Text style={styles.buttonText}>1</Text> <Text style={styles.buttonText}>1</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -374,11 +481,44 @@ export default function DemoScreen() {
<Text style={styles.buttonText}></Text> <Text style={styles.buttonText}></Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{storageValue && ( {storageValue ? (
<View style={styles.codeBlock}> <View style={styles.codeBlock}>
<Text style={styles.codeText}>{storageValue}</Text> <Text style={styles.codeText}>{storageValue}</Text>
</View> </View>
)} ) : null}
</View>
{/* 会话存储示例 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>🔄 (SessionStorage)</Text>
<Text style={styles.infoText}>
</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.primaryButton, styles.thirdButton]}
onPress={handleSaveToSession}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.secondaryButton, styles.thirdButton]}
onPress={handleLoadFromSession}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.errorButton, styles.thirdButton]}
onPress={handleClearSession}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</View>
{sessionValue ? (
<View style={styles.codeBlock}>
<Text style={styles.codeText}>{sessionValue}</Text>
</View>
) : null}
</View> </View>
{/* 日期格式化示例 */} {/* 日期格式化示例 */}
@ -387,18 +527,20 @@ export default function DemoScreen() {
<Text style={styles.infoText}> <Text style={styles.infoText}>
: {formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')} : {formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')}
</Text> </Text>
<Text style={styles.infoText}> <Text style={styles.infoText}>: {formatRelativeTime(new Date())}</Text>
: {formatRelativeTime(new Date())} <Text style={styles.infoText}>: {formatChatTime(Date.now())}</Text>
</Text> </View>
<Text style={styles.infoText}>
: {formatChatTime(Date.now())} {/* 主题演示 */}
</Text> <View style={styles.section}>
<Text style={styles.sectionTitle}>🎨 </Text>
<ThemeDemo />
</View> </View>
{/* 设置示例 */} {/* 设置示例 */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}> </Text> <Text style={styles.sectionTitle}> </Text>
<View style={styles.settingRow}> <View style={styles.settingRow}>
<Text style={styles.settingLabel}>: {theme}</Text> <Text style={styles.settingLabel}>: {theme}</Text>
<TouchableOpacity <TouchableOpacity
@ -475,9 +617,7 @@ export default function DemoScreen() {
</View> </View>
<View style={styles.footer}> <View style={styles.footer}>
<Text style={styles.footerText}> <Text style={styles.footerText}>使 📖</Text>
使 📖
</Text>
</View> </View>
</View> </View>
</ScrollView> </ScrollView>
@ -607,11 +747,15 @@ const styles = StyleSheet.create({
}, },
buttonRow: { buttonRow: {
flexDirection: 'row', flexDirection: 'row',
gap: 12, justifyContent: 'space-between',
}, },
halfButton: { halfButton: {
flex: 1, flex: 1,
}, },
thirdButton: {
flex: 1,
marginHorizontal: 4,
},
searchResults: { searchResults: {
marginTop: 12, marginTop: 12,
}, },
@ -651,27 +795,31 @@ const styles = StyleSheet.create({
buttonGrid: { buttonGrid: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 8, marginHorizontal: -4,
}, },
hapticsButton: { hapticsButton: {
backgroundColor: '#5856D6', backgroundColor: '#5856D6',
flex: 1, flex: 1,
minWidth: '30%', minWidth: '30%',
margin: 4,
}, },
successButton: { successButton: {
backgroundColor: '#34C759', backgroundColor: '#34C759',
flex: 1, flex: 1,
margin: 4,
minWidth: '30%', minWidth: '30%',
}, },
warningButton: { warningButton: {
backgroundColor: '#FF9500', backgroundColor: '#FF9500',
flex: 1, flex: 1,
minWidth: '30%', minWidth: '30%',
margin: 4,
}, },
errorButton: { errorButton: {
backgroundColor: '#FF3B30', backgroundColor: '#FF3B30',
flex: 1, flex: 1,
minWidth: '30%', minWidth: '30%',
margin: 4,
}, },
footer: { footer: {
marginTop: 24, marginTop: 24,
@ -683,4 +831,3 @@ const styles = StyleSheet.create({
color: '#999', color: '#999',
}, },
}); });

43
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 { StyleSheet, TouchableOpacity, Alert, ActivityIndicator } from 'react-native';
import * as Updates from 'expo-updates'; import * as Updates from 'expo-updates';
@ -24,23 +24,19 @@ export default function TabOneScreen() {
setUpdateInfo('发现新版本,正在下载...'); setUpdateInfo('发现新版本,正在下载...');
await Updates.fetchUpdateAsync(); await Updates.fetchUpdateAsync();
Alert.alert( Alert.alert('更新完成', '新版本已下载完成,是否立即重启应用?', [
'更新完成', {
'新版本已下载完成,是否立即重启应用?', text: '稍后',
[ style: 'cancel',
{ onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'),
text: '稍后', },
style: 'cancel', {
onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'), text: '立即重启',
onPress: async () => {
await Updates.reloadAsync();
}, },
{ },
text: '立即重启', ]);
onPress: async () => {
await Updates.reloadAsync();
},
},
]
);
} else { } else {
setUpdateInfo('当前已是最新版本'); setUpdateInfo('当前已是最新版本');
} }
@ -53,13 +49,8 @@ export default function TabOneScreen() {
}; };
const getUpdateInfo = () => { const getUpdateInfo = () => {
const { const { isEmbeddedLaunch, isEmergencyLaunch, updateId, channel, runtimeVersion } =
isEmbeddedLaunch, Updates.useUpdates();
isEmergencyLaunch,
updateId,
channel,
runtimeVersion,
} = Updates.useUpdates();
return ` return `
运行模式: ${__DEV__ ? '开发模式' : '生产模式'} 运行模式: ${__DEV__ ? '开发模式' : '生产模式'}
@ -71,6 +62,10 @@ export default function TabOneScreen() {
`.trim(); `.trim();
}; };
useEffect(() => {
console.log('=== TabOneScreen 组件已渲染 ===');
}, []);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>🚀 </Text> <Text style={styles.title}>🚀 </Text>

31
app/(tabs)/paper.tsx

@ -82,7 +82,7 @@ export default function PaperDemo() {
🔘 🔘
</Text> </Text>
<View style={styles.buttonRow}> <View style={styles.buttonRow}>
<Button mode="contained" onPress={() => setSnackbarVisible(true)}> <Button mode="contained" onPress={() => setSnackbarVisible(true)} style={{ marginRight: 8 }}>
Contained Contained
</Button> </Button>
<Button mode="outlined" onPress={() => setSnackbarVisible(true)}> <Button mode="outlined" onPress={() => setSnackbarVisible(true)}>
@ -90,14 +90,10 @@ export default function PaperDemo() {
</Button> </Button>
</View> </View>
<View style={styles.buttonRow}> <View style={styles.buttonRow}>
<Button mode="text" onPress={() => setSnackbarVisible(true)}> <Button mode="text" onPress={() => setSnackbarVisible(true)} style={{ marginRight: 8 }}>
Text Text
</Button> </Button>
<Button <Button mode="elevated" onPress={() => setSnackbarVisible(true)} icon="camera">
mode="elevated"
onPress={() => setSnackbarVisible(true)}
icon="camera"
>
With Icon With Icon
</Button> </Button>
</View> </View>
@ -141,18 +137,13 @@ export default function PaperDemo() {
<Switch value={switchValue} onValueChange={setSwitchValue} /> <Switch value={switchValue} onValueChange={setSwitchValue} />
</View> </View>
<View style={styles.chipContainer}> <View style={styles.chipContainer}>
<Chip icon="star" onPress={() => {}}> <Chip icon="star" onPress={() => {}} style={{ marginRight: 8, marginBottom: 8 }}>
</Chip> </Chip>
<Chip icon="heart" mode="outlined" onPress={() => {}}> <Chip icon="heart" mode="outlined" onPress={() => {}} style={{ marginRight: 8, marginBottom: 8 }}>
</Chip> </Chip>
<Chip <Chip icon="close" onPress={() => {}} onClose={() => {}} closeIcon="close-circle" style={{ marginBottom: 8 }}>
icon="close"
onPress={() => {}}
onClose={() => {}}
closeIcon="close-circle"
>
</Chip> </Chip>
</View> </View>
@ -221,12 +212,7 @@ export default function PaperDemo() {
</ScrollView> </ScrollView>
{/* 浮动操作按钮 */} {/* 浮动操作按钮 */}
<FAB <FAB icon="plus" style={styles.fab} onPress={() => setSnackbarVisible(true)} label="添加" />
icon="plus"
style={styles.fab}
onPress={() => setSnackbarVisible(true)}
label="添加"
/>
{/* 提示条 */} {/* 提示条 */}
<Snackbar <Snackbar
@ -279,7 +265,6 @@ const styles = StyleSheet.create({
}, },
buttonRow: { buttonRow: {
flexDirection: 'row', flexDirection: 'row',
gap: 8,
marginBottom: 8, marginBottom: 8,
}, },
row: { row: {
@ -291,7 +276,6 @@ const styles = StyleSheet.create({
chipContainer: { chipContainer: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 8,
}, },
progressBar: { progressBar: {
marginBottom: 12, marginBottom: 12,
@ -306,4 +290,3 @@ const styles = StyleSheet.create({
bottom: 0, bottom: 0,
}, },
}); });

57
app/_layout.tsx

@ -9,7 +9,10 @@ import { Alert, Platform } from 'react-native';
import 'react-native-reanimated'; import 'react-native-reanimated';
import { PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper'; import { PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper';
import { useColorScheme } from '@/components/useColorScheme'; // ✅ 从 hooks 目录导入
import { useColorScheme } from '@/hooks';
// ✅ 从 stores 目录导入
import { restoreUserState, restoreSettingsState, useTenantActions } from '@/stores';
export { export {
// Catch any errors thrown by the Layout component. // Catch any errors thrown by the Layout component.
@ -29,6 +32,7 @@ export default function RootLayout() {
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font, ...FontAwesome.font,
}); });
const { requestTenantInfo } = useTenantActions();
// Expo Router uses Error Boundaries to catch errors in the navigation tree. // Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => { useEffect(() => {
@ -41,6 +45,31 @@ export default function RootLayout() {
} }
}, [loaded]); }, [loaded]);
// 恢复持久化状态并初始化应用数据
useEffect(() => {
async function initializeApp() {
try {
// 1. 恢复本地存储的状态
await Promise.all([restoreUserState(), restoreSettingsState()]);
// 2. 调用初始化接口(获取平台数据等)
if (__DEV__) {
console.log('🚀 Initializing app data...');
}
await requestTenantInfo();
if (__DEV__) {
console.log('✅ Platform data loaded:');
}
} catch (error) {
console.error('Failed to initialize app:', error);
// 初始化失败不应该阻止应用启动,只记录错误
}
}
initializeApp();
}, []);
// 检查热更新 // 检查热更新
useEffect(() => { useEffect(() => {
async function checkForUpdates() { async function checkForUpdates() {
@ -56,22 +85,18 @@ export default function RootLayout() {
await Updates.fetchUpdateAsync(); await Updates.fetchUpdateAsync();
// 提示用户重启应用以应用更新 // 提示用户重启应用以应用更新
Alert.alert( Alert.alert('更新可用', '发现新版本,是否立即重启应用?', [
'更新可用', {
'发现新版本,是否立即重启应用?', text: '稍后',
[ style: 'cancel',
{ },
text: '稍后', {
style: 'cancel', text: '立即重启',
}, onPress: async () => {
{ await Updates.reloadAsync();
text: '立即重启',
onPress: async () => {
await Updates.reloadAsync();
},
}, },
] },
); ]);
} }
} catch (error) { } catch (error) {
// 处理更新检查错误 // 处理更新检查错误

119
app/test-page.tsx

@ -0,0 +1,119 @@
import { StyleSheet, ScrollView } from 'react-native';
import { Stack, useRouter } from 'expo-router';
import { ThemedText, ThemedView } from '@/components';
/**
*
*
* tabs
*
*
* - header
* -
* -
*/
export default function TestPage() {
const router = useRouter();
return (
<>
{/* 配置页面 header */}
<Stack.Screen
options={{
title: '测试页面',
headerShown: true,
headerBackTitle: '返回',
}}
/>
<ThemedView style={styles.container}>
<ScrollView style={styles.scrollView}>
<ThemedView style={styles.content}>
<ThemedText type="title" style={styles.title}>
</ThemedText>
<ThemedText style={styles.description}>
tabs
</ThemedText>
<ThemedView style={styles.section}>
<ThemedText type="subtitle"></ThemedText>
<ThemedText style={styles.item}> header</ThemedText>
<ThemedText style={styles.item}> </ThemedText>
<ThemedText style={styles.item}> </ThemedText>
<ThemedText style={styles.item}> </ThemedText>
</ThemedView>
<ThemedView style={styles.section}>
<ThemedText type="subtitle">使</ThemedText>
<ThemedText style={styles.item}> </ThemedText>
<ThemedText style={styles.item}> </ThemedText>
<ThemedText style={styles.item}> </ThemedText>
<ThemedText style={styles.item}> </ThemedText>
</ThemedView>
<ThemedView style={styles.section}>
<ThemedText type="subtitle"></ThemedText>
<ThemedText style={styles.item}>
文件路径: app/test-page.tsx
</ThemedText>
<ThemedText style={styles.item}>
访: /test-page
</ThemedText>
<ThemedText style={styles.item}>
跳转方式: router.push('/test-page')
</ThemedText>
</ThemedView>
<ThemedView style={styles.infoBox}>
<ThemedText type="defaultSemiBold">💡 </ThemedText>
<ThemedText style={styles.infoText}>
screens/
app/
</ThemedText>
</ThemedView>
</ThemedView>
</ScrollView>
</ThemedView>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
flex: 1,
},
content: {
padding: 20,
},
title: {
marginBottom: 16,
},
description: {
marginBottom: 24,
lineHeight: 24,
},
section: {
marginBottom: 24,
},
item: {
marginTop: 8,
marginLeft: 8,
lineHeight: 24,
},
infoBox: {
padding: 16,
borderRadius: 8,
backgroundColor: 'rgba(0, 122, 255, 0.1)',
marginTop: 8,
},
infoText: {
marginTop: 8,
lineHeight: 22,
},
});

259
app/theme-example.tsx

@ -0,0 +1,259 @@
/**
* 使
*
* 使
*/
import { ScrollView, View, Text, TouchableOpacity } from 'react-native';
import { Stack } from 'expo-router';
import {
useColorScheme,
useThemeColors,
useThemeInfo,
commonStyles,
createThemeStyles,
ThemedText,
ThemedView,
} from '@/theme';
// 方式 3: 创建自定义主题样式(推荐用于复杂组件)
const customStyles = createThemeStyles((colors) => ({
header: {
backgroundColor: colors.primary,
padding: 20,
borderRadius: 12,
marginBottom: 16,
},
headerText: {
color: '#FFFFFF',
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
},
section: {
backgroundColor: colors.card,
padding: 16,
borderRadius: 8,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
},
sectionTitle: {
color: colors.text,
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
},
codeBlock: {
backgroundColor: colors.backgroundTertiary,
padding: 12,
borderRadius: 6,
marginTop: 8,
},
codeText: {
color: colors.textSecondary,
fontSize: 12,
fontFamily: 'monospace',
},
}));
export default function ThemeExampleScreen() {
const theme = useColorScheme();
const colors = useThemeColors();
const { isDark } = useThemeInfo();
const s = commonStyles[theme]; // 通用样式类
const custom = customStyles[theme]; // 自定义样式
return (
<>
<Stack.Screen
options={{
title: '主题系统示例',
headerStyle: {
backgroundColor: colors.background,
},
headerTintColor: colors.text,
}}
/>
<ScrollView style={s.containerPadded}>
{/* 自定义样式示例 */}
<View style={custom.header}>
<Text style={custom.headerText}>
使
</Text>
<Text style={[custom.headerText, { fontSize: 14, marginTop: 8 }]}>
: {theme} {isDark ? '🌙' : '☀'}
</Text>
</View>
{/* 方式 1: 使用主题组件 */}
<View style={custom.section}>
<Text style={custom.sectionTitle}>
1: 使用主题组件
</Text>
<ThemedView style={{ padding: 12, borderRadius: 6 }}>
<ThemedText type="title"></ThemedText>
<ThemedText type="subtitle"></ThemedText>
<ThemedText></ThemedText>
</ThemedView>
<View style={custom.codeBlock}>
<Text style={custom.codeText}>
{`import { ThemedText, ThemedView } from '@/theme';
<ThemedView>
<ThemedText type="title"></ThemedText>
</ThemedView>`}
</Text>
</View>
</View>
{/* 方式 2: 使用通用样式类 */}
<View style={custom.section}>
<Text style={custom.sectionTitle}>
2: 使用通用样式类 CSS
</Text>
<View style={s.card}>
<Text style={s.textTitle}></Text>
<Text style={s.textSecondary}></Text>
<View style={s.spacingMd} />
<TouchableOpacity style={s.button}>
<Text style={s.buttonText}></Text>
</TouchableOpacity>
<View style={s.spacingSm} />
<TouchableOpacity style={s.buttonOutline}>
<Text style={s.buttonTextOutline}></Text>
</TouchableOpacity>
</View>
<View style={custom.codeBlock}>
<Text style={custom.codeText}>
{`import { useColorScheme, commonStyles } from '@/theme';
const theme = useColorScheme();
const s = commonStyles[theme];
<View style={s.card}>
<Text style={s.textTitle}></Text>
<TouchableOpacity style={s.button}>
<Text style={s.buttonText}></Text>
</TouchableOpacity>
</View>`}
</Text>
</View>
</View>
{/* 方式 3: 使用自定义主题样式 */}
<View style={custom.section}>
<Text style={custom.sectionTitle}>
3: 使用自定义主题样式
</Text>
<View style={{ padding: 12 }}>
<Text style={{ color: colors.text }}>
header section 使
</Text>
</View>
<View style={custom.codeBlock}>
<Text style={custom.codeText}>
{`import { createThemeStyles } from '@/theme';
const styles = createThemeStyles((colors) => ({
header: {
backgroundColor: colors.primary,
padding: 20,
},
headerText: {
color: '#FFFFFF',
fontSize: 24,
},
}));
const theme = useColorScheme();
<View style={styles[theme].header}>
<Text style={styles[theme].headerText}></Text>
</View>`}
</Text>
</View>
</View>
{/* 方式 4: 使用 Hooks + 内联样式 */}
<View style={custom.section}>
<Text style={custom.sectionTitle}>
4: 使用 Hooks +
</Text>
<View style={{
backgroundColor: colors.backgroundSecondary,
padding: 16,
borderRadius: 8,
}}>
<Text style={{ color: colors.text, fontSize: 16 }}>
使 useThemeColors()
</Text>
<View style={{ height: 12 }} />
<Text style={{ color: colors.textSecondary, fontSize: 14 }}>
</Text>
</View>
<View style={custom.codeBlock}>
<Text style={custom.codeText}>
{`import { useThemeColors } from '@/theme';
const colors = useThemeColors();
<View style={{
backgroundColor: colors.background,
padding: 16,
}}>
<Text style={{ color: colors.text }}>
</Text>
</View>`}
</Text>
</View>
</View>
{/* 颜色展示 */}
<View style={custom.section}>
<Text style={custom.sectionTitle}>
</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
{Object.entries(colors).map(([key, value]) => (
<View
key={key}
style={{
backgroundColor: value as string,
padding: 8,
borderRadius: 6,
minWidth: 100,
borderWidth: 1,
borderColor: colors.border,
}}
>
<Text style={{
color: key.includes('background') || key.includes('card') || key.includes('input')
? colors.text
: '#FFFFFF',
fontSize: 10,
fontWeight: '600',
}}>
{key}
</Text>
<Text style={{
color: key.includes('background') || key.includes('card') || key.includes('input')
? colors.textSecondary
: '#FFFFFF',
fontSize: 8,
}}>
{value}
</Text>
</View>
))}
</View>
</View>
{/* 底部间距 */}
<View style={s.spacingXl} />
</ScrollView>
</>
);
}

317
app/theme-test.tsx

@ -0,0 +1,317 @@
import { StyleSheet, ScrollView, TouchableOpacity, View, Text, useColorScheme as useSystemColorScheme } from 'react-native';
import { Stack } from 'expo-router';
import { useState, useEffect, useMemo } from 'react';
import { useTheme, useSettingsActions } from '@/stores';
import { useHaptics } from '@/hooks';
import Colors from '@/constants/Colors';
export default function ThemeTestScreen() {
const currentTheme = useTheme();
const { setTheme } = useSettingsActions();
const haptics = useHaptics();
const systemColorScheme = useSystemColorScheme();
// 强制重新渲染的状态
const [renderKey, setRenderKey] = useState(0);
// 直接计算实际应用的主题 - 不使用 useColorScheme hook
const actualTheme: 'light' | 'dark' = useMemo(() => {
return currentTheme === 'auto'
? (systemColorScheme === 'dark' ? 'dark' : 'light')
: currentTheme;
}, [currentTheme, systemColorScheme]);
// 使用 useMemo 确保颜色对象在主题改变时重新计算
const colors = useMemo(() => {
console.log('🎨 Recalculating colors for theme:', actualTheme);
return Colors[actualTheme] as Record<string, any>;
}, [actualTheme]);
// 监听主题变化
useEffect(() => {
console.log('🎨 Theme changed:', { currentTheme, systemColorScheme, actualTheme, renderKey });
setRenderKey(prev => prev + 1);
}, [currentTheme, systemColorScheme, actualTheme]);
const handleThemeChange = (newTheme: 'light' | 'dark' | 'auto') => {
haptics.selection();
console.log('🎨 Changing theme to:', newTheme);
setTheme(newTheme);
};
return (
<>
<Stack.Screen
options={{
title: '主题测试',
headerStyle: {
backgroundColor: colors.background,
},
headerTintColor: colors.text,
}}
/>
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
>
{/* 主题信息 */}
<View style={[styles.section, { backgroundColor: colors.backgroundSecondary }]}>
<Text style={[styles.title, { color: colors.text }]}>
</Text>
<Text style={[styles.infoText, { color: colors.textSecondary }]}>
: {currentTheme}
</Text>
<Text style={[styles.infoText, { color: colors.textSecondary }]}>
: {systemColorScheme || 'light'}
</Text>
<Text style={[styles.infoText, { color: colors.textSecondary }]}>
: {actualTheme}
</Text>
<Text style={[styles.infoText, { color: colors.textSecondary }]}>
: {renderKey}
</Text>
</View>
{/* 主题切换按钮 */}
<View style={[styles.section, { backgroundColor: colors.backgroundSecondary }]}>
<Text style={[styles.title, { color: colors.text }]}>
</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[
styles.themeButton,
{
backgroundColor: currentTheme === 'light' ? colors.primary : colors.backgroundTertiary,
borderColor: colors.border,
}
]}
onPress={() => handleThemeChange('light')}
>
<Text style={[
styles.buttonText,
{ color: currentTheme === 'light' ? '#fff' : colors.text }
]}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.themeButton,
{
backgroundColor: currentTheme === 'dark' ? colors.primary : colors.backgroundTertiary,
borderColor: colors.border,
}
]}
onPress={() => handleThemeChange('dark')}
>
<Text style={[
styles.buttonText,
{ color: currentTheme === 'dark' ? '#fff' : colors.text }
]}>
🌙
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.themeButton,
{
backgroundColor: currentTheme === 'auto' ? colors.primary : colors.backgroundTertiary,
borderColor: colors.border,
}
]}
onPress={() => handleThemeChange('auto')}
>
<Text style={[
styles.buttonText,
{ color: currentTheme === 'auto' ? '#fff' : colors.text }
]}>
🔄
</Text>
</TouchableOpacity>
</View>
</View>
{/* 文本颜色展示 */}
<View style={[styles.section, { backgroundColor: colors.backgroundSecondary }]}>
<Text style={[styles.title, { color: colors.text }]}>
</Text>
<Text style={[styles.colorText, { color: colors.text }]}>
Primary Text - {colors.text}
</Text>
<Text style={[styles.colorText, { color: colors.textSecondary }]}>
Secondary Text - {colors.textSecondary}
</Text>
<Text style={[styles.colorText, { color: colors.textTertiary }]}>
Tertiary Text - {colors.textTertiary}
</Text>
</View>
{/* 背景颜色展示 */}
<View style={[styles.section, { backgroundColor: colors.backgroundSecondary }]}>
<Text style={[styles.title, { color: colors.text }]}>
</Text>
<View style={[styles.colorBox, { backgroundColor: colors.background }]}>
<Text style={[styles.colorText, { color: colors.text }]}>
Primary Background - {colors.background}
</Text>
</View>
<View style={[styles.colorBox, { backgroundColor: colors.backgroundSecondary, borderWidth: 1, borderColor: colors.border }]}>
<Text style={[styles.colorText, { color: colors.text }]}>
Secondary Background - {colors.backgroundSecondary}
</Text>
</View>
<View style={[styles.colorBox, { backgroundColor: colors.backgroundTertiary }]}>
<Text style={[styles.colorText, { color: colors.text }]}>
Tertiary Background - {colors.backgroundTertiary}
</Text>
</View>
</View>
{/* 主题颜色展示 */}
<View style={[styles.section, { backgroundColor: colors.backgroundSecondary }]}>
<Text style={[styles.title, { color: colors.text }]}>
</Text>
<View style={styles.colorGrid}>
<View style={[styles.colorBox, { backgroundColor: colors.primary }]}>
<Text style={[styles.colorText, { color: '#fff' }]}>Primary - {colors.primary}</Text>
</View>
<View style={[styles.colorBox, { backgroundColor: colors.secondary }]}>
<Text style={[styles.colorText, { color: '#fff' }]}>Secondary - {colors.secondary}</Text>
</View>
<View style={[styles.colorBox, { backgroundColor: colors.success }]}>
<Text style={[styles.colorText, { color: '#fff' }]}>Success - {colors.success}</Text>
</View>
<View style={[styles.colorBox, { backgroundColor: colors.warning }]}>
<Text style={[styles.colorText, { color: '#fff' }]}>Warning - {colors.warning}</Text>
</View>
<View style={[styles.colorBox, { backgroundColor: colors.error }]}>
<Text style={[styles.colorText, { color: '#fff' }]}>Error - {colors.error}</Text>
</View>
<View style={[styles.colorBox, { backgroundColor: colors.info }]}>
<Text style={[styles.colorText, { color: '#fff' }]}>Info - {colors.info}</Text>
</View>
</View>
</View>
{/* UI 元素展示 */}
<View style={[styles.section, { backgroundColor: colors.backgroundSecondary }]}>
<Text style={[styles.title, { color: colors.text }]}>
UI
</Text>
<View style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }]}>
<Text style={[styles.cardText, { color: colors.text }]}>
</Text>
<Text style={[styles.cardSubtext, { color: colors.textSecondary }]}>
: {colors.card}
</Text>
</View>
<View style={[styles.input, { backgroundColor: colors.inputBackground, borderColor: colors.inputBorder }]}>
<Text style={[styles.inputText, { color: colors.textSecondary }]}>
</Text>
</View>
<View style={[styles.separator, { backgroundColor: colors.separator }]} />
<TouchableOpacity style={[styles.button, { backgroundColor: colors.buttonPrimary }]}>
<Text style={[styles.buttonText, { color: '#fff' }]}>
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
section: {
margin: 16,
padding: 16,
borderRadius: 12,
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
},
infoText: {
fontSize: 16,
marginBottom: 8,
},
buttonRow: {
flexDirection: 'row',
gap: 12,
},
themeButton: {
flex: 1,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 2,
alignItems: 'center',
},
buttonText: {
fontSize: 16,
fontWeight: '600',
},
colorText: {
fontSize: 14,
marginBottom: 8,
fontFamily: 'monospace',
},
colorBox: {
padding: 16,
borderRadius: 8,
marginBottom: 8,
alignItems: 'center',
},
colorGrid: {
gap: 8,
},
card: {
padding: 16,
borderRadius: 8,
borderWidth: 1,
marginBottom: 12,
},
cardText: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
cardSubtext: {
fontSize: 12,
fontFamily: 'monospace',
},
input: {
padding: 12,
borderRadius: 8,
borderWidth: 1,
marginBottom: 12,
},
inputText: {
fontSize: 14,
},
separator: {
height: 1,
marginVertical: 12,
},
button: {
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
});

12
components/EditScreenInfo.tsx

@ -14,21 +14,24 @@ export default function EditScreenInfo({ path }: { path: string }) {
<Text <Text
style={styles.getStartedText} style={styles.getStartedText}
lightColor="rgba(0,0,0,0.8)" lightColor="rgba(0,0,0,0.8)"
darkColor="rgba(255,255,255,0.8)"> darkColor="rgba(255,255,255,0.8)"
>
Open up the code for this screen: Open up the code for this screen:
</Text> </Text>
<View <View
style={[styles.codeHighlightContainer, styles.homeScreenFilename]} style={[styles.codeHighlightContainer, styles.homeScreenFilename]}
darkColor="rgba(255,255,255,0.05)" darkColor="rgba(255,255,255,0.05)"
lightColor="rgba(0,0,0,0.05)"> lightColor="rgba(0,0,0,0.05)"
>
<MonoText>{path}</MonoText> <MonoText>{path}</MonoText>
</View> </View>
<Text <Text
style={styles.getStartedText} style={styles.getStartedText}
lightColor="rgba(0,0,0,0.8)" lightColor="rgba(0,0,0,0.8)"
darkColor="rgba(255,255,255,0.8)"> darkColor="rgba(255,255,255,0.8)"
>
Change any of the text, save the file, and your app will automatically update. Change any of the text, save the file, and your app will automatically update.
</Text> </Text>
</View> </View>
@ -36,7 +39,8 @@ export default function EditScreenInfo({ path }: { path: string }) {
<View style={styles.helpContainer}> <View style={styles.helpContainer}>
<ExternalLink <ExternalLink
style={styles.helpLink} style={styles.helpLink}
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet"> href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet"
>
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}> <Text style={styles.helpLinkText} lightColor={Colors.light.tint}>
Tap here if your app doesn't automatically update after making changes Tap here if your app doesn't automatically update after making changes
</Text> </Text>

232
components/ThemeDemo.tsx

@ -0,0 +1,232 @@
/**
*
*
*
*/
import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { ThemedText, ThemedView, useThemeColor } from './Themed';
import { useTheme, useSettingsActions } from '@/stores';
import { useHaptics } from '@/hooks';
export function ThemeDemo() {
const theme = useTheme();
const { setTheme } = useSettingsActions();
const haptics = useHaptics();
// 获取主题颜色
const primary = useThemeColor({}, 'primary');
const secondary = useThemeColor({}, 'secondary');
const success = useThemeColor({}, 'success');
const warning = useThemeColor({}, 'warning');
const error = useThemeColor({}, 'error');
const info = useThemeColor({}, 'info');
const border = useThemeColor({}, 'border');
const card = useThemeColor({}, 'card');
const textSecondary = useThemeColor({}, 'textSecondary');
const handleThemeChange = (newTheme: 'light' | 'dark' | 'auto') => {
haptics.selection();
console.log('🎨 Changing theme to:', newTheme);
setTheme(newTheme);
console.log('🎨 Theme colors after change:', { primary, secondary, background: card });
};
return (
<ThemedView style={styles.container}>
<ScrollView>
{/* 主题切换器 */}
<View style={styles.section}>
<ThemedText type="subtitle"></ThemedText>
<View style={styles.themeButtons}>
<TouchableOpacity
style={[
styles.themeButton,
{ borderColor: border },
theme === 'light' && { backgroundColor: primary },
]}
onPress={() => handleThemeChange('light')}
>
<Text style={[styles.themeButtonText, theme === 'light' && styles.activeButtonText]}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.themeButton,
{ borderColor: border },
theme === 'dark' && { backgroundColor: primary },
]}
onPress={() => handleThemeChange('dark')}
>
<Text style={[styles.themeButtonText, theme === 'dark' && styles.activeButtonText]}>
🌙
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.themeButton,
{ borderColor: border },
theme === 'auto' && { backgroundColor: primary },
]}
onPress={() => handleThemeChange('auto')}
>
<Text style={[styles.themeButtonText, theme === 'auto' && styles.activeButtonText]}>
🔄
</Text>
</TouchableOpacity>
</View>
<ThemedText style={styles.hint}>
: {theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '自动'}
</ThemedText>
</View>
{/* 文本样式 */}
<View style={styles.section}>
<ThemedText type="subtitle"></ThemedText>
<ThemedText type="title"> (Title)</ThemedText>
<ThemedText type="subtitle"> (Subtitle)</ThemedText>
<ThemedText type="defaultSemiBold"> (SemiBold)</ThemedText>
<ThemedText type="default"> (Default)</ThemedText>
<ThemedText type="link"> (Link)</ThemedText>
</View>
{/* 颜色展示 */}
<View style={styles.section}>
<ThemedText type="subtitle"></ThemedText>
<View style={styles.colorGrid}>
<View style={styles.colorItem}>
<View style={[styles.colorBox, { backgroundColor: primary }]} />
<ThemedText style={styles.colorLabel}>Primary</ThemedText>
</View>
<View style={styles.colorItem}>
<View style={[styles.colorBox, { backgroundColor: secondary }]} />
<ThemedText style={styles.colorLabel}>Secondary</ThemedText>
</View>
<View style={styles.colorItem}>
<View style={[styles.colorBox, { backgroundColor: success }]} />
<ThemedText style={styles.colorLabel}>Success</ThemedText>
</View>
<View style={styles.colorItem}>
<View style={[styles.colorBox, { backgroundColor: warning }]} />
<ThemedText style={styles.colorLabel}>Warning</ThemedText>
</View>
<View style={styles.colorItem}>
<View style={[styles.colorBox, { backgroundColor: error }]} />
<ThemedText style={styles.colorLabel}>Error</ThemedText>
</View>
<View style={styles.colorItem}>
<View style={[styles.colorBox, { backgroundColor: info }]} />
<ThemedText style={styles.colorLabel}>Info</ThemedText>
</View>
</View>
</View>
{/* 卡片示例 */}
<View style={styles.section}>
<ThemedText type="subtitle"></ThemedText>
<View style={[styles.card, { backgroundColor: card, borderColor: border }]}>
<ThemedText type="defaultSemiBold"></ThemedText>
<ThemedText style={{ color: textSecondary }}>
</ThemedText>
</View>
</View>
{/* 按钮示例 */}
<View style={styles.section}>
<ThemedText type="subtitle"></ThemedText>
<TouchableOpacity style={[styles.button, { backgroundColor: primary }]}>
<Text style={styles.buttonText}>Primary Button</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, { backgroundColor: secondary }]}>
<Text style={styles.buttonText}>Secondary Button</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, { backgroundColor: success }]}>
<Text style={styles.buttonText}>Success Button</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, { backgroundColor: error }]}>
<Text style={styles.buttonText}>Error Button</Text>
</TouchableOpacity>
</View>
</ScrollView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
section: {
marginBottom: 24,
},
themeButtons: {
flexDirection: 'row',
gap: 12,
marginTop: 12,
},
themeButton: {
flex: 1,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
alignItems: 'center',
},
themeButtonText: {
fontSize: 14,
fontWeight: '600',
},
activeButtonText: {
color: '#FFFFFF',
},
hint: {
marginTop: 8,
fontSize: 12,
opacity: 0.7,
},
colorGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 16,
marginTop: 12,
},
colorItem: {
alignItems: 'center',
width: 80,
},
colorBox: {
width: 60,
height: 60,
borderRadius: 8,
marginBottom: 8,
},
colorLabel: {
fontSize: 12,
textAlign: 'center',
},
card: {
padding: 16,
borderRadius: 12,
borderWidth: 1,
marginTop: 12,
},
button: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 12,
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});

90
components/Themed.tsx

@ -1,12 +1,14 @@
/** /**
* Learn more about Light and Dark modes: *
* https://docs.expo.io/guides/color-schemes/ *
* Text View
* settingsStore
*/ */
import { Text as DefaultText, View as DefaultView } from 'react-native'; import { Text as DefaultText, View as DefaultView, TextStyle } from 'react-native';
import Colors from '@/constants/Colors'; import Colors from '@/constants/Colors';
import { useColorScheme } from './useColorScheme'; import { useColorScheme } from '@/hooks/useTheme';
type ThemeProps = { type ThemeProps = {
lightColor?: string; lightColor?: string;
@ -16,6 +18,13 @@ type ThemeProps = {
export type TextProps = ThemeProps & DefaultText['props']; export type TextProps = ThemeProps & DefaultText['props'];
export type ViewProps = ThemeProps & DefaultView['props']; export type ViewProps = ThemeProps & DefaultView['props'];
/**
*
*
* @param props - light dark
* @param colorName - Colors
* @returns
*/
export function useThemeColor( export function useThemeColor(
props: { light?: string; dark?: string }, props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark colorName: keyof typeof Colors.light & keyof typeof Colors.dark
@ -30,6 +39,11 @@ export function useThemeColor(
} }
} }
/**
* Text
*
*
*/
export function Text(props: TextProps) { export function Text(props: TextProps) {
const { style, lightColor, darkColor, ...otherProps } = props; const { style, lightColor, darkColor, ...otherProps } = props;
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
@ -37,9 +51,77 @@ export function Text(props: TextProps) {
return <DefaultText style={[{ color }, style]} {...otherProps} />; return <DefaultText style={[{ color }, style]} {...otherProps} />;
} }
/**
* View
*
*
*/
export function View(props: ViewProps) { export function View(props: ViewProps) {
const { style, lightColor, darkColor, ...otherProps } = props; const { style, lightColor, darkColor, ...otherProps } = props;
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />; return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />;
} }
/**
* Text
*
* title, subtitle, defaultSemiBold, link
*/
export type ThemedTextProps = TextProps & {
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
};
export function ThemedText({ style, type = 'default', ...rest }: ThemedTextProps) {
const color = useThemeColor({}, 'text');
const linkColor = useThemeColor({}, 'tint');
const typeStyles: Record<string, TextStyle> = {
default: {
fontSize: 16,
lineHeight: 24,
},
defaultSemiBold: {
fontSize: 16,
lineHeight: 24,
fontWeight: '600',
},
title: {
fontSize: 32,
fontWeight: 'bold',
lineHeight: 40,
},
subtitle: {
fontSize: 20,
fontWeight: '600',
lineHeight: 28,
},
link: {
fontSize: 16,
lineHeight: 24,
color: linkColor,
},
};
return (
<Text
style={[
{ color },
typeStyles[type],
style,
]}
{...rest}
/>
);
}
/**
* View
*/
export type ThemedViewProps = ViewProps;
export function ThemedView({ style, ...otherProps }: ThemedViewProps) {
const backgroundColor = useThemeColor({}, 'background');
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />;
}

16
components/index.ts

@ -0,0 +1,16 @@
/**
* Components
*/
// 主题组件
export { ThemedText, ThemedView, Text, View, useThemeColor } from './Themed';
export type { ThemedTextProps, ThemedViewProps, TextProps, ViewProps } from './Themed';
// 主题演示
export { ThemeDemo } from './ThemeDemo';
// 工具组件
export { ExternalLink } from './ExternalLink';
export { MonoText } from './StyledText';

4
components/useClientOnlyValue.ts

@ -1,4 +0,0 @@
// This function is web-only as native doesn't currently support server (or build-time) rendering.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
return client;
}

12
components/useClientOnlyValue.web.ts

@ -1,12 +0,0 @@
import React from 'react';
// `useEffect` is not invoked during server rendering, meaning
// we can use this to determine if we're on the server or not.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
const [value, setValue] = React.useState<S | C>(server);
React.useEffect(() => {
setValue(client);
}, [client]);
return value;
}

1
components/useColorScheme.ts

@ -1 +0,0 @@
export { useColorScheme } from 'react-native';

8
components/useColorScheme.web.ts

@ -1,8 +0,0 @@
// NOTE: The default React Native styling doesn't support server rendering.
// Server rendered styles should not change between the first render of the HTML
// and the first render on the client. Typically, web developers will use CSS media queries
// to render different styles on the client and server, these aren't directly supported in React Native
// but can be achieved using a styling library like Nativewind.
export function useColorScheme() {
return 'light';
}

109
constants/Colors.ts

@ -1,19 +1,112 @@
const tintColorLight = '#2f95dc'; /**
const tintColorDark = '#fff'; *
*
* light dark
* settingsStore
*/
const tintColorLight = '#007AFF';
const tintColorDark = '#0A84FF';
export default { export default {
light: { light: {
text: '#000', // 文本颜色
background: '#fff', text: '#000000',
textSecondary: '#666666',
textTertiary: '#999999',
textInverse: '#FFFFFF',
// 背景颜色
background: '#FFFFFF',
backgroundSecondary: '#F5F5F5',
backgroundTertiary: '#E5E5E5',
// 主题色
tint: tintColorLight, tint: tintColorLight,
tabIconDefault: '#ccc', primary: '#007AFF',
secondary: '#5856D6',
success: '#34C759',
warning: '#FF9500',
error: '#FF3B30',
info: '#5AC8FA',
// 边框颜色
border: '#E5E5E5',
borderSecondary: '#D1D1D6',
// Tab 图标
tabIconDefault: '#8E8E93',
tabIconSelected: tintColorLight, tabIconSelected: tintColorLight,
// 卡片
card: '#FFFFFF',
cardShadow: 'rgba(0, 0, 0, 0.1)',
// 输入框
inputBackground: '#FFFFFF',
inputBorder: '#D1D1D6',
inputPlaceholder: '#C7C7CC',
// 按钮
buttonPrimary: '#007AFF',
buttonSecondary: '#5856D6',
buttonDisabled: '#E5E5E5',
buttonText: '#FFFFFF',
// 分隔线
separator: '#E5E5E5',
// 覆盖层
overlay: 'rgba(0, 0, 0, 0.5)',
}, },
dark: { dark: {
text: '#fff', // 文本颜色
background: '#000', text: '#FFFFFF',
textSecondary: '#AEAEB2',
textTertiary: '#8E8E93',
textInverse: '#000000',
// 背景颜色
background: '#000000',
backgroundSecondary: '#1C1C1E',
backgroundTertiary: '#2C2C2E',
// 主题色
tint: tintColorDark, tint: tintColorDark,
tabIconDefault: '#ccc', primary: '#0A84FF',
secondary: '#5E5CE6',
success: '#32D74B',
warning: '#FF9F0A',
error: '#FF453A',
info: '#64D2FF',
// 边框颜色
border: '#38383A',
borderSecondary: '#48484A',
// Tab 图标
tabIconDefault: '#8E8E93',
tabIconSelected: tintColorDark, tabIconSelected: tintColorDark,
// 卡片
card: '#1C1C1E',
cardShadow: 'rgba(0, 0, 0, 0.3)',
// 输入框
inputBackground: '#1C1C1E',
inputBorder: '#38383A',
inputPlaceholder: '#636366',
// 按钮
buttonPrimary: '#0A84FF',
buttonSecondary: '#5E5CE6',
buttonDisabled: '#38383A',
buttonText: '#FFFFFF',
// 分隔线
separator: '#38383A',
// 覆盖层
overlay: 'rgba(0, 0, 0, 0.7)',
}, },
}; };

38
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仅可领取一次,不可重复领取',
];

56
docs/LIBRARIES.md

@ -5,25 +5,28 @@
## 📦 已安装的库列表 ## 📦 已安装的库列表
### 工具类库 ### 工具类库
| 库名 | 版本 | 用途 |
|------|------|------| | 库名 | 版本 | 用途 |
| **lodash-es** | ^4.17.21 | JavaScript 工具函数库(ES modules 版本) | | ------------------- | -------- | ---------------------------------------- |
| **dayjs** | ^1.11.19 | 轻量级日期处理库 | | **lodash-es** | ^4.17.21 | JavaScript 工具函数库(ES modules 版本) |
| **axios** | ^1.13.1 | HTTP 请求库 | | **dayjs** | ^1.11.19 | 轻量级日期处理库 |
| **zustand** | ^5.0.8 | 轻量级状态管理 | | **axios** | ^1.13.1 | HTTP 请求库 |
| **react-hook-form** | ^7.66.0 | 表单处理库 | | **zustand** | ^5.0.8 | 轻量级状态管理 |
| **zod** | ^4.1.12 | TypeScript 数据验证库 | | **react-hook-form** | ^7.66.0 | 表单处理库 |
| **zod** | ^4.1.12 | TypeScript 数据验证库 |
### Expo 原生模块 ### Expo 原生模块
| 库名 | 版本 | 用途 |
|------|------|------| | 库名 | 版本 | 用途 |
| **@react-native-async-storage/async-storage** | ^2.2.0 | 本地存储 | | --------------------------------------------- | ------- | -------------- |
| **expo-image** | ^3.0.10 | 优化的图片组件 | | **@react-native-async-storage/async-storage** | ^2.2.0 | 本地存储 |
| **expo-haptics** | ^15.0.7 | 触觉反馈 | | **expo-image** | ^3.0.10 | 优化的图片组件 |
| **expo-haptics** | ^15.0.7 | 触觉反馈 |
### 开发工具 ### 开发工具
| 库名 | 版本 | 用途 |
|------|------|------| | 库名 | 版本 | 用途 |
| -------------------- | -------- | ----------------------------- |
| **@types/lodash-es** | ^4.17.12 | Lodash-ES TypeScript 类型定义 | | **@types/lodash-es** | ^4.17.12 | Lodash-ES TypeScript 类型定义 |
--- ---
@ -39,13 +42,13 @@
import { map, filter, uniq, pick, cloneDeep, debounce } from 'lodash-es'; import { map, filter, uniq, pick, cloneDeep, debounce } from 'lodash-es';
// 数组操作 // 数组操作
map([1, 2, 3], n => n * 2); // [2, 4, 6] map([1, 2, 3], (n) => n * 2); // [2, 4, 6]
filter([1, 2, 3, 4], n => n % 2 === 0); // [2, 4] filter([1, 2, 3, 4], (n) => n % 2 === 0); // [2, 4]
uniq([1, 2, 2, 3]); // [1, 2, 3] uniq([1, 2, 2, 3]); // [1, 2, 3]
// 对象操作 // 对象操作
pick({ a: 1, b: 2, c: 3 }, ['a', 'b']); // { a: 1, b: 2 } pick({ a: 1, b: 2, c: 3 }, ['a', 'b']); // { a: 1, b: 2 }
cloneDeep(obj); // 深拷贝 cloneDeep(obj); // 深拷贝
// 防抖和节流 // 防抖和节流
const handleSearch = debounce((text) => { const handleSearch = debounce((text) => {
@ -54,7 +57,7 @@ const handleSearch = debounce((text) => {
// 也可以全量导入(不推荐,会增加包体积) // 也可以全量导入(不推荐,会增加包体积)
import _ from 'lodash-es'; import _ from 'lodash-es';
_.map([1, 2, 3], n => n * 2); _.map([1, 2, 3], (n) => n * 2);
``` ```
### 2. Day.js - 日期处理 ### 2. Day.js - 日期处理
@ -71,12 +74,12 @@ dayjs.locale('zh-cn');
dayjs().format('YYYY-MM-DD HH:mm:ss'); dayjs().format('YYYY-MM-DD HH:mm:ss');
// 相对时间 // 相对时间
dayjs().fromNow(); // '几秒前' dayjs().fromNow(); // '几秒前'
dayjs().subtract(1, 'day').fromNow(); // '1天前' dayjs().subtract(1, 'day').fromNow(); // '1天前'
// 日期操作 // 日期操作
dayjs().add(7, 'day'); // 7天后 dayjs().add(7, 'day'); // 7天后
dayjs().startOf('month'); // 本月第一天 dayjs().startOf('month'); // 本月第一天
``` ```
### 3. Axios - HTTP 请求 ### 3. Axios - HTTP 请求
@ -329,7 +332,7 @@ export const formatRelativeTime = (date: Date | string | number) => {
export const formatChatTime = (timestamp: number) => { export const formatChatTime = (timestamp: number) => {
const date = dayjs(timestamp); const date = dayjs(timestamp);
const now = dayjs(); const now = dayjs();
if (date.isSame(now, 'day')) { if (date.isSame(now, 'day')) {
return date.format('HH:mm'); return date.format('HH:mm');
} else if (date.isSame(now.subtract(1, 'day'), 'day')) { } else if (date.isSame(now.subtract(1, 'day'), 'day')) {
@ -384,4 +387,3 @@ export const formatChatTime = (timestamp: number) => {
--- ---
**提示**:所有库都已安装并配置好,可以直接在项目中使用!🎉 **提示**:所有库都已安装并配置好,可以直接在项目中使用!🎉

3
docs/USAGE_EXAMPLES.md

@ -339,7 +339,7 @@ export default function SettingsScreen() {
const theme = useSettingsStore((state) => state.theme); const theme = useSettingsStore((state) => state.theme);
const notificationsEnabled = useSettingsStore((state) => state.notificationsEnabled); const notificationsEnabled = useSettingsStore((state) => state.notificationsEnabled);
const hapticsEnabled = useSettingsStore((state) => state.hapticsEnabled); const hapticsEnabled = useSettingsStore((state) => state.hapticsEnabled);
const setTheme = useSettingsStore((state) => state.setTheme); const setTheme = useSettingsStore((state) => state.setTheme);
const setNotificationsEnabled = useSettingsStore((state) => state.setNotificationsEnabled); const setNotificationsEnabled = useSettingsStore((state) => state.setNotificationsEnabled);
const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled); const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
@ -426,4 +426,3 @@ export default function SettingsScreen() {
--- ---
**提示**:这些示例都是可以直接使用的代码,复制到你的项目中即可! **提示**:这些示例都是可以直接使用的代码,复制到你的项目中即可!

1
eas.json

@ -20,4 +20,3 @@
"production": {} "production": {}
} }
} }

26
hooks/index.ts

@ -0,0 +1,26 @@
/**
* Hooks
*/
// Debounce
export { useDebounce, useDebouncedCallback } from './useDebounce';
// Throttle
export { useThrottle } from './useThrottle';
// Haptics
export { useHaptics } from './useHaptics';
// Request
export { useRequest } from './useRequest';
// Theme
export {
useColorScheme,
useThemeColor,
useThemeColors,
useThemeInfo,
} from './useTheme';
// Client-only value (for SSR/Web compatibility)
export { useClientOnlyValue } from './useClientOnlyValue';

23
hooks/useClientOnlyValue.ts

@ -0,0 +1,23 @@
/**
* Client-only value Hook
*
* This hook is used to provide different values for server-side rendering (SSR)
* and client-side rendering. It's particularly useful for React Native Web
* to prevent hydration errors.
*
* @param server - Value to use during server-side rendering
* @param client - Value to use during client-side rendering
* @returns The appropriate value based on the rendering context
*
* @example
* ```tsx
* // Disable header on server, enable on client
* headerShown: useClientOnlyValue(false, true)
* ```
*/
// This function is web-only as native doesn't currently support server (or build-time) rendering.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
return client;
}

34
hooks/useClientOnlyValue.web.ts

@ -0,0 +1,34 @@
/**
* Client-only value Hook (Web version)
*
* This hook is used to provide different values for server-side rendering (SSR)
* and client-side rendering on web platforms.
*
* On web, we use `useEffect` to detect if we're on the client or server,
* since `useEffect` is not invoked during server rendering.
*
* @param server - Value to use during server-side rendering
* @param client - Value to use during client-side rendering
* @returns The appropriate value based on the rendering context
*
* @example
* ```tsx
* // Disable header on server, enable on client
* headerShown: useClientOnlyValue(false, true)
* ```
*/
import React from 'react';
// `useEffect` is not invoked during server rendering, meaning
// we can use this to determine if we're on the server or not.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
const [value, setValue] = React.useState<S | C>(server);
React.useEffect(() => {
setValue(client);
}, [client]);
return value;
}

2
src/hooks/useDebounce.ts → hooks/useDebounce.ts

@ -80,5 +80,3 @@ export function useDebounce<T extends (...args: any[]) => any>(
* console.log('Searching:', text); * console.log('Searching:', text);
* }, 500); * }, 500);
*/ */

11
src/hooks/useHaptics.ts → hooks/useHaptics.ts

@ -5,7 +5,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { useHapticsEnabled } from '@/src/stores/settingsStore'; import { useHapticsEnabled } from '@/stores/settingsStore';
/** /**
* Hook * Hook
@ -90,20 +90,20 @@ export function useHaptics() {
/** /**
* 使 * 使
* *
* function MyComponent() { * function MyComponent() {
* const haptics = useHaptics(); * const haptics = useHaptics();
* *
* const handlePress = () => { * const handlePress = () => {
* haptics.light(); * haptics.light();
* // 执行其他操作 * // 执行其他操作
* }; * };
* *
* const handleSuccess = () => { * const handleSuccess = () => {
* haptics.success(); * haptics.success();
* // 显示成功消息 * // 显示成功消息
* }; * };
* *
* return ( * return (
* <TouchableOpacity onPress={handlePress}> * <TouchableOpacity onPress={handlePress}>
* <Text>Press me</Text> * <Text>Press me</Text>
@ -111,4 +111,3 @@ export function useHaptics() {
* ); * );
* } * }
*/ */

291
hooks/useRequest.ts

@ -0,0 +1,291 @@
/**
* Hook
*
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { AxiosError } from 'axios';
import type { RequestConfig } from '@/utils/network/api';
/**
*
*/
export interface RequestState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
/**
*
*/
export interface UseRequestOptions<T> 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<T = any>(
requestFn: () => Promise<T>,
options: UseRequestOptions<T> = {}
) {
const { immediate = false, onSuccess, onError, onFinally, defaultData = null } = options;
const [state, setState] = useState<RequestState<T>>({
data: defaultData,
loading: false,
error: null,
});
const requestRef = useRef(requestFn);
requestRef.current = requestFn;
const abortControllerRef = useRef<AbortController | null>(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<T = any>(
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,
};
}

100
hooks/useTheme.ts

@ -0,0 +1,100 @@
/**
* Hooks
*
* 访
*/
import { useMemo } from 'react';
import { useColorScheme as useSystemColorScheme } from 'react-native';
import { useTheme as useThemeStore } from '@/stores';
import Colors from '@/constants/Colors';
/**
* light | dark
*
* settingsStore
* 'light' | 'dark' | 'auto'
*/
export function useColorScheme(): 'light' | 'dark' {
const userTheme = useThemeStore();
const systemTheme = useSystemColorScheme();
// 如果用户选择了 'auto',则使用系统主题
if (userTheme === 'auto') {
return systemTheme === 'dark' ? 'dark' : 'light';
}
// 否则使用用户选择的主题
return userTheme;
}
/**
*
*
* @param props - { light?: string; dark?: string }
* @param colorName - Colors
* @returns
*
* @example
* ```tsx
* const textColor = useThemeColor({}, 'text');
* const customColor = useThemeColor({ light: '#000', dark: '#fff' }, 'text');
* ```
*/
export function useThemeColor(
props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
): string {
const theme = useColorScheme();
const colorFromProps = props[theme];
if (colorFromProps) {
return colorFromProps;
} else {
return Colors[theme][colorName];
}
}
/**
*
*
* @returns
*
* @example
* ```tsx
* const colors = useThemeColors();
* <View style={{ backgroundColor: colors.background }}>
* <Text style={{ color: colors.text }}>Hello</Text>
* </View>
* ```
*/
export function useThemeColors() {
const theme = useColorScheme();
return useMemo(() => {
return Colors[theme];
}, [theme]);
}
/**
*
*
* @returns
*
* @example
* ```tsx
* const { theme, colors, isDark } = useThemeInfo();
* ```
*/
export function useThemeInfo() {
const theme = useColorScheme();
const colors = useThemeColors();
return useMemo(() => ({
theme,
colors,
isDark: theme === 'dark',
isLight: theme === 'light',
}), [theme, colors]);
}

5
src/hooks/useThrottle.ts → hooks/useThrottle.ts

@ -49,13 +49,12 @@ export function useThrottle<T extends (...args: any[]) => any>(
/** /**
* 使 * 使
* *
* const handleScroll = useThrottle((event) => { * const handleScroll = useThrottle((event) => {
* console.log('Scrolling:', event); * console.log('Scrolling:', event);
* }, 200); * }, 200);
* *
* <ScrollView onScroll={handleScroll}> * <ScrollView onScroll={handleScroll}>
* ... * ...
* </ScrollView> * </ScrollView>
*/ */

19
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;

15
package.json

@ -6,7 +6,11 @@
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "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": { "dependencies": {
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
@ -14,6 +18,7 @@
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/native": "^7.1.8", "@react-navigation/native": "^7.1.8",
"axios": "^1.13.1", "axios": "^1.13.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"expo": "~54.0.22", "expo": "~54.0.22",
"expo-constants": "~18.0.10", "expo-constants": "~18.0.10",
@ -27,6 +32,7 @@
"expo-updates": "^29.0.12", "expo-updates": "^29.0.12",
"expo-web-browser": "~15.0.9", "expo-web-browser": "~15.0.9",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"md5": "^2.3.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hook-form": "^7.66.0", "react-hook-form": "^7.66.0",
@ -41,8 +47,15 @@
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/md5": "^2.3.6",
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"prettier": "^3.6.2",
"react-test-renderer": "19.1.0", "react-test-renderer": "19.1.0",
"typescript": "~5.9.2" "typescript": "~5.9.2"
}, },

502
pnpm-lock.yaml

@ -23,6 +23,9 @@ importers:
axios: axios:
specifier: ^1.13.1 specifier: ^1.13.1
version: 1.13.1 version: 1.13.1
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dayjs: dayjs:
specifier: ^1.11.19 specifier: ^1.11.19
version: 1.11.19 version: 1.11.19
@ -62,6 +65,9 @@ importers:
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
md5:
specifier: ^2.3.0
version: 2.3.0
react: react:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0 version: 19.1.0
@ -99,12 +105,33 @@ importers:
specifier: ^5.0.8 specifier: ^5.0.8
version: 5.0.8(@types/[email protected])([email protected])([email protected]([email protected])) version: 5.0.8(@types/[email protected])([email protected])([email protected]([email protected]))
devDependencies: devDependencies:
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/lodash-es': '@types/lodash-es':
specifier: ^4.17.12 specifier: ^4.17.12
version: 4.17.12 version: 4.17.12
'@types/md5':
specifier: ^2.3.6
version: 2.3.6
'@types/react': '@types/react':
specifier: ~19.1.0 specifier: ~19.1.0
version: 19.1.17 version: 19.1.17
concurrently:
specifier: ^9.2.1
version: 9.2.1
cors:
specifier: ^2.8.5
version: 2.8.5
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: react-test-renderer:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0([email protected]) version: 19.1.0([email protected])
@ -1192,9 +1219,15 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/[email protected]':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
'@types/[email protected]':
resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
@ -1210,6 +1243,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/[email protected]':
resolution: {integrity: sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==}
@ -1248,6 +1284,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@ -1409,6 +1449,10 @@ packages:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
[email protected]:
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'}
[email protected]: [email protected]:
resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==}
@ -1452,6 +1496,10 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
[email protected]: [email protected]:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1471,6 +1519,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1562,16 +1613,41 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
[email protected]:
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
engines: {node: '>=18'}
hasBin: true
[email protected]: [email protected]:
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
[email protected]:
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
[email protected]:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
[email protected]:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==}
[email protected]:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
@ -1579,6 +1655,12 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
[email protected]:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
[email protected]:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
[email protected]: [email protected]:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1749,6 +1831,9 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
[email protected]: [email protected]:
resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==} resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==}
@ -1918,6 +2003,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
[email protected]:
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
engines: {node: '>= 18'}
[email protected]: [email protected]:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@ -1945,6 +2034,10 @@ packages:
resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
[email protected]:
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
engines: {node: '>= 0.8'}
[email protected]: [email protected]:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1976,6 +2069,10 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
[email protected]:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==} resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1984,6 +2081,10 @@ packages:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
[email protected]: [email protected]:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@ -2085,6 +2186,14 @@ packages:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
[email protected]:
resolution: {integrity: sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
[email protected]:
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
engines: {node: '>=8.0.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
@ -2092,6 +2201,14 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
[email protected]:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
engines: {node: '>=0.10.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@ -2124,9 +2241,16 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
[email protected]:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
[email protected]:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
[email protected]: [email protected]:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2136,10 +2260,18 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
[email protected]:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'} engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
@ -2148,6 +2280,13 @@ packages:
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
engines: {node: '>=8'} engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2358,12 +2497,23 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
[email protected]:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
[email protected]: [email protected]:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
[email protected]: [email protected]:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
[email protected]:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'}
[email protected]: [email protected]:
resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2503,6 +2653,10 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
engines: {node: '>= 0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -2557,6 +2711,10 @@ packages:
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
@ -2602,6 +2760,10 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
on-[email protected]: on-[email protected]:
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -2683,6 +2845,9 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'} engines: {node: '>=16 || 14 >=14.18'}
[email protected]:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -2713,6 +2878,11 @@ packages:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
[email protected]:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
[email protected]: [email protected]:
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2739,6 +2909,10 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
[email protected]:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@ -2750,6 +2924,10 @@ packages:
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}
hasBin: true hasBin: true
[email protected]:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2761,6 +2939,10 @@ packages:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==}
engines: {node: '>= 0.10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true hasBin: true
@ -2930,6 +3112,9 @@ packages:
resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
[email protected]:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2962,9 +3147,19 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
[email protected]:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
[email protected]:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
[email protected]:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
[email protected]: [email protected]:
resolution: {integrity: sha512-FySGAa0RGcFiN6zfrO9JvK1r7TB59xuzCcTHOBXBNoKgDejlOQCR2KL/FGk3/iDlsqyYg1ELZpOmlg09B01Czw==} resolution: {integrity: sha512-FySGAa0RGcFiN6zfrO9JvK1r7TB59xuzCcTHOBXBNoKgDejlOQCR2KL/FGk3/iDlsqyYg1ELZpOmlg09B01Czw==}
@ -2998,6 +3193,10 @@ packages:
resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
[email protected]:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'}
[email protected]: [email protected]:
resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -3006,6 +3205,10 @@ packages:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
[email protected]:
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
engines: {node: '>= 18'}
[email protected]: [email protected]:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
@ -3034,6 +3237,22 @@ packages:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
[email protected]: [email protected]:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@ -3207,6 +3426,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
[email protected]:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
[email protected]: [email protected]:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@ -3225,6 +3448,10 @@ packages:
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
engines: {node: '>=8'} engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@ -4938,10 +5165,16 @@ snapshots:
dependencies: dependencies:
'@babel/types': 7.28.5 '@babel/types': 7.28.5
'@types/[email protected]': {}
'@types/[email protected]': '@types/[email protected]':
dependencies: dependencies:
'@types/node': 24.10.0 '@types/node': 24.10.0
'@types/[email protected]':
dependencies:
'@types/node': 24.10.0
'@types/[email protected]': {} '@types/[email protected]': {}
'@types/[email protected]': '@types/[email protected]':
@ -4958,6 +5191,8 @@ snapshots:
'@types/[email protected]': {} '@types/[email protected]': {}
'@types/[email protected]': {}
'@types/[email protected]': '@types/[email protected]':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
@ -4999,6 +5234,11 @@ snapshots:
mime-types: 2.1.35 mime-types: 2.1.35
negotiator: 0.6.3 negotiator: 0.6.3
[email protected]:
dependencies:
mime-types: 3.0.1
negotiator: 1.0.0
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -5056,7 +5296,7 @@ snapshots:
[email protected]: [email protected]:
dependencies: dependencies:
follow-redirects: 1.15.11 follow-redirects: 1.15.11([email protected])
form-data: 4.0.4 form-data: 4.0.4
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
@ -5201,6 +5441,20 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
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
[email protected]: [email protected]:
dependencies: dependencies:
stream-buffers: 2.2.0 stream-buffers: 2.2.0
@ -5252,6 +5506,11 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
[email protected]:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -5269,6 +5528,8 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -5368,6 +5629,15 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
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
[email protected]: [email protected]:
dependencies: dependencies:
debug: 2.6.9 debug: 2.6.9
@ -5377,12 +5647,27 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
[email protected]:
dependencies:
safe-buffer: 5.2.1
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
browserslist: 4.27.0 browserslist: 4.27.0
[email protected]:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
[email protected]: [email protected]:
dependencies: dependencies:
node-fetch: 2.7.0 node-fetch: 2.7.0
@ -5395,6 +5680,10 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
[email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -5504,6 +5793,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]([email protected])([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]): [email protected]([email protected])([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]):
@ -5718,6 +6009,38 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
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
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -5758,6 +6081,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
[email protected]:
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
[email protected]: [email protected]:
dependencies: dependencies:
locate-path: 5.0.0 locate-path: 5.0.0
@ -5770,7 +6104,9 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]([email protected]):
optionalDependencies:
debug: 4.4.3
[email protected]: {} [email protected]: {}
@ -5787,10 +6123,14 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
mime-types: 2.1.35 mime-types: 2.1.35
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -5894,6 +6234,25 @@ snapshots:
statuses: 2.0.1 statuses: 2.0.1
toidentifier: 1.0.1 toidentifier: 1.0.1
[email protected]:
dependencies:
'@types/http-proxy': 1.17.17
debug: 4.4.3
http-proxy: 1.18.1([email protected])
is-glob: 4.0.3
is-plain-object: 5.0.0
micromatch: 4.0.8
transitivePeerDependencies:
- supports-color
[email protected]([email protected]):
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.15.11([email protected])
requires-port: 1.0.0
transitivePeerDependencies:
- debug
[email protected]: [email protected]:
dependencies: dependencies:
agent-base: 7.1.4 agent-base: 7.1.4
@ -5903,6 +6262,14 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
safer-buffer: 2.1.2
[email protected]:
dependencies:
safer-buffer: 2.1.2
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -5930,20 +6297,34 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
is-extglob: 2.1.1
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
is-docker: 2.2.1 is-docker: 2.2.1
@ -6159,10 +6540,20 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
charenc: 0.0.2
crypt: 0.0.2
is-buffer: 1.1.6
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
is-plain-obj: 2.1.0 is-plain-obj: 2.1.0
@ -6532,6 +6923,10 @@ snapshots:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
[email protected]:
dependencies:
mime-db: 1.54.0
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -6570,6 +6965,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -6603,6 +7000,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
on-[email protected]: on-[email protected]:
dependencies: dependencies:
ee-first: 1.1.1 ee-first: 1.1.1
@ -6680,6 +7079,8 @@ snapshots:
lru-cache: 10.4.3 lru-cache: 10.4.3
minipass: 7.1.2 minipass: 7.1.2
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -6704,6 +7105,8 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -6729,12 +7132,21 @@ snapshots:
kleur: 3.0.3 kleur: 3.0.3
sisteransi: 1.0.5 sisteransi: 1.0.5
[email protected]:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
side-channel: 1.1.0
[email protected]: [email protected]:
dependencies: dependencies:
decode-uri-component: 0.2.2 decode-uri-component: 0.2.2
@ -6748,6 +7160,13 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.7.0
unpipe: 1.0.0
[email protected]: [email protected]:
dependencies: dependencies:
deep-extend: 0.6.0 deep-extend: 0.6.0
@ -6971,6 +7390,8 @@ snapshots:
rc: 1.2.8 rc: 1.2.8
resolve: 1.7.1 resolve: 1.7.1
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -7000,8 +7421,24 @@ snapshots:
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
[email protected]:
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
[email protected]:
dependencies:
tslib: 2.8.1
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -7050,6 +7487,22 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
[email protected]:
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
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@ -7061,6 +7514,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
[email protected]:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.0
transitivePeerDependencies:
- supports-color
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -7079,6 +7541,34 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
[email protected]:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
[email protected]:
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
[email protected]:
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
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -7239,6 +7729,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@ -7249,6 +7741,12 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
content-type: 1.0.5
media-typer: 1.1.0
mime-types: 3.0.1
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}

31
src/schemas/auth.ts → schemas/auth.ts

@ -8,14 +8,8 @@ import { z } from 'zod';
* Schema * Schema
*/ */
export const loginSchema = z.object({ export const loginSchema = z.object({
email: z email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
.string() password: z.string().min(6, '密码至少6个字符').max(20, '密码最多20个字符'),
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
password: z
.string()
.min(6, '密码至少6个字符')
.max(20, '密码最多20个字符'),
rememberMe: z.boolean().optional(), rememberMe: z.boolean().optional(),
}); });
@ -29,10 +23,7 @@ export const registerSchema = z
.min(3, '用户名至少3个字符') .min(3, '用户名至少3个字符')
.max(20, '用户名最多20个字符') .max(20, '用户名最多20个字符')
.regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'), .regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
email: z email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
.string()
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
password: z password: z
.string() .string()
.min(6, '密码至少6个字符') .min(6, '密码至少6个字符')
@ -54,10 +45,7 @@ export const registerSchema = z
* Schema * Schema
*/ */
export const forgotPasswordSchema = z.object({ export const forgotPasswordSchema = z.object({
email: z email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
.string()
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
}); });
/** /**
@ -70,10 +58,7 @@ export const resetPasswordSchema = z
.min(6, '验证码为6位') .min(6, '验证码为6位')
.max(6, '验证码为6位') .max(6, '验证码为6位')
.regex(/^\d{6}$/, '验证码必须是6位数字'), .regex(/^\d{6}$/, '验证码必须是6位数字'),
password: z password: z.string().min(6, '密码至少6个字符').max(20, '密码最多20个字符'),
.string()
.min(6, '密码至少6个字符')
.max(20, '密码最多20个字符'),
confirmPassword: z.string().min(1, '请确认密码'), confirmPassword: z.string().min(1, '请确认密码'),
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {
@ -87,10 +72,7 @@ export const resetPasswordSchema = z
export const changePasswordSchema = z export const changePasswordSchema = z
.object({ .object({
oldPassword: z.string().min(1, '请输入当前密码'), oldPassword: z.string().min(1, '请输入当前密码'),
newPassword: z newPassword: z.string().min(6, '新密码至少6个字符').max(20, '新密码最多20个字符'),
.string()
.min(6, '新密码至少6个字符')
.max(20, '新密码最多20个字符'),
confirmPassword: z.string().min(1, '请确认新密码'), confirmPassword: z.string().min(1, '请确认新密码'),
}) })
.refine((data) => data.newPassword === data.confirmPassword, { .refine((data) => data.newPassword === data.confirmPassword, {
@ -127,4 +109,3 @@ export type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
export type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>; export type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;
export type ChangePasswordFormData = z.infer<typeof changePasswordSchema>; export type ChangePasswordFormData = z.infer<typeof changePasswordSchema>;
export type PhoneLoginFormData = z.infer<typeof phoneLoginSchema>; export type PhoneLoginFormData = z.infer<typeof phoneLoginSchema>;

36
schemas/index.ts

@ -0,0 +1,36 @@
/**
* Schemas
*/
// Auth Schemas
export {
loginSchema,
registerSchema,
forgotPasswordSchema,
resetPasswordSchema,
changePasswordSchema,
phoneLoginSchema,
} from './auth';
export type {
LoginFormData,
RegisterFormData,
ForgotPasswordFormData,
ResetPasswordFormData,
ChangePasswordFormData,
PhoneLoginFormData,
} from './auth';
// User Schemas
export {
userSchema,
updateUserSchema,
bindPhoneSchema,
bindEmailSchema,
} from './user';
export type {
UserFormData,
UpdateUserFormData,
BindPhoneFormData,
BindEmailFormData,
} from './user';

12
src/schemas/user.ts → schemas/user.ts

@ -21,11 +21,7 @@ export const userSchema = z.object({
* Schema * Schema
*/ */
export const updateProfileSchema = z.object({ export const updateProfileSchema = z.object({
nickname: z nickname: z.string().min(2, '昵称至少2个字符').max(20, '昵称最多20个字符').optional(),
.string()
.min(2, '昵称至少2个字符')
.max(20, '昵称最多20个字符')
.optional(),
avatar: z.string().url('请输入有效的头像URL').optional(), avatar: z.string().url('请输入有效的头像URL').optional(),
phone: z phone: z
.string() .string()
@ -55,10 +51,7 @@ export const bindPhoneSchema = z.object({
* Schema * Schema
*/ */
export const bindEmailSchema = z.object({ export const bindEmailSchema = z.object({
email: z email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
.string()
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
code: z code: z
.string() .string()
.min(6, '验证码为6位') .min(6, '验证码为6位')
@ -73,4 +66,3 @@ export type User = z.infer<typeof userSchema>;
export type UpdateProfileFormData = z.infer<typeof updateProfileSchema>; export type UpdateProfileFormData = z.infer<typeof updateProfileSchema>;
export type BindPhoneFormData = z.infer<typeof bindPhoneSchema>; export type BindPhoneFormData = z.infer<typeof bindPhoneSchema>;
export type BindEmailFormData = z.infer<typeof bindEmailSchema>; export type BindEmailFormData = z.infer<typeof bindEmailSchema>;

36
screens/index.ts

@ -0,0 +1,36 @@
/**
* Screens
*
*
*
*
* screens/
* index.ts #
* TestScreen/ #
* index.tsx #
* components/ #
* styles.ts #
* ProfileScreen/ #
* SettingsScreen/ #
*
* 使
* 1. screens/
* 2. app/ screens/
* 3. 便
*
*
* ```typescript
* // screens/TestScreen/index.tsx
* export default function TestScreen() {
* return <View>...</View>;
* }
*
* // app/test.tsx
* import TestScreen from '@/screens/TestScreen';
* export default TestScreen;
* ```
*/
// 当前暂无导出,等待添加业务页面组件
export {};

111
scripts/proxy-server.js

@ -0,0 +1,111 @@
/**
* 开发环境代理服务器
* 用于解决跨域问题和统一 API 请求
*/
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const cors = require('cors');
const app = express();
const PORT = 8086;
// 启用 CORS
app.use(cors({
origin: '*', // 允许所有源,开发环境使用
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'Accept',
// 自定义请求头
'cmdId',
'datetime',
'pwds',
'aseqId',
'nc',
'apiName',
'tid',
'custId',
'reqId',
'isMobileOpen',
'languageNum',
'project',
'platform',
'checkOr',
'tbc',
'reqKey',
'signature',
'authorization',
],
exposedHeaders: ['cmdId', 'datetime', 'pwds', 'aseqId', 'nc', 'checkOr', 'checkor'],
}));
// 目标 API 服务器地址
const API_TARGET = process.env.API_TARGET || 'https://51zhh5.notbug.org';
// 代理路径列表
const PROXY_PATHS = ['/api/v1', '/api/v2', '/api/v3'];
// 为每个路径配置代理
PROXY_PATHS.forEach((path) => {
app.use(
path,
createProxyMiddleware({
target: API_TARGET,
changeOrigin: true,
secure: false, // 如果目标服务器使用自签名证书,设置为 false
pathRewrite: (pathStr, req) => {
// Express 会自动去掉匹配的前缀,所以需要加回来
const fullPath = path + pathStr;
console.log(`[Proxy] Path rewrite: ${pathStr}${fullPath}`);
return fullPath;
},
onProxyReq: (proxyReq, req, res) => {
const fullPath = path + req.url;
console.log(`[Proxy] ${req.method} ${fullPath}${API_TARGET}${fullPath}`);
},
onProxyRes: (proxyRes, req, res) => {
const fullPath = path + req.url;
console.log(`[Proxy] ${req.method} ${fullPath}${proxyRes.statusCode}`);
// 确保 CORS 头被正确设置
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, cmdId, datetime, pwds, aseqId, nc, apiName, tid, custId, reqId, isMobileOpen, languageNum, project, platform, checkOr, tbc, reqKey, signature, authorization');
res.setHeader('Access-Control-Expose-Headers', 'cmdId, datetime, pwds, aseqId, nc, checkOr, checkor');
res.setHeader('Access-Control-Allow-Credentials', 'true');
},
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 ║
`);
});

29
src/services/authService.ts → services/authService.ts

@ -3,7 +3,7 @@
* API * API
*/ */
import { request } from '@/src/utils/api'; import { request } from '@/utils/network/api';
import type { import type {
LoginFormData, LoginFormData,
RegisterFormData, RegisterFormData,
@ -11,8 +11,8 @@ import type {
ResetPasswordFormData, ResetPasswordFormData,
ChangePasswordFormData, ChangePasswordFormData,
PhoneLoginFormData, PhoneLoginFormData,
} from '@/src/schemas/auth'; } from '@/schemas/auth';
import type { User } from '@/src/schemas/user'; import type { User } from '@/schemas/user';
/** /**
* API * API
@ -40,10 +40,7 @@ class AuthService {
* *
*/ */
async login(data: LoginFormData): Promise<LoginResponse> { async login(data: LoginFormData): Promise<LoginResponse> {
const response = await request.post<ApiResponse<LoginResponse>>( const response = await request.post<ApiResponse<LoginResponse>>('/auth/login', data);
'/auth/login',
data
);
return response.data; return response.data;
} }
@ -51,10 +48,7 @@ class AuthService {
* *
*/ */
async phoneLogin(data: PhoneLoginFormData): Promise<LoginResponse> { async phoneLogin(data: PhoneLoginFormData): Promise<LoginResponse> {
const response = await request.post<ApiResponse<LoginResponse>>( const response = await request.post<ApiResponse<LoginResponse>>('/auth/phone-login', data);
'/auth/phone-login',
data
);
return response.data; return response.data;
} }
@ -62,10 +56,7 @@ class AuthService {
* *
*/ */
async register(data: RegisterFormData): Promise<LoginResponse> { async register(data: RegisterFormData): Promise<LoginResponse> {
const response = await request.post<ApiResponse<LoginResponse>>( const response = await request.post<ApiResponse<LoginResponse>>('/auth/register', data);
'/auth/register',
data
);
return response.data; return response.data;
} }
@ -108,10 +99,9 @@ class AuthService {
* token * token
*/ */
async refreshToken(refreshToken: string): Promise<{ token: string }> { async refreshToken(refreshToken: string): Promise<{ token: string }> {
const response = await request.post<ApiResponse<{ token: string }>>( const response = await request.post<ApiResponse<{ token: string }>>('/auth/refresh-token', {
'/auth/refresh-token', refreshToken,
{ refreshToken } });
);
return response.data; return response.data;
} }
@ -131,4 +121,3 @@ class AuthService {
// 导出单例 // 导出单例
export const authService = new AuthService(); export const authService = new AuthService();
export default authService; export default authService;

8
services/index.ts

@ -0,0 +1,8 @@
/**
* Services
*/
export { default as authService } from './authService';
export { default as userService } from './userService';
export { default as tenantService } from './tenantService';

43
services/tenantService.ts

@ -0,0 +1,43 @@
/**
*
* API
*/
import { request } from '@/utils/network/api';
// import type { User, UpdateProfileFormData } from '@/schemas/user';
/**
* API
*/
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
/**
* tenant
*/
class TenantService {
/**
*
*/
getPlatformData(params?: Record<string, any>): Promise<ApiResponse> {
const data = {
language: '0',
...params
};
return request.post('/v2', data, {
headers: {
cmdId: 371130,
headerType: 1,
apiName: 'getPlatformData',
tid: '',
},
});
}
}
// 导出单例
export const tenantService = new TenantService();
export default tenantService;

19
src/services/userService.ts → services/userService.ts

@ -3,8 +3,8 @@
* API * API
*/ */
import { request } from '@/src/utils/api'; import { request } from '@/utils/network/api';
import type { User, UpdateProfileFormData } from '@/src/schemas/user'; import type { User, UpdateProfileFormData } from '@/schemas/user';
/** /**
* API * API
@ -50,15 +50,11 @@ class UserService {
const formData = new FormData(); const formData = new FormData();
formData.append('avatar', file); formData.append('avatar', file);
const response = await request.post<ApiResponse<{ url: string }>>( const response = await request.post<ApiResponse<{ url: string }>>('/user/avatar', formData, {
'/user/avatar', headers: {
formData, 'Content-Type': 'multipart/form-data',
{ },
headers: { });
'Content-Type': 'multipart/form-data',
},
}
);
return response.data; return response.data;
} }
@ -87,4 +83,3 @@ class UserService {
// 导出单例 // 导出单例
export const userService = new UserService(); export const userService = new UserService();
export default userService; export default userService;

29
src/index.ts

@ -1,29 +0,0 @@
/**
*
*/
// Utils
export { default as api, request } from './utils/api';
export { default as Storage, STORAGE_KEYS } from './utils/storage';
export * from './utils/date';
// Stores
export * from './stores/userStore';
export * from './stores/settingsStore';
// Schemas
export * from './schemas/auth';
export * from './schemas/user';
// Services
export { default as authService } from './services/authService';
export { default as userService } from './services/userService';
// Hooks
export * from './hooks/useDebounce';
export * from './hooks/useThrottle';
export * from './hooks/useHaptics';
// Types
export * from './types';

147
src/stores/settingsStore.ts

@ -1,147 +0,0 @@
/**
*
* 使 Zustand + AsyncStorage
*/
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
*
*/
export type Theme = 'light' | 'dark' | 'auto';
/**
*
*/
export type Language = 'zh-CN' | 'en-US';
/**
*
*/
interface SettingsState {
// 状态
theme: Theme;
language: Language;
notificationsEnabled: boolean;
soundEnabled: boolean;
hapticsEnabled: boolean;
// 操作
setTheme: (theme: Theme) => void;
setLanguage: (language: Language) => void;
setNotificationsEnabled: (enabled: boolean) => void;
setSoundEnabled: (enabled: boolean) => void;
setHapticsEnabled: (enabled: boolean) => void;
resetSettings: () => void;
}
/**
*
*/
const DEFAULT_SETTINGS = {
theme: 'auto' as Theme,
language: 'zh-CN' as Language,
notificationsEnabled: true,
soundEnabled: true,
hapticsEnabled: true,
};
/**
* Store
*/
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
// 初始状态
...DEFAULT_SETTINGS,
// 设置主题
setTheme: (theme) => {
set({ theme });
if (__DEV__) {
console.log('🎨 Theme changed:', theme);
}
},
// 设置语言
setLanguage: (language) => {
set({ language });
if (__DEV__) {
console.log('🌐 Language changed:', language);
}
},
// 设置通知开关
setNotificationsEnabled: (enabled) => {
set({ notificationsEnabled: enabled });
if (__DEV__) {
console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled');
}
},
// 设置声音开关
setSoundEnabled: (enabled) => {
set({ soundEnabled: enabled });
if (__DEV__) {
console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled');
}
},
// 设置触觉反馈开关
setHapticsEnabled: (enabled) => {
set({ hapticsEnabled: enabled });
if (__DEV__) {
console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled');
}
},
// 重置所有设置
resetSettings: () => {
set(DEFAULT_SETTINGS);
if (__DEV__) {
console.log('🔄 Settings reset to default');
}
},
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
/**
* Hooks
*/
// 获取主题
export const useTheme = () => useSettingsStore((state) => state.theme);
// 获取语言
export const useLanguage = () => useSettingsStore((state) => state.language);
// 获取通知状态
export const useNotificationsEnabled = () =>
useSettingsStore((state) => state.notificationsEnabled);
// 获取声音状态
export const useSoundEnabled = () =>
useSettingsStore((state) => state.soundEnabled);
// 获取触觉反馈状态
export const useHapticsEnabled = () =>
useSettingsStore((state) => state.hapticsEnabled);
// 获取设置操作方法
export const useSettingsActions = () =>
useSettingsStore((state) => ({
setTheme: state.setTheme,
setLanguage: state.setLanguage,
setNotificationsEnabled: state.setNotificationsEnabled,
setSoundEnabled: state.setSoundEnabled,
setHapticsEnabled: state.setHapticsEnabled,
resetSettings: state.resetSettings,
}));

142
src/stores/userStore.ts

@ -1,142 +0,0 @@
/**
*
* 使 Zustand + AsyncStorage
*/
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
*
*/
export interface User {
id: string;
username: string;
email: string;
avatar?: string;
nickname?: string;
phone?: string;
createdAt?: string;
}
/**
*
*/
interface UserState {
// 状态
user: User | null;
isLoggedIn: boolean;
token: string | null;
// 操作
setUser: (user: User) => void;
setToken: (token: string) => void;
login: (user: User, token: string) => void;
logout: () => void;
updateUser: (updates: Partial<User>) => void;
}
/**
* Store
*/
export const useUserStore = create<UserState>()(
persist(
(set, get) => ({
// 初始状态
user: null,
isLoggedIn: false,
token: null,
// 设置用户信息
setUser: (user) => {
set({ user, isLoggedIn: true });
},
// 设置 token
setToken: (token) => {
set({ token });
},
// 登录
login: (user, token) => {
set({
user,
token,
isLoggedIn: true,
});
// 同时保存 token 到 AsyncStorage(用于 API 请求)
AsyncStorage.setItem('auth_token', token);
if (__DEV__) {
console.log('✅ User logged in:', user.username);
}
},
// 登出
logout: () => {
set({
user: null,
token: null,
isLoggedIn: false,
});
// 清除 AsyncStorage 中的 token
AsyncStorage.removeItem('auth_token');
if (__DEV__) {
console.log('👋 User logged out');
}
},
// 更新用户信息
updateUser: (updates) => {
const currentUser = get().user;
if (currentUser) {
set({
user: { ...currentUser, ...updates },
});
if (__DEV__) {
console.log('📝 User updated:', updates);
}
}
},
}),
{
name: 'user-storage', // AsyncStorage 中的键名
storage: createJSONStorage(() => AsyncStorage),
// 可以选择性地持久化某些字段
partialize: (state) => ({
user: state.user,
token: state.token,
isLoggedIn: state.isLoggedIn,
}),
}
)
);
/**
* Hooks
*/
// 获取用户信息
export const useUser = () => useUserStore((state) => state.user);
// 获取登录状态
export const useIsLoggedIn = () => useUserStore((state) => state.isLoggedIn);
// 获取 token
export const useToken = () => useUserStore((state) => state.token);
// 获取用户操作方法
export const useUserActions = () =>
useUserStore((state) => ({
setUser: state.setUser,
setToken: state.setToken,
login: state.login,
logout: state.logout,
updateUser: state.updateUser,
}));

144
src/utils/api.ts

@ -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: <T = any>(url: string, config?: AxiosRequestConfig) =>
api.get<T, T>(url, config),
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
api.post<T, T>(url, data, config),
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
api.put<T, T>(url, data, config),
delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
api.delete<T, T>(url, config),
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
api.patch<T, T>(url, data, config),
};
export default api;

36
stores/index.ts

@ -0,0 +1,36 @@
/**
* Stores
*/
// User Store
export {
useUserStore,
useUser,
useIsLoggedIn,
useToken,
useUserActions,
restoreUserState,
} from './userStore';
export type { User } from './userStore';
// Settings Store
export {
useSettingsStore,
useTheme,
useLanguage,
useNotificationsEnabled,
useSoundEnabled,
useHapticsEnabled,
useSettingsActions,
restoreSettingsState,
} from './settingsStore';
export type { Theme, Language } from './settingsStore';
// Tenant Store
export {
default as useTenantStore,
useTenantInfo,
useTenantStates,
useTenantActions,
restoreTenantState,
} from './tenantStore';

167
stores/settingsStore.ts

@ -0,0 +1,167 @@
/**
*
* 使 Zustand + AsyncStorage
*/
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
*
*/
export type Theme = 'light' | 'dark' | 'auto';
/**
*
*/
export type Language = 'zh-CN' | 'en-US';
/**
*
*/
interface SettingsState {
// 状态
theme: Theme;
language: Language;
notificationsEnabled: boolean;
soundEnabled: boolean;
hapticsEnabled: boolean;
// 操作
setTheme: (theme: Theme) => void;
setLanguage: (language: Language) => void;
setNotificationsEnabled: (enabled: boolean) => void;
setSoundEnabled: (enabled: boolean) => void;
setHapticsEnabled: (enabled: boolean) => void;
resetSettings: () => void;
}
/**
*
*/
const DEFAULT_SETTINGS = {
theme: 'auto' as Theme,
language: 'zh-CN' as Language,
notificationsEnabled: true,
soundEnabled: true,
hapticsEnabled: true,
};
/**
* Store
*/
export const useSettingsStore = create<SettingsState>()((set, get) => ({
// 初始状态
...DEFAULT_SETTINGS,
// 设置主题
setTheme: (theme) => {
set({ theme });
// 手动持久化
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
if (__DEV__) {
console.log('🎨 Theme changed:', theme);
}
},
// 设置语言
setLanguage: (language) => {
set({ language });
// 手动持久化
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
if (__DEV__) {
console.log('🌐 Language changed:', language);
}
},
// 设置通知开关
setNotificationsEnabled: (enabled) => {
set({ notificationsEnabled: enabled });
// 手动持久化
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
if (__DEV__) {
console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled');
}
},
// 设置声音开关
setSoundEnabled: (enabled) => {
set({ soundEnabled: enabled });
// 手动持久化
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
if (__DEV__) {
console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled');
}
},
// 设置触觉反馈开关
setHapticsEnabled: (enabled) => {
set({ hapticsEnabled: enabled });
// 手动持久化
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
if (__DEV__) {
console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled');
}
},
// 重置所有设置
resetSettings: () => {
set(DEFAULT_SETTINGS);
// 手动持久化
AsyncStorage.setItem('settings-storage', JSON.stringify(DEFAULT_SETTINGS));
if (__DEV__) {
console.log('🔄 Settings reset to default');
}
},
}));
// 从 AsyncStorage 恢复状态的函数
export const restoreSettingsState = async () => {
try {
const stored = await AsyncStorage.getItem('settings-storage');
if (stored) {
const state = JSON.parse(stored);
useSettingsStore.setState(state);
if (__DEV__) {
console.log('✅ Settings state restored from storage');
}
}
} catch (error) {
console.error('Failed to restore settings state:', error);
}
};
/**
* Hooks
*/
// 获取主题
export const useTheme = () => useSettingsStore((state) => state.theme);
// 获取语言
export const useLanguage = () => useSettingsStore((state) => state.language);
// 获取通知状态
export const useNotificationsEnabled = () =>
useSettingsStore((state) => state.notificationsEnabled);
// 获取声音状态
export const useSoundEnabled = () => useSettingsStore((state) => state.soundEnabled);
// 获取触觉反馈状态
export const useHapticsEnabled = () => useSettingsStore((state) => state.hapticsEnabled);
// 获取设置操作方法
// 使用 useShallow 避免每次渲染都返回新对象
export const useSettingsActions = () =>
useSettingsStore(
useShallow((state) => ({
setTheme: state.setTheme,
setLanguage: state.setLanguage,
setNotificationsEnabled: state.setNotificationsEnabled,
setSoundEnabled: state.setSoundEnabled,
setHapticsEnabled: state.setHapticsEnabled,
resetSettings: state.resetSettings,
}))
);

122
stores/tenantStore.ts

@ -0,0 +1,122 @@
/**
*
* 使 Zustand + AsyncStorage
*/
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { STORAGE_KEYS } from '@/utils/storage';
import { tenantService } from '@/services/tenantService';
import { useEffect } from 'react';
/**
*
*/
// export interface Tenant {
// id: string;
// username: string;
// email: string;
// avatar?: string;
// nickname?: string;
// phone?: string;
// createdAt?: string;
// }
/**
*
*/
interface TenantState {
// 状态
tenantInfo: Record<string, any> | null;
// 操作
setTenantInfo: (data: Record<string, any>) => void;
requestTenantInfo: (data?: Record<string, any>) => Promise<any>;
}
/**
* Store
*/
const useTenantStore = create<TenantState>()((set, get) => ({
// 初始状态
tenantInfo: null,
// 设置租户信息(通用方法,包含持久化逻辑)
setTenantInfo: (data: any) => {
set({ tenantInfo: data });
// 手动持久化
// AsyncStorage.setItem(STORAGE_KEYS.TENANT_STORE, JSON.stringify({ tenantInfo: data }));
if (__DEV__) {
console.log('💾 Tenant info saved:', data);
}
},
// 获取租户信息(调用 API 并使用 setTenantInfo 保存)
requestTenantInfo: async () => {
try {
const params = {
domain_addr: 'https://51zhh5.notbug.org',
};
const { data } = await tenantService.getPlatformData(params);
// 调用 setTenantInfo 来保存数据,避免重复代码
get().setTenantInfo(data);
if (__DEV__) {
console.log('✅ Tenant info loaded:', data);
}
return Promise.resolve(data);
} catch (error) {
console.error('Failed to request tenant info:', error);
return Promise.reject(error);
}
},
}));
// 从 AsyncStorage 恢复状态的函数
export const restoreTenantState = async () => {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEYS.TENANT_STORE);
if (stored) {
const state = JSON.parse(stored);
useTenantStore.setState(state);
if (__DEV__) {
console.log('✅ Tenant state restored from storage');
}
}
} catch (error) {
console.error('Failed to restore tenant state:', error);
}
};
/**
* Hooks
*/
// 获取用户信息
export const useTenantInfo = () => useTenantStore((state) => state.tenantInfo);
// 获取租户状态
export const useTenantStates = () =>
useTenantStore(
useShallow((state) => ({
tenantInfo: state.tenantInfo,
tenantLoad: !!state.tenantInfo?.tid || !!state.tenantInfo?.create_time,
}))
);
// 获取租户操作方法
// 使用 useShallow 避免每次渲染都返回新对象
export const useTenantActions = () =>
useTenantStore(
useShallow((state) => ({
setTenantInfo: state.setTenantInfo,
requestTenantInfo: state.requestTenantInfo,
}))
);
export default useTenantStore;

161
stores/userStore.ts

@ -0,0 +1,161 @@
/**
*
* 使 Zustand + AsyncStorage
*/
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
*
*/
export interface User {
id: string;
username: string;
email: string;
avatar?: string;
nickname?: string;
phone?: string;
createdAt?: string;
}
/**
*
*/
interface UserState {
// 状态
user: User | null;
isLoggedIn: boolean;
token: string | null;
// 操作
setUser: (user: User) => void;
setToken: (token: string) => void;
login: (user: User, token: string) => void;
logout: () => void;
updateUser: (updates: Partial<User>) => void;
}
/**
* Store
*/
export const useUserStore = create<UserState>()((set, get) => ({
// 初始状态
user: null,
isLoggedIn: false,
token: null,
// 设置用户信息
setUser: (user) => {
const newState = { user, isLoggedIn: true };
set(newState);
// 手动持久化
AsyncStorage.setItem('user-storage', JSON.stringify(newState));
},
// 设置 token
setToken: (token) => {
set({ token });
// 手动持久化 - 延迟执行以确保状态已更新
setTimeout(() => {
const state = get();
AsyncStorage.setItem('user-storage', JSON.stringify(state));
}, 0);
},
// 登录
login: (user, token) => {
const newState = {
user,
token,
isLoggedIn: true,
};
set(newState);
// 同时保存 token 到 AsyncStorage(用于 API 请求)
AsyncStorage.setItem('auth_token', token);
// 手动持久化整个状态
AsyncStorage.setItem('user-storage', JSON.stringify(newState));
if (__DEV__) {
console.log('✅ User logged in:', user.username);
}
},
// 登出
logout: () => {
const newState = {
user: null,
token: null,
isLoggedIn: false,
};
set(newState);
// 清除 AsyncStorage 中的 token
AsyncStorage.removeItem('auth_token');
// 清除持久化状态
AsyncStorage.removeItem('user-storage');
if (__DEV__) {
console.log('👋 User logged out');
}
},
// 更新用户信息
updateUser: (updates) => {
const currentUser = get().user;
if (currentUser) {
const newUser = { ...currentUser, ...updates };
set({ user: newUser });
// 手动持久化
AsyncStorage.setItem('user-storage', JSON.stringify({ ...get(), user: newUser }));
if (__DEV__) {
console.log('📝 User updated:', updates);
}
}
},
}));
// 从 AsyncStorage 恢复状态的函数
export const restoreUserState = async () => {
try {
const stored = await AsyncStorage.getItem('user-storage');
if (stored) {
const state = JSON.parse(stored);
useUserStore.setState(state);
if (__DEV__) {
console.log('✅ User state restored from storage');
}
}
} catch (error) {
console.error('Failed to restore user state:', error);
}
};
/**
* Hooks
*/
// 获取用户信息
export const useUser = () => useUserStore((state) => state.user);
// 获取登录状态
export const useIsLoggedIn = () => useUserStore((state) => state.isLoggedIn);
// 获取 token
export const useToken = () => useUserStore((state) => state.token);
// 获取用户操作方法
// 使用 useShallow 避免每次渲染都返回新对象
export const useUserActions = () =>
useUserStore(
useShallow((state) => ({
setUser: state.setUser,
setToken: state.setToken,
login: state.login,
logout: state.logout,
updateUser: state.updateUser,
}))
);

36
theme/index.ts

@ -0,0 +1,36 @@
/**
*
*
*
*/
// 导出颜色配置
export { default as Colors } from '@/constants/Colors';
// 导出主题 Hooks
export {
useColorScheme,
useThemeColor,
useThemeColors,
useThemeInfo,
} from '@/hooks/useTheme';
// 导出主题组件
export {
ThemedText,
ThemedView,
Text as ThemeText,
View as ThemeView,
} from '@/components/Themed';
export type {
ThemedTextProps,
ThemedViewProps,
TextProps as ThemeTextProps,
ViewProps as ThemeViewProps,
} from '@/components/Themed';
// 导出主题工具函数
export * from './utils';
export * from './styles';

261
theme/styles.ts

@ -0,0 +1,261 @@
/**
*
*
*
*
* React Native CSS
*/
import { StyleSheet, TextStyle, ViewStyle } from 'react-native';
import Colors from '@/constants/Colors';
/**
*
*/
export type ThemeStyles = {
light: any;
dark: any;
};
/**
*
*
* CSS 使
*
* @param createStyles -
* @returns
*
* @example
* ```tsx
* const styles = createThemeStyles((colors) => ({
* container: {
* backgroundColor: colors.background,
* padding: 16,
* },
* text: {
* color: colors.text,
* fontSize: 16,
* },
* }));
*
* // 使用
* const theme = useColorScheme();
* <View style={styles[theme].container}>
* <Text style={styles[theme].text}>Hello</Text>
* </View>
* ```
*/
export function createThemeStyles<T extends StyleSheet.NamedStyles<T>>(
createStyles: (colors: typeof Colors.light) => T
): ThemeStyles {
return {
light: StyleSheet.create(createStyles(Colors.light)),
dark: StyleSheet.create(createStyles(Colors.dark)),
};
}
/**
*
*
*
*
* @param lightStyles -
* @param darkStyles -
* @returns
*
* @example
* ```tsx
* const styles = createResponsiveThemeStyles(
* {
* container: { backgroundColor: '#fff', padding: 16 },
* text: { color: '#000', fontSize: 14 },
* },
* {
* container: { backgroundColor: '#000', padding: 20 },
* text: { color: '#fff', fontSize: 16 },
* }
* );
* ```
*/
export function createResponsiveThemeStyles<T extends StyleSheet.NamedStyles<T>>(
lightStyles: T,
darkStyles: T
): ThemeStyles {
return {
light: StyleSheet.create(lightStyles),
dark: StyleSheet.create(darkStyles),
};
}
/**
*
*
* Tailwind CSS
*/
export const commonStyles = createThemeStyles((colors) => ({
// 容器样式
container: {
flex: 1,
backgroundColor: colors.background,
},
containerPadded: {
flex: 1,
backgroundColor: colors.background,
padding: 16,
},
containerCentered: {
flex: 1,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
},
// 卡片样式
card: {
backgroundColor: colors.card,
borderRadius: 8,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
},
cardElevated: {
backgroundColor: colors.card,
borderRadius: 8,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
// 文本样式
textPrimary: {
color: colors.text,
fontSize: 16,
} as TextStyle,
textSecondary: {
color: colors.textSecondary,
fontSize: 14,
} as TextStyle,
textTertiary: {
color: colors.textTertiary,
fontSize: 12,
} as TextStyle,
textTitle: {
color: colors.text,
fontSize: 24,
fontWeight: 'bold',
} as TextStyle,
textSubtitle: {
color: colors.text,
fontSize: 18,
fontWeight: '600',
} as TextStyle,
// 按钮样式
button: {
backgroundColor: colors.buttonPrimary,
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
} as ViewStyle,
buttonOutline: {
backgroundColor: 'transparent',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
borderWidth: 1,
borderColor: colors.buttonPrimary,
alignItems: 'center',
justifyContent: 'center',
} as ViewStyle,
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
} as TextStyle,
buttonTextOutline: {
color: colors.buttonPrimary,
fontSize: 16,
fontWeight: '600',
} as TextStyle,
// 输入框样式
input: {
backgroundColor: colors.inputBackground,
borderWidth: 1,
borderColor: colors.inputBorder,
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
fontSize: 16,
color: colors.text,
} as TextStyle,
inputFocused: {
backgroundColor: colors.inputBackground,
borderWidth: 2,
borderColor: colors.primary,
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
fontSize: 16,
color: colors.text,
} as TextStyle,
// 分隔线
separator: {
height: 1,
backgroundColor: colors.separator,
} as ViewStyle,
separatorVertical: {
width: 1,
backgroundColor: colors.separator,
} as ViewStyle,
// 间距
spacingXs: { height: 4 } as ViewStyle,
spacingSm: { height: 8 } as ViewStyle,
spacingMd: { height: 16 } as ViewStyle,
spacingLg: { height: 24 } as ViewStyle,
spacingXl: { height: 32 } as ViewStyle,
// 布局
row: {
flexDirection: 'row',
alignItems: 'center',
} as ViewStyle,
rowBetween: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
} as ViewStyle,
column: {
flexDirection: 'column',
} as ViewStyle,
center: {
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
}));
/**
*
*
* @param styles -
* @param theme -
* @returns
*
* @example
* ```tsx
* const theme = useColorScheme();
* const style = getThemeStyle(styles, theme);
* <View style={style.container} />
* ```
*/
export function getThemeStyle<T>(styles: ThemeStyles, theme: 'light' | 'dark'): T {
return styles[theme];
}

122
theme/utils.ts

@ -0,0 +1,122 @@
/**
*
*
*
*/
import Colors from '@/constants/Colors';
/**
*
*
* @param theme - 'light' | 'dark'
* @param colorName -
* @returns
*/
export function getThemeColor(
theme: 'light' | 'dark',
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
): string {
return Colors[theme][colorName];
}
/**
*
*
* @param theme - 'light' | 'dark'
* @returns
*/
export function getThemeColors(theme: 'light' | 'dark') {
return Colors[theme];
}
/**
*
*
* @param lightStyle -
* @param darkStyle -
* @param theme -
* @returns
*
* @example
* ```tsx
* const style = createThemedStyle(
* { backgroundColor: '#fff' },
* { backgroundColor: '#000' },
* theme
* );
* ```
*/
export function createThemedStyle<T>(
lightStyle: T,
darkStyle: T,
theme: 'light' | 'dark'
): T {
return theme === 'dark' ? darkStyle : lightStyle;
}
/**
*
*
* @param lightValue -
* @param darkValue -
* @param theme -
* @returns
*
* @example
* ```tsx
* const fontSize = selectByTheme(14, 16, theme);
* ```
*/
export function selectByTheme<T>(
lightValue: T,
darkValue: T,
theme: 'light' | 'dark'
): T {
return theme === 'dark' ? darkValue : lightValue;
}
/**
*
*
* @param color -
* @param opacity - 0-1
* @returns
*
* @example
* ```tsx
* const color = withOpacity('#000000', 0.5); // rgba(0, 0, 0, 0.5)
* ```
*/
export function withOpacity(color: string, opacity: number): string {
// 移除 # 号
const hex = color.replace('#', '');
// 转换为 RGB
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
/**
*
*
* @param theme -
* @returns
*/
export function isDarkTheme(theme: 'light' | 'dark'): boolean {
return theme === 'dark';
}
/**
*
*
* @param theme -
* @returns
*/
export function isLightTheme(theme: 'light' | 'dark'): boolean {
return theme === 'light';
}

11
tsconfig.json

@ -3,15 +3,8 @@
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"paths": { "paths": {
"@/*": [ "@/*": ["./*"]
"./*"
]
} }
}, },
"include": [ "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
} }

73
types/api.ts

@ -0,0 +1,73 @@
/**
* API
*/
/**
*
*/
export interface PaginationParams {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
/**
*
*/
export interface PaginationResponse<T> {
list: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
hasMore: boolean;
}
/**
*
*/
export interface ListResponse<T> {
items: T[];
total: number;
}
/**
* ID
*/
export interface IdParams {
id: string | number;
}
/**
*
*/
export interface BatchParams {
ids: (string | number)[];
}
/**
*
*/
export interface SearchParams {
keyword: string;
filters?: Record<string, any>;
}
/**
*
*/
export interface UploadResponse {
url: string;
filename: string;
size: number;
mimeType: string;
}
/**
*
*/
export interface OperationResponse {
success: boolean;
message?: string;
}

1
src/types/index.ts → types/index.ts

@ -78,4 +78,3 @@ declare global {
} }
export {}; export {};

0
utils/common.ts

132
utils/config.ts

@ -0,0 +1,132 @@
/**
*
*
*/
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 平台使用代理服务器
// 代理服务器运行在 http://localhost:8086
// 会将 /api/* 请求转发到目标服务器
return 'http://localhost:8086/api';
} else {
// iOS/Android 使用本机 IP
// ⚠ 重要:需要替换为你的本机 IP 地址
// 查看本机 IP:
// - Windows: ipconfig
// - Mac/Linux: ifconfig
// - 或者使用 Metro Bundler 显示的 IP
return 'http://192.168.1.100:8086/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;

1
src/utils/date.ts → utils/date.ts

@ -216,4 +216,3 @@ export const nowInSeconds = (): number => {
}; };
export default dayjs; export default dayjs;

38
utils/index.ts

@ -0,0 +1,38 @@
/**
* Utils
*/
// Network API
export {
default as api,
request,
cancelAllRequests,
cancelRequest,
createRetryRequest,
} from './network/api';
export type { ApiResponse, ApiError, RequestConfig } from './network/api';
// Storage
export { default as Storage, STORAGE_KEYS } from './storage';
export { default as SessionStorage, SESSION_KEYS } from './sessionStorage';
export { default as StorageManager } from './storageManager';
export type { StorageType, StorageOptions } from './storageManager';
// Config
export { default as config, printConfig } from './config';
// Date utilities
export {
formatDate,
formatRelativeTime,
formatChatTime,
parseDate,
isToday,
isYesterday,
isSameDay,
addDays,
subtractDays,
startOfDay,
endOfDay,
} from './date';

589
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<T = any> {
code: number;
message: string;
data: T;
success?: boolean;
}
/**
* API
*/
export interface ApiError {
code: number;
message: string;
errors?: Record<string, string[]>;
}
/**
*
*/
export interface RequestConfig extends AxiosRequestConfig {
/** 是否显示 loading */
showLoading?: boolean;
/** 是否显示错误提示 */
showError?: boolean;
/** 是否重试 */
retry?: boolean;
/** 重试次数 */
retryCount?: number;
/** 是否需要 token */
requiresAuth?: boolean;
/** 自定义错误处理 */
customErrorHandler?: (error: AxiosError<ApiError>) => 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<string, CancelTokenSource>();
// 是否正在刷新 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<string | null> {
try {
const refreshToken = await AsyncStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token');
}
// 调用刷新 token 接口
const response = await axios.post<ApiResponse<{ token: string; refreshToken: string }>>(
`${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,
fullURL: `${config.baseURL}${config.url}`,
params: config.params,
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<ApiError>) => {
// 从请求队列中移除
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: <T = any>(url: string, config?: RequestConfig) => api.get<T, T>(url, config),
/**
* POST
*/
post: <T = any>(url: string, data?: any, config?: RequestConfig) =>
api.post<T, T>(url, data, config),
/**
* PUT
*/
put: <T = any>(url: string, data?: any, config?: RequestConfig) =>
api.put<T, T>(url, data, config),
/**
* DELETE
*/
delete: <T = any>(url: string, config?: RequestConfig) => api.delete<T, T>(url, config),
/**
* PATCH
*/
patch: <T = any>(url: string, data?: any, config?: RequestConfig) =>
api.patch<T, T>(url, data, config),
/**
*
*/
upload: <T = any>(
url: string,
file: File | Blob,
onProgress?: (progress: number) => void,
config?: RequestConfig
) => {
const formData = new FormData();
formData.append('file', file);
return api.post<T, T>(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: any = 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: <T = any>(requests: Promise<T>[]) => Promise.all(requests),
/**
*
*/
series: async <T = any>(requests: (() => Promise<T>)[]): Promise<T[]> => {
const results: T[] = [];
for (const request of requests) {
const result = await request();
results.push(result);
}
return results;
},
};
/**
*
*/
export function createRetryRequest<T = any>(
requestFn: () => Promise<T>,
maxRetries = 3,
retryDelay = 1000
): Promise<T> {
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;

492
utils/network/des.ts

@ -0,0 +1,492 @@
// @ts-nocheck
//des
//this takes the key, the message, and whether to encrypt or decrypt
export function des(key, message, encrypt, mode, iv, padding) {
if (encrypt)
//如果是加密的话,首先转换编码
message = unescape(encodeURIComponent(message));
//declaring this locally speeds things up a bit
const spfunction1 = [
0x1010400, 0, 0x10000, 0x1010404, 0x1010004, 0x10404, 0x4, 0x10000, 0x400, 0x1010400, 0x1010404,
0x400, 0x1000404, 0x1010004, 0x1000000, 0x4, 0x404, 0x1000400, 0x1000400, 0x10400, 0x10400,
0x1010000, 0x1010000, 0x1000404, 0x10004, 0x1000004, 0x1000004, 0x10004, 0, 0x404, 0x10404,
0x1000000, 0x10000, 0x1010404, 0x4, 0x1010000, 0x1010400, 0x1000000, 0x1000000, 0x400,
0x1010004, 0x10000, 0x10400, 0x1000004, 0x400, 0x4, 0x1000404, 0x10404, 0x1010404, 0x10004,
0x1010000, 0x1000404, 0x1000004, 0x404, 0x10404, 0x1010400, 0x404, 0x1000400, 0x1000400, 0,
0x10004, 0x10400, 0, 0x1010004,
];
const spfunction2 = [
-0x7fef7fe0, -0x7fff8000, 0x8000, 0x108020, 0x100000, 0x20, -0x7fefffe0, -0x7fff7fe0,
-0x7fffffe0, -0x7fef7fe0, -0x7fef8000, -0x80000000, -0x7fff8000, 0x100000, 0x20, -0x7fefffe0,
0x108000, 0x100020, -0x7fff7fe0, 0, -0x80000000, 0x8000, 0x108020, -0x7ff00000, 0x100020,
-0x7fffffe0, 0, 0x108000, 0x8020, -0x7fef8000, -0x7ff00000, 0x8020, 0, 0x108020, -0x7fefffe0,
0x100000, -0x7fff7fe0, -0x7ff00000, -0x7fef8000, 0x8000, -0x7ff00000, -0x7fff8000, 0x20,
-0x7fef7fe0, 0x108020, 0x20, 0x8000, -0x80000000, 0x8020, -0x7fef8000, 0x100000, -0x7fffffe0,
0x100020, -0x7fff7fe0, -0x7fffffe0, 0x100020, 0x108000, 0, -0x7fff8000, 0x8020, -0x80000000,
-0x7fefffe0, -0x7fef7fe0, 0x108000,
];
const spfunction3 = [
0x208, 0x8020200, 0, 0x8020008, 0x8000200, 0, 0x20208, 0x8000200, 0x20008, 0x8000008, 0x8000008,
0x20000, 0x8020208, 0x20008, 0x8020000, 0x208, 0x8000000, 0x8, 0x8020200, 0x200, 0x20200,
0x8020000, 0x8020008, 0x20208, 0x8000208, 0x20200, 0x20000, 0x8000208, 0x8, 0x8020208, 0x200,
0x8000000, 0x8020200, 0x8000000, 0x20008, 0x208, 0x20000, 0x8020200, 0x8000200, 0, 0x200,
0x20008, 0x8020208, 0x8000200, 0x8000008, 0x200, 0, 0x8020008, 0x8000208, 0x20000, 0x8000000,
0x8020208, 0x8, 0x20208, 0x20200, 0x8000008, 0x8020000, 0x8000208, 0x208, 0x8020000, 0x20208,
0x8, 0x8020008, 0x20200,
];
const spfunction4 = [
0x802001, 0x2081, 0x2081, 0x80, 0x802080, 0x800081, 0x800001, 0x2001, 0, 0x802000, 0x802000,
0x802081, 0x81, 0, 0x800080, 0x800001, 0x1, 0x2000, 0x800000, 0x802001, 0x80, 0x800000, 0x2001,
0x2080, 0x800081, 0x1, 0x2080, 0x800080, 0x2000, 0x802080, 0x802081, 0x81, 0x800080, 0x800001,
0x802000, 0x802081, 0x81, 0, 0, 0x802000, 0x2080, 0x800080, 0x800081, 0x1, 0x802001, 0x2081,
0x2081, 0x80, 0x802081, 0x81, 0x1, 0x2000, 0x800001, 0x2001, 0x802080, 0x800081, 0x2001, 0x2080,
0x800000, 0x802001, 0x80, 0x800000, 0x2000, 0x802080,
];
const spfunction5 = [
0x100, 0x2080100, 0x2080000, 0x42000100, 0x80000, 0x100, 0x40000000, 0x2080000, 0x40080100,
0x80000, 0x2000100, 0x40080100, 0x42000100, 0x42080000, 0x80100, 0x40000000, 0x2000000,
0x40080000, 0x40080000, 0, 0x40000100, 0x42080100, 0x42080100, 0x2000100, 0x42080000,
0x40000100, 0, 0x42000000, 0x2080100, 0x2000000, 0x42000000, 0x80100, 0x80000, 0x42000100,
0x100, 0x2000000, 0x40000000, 0x2080000, 0x42000100, 0x40080100, 0x2000100, 0x40000000,
0x42080000, 0x2080100, 0x40080100, 0x100, 0x2000000, 0x42080000, 0x42080100, 0x80100,
0x42000000, 0x42080100, 0x2080000, 0, 0x40080000, 0x42000000, 0x80100, 0x2000100, 0x40000100,
0x80000, 0, 0x40080000, 0x2080100, 0x40000100,
];
const spfunction6 = [
0x20000010, 0x20400000, 0x4000, 0x20404010, 0x20400000, 0x10, 0x20404010, 0x400000, 0x20004000,
0x404010, 0x400000, 0x20000010, 0x400010, 0x20004000, 0x20000000, 0x4010, 0, 0x400010,
0x20004010, 0x4000, 0x404000, 0x20004010, 0x10, 0x20400010, 0x20400010, 0, 0x404010, 0x20404000,
0x4010, 0x404000, 0x20404000, 0x20000000, 0x20004000, 0x10, 0x20400010, 0x404000, 0x20404010,
0x400000, 0x4010, 0x20000010, 0x400000, 0x20004000, 0x20000000, 0x4010, 0x20000010, 0x20404010,
0x404000, 0x20400000, 0x404010, 0x20404000, 0, 0x20400010, 0x10, 0x4000, 0x20400000, 0x404010,
0x4000, 0x400010, 0x20004010, 0, 0x20404000, 0x20000000, 0x400010, 0x20004010,
];
const spfunction7 = [
0x200000, 0x4200002, 0x4000802, 0, 0x800, 0x4000802, 0x200802, 0x4200800, 0x4200802, 0x200000,
0, 0x4000002, 0x2, 0x4000000, 0x4200002, 0x802, 0x4000800, 0x200802, 0x200002, 0x4000800,
0x4000002, 0x4200000, 0x4200800, 0x200002, 0x4200000, 0x800, 0x802, 0x4200802, 0x200800, 0x2,
0x4000000, 0x200800, 0x4000000, 0x200800, 0x200000, 0x4000802, 0x4000802, 0x4200002, 0x4200002,
0x2, 0x200002, 0x4000000, 0x4000800, 0x200000, 0x4200800, 0x802, 0x200802, 0x4200800, 0x802,
0x4000002, 0x4200802, 0x4200000, 0x200800, 0, 0x2, 0x4200802, 0, 0x200802, 0x4200000, 0x800,
0x4000002, 0x4000800, 0x800, 0x200002,
];
const spfunction8 = [
0x10001040, 0x1000, 0x40000, 0x10041040, 0x10000000, 0x10001040, 0x40, 0x10000000, 0x40040,
0x10040000, 0x10041040, 0x41000, 0x10041000, 0x41040, 0x1000, 0x40, 0x10040000, 0x10000040,
0x10001000, 0x1040, 0x41000, 0x40040, 0x10040040, 0x10041000, 0x1040, 0, 0, 0x10040040,
0x10000040, 0x10001000, 0x41040, 0x40000, 0x41040, 0x40000, 0x10041000, 0x1000, 0x40,
0x10040040, 0x1000, 0x41040, 0x10001000, 0x40, 0x10000040, 0x10040000, 0x10040040, 0x10000000,
0x40000, 0x10001040, 0, 0x10041040, 0x40040, 0x10000040, 0x10040000, 0x10001000, 0x10001040, 0,
0x10041040, 0x41000, 0x41000, 0x1040, 0x1040, 0x40040, 0x10000000, 0x10041000,
];
//create the 16 or 48 subkeys we will need
const keys = des_createKeys(key);
let m = 0,
i,
j,
temp,
temp2,
right1,
right2,
left,
right,
looping;
let cbcleft, cbcleft2, cbcright, cbcright2;
let endloop, loopinc;
var len = message.length;
let chunk = 0;
//set up the loops for single and triple des
const iterations = keys.length == 32 ? 3 : 9; //single or triple des
if (iterations == 3) {
looping = encrypt ? [0, 32, 2] : [30, -2, -2];
} else {
looping = encrypt ? [0, 32, 2, 62, 30, -2, 64, 96, 2] : [94, 62, -2, 32, 64, 2, 30, -2, -2];
}
//pad the message depending on the padding parameter
if (padding == 2)
message += ' '; //pad the message with spaces
else if (padding == 1) {
if (encrypt) {
temp = 8 - (len % 8);
message += String.fromCharCode(temp, temp, temp, temp, temp, temp, temp, temp);
if (temp === 8) len += 8;
}
} //PKCS7 padding
else if (!padding) message += '\0\0\0\0\0\0\0\0'; //pad the message out with null bytes
//store the result here
let result = '';
let tempresult = '';
if (mode == 1) {
//CBC mode
cbcleft =
(iv.charCodeAt(m++) << 24) |
(iv.charCodeAt(m++) << 16) |
(iv.charCodeAt(m++) << 8) |
iv.charCodeAt(m++);
cbcright =
(iv.charCodeAt(m++) << 24) |
(iv.charCodeAt(m++) << 16) |
(iv.charCodeAt(m++) << 8) |
iv.charCodeAt(m++);
m = 0;
}
//loop through each 64 bit chunk of the message
while (m < len) {
left =
(message.charCodeAt(m++) << 24) |
(message.charCodeAt(m++) << 16) |
(message.charCodeAt(m++) << 8) |
message.charCodeAt(m++);
right =
(message.charCodeAt(m++) << 24) |
(message.charCodeAt(m++) << 16) |
(message.charCodeAt(m++) << 8) |
message.charCodeAt(m++);
//for Cipher Block Chaining mode, xor the message with the previous result
if (mode == 1) {
if (encrypt) {
left ^= cbcleft;
right ^= cbcright;
} else {
cbcleft2 = cbcleft;
cbcright2 = cbcright;
cbcleft = left;
cbcright = right;
}
}
//first each 64 but chunk of the message must be permuted according to IP
temp = ((left >>> 4) ^ right) & 0x0f0f0f0f;
right ^= temp;
left ^= temp << 4;
temp = ((left >>> 16) ^ right) & 0x0000ffff;
right ^= temp;
left ^= temp << 16;
temp = ((right >>> 2) ^ left) & 0x33333333;
left ^= temp;
right ^= temp << 2;
temp = ((right >>> 8) ^ left) & 0x00ff00ff;
left ^= temp;
right ^= temp << 8;
temp = ((left >>> 1) ^ right) & 0x55555555;
right ^= temp;
left ^= temp << 1;
left = (left << 1) | (left >>> 31);
right = (right << 1) | (right >>> 31);
//do this either 1 or 3 times for each chunk of the message
for (j = 0; j < iterations; j += 3) {
endloop = looping[j + 1];
loopinc = looping[j + 2];
//now go through and perform the encryption or decryption
for (i = looping[j]; i != endloop; i += loopinc) {
//for efficiency
right1 = right ^ keys[i];
right2 = ((right >>> 4) | (right << 28)) ^ keys[i + 1];
//the result is attained by passing these bytes through the S selection functions
temp = left;
left = right;
right =
temp ^
(spfunction2[(right1 >>> 24) & 0x3f] |
spfunction4[(right1 >>> 16) & 0x3f] |
spfunction6[(right1 >>> 8) & 0x3f] |
spfunction8[right1 & 0x3f] |
spfunction1[(right2 >>> 24) & 0x3f] |
spfunction3[(right2 >>> 16) & 0x3f] |
spfunction5[(right2 >>> 8) & 0x3f] |
spfunction7[right2 & 0x3f]);
}
temp = left;
left = right;
right = temp; //unreverse left and right
} //for either 1 or 3 iterations
//move then each one bit to the right
left = (left >>> 1) | (left << 31);
right = (right >>> 1) | (right << 31);
//now perform IP-1, which is IP in the opposite direction
temp = ((left >>> 1) ^ right) & 0x55555555;
right ^= temp;
left ^= temp << 1;
temp = ((right >>> 8) ^ left) & 0x00ff00ff;
left ^= temp;
right ^= temp << 8;
temp = ((right >>> 2) ^ left) & 0x33333333;
left ^= temp;
right ^= temp << 2;
temp = ((left >>> 16) ^ right) & 0x0000ffff;
right ^= temp;
left ^= temp << 16;
temp = ((left >>> 4) ^ right) & 0x0f0f0f0f;
right ^= temp;
left ^= temp << 4;
//for Cipher Block Chaining mode, xor the message with the previous result
if (mode == 1) {
if (encrypt) {
cbcleft = left;
cbcright = right;
} else {
left ^= cbcleft2;
right ^= cbcright2;
}
}
tempresult += String.fromCharCode(
left >>> 24,
(left >>> 16) & 0xff,
(left >>> 8) & 0xff,
left & 0xff,
right >>> 24,
(right >>> 16) & 0xff,
(right >>> 8) & 0xff,
right & 0xff
);
chunk += 8;
if (chunk == 512) {
result += tempresult;
tempresult = '';
chunk = 0;
}
} //for every 8 characters, or 64 bits in the message
//return the result as an array
result += tempresult;
if (!encrypt) result = result.replace(/\0*$/g, '');
if (!encrypt) {
//如果是解密的话,解密结束后对PKCS7 padding进行解码,并转换成utf-8编码
if (padding === 1) {
//PKCS7 padding解码
var len = result.length,
paddingChars = 0;
len && (paddingChars = result.charCodeAt(len - 1));
paddingChars <= 8 && (result = result.substring(0, len - paddingChars));
}
//转换成UTF-8编码
result = decodeURIComponent(escape(result));
}
return result;
} //end of des
//des_createKeys
//this takes as input a 64 bit key (even though only 56 bits are used)
//as an array of 2 integers, and returns 16 48 bit keys
function des_createKeys(key) {
//declaring this locally speeds things up a bit
const pc2bytes0 = [
0, 0x4, 0x20000000, 0x20000004, 0x10000, 0x10004, 0x20010000, 0x20010004, 0x200, 0x204,
0x20000200, 0x20000204, 0x10200, 0x10204, 0x20010200, 0x20010204,
];
const pc2bytes1 = [
0, 0x1, 0x100000, 0x100001, 0x4000000, 0x4000001, 0x4100000, 0x4100001, 0x100, 0x101, 0x100100,
0x100101, 0x4000100, 0x4000101, 0x4100100, 0x4100101,
];
const pc2bytes2 = [
0, 0x8, 0x800, 0x808, 0x1000000, 0x1000008, 0x1000800, 0x1000808, 0, 0x8, 0x800, 0x808,
0x1000000, 0x1000008, 0x1000800, 0x1000808,
];
const pc2bytes3 = [
0, 0x200000, 0x8000000, 0x8200000, 0x2000, 0x202000, 0x8002000, 0x8202000, 0x20000, 0x220000,
0x8020000, 0x8220000, 0x22000, 0x222000, 0x8022000, 0x8222000,
];
const pc2bytes4 = [
0, 0x40000, 0x10, 0x40010, 0, 0x40000, 0x10, 0x40010, 0x1000, 0x41000, 0x1010, 0x41010, 0x1000,
0x41000, 0x1010, 0x41010,
];
const pc2bytes5 = [
0, 0x400, 0x20, 0x420, 0, 0x400, 0x20, 0x420, 0x2000000, 0x2000400, 0x2000020, 0x2000420,
0x2000000, 0x2000400, 0x2000020, 0x2000420,
];
const pc2bytes6 = [
0, 0x10000000, 0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002, 0, 0x10000000,
0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002,
];
const pc2bytes7 = [
0, 0x10000, 0x800, 0x10800, 0x20000000, 0x20010000, 0x20000800, 0x20010800, 0x20000, 0x30000,
0x20800, 0x30800, 0x20020000, 0x20030000, 0x20020800, 0x20030800,
];
const pc2bytes8 = [
0, 0x40000, 0, 0x40000, 0x2, 0x40002, 0x2, 0x40002, 0x2000000, 0x2040000, 0x2000000, 0x2040000,
0x2000002, 0x2040002, 0x2000002, 0x2040002,
];
const pc2bytes9 = [
0, 0x10000000, 0x8, 0x10000008, 0, 0x10000000, 0x8, 0x10000008, 0x400, 0x10000400, 0x408,
0x10000408, 0x400, 0x10000400, 0x408, 0x10000408,
];
const pc2bytes10 = [
0, 0x20, 0, 0x20, 0x100000, 0x100020, 0x100000, 0x100020, 0x2000, 0x2020, 0x2000, 0x2020,
0x102000, 0x102020, 0x102000, 0x102020,
];
const pc2bytes11 = [
0, 0x1000000, 0x200, 0x1000200, 0x200000, 0x1200000, 0x200200, 0x1200200, 0x4000000, 0x5000000,
0x4000200, 0x5000200, 0x4200000, 0x5200000, 0x4200200, 0x5200200,
];
const pc2bytes12 = [
0, 0x1000, 0x8000000, 0x8001000, 0x80000, 0x81000, 0x8080000, 0x8081000, 0x10, 0x1010,
0x8000010, 0x8001010, 0x80010, 0x81010, 0x8080010, 0x8081010,
];
const pc2bytes13 = [
0, 0x4, 0x100, 0x104, 0, 0x4, 0x100, 0x104, 0x1, 0x5, 0x101, 0x105, 0x1, 0x5, 0x101, 0x105,
];
//how many iterations (1 for des, 3 for triple des)
const iterations = key.length > 8 ? 3 : 1; //changed by Paul 16/6/2007 to use Triple DES for 9+ byte keys
//stores the return keys
const keys = new Array(32 * iterations);
//now define the left shifts which need to be done
const shifts = [0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0];
//other variables
let lefttemp,
righttemp,
m = 0,
n = 0,
temp;
for (let j = 0; j < iterations; j++) {
//either 1 or 3 iterations
let left =
(key.charCodeAt(m++) << 24) |
(key.charCodeAt(m++) << 16) |
(key.charCodeAt(m++) << 8) |
key.charCodeAt(m++);
let right =
(key.charCodeAt(m++) << 24) |
(key.charCodeAt(m++) << 16) |
(key.charCodeAt(m++) << 8) |
key.charCodeAt(m++);
temp = ((left >>> 4) ^ right) & 0x0f0f0f0f;
right ^= temp;
left ^= temp << 4;
temp = ((right >>> -16) ^ left) & 0x0000ffff;
left ^= temp;
right ^= temp << -16;
temp = ((left >>> 2) ^ right) & 0x33333333;
right ^= temp;
left ^= temp << 2;
temp = ((right >>> -16) ^ left) & 0x0000ffff;
left ^= temp;
right ^= temp << -16;
temp = ((left >>> 1) ^ right) & 0x55555555;
right ^= temp;
left ^= temp << 1;
temp = ((right >>> 8) ^ left) & 0x00ff00ff;
left ^= temp;
right ^= temp << 8;
temp = ((left >>> 1) ^ right) & 0x55555555;
right ^= temp;
left ^= temp << 1;
//the right side needs to be shifted and to get the last four bits of the left side
temp = (left << 8) | ((right >>> 20) & 0x000000f0);
//left needs to be put upside down
left =
(right << 24) |
((right << 8) & 0xff0000) |
((right >>> 8) & 0xff00) |
((right >>> 24) & 0xf0);
right = temp;
//now go through and perform these shifts on the left and right keys
for (let i = 0; i < shifts.length; i++) {
//shift the keys either one or two bits to the left
if (shifts[i]) {
left = (left << 2) | (left >>> 26);
right = (right << 2) | (right >>> 26);
} else {
left = (left << 1) | (left >>> 27);
right = (right << 1) | (right >>> 27);
}
left &= -0xf;
right &= -0xf;
//now apply PC-2, in such a way that E is easier when encrypting or decrypting
//this conversion will look like PC-2 except only the last 6 bits of each byte are used
//rather than 48 consecutive bits and the order of lines will be according to
//how the S selection functions will be applied: S2, S4, S6, S8, S1, S3, S5, S7
lefttemp =
pc2bytes0[left >>> 28] |
pc2bytes1[(left >>> 24) & 0xf] |
pc2bytes2[(left >>> 20) & 0xf] |
pc2bytes3[(left >>> 16) & 0xf] |
pc2bytes4[(left >>> 12) & 0xf] |
pc2bytes5[(left >>> 8) & 0xf] |
pc2bytes6[(left >>> 4) & 0xf];
righttemp =
pc2bytes7[right >>> 28] |
pc2bytes8[(right >>> 24) & 0xf] |
pc2bytes9[(right >>> 20) & 0xf] |
pc2bytes10[(right >>> 16) & 0xf] |
pc2bytes11[(right >>> 12) & 0xf] |
pc2bytes12[(right >>> 8) & 0xf] |
pc2bytes13[(right >>> 4) & 0xf];
temp = ((righttemp >>> 16) ^ lefttemp) & 0x0000ffff;
keys[n++] = lefttemp ^ temp;
keys[n++] = righttemp ^ (temp << 16);
}
} //for each iterations
//return the keys we've created
return keys;
} //end of des_createKeys
function genkey(key, start, end) {
//8 byte / 64 bit Key (DES) or 192 bit Key
return { key: pad(key.slice(start, end)), vector: 1 };
}
function pad(key) {
for (let i = key.length; i < 24; i++) {
key += '0';
}
return key;
}
//3DES加密,使用PKCS7 padding
function encrypt_3des(key, input) {
const genKey = genkey(key, 0, 24);
return btoa(des(genKey.key, input, 1, 0, 0, 1));
}
//3DES解密,使用PKCS7 padding
function decrypt_3des(key, input) {
const genKey = genkey(key, 0, 24);
return des(genKey.key, atob(input), 0, 0, 0, 1);
}
////////////////////////////// TEST //////////////////////////////
export function stringToHex(s) {
let r = '0x';
const hexes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
for (let i = 0; i < s.length; i++) {
r += hexes[s.charCodeAt(i) >> 4] + hexes[s.charCodeAt(i) & 0xf];
}
return r;
}
export function stringToHexArray(s) {
const arr = [];
const hexes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
for (let i = 0; i < s.length; i++) {
arr.push(hexes[s.charCodeAt(i) >> 4] + hexes[s.charCodeAt(i) & 0xf]);
}
return arr;
}
export function hexToString(h) {
let r = '';
for (let i = h.substr(0, 2) == '0x' ? 2 : 0; i < h.length; i += 2) {
r += String.fromCharCode(parseInt(h.substr(i, 2), 16));
}
return r;
}
/* eslint-enable */

119
utils/network/error.ts

@ -0,0 +1,119 @@
// import { useTenantStore, useUserStore, useModalStore } from '@star/stores';
// import { mb_t } from '@star/languages';
// import { router } from '@routers';
import { endsWith, includes, startsWith } from 'lodash-es';
// import { ModalTypeEnum } from '@star/constants';
// 错误提示
export default class NetworkError {
tag: string;
key: string;
info: string;
level: string;
origin: string;
cmd: string;
code: number;
logout: boolean;
update: boolean;
time: string;
constructor(options: any) {
this.tag = '';
this.key = '';
this.info = '';
this.level = '';
this.origin = '';
this.cmd = '';
// this.code = 0
this.code = -1;
this.logout = false;
this.update = false;
this.time = new Date().toLocaleString().split('GMT')[0];
if (options) {
if (options.key) {
this.key = options.key;
}
if (options.tag) {
this.tag = options.tag;
}
if (options.info) {
if (startsWith(options.info, '{') && endsWith(options.info, '}')) {
const result = JSON.parse(options.info);
this.info = result?.msg || result?.exMessage || '';
} else {
this.info = options.info;
}
}
if (options.level) {
this.level = options.level;
}
if (options.origin) {
this.origin = options.origin;
}
if (options.cmd) {
this.cmd = options.cmd;
}
if (options.code) {
this.code = options.code;
}
if (options.logout) {
this.logout = options.logout;
}
if (options.update) {
this.update = options.update;
}
}
// const tenantStore = useTenantStore();
// if ([371130, 381119].includes(Number(this.cmd))) {
// tenantStore.setRepairStatus(true);
// }
if (this.info.indexOf('invalid_token') !== -1) {
this.info = '亲,麻烦重新登录哦';
} else if (this.info.indexOf('Request failed with status code 500') !== -1) {
this.info = '网络不给力,请检查网络' + '(500)';
} else if (this.info.indexOf('Request failed with status code 502') !== -1) {
this.info = '网络不给力,请检查网络' + '(502)';
} else if (this.info.indexOf('Error 503 Service Unavailable') !== -1) {
this.info = '网络不给力,请检查网络' + '(503)';
} else if (this.info.indexOf('Request failed with status code 504') !== -1) {
this.info = '网络不给力,请检查网络' + '(504)';
} else if (this.info.indexOf('timeout of 20000ms') !== -1) {
this.info = '请求超时,请检查网络';
} else if (this.info.indexOf('Error 400 Bad Request') !== -1) {
this.info = '网络不给力,请重新尝试';
} else if (this.info.indexOf('Network Error') !== -1) {
this.info = '网络错误,请检查网络';
} else if (options?.code === '1115') {
// tenantStore.setRepairData(options.origin);
// tenantStore.setRepairStatus(true);
} else {
console.error('err.info:', this.cmd, this.info, options);
// this.info = this.info
// if (this.info) {
// showFailToast(mb_t(this.info));
// }
// if (this.info && this.info.includes('重新登录')) {
// const userStore = useUserStore();
// userStore?.clearUserInfo?.();
// if (
// !startsWith(router.currentRoute.value?.path, '/activity') &&
// !includes(['home', 'mine', 'activity'], router.currentRoute.value?.name)
// ) {
// const modalStore = useModalStore();
// const path = router.currentRoute.value?.fullPath;
// modalStore.showRegisterModal({ redirect: path });
// router.replace?.('/home');
// }
// }
// if (this.info.includes('访问限制')) {
// tenantStore.setIPLimitStatus(true);
// }
// [371130, 370433]返回了请求没有任何数据 才跳维护 其他接口忽略
// if ([371130, 370433].includes(Number(this.cmd))) {
// // console.log(371130, 370433, this.info, '????????????????????????????');
// tenantStore.setRepairStatus(true);
// }
}
// this.info = mb_t(this.info);
}
}

434
utils/network/helper.ts

@ -0,0 +1,434 @@
import { HmacMD5 } from 'crypto-js';
import Base64 from 'crypto-js/enc-base64';
import Latin1 from 'crypto-js/enc-latin1';
import md5 from 'md5';
import { AxiosResponse } from 'axios';
import * as des from './des';
import NetworkError from './error';
import { toNumber, toString, startsWith, isString, isNumber } from 'lodash-es';
import { NetworkTypeEnum } from '@/constants/network';
import appConfig from '../config';
// import NetworkError from './error'
// import { storeToRefs, useTenantStore, useUserStore, useAppStore, start } from '../index';
// import { isMobile, getBetPlatform } from '@star/utils';
// import { langToNum } from '@star/languages';
// 请求到的数据返回
export type NetworkResponse<T> = {
type: NetworkTypeEnum;
data: T;
};
export const getBetPlatform = (isReturnIndex = false) => {
// 5=PC; 7=HOMESCREEN_IOS; 6=HOMESCREEN_ANDROID; 4=H5_IOS 3=IOS 2=H5_ANDROID; 1=ANDROID 8=马甲包
return 'H5_IOS';
// const platform = new URLSearchParams(window.location.search).get('platform');
// if (platform) {
// return platform?.includes('IOS') ? 'IOS' : platform?.includes('ANDROID') ? 'ANDROID' : '';
// }
// if (isAppMJB()) {
// return 'APPS_ANDROID';
// }
// if (isPWA()) {
// if (isIOS()) {
// return isReturnIndex ? 4 : 'HS_IOS';
// } else {
// return isReturnIndex ? 2 : 'HS_ANDROID';
// }
// }
// if (isAndroid()) {
// if (BASE_CONFIG.appVersion > 0) {
// return isReturnIndex ? 1 : 'ANDROID';
// } else {
// return isReturnIndex ? 2 : 'H5_ANDROID';
// }
// }
// if (isIOS()) {
// if (BASE_CONFIG.appVersion > 0) {
// return isReturnIndex ? 3 : 'IOS';
// } else {
// return isReturnIndex ? 4 : 'H5_IOS';
// }
// }
// return isReturnIndex ? 5 : 'PC';
};
const uuid = (len: number, radix: number) => {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid: any[] = [];
radix = radix || chars.length;
if (len) {
// Compact form
for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
// rfc4122, version 4 form
/* eslint-disable */
let r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
/* eslint-enable */
}
return uuid.join('');
};
// 格式化要发送的数据
export const formatSendData = (data: any, type: number = 0) => {
// url code
if (type === 0) {
const arr: any[] = [];
for (const k in data) {
const v = data[k];
if (v instanceof Array) {
for (const subK in v) {
arr.push(`${k}[]=${v[subK]}`);
}
} else {
arr.push(`${k}=${data[k]}`);
}
}
return arr.join('&');
} else if (type === 2) {
return data.join('/');
}
// json
return JSON.stringify(data);
};
export const getP = (p: any) => {
return HmacMD5(p, '7NEkojNzfkk=').toString();
};
export const enD = (rk: string, str: string) => {
const enc = des.des(rk, str, 1, 0, null, 1);
return Base64.stringify(Latin1.parse(enc));
};
export const dnD = (rk: string, str: string) => {
const s = Latin1.stringify(Base64.parse(str));
const d = des.des(rk, s, 0, 0, null, 1);
return d;
};
export const enP = (rk: string, vk: string, t: number) => {
const enc = des.des(vk, rk + t, 1, 0, null, 1);
return Base64.stringify(Latin1.parse(enc));
};
export const dnP = (vk: string, str: string) => {
const s = Latin1.stringify(Base64.parse(str));
const p = des.des(vk, s, 0, 0, null, 1);
return p;
};
export const enC = (rk: string, vk: string, m: string) => {
const enc = HmacMD5(m + rk, vk);
return Base64.stringify(enc);
};
export const getRequestKey = (cmdId: number, data: any) => {
return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`;
};
// 加工请求数据
export const transformRequest = (config: any) => {
const { headerType = 2, paramType = 0, cmdId, tid, ...reset } = config.headers;
const headers: Record<string, any> = {};
// const { tenantInfo } = storeToRefs(useTenantStore());
// const { userInfo } = storeToRefs(useUserStore());
// const { language } = storeToRefs(useAppStore());
const t = new Date().getTime();
const rk = md5(toString(Math.random() + t)).substring(0, 8);
const vk = appConfig.app.vk as string;
const pwds = enP(rk, vk, t);
const tenantInfo = {
tid: 3,
};
let userInfo = {
cust_id: '',
cust_name: '',
access_token: ''
};
// if (['17', '3310052', '310111', '310122', '310400', '4', '402', '401', '310635'].includes(cmdId)) {
// const publicParams = {
// tid: tid ?? tenantInfo.value?.tid ?? '',
// cust_id: (config.data?.cust_id === 0 ? '0' : '') || userInfo?.value?.cust_id || '',
// cust_name: (config.data?.cust_name === 0 ? '0' : config.data?.cust_name) || userInfo?.value?.cust_name || '',
// oper_code: (config.data?.cust_id === 0 ? '0' : '') || userInfo?.value?.cust_id || '',
// oper_name: (config.data?.cust_name === 0 ? '0' : config.data?.cust_name) || userInfo?.value?.cust_name || '',
// };
// const localData = localStorage.getItem('publicData') || '';
// const publicData = localData ? JSON.parse(localData) : {};
// if (['17', '3310052', '310111', '310122'].includes(cmdId)) {
// if (['4', '402', '401'].includes(cmdId)) {
// publicParams.tid = publicParams?.tid.toString();
// publicData.client_type = publicData?.client_type.toString();
// publicData.oper_class_type = publicData?.oper_class_type.toString();
// publicData.oper_source_type = publicData?.oper_source_type.toString();
// publicData.mark = publicData?.mark.toString();
// // publicData.ori_value = "";
// // publicData.new_value = "";
// // publicData.trans_type = "101";
// // publicData.busi_type = "206";
// // publicData.oper_method = "11";
// }
//
// Object.assign(config.data, publicParams, publicData);
// } else if (['310400'].includes(cmdId)) {
// console.log('publicParams', publicParams, publicData);
// config.data.memberOperInfoVo = {
// ...publicParams,
// ...publicData,
// ori_value: '',
// new_value: '',
// oper_method: 1,
// trans_type: 105,
// busi_type: 222,
// };
// } else if (['310635', '4', '402', '401'].includes(cmdId)) {
// config.data.trans_type = '101';
// config.data.client_type = publicData.client_type;
// config.data.browser_brand = publicData.browser_brand;
// config.data.os_name = publicData.os_name;
// config.data.os_version = publicData.os_version;
// config.data.device_platform = publicData.device_platform;
// config.data.device_model = publicData.device_model;
// config.data.device_number = publicData.device_number;
// config.data.device_fingerprint = publicData.device_fingerprint;
// }
//
// console.log('config.data', config.data);
// }
const sendStr = enD(rk, formatSendData(config.data, toNumber(paramType)));
const sendDateStr = enD(rk, t.toString());
const checkOr = enC(rk, vk, sendDateStr);
headers.cmdId = cmdId;
headers.aseqId = appConfig.app.aseqId;
headers.nc = appConfig.app.nc;
headers.tid = tid ?? tenantInfo.tid ?? '';
// 试玩游戏cust_id=0 header需要保持一致
headers.custId = (config.data?.cust_id === 0 ? '0' : '') || userInfo?.cust_id || '';
headers.reqId = uuid(32, 16);
// headers.isMobileOpen = isMobile() ? '1' : '0';
headers.isMobileOpen = '1';
// headers.languageNum = langToNum(language.value);
headers.languageNum = 0;
headers.project = 'tiyu-app';
headers.platform = getBetPlatform(); // 哪端
headers.checkOr = checkOr;
headers.pwds = pwds;
headers.datetime = t;
headers.tbc = md5(t + 'fT6phq0wkOPRlAoyToidAnkogUV7tBBD');
headers.reqKey = getRequestKey(cmdId, config.data); // 每一个接口请求的唯一key,前端用
if (toNumber(headerType) === 1) {
const grantType = 'password';
const scope = 'read write';
const signature = `react_clientgrant_type=${grantType}scope=${scope}cmd_id=${cmdId}react`;
headers.signature = md5(signature);
} else if (toNumber(headerType) === 2) {
const authorization = `Bearer ${userInfo?.access_token || ''}`;
const signature = `react_clientauthorization=${authorization}cmd_id=${cmdId}react`;
headers.authorization = authorization;
headers.signature = md5(signature);
} else if (toNumber(headerType) === 3) {
const grantType = 'refresh_token';
const scope = '';
const signature = `react_clientgrant_type=${grantType}scope=${scope}cmd_id=${cmdId}react`;
headers.signature = md5(signature);
}
// console.log(headers, cmdId, '<------ request headers');
// localStorage.getItem('SHOW_DATA') &&
// console.error(cmdId, '最终请求参数', formatSendData(config.data, toNumber(paramType))); // 查看最终请求参数
console.log(cmdId, 'request data --->', config.data);
return {
headers: { ...reset, ...headers },
data: sendStr,
};
};
// 解释响应数据
export const parseResponse = (response: AxiosResponse): Promise<NetworkResponse<any>> => {
try {
const { headers, data } = response;
const reqHeaders = response.config?.headers || {};
// if (reqHeaders.cmdId == 310122) {
// console.log(data, reqHeaders.cmdId, '<<<<<<<<<<<<<<<<<<<<<<<<<< parseResponse data');
// }
if (isString(data) && data.length > 0 && !startsWith(data, '<html>')) {
// 检查 headers 是否存在必要的属性
if (!headers || !headers.pwds || !headers.datetime) {
console.error('Response headers missing required fields:', headers);
throw new NetworkError({
key: '',
tag: 'Network error 0',
info: '响应头缺少必要字段',
origin: '',
cmd: reqHeaders.cmdId,
});
}
const drk = dnP(appConfig.app.vk, headers.pwds).substring(0, 8);
const dm = dnD(drk, data);
const dc = enC(drk, appConfig.app.vk, enD(drk, toString(headers.datetime)));
// if (reqHeaders.cmdId == 310122) {
// console.log(dm, JSON.parse(dm), reqHeaders.cmdId);
// }
// localStorage.getItem('SHOW_DATA') &&
// console.error(reqHeaders.cmdId, dm ? JSON.parse(dm) : dm); // 查看请求返回的数据
if (!dm) {
throw new NetworkError({
key: '',
tag: 'Network error 1',
info: '数据为空',
origin: '',
cmd: reqHeaders.cmdId,
});
}
if (dc !== headers.checkor) {
throw new NetworkError({
key: '',
tag: 'Network error 2',
info: '返回数据异常',
origin: '',
cmd: reqHeaders.cmdId,
});
}
const resData: any = JSON.parse(dm);
console.log(
JSON.parse(JSON.stringify(resData)),
reqHeaders.cmdId,
'<<<<<<<<<<<<<<<<<<<<<<<<<< parseResponse resData'
);
if (resData) {
if (resData.exLevel) {
// 接口请求的错误处理
let err: any;
// 预存奖励领取的时候 返回'0'提示
if ([724209].includes(+reqHeaders.cmdId)) {
throw {
key: '',
tag: 'Network error 3',
info: resData.exMessage,
origin: resData,
cmd: reqHeaders.cmdId,
code: resData.exCode,
};
} else {
err = new NetworkError({
key: '',
tag: 'Network error 3',
info: resData.exMessage,
origin: resData,
cmd: reqHeaders.cmdId,
code: resData.exCode,
});
}
// 版本号不对,强制更新
if (err.code === 1108) {
err.update = true;
} else if ([1200, 1201, 1109, 1112, 1202, 1007].includes(+err.code)) {
// 1200 refresh_token错误
// 1201 access_token错误
// 1112 IP限制
err.logout = true; // 退出登录
err.tag = 'Network error 4';
err.logoutMessage = '返回异常';
}
throw err;
} else if (resData.error) {
throw new NetworkError({
key: '',
tag: 'Network error 5',
info: resData.error_description,
origin: resData,
cmd: reqHeaders.cmdId,
code: resData.exCode,
});
} else if (resData.response_code === '0') {
// 回收第三方金额 没有的的时候 也是返回'0'
// 预存奖励领取的时候 返回'0'提示
if ([3911381, 724209].includes(+reqHeaders.cmdId)) {
resData.response_code = '1';
} else {
throw new NetworkError({
key: '',
tag: 'Network error 8',
info: resData.msg || '操作失败',
origin: resData,
cmd: reqHeaders.cmdId,
code: resData.exCode,
});
}
}
} else {
if (!isNumber(toNumber(resData))) {
throw new NetworkError({
key: '',
tag: 'Network error 1',
info: '数据为空',
origin: '',
cmd: reqHeaders.cmdId,
});
}
}
return Promise.resolve({ type: NetworkTypeEnum.SUCCESS, data: resData });
} else {
localStorage.getItem('SHOW_DATA') && console.error(reqHeaders.cmdId, data); // 查看请求返回的原始数据
throw new NetworkError({
key: '',
tag: 'Network error 7',
info: '请求没有返回任何数据',
// origin: context || response.toString(),
cmd: reqHeaders.cmdId,
code: -1,
});
}
} catch (e) {
// // 404等错误处理
// if (typeof context === 'object') {
// errStr = JSON.stringify(context);
// } else {
// const arr = context.match(/title[\s\S]*?title/g);
// if (arr && arr.length !== 0) {
// errStr = arr[0].replace(/title>/g, '').replace(/<\/title/g, '');
// }
// }
// let logout = false;
// if (errStr.indexOf('invalid_token') !== -1) {
// logout = true;
// }
// // 类似这种错误的时候 跳到登录页 "error":"access_denied" "error":"unauthorized"
// if (errStr.indexOf('access_denied') !== -1 || errStr.indexOf('unauthorized') !== -1) {
// logout = true;
// }
// err = new NetworkError({
// key: key,
// tag: 'Network error 6',
// info: errStr,
// origin: context,
// cmd: reqHeaders.cmdId,
// code: -1,
// logout: logout,
// });
return Promise.reject({ type: NetworkTypeEnum.ERROR, data: e });
}
};

220
utils/sessionStorage.ts

@ -0,0 +1,220 @@
/**
* Session Storage
*
* React Native sessionStorage
*
*
*
* -
* -
* -
* - API localStorage
*/
/**
* Session Storage
*/
export enum SESSION_KEYS {
TEMP_DATA = 'temp_data',
FORM_DRAFT = 'form_draft',
SEARCH_HISTORY = 'search_history',
CURRENT_TAB = 'current_tab',
SCROLL_POSITION = 'scroll_position',
FILTER_STATE = 'filter_state',
}
/**
* Session Storage
*
* 使 Map
*/
class SessionStorage {
private static storage: Map<string, string> = new Map();
/**
*
*/
static setString(key: string, value: string): void {
try {
this.storage.set(key, value);
if (__DEV__) {
console.log(`💾 SessionStorage set: ${key}`);
}
} catch (error) {
console.error(`SessionStorage setString error for key "${key}":`, error);
throw error;
}
}
/**
*
*/
static getString(key: string): string | null {
try {
const value = this.storage.get(key) ?? null;
if (__DEV__) {
console.log(`📖 SessionStorage get: ${key}`, value ? '✓' : '✗');
}
return value;
} catch (error) {
console.error(`SessionStorage getString error for key "${key}":`, error);
return null;
}
}
/**
* JSON
*/
static setObject<T>(key: string, value: T): void {
try {
const jsonValue = JSON.stringify(value);
this.storage.set(key, jsonValue);
if (__DEV__) {
console.log(`💾 SessionStorage set object: ${key}`);
}
} catch (error) {
console.error(`SessionStorage setObject error for key "${key}":`, error);
throw error;
}
}
/**
* JSON
*/
static getObject<T>(key: string): T | null {
try {
const jsonValue = this.storage.get(key);
if (jsonValue === undefined) {
return null;
}
const value = JSON.parse(jsonValue) as T;
if (__DEV__) {
console.log(`📖 SessionStorage get object: ${key}`);
}
return value;
} catch (error) {
console.error(`SessionStorage getObject error for key "${key}":`, error);
return null;
}
}
/**
*
*/
static remove(key: string): void {
try {
this.storage.delete(key);
if (__DEV__) {
console.log(`🗑 SessionStorage remove: ${key}`);
}
} catch (error) {
console.error(`SessionStorage remove error for key "${key}":`, error);
throw error;
}
}
/**
*
*/
static clear(): void {
try {
this.storage.clear();
if (__DEV__) {
console.log('🗑 SessionStorage cleared all');
}
} catch (error) {
console.error('SessionStorage clear error:', error);
throw error;
}
}
/**
*
*/
static getAllKeys(): string[] {
try {
const keys = Array.from(this.storage.keys());
if (__DEV__) {
console.log('🔑 SessionStorage all keys:', keys);
}
return keys;
} catch (error) {
console.error('SessionStorage getAllKeys error:', error);
return [];
}
}
/**
*
*/
static get length(): number {
return this.storage.size;
}
/**
*
*/
static has(key: string): boolean {
return this.storage.has(key);
}
/**
*
*/
static multiGet(keys: string[]): [string, string | null][] {
try {
return keys.map((key) => [key, this.storage.get(key) ?? null]);
} catch (error) {
console.error('SessionStorage multiGet error:', error);
return [];
}
}
/**
*
*/
static multiSet(keyValuePairs: [string, string][]): void {
try {
keyValuePairs.forEach(([key, value]) => {
this.storage.set(key, value);
});
if (__DEV__) {
console.log(`💾 SessionStorage multiSet: ${keyValuePairs.length} items`);
}
} catch (error) {
console.error('SessionStorage multiSet error:', error);
throw error;
}
}
/**
*
*/
static multiRemove(keys: string[]): void {
try {
keys.forEach((key) => {
this.storage.delete(key);
});
if (__DEV__) {
console.log(`🗑 SessionStorage multiRemove: ${keys.length} items`);
}
} catch (error) {
console.error('SessionStorage multiRemove error:', error);
throw error;
}
}
/**
*
*/
static getAll(): Record<string, string> {
const result: Record<string, string> = {};
this.storage.forEach((value, key) => {
result[key] = value;
});
return result;
}
}
export default SessionStorage;

19
src/utils/storage.ts → utils/storage.ts

@ -8,13 +8,17 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
/** /**
* *
*/ */
export const STORAGE_KEYS = { export enum STORAGE_KEYS {
AUTH_TOKEN: 'auth_token', AUTH_TOKEN = 'auth_token',
USER_INFO: 'user_info', USER_INFO = 'user_info',
SETTINGS: 'settings', SETTINGS = 'settings',
THEME: 'theme', THEME = 'theme',
LANGUAGE: 'language', LANGUAGE = 'language',
} as const; USER_PREFERENCES = 'user_preferences',
TENANT_STORE = 'tenant_storage',
USER_STORE = 'user_storage',
SETTINGS_STORE = 'settings_storage',
}
/** /**
* Storage * Storage
@ -178,4 +182,3 @@ class Storage {
} }
export default Storage; export default Storage;

242
utils/storageManager.ts

@ -0,0 +1,242 @@
/**
*
*
* 使 localStorage (AsyncStorage) sessionStorage
*
* 使
* - localStorage: 持久化数据
* - sessionStorage: 临时数据
*
*
* ```typescript
* // 使用 localStorage(默认)
* await StorageManager.set('user', userData);
*
* // 使用 sessionStorage
* await StorageManager.set('temp', tempData, { type: 'session' });
*
* // 获取数据(自动从正确的存储中读取)
* const user = await StorageManager.get('user');
* const temp = await StorageManager.get('temp', { type: 'session' });
* ```
*/
import Storage from './storage';
import SessionStorage from './sessionStorage';
/**
*
*/
export type StorageType = 'local' | 'session';
/**
*
*/
export interface StorageOptions {
/**
*
* - 'local': AsyncStorage
* - 'session':
*/
type?: StorageType;
}
/**
*
*/
class StorageManager {
/**
*
*/
static async setString(
key: string,
value: string,
options: StorageOptions = {}
): Promise<void> {
const { type = 'local' } = options;
if (type === 'session') {
SessionStorage.setString(key, value);
} else {
await Storage.setString(key, value);
}
}
/**
*
*/
static async getString(
key: string,
options: StorageOptions = {}
): Promise<string | null> {
const { type = 'local' } = options;
if (type === 'session') {
return SessionStorage.getString(key);
} else {
return await Storage.getString(key);
}
}
/**
*
*/
static async setObject<T>(
key: string,
value: T,
options: StorageOptions = {}
): Promise<void> {
const { type = 'local' } = options;
if (type === 'session') {
SessionStorage.setObject(key, value);
} else {
await Storage.setObject(key, value);
}
}
/**
*
*/
static async getObject<T>(
key: string,
options: StorageOptions = {}
): Promise<T | null> {
const { type = 'local' } = options;
if (type === 'session') {
return SessionStorage.getObject<T>(key);
} else {
return await Storage.getObject<T>(key);
}
}
/**
*
*/
static async remove(key: string, options: StorageOptions = {}): Promise<void> {
const { type = 'local' } = options;
if (type === 'session') {
SessionStorage.remove(key);
} else {
await Storage.remove(key);
}
}
/**
*
*/
static async clear(options: StorageOptions = {}): Promise<void> {
const { type = 'local' } = options;
if (type === 'session') {
SessionStorage.clear();
} else {
await Storage.clear();
}
}
/**
*
*/
static async getAllKeys(options: StorageOptions = {}): Promise<string[]> {
const { type = 'local' } = options;
if (type === 'session') {
return SessionStorage.getAllKeys();
} else {
return await Storage.getAllKeys();
}
}
/**
*
*/
static async has(key: string, options: StorageOptions = {}): Promise<boolean> {
const { type = 'local' } = options;
if (type === 'session') {
return SessionStorage.has(key);
} else {
const value = await Storage.getString(key);
return value !== null;
}
}
/**
*
*/
static async multiGet(
keys: string[],
options: StorageOptions = {}
): Promise<[string, string | null][]> {
const { type = 'local' } = options;
if (type === 'session') {
return SessionStorage.multiGet(keys);
} else {
return await Storage.multiGet(keys);
}
}
/**
*
*/
static async multiSet(
keyValuePairs: [string, string][],
options: StorageOptions = {}
): Promise<void> {
const { type = 'local' } = options;
if (type === 'session') {
SessionStorage.multiSet(keyValuePairs);
} else {
await Storage.multiSet(keyValuePairs);
}
}
/**
*
*/
static async multiRemove(
keys: string[],
options: StorageOptions = {}
): Promise<void> {
const { type = 'local' } = options;
if (type === 'session') {
SessionStorage.multiRemove(keys);
} else {
await Storage.multiRemove(keys);
}
}
/**
* session storage
*/
static getSize(options: StorageOptions = {}): number {
const { type = 'local' } = options;
if (type === 'session') {
return SessionStorage.length;
} else {
// AsyncStorage 不支持直接获取大小
return -1;
}
}
/**
* local + session
*/
static async clearAll(): Promise<void> {
await Storage.clear();
SessionStorage.clear();
if (__DEV__) {
console.log('🗑 All storage cleared (local + session)');
}
}
}
export default StorageManager;
Loading…
Cancel
Save