commit 7526a9b8276d1f2b9d50f1cfe084b387be456f75 Author: echo Date: Tue Nov 4 13:27:19 2025 +0800 feat: new project diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c49ff80 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# API 配置 +EXPO_PUBLIC_API_URL=https://api.example.com + +# 应用信息 +EXPO_PUBLIC_APP_NAME=RN Demo +EXPO_PUBLIC_APP_VERSION=1.0.0 + +# 其他配置 +# EXPO_PUBLIC_SENTRY_DSN= +# EXPO_PUBLIC_ANALYTICS_ID= + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53a588c --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ +.pnp +.pnp.js + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* +lerna-debug.log* +.pnpm-debug.log* + +# macOS +.DS_Store +*.pem + +# local env files +.env +.env*.local +.env.local +.env.development.local +.env.test.local +.env.production.local + +# typescript +*.tsbuildinfo +next-env.d.ts + +# testing +/coverage +*.lcov +.nyc_output + +# production +/build + +# misc +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.settings +.classpath + +# logs +logs +*.log + +# OS +Thumbs.db + +# Package manager lock files (keep pnpm-lock.yaml, ignore others) +package-lock.json +yarn.lock + +# EAS +.eas/ + +# generated native folders +/ios +/android diff --git a/README.md b/README.md new file mode 100644 index 0000000..83fb0b6 --- /dev/null +++ b/README.md @@ -0,0 +1,667 @@ +# React Native Expo 热更新项目 + +> 基于 Expo Router 和 EAS Update 的完整 React Native 项目,支持热更新(OTA)功能 + +[![Expo](https://img.shields.io/badge/Expo-~54.0-000020?style=flat&logo=expo)](https://expo.dev/) +[![React Native](https://img.shields.io/badge/React%20Native-0.81-61DAFB?style=flat&logo=react)](https://reactnative.dev/) +[![TypeScript](https://img.shields.io/badge/TypeScript-~5.9-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) +[![pnpm](https://img.shields.io/badge/pnpm-latest-F69220?style=flat&logo=pnpm)](https://pnpm.io/) + +## 📖 目录 + +- [项目特性](#-项目特性) +- [快速开始](#-快速开始) +- [项目结构](#-项目结构) +- [热更新使用指南](#-热更新使用指南) +- [开发指南](#-开发指南) +- [配置说明](#-配置说明) +- [常见问题](#-常见问题) +- [相关资源](#-相关资源) + +## ✨ 项目特性 + +- 🎯 **Expo Router** - 文件路由系统,类似 Next.js,支持类型安全的导航 +- 📘 **TypeScript** - 完整的类型支持,提升代码质量 +- 🔥 **EAS Update** - 热更新支持(CodePush 的官方替代方案) +- ⚡ **React Native 新架构** - 启用 Fabric 渲染器和 TurboModules +- 📦 **pnpm** - 快速、节省磁盘空间的包管理器 +- 🧭 **标签导航** - 开箱即用的导航示例 +- 🎨 **主题支持** - 内置深色/浅色主题切换 +- 📱 **跨平台** - 支持 iOS、Android 和 Web + +## 🚀 快速开始 + +### 前置要求 + +- **Node.js** 18+ ([下载](https://nodejs.org/)) +- **pnpm** ([安装指南](https://pnpm.io/installation)) +- **Expo Go** App(可选,用于快速预览) + - [iOS App Store](https://apps.apple.com/app/expo-go/id982107779) + - [Android Play Store](https://play.google.com/store/apps/details?id=host.exp.exponent) + +### 第一步:安装依赖 + +```bash +pnpm install +``` + +### 第二步:启动开发服务器 + +```bash +pnpm start +``` + +启动后,你会看到一个二维码和几个选项: + +- 按 **`a`** - 在 Android 模拟器/设备上运行 +- 按 **`i`** - 在 iOS 模拟器上运行(仅 macOS) +- 按 **`w`** - 在浏览器中运行 +- **扫描二维码** - 在 Expo Go App 中运行 + +### 第三步:在设备上查看 + +#### 方法 1:使用 Expo Go(推荐用于快速预览) + +1. 在手机上安装 Expo Go +2. 扫描终端中显示的二维码 +3. 应用会自动加载并运行 + +#### 方法 2:使用模拟器 + +**Android 模拟器:** +```bash +pnpm android +``` + +**iOS 模拟器(仅 macOS):** +```bash +pnpm ios +``` + +**Web 浏览器:** +```bash +pnpm web +``` + +## 📁 项目结构 + +``` +rn-demo/ +├── 📱 app/ # Expo Router 路由文件 +│ ├── (tabs)/ # 标签导航组 +│ │ ├── index.tsx # 首页 - 热更新演示 ⭐ +│ │ ├── two.tsx # 第二个标签页 +│ │ ├── demo.tsx # 完整示例页面 +│ │ └── _layout.tsx # 标签布局 +│ ├── _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 # 统一导出 ⭐ +│ +├── 🧩 components/ # 可复用组件 +│ ├── Themed.tsx # 主题化组件 +│ ├── ExternalLink.tsx # 外部链接组件 +│ └── useColorScheme.ts # 主题 Hook +│ +├── 🎯 constants/ # 常量配置 +│ └── Colors.ts # 颜色主题 +│ +├── 🎨 assets/ # 静态资源 +│ ├── images/ # 图片资源 +│ └── fonts/ # 字体文件 +│ +├── 📚 docs/ # 项目文档 +│ ├── USAGE_EXAMPLES.md # 使用示例 +│ └── LIBRARIES.md # 工具库使用指南 +│ +├── ⚙️ 配置文件 +│ ├── .env.example # 环境变量示例 🎯 NEW! +│ ├── app.json # Expo 配置 ⭐ +│ ├── eas.json # EAS 构建和更新配置 ⭐ +│ ├── package.json # 项目依赖 +│ ├── pnpm-lock.yaml # pnpm 锁文件 +│ ├── tsconfig.json # TypeScript 配置 +│ └── .gitignore # Git 忽略文件 +│ +└── 📄 文档文件 + ├── README.md # 项目主文档(本文件) + └── CHANGELOG.md # 更新日志 + +⭐ = 热更新相关的关键文件 +🎯 = 新增的文件/目录 +``` + +### 关键目录说明 + +#### 📱 `app/` - 路由和页面 +- **`app/_layout.tsx`** - 应用启动时自动检查更新 +- **`app/(tabs)/index.tsx`** - 热更新演示页面 +- **`app/(tabs)/demo.tsx`** - 完整示例页面,展示所有工具的使用 🎯 + +#### 💻 `src/` - 源代码目录 🎯 +项目的核心业务逻辑代码,包含: +- **`utils/`** - 工具函数(API、存储、日期) +- **`stores/`** - 状态管理(用户、设置) +- **`schemas/`** - 数据验证(认证、用户) +- **`services/`** - API 服务层 +- **`hooks/`** - 自定义 Hooks +- **`types/`** - TypeScript 类型定义 +- **`index.ts`** - 统一导出所有模块 + +#### 📚 `docs/` - 项目文档 🎯 +完善的项目文档,包含: +- **使用指南** - 如何使用各个工具 +- **代码示例** - 实际的代码示例 +- **配置说明** - 项目配置详解 + +### 核心文件说明 + +#### 热更新相关 ⭐ +- **`app.json`** - Expo 配置,包含热更新设置 +- **`eas.json`** - EAS 构建和更新通道配置 +- **`app/_layout.tsx`** - 自动检查更新逻辑 +- **`app/(tabs)/index.tsx`** - 手动检查更新功能 + +#### 工具配置 🎯 +- **`src/index.ts`** - 统一导出,从这里导入所有工具 +- **`.env.example`** - 环境变量配置示例 +- **`tsconfig.json`** - 配置了路径别名 `@/*` + +### 已安装的工具库 + +项目已安装并配置好以下工具库: + +| 类别 | 工具库 | 用途 | +|------|--------|------| +| **工具类** | lodash-es | JavaScript 工具函数 | +| | dayjs | 日期处理 | +| | axios | HTTP 请求 | +| **状态管理** | zustand | 轻量级状态管理 | +| **表单处理** | react-hook-form | 表单管理 | +| | zod | 数据验证 | +| **原生功能** | @react-native-async-storage/async-storage | 本地存储 | +| | expo-image | 优化的图片组件 | +| | expo-haptics | 触觉反馈 | + +### 快速开始 + +1. **查看完整示例** - 运行应用,点击 "完整示例" tab 🎯 +2. **阅读文档** - 查看 [docs/](./docs/) 目录中的文档 +3. **使用工具** - 从 `@/src` 导入所需的工具 + +```typescript +// 示例:导入工具 +import { + api, + Storage, + formatDate, + useUserStore, + authService, + useDebounce, + useHaptics, +} from '@/src'; +``` + +## 🔥 热更新使用指南 + +> **重要提示**:热更新功能需要在真实构建(Development Build 或 Production Build)中测试,Expo Go 不支持自定义热更新配置。 + +### 步骤 1:登录 EAS + +```bash +eas login +``` + +如果没有账号,访问 [expo.dev](https://expo.dev) 注册一个免费账号。 + +### 步骤 2:初始化 EAS 项目 + +```bash +eas init +``` + +这会: +- 创建一个唯一的项目 ID +- 自动更新 `app.json` 中的 `extra.eas.projectId` + +### 步骤 3:构建开发版本 + +**Android 开发构建:** +```bash +eas build --profile development --platform android +``` + +**iOS 开发构建(需要 macOS 和 Apple 开发者账号):** +```bash +eas build --profile development --platform ios +``` + +构建过程需要 **10-20 分钟**。完成后: +1. 在 [expo.dev](https://expo.dev) 控制台下载构建的 APK/IPA 文件 +2. 安装到你的设备上 + +### 步骤 4:发布热更新 + +1. **修改代码**(例如修改 `app/(tabs)/index.tsx` 中的文本) + +2. **发布更新到指定通道:** + +```bash +# 发布到开发通道 +eas update --channel development --message "测试热更新功能" + +# 发布到预览通道 +eas update --channel preview --message "修复了某个 bug" + +# 发布到生产通道 +eas update --channel production --message "v1.0.1: 添加了新功能" +``` + +### 步骤 5:测试更新 + +1. 打开已安装的应用 +2. 点击首页的 **"检查更新"** 按钮 +3. 应用会自动下载更新 +4. 点击 **"立即重启"** 应用更新 +5. 重启后即可看到更新内容 🎉 + +### 更新通道说明 + +项目配置了三个更新通道(在 `eas.json` 中定义): + +| 通道 | 用途 | 适用场景 | +|------|------|----------| +| **development** | 开发环境 | 日常开发和测试 | +| **preview** | 预览环境 | 内部测试和 QA | +| **production** | 生产环境 | 正式发布给用户 | + +不同的构建配置会订阅不同的更新通道: + +```bash +# 构建订阅 development 通道的版本 +eas build --profile development --platform android + +# 构建订阅 preview 通道的版本 +eas build --profile preview --platform android + +# 构建订阅 production 通道的版本 +eas build --profile production --platform android +``` + +### 推荐的发布流程 + +```bash +# 1. 开发和本地测试 +pnpm start + +# 2. 发布到 preview 通道进行内部测试 +eas update --channel preview --message "测试新功能" + +# 3. 测试通过后,发布到 production 通道 +eas update --channel production --message "v1.0.1: 修复了登录问题" + +# 4. 如果需要回滚 +eas update --channel production --branch main --message "回滚到稳定版本" +``` + +### 监控和管理更新 + +```bash +# 查看所有更新历史 +eas update:list + +# 查看特定通道的更新 +eas update:list --channel production + +# 查看更新详情 +eas update:view [update-id] +``` + +## 💻 开发指南 + +### 常用命令 + +```bash +# 开发相关 +pnpm start # 启动开发服务器 +pnpm android # 在 Android 上运行 +pnpm ios # 在 iOS 上运行(仅 macOS) +pnpm web # 在浏览器中运行 + +# 热更新相关 +eas login # 登录 EAS +eas init # 初始化 EAS 项目 +eas build --profile development --platform android # 构建开发版本 +eas update --channel development --message "更新说明" # 发布热更新 +eas update:list # 查看更新历史 + +# 构建相关 +eas build --profile preview --platform android # 构建预览版本 +eas build --profile production --platform android # 构建生产版本 +``` + +### 修改代码示例 + +尝试修改以下内容来测试热更新: + +#### 示例 1:修改首页标题 + +打开 `app/(tabs)/index.tsx`,找到: + +```tsx +🚀 热更新演示 +``` + +改为: + +```tsx +🎉 热更新测试成功! +``` + +#### 示例 2:修改按钮样式 + +在 `app/(tabs)/index.tsx` 的 `styles` 中修改: + +```tsx +button: { + backgroundColor: '#FF6B6B', // 改为红色 + paddingHorizontal: 30, + paddingVertical: 15, + borderRadius: 10, + marginBottom: 20, + minWidth: 200, + alignItems: 'center', +}, +``` + +#### 示例 3:添加新功能 + +在首页添加一个计数器: + +```tsx +const [count, setCount] = useState(0); + +// 在 JSX 中添加 + setCount(count + 1)} +> + 点击次数: {count} + +``` + +修改后,运行 `eas update` 发布更新,然后在应用中检查更新即可看到变化。 + +### 自定义更新行为 + +#### 自动静默更新 + +修改 `app/_layout.tsx` 中的更新逻辑,移除 Alert 提示: + +```tsx +if (update.isAvailable) { + await Updates.fetchUpdateAsync(); + await Updates.reloadAsync(); // 直接重启,不提示 +} +``` + +#### 强制更新 + +不提供"稍后"选项: + +```tsx +Alert.alert( + '发现新版本', + '请更新到最新版本', + [ + { + text: '立即更新', + onPress: async () => { + await Updates.reloadAsync(); + }, + }, + ], + { cancelable: false } // 不可取消 +); +``` + +## ⚙️ 配置说明 + +### app.json 配置 + +关键配置项: + +```json +{ + "expo": { + "name": "rn-demo", + "slug": "rn-demo", + "version": "1.0.0", + "newArchEnabled": true, // 启用 React Native 新架构 + "updates": { + "url": "https://u.expo.dev/your-project-id" // EAS Update 服务器 + }, + "runtimeVersion": { + "policy": "appVersion" // 运行时版本策略 + }, + "plugins": [ + "expo-router", // Expo Router 插件 + "expo-updates" // 热更新插件 + ], + "ios": { + "bundleIdentifier": "com.rndemo.app" + }, + "android": { + "package": "com.rndemo.app" + } + } +} +``` + +**配置说明:** +- `updates.url` - EAS Update 服务器地址(运行 `eas init` 后自动生成) +- `runtimeVersion.policy` - 运行时版本策略,确保更新兼容性 + - `appVersion` - 基于 `version` 字段(当前使用) + - `nativeVersion` - 基于原生构建号 +- `plugins` - 启用的 Expo 插件 +- `newArchEnabled` - 启用 React Native 新架构(Fabric + TurboModules) + +### eas.json 配置 + +构建和更新配置: + +```json +{ + "build": { + "development": { + "developmentClient": true, // 开发客户端 + "distribution": "internal", // 内部分发 + "channel": "development" // 订阅 development 更新通道 + }, + "preview": { + "distribution": "internal", + "channel": "preview" // 订阅 preview 更新通道 + }, + "production": { + "channel": "production" // 订阅 production 更新通道 + } + } +} +``` + +**配置说明:** +- `developmentClient` - 是否为开发客户端(包含开发工具) +- `distribution` - 分发方式(`internal` 或 `store`) +- `channel` - 更新通道,决定应用接收哪个通道的更新 + +## 📋 热更新最佳实践 + +### 何时使用热更新 + +✅ **适合热更新的场景:** +- ✅ 修复 JavaScript/TypeScript 代码 bug +- ✅ 更新 UI 样式和布局 +- ✅ 修改业务逻辑 +- ✅ 更新文本内容和翻译 +- ✅ 调整配置参数 +- ✅ 添加新的 JS 功能 + +❌ **不适合热更新的场景(需要重新构建):** +- ❌ 添加/删除原生依赖(如 `react-native-camera`) +- ❌ 修改原生代码(iOS/Android) +- ❌ 更改应用权限(如相机、位置权限) +- ❌ 修改 `app.json` 中的原生配置 +- ❌ 升级 React Native 或 Expo SDK 版本 +- ❌ 修改应用图标或启动屏幕 + +### 版本管理策略 + +**appVersion 策略**(当前使用): +- 基于 `app.json` 中的 `version` 字段 +- ✅ 优点:简单直观,易于理解 +- ⚠️ 注意:每次原生构建需要手动更新版本号 + +**nativeVersion 策略**: +- 基于原生构建号(iOS 的 `buildNumber`,Android 的 `versionCode`) +- ✅ 优点:自动管理,无需手动更新 +- ⚠️ 注意:需要配置原生构建号 + +### 更新策略建议 + +1. **开发阶段**:频繁发布到 `development` 通道 +2. **测试阶段**:发布到 `preview` 通道,进行 QA 测试 +3. **生产发布**:测试通过后发布到 `production` 通道 +4. **紧急修复**:直接发布到 `production` 通道,快速修复线上问题 + +## ❓ 常见问题 + +### Q: 为什么在 Expo Go 中看不到热更新功能? + +**A:** Expo Go 是开发工具,不支持自定义热更新。需要使用 `eas build` 构建真实应用。 + +### Q: 更新后没有生效? + +**A:** 检查以下几点: +1. 确保应用完全关闭后重新打开(不是后台切换) +2. 检查更新通道是否匹配(development/preview/production) +3. 确保 `runtimeVersion` 匹配 +4. 查看控制台是否有错误信息 +5. 检查网络连接 + +### Q: 如何回滚到之前的版本? + +**A:** +```bash +# 查看更新历史 +eas update:list --channel production + +# 重新发布之前的更新 +eas update --channel production --branch main --message "回滚到稳定版本" +``` + +### Q: 热更新的大小限制是多少? + +**A:** EAS Update 没有严格的大小限制,但建议: +- 保持更新包 < 10MB 以确保良好的用户体验 +- 避免在更新中包含大量图片或资源文件 +- 使用 CDN 托管大型资源 + +### Q: 如何强制用户更新? + +**A:** 修改 `app/_layout.tsx` 中的更新逻辑: + +```tsx +if (update.isAvailable) { + await Updates.fetchUpdateAsync(); + Alert.alert( + '发现新版本', + '请更新到最新版本', + [{ text: '立即更新', onPress: async () => await Updates.reloadAsync() }], + { cancelable: false } // 不可取消 + ); +} +``` + +### Q: 更新检查的频率是多少? + +**A:** +- **应用启动时**:自动检查(在 `app/_layout.tsx` 中配置) +- **手动检查**:用户点击"检查更新"按钮 +- **后台检查**:可以配置定时检查(需要自己实现) + +### Q: 如何在开发时测试热更新? + +**A:** +1. 构建 Development Build:`eas build --profile development --platform android` +2. 安装到设备 +3. 修改代码 +4. 发布更新:`eas update --channel development --message "测试"` +5. 在应用中点击"检查更新" + +### Q: Web 平台的警告信息怎么处理? + +**A:** 运行 `pnpm web` 时可能会看到 `pointerEvents` 的弃用警告,这是 `react-native-web` 库的内部问题,不影响功能,可以安全忽略。详见 [docs/KNOWN_ISSUES.md](./docs/KNOWN_ISSUES.md)。 + +## 💡 开发提示 + +- 开发时使用 `pnpm start` 即可,支持快速刷新(Fast Refresh) +- 只有在测试热更新功能时才需要构建真实应用 +- 热更新只能更新 JavaScript/TypeScript 代码,不能更新原生代码 +- 建议使用 Git 管理代码版本,方便回滚 +- 每次发布更新时添加清晰的 `--message` 说明,便于追踪 + +## 📚 相关资源 + +### 官方文档 +- [Expo 官方文档](https://docs.expo.dev/) +- [Expo Router 文档](https://docs.expo.dev/router/introduction/) +- [EAS Update 文档](https://docs.expo.dev/eas-update/introduction/) +- [EAS Build 文档](https://docs.expo.dev/build/introduction/) +- [React Native 文档](https://reactnative.dev/) + +### 学习资源 +- [Expo Router 最佳实践](https://docs.expo.dev/router/best-practices/) +- [EAS Update 最佳实践](https://docs.expo.dev/eas-update/best-practices/) +- [React Native 新架构](https://reactnative.dev/docs/the-new-architecture/landing-page) + +### 社区 +- [Expo Discord](https://chat.expo.dev/) +- [Expo Forums](https://forums.expo.dev/) +- [React Native Community](https://reactnative.dev/community/overview) + +## 📚 项目文档 + +更多详细文档请查看 [docs](./docs/) 目录: + +- **[使用示例](./docs/USAGE_EXAMPLES.md)** - 实际代码示例 +- **[工具库使用指南](./docs/LIBRARIES.md)** - 详细的工具库使用方法和示例 + + + +--- +**祝你开发愉快!** 🎉 + +如有问题,请查看 [常见问题](#-常见问题) 或访问 [Expo 官方文档](https://docs.expo.dev/)。 + diff --git a/app.json b/app.json new file mode 100644 index 0000000..b9ebfad --- /dev/null +++ b/app.json @@ -0,0 +1,53 @@ +{ + "expo": { + "name": "rn-demo", + "slug": "rn-demo", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "rndemo", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "splash": { + "image": "./assets/images/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.rndemo.app" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false, + "package": "com.rndemo.app" + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + "expo-updates" + ], + "experiments": { + "typedRoutes": true + }, + "updates": { + "url": "https://u.expo.dev/your-project-id" + }, + "runtimeVersion": { + "policy": "appVersion" + }, + "extra": { + "eas": { + "projectId": "your-project-id" + } + } + } +} diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..0e2d32e --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import FontAwesome from '@expo/vector-icons/FontAwesome'; +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'; + +// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ +function TabBarIcon(props: { + name: React.ComponentProps['name']; + color: string; +}) { + return ; +} + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + , + headerRight: () => ( + + + {({ pressed }) => ( + + )} + + + ), + }} + /> + , + }} + /> + , + }} + /> + + ); +} diff --git a/app/(tabs)/demo.tsx b/app/(tabs)/demo.tsx new file mode 100644 index 0000000..2a80bc0 --- /dev/null +++ b/app/(tabs)/demo.tsx @@ -0,0 +1,686 @@ +/** + * 完整示例页面 + * 展示所有工具的使用方法 + */ + +import React, { useState, useEffect } from 'react'; +import { + StyleSheet, + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + Switch, + Alert, + ActivityIndicator, +} from 'react-native'; +import { useForm, Controller } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Image } from 'expo-image'; + +// 导入所有工具 +import { + // 工具函数 + Storage, + STORAGE_KEYS, + formatDate, + formatRelativeTime, + formatChatTime, + + // 状态管理 + useUserStore, + useUser, + useIsLoggedIn, + useSettingsStore, + useTheme, + useLanguage, + useHapticsEnabled, + + // 验证规则 + loginSchema, + type LoginFormData, + + // API 服务 + authService, + + // 自定义 Hooks + useDebounce, + useThrottle, + useHaptics, +} from '@/src'; + +export default function DemoScreen() { + const haptics = useHaptics(); + + // 状态管理示例 + const user = useUser(); + const isLoggedIn = useIsLoggedIn(); + const login = useUserStore((state) => state.login); + const logout = useUserStore((state) => state.logout); + + // 设置状态 + 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 [searchText, setSearchText] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [loading, setLoading] = useState(false); + const [counter, setCounter] = useState(0); + const [storageValue, setStorageValue] = useState(''); + + // 表单配置 + const { + control, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + // 防抖搜索示例 + const debouncedSearch = useDebounce(async (text: string) => { + if (!text.trim()) { + setSearchResults([]); + return; + } + + console.log('执行搜索:', text); + // 模拟 API 调用 + await new Promise((resolve) => setTimeout(resolve, 500)); + setSearchResults([ + `结果 1: ${text}`, + `结果 2: ${text}`, + `结果 3: ${text}`, + ]); + }, 500); + + // 监听搜索文本变化 + useEffect(() => { + debouncedSearch(searchText); + }, [searchText]); + + // 节流点击示例 + const throttledClick = useThrottle(() => { + haptics.light(); + setCounter((prev) => prev + 1); + console.log('节流点击:', counter + 1); + }, 1000); + + // 登录处理 + const onLogin = async (data: LoginFormData) => { + try { + setLoading(true); + haptics.light(); + + // 模拟登录 API 调用 + console.log('登录数据:', data); + + // 实际项目中使用: + // const { user, token } = await authService.login(data); + // login(user, token); + + // 模拟登录成功 + const mockUser = { + id: '1', + username: data.email.split('@')[0], + email: data.email, + avatar: 'https://i.pravatar.cc/150?img=1', + nickname: '演示用户', + createdAt: new Date().toISOString(), + }; + + login(mockUser, 'mock-token-123456'); + haptics.success(); + Alert.alert('成功', '登录成功!'); + } catch (error: any) { + haptics.error(); + Alert.alert('失败', error.message || '登录失败'); + } finally { + setLoading(false); + } + }; + + // 登出处理 + const handleLogout = () => { + haptics.warning(); + Alert.alert( + '确认', + '确定要退出登录吗?', + [ + { text: '取消', style: 'cancel' }, + { + text: '确定', + onPress: () => { + logout(); + haptics.success(); + }, + }, + ] + ); + }; + + // 存储示例 + const handleSaveToStorage = async () => { + try { + haptics.light(); + const testData = { + message: 'Hello from AsyncStorage!', + timestamp: new Date().toISOString(), + counter, + }; + + await Storage.setObject(STORAGE_KEYS.USER_PREFERENCES, testData); + haptics.success(); + Alert.alert('成功', '数据已保存到本地存储'); + } catch (error) { + haptics.error(); + Alert.alert('失败', '保存失败'); + } + }; + + const handleLoadFromStorage = async () => { + try { + haptics.light(); + const data = await Storage.getObject(STORAGE_KEYS.USER_PREFERENCES); + + if (data) { + setStorageValue(JSON.stringify(data, null, 2)); + haptics.success(); + } else { + setStorageValue('暂无数据'); + haptics.warning(); + } + } catch (error) { + haptics.error(); + Alert.alert('失败', '读取失败'); + } + }; + + // 主题切换 + const handleThemeChange = () => { + haptics.selection(); + const themes: Array<'light' | 'dark' | 'auto'> = ['light', 'dark', 'auto']; + const currentIndex = themes.indexOf(theme); + const nextTheme = themes[(currentIndex + 1) % themes.length]; + setTheme(nextTheme); + }; + + // 语言切换 + const handleLanguageChange = () => { + haptics.selection(); + setLanguage(language === 'zh-CN' ? 'en-US' : 'zh-CN'); + }; + + return ( + + + {/* 标题 */} + 🎯 完整功能演示 + 展示所有工具的使用方法 + + {/* 用户状态显示 */} + + 👤 用户状态 (Zustand) + {isLoggedIn ? ( + + {user?.avatar && ( + + )} + + {user?.nickname} + {user?.email} + + 注册时间: {formatRelativeTime(user?.createdAt || '')} + + + + 退出 + + + ) : ( + 未登录 + )} + + + {/* 登录表单 */} + {!isLoggedIn && ( + + 🔐 登录表单 (React Hook Form + Zod) + + ( + + 邮箱 + + {errors.email && ( + {errors.email.message} + )} + + )} + /> + + ( + + 密码 + + {errors.password && ( + {errors.password.message} + )} + + )} + /> + + + {loading ? ( + + ) : ( + 登录 + )} + + + )} + + {/* 搜索示例 */} + + 🔍 防抖搜索 (useDebounce) + + {searchResults.length > 0 && ( + + {searchResults.map((result, index) => ( + + {result} + + ))} + + )} + + + {/* 节流点击示例 */} + + ⏱️ 节流点击 (useThrottle) + 点击次数: {counter} + + 快速点击测试(1秒节流) + + + + {/* 本地存储示例 */} + + 💾 本地存储 (AsyncStorage) + + + 保存数据 + + + 读取数据 + + + {storageValue && ( + + {storageValue} + + )} + + + {/* 日期格式化示例 */} + + 📅 日期格式化 (Day.js) + + 当前时间: {formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')} + + + 相对时间: {formatRelativeTime(new Date())} + + + 聊天时间: {formatChatTime(Date.now())} + + + + {/* 设置示例 */} + + ⚙️ 应用设置 + + + 主题: {theme} + + 切换 + + + + + 语言: {language} + + 切换 + + + + + 触觉反馈 + { + haptics.selection(); + setHapticsEnabled(value); + }} + /> + + + + {/* 触觉反馈示例 */} + + 📳 触觉反馈 (Expo Haptics) + + haptics.light()} + > + Light + + haptics.medium()} + > + Medium + + haptics.heavy()} + > + Heavy + + haptics.success()} + > + Success + + haptics.warning()} + > + Warning + + haptics.error()} + > + Error + + + + + + + 查看代码了解更多使用方法 📖 + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + content: { + padding: 16, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + marginBottom: 8, + color: '#333', + }, + subtitle: { + fontSize: 16, + color: '#666', + marginBottom: 24, + }, + section: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + marginBottom: 12, + color: '#333', + }, + userInfo: { + flexDirection: 'row', + alignItems: 'center', + }, + avatar: { + width: 60, + height: 60, + borderRadius: 30, + marginRight: 12, + }, + userDetails: { + flex: 1, + }, + userName: { + fontSize: 16, + fontWeight: '600', + color: '#333', + }, + userEmail: { + fontSize: 14, + color: '#666', + marginTop: 2, + }, + userDate: { + fontSize: 12, + color: '#999', + marginTop: 4, + }, + inputGroup: { + marginBottom: 16, + }, + label: { + fontSize: 14, + fontWeight: '500', + marginBottom: 6, + color: '#333', + }, + input: { + borderWidth: 1, + borderColor: '#ddd', + borderRadius: 8, + padding: 12, + fontSize: 16, + backgroundColor: '#fff', + }, + inputError: { + borderColor: '#ff3b30', + }, + errorText: { + color: '#ff3b30', + fontSize: 12, + marginTop: 4, + }, + button: { + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + loginButton: { + backgroundColor: '#007AFF', + marginTop: 8, + }, + logoutButton: { + backgroundColor: '#ff3b30', + paddingVertical: 8, + paddingHorizontal: 16, + }, + primaryButton: { + backgroundColor: '#007AFF', + }, + secondaryButton: { + backgroundColor: '#5856D6', + }, + smallButton: { + backgroundColor: '#007AFF', + paddingVertical: 8, + paddingHorizontal: 16, + }, + buttonRow: { + flexDirection: 'row', + gap: 12, + }, + halfButton: { + flex: 1, + }, + searchResults: { + marginTop: 12, + }, + searchResult: { + padding: 12, + backgroundColor: '#f9f9f9', + borderRadius: 8, + marginBottom: 8, + color: '#333', + }, + infoText: { + fontSize: 14, + color: '#666', + marginBottom: 8, + }, + codeBlock: { + backgroundColor: '#f9f9f9', + borderRadius: 8, + padding: 12, + marginTop: 12, + }, + codeText: { + fontFamily: 'monospace', + fontSize: 12, + color: '#333', + }, + settingRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + settingLabel: { + fontSize: 16, + color: '#333', + }, + buttonGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, + hapticsButton: { + backgroundColor: '#5856D6', + flex: 1, + minWidth: '30%', + }, + successButton: { + backgroundColor: '#34C759', + flex: 1, + minWidth: '30%', + }, + warningButton: { + backgroundColor: '#FF9500', + flex: 1, + minWidth: '30%', + }, + errorButton: { + backgroundColor: '#FF3B30', + flex: 1, + minWidth: '30%', + }, + footer: { + marginTop: 24, + marginBottom: 40, + alignItems: 'center', + }, + footerText: { + fontSize: 14, + color: '#999', + }, +}); + diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..a0f5c8d --- /dev/null +++ b/app/(tabs)/index.tsx @@ -0,0 +1,192 @@ +import { useState } from 'react'; +import { StyleSheet, TouchableOpacity, Alert, ActivityIndicator } from 'react-native'; +import * as Updates from 'expo-updates'; + +import { Text, View } from '@/components/Themed'; + +export default function TabOneScreen() { + const [isChecking, setIsChecking] = useState(false); + const [updateInfo, setUpdateInfo] = useState(''); + + const checkForUpdates = async () => { + if (__DEV__) { + Alert.alert('提示', '开发模式下无法检查更新,请使用生产构建测试热更新功能'); + return; + } + + setIsChecking(true); + setUpdateInfo('正在检查更新...'); + + try { + const update = await Updates.checkForUpdateAsync(); + + if (update.isAvailable) { + setUpdateInfo('发现新版本,正在下载...'); + await Updates.fetchUpdateAsync(); + + Alert.alert( + '更新完成', + '新版本已下载完成,是否立即重启应用?', + [ + { + text: '稍后', + style: 'cancel', + onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'), + }, + { + text: '立即重启', + onPress: async () => { + await Updates.reloadAsync(); + }, + }, + ] + ); + } else { + setUpdateInfo('当前已是最新版本'); + } + } catch (error) { + setUpdateInfo('检查更新失败: ' + (error as Error).message); + Alert.alert('错误', '检查更新失败,请稍后重试'); + } finally { + setIsChecking(false); + } + }; + + const getUpdateInfo = () => { + const { + isEmbeddedLaunch, + isEmergencyLaunch, + updateId, + channel, + runtimeVersion, + } = Updates.useUpdates(); + + return ` +运行模式: ${__DEV__ ? '开发模式' : '生产模式'} +是否为内嵌启动: ${isEmbeddedLaunch ? '是' : '否'} +是否为紧急启动: ${isEmergencyLaunch ? '是' : '否'} +更新 ID: ${updateId || '无'} +更新通道: ${channel || '无'} +运行时版本: ${runtimeVersion || '无'} + `.trim(); + }; + + return ( + + 🚀 热更新演示 + + + + 当前版本信息: + {getUpdateInfo()} + + + + {isChecking ? ( + + ) : ( + 检查更新 + )} + + + {updateInfo ? ( + + {updateInfo} + + ) : null} + + + 📝 使用说明: + + 1. 使用 EAS Build 构建生产版本{'\n'} + 2. 修改代码后运行 eas update 发布更新{'\n'} + 3. 打开应用点击"检查更新"按钮{'\n'} + 4. 应用会自动下载并提示重启 + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + marginBottom: 10, + }, + separator: { + marginVertical: 20, + height: 1, + width: '80%', + }, + infoContainer: { + backgroundColor: 'rgba(0, 122, 255, 0.1)', + padding: 15, + borderRadius: 10, + marginBottom: 20, + width: '100%', + }, + infoTitle: { + fontSize: 16, + fontWeight: 'bold', + marginBottom: 10, + }, + infoText: { + fontSize: 12, + fontFamily: 'monospace', + lineHeight: 18, + }, + button: { + backgroundColor: '#007AFF', + paddingHorizontal: 30, + paddingVertical: 15, + borderRadius: 10, + marginBottom: 20, + minWidth: 200, + alignItems: 'center', + }, + buttonDisabled: { + backgroundColor: '#999', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + }, + updateInfoContainer: { + backgroundColor: 'rgba(52, 199, 89, 0.1)', + padding: 15, + borderRadius: 10, + marginBottom: 20, + width: '100%', + }, + updateInfoText: { + fontSize: 14, + textAlign: 'center', + }, + instructionsContainer: { + backgroundColor: 'rgba(255, 149, 0, 0.1)', + padding: 15, + borderRadius: 10, + width: '100%', + }, + instructionsTitle: { + fontSize: 16, + fontWeight: 'bold', + marginBottom: 10, + }, + instructionsText: { + fontSize: 13, + lineHeight: 20, + }, +}); diff --git a/app/(tabs)/two.tsx b/app/(tabs)/two.tsx new file mode 100644 index 0000000..f2ea47e --- /dev/null +++ b/app/(tabs)/two.tsx @@ -0,0 +1,31 @@ +import { StyleSheet } from 'react-native'; + +import EditScreenInfo from '@/components/EditScreenInfo'; +import { Text, View } from '@/components/Themed'; + +export default function TabTwoScreen() { + return ( + + Tab Two + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + fontSize: 20, + fontWeight: 'bold', + }, + separator: { + marginVertical: 30, + height: 1, + width: '80%', + }, +}); diff --git a/app/+html.tsx b/app/+html.tsx new file mode 100644 index 0000000..cb31090 --- /dev/null +++ b/app/+html.tsx @@ -0,0 +1,38 @@ +import { ScrollViewStyleReset } from 'expo-router/html'; + +// This file is web-only and used to configure the root HTML for every +// web page during static rendering. +// The contents of this function only run in Node.js environments and +// do not have access to the DOM or browser APIs. +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {/* + Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. + However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} +