Browse Source

feat: update

master
echo 1 month ago
parent
commit
855f289579
  1. 5
      .env.development
  2. 232
      README.md
  3. 3
      app/(tabs)/_layout.tsx
  4. 224
      app/(tabs)/demo.tsx
  5. 31
      app/_layout.tsx
  6. 119
      app/test-page.tsx
  7. 259
      app/theme-example.tsx
  8. 317
      app/theme-test.tsx
  9. 232
      components/ThemeDemo.tsx
  10. 90
      components/Themed.tsx
  11. 16
      components/index.ts
  12. 4
      components/useClientOnlyValue.ts
  13. 12
      components/useClientOnlyValue.web.ts
  14. 1
      components/useColorScheme.ts
  15. 8
      components/useColorScheme.web.ts
  16. 109
      constants/Colors.ts
  17. 26
      hooks/index.ts
  18. 23
      hooks/useClientOnlyValue.ts
  19. 34
      hooks/useClientOnlyValue.web.ts
  20. 0
      hooks/useDebounce.ts
  21. 2
      hooks/useHaptics.ts
  22. 2
      hooks/useRequest.ts
  23. 100
      hooks/useTheme.ts
  24. 0
      hooks/useThrottle.ts
  25. 1
      package.json
  26. 12
      pnpm-lock.yaml
  27. 0
      schemas/auth.ts
  28. 36
      schemas/index.ts
  29. 0
      schemas/user.ts
  30. 36
      screens/index.ts
  31. 106
      scripts/proxy-server.js
  32. 6
      services/authService.ts
  33. 8
      services/index.ts
  34. 43
      services/tenantService.ts
  35. 4
      services/userService.ts
  36. 39
      src/index.ts
  37. 39
      src/services/appService.ts
  38. 144
      src/stores/settingsStore.ts
  39. 141
      src/stores/userStore.ts
  40. 36
      stores/index.ts
  41. 167
      stores/settingsStore.ts
  42. 122
      stores/tenantStore.ts
  43. 161
      stores/userStore.ts
  44. 36
      theme/index.ts
  45. 261
      theme/styles.ts
  46. 122
      theme/utils.ts
  47. 0
      types/api.ts
  48. 0
      types/index.ts
  49. 0
      utils/common.ts
  50. 8
      utils/config.ts
  51. 0
      utils/date.ts
  52. 38
      utils/index.ts
  53. 28
      utils/network/api.ts
  54. 0
      utils/network/des.ts
  55. 0
      utils/network/error.ts
  56. 49
      utils/network/helper.ts
  57. 220
      utils/sessionStorage.ts
  58. 18
      utils/storage.ts
  59. 242
      utils/storageManager.ts

5
.env.development

@ -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

232
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/ # 工具函数
│ │ ├── 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 # 统一导出 ⭐
├── 💻 业务代码目录
├── utils/ # 工具函数
│ ├── network/ # 网络相关
│ │ ├── api.ts # Axios API 配置
│ │ ├── 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 使用 🎯
---

3
app/(tabs)/_layout.tsx

@ -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: {

224
app/(tabs)/demo.tsx

@ -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 {
// 工具函数
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';
// 验证规则
import { loginSchema } from '@/schemas';
import type { LoginFormData } from '@/schemas';
// 验证规则
loginSchema,
type LoginFormData,
// API 服务
import { authService } from '@/services';
// API 服务
authService,
appService,
// 自定义 Hooks
import { useDebounce, useThrottle, useHaptics } from '@/hooks';
// 自定义 Hooks
useDebounce,
useThrottle,
useHaptics,
} from '@/src';
// 主题组件
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,
},

31
app/_layout.tsx

@ -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

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

259
app/theme-example.tsx

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

317
app/theme-test.tsx

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

232
components/ThemeDemo.tsx

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

90
components/Themed.tsx

@ -1,12 +1,14 @@
/**
* Learn more about Light and Dark modes:
* https://docs.expo.io/guides/color-schemes/
*
*
* Text View
* settingsStore
*/
import { Text as DefaultText, View as DefaultView } from 'react-native';
import { Text as DefaultText, View as DefaultView, TextStyle } from 'react-native';
import Colors from '@/constants/Colors';
import { 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

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

4
components/useClientOnlyValue.ts

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

12
components/useClientOnlyValue.web.ts

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

1
components/useColorScheme.ts

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

8
components/useColorScheme.web.ts

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

109
constants/Colors.ts

@ -1,19 +1,112 @@
const tintColorLight = '#2f95dc';
const tintColorDark = '#fff';
/**
*
*
* light dark
* settingsStore
*/
const tintColorLight = '#007AFF';
const tintColorDark = '#0A84FF';
export default {
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

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

23
hooks/useClientOnlyValue.ts

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

34
hooks/useClientOnlyValue.web.ts

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

0
src/hooks/useDebounce.ts → hooks/useDebounce.ts

2
src/hooks/useHaptics.ts → hooks/useHaptics.ts

@ -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

2
src/hooks/useRequest.ts → hooks/useRequest.ts

@ -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

@ -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]);
}

0
src/hooks/useThrottle.ts → hooks/useThrottle.ts

1
package.json

@ -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

@ -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:
[email protected]:
resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==}
[email protected]:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
[email protected]:
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
@ -5656,6 +5663,11 @@ snapshots:
dependencies:
browserslist: 4.27.0
[email protected]:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
[email protected]:
dependencies:
node-fetch: 2.7.0

0
src/schemas/auth.ts → schemas/auth.ts

36
schemas/index.ts

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

0
src/schemas/user.ts → schemas/user.ts

36
screens/index.ts

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

106
scripts/proxy-server.js

@ -5,37 +5,89 @@
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';
// 配置代理
app.use(
'/api',
createProxyMiddleware({
target: API_TARGET,
changeOrigin: true,
pathRewrite: {
'^/api': '/api', // 保持路径不变,或者根据需要重写
},
onProxyReq: (proxyReq, req, res) => {
console.log(`[Proxy] ${req.method} ${req.url}${API_TARGET}${req.url}`);
},
onProxyRes: (proxyRes, req, res) => {
console.log(`[Proxy] ${req.method} ${req.url}${proxyRes.statusCode}`);
},
onError: (err, req, res) => {
console.error('[Proxy Error]', err.message);
res.status(500).json({
error: 'Proxy Error',
message: err.message,
});
},
})
);
const API_TARGET = process.env.API_TARGET || 'https://51zhh5.notbug.org';
// 代理路径列表
const PROXY_PATHS = ['/api/v1', '/api/v2', '/api/v3'];
// 为每个路径配置代理
PROXY_PATHS.forEach((path) => {
app.use(
path,
createProxyMiddleware({
target: API_TARGET,
changeOrigin: true,
secure: false, // 如果目标服务器使用自签名证书,设置为 false
pathRewrite: (pathStr, req) => {
// Express 会自动去掉匹配的前缀,所以需要加回来
const fullPath = path + pathStr;
console.log(`[Proxy] Path rewrite: ${pathStr}${fullPath}`);
return fullPath;
},
onProxyReq: (proxyReq, req, res) => {
const fullPath = path + req.url;
console.log(`[Proxy] ${req.method} ${fullPath}${API_TARGET}${fullPath}`);
},
onProxyRes: (proxyRes, req, res) => {
const fullPath = path + req.url;
console.log(`[Proxy] ${req.method} ${fullPath}${proxyRes.statusCode}`);
// 确保 CORS 头被正确设置
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, cmdId, datetime, pwds, aseqId, nc, apiName, tid, custId, reqId, isMobileOpen, languageNum, project, platform, checkOr, tbc, reqKey, signature, authorization');
res.setHeader('Access-Control-Expose-Headers', 'cmdId, datetime, pwds, aseqId, nc, checkOr, checkor');
res.setHeader('Access-Control-Allow-Credentials', 'true');
},
onError: (err, req, res) => {
console.error('[Proxy Error]', err.message);
res.status(500).json({
error: 'Proxy Error',
message: err.message,
});
},
})
);
});
// 健康检查
app.get('/health', (req, res) => {

6
src/services/authService.ts → services/authService.ts

@ -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

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

43
services/tenantService.ts

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

4
src/services/userService.ts → services/userService.ts

@ -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

@ -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';

39
src/services/appService.ts

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

144
src/stores/settingsStore.ts

@ -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,
}));

141
src/stores/userStore.ts

@ -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

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

167
stores/settingsStore.ts

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

122
stores/tenantStore.ts

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

161
stores/userStore.ts

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

36
theme/index.ts

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

261
theme/styles.ts

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

122
theme/utils.ts

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

0
src/types/api.ts → types/api.ts

0
src/types/index.ts → types/index.ts

0
src/utils/common.ts → utils/common.ts

8
src/utils/config.ts → utils/config.ts

@ -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':

0
src/utils/date.ts → utils/date.ts

38
utils/index.ts

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

28
src/utils/network/api.ts → utils/network/api.ts

@ -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) => {

0
src/utils/network/des.ts → utils/network/des.ts

0
src/utils/network/error.ts → utils/network/error.ts

49
src/utils/network/helper.ts → utils/network/helper.ts

@ -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

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

18
src/utils/storage.ts → utils/storage.ts

@ -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

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