22 KiB
Expo HAS CHANGED Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code.
项目通用开发文档
======================================== 一、架构约定(数据层与 UI 层分离)
【核心原则】 你是react native+expo开发工程师 做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。 需要填充图片的地方,没有图片的话,请标明这里需要填充什么图片,便于后续填充。
仓库:https://gitea.baskbuddy.com/baskbuddy/mydreamjob 请在每周的周一-周五的9:20的时候拉取一次远端仓库、每天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页面 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、useGrowthData、useProfileData
API函数:
- 以 fetch 开头,采用 fetch + 名词
- 示例:fetchJobList、fetchUserProfile、fetchConversations
类型/接口:
- 使用名词,不添加 I 前缀
- 示例:interface Job,而非 interface IJob
- 示例:type SortType = 'comprehensive' | 'latest' | 'salary'
组件文件:
- 使用 PascalCase(大驼峰)
- 示例:JobListScreen.tsx、SearchBar.tsx
- 页面组件以 Screen 结尾:JobsScreen.tsx、ProfileScreen.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';
======================================== 五、真实后端就绪后(后端其他同事在做,用的Java+Springboot)
仅替换 src/api/api.ts 中函数体为真实 fetch(签名不变),页面与 hooks 无需改动。
迁移步骤:
- 确认后端接口地址和数据结构
- 在 src/api/api.ts 中替换 fetch 函数体
- 根据后端实际返回结构调整数据转换逻辑
- 删除 mock 数据(或保留用于本地开发)
- 测试验证各功能正常
======================================== 六、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/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: '北京', }, ]; 提交规范
提交信息遵循 https://www.conventionalcommits.org/en/v1.0.0/#specification
<type>: <中文简短标题>
常用类型:
- feat: 新增功能或资源;
- fix: 修复问题;
- docs: 修改 README、规范或其他文档;
- refactor: 重构代码或目录,不改变功能;
- chore: 构建、依赖或维护性修改;
- test: 新增或修改测试。
标题使用中文、简洁明确,并说明具体改动和原因。例如:
docs: 新增仓库协作与提交规范说明
feat: 新增可编辑同款简历模板
fix: 修复模板自动保存覆盖新内容
提交与 PR 前检查
git diff --check
git status --short
涉及技能修改时,运行对应的技能校验;涉及 HTML 模板时,至少进行一次浏览器预览,确认布局、资源路径和分页效果。