feat: update
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
# 开发环境配置
|
||||
EXPO_PUBLIC_API_URL=/
|
||||
# EXPO_PUBLIC_API_URL=/api # 注释掉,让 config.ts 根据平台自动选择
|
||||
EXPO_PUBLIC_API_TIMEOUT=10000
|
||||
|
||||
# 测试环境的域名
|
||||
API_TARGET=https://51zhh5.notbug.org
|
||||
|
||||
|
||||
230
README.md
230
README.md
@@ -20,15 +20,28 @@
|
||||
|
||||
## ✨ 项目特性
|
||||
|
||||
### 核心特性
|
||||
|
||||
- 🎯 **Expo Router** - 文件路由系统,类似 Next.js,支持类型安全的导航
|
||||
- 📘 **TypeScript** - 完整的类型支持,提升代码质量
|
||||
- 🔥 **EAS Update** - 热更新支持(CodePush 的官方替代方案)
|
||||
- ⚡ **React Native 新架构** - 启用 Fabric 渲染器和 TurboModules
|
||||
- 📦 **pnpm** - 快速、节省磁盘空间的包管理器
|
||||
- 🧭 **标签导航** - 开箱即用的导航示例
|
||||
- 🎨 **主题支持** - 内置深色/浅色主题切换
|
||||
- 🎨 **完整主题系统** - 深色/浅色/自动主题切换,完整的颜色配置 🎯
|
||||
- 📱 **跨平台** - 支持 iOS、Android 和 Web
|
||||
|
||||
### 架构特性 🎯
|
||||
|
||||
- 📁 **扁平化目录结构** - 清晰的模块组织,符合社区最佳实践
|
||||
- 🔄 **模块化导出** - 每个目录独立导出,避免循环依赖
|
||||
- 💾 **双存储系统** - AsyncStorage(持久化)+ SessionStorage(临时)
|
||||
- 🎨 **主题化组件** - ThemedText 和 ThemedView,自动适配主题
|
||||
- 🔐 **API 加密** - 请求/响应自动加密解密
|
||||
- 📊 **状态管理** - Zustand + AsyncStorage 持久化
|
||||
- ✅ **数据验证** - Zod 表单验证
|
||||
- 🎯 **类型安全** - 完整的 TypeScript 类型定义
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前置要求
|
||||
@@ -94,53 +107,100 @@ rn-demo/
|
||||
│ ├── (tabs)/ # 标签导航组
|
||||
│ │ ├── index.tsx # 首页 - 热更新演示 ⭐
|
||||
│ │ ├── two.tsx # 第二个标签页
|
||||
│ │ ├── demo.tsx # 完整示例页面
|
||||
│ │ ├── demo.tsx # 完整示例页面 🎯
|
||||
│ │ ├── paper.tsx # React Native Paper 示例
|
||||
│ │ └── _layout.tsx # 标签布局
|
||||
│ ├── test-page.tsx # 测试页面(无 tabs)🎯
|
||||
│ ├── _layout.tsx # 根布局 - 自动检查更新 ⭐
|
||||
│ ├── modal.tsx # 模态页面示例
|
||||
│ ├── +html.tsx # Web HTML 模板
|
||||
│ └── +not-found.tsx # 404 页面
|
||||
│
|
||||
├── 💻 src/ # 源代码目录
|
||||
│ ├── utils/ # 工具函数
|
||||
├── 💻 业务代码目录
|
||||
├── utils/ # 工具函数
|
||||
│ ├── network/ # 网络相关
|
||||
│ │ ├── api.ts # Axios API 配置
|
||||
│ │ ├── storage.ts # AsyncStorage 封装
|
||||
│ │ └── date.ts # Day.js 日期工具
|
||||
│ ├── stores/ # Zustand 状态管理
|
||||
│ │ ├── userStore.ts # 用户状态
|
||||
│ │ └── settingsStore.ts # 应用设置
|
||||
│ ├── schemas/ # Zod 验证规则
|
||||
│ │ ├── auth.ts # 认证验证
|
||||
│ │ └── user.ts # 用户验证
|
||||
│ ├── services/ # API 服务层
|
||||
│ │ ├── authService.ts # 认证服务
|
||||
│ │ └── userService.ts # 用户服务
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ │ ├── useDebounce.ts # 防抖 Hook
|
||||
│ │ ├── useThrottle.ts # 节流 Hook
|
||||
│ │ └── useHaptics.ts # 触觉反馈 Hook
|
||||
│ ├── types/ # TypeScript 类型
|
||||
│ │ └── index.ts # 全局类型定义
|
||||
│ └── index.ts # 统一导出 ⭐
|
||||
│ │ ├── helper.ts # 加密/解密工具
|
||||
│ │ └── error.ts # 错误处理
|
||||
│ ├── storage.ts # AsyncStorage 封装
|
||||
│ ├── sessionStorage.ts # Session Storage 实现 🎯
|
||||
│ ├── storageManager.ts # 统一存储管理器 🎯
|
||||
│ ├── config.ts # 配置管理
|
||||
│ ├── date.ts # Day.js 日期工具
|
||||
│ ├── common.ts # 通用工具
|
||||
│ └── index.ts # 统一导出
|
||||
│
|
||||
├── stores/ # Zustand 状态管理
|
||||
│ ├── userStore.ts # 用户状态
|
||||
│ ├── settingsStore.ts # 应用设置
|
||||
│ ├── tenantStore.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/ # 可复用组件
|
||||
│ ├── Themed.tsx # 主题化组件
|
||||
│ ├── Themed.tsx # 主题化组件 🎯
|
||||
│ ├── ThemeDemo.tsx # 主题演示组件 🎯
|
||||
│ ├── ExternalLink.tsx # 外部链接组件
|
||||
│ └── useColorScheme.ts # 主题 Hook
|
||||
│ └── index.ts # 统一导出 🎯
|
||||
│
|
||||
├── 🎨 theme/ # 主题系统 🎯 NEW!
|
||||
│ ├── index.ts # 统一导出
|
||||
│ ├── utils.ts # 主题工具函数
|
||||
│ └── styles.ts # 样式工厂(类似 CSS 类名)
|
||||
│
|
||||
├── 🎯 constants/ # 常量配置
|
||||
│ └── Colors.ts # 颜色主题
|
||||
│ ├── Colors.ts # 颜色主题(完整配置)🎯
|
||||
│ └── network.ts # 网络常量
|
||||
│
|
||||
├── 🎨 assets/ # 静态资源
|
||||
│ ├── images/ # 图片资源
|
||||
│ └── fonts/ # 字体文件
|
||||
│
|
||||
├── 📚 docs/ # 项目文档
|
||||
├── 📚 docs/ # 项目文档 🎯
|
||||
│ ├── 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 配置 ⭐
|
||||
│ ├── eas.json # EAS 构建和更新配置 ⭐
|
||||
│ ├── package.json # 项目依赖
|
||||
@@ -153,7 +213,7 @@ rn-demo/
|
||||
└── CHANGELOG.md # 更新日志
|
||||
|
||||
⭐ = 热更新相关的关键文件
|
||||
🎯 = 新增的文件/目录
|
||||
🎯 = 新增/更新的文件/目录
|
||||
```
|
||||
|
||||
### 关键目录说明
|
||||
@@ -163,18 +223,53 @@ rn-demo/
|
||||
- **`app/_layout.tsx`** - 应用启动时自动检查更新
|
||||
- **`app/(tabs)/index.tsx`** - 热更新演示页面
|
||||
- **`app/(tabs)/demo.tsx`** - 完整示例页面,展示所有工具的使用 🎯
|
||||
- **`app/test-page.tsx`** - 测试页面示例(不包含底部 tabs)🎯
|
||||
|
||||
#### 💻 `src/` - 源代码目录 🎯
|
||||
#### 💻 业务代码目录(扁平化结构)🎯
|
||||
|
||||
项目的核心业务逻辑代码,包含:
|
||||
项目采用扁平化目录结构,所有业务模块都在根目录下:
|
||||
|
||||
- **`utils/`** - 工具函数
|
||||
- 网络请求(Axios + 加密)
|
||||
- 存储管理(AsyncStorage + SessionStorage)🎯
|
||||
- 日期处理(Day.js)
|
||||
- 配置管理
|
||||
|
||||
- **`stores/`** - 状态管理(Zustand)
|
||||
- 用户状态(登录、用户信息)
|
||||
- 应用设置(主题、语言、触觉反馈)
|
||||
- 租户状态 🎯
|
||||
|
||||
- **`schemas/`** - 数据验证(Zod)
|
||||
- 认证表单验证
|
||||
- 用户数据验证
|
||||
|
||||
- **`utils/`** - 工具函数(API、存储、日期)
|
||||
- **`stores/`** - 状态管理(用户、设置)
|
||||
- **`schemas/`** - 数据验证(认证、用户)
|
||||
- **`services/`** - API 服务层
|
||||
- 认证服务
|
||||
- 用户服务
|
||||
- 租户服务 🎯
|
||||
|
||||
- **`hooks/`** - 自定义 Hooks
|
||||
- 防抖/节流
|
||||
- 触觉反馈
|
||||
- 请求管理 🎯
|
||||
- 主题 Hooks(useColorScheme, useTheme, useClientOnlyValue)🎯
|
||||
|
||||
- **`types/`** - TypeScript 类型定义
|
||||
- **`index.ts`** - 统一导出所有模块
|
||||
|
||||
- **`screens/`** - 业务页面组件 🎯
|
||||
|
||||
- **`theme/`** - 主题系统 🎯 NEW!
|
||||
- 统一的主题配置导出
|
||||
- 主题工具函数
|
||||
- 样式工厂(类似 CSS 类名)
|
||||
|
||||
每个目录都有 `index.ts` 文件,负责统一导出该目录下的所有模块。
|
||||
|
||||
#### 🧩 `components/` - 可复用组件
|
||||
|
||||
- **`Themed.tsx`** - 主题化组件(ThemedText, ThemedView)🎯
|
||||
- **`ThemeDemo.tsx`** - 主题演示组件 🎯
|
||||
|
||||
#### 📚 `docs/` - 项目文档 🎯
|
||||
|
||||
@@ -183,6 +278,9 @@ rn-demo/
|
||||
- **使用指南** - 如何使用各个工具
|
||||
- **代码示例** - 实际的代码示例
|
||||
- **配置说明** - 项目配置详解
|
||||
- **架构设计** - Stores 架构、存储系统等
|
||||
- **主题系统** - 主题配置和样式使用指南 🎯 NEW!
|
||||
- **迁移报告** - 扁平化目录结构迁移
|
||||
|
||||
### 核心文件说明
|
||||
|
||||
@@ -195,43 +293,54 @@ rn-demo/
|
||||
|
||||
#### 工具配置 🎯
|
||||
|
||||
- **`src/index.ts`** - 统一导出,从这里导入所有工具
|
||||
- **`.env.example`** - 环境变量配置示例
|
||||
- **各目录的 `index.ts`** - 统一导出,从这里导入所有工具
|
||||
- **`.env.development` / `.env.production`** - 环境变量配置 🎯
|
||||
- **`tsconfig.json`** - 配置了路径别名 `@/*`
|
||||
|
||||
### 已安装的工具库
|
||||
|
||||
项目已安装并配置好以下工具库:
|
||||
|
||||
| 类别 | 工具库 | 用途 |
|
||||
| ------------ | ----------------------------------------- | ------------------- |
|
||||
| **工具类** | lodash-es | JavaScript 工具函数 |
|
||||
| | dayjs | 日期处理 |
|
||||
| | axios | HTTP 请求 |
|
||||
| **状态管理** | zustand | 轻量级状态管理 |
|
||||
| **表单处理** | react-hook-form | 表单管理 |
|
||||
| | zod | 数据验证 |
|
||||
| **原生功能** | @react-native-async-storage/async-storage | 本地存储 |
|
||||
| | expo-image | 优化的图片组件 |
|
||||
| | expo-haptics | 触觉反馈 |
|
||||
| 类别 | 工具库 | 用途 | 状态 |
|
||||
| ------------ | ----------------------------------------- | ------------------- | ---- |
|
||||
| **工具类** | lodash-es | JavaScript 工具函数 | ✅ |
|
||||
| | dayjs | 日期处理 | ✅ |
|
||||
| | axios | HTTP 请求 | ✅ |
|
||||
| **状态管理** | zustand | 轻量级状态管理 | ✅ |
|
||||
| **表单处理** | react-hook-form | 表单管理 | ✅ |
|
||||
| | zod | 数据验证 | ✅ |
|
||||
| **原生功能** | @react-native-async-storage/async-storage | 本地存储 | ✅ |
|
||||
| | expo-image | 优化的图片组件 | ✅ |
|
||||
| | expo-haptics | 触觉反馈 | ✅ |
|
||||
| **UI 组件** | react-native-paper | Material Design 组件 | ✅ |
|
||||
|
||||
### 快速开始
|
||||
|
||||
1. **查看完整示例** - 运行应用,点击 "完整示例" tab 🎯
|
||||
1. **查看完整示例** - 运行应用,点击 "Demo" tab 🎯
|
||||
2. **阅读文档** - 查看 [docs/](./docs/) 目录中的文档
|
||||
3. **使用工具** - 从 `@/src` 导入所需的工具
|
||||
3. **使用工具** - 从各个模块目录导入所需的工具
|
||||
|
||||
```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 {
|
||||
api,
|
||||
Storage,
|
||||
formatDate,
|
||||
useUserStore,
|
||||
authService,
|
||||
useDebounce,
|
||||
useHaptics,
|
||||
} from '@/src';
|
||||
useColorScheme,
|
||||
useThemeColors,
|
||||
ThemedText,
|
||||
ThemedView,
|
||||
commonStyles,
|
||||
createThemeStyles,
|
||||
} from '@/theme';
|
||||
|
||||
// ✅ 也可以:直接从具体文件导入
|
||||
import { useUser } from '@/stores/userStore';
|
||||
import { formatDate } from '@/utils/date';
|
||||
```
|
||||
|
||||
## 🔥 热更新使用指南
|
||||
@@ -680,8 +789,11 @@ if (update.isAvailable) {
|
||||
|
||||
更多详细文档请查看 [docs](./docs/) 目录:
|
||||
|
||||
### 使用指南
|
||||
|
||||
- **[使用示例](./docs/USAGE_EXAMPLES.md)** - 实际代码示例
|
||||
- **[工具库使用指南](./docs/LIBRARIES.md)** - 详细的工具库使用方法和示例
|
||||
- **[存储系统使用指南](./docs/STORAGE_GUIDE.md)** - AsyncStorage 和 SessionStorage 使用 🎯
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ import { Link, Tabs } from 'expo-router';
|
||||
import { Pressable } from 'react-native';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
|
||||
import { useColorScheme, useClientOnlyValue } from '@/hooks';
|
||||
|
||||
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||
function TabBarIcon(props: {
|
||||
|
||||
@@ -18,17 +18,23 @@ import {
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Image } from 'expo-image';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
// ✅ 扁平化导入:从根目录的各个模块导入
|
||||
|
||||
// 导入所有工具
|
||||
import {
|
||||
// 工具函数
|
||||
import {
|
||||
Storage,
|
||||
STORAGE_KEYS,
|
||||
SessionStorage,
|
||||
SESSION_KEYS,
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
formatChatTime,
|
||||
formatChatTime
|
||||
} from '@/utils';
|
||||
|
||||
// 状态管理
|
||||
import {
|
||||
useUserStore,
|
||||
useUser,
|
||||
useIsLoggedIn,
|
||||
@@ -36,24 +42,28 @@ import {
|
||||
useTheme,
|
||||
useLanguage,
|
||||
useHapticsEnabled,
|
||||
useSettingsActions,
|
||||
useTenantStates,
|
||||
useTenantInfo,
|
||||
} from '@/stores';
|
||||
|
||||
// 验证规则
|
||||
loginSchema,
|
||||
type LoginFormData,
|
||||
import { loginSchema } from '@/schemas';
|
||||
import type { LoginFormData } from '@/schemas';
|
||||
|
||||
// API 服务
|
||||
authService,
|
||||
appService,
|
||||
import { authService } from '@/services';
|
||||
|
||||
// 自定义 Hooks
|
||||
useDebounce,
|
||||
useThrottle,
|
||||
useHaptics,
|
||||
} from '@/src';
|
||||
import { useDebounce, useThrottle, useHaptics } from '@/hooks';
|
||||
|
||||
// 主题组件
|
||||
import { ThemeDemo } from '@/components/ThemeDemo';
|
||||
|
||||
export default function DemoScreen() {
|
||||
console.log('=== DemoScreen 组件已渲染 ===');
|
||||
const haptics = useHaptics();
|
||||
const router = useRouter();
|
||||
|
||||
// 状态管理示例
|
||||
const user = useUser();
|
||||
@@ -61,13 +71,17 @@ export default function DemoScreen() {
|
||||
const login = useUserStore((state) => state.login);
|
||||
const logout = useUserStore((state) => state.logout);
|
||||
|
||||
const { tenantLoad } = useTenantStates();
|
||||
const tenantInfo = useTenantInfo();
|
||||
|
||||
// 设置状态
|
||||
const theme = useTheme();
|
||||
const language = useLanguage();
|
||||
const hapticsEnabled = useHapticsEnabled();
|
||||
const setTheme = useSettingsStore((state) => state.setTheme);
|
||||
const setLanguage = useSettingsStore((state) => state.setLanguage);
|
||||
const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
|
||||
const { setTheme, setLanguage, setHapticsEnabled } = useSettingsActions();
|
||||
// const setTheme = useSettingsStore((state) => state.setTheme);
|
||||
// const setLanguage = useSettingsStore((state) => state.setLanguage);
|
||||
// const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
|
||||
|
||||
// 本地状态
|
||||
const [searchText, setSearchText] = useState('');
|
||||
@@ -75,6 +89,7 @@ export default function DemoScreen() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [counter, setCounter] = useState(0);
|
||||
const [storageValue, setStorageValue] = useState('');
|
||||
const [sessionValue, setSessionValue] = useState('');
|
||||
|
||||
// 表单配置
|
||||
const {
|
||||
@@ -103,21 +118,6 @@ export default function DemoScreen() {
|
||||
setSearchResults([`结果 1: ${text}`, `结果 2: ${text}`, `结果 3: ${text}`]);
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('=== useEffect 开始执行 ===');
|
||||
console.log('appService:', appService);
|
||||
console.log('getPlatformData 方法:', appService.getPlatformData);
|
||||
|
||||
appService
|
||||
.getPlatformData()
|
||||
.then((res: any) => {
|
||||
console.log('getPlatformData 成功:', res);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error('getPlatformData 失败:', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 监听搜索文本变化
|
||||
useEffect(() => {
|
||||
debouncedSearch(searchText);
|
||||
@@ -216,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 = () => {
|
||||
haptics.selection();
|
||||
@@ -238,6 +291,68 @@ export default function DemoScreen() {
|
||||
<Text style={styles.title}>🎯 完整功能演示</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>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 用户状态显示 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>👤 用户状态 (Zustand)</Text>
|
||||
@@ -373,6 +488,39 @@ export default function DemoScreen() {
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 日期格式化示例 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>📅 日期格式化 (Day.js)</Text>
|
||||
@@ -383,6 +531,12 @@ export default function DemoScreen() {
|
||||
<Text style={styles.infoText}>聊天时间: {formatChatTime(Date.now())}</Text>
|
||||
</View>
|
||||
|
||||
{/* 主题演示 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>🎨 主题系统演示</Text>
|
||||
<ThemeDemo />
|
||||
</View>
|
||||
|
||||
{/* 设置示例 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>⚙️ 应用设置</Text>
|
||||
@@ -598,6 +752,10 @@ const styles = StyleSheet.create({
|
||||
halfButton: {
|
||||
flex: 1,
|
||||
},
|
||||
thirdButton: {
|
||||
flex: 1,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
searchResults: {
|
||||
marginTop: 12,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,10 @@ import { Alert, Platform } from 'react-native';
|
||||
import 'react-native-reanimated';
|
||||
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 {
|
||||
// Catch any errors thrown by the Layout component.
|
||||
@@ -29,6 +32,7 @@ export default function RootLayout() {
|
||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||
...FontAwesome.font,
|
||||
});
|
||||
const { requestTenantInfo } = useTenantActions();
|
||||
|
||||
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
||||
useEffect(() => {
|
||||
@@ -41,6 +45,31 @@ export default function RootLayout() {
|
||||
}
|
||||
}, [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(() => {
|
||||
async function checkForUpdates() {
|
||||
|
||||
119
app/test-page.tsx
Normal file
119
app/test-page.tsx
Normal file
@@ -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
Normal file
259
app/theme-example.tsx
Normal file
@@ -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
Normal file
317
app/theme-test.tsx
Normal file
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
232
components/ThemeDemo.tsx
Normal file
232
components/ThemeDemo.tsx
Normal file
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 { useColorScheme } from './useColorScheme';
|
||||
import { useColorScheme } from '@/hooks/useTheme';
|
||||
|
||||
type ThemeProps = {
|
||||
lightColor?: string;
|
||||
@@ -16,6 +18,13 @@ type ThemeProps = {
|
||||
export type TextProps = ThemeProps & DefaultText['props'];
|
||||
export type ViewProps = ThemeProps & DefaultView['props'];
|
||||
|
||||
/**
|
||||
* 获取主题颜色
|
||||
*
|
||||
* @param props - 包含 light 和 dark 颜色的对象
|
||||
* @param colorName - Colors 中定义的颜色名称
|
||||
* @returns 当前主题对应的颜色值
|
||||
*/
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
@@ -30,6 +39,11 @@ export function useThemeColor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题化 Text 组件
|
||||
*
|
||||
* 自动应用当前主题的文本颜色
|
||||
*/
|
||||
export function Text(props: TextProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
@@ -37,9 +51,77 @@ export function Text(props: TextProps) {
|
||||
return <DefaultText style={[{ color }, style]} {...otherProps} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题化 View 组件
|
||||
*
|
||||
* 自动应用当前主题的背景颜色
|
||||
*/
|
||||
export function View(props: ViewProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||
|
||||
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
Normal file
16
components/index.ts
Normal file
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
export { useColorScheme } from 'react-native';
|
||||
@@ -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';
|
||||
}
|
||||
@@ -1,19 +1,112 @@
|
||||
const tintColorLight = '#2f95dc';
|
||||
const tintColorDark = '#fff';
|
||||
/**
|
||||
* 主题颜色配置
|
||||
*
|
||||
* 支持 light 和 dark 两种主题
|
||||
* 可以通过 settingsStore 切换主题
|
||||
*/
|
||||
|
||||
const tintColorLight = '#007AFF';
|
||||
const tintColorDark = '#0A84FF';
|
||||
|
||||
export default {
|
||||
light: {
|
||||
text: '#000',
|
||||
background: '#fff',
|
||||
// 文本颜色
|
||||
text: '#000000',
|
||||
textSecondary: '#666666',
|
||||
textTertiary: '#999999',
|
||||
textInverse: '#FFFFFF',
|
||||
|
||||
// 背景颜色
|
||||
background: '#FFFFFF',
|
||||
backgroundSecondary: '#F5F5F5',
|
||||
backgroundTertiary: '#E5E5E5',
|
||||
|
||||
// 主题色
|
||||
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,
|
||||
|
||||
// 卡片
|
||||
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: {
|
||||
text: '#fff',
|
||||
background: '#000',
|
||||
// 文本颜色
|
||||
text: '#FFFFFF',
|
||||
textSecondary: '#AEAEB2',
|
||||
textTertiary: '#8E8E93',
|
||||
textInverse: '#000000',
|
||||
|
||||
// 背景颜色
|
||||
background: '#000000',
|
||||
backgroundSecondary: '#1C1C1E',
|
||||
backgroundTertiary: '#2C2C2E',
|
||||
|
||||
// 主题色
|
||||
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,
|
||||
|
||||
// 卡片
|
||||
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)',
|
||||
},
|
||||
};
|
||||
|
||||
26
hooks/index.ts
Normal file
26
hooks/index.ts
Normal file
@@ -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
Normal file
23
hooks/useClientOnlyValue.ts
Normal file
@@ -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
Normal file
34
hooks/useClientOnlyValue.web.ts
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useHapticsEnabled } from '@/src/stores/settingsStore';
|
||||
import { useHapticsEnabled } from '@/stores/settingsStore';
|
||||
|
||||
/**
|
||||
* 触觉反馈 Hook
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { AxiosError } from 'axios';
|
||||
import type { RequestConfig } from '@/src/utils/network/api';
|
||||
import type { RequestConfig } from '@/utils/network/api';
|
||||
|
||||
/**
|
||||
* 请求状态
|
||||
100
hooks/useTheme.ts
Normal file
100
hooks/useTheme.ts
Normal file
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"@types/md5": "^2.3.6",
|
||||
"@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",
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -120,6 +120,9 @@ importers:
|
||||
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
|
||||
@@ -1641,6 +1644,10 @@ packages:
|
||||
core-js-compat@3.46.0:
|
||||
resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==}
|
||||
|
||||
cors@2.8.5:
|
||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
||||
|
||||
@@ -5656,6 +5663,11 @@ snapshots:
|
||||
dependencies:
|
||||
browserslist: 4.27.0
|
||||
|
||||
cors@2.8.5:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
vary: 1.1.2
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
|
||||
36
schemas/index.ts
Normal file
36
schemas/index.ts
Normal file
@@ -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';
|
||||
|
||||
36
screens/index.ts
Normal file
36
screens/index.ts
Normal file
@@ -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 {};
|
||||
|
||||
@@ -5,27 +5,78 @@
|
||||
|
||||
const express = require('express');
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const cors = require('cors');
|
||||
|
||||
const app = express();
|
||||
const PORT = 8080;
|
||||
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 || 'http://localhost:3000';
|
||||
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(
|
||||
'/api',
|
||||
path,
|
||||
createProxyMiddleware({
|
||||
target: API_TARGET,
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
'^/api': '/api', // 保持路径不变,或者根据需要重写
|
||||
secure: false, // 如果目标服务器使用自签名证书,设置为 false
|
||||
pathRewrite: (pathStr, req) => {
|
||||
// Express 会自动去掉匹配的前缀,所以需要加回来
|
||||
const fullPath = path + pathStr;
|
||||
console.log(`[Proxy] Path rewrite: ${pathStr} → ${fullPath}`);
|
||||
return fullPath;
|
||||
},
|
||||
onProxyReq: (proxyReq, req, res) => {
|
||||
console.log(`[Proxy] ${req.method} ${req.url} → ${API_TARGET}${req.url}`);
|
||||
const fullPath = path + req.url;
|
||||
console.log(`[Proxy] ${req.method} ${fullPath} → ${API_TARGET}${fullPath}`);
|
||||
},
|
||||
onProxyRes: (proxyRes, req, res) => {
|
||||
console.log(`[Proxy] ${req.method} ${req.url} ← ${proxyRes.statusCode}`);
|
||||
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);
|
||||
@@ -36,6 +87,7 @@ app.use(
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 处理登录、注册等认证相关的 API 请求
|
||||
*/
|
||||
|
||||
import { request } from '@/src/utils/network/api';
|
||||
import { request } from '@/utils/network/api';
|
||||
import type {
|
||||
LoginFormData,
|
||||
RegisterFormData,
|
||||
@@ -11,8 +11,8 @@ import type {
|
||||
ResetPasswordFormData,
|
||||
ChangePasswordFormData,
|
||||
PhoneLoginFormData,
|
||||
} from '@/src/schemas/auth';
|
||||
import type { User } from '@/src/schemas/user';
|
||||
} from '@/schemas/auth';
|
||||
import type { User } from '@/schemas/user';
|
||||
|
||||
/**
|
||||
* API 响应接口
|
||||
8
services/index.ts
Normal file
8
services/index.ts
Normal file
@@ -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
Normal file
43
services/tenantService.ts
Normal file
@@ -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;
|
||||
@@ -3,8 +3,8 @@
|
||||
* 处理用户信息相关的 API 请求
|
||||
*/
|
||||
|
||||
import { request } from '@/src/utils/network/api';
|
||||
import type { User, UpdateProfileFormData } from '@/src/schemas/user';
|
||||
import { request } from '@/utils/network/api';
|
||||
import type { User, UpdateProfileFormData } from '@/schemas/user';
|
||||
|
||||
/**
|
||||
* API 响应接口
|
||||
39
src/index.ts
39
src/index.ts
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* 统一导出所有模块
|
||||
*/
|
||||
|
||||
// Utils
|
||||
export {
|
||||
default as api,
|
||||
request,
|
||||
cancelAllRequests,
|
||||
cancelRequest,
|
||||
createRetryRequest,
|
||||
} from './utils/network/api';
|
||||
export type { ApiResponse, ApiError, RequestConfig } from './utils/network/api';
|
||||
export { default as Storage, STORAGE_KEYS } from './utils/storage';
|
||||
export { default as config, printConfig } from './utils/config';
|
||||
export * from './utils/date';
|
||||
|
||||
// Stores
|
||||
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';
|
||||
export { default as appService } from './services/appService';
|
||||
|
||||
// Hooks
|
||||
export * from './hooks/useDebounce';
|
||||
export * from './hooks/useThrottle';
|
||||
export * from './hooks/useHaptics';
|
||||
export * from './hooks/useRequest';
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
export * from './types/api';
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* 基础服务
|
||||
* 处理应用相关的 API 请求
|
||||
*/
|
||||
|
||||
import { request } from '@/src/utils/network/api';
|
||||
import type { User, UpdateProfileFormData } from '@/src/schemas/user';
|
||||
|
||||
/**
|
||||
* API 响应接口
|
||||
*/
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户服务类
|
||||
*/
|
||||
class AppService {
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
getPlatformData(data?: Record<string, any>): Promise<any> {
|
||||
return request.post('/v2', data, {
|
||||
headers: {
|
||||
cmdId: 371130,
|
||||
headerType: 1,
|
||||
apiName: 'getPlatformData',
|
||||
tid: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const appService = new AppService();
|
||||
export default appService;
|
||||
@@ -1,144 +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,
|
||||
}));
|
||||
@@ -1,141 +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,
|
||||
}));
|
||||
36
stores/index.ts
Normal file
36
stores/index.ts
Normal file
@@ -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
Normal file
167
stores/settingsStore.ts
Normal file
@@ -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
Normal file
122
stores/tenantStore.ts
Normal file
@@ -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
Normal file
161
stores/userStore.ts
Normal file
@@ -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
Normal file
36
theme/index.ts
Normal file
@@ -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
Normal file
261
theme/styles.ts
Normal file
@@ -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
Normal file
122
theme/utils.ts
Normal file
@@ -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';
|
||||
}
|
||||
|
||||
@@ -45,8 +45,10 @@ export const getApiBaseUrl = (): string => {
|
||||
case 'development':
|
||||
// 开发环境
|
||||
if (Platform.OS === 'web') {
|
||||
// Web 平台使用相对路径(会被 webpack devServer 代理)
|
||||
return '/api';
|
||||
// Web 平台使用代理服务器
|
||||
// 代理服务器运行在 http://localhost:8086
|
||||
// 会将 /api/* 请求转发到目标服务器
|
||||
return 'http://localhost:8086/api';
|
||||
} else {
|
||||
// iOS/Android 使用本机 IP
|
||||
// ⚠️ 重要:需要替换为你的本机 IP 地址
|
||||
@@ -54,7 +56,7 @@ export const getApiBaseUrl = (): string => {
|
||||
// - Windows: ipconfig
|
||||
// - Mac/Linux: ifconfig
|
||||
// - 或者使用 Metro Bundler 显示的 IP
|
||||
return 'http://192.168.1.100:3000/api';
|
||||
return 'http://192.168.1.100:8086/api';
|
||||
}
|
||||
|
||||
case 'staging':
|
||||
38
utils/index.ts
Normal file
38
utils/index.ts
Normal file
@@ -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';
|
||||
|
||||
@@ -192,9 +192,9 @@ api.interceptors.request.use(
|
||||
config.url = '/v2/';
|
||||
}
|
||||
|
||||
if (__DEV__ && apiName) {
|
||||
config.url = `${config.url}?${apiName}`;
|
||||
}
|
||||
// if (__DEV__ && apiName) {
|
||||
// config.url = `${config.url}?${apiName}`;
|
||||
// }
|
||||
|
||||
// // 从本地存储获取 token
|
||||
// const token = await AsyncStorage.getItem('auth_token');
|
||||
@@ -208,16 +208,16 @@ api.interceptors.request.use(
|
||||
(config as any).metadata = { startTime: Date.now() };
|
||||
|
||||
// 打印请求信息(开发环境)
|
||||
// if (__DEV__) {
|
||||
// console.log('📤 API Request:', {
|
||||
// method: config.method?.toUpperCase(),
|
||||
// url: config.url,
|
||||
// baseURL: config.baseURL,
|
||||
// params: config.params,
|
||||
// data: config.data,
|
||||
// headers: config.headers,
|
||||
// });
|
||||
// }
|
||||
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) {
|
||||
@@ -510,7 +510,7 @@ export const request = {
|
||||
onProgress?: (progress: number) => void,
|
||||
config?: RequestConfig
|
||||
) => {
|
||||
const response = await api.get(url, {
|
||||
const response: any = await api.get(url, {
|
||||
...config,
|
||||
responseType: 'blob',
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
@@ -7,7 +7,7 @@ import * as des from './des';
|
||||
import NetworkError from './error';
|
||||
import { toNumber, toString, startsWith, isString, isNumber } from 'lodash-es';
|
||||
import { NetworkTypeEnum } from '@/constants/network';
|
||||
import config from '../config';
|
||||
import appConfig from '../config';
|
||||
|
||||
// import NetworkError from './error'
|
||||
// import { storeToRefs, useTenantStore, useUserStore, useAppStore, start } from '../index';
|
||||
@@ -107,33 +107,33 @@ export const formatSendData = (data: any, type: number = 0) => {
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
|
||||
export const getP = (p) => {
|
||||
export const getP = (p: any) => {
|
||||
return HmacMD5(p, '7NEkojNzfkk=').toString();
|
||||
};
|
||||
|
||||
export const enD = (rk, str) => {
|
||||
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, str) => {
|
||||
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, vk, t) => {
|
||||
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, str) => {
|
||||
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, vk, m) => {
|
||||
export const enC = (rk: string, vk: string, m: string) => {
|
||||
const enc = HmacMD5(m + rk, vk);
|
||||
return Base64.stringify(enc);
|
||||
};
|
||||
@@ -151,15 +151,16 @@ export const transformRequest = (config: any) => {
|
||||
// const { language } = storeToRefs(useAppStore());
|
||||
const t = new Date().getTime();
|
||||
const rk = md5(toString(Math.random() + t)).substring(0, 8);
|
||||
const vk = config.app.vk;
|
||||
const vk = appConfig.app.vk as string;
|
||||
const pwds = enP(rk, vk, t);
|
||||
|
||||
const tenantInfo = {
|
||||
tid: 3,
|
||||
};
|
||||
let userInfo = {
|
||||
cust_id: 1,
|
||||
cust_name: 'test',
|
||||
cust_id: '',
|
||||
cust_name: '',
|
||||
access_token: ''
|
||||
};
|
||||
|
||||
// if (['17', '3310052', '310111', '310122', '310400', '4', '402', '401', '310635'].includes(cmdId)) {
|
||||
@@ -218,8 +219,8 @@ export const transformRequest = (config: any) => {
|
||||
const checkOr = enC(rk, vk, sendDateStr);
|
||||
|
||||
headers.cmdId = cmdId;
|
||||
headers.aseqId = config.app.aseqId;
|
||||
headers.nc = config.app.nc;
|
||||
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 || '';
|
||||
@@ -254,8 +255,8 @@ export const transformRequest = (config: any) => {
|
||||
}
|
||||
|
||||
// console.log(headers, cmdId, '<------ request headers');
|
||||
localStorage.getItem('SHOW_DATA') &&
|
||||
console.error(cmdId, '最终请求参数', formatSendData(config.data, toNumber(paramType))); // 查看最终请求参数
|
||||
// localStorage.getItem('SHOW_DATA') &&
|
||||
// console.error(cmdId, '最终请求参数', formatSendData(config.data, toNumber(paramType))); // 查看最终请求参数
|
||||
console.log(cmdId, 'request data --->', config.data);
|
||||
return {
|
||||
headers: { ...reset, ...headers },
|
||||
@@ -272,14 +273,26 @@ export const parseResponse = (response: AxiosResponse): Promise<NetworkResponse<
|
||||
// console.log(data, reqHeaders.cmdId, '<<<<<<<<<<<<<<<<<<<<<<<<<< parseResponse data');
|
||||
// }
|
||||
if (isString(data) && data.length > 0 && !startsWith(data, '<html>')) {
|
||||
const drk = dnP(config.app.vk, headers.pwds ?? '').substring(0, 8);
|
||||
// 检查 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, config.app.vk, enD(drk, toString(headers.datetime)));
|
||||
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); // 查看请求返回的数据
|
||||
// localStorage.getItem('SHOW_DATA') &&
|
||||
// console.error(reqHeaders.cmdId, dm ? JSON.parse(dm) : dm); // 查看请求返回的数据
|
||||
if (!dm) {
|
||||
throw new NetworkError({
|
||||
key: '',
|
||||
220
utils/sessionStorage.ts
Normal file
220
utils/sessionStorage.ts
Normal file
@@ -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;
|
||||
|
||||
@@ -8,13 +8,17 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
/**
|
||||
* 存储键名常量
|
||||
*/
|
||||
export const STORAGE_KEYS = {
|
||||
AUTH_TOKEN: 'auth_token',
|
||||
USER_INFO: 'user_info',
|
||||
SETTINGS: 'settings',
|
||||
THEME: 'theme',
|
||||
LANGUAGE: 'language',
|
||||
} as const;
|
||||
export enum STORAGE_KEYS {
|
||||
AUTH_TOKEN = 'auth_token',
|
||||
USER_INFO = 'user_info',
|
||||
SETTINGS = 'settings',
|
||||
THEME = 'theme',
|
||||
LANGUAGE = 'language',
|
||||
USER_PREFERENCES = 'user_preferences',
|
||||
TENANT_STORE = 'tenant_storage',
|
||||
USER_STORE = 'user_storage',
|
||||
SETTINGS_STORE = 'settings_storage',
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage 工具类
|
||||
242
utils/storageManager.ts
Normal file
242
utils/storageManager.ts
Normal file
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user