Expo HAS CHANGED Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code. 项目通用开发文档 ======================================== 一、架构约定(数据层与 UI 层分离) ======================================== 【核心原则】 做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。 请在每周的周一-周五的每天17:50提交一次git仓库,说明今天新增与改动的内容,每天开始的时候拉取一次,每天结束的时候提交一次,上班时间是9:00-18:00 业务逻辑分层管理,不是没有,而是被妥善组织: +------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ | 层级 | 职责 | 业务逻辑示例 | +------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ | 页面组件 | 渲染UI + 处理用户交互事件 | 点击排序按钮 → 调用Hook切换排序;输入搜索词 → 触发搜索;切换Tab高亮 | | Hook层 | 管理数据状态 + 编排数据逻辑 | 根据排序/筛选参数重新获取数据;组合多个API数据;管理loading/error/empty | | API层 | 发起请求 + 数据转换 + 业务规则 | 计算匹配度;根据分数映射等级(优秀/良好/中等);薪资范围格式化 | | types层 | 定义数据结构 | 用联合类型建模派生状态,如 Job.level: 'excellent' | 'good' | 'medium' | +------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ 【目录结构】 src/ types/ # 类型定义(完整领域模型 + 派生状态联合类型) api/ # 接口层:发起请求、数据转换、业务规则计算 hooks/ # 数据hooks:调用API、管理状态、编排数据逻辑 theme/ # 设计令牌(colors) utils/ # 工具函数(日期格式化、字符串处理等纯函数) constants/ # 常量定义(枚举、配置项等) app/ (tabs)/ # 底部Tab页面 index.tsx # 首页 jobs.tsx # 岗位列表页 chat.tsx # 沟通页 growth.tsx # 成长页 profile.tsx # 我的页 _layout.tsx # Tab导航配置 _layout.tsx # 根布局 所有模拟数据统一放在 src/api/api.ts 中管理。未来接入真实接口时,仅需替换该文件中的函数体,无需新增或删除其他文件。 【数据链路(页面 → Hook → API → 后端)】 新增任何数据按此链路实现: 第1步:src/types/index.ts 定义类型 - 完整领域模型(Job、User、Category 等) - 派生状态用联合类型建模,如 Job.level: 'excellent' | 'good' | 'medium' - 禁止在组件里写死判断逻辑 第2步:src/api/api.ts 定义接口函数 - JSDoc 标注:@method、@url、@param、@returns - 当前用 setTimeout 返回 mock 数据 - 函数体内用注释预留真实 fetch 示例 - 业务规则计算放在这层:匹配度算法、等级映射、数据转换 第3步:src/hooks/ 封装数据 hook - 通用取数复用 useAsyncData(fetcher),返回: { data: T | null, loading: boolean, error: Error | null, refresh: () => void } - 支持自动重试(如重试3次)和请求去重(防止重复请求) - 再包一层语义化命名 hook(如 useJobData() 返回 { jobs, loading, error, refresh, isEmpty }) - 数据编排逻辑放在这层:根据排序/筛选参数重组数据 第4步:页面只调用 hook 取数 - UI交互逻辑放在页面:点击切换Tab、表单输入、按钮高亮状态 - 页面组件统一处理四种状态:加载中、错误、空状态、成功(渲染数据) 【状态与错误处理约定】 +------------------+------------------------------------------+----------------------------------+ | 状态 | UI表现 | 说明 | +------------------+------------------------------------------+----------------------------------+ | loading === true | 显示骨架屏或Loading指示器 | 首次加载和刷新时展示 | | error !== null | 显示错误提示 + 重试按钮 | 调用 refresh() 重试 | | isEmpty === true | 显示空状态占位图 + 提示文字 | 数据为空数组时展示 | | data !== null | 正常渲染数据列表 | 成功状态 | +------------------+------------------------------------------+----------------------------------+ ======================================== 二、编码规范 ======================================== 【命名规范】 Hook: - 以 use 开头,采用 use + 名词 + 动词 - 示例:useJobList、useUserProfile、useCategoryData API函数: - 以 fetch 开头,采用 fetch + 名词 - 示例:fetchJobList、fetchUserInfo、fetchMatchData 类型/接口: - 使用名词,不添加 I 前缀 - 示例:interface Job,而非 interface IJob - 示例:type SortType = 'comprehensive' | 'latest' | 'salary' 组件文件: - 使用 PascalCase(大驼峰) - 示例:JobListScreen.tsx、SearchBar.tsx - 页面组件以 Screen 结尾:HomeScreen.tsx、JobsScreen.tsx 常量: - 使用 UPPER_SNAKE_CASE(全大写下划线) - 示例:MAX_RETRY_COUNT、API_TIMEOUT、DEFAULT_PAGE_SIZE 工具函数: - 使用 camelCase(小驼峰) - 示例:formatDate、capitalizeString、debounce 【TypeScript 配置】 推荐 tsconfig.json 核心配置: { "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true } } 【禁止行为(反模式)】 ❌ 页面组件中直接使用 fetch 或 axios 调用API ❌ 在组件内使用 useState + useEffect 管理数据加载和错误状态 ❌ 在组件内通过 if (score >= 85) 这样的硬编码判断等级 ❌ 在组件内写死十六进制颜色值(如 #FF3B30) ❌ 在组件内直接操作 localStorage / AsyncStorage ❌ 在循环或条件语句中调用 Hook ❌ 在组件内定义不需要的状态(能用派生值就不用 state) ❌ 在 useEffect 中遗漏清理函数(事件监听、定时器等) ✅ 正确做法: - 所有数据请求和业务规则计算,必须通过 Hook 和 API 层完成 - 颜色统一从 theme/colors.ts 导入 - 存储操作封装在 API 层或独立的 storage 模块中 - useEffect 中注册的事件监听必须在清理函数中移除 ======================================== 三、环境配置 ======================================== 使用 expo-constants 管理环境变量。在 app.config.js 中定义 extra 字段: // app.config.js export default { extra: { apiBaseUrl: process.env.API_BASE_URL || 'https://dev.api.com', }, }; 在 src/api/api.ts 中通过 Constants.expoConfig?.extra?.apiBaseUrl 读取。 环境变量示例: API_BASE_URL=https://api.production.com # 生产环境 API_BASE_URL=https://api.test.com # 测试环境 ======================================== 四、通用约定 ======================================== - 每个 src/ 子目录用 index.ts 统一导出(barrel) - 页面从 ../../src/hooks、../../src/theme 等导入,不直接引用子文件 - 导入用相对路径(../../src/...),不引入 path alias / babel 配置 导入示例: // ✅ 正确 import { useJobList } from '../../src/hooks'; import { colors } from '../../src/theme'; // ❌ 错误 import { useJobList } from '../../src/hooks/useJobList'; ======================================== 五、真实后端就绪后 ======================================== 仅替换 src/api/api.ts 中函数体为真实 fetch(签名不变),页面与 hooks 无需改动。 迁移步骤: 1. 确认后端接口地址和数据结构 2. 在 src/api/api.ts 中替换 fetch 函数体 3. 根据后端实际返回结构调整数据转换逻辑 4. 删除 mock 数据(或保留用于本地开发) 5. 测试验证各功能正常 ======================================== 六、Git 提交规范 ======================================== 采用 Conventional Commits 规范: (): type 类型: - feat: 新功能 - fix: 修复Bug - docs: 文档更新 - style: 代码格式(不影响功能) - refactor: 重构(既不是新功能也不是修复) - test: 测试相关 - chore: 构建/工具变动 scope 范围(可选): - jobs: 岗位相关 - user: 用户相关 - api: 接口相关 - hook: Hook相关 - ui: UI组件相关 示例: feat(jobs): 添加岗位列表排序功能 fix(hook): 修复useJobList刷新时数据未重置的问题 docs(readme): 更新环境配置说明 refactor(api): 重构等级映射逻辑,提取为独立函数 ======================================== 七、测试策略 ======================================== API层测试: - 测试 mock 数据返回结构是否符合 types 定义 - 测试等级映射逻辑是否正确(如 92分 → 'excellent') Hook层测试: - 测试排序、筛选等数据逻辑是否正确 - 测试 loading/error 状态切换是否正确 页面层测试: - 测试 UI 交互是否正常(点击、输入、切换) - 测试四种状态(加载中/错误/空状态/成功)渲染是否正确 ======================================== 八、Code Review 检查清单 ======================================== 提交PR前自查: 【架构规范】 - [ ] 页面组件是否直接调用了API?(应通过Hook调用) - [ ] 数据请求是否走通了 页面 → Hook → API → 后端 链路? 【业务逻辑】 - [ ] 是否有硬编码的业务规则判断?(应在API层处理) - [ ] 等级/状态等派生值是否通过联合类型建模? 【UI规范】 - [ ] 组件内是否有硬编码的颜色值?(应从 theme 导入) - [ ] 是否处理了四种UI状态?(加载中/错误/空状态/成功) 【命名规范】 - [ ] Hook 是否以 use 开头? - [ ] API 函数是否以 fetch 开头? - [ ] 组件文件名是否为 PascalCase? - [ ] 常量是否为 UPPER_SNAKE_CASE? 【代码质量】 - [ ] 是否添加了必要的 JSDoc 注释? - [ ] useEffect 中是否遗漏了清理函数? - [ ] 是否存在未使用的导入或变量? 【提交规范】 - [ ] Commit message 是否符合 Conventional Commits 格式? ======================================== 九、配色规范 ======================================== 全局配色集中在 src/theme/colors.ts,组件内不写死十六进制色值,统一 import { colors } from '../../src/theme'。 ### 🔵 主色家族(蓝) | 变量名 | 色值 | 说明 | |---|---|---| | **primary** | `#2C83FD` | 主色(蓝)· 品牌锚点 / Logo / ≥24pt 展示字 | | primaryLight | `#E8F2FF` | 主色浅色调 · 卡片 / 标签底色 / 空状态背景 | | primaryBorder | `#D1E5FF` | 主色描边 · 标签边框 / 输入框聚焦边 | | primaryBright | `#5399FD` | 主色亮色调 · 按钮渐变终点 / 装饰图形 | | primaryDark | `#175AB0` | 主色深色调 · 图标 / ≥18pt 大字 / 正向评级文字 | | **primaryText** ⭐ | `#104686` | **主色文字专用** · 浅底上写蓝字(白底 9.4:1 AAA) | | primaryBtn ⭐ | `#1F6FDB` | **主按钮底色** · 白字(对比 4.8:1 AA),hover 切 primaryDark | --- ### 🟠 行动色家族(珊瑚橙) | 变量名 | 色值 | 说明 | |---|---|---| | **action** | `#FF6F61` | 行动色(珊瑚橙)· 品牌锚点 / 装饰 / ≥24pt 大字 | | actionLight | `#FFEFED` | 行动色浅色调 · 提醒卡片底 / 标签底 | | actionBorder | `#FFD7D1` | 行动色描边 · 弱警示描边 / 标签边框 | | **actionBtn** ⭐ | `#F25A4C` | **行动按钮底色** · 白字(对比 5.4:1 AA,比 #FF6F61 稳) | | **actionText** ⭐ | `#D94A3C` | **行动色文字专用** · 浅底上写橙色字(白底 7.1:1 AA) | | actionDeep | `#B03A2E` | 行动色极深 · 按下态 / 极小文字底(10.1:1 AAA) | --- ### 🔴 警示色(红 · 严格独立于行动色) | 变量名 | 色值 | 说明 | |---|---|---| | **danger** | `#FF3B30` | 警示色 · 风险 / 警告 / 未读红点 / 错误标(白字 5.1:1 AA) | | dangerLight | `#FFF1F0` | 警示浅色调 · 错误提示卡片底 | | dangerBorder | `#FFD6D4` | 警示描边 · 错误输入框描边 | | dangerBtn | `#E5352B` | 警示按钮底 · 删除 / 危险操作(白字 5.8:1 AA) | | dangerText | `#C02A22` | 警示正文 · 错误信息文字(白底 8.3:1 AAA) | --- ### ✅ 成功色(暖绿 · 避免"主色=成功"混淆) | 变量名 | 色值 | 说明 | |---|---|---| | success | `#10B981` | 成功色 · 完成 / 通过 图标 / 标签(图形用) | | successLight | `#E8F8EF` | 成功浅色调 · 成功卡片底 / 已通过标签底 | | successBorder | `#C7EDD8` | 成功描边 · 已完成卡片边框 | | successBtn | `#059669` | 成功按钮底 · 白字(对比 5.6:1 AA) | | successText | `#047857` | 成功正文 · 已达成 / 正向收益文字(白底 7.8:1 AAA) | --- ### 🟣 信息色(紫 · 补冷色锚点,与蓝主色区分) | 变量名 | 色值 | 说明 | |---|---|---| | info | `#7C5CFC` | 信息色 · 通知 / 链接 / 金额数字 / 信息标签(图形用) | | infoLight | `#F1EEFF` | 信息浅色调 · 通知卡片底 / 系统消息底 | | infoBorder | `#E0DAFF` | 信息描边 · 信息类标签边框 | | infoBtn | `#6642E8` | 信息按钮底 · 白字(对比 6.0:1 AA) | | infoText | `#5B3ED8` | 信息正文 · 链接 / 金额数字(白底 7.4:1 AAA) | --- ### ⚪ 中性色家族(7 档 · 原来 3 档不够用) | 变量名 | 色值 | 说明 | |---|---|---| | **white** | `#FFFFFF` | 中性底色 · 卡片 / 弹窗底 | | **surface** | `#FFFFFF` | 卡片 / 列表项底(与 pageBg 拉开 1 档层次) | | **pageBg** | `#F6F8F9` | 页面背景浅灰 · 全局背景 | | borderLight | `#EFF2F6` | 浅描边 · 卡片分割 / 细分割线 / 列表项分隔 | | **border** | `#E8ECF4` | 中性描边 · 输入框边框 / 卡片边框 | | borderStrong | `#D4DAE3` | 强描边 · 输入框聚焦前 / 选中卡片描边 | | textDisabled | `#B0B8C1` | 禁用文字 · 禁用按钮 / 只读内容 | | textHint | `#8E99A4` | 占位符文字 · placeholder / 辅助提示 | | **textSecondary** ⭐ | `#6B7A7B` | **次要文字**(原 #7F8C8D 加深 · 白底 5.1:1 过 AA) | | **textPrimary** | `#2C3E50` | 正文深色 · 标题 / 正文主阅读文字(13.1:1 AAA) | | textInverse | `#FFFFFF` | 深色底上的反白文字 | | focusRing | `#1F6FDB` | 焦点环色 · 键盘聚焦指示器(WCAG 2.4.11) | --- ### 🌙 暗模式配色(2026 默认必做 · 翻色不是简单反白) | 变量名 | 亮模式 | 暗模式 | 说明 | |---|---|---|---| | pageBg | `#F6F8F9` | `#0E1116` | 近黑带蓝灰 · OLED 省电 + 不刺眼 | | surface | `#FFFFFF` | `#161B22` | 卡片底比背景亮 1 档 · 拉开层次 | | border | `#E8ECF4` | `#2A313C` | 暗描边灰蓝调 · 不发白 | | textPrimary | `#2C3E50` | `#E6EDF3` | 暗正文暖白 · 减少眩光 | | textSecondary | `#6B7A7B` | `#91A0B3` | 暗次文(与 surface 对比 6.2:1 AA) | | primary | `#2C83FD` | `#5399FD` | 暗模式主色提亮 1 档 · 黑底上不"消失" | | primaryBtn | `#1F6FDB` | `#1F6FDB` | 按钮底不随暗模式提亮 · 白字 4.8:1 AA | | action | `#FF6F61` | `#FF8273` | 暗行动色更亮 · 保证抓眼 | --- ### 使用说明 - **primary / action**:品牌锚点、图标、装饰、≥18pt 大字、选中态指示;不作为白字按钮底(对比度不足)。 - **primaryBtn / actionBtn**:按钮底色专用(白字过 WCAG AA),需要按钮时优先使用。 - **primaryText / actionText**:浅底上写彩色字专用(普通 primary/action 直接写字对比度不足)。 - **primaryDark**:正向评级(如「优秀」)文字。 - **primaryLight / primaryBorder**:卡片底色 / 标签边框。 使用指引: - 按钮主背景使用 primaryBtn,渐变按钮使用 primaryBtn → primaryBright - 正向评级(如「优秀」)文字使用 primaryDark - 次要操作按钮(如「取消」)使用 actionBtn - 卡片底色用 primaryLight,边框用 primaryBorder ======================================== 十、技术要求 ======================================== - 使用 Expo Router(app 目录结构) - 使用 TypeScript - 使用 @expo/vector-icons(Ionicons) - 使用 expo-linear-gradient(渐变) - API 层用注释标明:接口地址、请求方式、参数、返回值 ======================================== 十一、接口定义示例(在 api.ts 中) ======================================== // 接口1:获取用户信息 // GET /api/user/profile // 返回值:{ name: string, slogan: string } export const fetchUserInfo = async (): Promise => { // TODO: 替换为真实接口 return new Promise((resolve) => { setTimeout(() => { resolve({ name: '李明', slogan: '精准高效·全链条一站式求职服务' }); }, 500); }); }; // 接口2:获取匹配度 // GET /api/match/position // 参数:{ userId: string } // 返回值:{ position: string, matchRate: number } export const fetchMatchData = async (params: { userId: string }): Promise => { // TODO: 替换为真实接口 return new Promise((resolve) => { setTimeout(() => { resolve({ position: '高级前端工程师', matchRate: 95 }); }, 500); }); }; // 接口3:获取岗位列表(含排序/筛选/搜索/分页) // GET /api/jobs // 参数:{ sortBy: string, keyword: string, industry: string, city: string, page: number, pageSize: number } // 返回值:{ jobs: Job[], total: number } export const fetchJobList = async (params: { sortBy: 'comprehensive' | 'latest' | 'salary'; keyword?: string; industry?: string; city?: string; salary?: string; experience?: string; page?: number; pageSize?: number; }): Promise<{ jobs: Job[]; total: number }> => { // TODO: 替换为真实接口 return new Promise((resolve) => { setTimeout(() => { // 1. 从mock数据源复制 let result = [...mockJobData]; // 2. 关键词搜索 if (params.keyword && params.keyword.trim() !== '') { const keyword = params.keyword.trim().toLowerCase(); result = result.filter( (job) => job.positionName.toLowerCase().includes(keyword) || job.companyName.toLowerCase().includes(keyword) ); } // 3. 筛选:行业 if (params.industry && params.industry !== '全部行业') { result = result.filter((job) => job.industry === params.industry); } // 4. 筛选:城市 if (params.city && params.city !== '全部城市') { result = result.filter((job) => job.city.includes(params.city)); } // 5. 排序逻辑 switch (params.sortBy) { case 'salary': result.sort((a, b) => b.salaryMax - a.salaryMax); break; case 'latest': result.sort((a, b) => parseInt(b.id) - parseInt(a.id)); break; case 'comprehensive': default: result.sort((a, b) => b.score - a.score); break; } // 6. 计算总数 const total = result.length; // 7. 分页 const page = params.page || 1; const pageSize = params.pageSize || 10; const startIndex = (page - 1) * pageSize; const endIndex = Math.min(startIndex + pageSize, total); const paginatedJobs = result.slice(startIndex, endIndex); // 8. 等级映射(业务规则) const jobsWithLevel = paginatedJobs.map((job) => ({ ...job, level: job.score >= 85 ? 'excellent' : job.score >= 70 ? 'good' : 'medium', })); resolve({ jobs: jobsWithLevel, total }); }, 300); }); }; // 模拟数据 const mockJobData: Job[] = [ { id: '1', companyName: '字节跳动', companyLocation: '北京 · 海淀区', positionName: '高级前端工程师', salaryMin: 25, salaryMax: 40, score: 92, level: 'excellent', experience: '3-5年', education: '本科', benefits: ['五险一金', '年终奖金', '弹性工作'], industry: '互联网/IT', city: '北京', }, { id: '2', companyName: '腾讯科技', companyLocation: '深圳 · 南山区', positionName: '产品经理', salaryMin: 20, salaryMax: 35, score: 78, level: 'good', experience: '3-5年', education: '本科', benefits: ['五险一金', '年终奖金', '弹性工作'], industry: '互联网/IT', city: '深圳', }, { id: '3', companyName: '阿里巴巴', companyLocation: '杭州 · 余杭区', positionName: 'UI设计师', salaryMin: 15, salaryMax: 25, score: 85, level: 'excellent', experience: '3-5年', education: '本科', benefits: ['五险一金', '年终奖金', '弹性工作'], industry: '互联网/IT', city: '杭州', }, { id: '4', companyName: '美团', companyLocation: '北京 · 朝阳区', positionName: 'Java开发工程师', salaryMin: 18, salaryMax: 30, score: 65, level: 'medium', experience: '3-5年', education: '本科', benefits: ['五险一金', '年终奖金', '弹性工作'], industry: '互联网/IT', city: '北京', }, ];