344 lines
12 KiB
Markdown
344 lines
12 KiB
Markdown
# 正念 APP(客户端)
|
||
|
||
本目录用于存放「正念 APP」客户端工程,定位是面向宝妈人群的情绪价值与正念练习支持应用。
|
||
|
||
## 功能概览(一期)
|
||
|
||
- **APP-PUSH 推送**:定时推送情绪文字(固定时间 / 用户自定义时间),推送内容可由后端配置更新
|
||
- **iOS 小组件(Widget)**:展示当前情绪文字与背景图/渐变色,支持多尺寸与可配置刷新频率
|
||
- **情绪卡片滑动**:卡片列表左右滑动切换(类似 Tinder),收藏/分享可作为后续迭代
|
||
|
||
## 技术栈
|
||
|
||
- **React Native + Expo**
|
||
- **TypeScript**
|
||
- **React Navigation**:页面路由
|
||
- **状态管理**:Zustand(或 Redux Toolkit)
|
||
- **网络请求**:Axios(或 Fetch)
|
||
- **推送**:Expo Notifications
|
||
- **环境区分**:Expo App Config + `.env.dev` / `.env.prod`
|
||
- **iOS 打包**:EAS Build
|
||
|
||
## 开发环境要求
|
||
|
||
- **Node.js**:建议使用 **20+(LTS)**
|
||
- **包管理器**:pnpm
|
||
- **Expo CLI**:推荐通过 `npx expo` 使用(避免全局版本漂移)
|
||
- **iOS 真机调试**:macOS + Xcode(如需)
|
||
|
||
|
||
> 本项目已内置 `postinstall` 补丁脚本用于兼容(安装依赖后会自动修复),但仍建议升级到 Node 20+。
|
||
|
||
## 快速开始
|
||
|
||
> 说明:本仓库已包含 Expo 工程文件;如果你只是想把客户端跑起来,从「安装依赖」开始即可。
|
||
|
||
### 1)初始化工程(若尚未创建)
|
||
|
||
在 `client/` 目录下创建 Expo 工程(示例):
|
||
|
||
```bash
|
||
cd client
|
||
npx create-expo-app@latest .
|
||
```
|
||
|
||
执行后按提示选择模板即可;如果你希望固定使用 TypeScript 模板,也可以在创建时选择对应模板(以实际 CLI 提示为准)。
|
||
|
||
### 2)安装依赖
|
||
|
||
```bash
|
||
cd client
|
||
pnpm install
|
||
```
|
||
|
||
### 3)配置环境变量(dev / prod)
|
||
|
||
按根目录约定,客户端使用 `.env.dev` 与 `.env.prod` 区分环境;`.env` 文件不提交到 Git,仓库内应提供 `.env.example` 作为模板。
|
||
|
||
#### 本地开发如何注入环境变量(推荐)
|
||
|
||
Expo 在本地开发时会自动读取当前目录下的 `.env` / `.env.local`(仅以 `EXPO_PUBLIC_` 前缀变量注入到客户端运行时)。推荐做法:
|
||
|
||
```bash
|
||
cd client
|
||
cp .env.example .env.local
|
||
# 然后按需修改 .env.local
|
||
pnpm start
|
||
```
|
||
|
||
> 修改 `.env*` 后需要重启 `pnpm start` 才会生效。
|
||
|
||
#### 本地开发如何区分 dev / prod(可选)
|
||
|
||
如果你希望坚持使用 `.env.dev` / `.env.prod` 文件名,可以在启动前导出环境变量(zsh 示例):
|
||
|
||
```bash
|
||
cd client
|
||
set -a
|
||
source .env.dev
|
||
set +a
|
||
pnpm start
|
||
```
|
||
|
||
> 注意:`source` 方式要求 `.env.dev` 内容是合法的 shell 格式(例如 `KEY=value`),不要带 `export` 以外的复杂语法;值包含空格时需要加引号。
|
||
|
||
#### 必填环境变量
|
||
|
||
- **`EXPO_PUBLIC_API_BASE_URL`**:后端 API 基地址
|
||
- 本地:`http://localhost:8000`
|
||
- 真机联调:改为电脑局域网 IP(例如 `http://192.168.1.10:8000`)
|
||
- **`EXPO_PUBLIC_ENV`**:环境标识(建议 `dev` / `prod`)
|
||
|
||
#### 可选环境变量
|
||
|
||
- **`EXPO_PUBLIC_DEFAULT_LANGUAGE`**:默认语言策略
|
||
- `auto`:优先设备语言(默认)
|
||
- `zh-CN/en/es/pt/zh-TW`:固定默认语言(仍允许用户在设置中切换并持久化)
|
||
|
||
建议字段(示例):
|
||
|
||
```env
|
||
EXPO_PUBLIC_API_BASE_URL=http://localhost:8000
|
||
EXPO_PUBLIC_ENV=dev
|
||
EXPO_PUBLIC_DEFAULT_LANGUAGE=auto
|
||
```
|
||
|
||
> 提示:在 Expo 中建议使用 `EXPO_PUBLIC_` 前缀,方便在运行时读取并支持 EAS 构建注入。
|
||
|
||
### 4)启动开发服务器
|
||
|
||
```bash
|
||
cd client
|
||
pnpm start
|
||
```
|
||
|
||
启动后你会看到终端输出的二维码(QR Code)与可用命令提示。
|
||
|
||
### 5)运行到 iOS / Android / 真机 / Web
|
||
|
||
- **iOS 模拟器(macOS + Xcode)**:
|
||
- 方式一:在 `pnpm start` 的终端里按 `i`
|
||
- 方式二:直接运行:
|
||
|
||
```bash
|
||
cd client
|
||
pnpm ios
|
||
```
|
||
|
||
- **Android 模拟器(Android Studio)**:
|
||
- 方式一:在 `pnpm start` 的终端里按 `a`
|
||
- 方式二:直接运行:
|
||
|
||
```bash
|
||
cd client
|
||
pnpm android
|
||
```
|
||
|
||
- **真机(推荐使用 Expo Go)**:
|
||
- 手机安装 **Expo Go**
|
||
- 确保手机与电脑在**同一局域网**
|
||
- 运行 `pnpm start` 后,用 Expo Go 扫描终端二维码即可打开 App
|
||
|
||
- **Web(可选)**:
|
||
|
||
```bash
|
||
cd client
|
||
pnpm web
|
||
```
|
||
|
||
常用命令(脚本):
|
||
|
||
```bash
|
||
pnpm ios
|
||
pnpm android
|
||
pnpm web
|
||
```
|
||
|
||
### 6)常见问题
|
||
|
||
- **真机打不开 / 扫码后卡住**:
|
||
- 检查手机与电脑是否同一 Wi-Fi
|
||
- 尝试重启 `pnpm start -- --clear` 清缓存
|
||
- **启动时提示 `Networking has been disabled` / `Network connection is unreliable`**:
|
||
- 这是 Expo CLI 的网络探测/请求失败导致的,按下面方式跳过网络请求即可:
|
||
|
||
```bash
|
||
cd client
|
||
EXPO_OFFLINE=1 pnpm start
|
||
```
|
||
|
||
- 如果你本机设置了代理(例如 `HTTP_PROXY/ALL_PROXY`),也可以临时关闭代理后再启动:
|
||
|
||
```bash
|
||
cd client
|
||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY
|
||
pnpm start
|
||
```
|
||
|
||
- **Watchman 提示 Recrawl(不影响运行,但建议处理)**:
|
||
- 按提示执行一次即可清除警告:
|
||
|
||
```bash
|
||
watchman watch-del '/Users/jojo/Desktop/lxy/gitea/mindfulness/client' ; watchman watch-project '/Users/jojo/Desktop/lxy/gitea/mindfulness/client'
|
||
```
|
||
- **联调时请求打到 localhost**:
|
||
- 真机上 `localhost` 指向手机自身,请把 `EXPO_PUBLIC_API_BASE_URL` 改成电脑的局域网 IP(例如 `http://192.168.1.10:8000`),或使用内网穿透方案
|
||
|
||
## 与后端联调
|
||
|
||
- **后端基座**:FastAPI(详见 `server/README.md`)
|
||
- **API Base URL**:通过 `EXPO_PUBLIC_API_BASE_URL` 配置
|
||
- **本地真机注意**:如果在手机上调试,请将 `localhost` 替换为电脑局域网 IP,或使用内网穿透方案
|
||
|
||
## 多语言(i18n):CN / EN / ES / PT / TC
|
||
|
||
### 语言码约定
|
||
|
||
- **CN(简体中文)**:`zh-CN`
|
||
- **EN(英语)**:`en`
|
||
- **ES(西班牙语)**:`es`
|
||
- **PT(葡萄牙语)**:`pt`
|
||
- **TC(繁体中文)**:`zh-TW`
|
||
|
||
### 推荐选型(Expo/RN 常用组合)
|
||
|
||
- **文案管理**:`i18next` + `react-i18next`
|
||
- **系统语言读取**:`expo-localization`
|
||
|
||
### 建议目录结构
|
||
|
||
```text
|
||
src/
|
||
└── i18n/
|
||
├── index.ts # i18n 初始化(默认语言、回退语言、资源注册)
|
||
├── locales/
|
||
│ ├── zh-CN.json
|
||
│ ├── en.json
|
||
│ ├── es.json
|
||
│ ├── pt.json
|
||
│ └── zh-TW.json
|
||
└── types.ts #(可选)语言码与 key 的类型定义
|
||
```
|
||
|
||
### 约定与最佳实践
|
||
|
||
- **key 规则**:使用稳定的点分层 key,例如 `common.ok`、`push.permissionTitle`、`cards.swipeHint`
|
||
- **默认与回退**:默认优先跟随用户当前设备语言(在支持列表内时生效);设备语言不支持时回退到 `zh-CN`(或团队指定默认)
|
||
- **插值与复数**:优先使用 i18next 的插值(例如 `{{name}}`),避免在代码里拼字符串
|
||
- **动态切换**:提供“语言设置”入口允许手动切换;切换后持久化(例如 AsyncStorage),后续启动优先生效,并立即刷新文案
|
||
- **语言优先级**:用户设置(若存在)> 设备语言(在支持列表内)> 默认回退(`zh-CN` 或团队指定默认)
|
||
- **设置入口建议**:设置页 -> 语言(或 设置页 -> 通用 -> 语言)
|
||
- **与服务端一致性**:若后端也需要多语言(推送文案等),建议统一语言码(`zh-CN/en/es/pt/zh-TW`)并在接口里明确 `lang`
|
||
|
||
## 推送(Expo Notifications)
|
||
|
||
客户端侧通常需要:
|
||
|
||
- **申请通知权限**
|
||
- **获取 Expo Push Token**
|
||
- **将 Token 上报后端**(用于定时推送任务)
|
||
|
||
后端侧通常需要:
|
||
|
||
- **保存用户 Push Token**
|
||
- **按策略触发推送**(固定时间 / 用户自定义时间)
|
||
- **支持后台配置推送文案并更新**
|
||
|
||
> 具体实现以客户端工程代码为准;当代码接入后,建议在 README 补充「Token 上报接口」与「字段定义」。
|
||
|
||
## iOS 小组件(Widget)
|
||
|
||
小组件通常需要:
|
||
|
||
- **数据源**:来自本地缓存或后端拉取(需要设计刷新策略与缓存)
|
||
- **展示内容**:当前情绪文字 + 背景图/渐变
|
||
- **尺寸**:小 / 中 / 大
|
||
- **刷新频率**:如每小时/每天(iOS 对频率有系统限制,以实际效果为准)
|
||
|
||
> 若你计划使用 Expo 的相关能力,请在工程中明确选型与实现路径,并补充到本 README。
|
||
|
||
## EAS Build(iOS 打包)
|
||
|
||
建议按根目录约定区分 dev/prod:
|
||
|
||
- **dev**:`com.damer.mindfulness.dev`
|
||
- **prod**:`com.damer.mindfulness`
|
||
|
||
常见流程(示例):
|
||
|
||
```bash
|
||
cd client
|
||
eas login
|
||
eas build --platform ios --profile development
|
||
```
|
||
|
||
> 实际 profile、证书、bundle id、环境变量注入方式以工程内 `eas.json` / app config 为准。
|
||
|
||
## 目录结构(建议)
|
||
|
||
当客户端工程落地后,建议使用更标准、可扩展的目录结构(Expo + TypeScript 常见组织方式):
|
||
|
||
```text
|
||
client/
|
||
├── app/ #(推荐)expo-router 路由目录(若使用 expo-router)
|
||
│ ├── (tabs)/ # Tabs 分组(可选)
|
||
│ ├── _layout.tsx # 根布局
|
||
│ └── index.tsx # 首页
|
||
├── src/ # 业务源码(与路由/平台代码解耦)
|
||
│ ├── components/ # 通用 UI 组件
|
||
│ ├── features/ # 按功能域拆分(推荐)
|
||
│ │ ├── push/ # 推送相关(权限、token、上报等)
|
||
│ │ ├── widget/ # 小组件数据与样式相关
|
||
│ │ └── cards/ # 情绪卡片滑动相关
|
||
│ ├── hooks/ # 自定义 hooks
|
||
│ ├── navigation/ #(若不用 expo-router)React Navigation 配置
|
||
│ ├── screens/ #(若不用 expo-router)页面
|
||
│ ├── services/ # API 封装(request、接口定义)
|
||
│ ├── store/ # 状态管理(Zustand/RTK)
|
||
│ ├── utils/ # 工具函数(时间、格式化、校验等)
|
||
│ ├── constants/ # 常量与配置(主题、枚举等)
|
||
│ └── types/ # 全局类型声明
|
||
├── assets/ # 静态资源(图片/字体/音频等)
|
||
├── app.json / app.config.ts # Expo 配置(环境区分可在此处理)
|
||
├── eas.json # EAS Build 配置(如使用)
|
||
├── package.json
|
||
└── README.md
|
||
```
|
||
|
||
> 说明:如果你不使用 `expo-router`,可以删除 `app/`,并以 `src/navigation/` + `src/screens/` 作为主路由结构;其余目录保持不变即可。
|
||
|
||
## iOS 小组件开发(V1:写死文案)
|
||
|
||
> 重要:**Expo Go 不支持 WidgetKit**。要做真正的小组件,必须预构建 iOS 工程并用 Xcode 跑。
|
||
|
||
### 1)生成 iOS 工程
|
||
|
||
```bash
|
||
cd client
|
||
npx expo prebuild -p ios --no-install
|
||
```
|
||
|
||
生成后会得到 `client/ios/`(Xcode 工程文件都在里面)。
|
||
|
||
### 2)在 Xcode 创建 Widget Extension
|
||
|
||
- 用 Xcode 打开:`client/ios/client.xcworkspace`
|
||
- 菜单:`File -> New -> Target... -> Widget Extension`
|
||
- Target 名称示例:`MindfulnessWidget`
|
||
|
||
### 3)写死文案与三尺寸布局
|
||
|
||
仓库里已提供 SwiftUI 代码骨架(写死文案 + Small/Medium/Large + 点击跳转):
|
||
|
||
- `client/ios/情绪小组件/EmotionWidget.swift`
|
||
|
||
创建 Widget target 后,把该文件加入到 Widget target 中即可(Target Membership 勾选:`情绪小组件`)。
|
||
|
||
### 4)点击跳转到 Home(Deep Link)
|
||
|
||
Widget 点击跳转使用:
|
||
|
||
- `client:///(app)/home`
|
||
|
||
代码里已通过 `widgetURL` 设置(见 `MindfulnessWidget.swift`)。
|