轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

24 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://s.apifox.cn/a64fdc2b-3980-4dc1-9b02-b1097417e76e 测试号:手机号:19900009021 密码:ZhilaiTest!2026#A 做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 **学聪明点。**不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。 需要填充图片的地方,没有图片的话,请标明这里需要填充什么图片,便于后续填充。 整个项目所有卡片的圆角都必须是8px,无阴影,边距间隔一致,为8px。 引导页背景应当是蓝和绿的渐变色 仓库:https://gitea.baskbuddy.com/baskbuddy/mydreamjob 严格要求:请在每周的工作日的9:20的时候拉取一次远端仓库、每天17:50提交一次git仓库并推送到远端,每天一次,其他时间可以改动但不需要提交,说明今天新增与改动的内容,上班时间是9:00-18:00,有页面出口就一定要有入口。

提交之后必须推送(2026-09-18 补充)。原约定只写了「提交」,没写「推送」,实际后果是 2026-09-11 至 09-17 整整一周的 12 个提交从未离开本机,远端一直停在 09-10: ① 这段工作只存在于一台机器上,磁盘故障即丢失; ② 后端同事(Java + SpringBoot)看不到前端的接口契约; ③ 9:20 的「拉取」形同虚设 —— 本地领先 12、落后 0,拉不到任何东西。 故 17:50 的动作是两步:git commit → git push。次日 9:20 拉取前先看 git rev-list --left-right --count HEAD...@{u},正常应为 0 0。 业务逻辑分层管理,不是没有,而是被妥善组织: +------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ | 层级 | 职责 | 业务逻辑示例 | +------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ | 页面组件 | 渲染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 无需改动。

迁移步骤:

  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-09-18):本项目暂不接入暗模式,维持纯亮色。原因不是「先不做」,而是成本口径被低估了: 全仓 3302 处静态 colors.X 引用分布在 124 个文件,其中 119 个文件的样式是模块级 StyleSheet.create —— 模块级样式在 App 启动时就固化了颜色,与「运行时切换主题」根本不相容。 接入暗模式不是「加个开关」,而是要逐个样式表改造成随主题求值,属重构级工程;加上当前没有 自动化视觉回归手段,这 3302 处改动无法验证。故本轮不改代码,只记录决策。

下表已备好,src/theme/colors.ts 的 darkColors 也已存在(亮→暗映射),将来若要做, 应作为独立重构批次推进(含视觉回归方案),不要顺带塞进功能批次。

翻色不是简单反白 —— 下表各值的取色理由仍然成立,将来直接可用。

变量名 亮模式 暗模式 说明
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 模板时,至少进行一次浏览器预览,确认布局、资源路径和分页效果。